feat(character): 实现角色状态机与输入路由系统

新增角色状态机框架,包含空闲、移动、跳跃、攻击等状态
添加输入路由组件,统一处理键盘和手柄输入
引入动作库管理角色动作定义,支持PVF脚本配置
重构角色对象,整合运动器、状态机和输入处理
修复Actor子节点排序时的迭代安全问题
This commit is contained in:
2026-04-03 08:08:23 +08:00
parent b5c432e77a
commit bb75a57afb
14 changed files with 1463 additions and 6 deletions

View File

@@ -0,0 +1,28 @@
#pragma once
#include "character/CharacterActionTypes.h"
#include "character/CharacterDataLoader.h"
#include <map>
#include <optional>
#include <string>
namespace frostbite2D {
/// 管理角色动作定义。优先从 PVF 动作脚本读取,缺失时回退到内置默认动作。
class CharacterActionLibrary {
public:
bool LoadForConfig(const character::CharacterConfig& config);
const CharacterActionDefinition* FindAction(const std::string& actionId) const;
const CharacterActionDefinition* GetDefaultAction() const;
private:
void BuildFallbackActions(const character::CharacterConfig& config);
bool TryLoadPvfActionScripts(const character::CharacterConfig& config);
std::map<std::string, CharacterActionDefinition> actions_;
};
std::optional<CharacterActionLibrary> loadCharacterActionLibrary(
const character::CharacterConfig& config);
} // namespace frostbite2D

View File

@@ -0,0 +1,199 @@
#pragma once
#include <frostbite2D/types/type_math.h>
#include <string>
#include <vector>
namespace frostbite2D {
/// 输入层产出的统一命令,屏蔽键盘与手柄的设备差异。
enum class CharacterCommandType {
Move,
JumpPressed,
AttackPressed,
Skill1Pressed,
DashPressed,
};
/// 单条输入命令。移动命令携带轴值,按钮命令只关心触发时机。
struct CharacterCommand {
CharacterCommandType type = CharacterCommandType::Move;
Vec2 moveAxis = Vec2::Zero();
float timestamp = 0.0f;
};
/// 角色在当前帧想要执行的意图,供状态机消费。
struct CharacterIntent {
float moveX = 0.0f;
float moveY = 0.0f;
bool wantJump = false;
bool wantAttack = false;
bool wantSkill1 = false;
bool wantDash = false;
};
/// 主状态机的大状态,普攻和技能统一走 Action。
enum class CharacterStateId {
Idle,
Move,
Jump,
Fall,
Landing,
Action,
Hurt,
Dead,
};
/// 角色的逻辑世界坐标。x/y 是地面平面z 是高度轴。
struct CharacterWorldPosition {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
/// 当前项目的 2.5D 投影:地面 y 决定站位,高度 z 决定视觉抬升。
Vec2 ToScreenPosition() const { return Vec2(x, y - z); }
Vec2 ToGroundPosition() const { return Vec2(x, y); }
};
/// 运动器只负责推进坐标和速度,不直接决定状态流转。
struct CharacterMotor {
CharacterWorldPosition position;
Vec2 groundVelocity = Vec2::Zero();
float verticalVelocity = 0.0f;
bool grounded = true;
int facing = 1;
float moveSpeed = 220.0f;
float jumpSpeed = 520.0f;
float gravity = 1600.0f;
void SetGroundPosition(const Vec2& groundPosition) {
position.x = groundPosition.x;
position.y = groundPosition.y;
}
void ApplyGroundInput(float moveX, float moveY, float speedScale = 1.0f) {
Vec2 input(moveX, moveY);
if (input.lengthSquared() > 1.0f) {
input = input.normalized();
}
groundVelocity = input * (moveSpeed * speedScale);
if (input.x > 0.01f) {
facing = 1;
} else if (input.x < -0.01f) {
facing = -1;
}
}
void StopGroundMovement() { groundVelocity = Vec2::Zero(); }
void Jump() {
grounded = false;
verticalVelocity = jumpSpeed;
if (position.z < 0.0f) {
position.z = 0.0f;
}
}
void Update(float deltaTime) {
position.x += groundVelocity.x * deltaTime;
position.y += groundVelocity.y * deltaTime;
if (!grounded || position.z > 0.0f || verticalVelocity > 0.0f) {
position.z += verticalVelocity * deltaTime;
verticalVelocity -= gravity * deltaTime;
grounded = false;
if (position.z <= 0.0f) {
position.z = 0.0f;
verticalVelocity = 0.0f;
grounded = true;
}
} else {
position.z = 0.0f;
verticalVelocity = 0.0f;
grounded = true;
}
}
};
/// 帧事件是动作定义驱动逻辑的钩子,后续可扩展命中框与位移曲线。
enum class CharacterFrameEventType {
OpenHitWindow,
CloseHitWindow,
OpenInputBuffer,
CloseInputBuffer,
SetVelocityXY,
SetVelocityZ,
AnimationEvent,
FinishAction,
};
/// 单个动作帧上的逻辑事件。
struct CharacterFrameEventDefinition {
int frame = 0;
CharacterFrameEventType type = CharacterFrameEventType::AnimationEvent;
Vec2 velocityXY = Vec2::Zero();
float velocityZ = 0.0f;
std::string stringValue;
};
/// 取消规则决定动作在什么窗口内可以接下一个动作。
struct CharacterCancelRuleDefinition {
std::string targetAction;
int beginFrame = 0;
int endFrame = 0;
bool requireGrounded = false;
bool requireAirborne = false;
};
/// 逻辑动作定义。actionId 是玩法动作名animationTag 是资源动作名。
struct CharacterActionDefinition {
std::string actionId;
std::string animationTag;
int totalFrames = 1;
bool loop = false;
bool canMove = false;
bool canTurn = true;
bool canBeInterrupted = true;
int inputBufferOpenFrame = 0;
float moveSpeedScale = 0.0f;
std::vector<CharacterCancelRuleDefinition> cancelRules;
std::vector<CharacterFrameEventDefinition> frameEvents;
};
/// 运行期状态标记,承载霸体、无敌等战斗属性。
struct CharacterStatus {
bool superArmor = false;
bool invincible = false;
bool controlLocked = false;
};
/// 外部战斗系统打入角色时携带的受击上下文。
struct HitContext {
std::string hurtAction = "hurt_light";
float launchZVelocity = 0.0f;
bool ignoreInterrupt = false;
};
/// 短时缓存输入,提供预输入和连段窗口的基础能力。
class CharacterCommandBuffer {
public:
void Advance(float deltaTime);
void Submit(const CharacterCommand& command);
CharacterIntent BuildIntent() const;
bool HasBuffered(CharacterCommandType type) const;
void Consume(CharacterCommandType type);
void Clear();
private:
struct BufferedButton {
CharacterCommandType type = CharacterCommandType::AttackPressed;
float age = 0.0f;
};
Vec2 moveAxis_ = Vec2::Zero();
std::vector<BufferedButton> buttons_;
float bufferWindowSeconds_ = 0.2f;
};
} // namespace frostbite2D

View File

@@ -24,6 +24,9 @@ public:
void SetAction(const std::string& actionName);
void SetDirection(int direction);
const std::string& GetCurrentAction() const { return currentActionTag_; }
bool HasAction(const std::string& actionName) const {
return actionAnimations_.count(actionName) > 0;
}
private:
static std::string FormatImgPath(std::string path, Animation::ReplaceData data);

View File

@@ -0,0 +1,35 @@
#pragma once
#include "character/CharacterActionTypes.h"
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/key_event.h>
namespace frostbite2D {
/// 将 SDL 键盘/手柄事件统一转换为角色命令,避免状态机直接依赖设备细节。
class CharacterInputRouter {
public:
bool OnKeyDown(const KeyEvent& event);
bool OnKeyUp(KeyCode keyCode);
bool OnJoystickAxis(const JoystickAxisEvent& event);
bool OnJoystickButtonDown(const JoystickButtonDownEvent& event);
bool OnJoystickButtonUp(const JoystickButtonUpEvent& event);
void EmitCommands(CharacterCommandBuffer& commandBuffer);
void ClearFrameState();
private:
bool left_ = false;
bool right_ = false;
bool up_ = false;
bool down_ = false;
bool jumpPressed_ = false;
bool attackPressed_ = false;
bool skill1Pressed_ = false;
bool dashPressed_ = false;
float leftStickX_ = 0.0f;
float leftStickY_ = 0.0f;
float joystickDeadZone_ = 0.2f;
};
} // namespace frostbite2D

View File

@@ -1,9 +1,14 @@
#pragma once
#include "character/CharacterActionLibrary.h"
#include "character/CharacterAnimation.h"
#include "character/CharacterDataLoader.h"
#include "character/CharacterEquipmentManager.h"
#include "character/CharacterInputRouter.h"
#include "character/CharacterStateMachine.h"
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/key_event.h>
#include <optional>
#include <string>
@@ -19,11 +24,32 @@ public:
void SetAction(const std::string& actionName);
void SetDirection(int direction);
void SetCharacterPosition(const Vec2& pos);
void SetWorldPosition(const CharacterWorldPosition& pos);
void PushCommand(const CharacterCommand& command);
void ApplyHit(const HitContext& hit);
void OnUpdate(float deltaTime) override;
bool OnKeyDown(const KeyEvent& event) override;
bool OnKeyUp(const KeyEvent& event) override;
bool OnJoystickAxis(const JoystickEvent& event) override;
bool OnJoystickButtonDown(const JoystickEvent& event) override;
bool OnJoystickButtonUp(const JoystickEvent& event) override;
int GetJobId() const { return jobId_; }
int GetGrowType() const { return growType_; }
int GetDirection() const { return direction_; }
const std::string& GetCurrentAction() const { return currentAction_; }
CharacterStateId GetState() const { return stateMachine_.GetState(); }
const CharacterIntent& GetCurrentIntent() const { return currentIntent_; }
const CharacterWorldPosition& GetWorldPosition() const { return motor_.position; }
const CharacterMotor& GetMotor() const { return motor_; }
CharacterMotor& GetMotorMutable() { return motor_; }
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 SetFacing(int direction);
const character::CharacterConfig* GetConfig() const {
return config_ ? &config_.value() : nullptr;
@@ -37,7 +63,15 @@ private:
std::string currentAction_;
std::optional<character::CharacterConfig> config_;
CharacterEquipmentManager equipmentManager_;
CharacterActionLibrary actionLibrary_;
CharacterInputRouter inputRouter_;
CharacterCommandBuffer commandBuffer_;
CharacterIntent currentIntent_;
CharacterStateMachine stateMachine_;
CharacterMotor motor_;
CharacterStatus status_;
RefPtr<CharacterAnimation> animationManager_ = nullptr;
float lastDeltaTime_ = 0.0f;
};
} // namespace frostbite2D

View File

@@ -0,0 +1,59 @@
#pragma once
#include "character/CharacterActionLibrary.h"
namespace frostbite2D {
class CharacterObject;
/// 角色主状态机。输入先变成意图,再由状态机决定当前大状态和动作推进。
class CharacterStateMachine {
public:
void Reset();
void Update(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent,
float deltaTime);
void ForceHurt(const CharacterActionDefinition* hurtAction);
CharacterStateId GetState() const { return currentState_; }
const std::string& GetCurrentActionId() const { return currentActionId_; }
const CharacterActionDefinition* GetCurrentActionDefinition() const {
return currentAction_;
}
bool CanAcceptGroundActions() const;
bool CanBeInterrupted() const;
bool IsMovementLocked() const;
bool CanTurn() 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);
bool TryStartAction(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
CharacterCommandType commandType,
const std::string& actionId);
void ApplyFrameEvents(CharacterObject& owner, int previousFrame, int currentFrame);
CharacterStateId currentState_ = CharacterStateId::Idle;
std::string currentActionId_;
const CharacterActionDefinition* currentAction_ = nullptr;
float stateTime_ = 0.0f;
float actionFrameProgress_ = 0.0f;
int actionFrame_ = 0;
float landingTimer_ = 0.0f;
};
} // namespace frostbite2D