feat(角色状态机): 重构角色状态系统为基于类的设计
新增通用状态类和职业专属状态类,将状态逻辑从状态机中解耦 添加状态注册机制,支持按职业配置状态 实现基础状态如待机、移动、跳跃、受击等 为剑士职业实现专属攻击状态 补充状态机开发说明文档
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
94
Game/include/character/states/CharacterStateBase.h
Normal file
94
Game/include/character/states/CharacterStateBase.h
Normal 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
|
||||
13
Game/include/character/states/CharacterStateRegistry.h
Normal file
13
Game/include/character/states/CharacterStateRegistry.h
Normal 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
|
||||
15
Game/include/character/states/common/DeadState.h
Normal file
15
Game/include/character/states/common/DeadState.h
Normal 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
|
||||
18
Game/include/character/states/common/FallState.h
Normal file
18
Game/include/character/states/common/FallState.h
Normal 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
|
||||
21
Game/include/character/states/common/HurtState.h
Normal file
21
Game/include/character/states/common/HurtState.h
Normal 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
|
||||
18
Game/include/character/states/common/IdleState.h
Normal file
18
Game/include/character/states/common/IdleState.h
Normal 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
|
||||
18
Game/include/character/states/common/JumpState.h
Normal file
18
Game/include/character/states/common/JumpState.h
Normal 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
|
||||
18
Game/include/character/states/common/LandingState.h
Normal file
18
Game/include/character/states/common/LandingState.h
Normal 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
|
||||
18
Game/include/character/states/common/MoveState.h
Normal file
18
Game/include/character/states/common/MoveState.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
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;
|
||||
if (currentState_ == CharacterStateId::Action) {
|
||||
InvokeStateHook(owner, previousAction, "on_exit");
|
||||
stateTime_ = 0.0f;
|
||||
actionFrameProgress_ = 0.0f;
|
||||
actionFrame_ = 0;
|
||||
if (action) {
|
||||
owner.PlayAnimationTag(action->animationTag);
|
||||
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;
|
||||
}
|
||||
|
||||
98
Game/src/character/states/CharacterStateBase.cpp
Normal file
98
Game/src/character/states/CharacterStateBase.cpp
Normal 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
|
||||
32
Game/src/character/states/CharacterStateRegistry.cpp
Normal file
32
Game/src/character/states/CharacterStateRegistry.cpp
Normal 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
|
||||
16
Game/src/character/states/common/DeadState.cpp
Normal file
16
Game/src/character/states/common/DeadState.cpp
Normal 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
|
||||
43
Game/src/character/states/common/FallState.cpp
Normal file
43
Game/src/character/states/common/FallState.cpp
Normal 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
|
||||
77
Game/src/character/states/common/HurtState.cpp
Normal file
77
Game/src/character/states/common/HurtState.cpp
Normal 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
|
||||
55
Game/src/character/states/common/IdleState.cpp
Normal file
55
Game/src/character/states/common/IdleState.cpp
Normal 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
|
||||
43
Game/src/character/states/common/JumpState.cpp
Normal file
43
Game/src/character/states/common/JumpState.cpp
Normal 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
|
||||
64
Game/src/character/states/common/LandingState.cpp
Normal file
64
Game/src/character/states/common/LandingState.cpp
Normal 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
|
||||
53
Game/src/character/states/common/MoveState.cpp
Normal file
53
Game/src/character/states/common/MoveState.cpp
Normal 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
|
||||
110
Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp
Normal file
110
Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp
Normal 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
|
||||
694
docs/角色状态机开发说明.md
Normal file
694
docs/角色状态机开发说明.md
Normal file
@@ -0,0 +1,694 @@
|
||||
# 角色状态机开发说明
|
||||
|
||||
## 1. 这套架构是做什么的
|
||||
|
||||
这一套角色系统的目标,是把“输入处理”、“状态切换”、“动作定义”、“动画表现”和“位移推进”拆开,避免所有逻辑都堆在一个状态机函数里。
|
||||
|
||||
当前架构分成五层:
|
||||
|
||||
1. 输入层:接收键盘、手柄等事件
|
||||
2. 命令层:把输入整理成统一命令和意图
|
||||
3. 状态层:决定角色此刻处于什么状态
|
||||
4. 动作层:从 `.act` / `.lst` 读取动作参数
|
||||
5. 表现层:根据动作定义播放动画,并由 `CharacterMotor` 推进坐标
|
||||
|
||||
这意味着:
|
||||
|
||||
- 状态类负责“逻辑决策”
|
||||
- `.act` 负责“动作配置和时序参数”
|
||||
- 动画系统负责“资源表现”
|
||||
- `CharacterMotor` 负责“x/y/z 逻辑坐标推进”
|
||||
|
||||
不要再把动作时长、动画标签、可否转向、可否移动这类信息硬编码回状态机里。
|
||||
|
||||
## 2. 整体驱动流程
|
||||
|
||||
角色每帧的主流程如下:
|
||||
|
||||
```text
|
||||
输入事件
|
||||
-> CharacterInputRouter
|
||||
-> CharacterCommandBuffer
|
||||
-> CharacterIntent
|
||||
-> CharacterStateMachine::Update()
|
||||
-> 当前状态类 OnUpdate()
|
||||
-> CharacterMotor::Update()
|
||||
-> CharacterObject::SetWorldPosition()
|
||||
-> CharacterObject::PlayAnimationTag()
|
||||
```
|
||||
|
||||
可以按下面的理解来记:
|
||||
|
||||
- `CharacterInputRouter` 负责把键盘、手柄输入统一成命令
|
||||
- `CharacterCommandBuffer` 负责缓冲按钮和移动轴
|
||||
- `CharacterIntent` 是这一帧角色“想做什么”
|
||||
- `CharacterStateMachine` 负责找到当前状态类并调用它
|
||||
- 状态类决定要不要切状态、要不要吃掉输入、要不要启动动作
|
||||
- `CharacterMotor` 负责真正推进世界坐标
|
||||
- `CharacterObject` 负责把逻辑位置投影到屏幕,并驱动动画表现
|
||||
|
||||
## 3. 坐标与表现
|
||||
|
||||
角色逻辑坐标是三维:`x / y / z`
|
||||
|
||||
- `x`:横向
|
||||
- `y`:地面纵深
|
||||
- `z`:高度
|
||||
|
||||
当前渲染不是直接画三维,而是把逻辑坐标投影到屏幕:
|
||||
|
||||
- 屏幕坐标约等于 `Vec2(x, y - z)`
|
||||
- 同层遮挡排序仍然主要看地面 `y`
|
||||
|
||||
因此:
|
||||
|
||||
- 看起来像 2D
|
||||
- 但跳跃、击飞、下落这些逻辑,实际依赖 `z` 和 `verticalVelocity`
|
||||
|
||||
相关核心数据在 `CharacterActionTypes.h`:
|
||||
|
||||
- `CharacterWorldPosition`
|
||||
- `CharacterMotor`
|
||||
- `CharacterIntent`
|
||||
- `CharacterCommandBuffer`
|
||||
- `CharacterStateId`
|
||||
|
||||
## 4. 目录结构与职责
|
||||
|
||||
当前状态系统相关代码主要在这些位置:
|
||||
|
||||
```text
|
||||
Game/include/character/
|
||||
Game/src/character/
|
||||
Game/include/character/states/
|
||||
Game/src/character/states/
|
||||
```
|
||||
|
||||
### 4.1 核心文件
|
||||
|
||||
- `CharacterObject`
|
||||
- 角色主对象
|
||||
- 负责构建、更新、动画播放、受击入口
|
||||
- `CharacterStateMachine`
|
||||
- 负责注册状态、切换状态、分发更新、推进动作帧事件
|
||||
- `CharacterStateBase`
|
||||
- 所有状态类的基类
|
||||
- 提供公共 helper
|
||||
- `CharacterStateRegistry`
|
||||
- 负责把通用状态和职业状态注册进状态机
|
||||
|
||||
### 4.2 状态目录分层
|
||||
|
||||
```text
|
||||
states/
|
||||
common/
|
||||
IdleState
|
||||
MoveState
|
||||
JumpState
|
||||
FallState
|
||||
LandingState
|
||||
HurtState
|
||||
DeadState
|
||||
jobs/
|
||||
swordman/
|
||||
SwordmanAttackState
|
||||
```
|
||||
|
||||
约定如下:
|
||||
|
||||
- `common/`:所有职业都能复用的状态
|
||||
- `jobs/<job>/`:职业专属状态
|
||||
- 当前除攻击外,其余基础状态都在 `common/`
|
||||
- 剑士攻击逻辑在 `jobs/swordman/`
|
||||
|
||||
## 5. 状态机本体现在负责什么
|
||||
|
||||
`CharacterStateMachine` 现在是一个“调度器”,不是“所有逻辑的堆放点”。
|
||||
|
||||
它主要负责:
|
||||
|
||||
- `Configure()`:按职业注册状态
|
||||
- `Reset()`:重置运行时状态
|
||||
- `Update()`:找到当前状态类并调用 `OnUpdate`
|
||||
- `ChangeState()`:做状态切换,并触发 `OnExit -> OnEnter`
|
||||
- `TryStartAction()`:按动作 id 启动动作
|
||||
- `ApplyFrameEvents()`:根据 `.act` 定义执行帧事件
|
||||
|
||||
它不应该再负责:
|
||||
|
||||
- 直接写死所有状态的业务逻辑
|
||||
- 直接写死 idle/move/jump/attack 的完整流程
|
||||
- 直接把职业特有攻击逻辑塞在一个大 switch 里
|
||||
|
||||
## 6. 状态类生命周期
|
||||
|
||||
每个状态类都继承 `CharacterStateBase`,支持以下生命周期:
|
||||
|
||||
### 6.1 `OnEnter`
|
||||
|
||||
进入状态时调用,常见用途:
|
||||
|
||||
- 重置计时器
|
||||
- 设置动作帧游标
|
||||
- 播放对应动画
|
||||
- 调用进入 hook
|
||||
|
||||
### 6.2 `OnUpdate`
|
||||
|
||||
每帧调用,常见用途:
|
||||
|
||||
- 检查输入
|
||||
- 检查 grounded / velocity / 受击等条件
|
||||
- 切换状态
|
||||
- 推进动作帧
|
||||
- 执行取消规则
|
||||
- 消费输入命令
|
||||
|
||||
### 6.3 `OnExit`
|
||||
|
||||
离开状态时调用,常见用途:
|
||||
|
||||
- 调用退出 hook
|
||||
- 做简单清理
|
||||
|
||||
### 6.4 `TryEnter`
|
||||
|
||||
这个不是所有状态都有。
|
||||
|
||||
只有“承担动作入口判定职责”的职业动作状态才需要实现 `ICharacterActionStateNode`,例如当前的:
|
||||
|
||||
- `SwordmanAttackState`
|
||||
|
||||
它的作用是:
|
||||
|
||||
- 在通用状态里,不直接写死“按攻击键就进哪个动作”
|
||||
- 而是统一调用 `TryStartRegisteredAction()`
|
||||
- 再由职业动作状态自己的 `TryEnter()` 决定是 `attack_1`、`skill_1` 还是别的动作
|
||||
|
||||
这样职业差异就解耦开了。
|
||||
|
||||
## 7. CharacterStateBase 提供了什么
|
||||
|
||||
`CharacterStateBase` 是给所有状态类复用的基类,常用 helper 包括:
|
||||
|
||||
- `ChangeState()`
|
||||
- 切换到另一个状态
|
||||
- `PlayActionById()`
|
||||
- 通过逻辑动作 id 查动作定义并播放动画
|
||||
- `InvokeHook()`
|
||||
- 触发状态 hook,当前先走 C++ 预留接口
|
||||
- `TryStartAction()`
|
||||
- 按命令类型和动作 id 尝试启动动作
|
||||
- `TryStartRegisteredAction()`
|
||||
- 让职业动作状态决定是否启动动作
|
||||
- `ApplyFrameEvents()`
|
||||
- 执行动作的帧事件
|
||||
- `GetCurrentAction()` / `SetCurrentAction()` / `ClearCurrentAction()`
|
||||
- 读写当前动作定义
|
||||
- `StateTime()` / `ActionFrameProgress()` / `ActionFrame()` / `LandingTimer()`
|
||||
- 访问状态机内部运行时变量
|
||||
|
||||
开发新状态时,优先复用这些 helper,不要重复去碰状态机内部字段。
|
||||
|
||||
## 8. 当前已有状态怎么分工
|
||||
|
||||
### 8.1 通用状态
|
||||
|
||||
- `IdleState`
|
||||
- 地面待机
|
||||
- 检查移动、跳跃、职业动作入口
|
||||
- `MoveState`
|
||||
- 地面移动
|
||||
- 检查停止移动、跳跃、职业动作入口
|
||||
- `JumpState`
|
||||
- 上升阶段
|
||||
- 空中可控移动,升到顶后转 `Fall`
|
||||
- `FallState`
|
||||
- 下落阶段
|
||||
- 落地后转 `Landing`
|
||||
- `LandingState`
|
||||
- 落地收招阶段
|
||||
- 用短 landing timer 过渡,再回 `Idle` 或 `Move`
|
||||
- `HurtState`
|
||||
- 受击状态
|
||||
- 推进受击动作帧,到结束后回地面或空中状态
|
||||
- `DeadState`
|
||||
- 死亡状态
|
||||
- 当前只停止地面移动
|
||||
|
||||
### 8.2 剑士职业状态
|
||||
|
||||
- `SwordmanAttackState`
|
||||
- 当前负责剑士攻击与技能入口
|
||||
- 在 `TryEnter()` 决定是否启动 `attack_1` 或 `skill_1`
|
||||
- 在 `OnUpdate()` 中推进动作帧、处理 cancel rule、结束后回 `Idle / Move / Fall`
|
||||
|
||||
注意:
|
||||
|
||||
- `Action` 是一个逻辑状态 id
|
||||
- 但它的具体实现是职业可替换的
|
||||
- 现在剑士把 `Action` 的实现接管到了 `SwordmanAttackState`
|
||||
|
||||
## 9. 动作和动画到底怎么关联
|
||||
|
||||
这是开发时最容易混的地方。
|
||||
|
||||
### 9.1 两层名字
|
||||
|
||||
当前系统里有两层名字:
|
||||
|
||||
1. 逻辑动作名:例如 `idle`、`move`、`attack_1`
|
||||
2. 动画标签名:例如 `rest`、`move`、`attack1`
|
||||
|
||||
### 9.2 读取来源
|
||||
|
||||
逻辑动作定义从 `script.pvf` 中读取,路径关系是:
|
||||
|
||||
```text
|
||||
character/<jobTag>/action_logic/action_list.lst
|
||||
-> 指向多个 .act
|
||||
-> 每个 .act 提供一个动作定义
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
0 `swordman/action_logic/idle.act`
|
||||
1 `swordman/action_logic/move.act`
|
||||
2 `swordman/action_logic/attack_1.act`
|
||||
```
|
||||
|
||||
### 9.3 `.act` 里关注哪些字段
|
||||
|
||||
当前最关键的是:
|
||||
|
||||
- `action id`
|
||||
- `animation tag`
|
||||
- `total frames`
|
||||
- `loop`
|
||||
- `can move`
|
||||
- `can turn`
|
||||
- `can be interrupted`
|
||||
|
||||
其中:
|
||||
|
||||
- `action id` 是玩法逻辑识别名
|
||||
- `animation tag` 是动画系统真正去播放的标签
|
||||
|
||||
所以状态类通常不直接写动画名,而是:
|
||||
|
||||
- 先用逻辑动作 id 找动作定义
|
||||
- 再从动作定义里拿 `animationTag`
|
||||
- 最后调用 `PlayAnimationTag()`
|
||||
|
||||
### 9.4 一个例子
|
||||
|
||||
例如 `attack_1.act` 里:
|
||||
|
||||
```text
|
||||
[action id]
|
||||
`attack_1`
|
||||
|
||||
[animation tag]
|
||||
`attack1`
|
||||
```
|
||||
|
||||
那状态系统理解为:
|
||||
|
||||
- 逻辑上现在执行的是 `attack_1`
|
||||
- 表现上要播的动画是 `attack1`
|
||||
|
||||
## 10. `.lst` 与 `.act` 的当前约定
|
||||
|
||||
### 10.1 `action_list.lst` 格式
|
||||
|
||||
当前只支持新格式:
|
||||
|
||||
```text
|
||||
#PVF_File
|
||||
0 `swordman/action_logic/idle.act`
|
||||
1 `swordman/action_logic/move.act`
|
||||
2 `swordman/action_logic/jump.act`
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 第一列是索引
|
||||
- 第二列是相对职业目录的 `.act` 路径
|
||||
- 项目里当前只按“索引 / 路径”对来解析
|
||||
|
||||
### 10.2 `.act` 写法注意
|
||||
|
||||
字符串建议写成:
|
||||
|
||||
```text
|
||||
`attack_1`
|
||||
```
|
||||
|
||||
不要依赖源码层再做“去反引号”的兼容逻辑。
|
||||
|
||||
另外布尔项在 PVF 中不要写 C++ 风格的裸 `true` / `false`。要用当前 PVF 能稳定落地的格式,例如项目现有约定支持的 0/1 或能被脚本解析的文本形式。
|
||||
|
||||
## 11. 当前动作帧是怎么推进的
|
||||
|
||||
动作状态下,核心流程是:
|
||||
|
||||
1. `TryStartAction()` 启动某个逻辑动作
|
||||
2. 状态机记录 `currentAction_` 和 `currentActionId_`
|
||||
3. 进入 `Action` 状态的 `OnEnter()`
|
||||
4. `OnUpdate()` 每帧用 `deltaTime * 60` 推进逻辑帧
|
||||
5. 根据动作定义应用帧事件和取消规则
|
||||
6. 到达 `totalFrames` 后回收动作并切回后续状态
|
||||
|
||||
当前帧事件入口在:
|
||||
|
||||
- `CharacterStateMachine::ApplyFrameEvents()`
|
||||
|
||||
当前已支持的关键帧事件类型包括:
|
||||
|
||||
- `SetVelocityXY`
|
||||
- `SetVelocityZ`
|
||||
- `FinishAction`
|
||||
|
||||
后面如果要加命中框、输入开放窗、特效事件,也优先沿着帧事件扩,而不是继续把时序写死到状态类里。
|
||||
|
||||
## 12. 如何新增一个通用状态
|
||||
|
||||
下面以“新增 DashState”为例。
|
||||
|
||||
### 步骤 1:新建状态类
|
||||
|
||||
在:
|
||||
|
||||
```text
|
||||
Game/include/character/states/common/DashState.h
|
||||
Game/src/character/states/common/DashState.cpp
|
||||
```
|
||||
|
||||
类定义继承:
|
||||
|
||||
```cpp
|
||||
class DashState : public CharacterStateBase {
|
||||
public:
|
||||
DashState();
|
||||
|
||||
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;
|
||||
};
|
||||
```
|
||||
|
||||
### 步骤 2:构造函数绑定状态 id
|
||||
|
||||
```cpp
|
||||
DashState::DashState()
|
||||
: CharacterStateBase(CharacterStateId::Dash) {
|
||||
}
|
||||
```
|
||||
|
||||
如果新增的是全新状态 id,需要先扩展 `CharacterStateId` 枚举。
|
||||
|
||||
### 步骤 3:实现状态逻辑
|
||||
|
||||
常规做法:
|
||||
|
||||
- `OnEnter()`:重置计时器、播 dash 动画
|
||||
- `OnUpdate()`:推进位移,检查结束条件,决定切回哪个状态
|
||||
- `OnExit()`:收尾或发 hook
|
||||
|
||||
### 步骤 4:注册状态
|
||||
|
||||
在 `CharacterStateRegistry` 中注册:
|
||||
|
||||
- 通用状态放 `RegisterCommonCharacterStates()`
|
||||
- 职业状态放 `RegisterJobCharacterStates()`
|
||||
|
||||
### 步骤 5:补切换入口
|
||||
|
||||
仅注册状态还不够,还要在合适的已有状态里决定“什么时候能进这个状态”。
|
||||
|
||||
例如:
|
||||
|
||||
- 在 `IdleState::OnUpdate()` 检查 dash 输入
|
||||
- 在 `MoveState::OnUpdate()` 检查 dash 输入
|
||||
- 或在职业动作状态中允许某些动作 cancel 到 dash
|
||||
|
||||
### 步骤 6:准备动作资源
|
||||
|
||||
如果 dash 有专属动作,要补:
|
||||
|
||||
- `action_list.lst`
|
||||
- `dash.act`
|
||||
- 对应动画标签资源
|
||||
|
||||
## 13. 如何新增一个职业动作状态
|
||||
|
||||
如果是职业差异比较大的状态,建议放到:
|
||||
|
||||
```text
|
||||
states/jobs/<job>/
|
||||
```
|
||||
|
||||
### 13.1 什么时候应该做成职业状态
|
||||
|
||||
满足以下情况时建议独立做:
|
||||
|
||||
- 输入入口不同
|
||||
- 动作链不同
|
||||
- 取消规则不同
|
||||
- 动作帧事件不同
|
||||
- 和其他职业共享价值不高
|
||||
|
||||
### 13.2 如果它承担动作入口
|
||||
|
||||
就让它同时继承:
|
||||
|
||||
- `CharacterStateBase`
|
||||
- `ICharacterActionStateNode`
|
||||
|
||||
然后实现:
|
||||
|
||||
- `TryEnter()`:决定什么输入启动什么动作
|
||||
- `OnEnter()`:进入动作表现
|
||||
- `OnUpdate()`:推进帧、处理 cancel、判断结束
|
||||
- `OnExit()`:退出收尾
|
||||
|
||||
### 13.3 参考当前剑士攻击状态
|
||||
|
||||
可以直接参考:
|
||||
|
||||
- `SwordmanAttackState::TryEnter()`
|
||||
- `SwordmanAttackState::OnUpdate()`
|
||||
|
||||
它已经演示了:
|
||||
|
||||
- 用 `Skill1Pressed` 启动 `skill_1`
|
||||
- 用 `AttackPressed` 启动 `attack_1`
|
||||
- 用 cancel rule 在窗口内切到 `attack_2`
|
||||
|
||||
## 14. 新增一个动作资源要做什么
|
||||
|
||||
如果只是给已有状态补一个新动作,通常要做这几件事:
|
||||
|
||||
1. 在 `action_list.lst` 中增加映射
|
||||
2. 新建对应 `.act`
|
||||
3. 在 `.act` 中写好 `action id` 和 `animation tag`
|
||||
4. 确保动画资源里真的有这个 tag
|
||||
5. 在状态逻辑里通过逻辑动作 id 去触发它
|
||||
|
||||
最小示例:
|
||||
|
||||
```text
|
||||
#PVF_File
|
||||
0 `swordman/action_logic/idle.act`
|
||||
1 `swordman/action_logic/move.act`
|
||||
2 `swordman/action_logic/dash.act`
|
||||
```
|
||||
|
||||
对应 `dash.act` 示例:
|
||||
|
||||
```text
|
||||
#PVF_File
|
||||
|
||||
[action id]
|
||||
`dash`
|
||||
|
||||
[animation tag]
|
||||
`dash`
|
||||
|
||||
[total frames]
|
||||
12
|
||||
|
||||
[loop]
|
||||
0
|
||||
|
||||
[can move]
|
||||
1
|
||||
|
||||
[can turn]
|
||||
0
|
||||
|
||||
[can be interrupted]
|
||||
0
|
||||
```
|
||||
|
||||
## 15. 如何理解当前输入接入
|
||||
|
||||
当前输入来源不只键盘,主要有:
|
||||
|
||||
- 键盘
|
||||
- 手柄轴
|
||||
- 手柄按键
|
||||
|
||||
统一入口都收敛到 `CharacterInputRouter`,它负责把平台输入转成统一命令。
|
||||
|
||||
所以状态层不要关心:
|
||||
|
||||
- 这是键盘来的
|
||||
- 还是手柄来的
|
||||
|
||||
状态层只关心:
|
||||
|
||||
- 有没有 `JumpPressed`
|
||||
- 有没有 `AttackPressed`
|
||||
- 当前 `moveX / moveY` 是多少
|
||||
|
||||
这就是输入解耦的关键。
|
||||
|
||||
## 16. 当前脚本扩展点在哪里
|
||||
|
||||
`CharacterObject::RunStateHook()` 是当前预留给脚本系统的入口。
|
||||
|
||||
状态在以下时机会调用它:
|
||||
|
||||
- `on_enter`
|
||||
- `on_update`
|
||||
- `on_exit`
|
||||
|
||||
目前它还是空实现,意思是:
|
||||
|
||||
- 当前实际逻辑仍由 C++ 状态类负责
|
||||
- 但未来可以把一部分进入、更新、退出逻辑下沉到脚本
|
||||
|
||||
如果后面要接脚本,建议保持下面的边界:
|
||||
|
||||
- C++ 继续控制状态切换主流程和底层安全逻辑
|
||||
- 脚本负责更灵活的状态表现、特效、派生行为、部分判定
|
||||
|
||||
不要一开始就把所有底层切换权完全放给脚本,否则调试成本会很高。
|
||||
|
||||
## 17. 常见问题排查
|
||||
|
||||
### 17.1 按键了角色没反应
|
||||
|
||||
优先查:
|
||||
|
||||
1. 输入事件有没有进 `CharacterInputRouter`
|
||||
2. 有没有生成对应命令进入 `CharacterCommandBuffer`
|
||||
3. 当前状态的 `OnUpdate()` 有没有检查这个命令
|
||||
4. 命令有没有被过早消费
|
||||
5. 对应动作 id 有没有加载成功
|
||||
|
||||
### 17.2 状态切了但动画不对
|
||||
|
||||
优先查:
|
||||
|
||||
1. `.act` 的 `animation tag` 是否正确
|
||||
2. 动画资源里是否存在这个 tag
|
||||
3. 当前状态是否通过逻辑动作 id 正确拿到了动作定义
|
||||
4. 是否被 fallback 动画顶掉了
|
||||
|
||||
### 17.3 动作资源没读到
|
||||
|
||||
优先查:
|
||||
|
||||
1. `action_list.lst` 路径格式是否正确
|
||||
2. `.act` 路径是否相对于 `character/<jobTag>/`
|
||||
3. `jobTag` 是否和实际职业目录一致
|
||||
4. PVF 中是否真的导入了这些文件
|
||||
|
||||
### 17.4 朝左时时装和人物不契合
|
||||
|
||||
这个问题通常优先查:
|
||||
|
||||
- Ani 锚点
|
||||
- 左右翻转实现
|
||||
- 各部件的挂点/偏移
|
||||
|
||||
它一般不是状态机逻辑问题。
|
||||
|
||||
## 18. 推荐开发约定
|
||||
|
||||
为了后面大家都能维护,建议遵守下面几条:
|
||||
|
||||
- 一个状态一个类
|
||||
- 通用状态优先放 `common/`
|
||||
- 职业差异大的状态放 `jobs/<job>/`
|
||||
- 动作参数优先写 `.act`
|
||||
- 状态类只写决策,不写死资源细节
|
||||
- 新功能先找有没有通用 helper 能复用
|
||||
- 新状态一定补入口,不要只注册不切换
|
||||
|
||||
## 19. 一个最小状态模板
|
||||
|
||||
下面是一个最小可抄的状态模板:
|
||||
|
||||
```cpp
|
||||
class ExampleState : public CharacterStateBase {
|
||||
public:
|
||||
ExampleState()
|
||||
: CharacterStateBase(CharacterStateId::Idle) {
|
||||
}
|
||||
|
||||
void OnEnter(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId previousState) override {
|
||||
(void)previousState;
|
||||
PlayActionById(context.owner, "idle");
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("idle"), "on_enter");
|
||||
}
|
||||
|
||||
void OnUpdate(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context) override {
|
||||
auto& motor = context.owner.GetMotorMutable();
|
||||
if (!motor.grounded) {
|
||||
ChangeState(machine, context, CharacterStateId::Fall);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void OnExit(CharacterStateMachine& machine,
|
||||
CharacterStateContext& context,
|
||||
CharacterStateId nextState) override {
|
||||
(void)nextState;
|
||||
InvokeHook(machine, context.owner, context.owner.FindAction("idle"), "on_exit");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 20. 新同事接入时建议先读什么
|
||||
|
||||
建议按这个顺序看:
|
||||
|
||||
1. `CharacterObject::OnUpdate()`
|
||||
2. `CharacterStateMachine`
|
||||
3. `CharacterStateBase`
|
||||
4. `CharacterStateRegistry`
|
||||
5. `states/common/` 下的基础状态
|
||||
6. `states/jobs/swordman/SwordmanAttackState`
|
||||
7. `CharacterActionTypes.h`
|
||||
8. 对应职业的 `action_list.lst` 和 `.act`
|
||||
|
||||
这样能最快建立“输入 -> 状态 -> 动作 -> 动画 -> 位移”的完整认知。
|
||||
|
||||
---
|
||||
|
||||
如果后面架构继续扩展,建议再补两份专题文档:
|
||||
|
||||
- 一份讲 `.act` / 帧事件 / cancel rule 设计
|
||||
- 一份讲脚本如何接入状态生命周期
|
||||
Reference in New Issue
Block a user