286 lines
14 KiB
C++
286 lines
14 KiB
C++
#ifndef CHARACTER_CLASS
|
|
#define CHARACTER_CLASS
|
|
#include <vector>
|
|
#include <string>
|
|
#include <map>
|
|
#include <unordered_map>
|
|
#include <algorithm>
|
|
#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<Animator*> 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<glm::mat4>& 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<glm::mat4> 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::pair<const Animation*, const Animation*>, std::unordered_map<std::string, glm::mat4>> parentCorrections;
|
|
|
|
void BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform);
|
|
const std::unordered_map<std::string, glm::mat4>& 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
|
|
bool IsInvulnerable() const; // has iframes: a status effect keeps every hit off it
|
|
bool HasHyperArmor() const; // in a move that takes hits without flinching, being stunned or pushed about
|
|
const std::vector<StatusEffect>& 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<StatusEffect> 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<CharacterAction> pendingActions;
|
|
};
|
|
#endif
|