#ifndef CHARACTER_CLASS #define CHARACTER_CLASS #include #include #include #include #include #include "t.h" #include "Rig.h" #include "Object.h" #include "Animator.h" #include "CharacterAction.h" #include "Combat.h" #include "CharacterTemplate.h" #include "StatusEffect.h" class Character; class CharacterManager; class CharacterMind; class Ragdoll; class Physics; class p; // The physical side of a character: where it is, its physics body, its animation, // and carrying out whatever actions its mind sends it. class CharacterBody : public t_package { public: Rig* rig = nullptr; p* physics = nullptr; // made by Character::CreatePhysicsBody, its box is t's scale std::vector animators; Animator* animator = nullptr; // the base layer, always fully on: standing idle Animator* walkAnimator = nullptr; // on top of the idles, faded in while walking Animator* jumpAnimator = nullptr; // on top of walking, played from the start each jump and held while in the air Animator* actionAnimator = nullptr; // one-shots on top of everything, see PlayAction CharacterCombat combat; // its attacks, played on the action layer Animation* flinchAnimation = nullptr; // played on the action layer when it's hit, see Character::Flinch Animation* knockdownAnimation = nullptr; // played on the action layer when it's knocked down, held at the end Animation* dashAnimation = nullptr; // played on the action layer while it dashes float walkSpeed = 1.17f; // m/s on flat ground float turnSpeed = 12.0f; // how quickly it turns to face where it's walking float walkFadeSpeed = 8.0f; // how quickly it fades between idle and walking float jumpSpeed = 10.0f; // m/s straight up when it jumps, about 5 m high float jumpDelay = 0.0f; // real seconds into the jump animation it actually leaves the ground float airFadeSpeed = 14.0f; // how quickly the jump fades in on takeoff and out on landing float sprintMultiplier = 1.4f; // times walkSpeed while sprinting float sprintAnimationSpeed = 1.25f; // how much faster the walk plays while sprinting float dashDistance = 4.0f; // m covered by a dash float dashDelay = 0.0f; // real seconds into the dash animation it starts moving float dashTime = 0.4f; // real seconds a dash carries it for, after which it's free to do anything again float dashCooldown = 0.5f; // real seconds after one dash ends before the next float statusSpeedMultiplier = 1.0f; // from its status effects, set by Character every frame bool canJump = true; // likewise CharacterBody() { animator = AddAnimator(0); animator->blendWeight = 1.0f; // base always fully visible } Animator* AddAnimator(int weight) { Animator* a = new Animator(); a->weight = weight; if (rig) a->SetRig(rig); animators.push_back(a); std::sort(animators.begin(), animators.end(), [](Animator* a, Animator* b) { return a->weight < b->weight; }); return a; } // The skinning matrices for this frame, from every animator layered together. Worked out in Step. // Base (weight 0) is the foundation; each higher-weight animator blends in by its blendWeight, // but only on the bones its animation has tracks for, so an arm-only clip leaves the legs walking. const std::vector& GetBlendedBoneMatrices() const { return blendedBones.empty() ? animator->GetFinalBoneMatrices() : blendedBones; } void SetRig(Rig* r) { rig = r; for (Animator* a : animators) a->SetRig(r); } void SetAnimation(Animation* anim) { animator->SetAnimation(anim); animator->PlayAnimation(); animator->blendWeight = 1.0f; // base never blends, always instant } // Plays an animation once on top of everything else. Starts over if it's already playing. // speed scales how fast it plays, 2 takes half as long. Holding, it stays on its last pose until stopped. void PlayAction(Animation* anim, float speed = 1.0f, bool hold = false) { if (!anim) return; if (!actionAnimator) { actionAnimator = AddAnimator(4); actionAnimator->blendDuration = 0.1f; } actionAnimator->loops = false; actionAnimator->holdsAtEnd = hold; actionAnimator->playbackSpeed = speed; actionAnimator->SetAnimation(anim); actionAnimator->PlayAnimation(); } // The loop played while walking. It keeps running underneath so it picks up mid-stride. void SetWalkAnimation(Animation* anim) { if (!walkAnimator) walkAnimator = AddAnimator(2); walkAnimator->loops = true; walkAnimator->SetAnimation(anim); walkAnimator->PlayAnimation(); walkAnimator->blendWeight = walkBlend; } // The clip for jumping. It plays once from takeoff and holds its last pose until it lands. void SetJumpAnimation(Animation* anim) { if (!jumpAnimator) jumpAnimator = AddAnimator(3); jumpAnimator->loops = false; jumpAnimator->holdsAtEnd = true; jumpAnimator->SetAnimation(anim); jumpAnimator->isPlaying = false; // until it jumps jumpAnimator->blendWeight = 0.0f; } bool IsInAir() const { return inAir; } bool IsDashing() const { return dashWindup > 0.0f || dashLeft > 0.0f; } bool IsSprinting() const { return sprinting; } // Stops a dash where it is, like when it's hit void CancelDash(); void StopDashAnimation(); // Carries out one action. Movement actions only count for the frame they arrive in. void Perform(const CharacterAction& action); // Pushes the physics body toward walking speed along wherever it faces, from this frame's Move action. // speedFactor scales it, 0 stops it. The walk animation plays faster or slower to match. void ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt); // What this frame's Move action asked for, until ApplyMovement uses it up bool IsTryingToMove() const { return glm::length(wishDirection) > 0.0f; } // Turning and animation void Step(float dt); void SetWalking(bool walking); glm::vec3 GetForwardDirection() const; // Turns to face along direction, flattened. Walking turns it too, this is for while it stands still. // Instantly snaps round rather than turning over a few frames, for things that have to go the right way at once. void Face(const glm::vec3& direction, bool instantly = false); private: glm::vec3 wishDirection = glm::vec3(0.0f); // from this frame's Move action bool strafing = false; // facing somewhere it was told to, walking any way glm::vec3 turnDirection = glm::vec3(0.0f); // where it's turning to face, kept after it stops bool walking = false; float walkBlend = 0.0f; // 0 = idle, 1 = walking bool jumpWanted = false; // from this frame's Jump action, until ApplyMovement uses it up bool inAir = false; float takeoffTime = 0.0f; // seconds it still counts as in the air after jumping, while the ground check catches up float windup = -1.0f; // game seconds until it leaves the ground, below 0 when it isn't about to jump float airBlend = 0.0f; // 0 = on the ground, 1 = the jump fully showing bool sprinting = false; // from this frame's Move action float dashWindup = 0.0f; // game seconds until the dash starts moving it float dashLeft = 0.0f; // game seconds of dash left, 0 when it isn't dashing float dashCooldownLeft = 0.0f; // game seconds until it can dash again glm::vec3 dashDirection = glm::vec3(0.0f); std::vector blendedBones; // Per (base clip, layer clip): bone name -> what turns the layer's track into one relative to the bone's // real parent. Only bones whose parent differs between the two files have an entry. std::map, std::unordered_map> parentCorrections; void BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform); const std::unordered_map& GetParentCorrections(const Animation* layer); }; class Character : public Object, public Renderable { public: CharacterMind* mind = nullptr; // what decides its actions, nullptr does nothing CharacterBody* body = nullptr; const CharacterTemplate* characterTemplate = nullptr; // the kind of fighter it is, see ApplyTemplate std::string displayName; // shown over its head and in menus, its name when empty Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators SurfaceTexture surface; glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength (0 = keep the rig's own texture) float visualScale = 1.4f; // rig is ~1.8 tall natively; its physics box is 2.5 float metallic = 0.0f; float roughness = 0.7f; float ao = 1.0f; float maxHealth = 100.0f; bool healsOnDeath = false; // dying puts it straight back to full health instead float GetHealth() const { return health; } int GetDeaths() const { return deaths; } // how many times it's gone down, so things on it can tell it respawned bool IsDead() const { return dead; } // Takes away health, dying at 0. Returns how much it actually lost. float TakeDamage(float amount); // Gives back health up to maxHealth, nothing for the dead. Returns how much it actually gained. float Heal(float amount); // Can't do anything for seconds of real time: no moving, turning, attacking or jumping, and whatever move it // was in is cut off. A longer stun replaces a shorter one, a shorter one does nothing. void Stun(float seconds); bool IsStunned() const { return stunLeft > 0.0f; } // Puts a status effect on it for seconds of real time, below 0 forever. Already on, the longer of the two stays. void AddStatus(StatusEffect::Type type, float seconds = -1.0f); void RemoveStatus(StatusEffect::Type type); bool HasStatus(StatusEffect::Type type) const; // Knocked to the floor for seconds of real time, unable to do anything. Only hits that hit the knocked down land. void Knockdown(float seconds); bool IsKnockedDown() const { return HasStatus(StatusEffect::Type::Knockdown); } bool CanAct() const; // not stunned or held by a status effect bool DodgesHits() const; // a status effect keeps ordinary hits off it const std::vector& GetStatuses() const { return statuses; } // Recoils from a hit: whatever swing it was in is cut off and its flinch plays over everything, if it has one. // Nothing for the dead. void Flinch(); // Goes down, stopping where it is, until it's revived, which the game does straight away somewhere else. // One that heals on death just goes back to full health where it is. void Die(); // Back on its feet at full health, wherever its body box is void Revive(); // Asks the mind for this frame's actions and hands them to the body. The dead don't think. void Think(); // Moves the body from this frame's actions, slower while it's attacking void ApplyMovement(bool grounded, float dt, float referenceDt); glm::vec3 GetForwardDirection() const { return body->GetForwardDirection(); } const std::string& GetDisplayName() const { return displayName.empty() ? name : displayName; } // Where a bone of the clip playing on the action layer is in the world this frame, rotation and all. // Works for bones only that clip has, like a weapon bone. False unless animation is what's playing there. bool GetActionBoneTransform(const Animation* animation, const std::string& bone, glm::mat4& transform) const; // Where a bone is in the world this frame with every layer blended in, rotation and all. // False for a bone the rig doesn't have, or while it's a ragdoll. bool GetBoneTransform(const std::string& bone, glm::mat4& transform) const; void EnableRagdoll(Physics& physics); // Becomes that kind of fighter: its rig, animations, combo and stats, at full health unless it's dead. // Its looks, name and physics body are left alone. void ApplyTemplate(const CharacterTemplate& characterTemplate); // The box physics moves the character around with, sized by body->t's scale void CreatePhysicsBody(Physics& physics, float massKg); // Its physics body's group, so it never collides with anything else of its own int GetCollisionGroup(); Character(); void Render(Shader& shader) override; bool GetWorldBounds(BoundingBox& bounds) const override; void Step(float dt); private: glm::mat4 GetRigMatrix() const; float health = 100.0f; int deaths = 0; float stunLeft = 0.0f; // game seconds std::vector statuses; bool wasKnockedDown = false; // last frame, so getting up stops the held pose bool dead = false; Physics* world = nullptr; // the physics its body is in, for EnableRagdoll int collisionGroup = 0; std::vector pendingActions; }; #endif