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

@@ -0,0 +1,442 @@
#include "character/CharacterActionLibrary.h"
#include <SDL2/SDL.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <vector>
namespace frostbite2D {
namespace {
std::string toLower(const std::string& value) {
std::string result = value;
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return result;
}
int toInt(const std::string& value, int fallback = 0) {
try {
return std::stoi(value);
} catch (...) {
return fallback;
}
}
float toFloat(const std::string& value, float fallback = 0.0f) {
try {
return std::stof(value);
} catch (...) {
return fallback;
}
}
bool isTrueToken(const std::string& value) {
std::string token = toLower(value);
return token == "1" || token == "true" || token == "[true]" || token == "yes";
}
std::string trim(const std::string& value) {
size_t begin = 0;
size_t end = value.size();
while (begin < end && std::isspace(static_cast<unsigned char>(value[begin]))) {
++begin;
}
while (end > begin
&& std::isspace(static_cast<unsigned char>(value[end - 1]))) {
--end;
}
return value.substr(begin, end - begin);
}
std::string trimBackticks(const std::string& value) {
std::string result = trim(value);
if (result.size() >= 2 && result.front() == '`' && result.back() == '`') {
return result.substr(1, result.size() - 2);
}
return result;
}
std::string fileStem(const std::string& path) {
size_t slashPos = path.find_last_of("/\\");
size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1;
size_t dotPos = path.find_last_of('.');
if (dotPos == std::string::npos || dotPos < begin) {
return path.substr(begin);
}
return path.substr(begin, dotPos - begin);
}
class ScriptTokenStream {
public:
explicit ScriptTokenStream(const std::string& path)
: path_(PvfArchive::get().normalizePath(path)) {
auto rawData = PvfArchive::get().getFileRawData(path_);
if (!rawData) {
return;
}
ScriptParser parser(*rawData, path_);
if (!parser.isValid()) {
return;
}
for (const auto& value : parser.parseAll()) {
tokens_.push_back(value.toString());
}
valid_ = true;
}
bool IsValid() const { return valid_; }
bool IsEnd() const { return index_ >= tokens_.size(); }
std::string Next() {
if (IsEnd()) {
return {};
}
return tokens_[index_++];
}
private:
std::string path_;
std::vector<std::string> tokens_;
size_t index_ = 0;
bool valid_ = false;
};
// action_list.lst 同时兼容两种格式:
// 1. 标准 PVF #PVF_File + 索引 + `path`
// 2. 早期临时版的 actionId + path 成对写法
std::vector<std::pair<std::string, std::string>> parseActionListEntries(
ScriptTokenStream& listStream) {
std::vector<std::pair<std::string, std::string>> entries;
std::vector<std::string> tokens;
while (!listStream.IsEnd()) {
std::string token = trim(listStream.Next());
if (!token.empty()) {
tokens.push_back(token);
}
}
if (tokens.empty()) {
return entries;
}
if (tokens.front() == "#PVF_File") {
for (size_t i = 1; i + 1 < tokens.size(); i += 2) {
std::string actionPath = trimBackticks(tokens[i + 1]);
if (actionPath.empty()) {
continue;
}
entries.push_back({toLower(fileStem(actionPath)), actionPath});
}
return entries;
}
for (size_t i = 0; i + 1 < tokens.size(); i += 2) {
entries.push_back({toLower(tokens[i]), trimBackticks(tokens[i + 1])});
}
return entries;
}
CharacterFrameEventType parseFrameEventType(const std::string& token) {
std::string lower = toLower(token);
if (lower == "open_hit_window") {
return CharacterFrameEventType::OpenHitWindow;
}
if (lower == "close_hit_window") {
return CharacterFrameEventType::CloseHitWindow;
}
if (lower == "open_input_buffer") {
return CharacterFrameEventType::OpenInputBuffer;
}
if (lower == "close_input_buffer") {
return CharacterFrameEventType::CloseInputBuffer;
}
if (lower == "set_velocity_xy") {
return CharacterFrameEventType::SetVelocityXY;
}
if (lower == "set_velocity_z") {
return CharacterFrameEventType::SetVelocityZ;
}
if (lower == "finish_action") {
return CharacterFrameEventType::FinishAction;
}
return CharacterFrameEventType::AnimationEvent;
}
void addOrReplaceAction(std::map<std::string, CharacterActionDefinition>& actions,
CharacterActionDefinition definition) {
if (definition.actionId.empty()) {
return;
}
actions[definition.actionId] = std::move(definition);
}
} // namespace
void CharacterCommandBuffer::Advance(float deltaTime) {
for (auto& button : buttons_) {
button.age += deltaTime;
}
buttons_.erase(
std::remove_if(buttons_.begin(), buttons_.end(),
[this](const BufferedButton& button) {
return button.age > bufferWindowSeconds_;
}),
buttons_.end());
}
void CharacterCommandBuffer::Submit(const CharacterCommand& command) {
if (command.type == CharacterCommandType::Move) {
moveAxis_ = command.moveAxis;
if (moveAxis_.lengthSquared() > 1.0f) {
moveAxis_ = moveAxis_.normalized();
}
return;
}
buttons_.push_back({command.type, 0.0f});
}
CharacterIntent CharacterCommandBuffer::BuildIntent() const {
CharacterIntent intent;
intent.moveX = moveAxis_.x;
intent.moveY = moveAxis_.y;
intent.wantJump = HasBuffered(CharacterCommandType::JumpPressed);
intent.wantAttack = HasBuffered(CharacterCommandType::AttackPressed);
intent.wantSkill1 = HasBuffered(CharacterCommandType::Skill1Pressed);
intent.wantDash = HasBuffered(CharacterCommandType::DashPressed);
return intent;
}
bool CharacterCommandBuffer::HasBuffered(CharacterCommandType type) const {
for (const auto& button : buttons_) {
if (button.type == type) {
return true;
}
}
return false;
}
void CharacterCommandBuffer::Consume(CharacterCommandType type) {
auto it = std::find_if(buttons_.begin(), buttons_.end(),
[type](const BufferedButton& button) {
return button.type == type;
});
if (it != buttons_.end()) {
buttons_.erase(it);
}
}
void CharacterCommandBuffer::Clear() {
moveAxis_ = Vec2::Zero();
buttons_.clear();
}
bool CharacterActionLibrary::LoadForConfig(const character::CharacterConfig& config) {
actions_.clear();
// 先填一套最小可运行的默认动作,保证资源或 PVF 缺失时角色仍能工作。
BuildFallbackActions(config);
// 再尝试用 PVF 动作逻辑覆盖默认值。
TryLoadPvfActionScripts(config);
return !actions_.empty();
}
const CharacterActionDefinition* CharacterActionLibrary::FindAction(
const std::string& actionId) const {
auto it = actions_.find(actionId);
if (it != actions_.end()) {
return &it->second;
}
return nullptr;
}
const CharacterActionDefinition* CharacterActionLibrary::GetDefaultAction() const {
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;
}
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);
}
bool CharacterActionLibrary::TryLoadPvfActionScripts(
const character::CharacterConfig& config) {
std::string listPath = "character/" + config.jobTag + "/action_logic/action_list.lst";
ScriptTokenStream listStream(listPath);
if (!listStream.IsValid()) {
return false;
}
auto entries = parseActionListEntries(listStream);
for (const auto& [actionId, relativePath] : entries) {
if (actionId.empty() || relativePath.empty()) {
continue;
}
ScriptTokenStream actionStream(
"character/" + config.jobTag + "/action_logic/" + relativePath);
if (!actionStream.IsValid()) {
continue;
}
CharacterActionDefinition definition;
definition.actionId = actionId;
while (!actionStream.IsEnd()) {
std::string token = toLower(actionStream.Next());
if (token.empty()) {
break;
}
if (token == "[action id]") {
definition.actionId = toLower(actionStream.Next());
} else if (token == "[animation tag]") {
definition.animationTag = actionStream.Next();
} else if (token == "[total frames]") {
definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames);
} else if (token == "[loop]") {
definition.loop = isTrueToken(actionStream.Next());
} else if (token == "[can move]") {
definition.canMove = isTrueToken(actionStream.Next());
} else if (token == "[can turn]") {
definition.canTurn = isTrueToken(actionStream.Next());
} else if (token == "[can be interrupted]") {
definition.canBeInterrupted = isTrueToken(actionStream.Next());
} else if (token == "[move speed scale]") {
definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale);
} else if (token == "[cancel rule]") {
CharacterCancelRuleDefinition rule;
rule.targetAction = toLower(actionStream.Next());
rule.beginFrame = toInt(actionStream.Next(), 0);
rule.endFrame = toInt(actionStream.Next(), rule.beginFrame);
rule.requireGrounded = isTrueToken(actionStream.Next());
rule.requireAirborne = isTrueToken(actionStream.Next());
definition.cancelRules.push_back(rule);
} else if (token == "[frame event]") {
CharacterFrameEventDefinition frameEvent;
frameEvent.frame = toInt(actionStream.Next(), 0);
frameEvent.type = parseFrameEventType(actionStream.Next());
if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) {
frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f);
frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f);
} else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) {
frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f);
} else {
frameEvent.stringValue = actionStream.Next();
}
definition.frameEvents.push_back(frameEvent);
}
}
if (definition.animationTag.empty()) {
// 如果动作脚本没有显式指定动画标签,则尝试用同名动作映射。
if (config.animationPath.count(actionId) > 0) {
definition.animationTag = actionId;
}
}
addOrReplaceAction(actions_, std::move(definition));
}
SDL_Log("CharacterActionLibrary: loaded %d actions for job %d",
static_cast<int>(actions_.size()), config.jobId);
return true;
}
std::optional<CharacterActionLibrary> loadCharacterActionLibrary(
const character::CharacterConfig& config) {
CharacterActionLibrary library;
if (!library.LoadForConfig(config)) {
return std::nullopt;
}
return library;
}
} // namespace frostbite2D

View File

@@ -0,0 +1,142 @@
#include "character/CharacterInputRouter.h"
#include <SDL2/SDL_gamecontroller.h>
#include <cmath>
namespace frostbite2D {
namespace {
float applyDeadZone(float value, float deadZone) {
return std::fabs(value) >= deadZone ? value : 0.0f;
}
} // namespace
bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) {
switch (event.getKeyCode()) {
case KeyCode::a:
case KeyCode::Left:
left_ = true;
return true;
case KeyCode::d:
case KeyCode::Right:
right_ = true;
return true;
case KeyCode::w:
case KeyCode::Up:
up_ = true;
return true;
case KeyCode::s:
case KeyCode::Down:
down_ = true;
return true;
case KeyCode::j:
attackPressed_ = true;
return true;
case KeyCode::k:
jumpPressed_ = true;
return true;
case KeyCode::l:
skill1Pressed_ = true;
return true;
case KeyCode::LShift:
dashPressed_ = true;
return true;
default:
return false;
}
}
bool CharacterInputRouter::OnKeyUp(KeyCode keyCode) {
switch (keyCode) {
case KeyCode::a:
case KeyCode::Left:
left_ = false;
return true;
case KeyCode::d:
case KeyCode::Right:
right_ = false;
return true;
case KeyCode::w:
case KeyCode::Up:
up_ = false;
return true;
case KeyCode::s:
case KeyCode::Down:
down_ = false;
return true;
default:
return false;
}
}
bool CharacterInputRouter::OnJoystickAxis(const JoystickAxisEvent& event) {
if (event.getAxis() == SDL_CONTROLLER_AXIS_LEFTX) {
leftStickX_ = applyDeadZone(event.getNormalizedValue(), joystickDeadZone_);
return true;
}
if (event.getAxis() == SDL_CONTROLLER_AXIS_LEFTY) {
leftStickY_ = applyDeadZone(event.getNormalizedValue(), joystickDeadZone_);
return true;
}
return false;
}
bool CharacterInputRouter::OnJoystickButtonDown(
const JoystickButtonDownEvent& event) {
switch (event.getButton()) {
case SDL_CONTROLLER_BUTTON_A:
jumpPressed_ = true;
return true;
case SDL_CONTROLLER_BUTTON_X:
attackPressed_ = true;
return true;
case SDL_CONTROLLER_BUTTON_B:
case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
skill1Pressed_ = true;
return true;
case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
dashPressed_ = true;
return true;
default:
return false;
}
}
bool CharacterInputRouter::OnJoystickButtonUp(
const JoystickButtonUpEvent& event) {
(void)event;
return false;
}
void CharacterInputRouter::EmitCommands(CharacterCommandBuffer& commandBuffer) {
Vec2 moveAxis(leftStickX_, leftStickY_);
if (moveAxis.lengthSquared() < 0.0001f) {
moveAxis.x = (right_ ? 1.0f : 0.0f) - (left_ ? 1.0f : 0.0f);
moveAxis.y = (down_ ? 1.0f : 0.0f) - (up_ ? 1.0f : 0.0f);
}
commandBuffer.Submit({CharacterCommandType::Move, moveAxis, 0.0f});
if (jumpPressed_) {
commandBuffer.Submit({CharacterCommandType::JumpPressed, Vec2::Zero(), 0.0f});
}
if (attackPressed_) {
commandBuffer.Submit({CharacterCommandType::AttackPressed, Vec2::Zero(), 0.0f});
}
if (skill1Pressed_) {
commandBuffer.Submit({CharacterCommandType::Skill1Pressed, Vec2::Zero(), 0.0f});
}
if (dashPressed_) {
commandBuffer.Submit({CharacterCommandType::DashPressed, Vec2::Zero(), 0.0f});
}
ClearFrameState();
}
void CharacterInputRouter::ClearFrameState() {
jumpPressed_ = false;
attackPressed_ = false;
skill1Pressed_ = false;
dashPressed_ = false;
}
} // namespace frostbite2D

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

View File

@@ -0,0 +1,333 @@
#include "character/CharacterStateMachine.h"
#include "character/CharacterObject.h"
#include <cmath>
namespace frostbite2D {
namespace {
constexpr float kLogicFramesPerSecond = 60.0f;
constexpr float kLandingDuration = 0.08f;
constexpr float kMovementActionScale = 0.35f;
bool hasMoveIntent(const CharacterIntent& intent) {
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
}
} // namespace
void CharacterStateMachine::Reset() {
currentState_ = CharacterStateId::Idle;
currentActionId_.clear();
currentAction_ = nullptr;
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
landingTimer_ = 0.0f;
}
void CharacterStateMachine::Update(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent,
float deltaTime) {
stateTime_ += deltaTime;
switch (currentState_) {
case CharacterStateId::Idle:
case CharacterStateId::Move:
case CharacterStateId::Landing:
UpdateGroundState(owner, commandBuffer, intent);
break;
case CharacterStateId::Jump:
case CharacterStateId::Fall:
UpdateAirState(owner, commandBuffer, intent);
break;
case CharacterStateId::Action:
UpdateActionState(owner, commandBuffer, intent, deltaTime);
break;
case CharacterStateId::Hurt:
UpdateHurtState(owner, deltaTime);
break;
case CharacterStateId::Dead:
owner.GetMotorMutable().StopGroundMovement();
break;
}
}
void CharacterStateMachine::ForceHurt(const CharacterActionDefinition* hurtAction) {
currentState_ = CharacterStateId::Hurt;
currentAction_ = hurtAction;
currentActionId_ = hurtAction ? hurtAction->actionId : std::string("hurt_light");
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
}
bool CharacterStateMachine::CanAcceptGroundActions() const {
return currentState_ == CharacterStateId::Idle
|| currentState_ == CharacterStateId::Move
|| currentState_ == CharacterStateId::Landing;
}
bool CharacterStateMachine::CanBeInterrupted() const {
if (currentState_ == CharacterStateId::Action && currentAction_) {
return currentAction_->canBeInterrupted;
}
return currentState_ != CharacterStateId::Dead;
}
bool CharacterStateMachine::IsMovementLocked() const {
return currentState_ == CharacterStateId::Action && currentAction_
&& !currentAction_->canMove;
}
bool CharacterStateMachine::CanTurn() const {
return !(currentState_ == CharacterStateId::Action && currentAction_
&& !currentAction_->canTurn);
}
void CharacterStateMachine::ChangeState(CharacterObject& owner,
CharacterStateId nextState) {
if (currentState_ == nextState) {
return;
}
currentState_ = nextState;
stateTime_ = 0.0f;
if (nextState != CharacterStateId::Action && nextState != CharacterStateId::Hurt) {
currentAction_ = nullptr;
currentActionId_.clear();
}
switch (nextState) {
case CharacterStateId::Idle:
owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest"));
break;
case CharacterStateId::Move:
owner.PlayAnimationTag(owner.ResolveAnimationTag("run", "walk"));
break;
case CharacterStateId::Jump:
case CharacterStateId::Fall:
owner.PlayAnimationTag(owner.ResolveAnimationTag("jump", "rest"));
break;
case CharacterStateId::Landing:
landingTimer_ = 0.0f;
owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest"));
break;
case CharacterStateId::Action:
case CharacterStateId::Hurt:
case CharacterStateId::Dead:
break;
}
}
void CharacterStateMachine::EnterAction(CharacterObject& owner,
const CharacterActionDefinition* action) {
// 普攻和技能都走通用 Action 状态,差异由动作定义驱动。
currentAction_ = action;
currentActionId_ = action ? action->actionId : std::string();
currentState_ = CharacterStateId::Action;
stateTime_ = 0.0f;
actionFrameProgress_ = 0.0f;
actionFrame_ = 0;
if (action) {
owner.PlayAnimationTag(action->animationTag);
}
}
void CharacterStateMachine::UpdateGroundState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent) {
auto& motor = owner.GetMotorMutable();
if (!motor.grounded) {
ChangeState(owner, CharacterStateId::Fall);
return;
}
if (currentState_ == CharacterStateId::Landing) {
landingTimer_ += owner.GetLastDeltaTime();
motor.StopGroundMovement();
if (landingTimer_ < kLandingDuration) {
return;
}
}
if (TryStartAction(owner, commandBuffer, CharacterCommandType::Skill1Pressed,
"skill_1")) {
return;
}
if (TryStartAction(owner, commandBuffer, CharacterCommandType::AttackPressed,
"attack_1")) {
return;
}
if (commandBuffer.HasBuffered(CharacterCommandType::JumpPressed)) {
commandBuffer.Consume(CharacterCommandType::JumpPressed);
motor.Jump();
ChangeState(owner, CharacterStateId::Jump);
return;
}
motor.ApplyGroundInput(intent.moveX, intent.moveY);
owner.SetFacing(motor.facing);
if (hasMoveIntent(intent)) {
ChangeState(owner, CharacterStateId::Move);
} else {
motor.StopGroundMovement();
ChangeState(owner, CharacterStateId::Idle);
}
}
void CharacterStateMachine::UpdateAirState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent) {
auto& motor = owner.GetMotorMutable();
motor.ApplyGroundInput(intent.moveX, intent.moveY, 0.75f);
owner.SetFacing(motor.facing);
if (motor.grounded) {
ChangeState(owner, CharacterStateId::Landing);
return;
}
ChangeState(owner, motor.verticalVelocity > 0.0f ? CharacterStateId::Jump
: CharacterStateId::Fall);
if (commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
commandBuffer.Consume(CharacterCommandType::AttackPressed);
}
if (commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
}
}
void CharacterStateMachine::UpdateActionState(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
const CharacterIntent& intent,
float deltaTime) {
auto& motor = owner.GetMotorMutable();
if (!currentAction_) {
ChangeState(owner, CharacterStateId::Idle);
return;
}
if (currentAction_->canMove) {
float moveScale = currentAction_->moveSpeedScale > 0.0f
? currentAction_->moveSpeedScale
: kMovementActionScale;
motor.ApplyGroundInput(intent.moveX, intent.moveY, moveScale);
} else {
motor.StopGroundMovement();
}
if (currentAction_->canTurn) {
owner.SetFacing(motor.facing);
}
actionFrameProgress_ += deltaTime * kLogicFramesPerSecond;
int previousFrame = actionFrame_;
actionFrame_ = std::max(0, static_cast<int>(actionFrameProgress_));
ApplyFrameEvents(owner, previousFrame + 1, actionFrame_);
for (const auto& rule : currentAction_->cancelRules) {
// 取消窗口由动作定义提供,状态机只负责在允许的帧段里消费缓存输入。
if (actionFrame_ < rule.beginFrame || actionFrame_ > rule.endFrame) {
continue;
}
if (rule.requireGrounded && !motor.grounded) {
continue;
}
if (rule.requireAirborne && motor.grounded) {
continue;
}
if (rule.targetAction == "attack_2"
&& commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
if (TryStartAction(owner, commandBuffer, CharacterCommandType::AttackPressed,
rule.targetAction)) {
return;
}
}
}
if (currentAction_->totalFrames > 0 && actionFrame_ >= currentAction_->totalFrames) {
currentAction_ = nullptr;
currentActionId_.clear();
if (!motor.grounded) {
ChangeState(owner, CharacterStateId::Fall);
} else if (hasMoveIntent(intent)) {
ChangeState(owner, CharacterStateId::Move);
} else {
ChangeState(owner, CharacterStateId::Idle);
}
}
}
void CharacterStateMachine::UpdateHurtState(CharacterObject& owner, float deltaTime) {
(void)deltaTime;
auto& motor = owner.GetMotorMutable();
motor.StopGroundMovement();
if (!currentAction_) {
currentAction_ = owner.FindAction("hurt_light");
}
actionFrameProgress_ += owner.GetLastDeltaTime() * kLogicFramesPerSecond;
actionFrame_ = std::max(0, static_cast<int>(actionFrameProgress_));
if (currentAction_ && actionFrame_ >= currentAction_->totalFrames) {
currentAction_ = nullptr;
currentActionId_.clear();
if (motor.grounded) {
ChangeState(owner, CharacterStateId::Idle);
} else {
ChangeState(owner, CharacterStateId::Fall);
}
}
}
bool CharacterStateMachine::TryStartAction(CharacterObject& owner,
CharacterCommandBuffer& commandBuffer,
CharacterCommandType commandType,
const std::string& actionId) {
if (!commandBuffer.HasBuffered(commandType)) {
return false;
}
const CharacterActionDefinition* action = owner.FindAction(actionId);
commandBuffer.Consume(commandType);
if (!action) {
return false;
}
EnterAction(owner, action);
return true;
}
void CharacterStateMachine::ApplyFrameEvents(CharacterObject& owner,
int previousFrame,
int currentFrame) {
if (!currentAction_) {
return;
}
auto& motor = owner.GetMotorMutable();
for (const auto& event : currentAction_->frameEvents) {
// 帧事件把“动作进行到第几帧时要做什么”从硬编码里抽出来。
if (event.frame < previousFrame || event.frame > currentFrame) {
continue;
}
switch (event.type) {
case CharacterFrameEventType::SetVelocityXY:
motor.groundVelocity = event.velocityXY;
break;
case CharacterFrameEventType::SetVelocityZ:
motor.verticalVelocity = event.velocityZ;
motor.grounded = false;
break;
case CharacterFrameEventType::FinishAction:
actionFrame_ = currentAction_->totalFrames;
break;
default:
break;
}
}
}
} // namespace frostbite2D

View File

@@ -38,6 +38,7 @@ Ptr<Sprite> createMapSprite(const std::string& path, int index) {
} // namespace
GameMap::GameMap() {
EnableEventReceive();
initLayers();
}

View File

@@ -34,6 +34,8 @@ void GameMapTestScene::onEnter() {
} else {
Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
character_->SetCharacterPosition(spawnPos);
character_->EnableEventReceive();
character_->SetEventPriority(-100);
map_->AddObject(character_);
}