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

@@ -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<std::string, CharacterActionDefinition> actions_;

View File

@@ -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);

View File

@@ -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);

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

View File

@@ -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,

View File

@@ -4,6 +4,7 @@
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <sstream>
#include <vector>
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 "<none>";
}
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));
}

View File

@@ -3,6 +3,7 @@
#include <SDL2/SDL.h>
#include <array>
#include <cstdio>
#include <sstream>
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 "<none>";
}
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) {

View File

@@ -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;

View File

@@ -1,9 +1,42 @@
#include "character/CharacterObject.h"
#include "character/CharacterObject.h"
#include <SDL2/SDL.h>
#include <frostbite2D/core/application.h>
#include <sstream>
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 "<unknown>";
}
}
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<CharacterAnimation>();
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<int>(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<const JoystickAxisEvent&>(event));
}
bool CharacterObject::OnJoystickButtonDown(const JoystickEvent& event) {
return inputRouter_.OnJoystickButtonDown(
static_cast<const JoystickButtonDownEvent&>(event));
}
bool CharacterObject::OnJoystickButtonUp(const JoystickEvent& event) {
return inputRouter_.OnJoystickButtonUp(
static_cast<const JoystickButtonUpEvent&>(event));
switch (event.getType()) {
case EventType::KeyDown: {
const auto& keyEvent = static_cast<const KeyEvent&>(event);
if (keyEvent.isRepeat()) {
return false;
}
return inputRouter_.OnKeyDown(keyEvent);
}
case EventType::KeyUp:
return inputRouter_.OnKeyUp(static_cast<const KeyUpEvent&>(event));
case EventType::JoystickAxis:
return inputRouter_.OnJoystickAxis(static_cast<const JoystickAxisEvent&>(event));
case EventType::JoystickButtonDown:
return inputRouter_.OnJoystickButtonDown(
static_cast<const JoystickButtonDownEvent&>(event));
case EventType::JoystickButtonUp:
return inputRouter_.OnJoystickButtonUp(
static_cast<const JoystickButtonUpEvent&>(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 "<none>";
}
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 : "<unknown>") << '\n'
<< " jobId: " << jobId_ << '\n'
<< " jobTag: "
<< (config_ ? NonEmptyOrPlaceholder(config_->jobTag, "<empty>") : "<config not loaded>")
<< '\n'
<< " state: " << StateIdToString(stateMachine_.GetState()) << '\n'
<< " currentActionId: "
<< NonEmptyOrPlaceholder(stateMachine_.GetCurrentActionId(), "<none>") << '\n'
<< " currentAnimationTag: " << NonEmptyOrPlaceholder(currentAction_, "<none>") << '\n'
<< " requestedActionId: "
<< NonEmptyOrPlaceholder(requestedActionId, "<none>") << '\n'
<< " requestedAnimationTag: "
<< NonEmptyOrPlaceholder(requestedAnimationTag, "<empty>") << '\n'
<< " loadedActionIds: " << actionLibrary_.DescribeActionIds() << '\n'
<< " configuredAnimationTags: " << DescribeConfiguredAnimationTags() << '\n'
<< " loadedAnimationTags: "
<< (animationManager_ ? animationManager_->DescribeAvailableActions()
: std::string("<animation manager not initialized>"));
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) {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,