feat(角色状态机): 重构角色状态系统为基于类的设计

新增通用状态类和职业专属状态类,将状态逻辑从状态机中解耦
添加状态注册机制,支持按职业配置状态
实现基础状态如待机、移动、跳跃、受击等
为剑士职业实现专属攻击状态
补充状态机开发说明文档
This commit is contained in:
2026-04-03 17:11:22 +08:00
parent de522a1e64
commit f64180ebed
25 changed files with 1658 additions and 235 deletions

View File

@@ -44,11 +44,15 @@ public:
const CharacterWorldPosition& GetWorldPosition() const { return motor_.position; }
const CharacterMotor& GetMotor() const { return motor_; }
CharacterMotor& GetMotorMutable() { return motor_; }
CharacterCommandBuffer& GetCommandBufferMutable() { return commandBuffer_; }
float GetLastDeltaTime() const { return lastDeltaTime_; }
const CharacterActionDefinition* FindAction(const std::string& actionId) const;
std::string ResolveAnimationTag(const std::string& preferred,
const std::string& fallback) const;
void PlayAnimationTag(const std::string& actionTag);
void RunStateHook(const std::string& phase,
CharacterStateId stateId,
const CharacterActionDefinition* action);
void SetFacing(int direction);
const character::CharacterConfig* GetConfig() const {

View File

@@ -1,20 +1,28 @@
#pragma once
#include "character/CharacterActionLibrary.h"
#include "character/CharacterDataLoader.h"
#include "character/states/CharacterStateBase.h"
#include <memory>
#include <unordered_map>
namespace frostbite2D {
class CharacterObject;
/// 角色主状态机。输入先变成意图,再由状态机决定当前大状态和动作推进。
class CharacterStateMachine {
public:
using StateRegistry = std::unordered_map<CharacterStateId, std::unique_ptr<ICharacterStateNode>>;
CharacterStateMachine() = default;
void Configure(const character::CharacterConfig& config);
void Reset();
void Update(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent,
float deltaTime);
void ForceHurt(const CharacterActionDefinition* hurtAction);
void ForceHurt(CharacterObject& owner, const CharacterActionDefinition* hurtAction);
CharacterStateId GetState() const { return currentState_; }
const std::string& GetCurrentActionId() const { return currentActionId_; }
@@ -27,24 +35,21 @@ public:
bool IsMovementLocked() const;
bool CanTurn() const;
void RegisterState(std::unique_ptr<ICharacterStateNode> stateNode);
void ClearRegisteredStates();
ICharacterStateNode* FindStateNode(CharacterStateId stateId) const;
private:
void ChangeState(CharacterObject& owner, CharacterStateId nextState);
void EnterAction(CharacterObject& owner, const CharacterActionDefinition* action);
void UpdateGroundState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent);
void UpdateAirState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent);
void UpdateActionState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent,
float deltaTime);
void UpdateHurtState(CharacterObject& owner, float deltaTime);
void InvokeStateHook(CharacterObject& owner,
const CharacterActionDefinition* action,
const char* phase) const;
bool TryStartAction(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
CharacterCommandType commandType,
const std::string& actionId);
bool TryStartRegisteredAction(CharacterStateContext& context);
void ApplyFrameEvents(CharacterObject& owner, int previousFrame, int currentFrame);
CharacterStateId currentState_ = CharacterStateId::Idle;
@@ -54,6 +59,9 @@ private:
float actionFrameProgress_ = 0.0f;
int actionFrame_ = 0;
float landingTimer_ = 0.0f;
StateRegistry stateRegistry_;
friend class CharacterStateBase;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,94 @@
#pragma once
#include "character/CharacterActionTypes.h"
#include <string>
namespace frostbite2D {
class CharacterObject;
class CharacterStateMachine;
struct CharacterActionDefinition;
struct CharacterStateContext {
CharacterObject& owner;
CharacterCommandBuffer& commandBuffer;
const CharacterIntent& intent;
float deltaTime = 0.0f;
};
class ICharacterStateNode {
public:
virtual ~ICharacterStateNode() = default;
virtual CharacterStateId GetId() const = 0;
virtual void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)machine;
(void)context;
(void)previousState;
}
virtual void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) = 0;
virtual void OnExit(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) {
(void)machine;
(void)context;
(void)nextState;
}
};
class ICharacterActionStateNode {
public:
virtual ~ICharacterActionStateNode() = default;
virtual bool TryEnter(CharacterStateMachine& machine,
CharacterStateContext& context) = 0;
};
class CharacterStateBase : public ICharacterStateNode {
public:
explicit CharacterStateBase(CharacterStateId stateId);
~CharacterStateBase() override = default;
CharacterStateId GetId() const override;
protected:
void ChangeState(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) const;
void PlayActionById(CharacterObject& owner,
const std::string& actionId,
const std::string& fallbackTag = "rest") const;
void InvokeHook(CharacterStateMachine& machine,
CharacterObject& owner,
const CharacterActionDefinition* action,
const char* phase) const;
bool TryStartAction(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterCommandType commandType,
const std::string& actionId) const;
bool TryStartRegisteredAction(CharacterStateMachine& machine,
CharacterStateContext& context) const;
void ApplyFrameEvents(CharacterStateMachine& machine,
CharacterObject& owner,
int previousFrame,
int currentFrame) const;
const CharacterActionDefinition* GetCurrentAction(const CharacterStateMachine& machine) const;
const std::string& GetCurrentActionId(const CharacterStateMachine& machine) const;
void SetCurrentAction(CharacterStateMachine& machine,
const CharacterActionDefinition* action,
const std::string& actionId) const;
void ClearCurrentAction(CharacterStateMachine& machine) const;
float& StateTime(CharacterStateMachine& machine) const;
float& ActionFrameProgress(CharacterStateMachine& machine) const;
int& ActionFrame(CharacterStateMachine& machine) const;
float& LandingTimer(CharacterStateMachine& machine) const;
private:
CharacterStateId stateId_ = CharacterStateId::Idle;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,13 @@
#pragma once
#include "character/CharacterDataLoader.h"
namespace frostbite2D {
class CharacterStateMachine;
void RegisterCommonCharacterStates(CharacterStateMachine& machine);
void RegisterJobCharacterStates(CharacterStateMachine& machine,
const character::CharacterConfig& config);
} // namespace frostbite2D

View File

@@ -0,0 +1,15 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class DeadState : public CharacterStateBase {
public:
DeadState();
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,18 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class FallState : public CharacterStateBase {
public:
FallState();
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,21 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class HurtState : public CharacterStateBase {
public:
HurtState();
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
void OnExit(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,18 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class IdleState : public CharacterStateBase {
public:
IdleState();
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,18 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class JumpState : public CharacterStateBase {
public:
JumpState();
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,18 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class LandingState : public CharacterStateBase {
public:
LandingState();
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,18 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class MoveState : public CharacterStateBase {
public:
MoveState();
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,23 @@
#pragma once
#include "character/states/CharacterStateBase.h"
namespace frostbite2D {
class SwordmanAttackState : public CharacterStateBase, public ICharacterActionStateNode {
public:
SwordmanAttackState();
bool TryEnter(CharacterStateMachine& machine,
CharacterStateContext& context) override;
void OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) override;
void OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) override;
void OnExit(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) override;
};
} // namespace frostbite2D

View File

@@ -28,6 +28,7 @@ bool CharacterObject::Construction(int jobId) {
growType_ = -1;
direction_ = 1;
config_ = *config;
stateMachine_.Configure(*config_);
equipmentManager_.Init(config_->baseJobConfig);
if (auto actionLibrary = loadCharacterActionLibrary(*config_)) {
actionLibrary_ = *actionLibrary;
@@ -104,9 +105,7 @@ void CharacterObject::ApplyHit(const HitContext& hit) {
if (hit.launchZVelocity > 0.0f) {
motor_.grounded = false;
}
stateMachine_.ForceHurt(hurtAction);
PlayAnimationTag(hurtAction ? hurtAction->animationTag
: ResolveAnimationTag("hurt_light", "rest"));
stateMachine_.ForceHurt(*this, hurtAction);
}
void CharacterObject::OnUpdate(float deltaTime) {
@@ -172,6 +171,15 @@ void CharacterObject::PlayAnimationTag(const std::string& actionTag) {
SetAction(actionTag);
}
void CharacterObject::RunStateHook(const std::string& phase,
CharacterStateId stateId,
const CharacterActionDefinition* action) {
(void)phase;
(void)stateId;
(void)action;
// 预留给后续脚本系统;当前默认由 C++ 状态节点接管生命周期逻辑。
}
void CharacterObject::SetFacing(int direction) {
motor_.facing = direction >= 0 ? 1 : -1;
SetDirection(motor_.facing);

View File

@@ -1,33 +1,16 @@
#include "character/CharacterStateMachine.h"
#include "character/CharacterObject.h"
#include <cmath>
#include "character/states/CharacterStateRegistry.h"
namespace frostbite2D {
namespace {
constexpr float kLogicFramesPerSecond = 60.0f;
constexpr float kLandingDuration = 0.08f;
constexpr float kMovementActionScale = 0.35f;
bool hasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
void CharacterStateMachine::Configure(const character::CharacterConfig& config) {
ClearRegisteredStates();
RegisterCommonCharacterStates(*this);
RegisterJobCharacterStates(*this, config);
Reset();
}
void playActionById(CharacterObject& owner,
const std::string& actionId,
const std::string& fallbackTag = "rest") {
if (const auto* action = owner.FindAction(actionId)) {
if (!action->animationTag.empty()) {
owner.PlayAnimationTag(action->animationTag);
return;
}
}
owner.PlayAnimationTag(owner.ResolveAnimationTag(actionId, fallbackTag));
}
} // namespace
void CharacterStateMachine::Reset() {
currentState_ = CharacterStateId::Idle;
currentActionId_.clear();
@@ -44,35 +27,30 @@ void CharacterStateMachine::Update(CharacterObject& owner,
float deltaTime) {
stateTime_ += deltaTime;
switch (currentState_) {
case CharacterStateId::Idle:
case CharacterStateId::Move:
case CharacterStateId::Landing:
UpdateGroundState(owner, commandBuffer, intent);
break;
case CharacterStateId::Jump:
case CharacterStateId::Fall:
UpdateAirState(owner, commandBuffer, intent);
break;
case CharacterStateId::Action:
UpdateActionState(owner, commandBuffer, intent, deltaTime);
break;
case CharacterStateId::Hurt:
UpdateHurtState(owner, deltaTime);
break;
case CharacterStateId::Dead:
owner.GetMotorMutable().StopGroundMovement();
break;
CharacterStateContext context{owner, commandBuffer, intent, deltaTime};
if (auto* stateNode = FindStateNode(currentState_)) {
stateNode->OnUpdate(*this, context);
}
}
void CharacterStateMachine::ForceHurt(const CharacterActionDefinition* hurtAction) {
currentState_ = CharacterStateId::Hurt;
void CharacterStateMachine::ForceHurt(CharacterObject& owner,
const CharacterActionDefinition* hurtAction) {
const CharacterActionDefinition* previousAction = currentAction_;
currentAction_ = hurtAction;
currentActionId_ = hurtAction ? hurtAction->actionId : std::string("hurt_light");
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
if (currentState_ == CharacterStateId::Hurt) {
InvokeStateHook(owner, previousAction, "on_exit");
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
owner.GetLastDeltaTime()};
if (auto* hurtNode = FindStateNode(CharacterStateId::Hurt)) {
hurtNode->OnEnter(*this, context, CharacterStateId::Hurt);
}
return;
}
ChangeState(owner, CharacterStateId::Hurt);
}
bool CharacterStateMachine::CanAcceptGroundActions() const {
@@ -98,12 +76,39 @@ bool CharacterStateMachine::CanTurn() const {
&& !currentAction_->canTurn);
}
void CharacterStateMachine::RegisterState(std::unique_ptr<ICharacterStateNode> stateNode) {
if (!stateNode) {
return;
}
stateRegistry_[stateNode->GetId()] = std::move(stateNode);
}
void CharacterStateMachine::ClearRegisteredStates() {
stateRegistry_.clear();
}
ICharacterStateNode* CharacterStateMachine::FindStateNode(CharacterStateId stateId) const {
auto it = stateRegistry_.find(stateId);
if (it == stateRegistry_.end()) {
return nullptr;
}
return it->second.get();
}
void CharacterStateMachine::ChangeState(CharacterObject& owner,
CharacterStateId nextState) {
if (currentState_ == nextState) {
return;
}
CharacterStateId previousState = currentState_;
CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
owner.GetLastDeltaTime()};
if (auto* previousNode = FindStateNode(previousState)) {
previousNode->OnExit(*this, context, nextState);
}
currentState_ = nextState;
stateTime_ = 0.0f;
if (nextState != CharacterStateId::Action && nextState != CharacterStateId::Hurt) {
@@ -111,189 +116,38 @@ void CharacterStateMachine::ChangeState(CharacterObject& owner,
currentActionId_.clear();
}
switch (nextState) {
case CharacterStateId::Idle:
playActionById(owner, "idle");
break;
case CharacterStateId::Move:
playActionById(owner, "move");
break;
case CharacterStateId::Jump:
playActionById(owner, "jump");
break;
case CharacterStateId::Fall:
playActionById(owner, "fall");
break;
case CharacterStateId::Landing:
landingTimer_ = 0.0f;
playActionById(owner, "landing");
break;
case CharacterStateId::Action:
case CharacterStateId::Hurt:
case CharacterStateId::Dead:
break;
if (auto* nextNode = FindStateNode(nextState)) {
nextNode->OnEnter(*this, context, previousState);
}
}
void CharacterStateMachine::EnterAction(CharacterObject& owner,
const CharacterActionDefinition* action) {
// 普攻和技能都走通用 Action 状态,差异由动作定义驱动。
const CharacterActionDefinition* previousAction = currentAction_;
currentAction_ = action;
currentActionId_ = action ? action->actionId : std::string();
currentState_ = CharacterStateId::Action;
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
if (action) {
owner.PlayAnimationTag(action->animationTag);
if (currentState_ == CharacterStateId::Action) {
InvokeStateHook(owner, previousAction, "on_exit");
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
owner.GetLastDeltaTime()};
if (auto* actionNode = FindStateNode(CharacterStateId::Action)) {
actionNode->OnEnter(*this, context, CharacterStateId::Action);
}
return;
}
ChangeState(owner, CharacterStateId::Action);
}
void CharacterStateMachine::UpdateGroundState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent) {
auto& motor = owner.GetMotorMutable();
if (!motor.grounded) {
ChangeState(owner, CharacterStateId::Fall);
void CharacterStateMachine::InvokeStateHook(CharacterObject& owner,
const CharacterActionDefinition* action,
const char* phase) const {
if (!action || !phase || phase[0] == '\0') {
return;
}
if (currentState_ == CharacterStateId::Landing) {
landingTimer_ += owner.GetLastDeltaTime();
motor.StopGroundMovement();
if (landingTimer_ < kLandingDuration) {
return;
}
}
if (TryStartAction(owner, commandBuffer, CharacterCommandType::Skill1Pressed,
"skill_1")) {
return;
}
if (TryStartAction(owner, commandBuffer, CharacterCommandType::AttackPressed,
"attack_1")) {
return;
}
if (commandBuffer.HasBuffered(CharacterCommandType::JumpPressed)) {
commandBuffer.Consume(CharacterCommandType::JumpPressed);
motor.Jump();
ChangeState(owner, CharacterStateId::Jump);
return;
}
motor.ApplyGroundInput(intent.moveX, intent.moveY);
owner.SetFacing(motor.facing);
if (hasMoveIntent(intent)) {
ChangeState(owner, CharacterStateId::Move);
} else {
motor.StopGroundMovement();
ChangeState(owner, CharacterStateId::Idle);
}
}
void CharacterStateMachine::UpdateAirState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent) {
auto& motor = owner.GetMotorMutable();
motor.ApplyGroundInput(intent.moveX, intent.moveY, 0.75f);
owner.SetFacing(motor.facing);
if (motor.grounded) {
ChangeState(owner, CharacterStateId::Landing);
return;
}
ChangeState(owner, motor.verticalVelocity > 0.0f ? CharacterStateId::Jump
: CharacterStateId::Fall);
if (commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
commandBuffer.Consume(CharacterCommandType::AttackPressed);
}
if (commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
}
}
void CharacterStateMachine::UpdateActionState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent,
float deltaTime) {
auto& motor = owner.GetMotorMutable();
if (!currentAction_) {
ChangeState(owner, CharacterStateId::Idle);
return;
}
if (currentAction_->canMove) {
float moveScale = currentAction_->moveSpeedScale > 0.0f
? currentAction_->moveSpeedScale
: kMovementActionScale;
motor.ApplyGroundInput(intent.moveX, intent.moveY, moveScale);
} else {
motor.StopGroundMovement();
}
if (currentAction_->canTurn) {
owner.SetFacing(motor.facing);
}
actionFrameProgress_ += deltaTime * kLogicFramesPerSecond;
int previousFrame = actionFrame_;
actionFrame_ = std::max(0, static_cast<int>(actionFrameProgress_));
ApplyFrameEvents(owner, previousFrame + 1, actionFrame_);
for (const auto& rule : currentAction_->cancelRules) {
// 取消窗口由动作定义提供,状态机只负责在允许的帧段里消费缓存输入。
if (actionFrame_ < rule.beginFrame || actionFrame_ > rule.endFrame) {
continue;
}
if (rule.requireGrounded && !motor.grounded) {
continue;
}
if (rule.requireAirborne && motor.grounded) {
continue;
}
if (rule.targetAction == "attack_2"
&& commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
if (TryStartAction(owner, commandBuffer, CharacterCommandType::AttackPressed,
rule.targetAction)) {
return;
}
}
}
if (currentAction_->totalFrames > 0 && actionFrame_ >= currentAction_->totalFrames) {
currentAction_ = nullptr;
currentActionId_.clear();
if (!motor.grounded) {
ChangeState(owner, CharacterStateId::Fall);
} else if (hasMoveIntent(intent)) {
ChangeState(owner, CharacterStateId::Move);
} else {
ChangeState(owner, CharacterStateId::Idle);
}
}
}
void CharacterStateMachine::UpdateHurtState(CharacterObject& owner, float deltaTime) {
(void)deltaTime;
auto& motor = owner.GetMotorMutable();
motor.StopGroundMovement();
if (!currentAction_) {
currentAction_ = owner.FindAction("hurt_light");
}
actionFrameProgress_ += owner.GetLastDeltaTime() * kLogicFramesPerSecond;
actionFrame_ = std::max(0, static_cast<int>(actionFrameProgress_));
if (currentAction_ && actionFrame_ >= currentAction_->totalFrames) {
currentAction_ = nullptr;
currentActionId_.clear();
if (motor.grounded) {
ChangeState(owner, CharacterStateId::Idle);
} else {
ChangeState(owner, CharacterStateId::Fall);
}
}
owner.RunStateHook(phase, currentState_, action);
}
bool CharacterStateMachine::TryStartAction(CharacterObject& owner,
@@ -314,6 +168,15 @@ bool CharacterStateMachine::TryStartAction(CharacterObject& owner,
return true;
}
bool CharacterStateMachine::TryStartRegisteredAction(CharacterStateContext& context) {
auto* stateNode = FindStateNode(CharacterStateId::Action);
auto* actionNode = dynamic_cast<ICharacterActionStateNode*>(stateNode);
if (!actionNode) {
return false;
}
return actionNode->TryEnter(*this, context);
}
void CharacterStateMachine::ApplyFrameEvents(CharacterObject& owner,
int previousFrame,
int currentFrame) {
@@ -323,7 +186,6 @@ void CharacterStateMachine::ApplyFrameEvents(CharacterObject& owner,
auto& motor = owner.GetMotorMutable();
for (const auto& event : currentAction_->frameEvents) {
// 帧事件把“动作进行到第几帧时要做什么”从硬编码里抽出来。
if (event.frame < previousFrame || event.frame > currentFrame) {
continue;
}

View File

@@ -0,0 +1,98 @@
#include "character/states/CharacterStateBase.h"
#include "character/CharacterObject.h"
#include "character/CharacterStateMachine.h"
namespace frostbite2D {
CharacterStateBase::CharacterStateBase(CharacterStateId stateId)
: stateId_(stateId) {
}
CharacterStateId CharacterStateBase::GetId() const {
return stateId_;
}
void CharacterStateBase::ChangeState(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) const {
machine.ChangeState(context.owner, nextState);
}
void CharacterStateBase::PlayActionById(CharacterObject& owner,
const std::string& actionId,
const std::string& fallbackTag) const {
if (const auto* action = owner.FindAction(actionId)) {
if (!action->animationTag.empty()) {
owner.PlayAnimationTag(action->animationTag);
return;
}
}
owner.PlayAnimationTag(owner.ResolveAnimationTag(actionId, fallbackTag));
}
void CharacterStateBase::InvokeHook(CharacterStateMachine& machine,
CharacterObject& owner,
const CharacterActionDefinition* action,
const char* phase) const {
machine.InvokeStateHook(owner, action, phase);
}
bool CharacterStateBase::TryStartAction(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterCommandType commandType,
const std::string& actionId) const {
return machine.TryStartAction(context.owner, context.commandBuffer, commandType, actionId);
}
bool CharacterStateBase::TryStartRegisteredAction(CharacterStateMachine& machine,
CharacterStateContext& context) const {
return machine.TryStartRegisteredAction(context);
}
void CharacterStateBase::ApplyFrameEvents(CharacterStateMachine& machine,
CharacterObject& owner,
int previousFrame,
int currentFrame) const {
machine.ApplyFrameEvents(owner, previousFrame, currentFrame);
}
const CharacterActionDefinition* CharacterStateBase::GetCurrentAction(
const CharacterStateMachine& machine) const {
return machine.currentAction_;
}
const std::string& CharacterStateBase::GetCurrentActionId(
const CharacterStateMachine& machine) const {
return machine.currentActionId_;
}
void CharacterStateBase::SetCurrentAction(CharacterStateMachine& machine,
const CharacterActionDefinition* action,
const std::string& actionId) const {
machine.currentAction_ = action;
machine.currentActionId_ = actionId;
}
void CharacterStateBase::ClearCurrentAction(CharacterStateMachine& machine) const {
machine.currentAction_ = nullptr;
machine.currentActionId_.clear();
}
float& CharacterStateBase::StateTime(CharacterStateMachine& machine) const {
return machine.stateTime_;
}
float& CharacterStateBase::ActionFrameProgress(CharacterStateMachine& machine) const {
return machine.actionFrameProgress_;
}
int& CharacterStateBase::ActionFrame(CharacterStateMachine& machine) const {
return machine.actionFrame_;
}
float& CharacterStateBase::LandingTimer(CharacterStateMachine& machine) const {
return machine.landingTimer_;
}
} // namespace frostbite2D

View File

@@ -0,0 +1,32 @@
#include "character/states/CharacterStateRegistry.h"
#include "character/CharacterStateMachine.h"
#include "character/states/common/DeadState.h"
#include "character/states/common/FallState.h"
#include "character/states/common/HurtState.h"
#include "character/states/common/IdleState.h"
#include "character/states/common/JumpState.h"
#include "character/states/common/LandingState.h"
#include "character/states/common/MoveState.h"
#include "character/states/jobs/swordman/SwordmanAttackState.h"
#include <memory>
namespace frostbite2D {
void RegisterCommonCharacterStates(CharacterStateMachine& machine) {
machine.RegisterState(std::make_unique<IdleState>());
machine.RegisterState(std::make_unique<MoveState>());
machine.RegisterState(std::make_unique<JumpState>());
machine.RegisterState(std::make_unique<FallState>());
machine.RegisterState(std::make_unique<LandingState>());
machine.RegisterState(std::make_unique<HurtState>());
machine.RegisterState(std::make_unique<DeadState>());
}
void RegisterJobCharacterStates(CharacterStateMachine& machine,
const character::CharacterConfig& config) {
if (config.jobTag == "swordman") {
machine.RegisterState(std::make_unique<SwordmanAttackState>());
}
}
} // namespace frostbite2D

View File

@@ -0,0 +1,16 @@
#include "character/states/common/DeadState.h"
#include "character/CharacterObject.h"
namespace frostbite2D {
DeadState::DeadState()
: CharacterStateBase(CharacterStateId::Dead) {
}
void DeadState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
(void)machine;
context.owner.GetMotorMutable().StopGroundMovement();
}
} // namespace frostbite2D

View File

@@ -0,0 +1,43 @@
#include "character/states/common/FallState.h"
#include "character/CharacterObject.h"
namespace frostbite2D {
FallState::FallState()
: CharacterStateBase(CharacterStateId::Fall) {
}
void FallState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
PlayActionById(context.owner, "fall");
InvokeHook(machine, context.owner, context.owner.FindAction("fall"), "on_enter");
}
void FallState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, 0.75f);
owner.SetFacing(motor.facing);
if (motor.grounded) {
ChangeState(machine, context, CharacterStateId::Landing);
return;
}
if (motor.verticalVelocity > 0.0f) {
ChangeState(machine, context, CharacterStateId::Jump);
return;
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
context.commandBuffer.Consume(CharacterCommandType::AttackPressed);
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
context.commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
}
}
} // namespace frostbite2D

View File

@@ -0,0 +1,77 @@
#include "character/states/common/HurtState.h"
#include "character/CharacterObject.h"
#include <algorithm>
#include <cmath>
namespace frostbite2D {
namespace {
CharacterStateId ResolveInterruptedState(const CharacterStateContext& context) {
auto& motor = context.owner.GetMotorMutable();
if (!motor.grounded) {
return CharacterStateId::Fall;
}
return (std::abs(context.intent.moveX) > 0.01f || std::abs(context.intent.moveY) > 0.01f)
? CharacterStateId::Move
: CharacterStateId::Idle;
}
} // namespace
HurtState::HurtState()
: CharacterStateBase(CharacterStateId::Hurt) {
}
void HurtState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
ActionFrameProgress(machine) = 0.0f;
ActionFrame(machine) = 0;
const CharacterActionDefinition* action = GetCurrentAction(machine);
if (!action) {
action = context.owner.FindAction("hurt_light");
SetCurrentAction(machine, action, action ? action->actionId : "hurt_light");
}
if (action) {
context.owner.PlayAnimationTag(action->animationTag);
InvokeHook(machine, context.owner, action, "on_enter");
}
}
void HurtState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
motor.StopGroundMovement();
const CharacterActionDefinition* action = GetCurrentAction(machine);
if (!action) {
action = owner.FindAction("hurt_light");
SetCurrentAction(machine, action, action ? action->actionId : "hurt_light");
}
if (!action) {
context.owner.SetFacing(motor.facing);
ChangeState(machine, context, ResolveInterruptedState(context));
return;
}
InvokeHook(machine, owner, action, "on_update");
ActionFrameProgress(machine) += context.deltaTime * 60.0f;
ActionFrame(machine) = std::max(0, static_cast<int>(ActionFrameProgress(machine)));
if (action->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) {
ClearCurrentAction(machine);
context.owner.SetFacing(motor.facing);
ChangeState(machine, context, ResolveInterruptedState(context));
}
}
void HurtState::OnExit(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) {
(void)nextState;
InvokeHook(machine, context.owner, GetCurrentAction(machine), "on_exit");
}
} // namespace frostbite2D

View File

@@ -0,0 +1,55 @@
#include "character/states/common/IdleState.h"
#include "character/CharacterObject.h"
#include <cmath>
namespace frostbite2D {
namespace {
bool HasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
}
} // namespace
IdleState::IdleState()
: CharacterStateBase(CharacterStateId::Idle) {
}
void IdleState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
PlayActionById(context.owner, "idle");
InvokeHook(machine, context.owner, context.owner.FindAction("idle"), "on_enter");
}
void IdleState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
if (!motor.grounded) {
ChangeState(machine, context, CharacterStateId::Fall);
return;
}
if (TryStartRegisteredAction(machine, context)) {
return;
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::JumpPressed)) {
context.commandBuffer.Consume(CharacterCommandType::JumpPressed);
motor.Jump();
ChangeState(machine, context, CharacterStateId::Jump);
return;
}
if (HasMoveIntent(context.intent)) {
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY);
owner.SetFacing(motor.facing);
ChangeState(machine, context, CharacterStateId::Move);
return;
}
motor.StopGroundMovement();
}
} // namespace frostbite2D

View File

@@ -0,0 +1,43 @@
#include "character/states/common/JumpState.h"
#include "character/CharacterObject.h"
namespace frostbite2D {
JumpState::JumpState()
: CharacterStateBase(CharacterStateId::Jump) {
}
void JumpState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
PlayActionById(context.owner, "jump");
InvokeHook(machine, context.owner, context.owner.FindAction("jump"), "on_enter");
}
void JumpState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, 0.75f);
owner.SetFacing(motor.facing);
if (motor.grounded) {
ChangeState(machine, context, CharacterStateId::Landing);
return;
}
if (motor.verticalVelocity <= 0.0f) {
ChangeState(machine, context, CharacterStateId::Fall);
return;
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
context.commandBuffer.Consume(CharacterCommandType::AttackPressed);
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
context.commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
}
}
} // namespace frostbite2D

View File

@@ -0,0 +1,64 @@
#include "character/states/common/LandingState.h"
#include "character/CharacterObject.h"
#include <cmath>
namespace frostbite2D {
namespace {
constexpr float kLandingDuration = 0.08f;
bool HasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
}
} // namespace
LandingState::LandingState()
: CharacterStateBase(CharacterStateId::Landing) {
}
void LandingState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
LandingTimer(machine) = 0.0f;
PlayActionById(context.owner, "landing");
InvokeHook(machine, context.owner, context.owner.FindAction("landing"), "on_enter");
}
void LandingState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
if (!motor.grounded) {
ChangeState(machine, context, CharacterStateId::Fall);
return;
}
LandingTimer(machine) += context.deltaTime;
motor.StopGroundMovement();
if (LandingTimer(machine) < kLandingDuration) {
return;
}
if (TryStartRegisteredAction(machine, context)) {
return;
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::JumpPressed)) {
context.commandBuffer.Consume(CharacterCommandType::JumpPressed);
motor.Jump();
ChangeState(machine, context, CharacterStateId::Jump);
return;
}
if (HasMoveIntent(context.intent)) {
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY);
owner.SetFacing(motor.facing);
ChangeState(machine, context, CharacterStateId::Move);
return;
}
ChangeState(machine, context, CharacterStateId::Idle);
}
} // namespace frostbite2D

View File

@@ -0,0 +1,53 @@
#include "character/states/common/MoveState.h"
#include "character/CharacterObject.h"
#include <cmath>
namespace frostbite2D {
namespace {
bool HasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
}
} // namespace
MoveState::MoveState()
: CharacterStateBase(CharacterStateId::Move) {
}
void MoveState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
PlayActionById(context.owner, "move");
InvokeHook(machine, context.owner, context.owner.FindAction("move"), "on_enter");
}
void MoveState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
if (!motor.grounded) {
ChangeState(machine, context, CharacterStateId::Fall);
return;
}
if (TryStartRegisteredAction(machine, context)) {
return;
}
if (context.commandBuffer.HasBuffered(CharacterCommandType::JumpPressed)) {
context.commandBuffer.Consume(CharacterCommandType::JumpPressed);
motor.Jump();
ChangeState(machine, context, CharacterStateId::Jump);
return;
}
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY);
owner.SetFacing(motor.facing);
if (!HasMoveIntent(context.intent)) {
motor.StopGroundMovement();
ChangeState(machine, context, CharacterStateId::Idle);
}
}
} // namespace frostbite2D

View File

@@ -0,0 +1,110 @@
#include "character/states/jobs/swordman/SwordmanAttackState.h"
#include "character/CharacterObject.h"
#include <algorithm>
#include <cmath>
namespace frostbite2D {
namespace {
constexpr float kLogicFramesPerSecond = 60.0f;
constexpr float kMovementActionScale = 0.35f;
bool HasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
}
CharacterStateId ResolveGroundState(const CharacterIntent& intent) {
return HasMoveIntent(intent) ? CharacterStateId::Move : CharacterStateId::Idle;
}
} // namespace
SwordmanAttackState::SwordmanAttackState()
: CharacterStateBase(CharacterStateId::Action) {
}
bool SwordmanAttackState::TryEnter(CharacterStateMachine& machine,
CharacterStateContext& context) {
if (TryStartAction(machine, context, CharacterCommandType::Skill1Pressed, "skill_1")) {
return true;
}
return TryStartAction(machine, context, CharacterCommandType::AttackPressed, "attack_1");
}
void SwordmanAttackState::OnEnter(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId previousState) {
(void)previousState;
ActionFrameProgress(machine) = 0.0f;
ActionFrame(machine) = 0;
const CharacterActionDefinition* action = GetCurrentAction(machine);
if (action) {
context.owner.PlayAnimationTag(action->animationTag);
InvokeHook(machine, context.owner, action, "on_enter");
}
}
void SwordmanAttackState::OnUpdate(CharacterStateMachine& machine,
CharacterStateContext& context) {
auto& owner = context.owner;
auto& motor = owner.GetMotorMutable();
const CharacterActionDefinition* action = GetCurrentAction(machine);
if (!action) {
ChangeState(machine, context, ResolveGroundState(context.intent));
return;
}
InvokeHook(machine, owner, action, "on_update");
if (action->canMove) {
float moveScale = action->moveSpeedScale > 0.0f ? action->moveSpeedScale : kMovementActionScale;
motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, moveScale);
} else {
motor.StopGroundMovement();
}
if (action->canTurn) {
owner.SetFacing(motor.facing);
}
ActionFrameProgress(machine) += context.deltaTime * kLogicFramesPerSecond;
int previousFrame = ActionFrame(machine);
ActionFrame(machine) = std::max(0, static_cast<int>(ActionFrameProgress(machine)));
ApplyFrameEvents(machine, owner, previousFrame + 1, ActionFrame(machine));
for (const auto& rule : action->cancelRules) {
if (ActionFrame(machine) < rule.beginFrame || ActionFrame(machine) > rule.endFrame) {
continue;
}
if (rule.requireGrounded && !motor.grounded) {
continue;
}
if (rule.requireAirborne && motor.grounded) {
continue;
}
if (rule.targetAction == "attack_2"
&& context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
if (TryStartAction(machine, context, CharacterCommandType::AttackPressed, rule.targetAction)) {
return;
}
}
}
if (action->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) {
ClearCurrentAction(machine);
if (!motor.grounded) {
ChangeState(machine, context, CharacterStateId::Fall);
} else {
ChangeState(machine, context, ResolveGroundState(context.intent));
}
}
}
void SwordmanAttackState::OnExit(CharacterStateMachine& machine,
CharacterStateContext& context,
CharacterStateId nextState) {
(void)nextState;
InvokeHook(machine, context.owner, GetCurrentAction(machine), "on_exit");
}
} // namespace frostbite2D