Files
dt/Character.h
T
2026-11-03 20:05:02 -05:00

162 lines
5.8 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"
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 idle, faded in while walking
Animator* actionAnimator = nullptr; // one-shots on top of both, see PlayAction
float walkSpeed = 2.34f; // 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
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.
void PlayAction(Animation* anim) {
if (!anim)
return;
if (!actionAnimator) {
actionAnimator = AddAnimator(2);
actionAnimator->blendDuration = 0.1f;
}
actionAnimator->loops = false;
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(1);
walkAnimator->loops = true;
walkAnimator->SetAnimation(anim);
walkAnimator->PlayAnimation();
walkAnimator->blendWeight = walkBlend;
}
// 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.
void ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt);
// Turning and animation
void Step(float dt);
void SetWalking(bool walking);
glm::vec3 GetForwardDirection() const;
private:
glm::vec3 wishDirection = glm::vec3(0.0f); // from this frame's Move action
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
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;
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;
// Asks the mind for this frame's actions and hands them to the body
void Think();
glm::vec3 GetForwardDirection() const { return body->GetForwardDirection(); }
void EnableRagdoll(Physics& physics);
// 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) {
if (ragdoll) return;
body->Step(dt);
}
private:
glm::mat4 GetRigMatrix() const;
int collisionGroup = 0;
std::vector<CharacterAction> pendingActions;
};
#endif