Files
Frostbite2D/Game/include/character/CharacterObject.h
Lenheart 1200cf0181 refactor(character): 重构角色动作与动画系统
- 移除自动回退动作生成逻辑,改为严格检查动作定义
- 增加动作资源缺失时的详细错误报告机制
- 统一输入事件处理接口,优化角色对象生命周期管理
- 改进动画标签管理,移除隐式回退逻辑
- 增强状态机对无效动作的处理能力
2026-04-04 05:53:07 +08:00

194 lines
7.8 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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/event.h>
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/key_event.h>
#include <optional>
#include <string>
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;
/// @brief 统一输入入口。
///
/// `Actor` 底层会把不同 SDL 事件派发到这里;本类再把它们转交给 `CharacterInputRouter`。
bool OnEvent(const Event& 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_; }
CharacterCommandBuffer& GetCommandBufferMutable() { return commandBuffer_; }
float GetLastDeltaTime() const { return lastDeltaTime_; }
/// @brief 按逻辑动作 id 查询动作定义。
const CharacterActionDefinition* FindAction(const std::string& actionId) 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 {
return config_ ? &config_.value() : nullptr;
}
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