refactor(character): 重构角色动作与动画系统

- 移除自动回退动作生成逻辑,改为严格检查动作定义
- 增加动作资源缺失时的详细错误报告机制
- 统一输入事件处理接口,优化角色对象生命周期管理
- 改进动画标签管理,移除隐式回退逻辑
- 增强状态机对无效动作的处理能力
This commit is contained in:
2026-04-04 05:53:07 +08:00
parent f64180ebed
commit 1200cf0181
13 changed files with 380 additions and 202 deletions

View File

@@ -7,6 +7,7 @@
#include "character/CharacterInputRouter.h"
#include "character/CharacterStateMachine.h"
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/event/event.h>
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/key_event.h>
#include <optional>
@@ -14,26 +15,76 @@
namespace frostbite2D {
/// @brief 角色系统的主聚合对象。
///
/// 这个类可以理解为“角色外壳”:
/// - 输入事件先进入 `OnEvent()`,再由 `CharacterInputRouter` 转成统一命令
/// - `OnUpdate()` 把命令整理成意图,交给 `CharacterStateMachine` 决策
/// - `CharacterMotor` 负责推进逻辑世界坐标
/// - 最后通过 `Actor` 和 `CharacterAnimation` 把结果表现到屏幕上
///
/// 所以阅读这个类时,可以重点抓住两条主线:
/// 1. 数据如何从输入流到状态机
/// 2. 状态机如何把逻辑结果投影成最终动画和位置
class CharacterObject : public Actor {
public:
CharacterObject() = default;
~CharacterObject() override = default;
/// @brief 二段初始化角色。
/// @param jobId 职业 id用来加载职业配置、动作定义和动画资源。
/// @return 初始化成功返回 true。
bool Construction(int jobId);
// ---------------------------------------------------------------------------
// 表现与外部驱动入口
// ---------------------------------------------------------------------------
/// @brief 直接切换当前播放的资源动作标签。
///
/// 这里操作的是“动画表现层名字”,不一定等于逻辑动作 id。
void SetAction(const std::string& actionName);
/// @brief 设置朝向,并同步到动画镜像。
/// @param direction 大于等于 0 视为朝右,小于 0 视为朝左。
void SetDirection(int direction);
/// @brief 只改地面平面上的 x/y再重新投影到屏幕。
void SetCharacterPosition(const Vec2& pos);
/// @brief 写入逻辑世界坐标,并把它转换成屏幕坐标和遮挡顺序。
void SetWorldPosition(const CharacterWorldPosition& pos);
/// @brief 向输入缓冲区手动压入一条命令。
///
/// 一般用于 AI、脚本或测试代码模拟输入而不是直接发 SDL 事件。
void PushCommand(const CharacterCommand& command);
/// @brief 处理一次受击请求。
///
/// 这里会先检查无敌、霸体、当前动作是否允许被打断,再决定是否强制切到受击状态。
void ApplyHit(const HitContext& hit);
/// @brief 开关输入处理。
///
/// 关闭后角色仍会继续更新状态和运动,只是不再消费外部输入事件。
void SetInputEnabled(bool enabled) { inputEnabled_ = enabled; }
bool IsInputEnabled() const { return inputEnabled_; }
/// @brief 角色每帧主循环。
///
/// 顺序固定为:命令缓冲推进 -> 采样实时输入 -> 生成意图 -> 状态机更新
/// -> motor 推进 -> 投影到 Actor 坐标。
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;
/// @brief 统一输入入口。
///
/// `Actor` 底层会把不同 SDL 事件派发到这里;本类再把它们转交给 `CharacterInputRouter`。
bool OnEvent(const Event& event) override;
// ---------------------------------------------------------------------------
// 运行时查询接口
// ---------------------------------------------------------------------------
int GetJobId() const { return jobId_; }
int GetGrowType() const { return growType_; }
@@ -46,13 +97,37 @@ public:
CharacterMotor& GetMotorMutable() { return motor_; }
CharacterCommandBuffer& GetCommandBufferMutable() { return commandBuffer_; }
float GetLastDeltaTime() const { return lastDeltaTime_; }
/// @brief 按逻辑动作 id 查询动作定义。
const CharacterActionDefinition* FindAction(const std::string& actionId) const;
std::string ResolveAnimationTag(const std::string& preferred,
const std::string& fallback) const;
/// @brief 严格查询逻辑动作;不存在时会输出详细错误并请求退出程序。
const CharacterActionDefinition* RequireAction(const std::string& actionId,
const char* phase) const;
/// @brief 按动作定义播放动画;缺少 animationTag 或资源不存在时会输出详细错误并退出。
bool PlayActionDefinition(const CharacterActionDefinition& action, const char* phase);
/// @brief 播放某个动画标签。
///
/// 当前只是对 `SetAction()` 的语义包装,方便状态机表达“播放动作标签”。
void PlayAnimationTag(const std::string& actionTag);
/// @brief 输出角色动作/动画的致命错误,并请求应用退出。
void ReportFatalCharacterError(const char* phase,
const char* reason,
const std::string& requestedActionId = std::string(),
const std::string& requestedAnimationTag = std::string())
const;
/// @brief 触发状态生命周期钩子。
///
/// 当前为空实现,预留给后续脚本系统接管 `on_enter/on_update/on_exit`。
void RunStateHook(const std::string& phase,
CharacterStateId stateId,
const CharacterActionDefinition* action);
/// @brief 同步逻辑朝向到 motor 与动画。
void SetFacing(int direction);
const character::CharacterConfig* GetConfig() const {
@@ -61,21 +136,58 @@ public:
const CharacterEquipmentManager& GetEquipmentManager() const { return equipmentManager_; }
private:
bool SetActionStrict(const std::string& actionName,
const char* phase,
const std::string& requestedActionId);
std::string DescribeConfiguredAnimationTags() const;
/// 职业 id用于加载职业配置、动作表和资源。
int jobId_ = -1;
/// 转职/成长阶段,当前项目里先保留这个槽位。
int growType_ = -1;
/// 表现层朝向1 表示右,-1 表示左。
int direction_ = 1;
/// 当前正在播放的资源动作标签,不一定等于逻辑 actionId。
std::string currentAction_;
/// 角色静态配置,包含职业、动画路径等加载结果。
std::optional<character::CharacterConfig> config_;
/// 负责拼装装备部件,决定分层 Avatar 的表现资源。
CharacterEquipmentManager equipmentManager_;
/// 动作定义表:把逻辑 actionId 映射到帧数据、取消规则、动画标签等。
CharacterActionLibrary actionLibrary_;
/// 输入适配层,把键盘/手柄事件收敛成统一命令。
CharacterInputRouter inputRouter_;
/// 输入缓冲区,支持预输入和连段窗口。
CharacterCommandBuffer commandBuffer_;
/// 当前帧从命令缓冲整理出的“角色意图”。
CharacterIntent currentIntent_;
/// 状态调度器,负责状态切换、动作进入和帧事件推进。
CharacterStateMachine stateMachine_;
/// 纯运动数据:位置、速度、落地状态和朝向都在这里维护。
CharacterMotor motor_;
/// 战斗附加状态,如霸体、无敌、控制锁定。
CharacterStatus status_;
/// 真正负责播放角色分层动画的 Actor 子节点。
RefPtr<CharacterAnimation> animationManager_ = nullptr;
/// 缓存上一帧 deltaTime方便状态和脚本系统查询。
float lastDeltaTime_ = 0.0f;
/// 是否允许从事件系统接收输入AI/剧情接管时可临时关闭。
bool inputEnabled_ = true;
};
} // namespace frostbite2D