refactor(character): 重构角色动作与动画系统
- 移除自动回退动作生成逻辑,改为严格检查动作定义 - 增加动作资源缺失时的详细错误报告机制 - 统一输入事件处理接口,优化角色对象生命周期管理 - 改进动画标签管理,移除隐式回退逻辑 - 增强状态机对无效动作的处理能力
This commit is contained in:
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user