feat(character): 实现角色状态机与输入路由系统

新增角色状态机框架,包含空闲、移动、跳跃、攻击等状态
添加输入路由组件,统一处理键盘和手柄输入
引入动作库管理角色动作定义,支持PVF脚本配置
重构角色对象,整合运动器、状态机和输入处理
修复Actor子节点排序时的迭代安全问题
This commit is contained in:
2026-04-03 08:08:23 +08:00
parent b5c432e77a
commit bb75a57afb
14 changed files with 1463 additions and 6 deletions

View File

@@ -4,10 +4,18 @@
namespace frostbite2D {
bool CharacterObject::Construction(int jobId) {
EnableEventReceive();
RemoveAllChildren();
animationManager_ = nullptr;
config_.reset();
currentAction_.clear();
actionLibrary_ = CharacterActionLibrary();
commandBuffer_.Clear();
currentIntent_ = CharacterIntent();
stateMachine_.Reset();
motor_ = CharacterMotor();
status_ = CharacterStatus();
lastDeltaTime_ = 0.0f;
auto config = character::loadCharacterConfig(jobId);
if (!config) {
@@ -21,6 +29,9 @@ bool CharacterObject::Construction(int jobId) {
direction_ = 1;
config_ = *config;
equipmentManager_.Init(config_->baseJobConfig);
if (auto actionLibrary = loadCharacterActionLibrary(*config_)) {
actionLibrary_ = *actionLibrary;
}
animationManager_ = MakePtr<CharacterAnimation>();
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
@@ -29,13 +40,16 @@ bool CharacterObject::Construction(int jobId) {
}
AddChild(animationManager_);
if (config_->animationPath.count("rest") > 0) {
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);
}
SetDirection(1);
SetWorldPosition({});
SetFacing(1);
return true;
}
@@ -54,8 +68,113 @@ void CharacterObject::SetDirection(int direction) {
}
void CharacterObject::SetCharacterPosition(const Vec2& pos) {
SetPosition(pos);
motor_.SetGroundPosition(pos);
SetWorldPosition(motor_.position);
}
void CharacterObject::SetWorldPosition(const CharacterWorldPosition& pos) {
motor_.position = pos;
// 逻辑世界坐标是 x/y/z真正渲染时再投影到屏幕坐标。
SetPosition(pos.ToScreenPosition());
// 地面 y 继续决定同层对象前后遮挡顺序。
SetZOrder(static_cast<int>(pos.y));
}
void CharacterObject::PushCommand(const CharacterCommand& command) {
commandBuffer_.Submit(command);
}
void CharacterObject::ApplyHit(const HitContext& hit) {
if (status_.invincible) {
return;
}
if (status_.superArmor && !hit.ignoreInterrupt) {
return;
}
if (!hit.ignoreInterrupt && !stateMachine_.CanBeInterrupted()) {
return;
}
const CharacterActionDefinition* hurtAction = FindAction(hit.hurtAction);
if (!hurtAction) {
hurtAction = FindAction("hurt_light");
}
motor_.verticalVelocity = hit.launchZVelocity;
if (hit.launchZVelocity > 0.0f) {
motor_.grounded = false;
}
stateMachine_.ForceHurt(hurtAction);
PlayAnimationTag(hurtAction ? hurtAction->animationTag
: ResolveAnimationTag("damage", "rest"));
}
void CharacterObject::OnUpdate(float deltaTime) {
// 角色主循环顺序固定为:
// 输入采样 -> 缓冲推进 -> 意图生成 -> 状态机决策 -> motor 推进 -> 投影到屏幕。
lastDeltaTime_ = deltaTime;
commandBuffer_.Advance(deltaTime);
inputRouter_.EmitCommands(commandBuffer_);
currentIntent_ = commandBuffer_.BuildIntent();
stateMachine_.Update(*this, commandBuffer_, currentIntent_, deltaTime);
motor_.Update(deltaTime);
SetWorldPosition(motor_.position);
SetFacing(motor_.facing);
}
bool CharacterObject::OnKeyDown(const KeyEvent& event) {
if (event.isRepeat()) {
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));
}
const CharacterActionDefinition* CharacterObject::FindAction(
const std::string& actionId) const {
return actionLibrary_.FindAction(actionId);
}
std::string CharacterObject::ResolveAnimationTag(
const std::string& preferred,
const std::string& fallback) const {
// 逻辑动作与资源动作是两层名字:优先选动作定义里的标签,不存在再回退。
if (animationManager_ && animationManager_->HasAction(preferred)) {
return preferred;
}
if (animationManager_ && animationManager_->HasAction(fallback)) {
return fallback;
}
if (config_ && !config_->animationPath.empty()) {
return config_->animationPath.begin()->first;
}
return preferred;
}
void CharacterObject::PlayAnimationTag(const std::string& actionTag) {
SetAction(actionTag);
}
void CharacterObject::SetFacing(int direction) {
motor_.facing = direction >= 0 ? 1 : -1;
SetDirection(motor_.facing);
}
} // namespace frostbite2D