readme
This commit is contained in:
+683
@@ -0,0 +1,683 @@
|
||||
#include "Character.h"
|
||||
#include "Animator.h"
|
||||
#include "CharacterMind.h"
|
||||
#include "Ragdoll.h"
|
||||
#include "Physics.h"
|
||||
#include "tFunctions.h"
|
||||
#include "Engine.h"
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
// -------------------------------------------------------
|
||||
// CharacterBody
|
||||
// -------------------------------------------------------
|
||||
|
||||
void CharacterBody::Perform(const CharacterAction& action) {
|
||||
switch (action.type) {
|
||||
case CharacterActionType::Move: {
|
||||
sprinting = action.sprint;
|
||||
if (IsDashing())
|
||||
break; // the dash carries it, wherever it was going
|
||||
wishDirection = glm::vec3(action.direction.x, 0.0f, action.direction.z);
|
||||
if (glm::length(wishDirection) > 0.0001f) {
|
||||
wishDirection = glm::normalize(wishDirection);
|
||||
if (!combat.GetCurrentMove() && !combat.GetCurrentAbility() && !inAir) // mid swing it keeps facing where it swung, and in the air where it jumped
|
||||
turnDirection = wishDirection;
|
||||
}
|
||||
|
||||
// Told where to look, it keeps looking there whichever way it walks
|
||||
const glm::vec3 facing(action.facing.x, 0.0f, action.facing.z);
|
||||
strafing = glm::length(facing) > 0.0001f;
|
||||
if (strafing)
|
||||
turnDirection = glm::normalize(facing);
|
||||
break;
|
||||
}
|
||||
case CharacterActionType::Jump:
|
||||
if (!IsDashing())
|
||||
jumpWanted = true;
|
||||
break;
|
||||
case CharacterActionType::Dash: {
|
||||
// On the ground, between attacks and not too soon after the last one
|
||||
if (IsDashing() || dashCooldownLeft > 0.0f || inAir || windup >= 0.0f || !physics
|
||||
|| combat.GetCurrentMove() || combat.GetCurrentAbility())
|
||||
break;
|
||||
glm::vec3 direction(action.direction.x, 0.0f, action.direction.z);
|
||||
direction = glm::length(direction) > 0.0001f ? glm::normalize(direction) : GetForwardDirection();
|
||||
dashDirection = direction;
|
||||
dashWindup = dashDelay * GetGameTimeScale();
|
||||
dashLeft = dashTime * GetGameTimeScale();
|
||||
Face(direction, true);
|
||||
PlayAction(dashAnimation);
|
||||
break;
|
||||
}
|
||||
case CharacterActionType::Ability:
|
||||
if (IsDashing() || !combat.IsAbilityReady(action.slot))
|
||||
break;
|
||||
Face(action.direction, true);
|
||||
combat.UseAbility(action.slot, action.point);
|
||||
break;
|
||||
case CharacterActionType::Attack:
|
||||
if (IsDashing())
|
||||
break;
|
||||
Face(action.direction, true); // round to it before the swing starts, so the step and hitbox go that way
|
||||
combat.QueueAttack();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterBody::CancelDash() {
|
||||
if (!IsDashing())
|
||||
return;
|
||||
dashWindup = 0.0f;
|
||||
dashLeft = 0.0f;
|
||||
dashCooldownLeft = dashCooldown * GetGameTimeScale();
|
||||
StopDashAnimation();
|
||||
}
|
||||
|
||||
void CharacterBody::StopDashAnimation() {
|
||||
if (actionAnimator && dashAnimation && actionAnimator->m_CurrentAnimation == dashAnimation && actionAnimator->isPlaying)
|
||||
actionAnimator->StopAnimation();
|
||||
}
|
||||
|
||||
void CharacterBody::ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt) {
|
||||
dashCooldownLeft = std::max(dashCooldownLeft - dt, 0.0f);
|
||||
|
||||
// Dashing it goes where the dash sends it, fastest at the start and slowing to a stop, nothing else moves it
|
||||
if (IsDashing()) {
|
||||
wishDirection = glm::vec3(0.0f);
|
||||
jumpWanted = false;
|
||||
SetWalking(false);
|
||||
if (!physics || dt <= 0.0f)
|
||||
return;
|
||||
|
||||
// Winding up it stands where it is
|
||||
if (dashWindup > 0.0f) {
|
||||
physics->linearvelocity.x = 0.0f;
|
||||
physics->linearvelocity.z = 0.0f;
|
||||
dashWindup -= dt;
|
||||
if (dashWindup > 0.0f)
|
||||
return;
|
||||
dt = -dashWindup; // whatever's left of this frame goes to the dash itself
|
||||
dashWindup = 0.0f;
|
||||
}
|
||||
|
||||
const float length = dashTime * GetGameTimeScale();
|
||||
const float speed = 2.0f * dashDistance / length * (dashLeft / length); // falls off in a straight line, covering dashDistance
|
||||
physics->linearvelocity.x = dashDirection.x * speed;
|
||||
physics->linearvelocity.z = dashDirection.z * speed;
|
||||
physics->WakeUp();
|
||||
dashLeft -= dt;
|
||||
if (dashLeft <= 0.0f) {
|
||||
dashLeft = 0.0f;
|
||||
dashCooldownLeft = dashCooldown * GetGameTimeScale();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// An ability lunging it forward takes over, like a dash does. Once it's done, standing still brings it to a stop.
|
||||
float lungeSpeed = 0.0f;
|
||||
if (combat.GetAbilityLunge(lungeSpeed)) { // stunned, the ability is already cut off
|
||||
wishDirection = glm::vec3(0.0f);
|
||||
SetWalking(false);
|
||||
if (physics) {
|
||||
const glm::vec3 forward = GetForwardDirection();
|
||||
physics->linearvelocity.x = forward.x * lungeSpeed;
|
||||
physics->linearvelocity.z = forward.z * lungeSpeed;
|
||||
physics->WakeUp();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool moving = glm::length(wishDirection) > 0.0f && speedFactor > 0.0f;
|
||||
SetWalking(moving);
|
||||
// The rest of the dash animation is only there for standing still, walking or jumping out of it cuts it off
|
||||
if (moving || jumpWanted)
|
||||
StopDashAnimation();
|
||||
// Sprinting only when it's free to go full speed, not mid attack
|
||||
const float sprint = sprinting && speedFactor >= 1.0f ? sprintMultiplier : 1.0f;
|
||||
|
||||
// Straight along the input whichever way it's facing, turning to face it as it goes
|
||||
const glm::vec3 walkDirection = wishDirection;
|
||||
wishDirection = glm::vec3(0.0f); // used up, the next Move action has to ask again
|
||||
const bool jump = jumpWanted;
|
||||
jumpWanted = false;
|
||||
|
||||
// Just after taking off it's still close enough to the ground to count as on it, so it's in the air regardless
|
||||
takeoffTime = std::max(takeoffTime - dt, 0.0f);
|
||||
inAir = !grounded || takeoffTime > 0.0f;
|
||||
|
||||
// Starting a jump, if it's on the ground and not in the middle of an attack. The animation starts now,
|
||||
// and it leaves the ground jumpDelay into it.
|
||||
if (jump && canJump && !inAir && windup < 0.0f && physics && !combat.GetCurrentMove() && !combat.GetCurrentAbility()) {
|
||||
windup = jumpDelay * GetGameTimeScale();
|
||||
if (jumpAnimator && jumpAnimator->m_CurrentAnimation)
|
||||
jumpAnimator->PlayAnimation();
|
||||
}
|
||||
if (windup >= 0.0f) {
|
||||
windup -= dt;
|
||||
if (windup < 0.0f && !inAir && physics) {
|
||||
physics->linearvelocity.y = jumpSpeed;
|
||||
physics->WakeUp();
|
||||
inAir = true;
|
||||
takeoffTime = 0.2f;
|
||||
}
|
||||
}
|
||||
if (walkAnimator && moving)
|
||||
walkAnimator->playbackSpeed = speedFactor * statusSpeedMultiplier * (sprint > 1.0f ? sprintAnimationSpeed : 1.0f); // slower walking, slower steps
|
||||
|
||||
// With time stopped there's nothing to apply, and the rescaling below would divide zero by zero
|
||||
if (!physics || dt <= 0.0f)
|
||||
return;
|
||||
|
||||
const float movespeed = walkSpeed * speedFactor * sprint * statusSpeedMultiplier;
|
||||
const glm::vec3 inputVel = moving ? walkDirection * movespeed : glm::vec3(0.0f);
|
||||
|
||||
const float accel = 40.0f;
|
||||
const float damp = 0.75f;
|
||||
|
||||
if (grounded) {
|
||||
// Damping and acceleration worked out per reference step, then rescaled so frame rate doesn't change them
|
||||
const float dampStep = std::pow(damp, dt / referenceDt);
|
||||
const float accelScale = referenceDt * damp * (1.0f - dampStep) / (dt * dampStep * (1.0f - damp));
|
||||
physics->linearvelocity.x *= dampStep;
|
||||
physics->linearvelocity.z *= dampStep;
|
||||
physics->linearvelocity.x += inputVel.x * accel * dt * accelScale;
|
||||
physics->linearvelocity.z += inputVel.z * accel * dt * accelScale;
|
||||
}
|
||||
// In the air it just carries on the way it was going when it left the ground
|
||||
}
|
||||
|
||||
void CharacterBody::Step(float dt) {
|
||||
if (glm::length(turnDirection) > 0.0001f) {
|
||||
const glm::quat target = LookAt(-turnDirection);
|
||||
const glm::quat current = t.GetRotationQuaternion();
|
||||
const float blend = 1.0f - std::exp(-turnSpeed * dt);
|
||||
t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend)));
|
||||
}
|
||||
|
||||
const float target = walking ? 1.0f : 0.0f;
|
||||
walkBlend += (target - walkBlend) * (1.0f - std::exp(-walkFadeSpeed * dt));
|
||||
const float airTarget = (inAir || windup >= 0.0f) && jumpAnimator && jumpAnimator->isPlaying ? 1.0f : 0.0f;
|
||||
airBlend += (airTarget - airBlend) * (1.0f - std::exp(-airFadeSpeed * dt));
|
||||
|
||||
combat.Step(*this, dt);
|
||||
// Clips play in real time, the way they were animated, rather than on the sped up game clock
|
||||
const float animationDt = dt / GetGameTimeScale();
|
||||
for (Animator* a : animators)
|
||||
a->Step(animationDt);
|
||||
|
||||
// After stepping, since the animator ramps its own weight up to full when it plays
|
||||
if (walkAnimator)
|
||||
walkAnimator->blendWeight = walkBlend;
|
||||
if (jumpAnimator) {
|
||||
jumpAnimator->blendWeight = airBlend;
|
||||
if (!inAir && windup < 0.0f && airBlend < 0.01f)
|
||||
jumpAnimator->isPlaying = false; // landed and faded out, off until the next jump
|
||||
}
|
||||
|
||||
if (rig && animator->m_CurrentAnimation) {
|
||||
blendedBones = animator->GetFinalBoneMatrices();
|
||||
BlendBones(animator->m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
// Blends two parent-relative transforms, position and scale linearly and rotation along the shortest arc
|
||||
static glm::mat4 BlendLocalTransforms(const glm::mat4& from, const glm::mat4& to, float t) {
|
||||
const glm::vec3 fromScale(glm::length(glm::vec3(from[0])), glm::length(glm::vec3(from[1])), glm::length(glm::vec3(from[2])));
|
||||
const glm::vec3 toScale(glm::length(glm::vec3(to[0])), glm::length(glm::vec3(to[1])), glm::length(glm::vec3(to[2])));
|
||||
const glm::mat3 fromRotation(glm::vec3(from[0]) / fromScale.x, glm::vec3(from[1]) / fromScale.y, glm::vec3(from[2]) / fromScale.z);
|
||||
const glm::mat3 toRotation(glm::vec3(to[0]) / toScale.x, glm::vec3(to[1]) / toScale.y, glm::vec3(to[2]) / toScale.z);
|
||||
|
||||
const glm::quat rotation = glm::normalize(glm::slerp(glm::quat_cast(fromRotation), glm::quat_cast(toRotation), t));
|
||||
glm::mat4 result = glm::mat4_cast(rotation) * glm::scale(glm::mat4(1.0f), glm::mix(fromScale, toScale, t));
|
||||
result[3] = glm::vec4(glm::mix(glm::vec3(from[3]), glm::vec3(to[3]), t), 1.0f);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Every node's rest transform relative to the model, and its parent's name, from one clip's hierarchy
|
||||
static void CollectRestPose(const AssimpNodeData& node, const glm::mat4& parentTransform, const std::string& parentName,
|
||||
std::unordered_map<std::string, glm::mat4>& globals, std::unordered_map<std::string, std::string>& parents) {
|
||||
const glm::mat4 global = parentTransform * node.transformation;
|
||||
globals[node.name] = global;
|
||||
parents[node.name] = parentName;
|
||||
for (const AssimpNodeData& child : node.children)
|
||||
CollectRestPose(child, global, node.name, globals, parents);
|
||||
}
|
||||
|
||||
// A clip exported with only some bones, like an arm on its own, has them hung off whatever ancestor made it into
|
||||
// the file. Its tracks are then relative to that ancestor instead of the bone's real parent. Going through the rest
|
||||
// pose of both files turns one into the other, so the arm still rides on the torso.
|
||||
const std::unordered_map<std::string, glm::mat4>& CharacterBody::GetParentCorrections(const Animation* layer) {
|
||||
const Animation* base = animator->m_CurrentAnimation;
|
||||
const auto key = std::make_pair(base, layer);
|
||||
const auto cached = parentCorrections.find(key);
|
||||
if (cached != parentCorrections.end())
|
||||
return cached->second;
|
||||
|
||||
std::unordered_map<std::string, glm::mat4>& corrections = parentCorrections[key];
|
||||
if (!base || !layer || base == layer)
|
||||
return corrections;
|
||||
|
||||
std::unordered_map<std::string, glm::mat4> baseGlobals, layerGlobals;
|
||||
std::unordered_map<std::string, std::string> baseParents, layerParents;
|
||||
CollectRestPose(base->GetRootNode(), glm::mat4(1.0f), "", baseGlobals, baseParents);
|
||||
CollectRestPose(layer->GetRootNode(), glm::mat4(1.0f), "", layerGlobals, layerParents);
|
||||
|
||||
for (const std::string& bone : layer->m_BoneNames) {
|
||||
const auto baseParent = baseParents.find(bone);
|
||||
const auto layerParent = layerParents.find(bone);
|
||||
if (baseParent == baseParents.end() || layerParent == layerParents.end())
|
||||
continue;
|
||||
if (baseParent->second == layerParent->second)
|
||||
continue;
|
||||
|
||||
const auto baseParentGlobal = baseGlobals.find(baseParent->second);
|
||||
const auto layerParentGlobal = layerGlobals.find(layerParent->second);
|
||||
const glm::mat4 from = layerParentGlobal != layerGlobals.end() ? layerParentGlobal->second : glm::mat4(1.0f);
|
||||
const glm::mat4 to = baseParentGlobal != baseGlobals.end() ? baseParentGlobal->second : glm::mat4(1.0f);
|
||||
corrections[bone] = glm::inverse(to) * from;
|
||||
}
|
||||
return corrections;
|
||||
}
|
||||
|
||||
// Walks the base animation's hierarchy. Each bone starts from its rest pose, then every layer with a track for it
|
||||
// blends in on top in weight order, before it's passed down to its children.
|
||||
void CharacterBody::BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform) {
|
||||
glm::mat4 local = node.transformation;
|
||||
for (Animator* a : animators) {
|
||||
if (!a->m_CurrentAnimation || a->blendWeight <= 0.0f)
|
||||
continue;
|
||||
if (a != animator && !a->isPlaying)
|
||||
continue;
|
||||
const auto& transforms = a->GetLocalTransforms();
|
||||
const auto found = transforms.find(node.name);
|
||||
if (found == transforms.end())
|
||||
continue;
|
||||
|
||||
glm::mat4 layerLocal = found->second;
|
||||
if (a != animator) {
|
||||
const auto& corrections = GetParentCorrections(a->m_CurrentAnimation);
|
||||
const auto correction = corrections.find(node.name);
|
||||
if (correction != corrections.end())
|
||||
layerLocal = correction->second * layerLocal;
|
||||
}
|
||||
|
||||
local = a == animator || a->blendWeight >= 1.0f
|
||||
? layerLocal
|
||||
: BlendLocalTransforms(local, layerLocal, a->blendWeight);
|
||||
}
|
||||
|
||||
glm::mat4 global = parentTransform * local;
|
||||
if (node.name == "Head")
|
||||
global = glm::scale(global, glm::vec3(HeadScale));
|
||||
|
||||
const auto info = rig->m_BoneInfoMap.find(node.name);
|
||||
if (info != rig->m_BoneInfoMap.end()) {
|
||||
const int index = info->second.id;
|
||||
if (index >= 0 && index < (int)blendedBones.size())
|
||||
blendedBones[index] = global * info->second.offset;
|
||||
}
|
||||
|
||||
for (const AssimpNodeData& child : node.children)
|
||||
BlendBones(child, global);
|
||||
}
|
||||
|
||||
void CharacterBody::SetWalking(bool isWalking) {
|
||||
walking = isWalking;
|
||||
}
|
||||
|
||||
void CharacterBody::Face(const glm::vec3& direction, bool instantly) {
|
||||
const glm::vec3 flat(direction.x, 0.0f, direction.z);
|
||||
if (glm::length(flat) <= 0.0001f)
|
||||
return;
|
||||
turnDirection = glm::normalize(flat);
|
||||
if (instantly)
|
||||
t.RotateToQuaternion(LookAt(-turnDirection));
|
||||
}
|
||||
|
||||
glm::vec3 CharacterBody::GetForwardDirection() const {
|
||||
glm::vec3 forward = -t.GetFrontVector();
|
||||
forward.y = 0.0f;
|
||||
const float length = glm::length(forward);
|
||||
if (length <= 0.0001f)
|
||||
return glm::vec3(0.0f, 0.0f, -1.0f);
|
||||
return forward / length;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Character
|
||||
// -------------------------------------------------------
|
||||
|
||||
void Character::Think() {
|
||||
if (!mind || dead)
|
||||
return;
|
||||
pendingActions.clear();
|
||||
mind->ProduceActions(*this, pendingActions); // still asked while stunned, so it keeps track of things
|
||||
if (!CanAct())
|
||||
return;
|
||||
for (const CharacterAction& action : pendingActions)
|
||||
body->Perform(action);
|
||||
}
|
||||
|
||||
int Character::GetCollisionGroup() {
|
||||
// Kept clear of the ragdoll's groups, which count up from 1
|
||||
static int nextGroup = 1000;
|
||||
if (collisionGroup == 0)
|
||||
collisionGroup = nextGroup++;
|
||||
return collisionGroup;
|
||||
}
|
||||
|
||||
void Character::CreatePhysicsBody(Physics& physics, float massKg) {
|
||||
if (body->physics)
|
||||
return;
|
||||
p* physicsBody = new p(body);
|
||||
physicsBody->mass = massKg;
|
||||
physicsBody->lockRotation = true; // it turns by facing where it walks, never by tipping over
|
||||
physicsBody->friction = 0.0f; // walking does its own stopping
|
||||
physicsBody->collisionGroup = GetCollisionGroup();
|
||||
physics.AddObject(physicsBody);
|
||||
body->physics = physicsBody;
|
||||
world = &physics;
|
||||
}
|
||||
|
||||
void Character::EnableRagdoll(Physics& physics) {
|
||||
if (!ragdoll)
|
||||
ragdoll = new Ragdoll(*body, GetRigMatrix(), physics);
|
||||
}
|
||||
|
||||
void Character::Stun(float seconds) {
|
||||
if (dead || seconds <= 0.0f)
|
||||
return;
|
||||
const float length = seconds * GetGameTimeScale();
|
||||
if (length <= stunLeft)
|
||||
return;
|
||||
stunLeft = length;
|
||||
body->combat.Reset();
|
||||
body->CancelDash();
|
||||
}
|
||||
|
||||
void Character::AddStatus(StatusEffect::Type type, float seconds) {
|
||||
for (StatusEffect& status : statuses) {
|
||||
if (status.type != type)
|
||||
continue;
|
||||
if (status.timeLeft >= 0.0f && (seconds < 0.0f || seconds > status.timeLeft))
|
||||
status.timeLeft = seconds;
|
||||
return;
|
||||
}
|
||||
statuses.push_back({ type, seconds });
|
||||
}
|
||||
|
||||
void Character::RemoveStatus(StatusEffect::Type type) {
|
||||
statuses.erase(std::remove_if(statuses.begin(), statuses.end(),
|
||||
[type](const StatusEffect& status) { return status.type == type; }), statuses.end());
|
||||
}
|
||||
|
||||
void Character::Knockdown(float seconds) {
|
||||
if (dead || ragdoll || seconds <= 0.0f)
|
||||
return;
|
||||
AddStatus(StatusEffect::Type::Knockdown, seconds);
|
||||
body->combat.Reset();
|
||||
body->CancelDash();
|
||||
body->SetWalking(false);
|
||||
body->PlayAction(body->knockdownAnimation, 1.0f, true);
|
||||
wasKnockedDown = true;
|
||||
}
|
||||
|
||||
bool Character::CanAct() const {
|
||||
if (IsStunned())
|
||||
return false;
|
||||
for (const StatusEffect& status : statuses)
|
||||
if (GetStatusEffectInfo(status.type).preventsActing)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Character::DodgesHits() const {
|
||||
for (const StatusEffect& status : statuses)
|
||||
if (GetStatusEffectInfo(status.type).dodgesHits)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Character::HasStatus(StatusEffect::Type type) const {
|
||||
for (const StatusEffect& status : statuses)
|
||||
if (status.type == type)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Character::Step(float dt) {
|
||||
stunLeft = std::max(stunLeft - dt, 0.0f);
|
||||
const float realDt = dt / GetGameTimeScale();
|
||||
for (StatusEffect& status : statuses)
|
||||
if (status.timeLeft >= 0.0f)
|
||||
status.timeLeft = std::max(status.timeLeft - realDt, 0.0f);
|
||||
statuses.erase(std::remove_if(statuses.begin(), statuses.end(),
|
||||
[](const StatusEffect& status) { return status.timeLeft == 0.0f; }), statuses.end());
|
||||
|
||||
// Back up, out of the pose it was lying in
|
||||
const bool knockedDown = IsKnockedDown();
|
||||
if (wasKnockedDown && !knockedDown && body->actionAnimator && body->knockdownAnimation
|
||||
&& body->actionAnimator->m_CurrentAnimation == body->knockdownAnimation) {
|
||||
body->actionAnimator->holdsAtEnd = false;
|
||||
body->actionAnimator->StopAnimation();
|
||||
}
|
||||
wasKnockedDown = knockedDown;
|
||||
if (ragdoll) {
|
||||
ragdoll->Update(dt);
|
||||
return;
|
||||
}
|
||||
body->Step(dt);
|
||||
}
|
||||
|
||||
void Character::ApplyMovement(bool grounded, float dt, float referenceDt) {
|
||||
// Swinging and abilities slow it down
|
||||
const AttackMove* attack = body->combat.GetCurrentMove();
|
||||
const Ability* ability = body->combat.GetCurrentAbility();
|
||||
const float speed = IsStunned() ? 0.0f : ability ? ability->moveSpeedMultiplier : attack ? attack->moveSpeedMultiplier : 1.0f;
|
||||
|
||||
// Status effects all multiply together
|
||||
float statusSpeed = 1.0f, gravity = 1.0f;
|
||||
bool canJump = true;
|
||||
for (const StatusEffect& status : statuses) {
|
||||
const StatusEffectInfo& info = GetStatusEffectInfo(status.type);
|
||||
statusSpeed *= info.speedMultiplier;
|
||||
gravity *= info.gravityMultiplier;
|
||||
canJump = canJump && !info.preventsJumping;
|
||||
}
|
||||
body->statusSpeedMultiplier = statusSpeed;
|
||||
body->canJump = canJump;
|
||||
body->ApplyMovement(grounded, speed, dt, referenceDt);
|
||||
|
||||
// Physics already pulls it down once, this is the rest
|
||||
if (!grounded && gravity != 1.0f && world && body->physics)
|
||||
body->physics->linearvelocity += world->GetGravity() * (gravity - 1.0f) * dt;
|
||||
}
|
||||
|
||||
float Character::TakeDamage(float amount) {
|
||||
if (dead || amount <= 0.0f)
|
||||
return 0.0f;
|
||||
health = std::min(health, maxHealth);
|
||||
const float taken = std::min(amount, health);
|
||||
health -= taken;
|
||||
if (health <= 0.0f)
|
||||
Die();
|
||||
return taken;
|
||||
}
|
||||
|
||||
float Character::Heal(float amount) {
|
||||
if (dead || amount <= 0.0f)
|
||||
return 0.0f;
|
||||
const float before = health;
|
||||
health = std::min(health + amount, maxHealth);
|
||||
return std::max(health - before, 0.0f);
|
||||
}
|
||||
|
||||
void Character::Flinch() {
|
||||
if (dead || ragdoll || !body->flinchAnimation || IsKnockedDown()) // on the floor it stays lying there
|
||||
return;
|
||||
// It shares the action layer with attacks, so a punch can't carry on underneath it
|
||||
body->combat.Reset();
|
||||
body->CancelDash();
|
||||
body->PlayAction(body->flinchAnimation);
|
||||
}
|
||||
|
||||
void Character::Die() {
|
||||
if (dead)
|
||||
return;
|
||||
|
||||
// Some just pick themselves straight back up, like training dummies
|
||||
if (healsOnDeath) {
|
||||
health = maxHealth;
|
||||
return;
|
||||
}
|
||||
|
||||
// Stops where it is. Whatever it was doing is cut off, and it stops thinking until it's revived.
|
||||
dead = true;
|
||||
deaths++;
|
||||
health = 0.0f;
|
||||
body->SetWalking(false);
|
||||
body->combat.Reset();
|
||||
body->CancelDash();
|
||||
}
|
||||
|
||||
void Character::Revive() {
|
||||
if (!dead)
|
||||
return;
|
||||
delete ragdoll;
|
||||
ragdoll = nullptr;
|
||||
dead = false;
|
||||
health = maxHealth;
|
||||
stunLeft = 0.0f;
|
||||
// Timed status effects are gone, ones that last forever stay
|
||||
statuses.erase(std::remove_if(statuses.begin(), statuses.end(),
|
||||
[](const StatusEffect& status) { return status.timeLeft >= 0.0f; }), statuses.end());
|
||||
body->combat.Reset();
|
||||
|
||||
if (p* box = body->physics) {
|
||||
box->EnableCollision();
|
||||
box->EnableVelocity();
|
||||
box->linearvelocity = glm::vec3(0.0f);
|
||||
box->WakeUp();
|
||||
}
|
||||
}
|
||||
|
||||
bool Character::GetActionBoneTransform(const Animation* animation, const std::string& bone, glm::mat4& transform) const {
|
||||
const Animator* action = body ? body->actionAnimator : nullptr;
|
||||
if (ragdoll || !action || !action->isPlaying || !animation || action->m_CurrentAnimation != animation || !body->rig)
|
||||
return false;
|
||||
const auto info = body->rig->m_BoneInfoMap.find(bone);
|
||||
if (info == body->rig->m_BoneInfoMap.end())
|
||||
return false;
|
||||
const std::vector<glm::mat4>& bones = action->GetFinalBoneMatrices();
|
||||
const int index = info->second.id;
|
||||
if (index < 0 || index >= (int)bones.size())
|
||||
return false;
|
||||
|
||||
// The skinning matrix takes the bone's rest pose to where it is now, so undoing the rest pose leaves where it is
|
||||
transform = GetRigMatrix() * bones[index] * glm::inverse(info->second.offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Character::GetBoneTransform(const std::string& bone, glm::mat4& transform) const {
|
||||
if (ragdoll || !body || !body->rig)
|
||||
return false;
|
||||
const auto info = body->rig->m_BoneInfoMap.find(bone);
|
||||
if (info == body->rig->m_BoneInfoMap.end())
|
||||
return false;
|
||||
const std::vector<glm::mat4>& bones = body->GetBlendedBoneMatrices();
|
||||
const int index = info->second.id;
|
||||
if (index < 0 || index >= (int)bones.size())
|
||||
return false;
|
||||
transform = GetRigMatrix() * bones[index] * glm::inverse(info->second.offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
glm::mat4 Character::GetRigMatrix() const {
|
||||
// body->t's scale is the hitbox only; the rig is drawn at visualScale so it fills that hitbox
|
||||
return body->t.GetTranslationMatrix() * body->t.GetRotationMatrix()
|
||||
* glm::scale(glm::mat4(1.0f), glm::vec3(visualScale));
|
||||
}
|
||||
|
||||
bool Character::GetWorldBounds(BoundingBox& bounds) const {
|
||||
if (!body)
|
||||
return false;
|
||||
|
||||
if (ragdoll && !ragdoll->GetParts().empty()) {
|
||||
bounds.min = glm::vec3(FLT_MAX);
|
||||
bounds.max = glm::vec3(-FLT_MAX);
|
||||
for (const Ragdoll::Part* part : ragdoll->GetParts()) {
|
||||
const BoundingBox partBounds = part->transform.t.GetAABB();
|
||||
bounds.min = glm::min(bounds.min, partBounds.min);
|
||||
bounds.max = glm::max(bounds.max, partBounds.max);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The rig is about 2 units across natively in every direction it reaches, arms out included.
|
||||
// A cube that size around the body covers any pose and any turn.
|
||||
const glm::vec3 reach(visualScale);
|
||||
bounds.min = body->t.GetTranslation() - reach;
|
||||
bounds.max = body->t.GetTranslation() + reach;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Character::ApplyTemplate(const CharacterTemplate& from) {
|
||||
characterTemplate = &from;
|
||||
|
||||
if (from.rig)
|
||||
body->SetRig(from.rig);
|
||||
if (from.idleAnimation) {
|
||||
body->SetAnimation(from.idleAnimation);
|
||||
body->animator->loops = true;
|
||||
}
|
||||
if (from.walkAnimation)
|
||||
body->SetWalkAnimation(from.walkAnimation);
|
||||
body->jumpDelay = from.jumpDelay;
|
||||
if (from.jumpAnimation)
|
||||
body->SetJumpAnimation(from.jumpAnimation);
|
||||
else if (body->jumpAnimator)
|
||||
body->jumpAnimator->ClearAnimation(); // jumps without one
|
||||
body->flinchAnimation = from.flinchAnimation;
|
||||
body->knockdownAnimation = from.knockdownAnimation;
|
||||
|
||||
body->combat.Reset();
|
||||
body->combat.combo = from.combo;
|
||||
body->combat.comboName = from.comboName;
|
||||
body->combat.abilities = from.abilities;
|
||||
healsOnDeath = from.healsOnDeath;
|
||||
body->walkSpeed = from.walkSpeed;
|
||||
body->sprintMultiplier = from.sprintMultiplier;
|
||||
body->dashAnimation = from.dashAnimation;
|
||||
body->dashDistance = from.dashDistance;
|
||||
body->dashDelay = from.dashDelay;
|
||||
body->dashTime = from.dashTime;
|
||||
body->dashCooldown = from.dashCooldown;
|
||||
|
||||
maxHealth = from.maxHealth;
|
||||
if (!dead)
|
||||
health = maxHealth;
|
||||
}
|
||||
|
||||
Character::Character() {
|
||||
body = new CharacterBody();
|
||||
mind = nullptr;
|
||||
shadertype = ShaderType::RigShader;
|
||||
}
|
||||
|
||||
void Character::Render(Shader& shader) {
|
||||
if (!body || !body->rig || !body->animator) return;
|
||||
const std::vector<glm::mat4> transforms = ragdoll ? ragdoll->GetBoneMatrices()
|
||||
: body->animator->m_CurrentAnimation ? body->GetBlendedBoneMatrices()
|
||||
: std::vector<glm::mat4>(100, glm::mat4(1.0f));
|
||||
shader.SetMat4Array("finalBonesMatrices", transforms.data(), glm::min((int)transforms.size(), 100));
|
||||
shader.Set4F("colorOverride", colorOverride);
|
||||
surface.Apply(shader, 1, glm::vec3(visualScale));
|
||||
shader.Set1F("metallic", metallic);
|
||||
shader.Set1F("roughness", roughness);
|
||||
shader.Set1F("ao", ao);
|
||||
body->rig->Render(shader, ragdoll ? glm::mat4(1.0f) : GetRigMatrix()); // ragdoll bones are already in world space
|
||||
shader.Set4F("colorOverride", glm::vec4(0.0f)); // don't leak into other rig draws
|
||||
}
|
||||
Reference in New Issue
Block a user