From 1200cf01816e63c73009dea6e5c08550062b7fa5 Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Sat, 4 Apr 2026 05:53:07 +0800 Subject: [PATCH] =?UTF-8?q?refactor(character):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E5=8A=A8=E4=BD=9C=E4=B8=8E=E5=8A=A8=E7=94=BB?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除自动回退动作生成逻辑,改为严格检查动作定义 - 增加动作资源缺失时的详细错误报告机制 - 统一输入事件处理接口,优化角色对象生命周期管理 - 改进动画标签管理,移除隐式回退逻辑 - 增强状态机对无效动作的处理能力 --- .../character/CharacterActionLibrary.h | 4 +- Game/include/character/CharacterAnimation.h | 3 +- Game/include/character/CharacterInputRouter.h | 2 +- Game/include/character/CharacterObject.h | 126 +++++++++- .../character/states/CharacterStateBase.h | 4 +- Game/src/character/CharacterActionLibrary.cpp | 113 ++------- Game/src/character/CharacterAnimation.cpp | 42 +++- Game/src/character/CharacterInputRouter.cpp | 4 +- Game/src/character/CharacterObject.cpp | 219 +++++++++++++----- Game/src/character/CharacterStateMachine.cpp | 19 +- .../character/states/CharacterStateBase.cpp | 14 +- .../src/character/states/common/HurtState.cpp | 20 +- .../jobs/swordman/SwordmanAttackState.cpp | 12 +- 13 files changed, 380 insertions(+), 202 deletions(-) diff --git a/Game/include/character/CharacterActionLibrary.h b/Game/include/character/CharacterActionLibrary.h index 70a4dd5..82ac9c1 100644 --- a/Game/include/character/CharacterActionLibrary.h +++ b/Game/include/character/CharacterActionLibrary.h @@ -8,15 +8,15 @@ namespace frostbite2D { -/// 管理角色动作定义。优先从 PVF 动作脚本读取,缺失时回退到内置默认动作。 +/// 管理角色动作定义。优先从 PVF 动作脚本读取,不再自动生成回退动作。 class CharacterActionLibrary { public: bool LoadForConfig(const character::CharacterConfig& config); const CharacterActionDefinition* FindAction(const std::string& actionId) const; const CharacterActionDefinition* GetDefaultAction() const; + std::string DescribeActionIds() const; private: - void BuildFallbackActions(const character::CharacterConfig& config); bool TryLoadPvfActionScripts(const character::CharacterConfig& config); std::map actions_; diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h index 3246693..1954d8d 100644 --- a/Game/include/character/CharacterAnimation.h +++ b/Game/include/character/CharacterAnimation.h @@ -21,12 +21,13 @@ public: const character::CharacterConfig& config, const CharacterEquipmentManager& equipmentManager); - void SetAction(const std::string& actionName); + bool 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; } + std::string DescribeAvailableActions() const; private: static std::string FormatImgPath(std::string path, Animation::ReplaceData data); diff --git a/Game/include/character/CharacterInputRouter.h b/Game/include/character/CharacterInputRouter.h index 1e6148e..0806e1c 100644 --- a/Game/include/character/CharacterInputRouter.h +++ b/Game/include/character/CharacterInputRouter.h @@ -10,7 +10,7 @@ namespace frostbite2D { class CharacterInputRouter { public: bool OnKeyDown(const KeyEvent& event); - bool OnKeyUp(KeyCode keyCode); + bool OnKeyUp(const KeyUpEvent& event); bool OnJoystickAxis(const JoystickAxisEvent& event); bool OnJoystickButtonDown(const JoystickButtonDownEvent& event); bool OnJoystickButtonUp(const JoystickButtonUpEvent& event); diff --git a/Game/include/character/CharacterObject.h b/Game/include/character/CharacterObject.h index 4863be6..0702150 100644 --- a/Game/include/character/CharacterObject.h +++ b/Game/include/character/CharacterObject.h @@ -7,6 +7,7 @@ #include "character/CharacterInputRouter.h" #include "character/CharacterStateMachine.h" #include +#include #include #include #include @@ -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 config_; + + /// 负责拼装装备部件,决定分层 Avatar 的表现资源。 CharacterEquipmentManager equipmentManager_; + + /// 动作定义表:把逻辑 actionId 映射到帧数据、取消规则、动画标签等。 CharacterActionLibrary actionLibrary_; + + /// 输入适配层,把键盘/手柄事件收敛成统一命令。 CharacterInputRouter inputRouter_; + + /// 输入缓冲区,支持预输入和连段窗口。 CharacterCommandBuffer commandBuffer_; + + /// 当前帧从命令缓冲整理出的“角色意图”。 CharacterIntent currentIntent_; + + /// 状态调度器,负责状态切换、动作进入和帧事件推进。 CharacterStateMachine stateMachine_; + + /// 纯运动数据:位置、速度、落地状态和朝向都在这里维护。 CharacterMotor motor_; + + /// 战斗附加状态,如霸体、无敌、控制锁定。 CharacterStatus status_; + + /// 真正负责播放角色分层动画的 Actor 子节点。 RefPtr animationManager_ = nullptr; + + /// 缓存上一帧 deltaTime,方便状态和脚本系统查询。 float lastDeltaTime_ = 0.0f; + + /// 是否允许从事件系统接收输入;AI/剧情接管时可临时关闭。 + bool inputEnabled_ = true; }; } // namespace frostbite2D diff --git a/Game/include/character/states/CharacterStateBase.h b/Game/include/character/states/CharacterStateBase.h index 00d5272..a1ef881 100644 --- a/Game/include/character/states/CharacterStateBase.h +++ b/Game/include/character/states/CharacterStateBase.h @@ -57,9 +57,7 @@ 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 PlayActionById(CharacterObject& owner, const std::string& actionId) const; void InvokeHook(CharacterStateMachine& machine, CharacterObject& owner, const CharacterActionDefinition* action, diff --git a/Game/src/character/CharacterActionLibrary.cpp b/Game/src/character/CharacterActionLibrary.cpp index e669a97..7e78aa0 100644 --- a/Game/src/character/CharacterActionLibrary.cpp +++ b/Game/src/character/CharacterActionLibrary.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace frostbite2D { @@ -227,9 +228,6 @@ void CharacterCommandBuffer::Clear() { bool CharacterActionLibrary::LoadForConfig(const character::CharacterConfig& config) { actions_.clear(); - // 先填一套最小可运行的默认动作,保证资源或 PVF 缺失时角色仍能工作。 - BuildFallbackActions(config); - // 再尝试用 PVF 动作逻辑覆盖默认值。 TryLoadPvfActionScripts(config); return !actions_.empty(); } @@ -247,95 +245,22 @@ const CharacterActionDefinition* CharacterActionLibrary::GetDefaultAction() cons return FindAction("idle"); } -void CharacterActionLibrary::BuildFallbackActions( - const character::CharacterConfig& config) { - auto animationTagOrDefault = [&config](const std::string& actionId, - const std::string& fallback) { - if (config.animationPath.count(actionId) > 0) { - return actionId; +std::string CharacterActionLibrary::DescribeActionIds() const { + if (actions_.empty()) { + return ""; + } + + std::ostringstream stream; + bool first = true; + for (const auto& [actionId, definition] : actions_) { + (void)definition; + if (!first) { + stream << ", "; } - if (config.animationPath.count(fallback) > 0) { - return fallback; - } - if (!config.animationPath.empty()) { - return config.animationPath.begin()->first; - } - return fallback; - }; - - CharacterActionDefinition idle; - idle.actionId = "idle"; - idle.animationTag = animationTagOrDefault("rest", "rest"); - idle.totalFrames = 1; - idle.loop = true; - idle.canMove = true; - addOrReplaceAction(actions_, idle); - - CharacterActionDefinition move; - move.actionId = "move"; - move.animationTag = animationTagOrDefault("run", animationTagOrDefault("walk", "rest")); - move.totalFrames = 1; - move.loop = true; - move.canMove = true; - addOrReplaceAction(actions_, move); - - CharacterActionDefinition jump; - jump.actionId = "jump"; - jump.animationTag = animationTagOrDefault("jump", "rest"); - jump.totalFrames = 18; - jump.canTurn = true; - addOrReplaceAction(actions_, jump); - - CharacterActionDefinition fall; - fall.actionId = "fall"; - fall.animationTag = animationTagOrDefault("jump", "rest"); - fall.totalFrames = 1; - fall.loop = true; - fall.canTurn = true; - addOrReplaceAction(actions_, fall); - - CharacterActionDefinition landing; - landing.actionId = "landing"; - landing.animationTag = animationTagOrDefault("rest", "rest"); - landing.totalFrames = 6; - addOrReplaceAction(actions_, landing); - - CharacterActionDefinition attack1; - attack1.actionId = "attack_1"; - attack1.animationTag = animationTagOrDefault("attack1", animationTagOrDefault("attack", "rest")); - attack1.totalFrames = 24; - attack1.canMove = false; - attack1.canTurn = false; - attack1.canBeInterrupted = true; - attack1.cancelRules.push_back({"attack_2", 12, 22, true, false}); - addOrReplaceAction(actions_, attack1); - - CharacterActionDefinition attack2; - attack2.actionId = "attack_2"; - attack2.animationTag = animationTagOrDefault("attack2", attack1.animationTag); - attack2.totalFrames = 28; - attack2.canMove = false; - attack2.canTurn = false; - attack2.canBeInterrupted = true; - addOrReplaceAction(actions_, attack2); - - CharacterActionDefinition skill1; - skill1.actionId = "skill_1"; - skill1.animationTag = animationTagOrDefault("skill1", attack1.animationTag); - skill1.totalFrames = 36; - skill1.canMove = false; - skill1.canTurn = false; - skill1.canBeInterrupted = false; - addOrReplaceAction(actions_, skill1); - - CharacterActionDefinition hurt; - hurt.actionId = "hurt_light"; - hurt.animationTag = animationTagOrDefault("damage", animationTagOrDefault("hurt", "rest")); - hurt.totalFrames = 18; - hurt.canMove = false; - hurt.canTurn = false; - hurt.canBeInterrupted = true; - addOrReplaceAction(actions_, hurt); + stream << actionId; + first = false; + } + return stream.str(); } bool CharacterActionLibrary::TryLoadPvfActionScripts( @@ -413,12 +338,6 @@ bool CharacterActionLibrary::TryLoadPvfActionScripts( } } - if (definition.animationTag.empty()) { - // 如果动作脚本没有显式指定动画标签,则尝试用同名动作映射。 - if (config.animationPath.count(actionId) > 0) { - definition.animationTag = actionId; - } - } addOrReplaceAction(actions_, std::move(definition)); } diff --git a/Game/src/character/CharacterAnimation.cpp b/Game/src/character/CharacterAnimation.cpp index ce6b8b9..6203507 100644 --- a/Game/src/character/CharacterAnimation.cpp +++ b/Game/src/character/CharacterAnimation.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace frostbite2D { namespace { @@ -51,12 +52,6 @@ bool CharacterAnimation::Init(CharacterObject* parent, return false; } - if (actionAnimations_.count("rest") > 0) { - SetAction("rest"); - } else { - SetAction(actionAnimations_.begin()->first); - } - return true; } @@ -129,13 +124,37 @@ void CharacterAnimation::CreateAnimationBySlot( } } -void CharacterAnimation::SetAction(const std::string& actionName) { +std::string CharacterAnimation::DescribeAvailableActions() const { + if (actionAnimations_.empty()) { + return ""; + } + + std::ostringstream stream; + bool first = true; + for (const auto& [actionName, animations] : actionAnimations_) { + (void)animations; + if (!first) { + stream << ", "; + } + stream << actionName; + first = false; + } + return stream.str(); +} + +bool CharacterAnimation::SetAction(const std::string& actionName) { auto nextIt = actionAnimations_.find(actionName); if (nextIt == actionAnimations_.end()) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "CharacterAnimation: action %s missing, keep %s", actionName.c_str(), - currentActionTag_.c_str()); - return; + if (parent_) { + parent_->ReportFatalCharacterError("CharacterAnimation::SetAction", + "requested animation tag is not loaded", + std::string(), actionName); + } else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "CharacterAnimation: action %s missing and parent is null", + actionName.c_str()); + } + return false; } if (!currentActionTag_.empty()) { @@ -154,6 +173,7 @@ void CharacterAnimation::SetAction(const std::string& actionName) { } currentActionTag_ = actionName; SetDirection(direction_); + return true; } void CharacterAnimation::SetDirection(int direction) { diff --git a/Game/src/character/CharacterInputRouter.cpp b/Game/src/character/CharacterInputRouter.cpp index d28a515..c5e5117 100644 --- a/Game/src/character/CharacterInputRouter.cpp +++ b/Game/src/character/CharacterInputRouter.cpp @@ -46,8 +46,8 @@ bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) { } } -bool CharacterInputRouter::OnKeyUp(KeyCode keyCode) { - switch (keyCode) { +bool CharacterInputRouter::OnKeyUp(const KeyUpEvent& event) { + switch (event.getKeyCode()) { case KeyCode::a: case KeyCode::Left: left_ = false; diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 95cc95b..4f2f6d1 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -1,9 +1,42 @@ -#include "character/CharacterObject.h" +#include "character/CharacterObject.h" #include +#include +#include namespace frostbite2D { +namespace { + +const char* StateIdToString(CharacterStateId stateId) { + switch (stateId) { + case CharacterStateId::Idle: + return "Idle"; + case CharacterStateId::Move: + return "Move"; + case CharacterStateId::Jump: + return "Jump"; + case CharacterStateId::Fall: + return "Fall"; + case CharacterStateId::Landing: + return "Landing"; + case CharacterStateId::Action: + return "Action"; + case CharacterStateId::Hurt: + return "Hurt"; + case CharacterStateId::Dead: + return "Dead"; + default: + return ""; + } +} + +const char* NonEmptyOrPlaceholder(const std::string& value, const char* placeholder) { + return value.empty() ? placeholder : value.c_str(); +} + +} // namespace bool CharacterObject::Construction(int jobId) { + // Reset all runtime state before rebuilding the character from config. EnableEventReceive(); RemoveAllChildren(); animationManager_ = nullptr; @@ -16,6 +49,7 @@ bool CharacterObject::Construction(int jobId) { motor_ = CharacterMotor(); status_ = CharacterStatus(); lastDeltaTime_ = 0.0f; + inputEnabled_ = true; auto config = character::loadCharacterConfig(jobId); if (!config) { @@ -36,17 +70,19 @@ bool CharacterObject::Construction(int jobId) { animationManager_ = MakePtr(); if (!animationManager_->Init(this, *config_, equipmentManager_)) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "CharacterObject: no layered avatar animations available, character will stay empty"); + ReportFatalCharacterError("CharacterObject::Construction", + "no usable animation tags were loaded"); + return false; } AddChild(animationManager_); - if (const auto* idleAction = actionLibrary_.GetDefaultAction()) { - PlayAnimationTag(idleAction->animationTag); - } else if (config_->animationPath.count("rest") > 0) { - SetAction("rest"); - } else if (!config_->animationPath.empty()) { - SetAction(config_->animationPath.begin()->first); + const CharacterActionDefinition* idleAction = RequireAction( + "idle", "CharacterObject::Construction"); + if (!idleAction) { + return false; + } + if (!PlayActionDefinition(*idleAction, "CharacterObject::Construction")) { + return false; } SetWorldPosition({}); @@ -55,10 +91,35 @@ bool CharacterObject::Construction(int jobId) { } void CharacterObject::SetAction(const std::string& actionName) { - currentAction_ = actionName; - if (animationManager_) { - animationManager_->SetAction(actionName); + SetActionStrict(actionName, "CharacterObject::SetAction", std::string()); +} + +bool CharacterObject::SetActionStrict(const std::string& actionName, + const char* phase, + const std::string& requestedActionId) { + if (actionName.empty()) { + ReportFatalCharacterError(phase, "requested animation tag is empty", + requestedActionId, actionName); + return false; } + if (!animationManager_) { + ReportFatalCharacterError(phase, "animation manager is not initialized", + requestedActionId, actionName); + return false; + } + if (!animationManager_->HasAction(actionName)) { + ReportFatalCharacterError(phase, "requested animation tag is not loaded", + requestedActionId, actionName); + return false; + } + if (!animationManager_->SetAction(actionName)) { + ReportFatalCharacterError(phase, "failed to activate requested animation tag", + requestedActionId, actionName); + return false; + } + + currentAction_ = actionName; + return true; } void CharacterObject::SetDirection(int direction) { @@ -75,9 +136,7 @@ void CharacterObject::SetCharacterPosition(const Vec2& pos) { void CharacterObject::SetWorldPosition(const CharacterWorldPosition& pos) { motor_.position = pos; - // 逻辑世界坐标是 x/y/z,真正渲染时再投影到屏幕坐标。 SetPosition(pos.ToScreenPosition()); - // 地面 y 继续决定同层对象前后遮挡顺序。 SetZOrder(static_cast(pos.y)); } @@ -96,9 +155,10 @@ void CharacterObject::ApplyHit(const HitContext& hit) { return; } - const CharacterActionDefinition* hurtAction = FindAction(hit.hurtAction); + const CharacterActionDefinition* hurtAction = RequireAction( + hit.hurtAction, "CharacterObject::ApplyHit"); if (!hurtAction) { - hurtAction = FindAction("hurt_light"); + return; } motor_.verticalVelocity = hit.launchZVelocity; @@ -109,8 +169,7 @@ void CharacterObject::ApplyHit(const HitContext& hit) { } void CharacterObject::OnUpdate(float deltaTime) { - // 角色主循环顺序固定为: - // 输入采样 -> 缓冲推进 -> 意图生成 -> 状态机决策 -> motor 推进 -> 投影到屏幕。 + // Fixed update order keeps input, state transitions, and motion deterministic. lastDeltaTime_ = deltaTime; commandBuffer_.Advance(deltaTime); inputRouter_.EmitCommands(commandBuffer_); @@ -121,29 +180,32 @@ void CharacterObject::OnUpdate(float deltaTime) { SetFacing(motor_.facing); } -bool CharacterObject::OnKeyDown(const KeyEvent& event) { - if (event.isRepeat()) { +bool CharacterObject::OnEvent(const Event& event) { + if (!IsEventReceiveEnabled() || !inputEnabled_) { return false; } - return inputRouter_.OnKeyDown(event); -} -bool CharacterObject::OnKeyUp(const KeyEvent& event) { - return inputRouter_.OnKeyUp(event.getKeyCode()); -} - -bool CharacterObject::OnJoystickAxis(const JoystickEvent& event) { - return inputRouter_.OnJoystickAxis(static_cast(event)); -} - -bool CharacterObject::OnJoystickButtonDown(const JoystickEvent& event) { - return inputRouter_.OnJoystickButtonDown( - static_cast(event)); -} - -bool CharacterObject::OnJoystickButtonUp(const JoystickEvent& event) { - return inputRouter_.OnJoystickButtonUp( - static_cast(event)); + switch (event.getType()) { + case EventType::KeyDown: { + const auto& keyEvent = static_cast(event); + if (keyEvent.isRepeat()) { + return false; + } + return inputRouter_.OnKeyDown(keyEvent); + } + case EventType::KeyUp: + return inputRouter_.OnKeyUp(static_cast(event)); + case EventType::JoystickAxis: + return inputRouter_.OnJoystickAxis(static_cast(event)); + case EventType::JoystickButtonDown: + return inputRouter_.OnJoystickButtonDown( + static_cast(event)); + case EventType::JoystickButtonUp: + return inputRouter_.OnJoystickButtonUp( + static_cast(event)); + default: + return false; + } } const CharacterActionDefinition* CharacterObject::FindAction( @@ -151,24 +213,78 @@ const CharacterActionDefinition* CharacterObject::FindAction( return actionLibrary_.FindAction(actionId); } -std::string CharacterObject::ResolveAnimationTag( - const std::string& preferred, - const std::string& fallback) const { - // 逻辑动作与资源动作是两层名字:优先选动作定义里的标签,不存在再回退。 - if (animationManager_ && animationManager_->HasAction(preferred)) { - return preferred; +const CharacterActionDefinition* CharacterObject::RequireAction( + const std::string& actionId, + const char* phase) const { + const CharacterActionDefinition* action = FindAction(actionId); + if (!action) { + ReportFatalCharacterError(phase, "requested logical action does not exist", actionId); + return nullptr; } - if (animationManager_ && animationManager_->HasAction(fallback)) { - return fallback; + return action; +} + +bool CharacterObject::PlayActionDefinition(const CharacterActionDefinition& action, + const char* phase) { + if (action.animationTag.empty()) { + ReportFatalCharacterError(phase, "logical action has empty animationTag", + action.actionId, action.animationTag); + return false; } - if (config_ && !config_->animationPath.empty()) { - return config_->animationPath.begin()->first; - } - return preferred; + return SetActionStrict(action.animationTag, phase, action.actionId); } void CharacterObject::PlayAnimationTag(const std::string& actionTag) { - SetAction(actionTag); + SetActionStrict(actionTag, "CharacterObject::PlayAnimationTag", std::string()); +} + +std::string CharacterObject::DescribeConfiguredAnimationTags() const { + if (!config_ || config_->animationPath.empty()) { + return ""; + } + + std::ostringstream stream; + bool first = true; + for (const auto& [actionTag, path] : config_->animationPath) { + (void)path; + if (!first) { + stream << ", "; + } + stream << actionTag; + first = false; + } + return stream.str(); +} + +void CharacterObject::ReportFatalCharacterError( + const char* phase, + const char* reason, + const std::string& requestedActionId, + const std::string& requestedAnimationTag) const { + std::ostringstream stream; + stream << "CharacterObject fatal error: " + << (reason && reason[0] != '\0' ? reason : "unknown error") << '\n' + << " phase: " << (phase && phase[0] != '\0' ? phase : "") << '\n' + << " jobId: " << jobId_ << '\n' + << " jobTag: " + << (config_ ? NonEmptyOrPlaceholder(config_->jobTag, "") : "") + << '\n' + << " state: " << StateIdToString(stateMachine_.GetState()) << '\n' + << " currentActionId: " + << NonEmptyOrPlaceholder(stateMachine_.GetCurrentActionId(), "") << '\n' + << " currentAnimationTag: " << NonEmptyOrPlaceholder(currentAction_, "") << '\n' + << " requestedActionId: " + << NonEmptyOrPlaceholder(requestedActionId, "") << '\n' + << " requestedAnimationTag: " + << NonEmptyOrPlaceholder(requestedAnimationTag, "") << '\n' + << " loadedActionIds: " << actionLibrary_.DescribeActionIds() << '\n' + << " configuredAnimationTags: " << DescribeConfiguredAnimationTags() << '\n' + << " loadedAnimationTags: " + << (animationManager_ ? animationManager_->DescribeAvailableActions() + : std::string("")); + + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "%s", stream.str().c_str()); + Application::get().quit(); } void CharacterObject::RunStateHook(const std::string& phase, @@ -177,7 +293,6 @@ void CharacterObject::RunStateHook(const std::string& phase, (void)phase; (void)stateId; (void)action; - // 预留给后续脚本系统;当前默认由 C++ 状态节点接管生命周期逻辑。 } void CharacterObject::SetFacing(int direction) { diff --git a/Game/src/character/CharacterStateMachine.cpp b/Game/src/character/CharacterStateMachine.cpp index 7bcd91a..0a252d1 100644 --- a/Game/src/character/CharacterStateMachine.cpp +++ b/Game/src/character/CharacterStateMachine.cpp @@ -35,9 +35,15 @@ void CharacterStateMachine::Update(CharacterObject& owner, void CharacterStateMachine::ForceHurt(CharacterObject& owner, const CharacterActionDefinition* hurtAction) { + if (!hurtAction) { + owner.ReportFatalCharacterError("CharacterStateMachine::ForceHurt", + "hurt action definition is null"); + return; + } + const CharacterActionDefinition* previousAction = currentAction_; currentAction_ = hurtAction; - currentActionId_ = hurtAction ? hurtAction->actionId : std::string("hurt_light"); + currentActionId_ = hurtAction->actionId; if (currentState_ == CharacterStateId::Hurt) { InvokeStateHook(owner, previousAction, "on_exit"); stateTime_ = 0.0f; @@ -123,9 +129,15 @@ void CharacterStateMachine::ChangeState(CharacterObject& owner, void CharacterStateMachine::EnterAction(CharacterObject& owner, const CharacterActionDefinition* action) { + if (!action) { + owner.ReportFatalCharacterError("CharacterStateMachine::EnterAction", + "action definition is null"); + return; + } + const CharacterActionDefinition* previousAction = currentAction_; currentAction_ = action; - currentActionId_ = action ? action->actionId : std::string(); + currentActionId_ = action->actionId; if (currentState_ == CharacterStateId::Action) { InvokeStateHook(owner, previousAction, "on_exit"); stateTime_ = 0.0f; @@ -158,7 +170,8 @@ bool CharacterStateMachine::TryStartAction(CharacterObject& owner, return false; } - const CharacterActionDefinition* action = owner.FindAction(actionId); + const CharacterActionDefinition* action = owner.RequireAction( + actionId, "CharacterStateMachine::TryStartAction"); commandBuffer.Consume(commandType); if (!action) { return false; diff --git a/Game/src/character/states/CharacterStateBase.cpp b/Game/src/character/states/CharacterStateBase.cpp index 95a1f86..68eadbf 100644 --- a/Game/src/character/states/CharacterStateBase.cpp +++ b/Game/src/character/states/CharacterStateBase.cpp @@ -19,16 +19,12 @@ void CharacterStateBase::ChangeState(CharacterStateMachine& machine, } 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; - } + const std::string& actionId) const { + const auto* action = owner.RequireAction(actionId, "CharacterStateBase::PlayActionById"); + if (!action) { + return; } - - owner.PlayAnimationTag(owner.ResolveAnimationTag(actionId, fallbackTag)); + owner.PlayActionDefinition(*action, "CharacterStateBase::PlayActionById"); } void CharacterStateBase::InvokeHook(CharacterStateMachine& machine, diff --git a/Game/src/character/states/common/HurtState.cpp b/Game/src/character/states/common/HurtState.cpp index dccaf0e..497ab6c 100644 --- a/Game/src/character/states/common/HurtState.cpp +++ b/Game/src/character/states/common/HurtState.cpp @@ -30,13 +30,15 @@ void HurtState::OnEnter(CharacterStateMachine& machine, ActionFrame(machine) = 0; const CharacterActionDefinition* action = GetCurrentAction(machine); if (!action) { - action = context.owner.FindAction("hurt_light"); - SetCurrentAction(machine, action, action ? action->actionId : "hurt_light"); + context.owner.ReportFatalCharacterError("HurtState::OnEnter", + "hurt state entered without current action"); + return; } - if (action) { - context.owner.PlayAnimationTag(action->animationTag); - InvokeHook(machine, context.owner, action, "on_enter"); + + if (!context.owner.PlayActionDefinition(*action, "HurtState::OnEnter")) { + return; } + InvokeHook(machine, context.owner, action, "on_enter"); } void HurtState::OnUpdate(CharacterStateMachine& machine, @@ -47,12 +49,8 @@ void HurtState::OnUpdate(CharacterStateMachine& machine, 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)); + owner.ReportFatalCharacterError("HurtState::OnUpdate", + "hurt state updated without current action"); return; } diff --git a/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp b/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp index 4141ce8..0581385 100644 --- a/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp +++ b/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp @@ -38,10 +38,16 @@ void SwordmanAttackState::OnEnter(CharacterStateMachine& machine, 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"); + if (!action) { + context.owner.ReportFatalCharacterError("SwordmanAttackState::OnEnter", + "action state entered without current action"); + return; } + + if (!context.owner.PlayActionDefinition(*action, "SwordmanAttackState::OnEnter")) { + return; + } + InvokeHook(machine, context.owner, action, "on_enter"); } void SwordmanAttackState::OnUpdate(CharacterStateMachine& machine,