From bb75a57afb8345f9488c57e941893a73aad9c585 Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Fri, 3 Apr 2026 08:08:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(character):=20=E5=AE=9E=E7=8E=B0=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=8A=B6=E6=80=81=E6=9C=BA=E4=B8=8E=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增角色状态机框架,包含空闲、移动、跳跃、攻击等状态 添加输入路由组件,统一处理键盘和手柄输入 引入动作库管理角色动作定义,支持PVF脚本配置 重构角色对象,整合运动器、状态机和输入处理 修复Actor子节点排序时的迭代安全问题 --- Frostbite2D/include/frostbite2D/2d/actor.h | 5 + Frostbite2D/src/frostbite2D/2d/actor.cpp | 61 ++- .../character/CharacterActionLibrary.h | 28 ++ Game/include/character/CharacterActionTypes.h | 199 ++++++++ Game/include/character/CharacterAnimation.h | 3 + Game/include/character/CharacterInputRouter.h | 35 ++ Game/include/character/CharacterObject.h | 34 ++ .../include/character/CharacterStateMachine.h | 59 +++ Game/src/character/CharacterActionLibrary.cpp | 442 ++++++++++++++++++ Game/src/character/CharacterInputRouter.cpp | 142 ++++++ Game/src/character/CharacterObject.cpp | 125 ++++- Game/src/character/CharacterStateMachine.cpp | 333 +++++++++++++ Game/src/map/GameMap.cpp | 1 + Game/src/scene/GameMapTestScene.cpp | 2 + 14 files changed, 1463 insertions(+), 6 deletions(-) create mode 100644 Game/include/character/CharacterActionLibrary.h create mode 100644 Game/include/character/CharacterActionTypes.h create mode 100644 Game/include/character/CharacterInputRouter.h create mode 100644 Game/include/character/CharacterStateMachine.h create mode 100644 Game/src/character/CharacterActionLibrary.cpp create mode 100644 Game/src/character/CharacterInputRouter.cpp create mode 100644 Game/src/character/CharacterStateMachine.cpp diff --git a/Frostbite2D/include/frostbite2D/2d/actor.h b/Frostbite2D/include/frostbite2D/2d/actor.h index 7b67190..eae689e 100644 --- a/Frostbite2D/include/frostbite2D/2d/actor.h +++ b/Frostbite2D/include/frostbite2D/2d/actor.h @@ -164,6 +164,9 @@ public: virtual void OnTransformChanged() {}; private: + void markChildrenOrderDirty(); + void reorderChildrenByZOrder(); + Actor* parent_; Scene* scene_; ActorList children_; @@ -197,6 +200,8 @@ private: uint32 nextUpdateListenerId_ = 1; bool iteratingUpdateListeners_ = false; bool updateListenersDirty_ = false; + bool iteratingChildren_ = false; + bool childrenOrderDirty_ = false; struct EventListener { uint32 id; diff --git a/Frostbite2D/src/frostbite2D/2d/actor.cpp b/Frostbite2D/src/frostbite2D/2d/actor.cpp index 8e79844..56fabb7 100644 --- a/Frostbite2D/src/frostbite2D/2d/actor.cpp +++ b/Frostbite2D/src/frostbite2D/2d/actor.cpp @@ -188,7 +188,13 @@ void Actor::AddChild(Ptr child) { child->SetParent(this); child->SetScene(scene_); - insertChildByZOrder(child); + if (iteratingChildren_) { + // 正在遍历孩子时不能立即重新插入排序位置,否则会破坏链表迭代状态。 + children_.PushBack(child); + markChildrenOrderDirty(); + } else { + insertChildByZOrder(child); + } child->OnAdded(this); } @@ -200,8 +206,8 @@ void Actor::SetZOrder(int zOrder) { zOrder_ = zOrder; if (parent_) { - parent_->RemoveChild(this); - parent_->AddChild(this); + // 不再在这里直接 RemoveChild/AddChild,避免更新阶段改动正在遍历的链表。 + parent_->markChildrenOrderDirty(); } } @@ -223,19 +229,68 @@ void Actor::RemoveAllChildren() { } void Actor::UpdateChildren(float deltaTime) { + iteratingChildren_ = true; for (auto it = children_.begin(); it != children_.end(); ++it) { if (*it && (*it)->IsVisible()) { (*it)->Update(deltaTime); } } + iteratingChildren_ = false; + + if (childrenOrderDirty_) { + reorderChildrenByZOrder(); + } } void Actor::RenderChildren() { + if (childrenOrderDirty_ && !iteratingChildren_) { + reorderChildrenByZOrder(); + } + + iteratingChildren_ = true; for (auto it = children_.begin(); it != children_.end(); ++it) { if (*it && (*it)->IsVisible()) { (*it)->Render(); } } + iteratingChildren_ = false; +} + +void Actor::markChildrenOrderDirty() { + childrenOrderDirty_ = true; +} + +void Actor::reorderChildrenByZOrder() { + if (!childrenOrderDirty_) { + return; + } + + // 统一在安全时机按 zOrder 重建顺序,解决上下移动时频繁改层级导致的卡死。 + std::vector> orderedChildren; + for (auto it = children_.begin(); it != children_.end(); ++it) { + if (*it) { + orderedChildren.push_back(*it); + } + } + + std::sort(orderedChildren.begin(), orderedChildren.end(), + [](const Ptr& lhs, const Ptr& rhs) { + if (!lhs || !rhs) { + return static_cast(lhs); + } + return lhs->GetZOrder() < rhs->GetZOrder(); + }); + + while (!children_.IsEmpty()) { + Ptr child = children_.GetFirst(); + children_.Remove(child); + } + + for (auto& child : orderedChildren) { + children_.PushBack(child); + } + + childrenOrderDirty_ = false; } uint32 Actor::AddUpdateListener(UpdateCallback callback, UpdatePhase phase) { diff --git a/Game/include/character/CharacterActionLibrary.h b/Game/include/character/CharacterActionLibrary.h new file mode 100644 index 0000000..70a4dd5 --- /dev/null +++ b/Game/include/character/CharacterActionLibrary.h @@ -0,0 +1,28 @@ +#pragma once + +#include "character/CharacterActionTypes.h" +#include "character/CharacterDataLoader.h" +#include +#include +#include + +namespace frostbite2D { + +/// 管理角色动作定义。优先从 PVF 动作脚本读取,缺失时回退到内置默认动作。 +class CharacterActionLibrary { +public: + bool LoadForConfig(const character::CharacterConfig& config); + const CharacterActionDefinition* FindAction(const std::string& actionId) const; + const CharacterActionDefinition* GetDefaultAction() const; + +private: + void BuildFallbackActions(const character::CharacterConfig& config); + bool TryLoadPvfActionScripts(const character::CharacterConfig& config); + + std::map actions_; +}; + +std::optional loadCharacterActionLibrary( + const character::CharacterConfig& config); + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterActionTypes.h b/Game/include/character/CharacterActionTypes.h new file mode 100644 index 0000000..4223627 --- /dev/null +++ b/Game/include/character/CharacterActionTypes.h @@ -0,0 +1,199 @@ +#pragma once + +#include +#include +#include + +namespace frostbite2D { + +/// 输入层产出的统一命令,屏蔽键盘与手柄的设备差异。 +enum class CharacterCommandType { + Move, + JumpPressed, + AttackPressed, + Skill1Pressed, + DashPressed, +}; + +/// 单条输入命令。移动命令携带轴值,按钮命令只关心触发时机。 +struct CharacterCommand { + CharacterCommandType type = CharacterCommandType::Move; + Vec2 moveAxis = Vec2::Zero(); + float timestamp = 0.0f; +}; + +/// 角色在当前帧想要执行的意图,供状态机消费。 +struct CharacterIntent { + float moveX = 0.0f; + float moveY = 0.0f; + bool wantJump = false; + bool wantAttack = false; + bool wantSkill1 = false; + bool wantDash = false; +}; + +/// 主状态机的大状态,普攻和技能统一走 Action。 +enum class CharacterStateId { + Idle, + Move, + Jump, + Fall, + Landing, + Action, + Hurt, + Dead, +}; + +/// 角色的逻辑世界坐标。x/y 是地面平面,z 是高度轴。 +struct CharacterWorldPosition { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + + /// 当前项目的 2.5D 投影:地面 y 决定站位,高度 z 决定视觉抬升。 + Vec2 ToScreenPosition() const { return Vec2(x, y - z); } + Vec2 ToGroundPosition() const { return Vec2(x, y); } +}; + +/// 运动器只负责推进坐标和速度,不直接决定状态流转。 +struct CharacterMotor { + CharacterWorldPosition position; + Vec2 groundVelocity = Vec2::Zero(); + float verticalVelocity = 0.0f; + bool grounded = true; + int facing = 1; + float moveSpeed = 220.0f; + float jumpSpeed = 520.0f; + float gravity = 1600.0f; + + void SetGroundPosition(const Vec2& groundPosition) { + position.x = groundPosition.x; + position.y = groundPosition.y; + } + + void ApplyGroundInput(float moveX, float moveY, float speedScale = 1.0f) { + Vec2 input(moveX, moveY); + if (input.lengthSquared() > 1.0f) { + input = input.normalized(); + } + groundVelocity = input * (moveSpeed * speedScale); + if (input.x > 0.01f) { + facing = 1; + } else if (input.x < -0.01f) { + facing = -1; + } + } + + void StopGroundMovement() { groundVelocity = Vec2::Zero(); } + + void Jump() { + grounded = false; + verticalVelocity = jumpSpeed; + if (position.z < 0.0f) { + position.z = 0.0f; + } + } + + void Update(float deltaTime) { + position.x += groundVelocity.x * deltaTime; + position.y += groundVelocity.y * deltaTime; + + if (!grounded || position.z > 0.0f || verticalVelocity > 0.0f) { + position.z += verticalVelocity * deltaTime; + verticalVelocity -= gravity * deltaTime; + grounded = false; + + if (position.z <= 0.0f) { + position.z = 0.0f; + verticalVelocity = 0.0f; + grounded = true; + } + } else { + position.z = 0.0f; + verticalVelocity = 0.0f; + grounded = true; + } + } +}; + +/// 帧事件是动作定义驱动逻辑的钩子,后续可扩展命中框与位移曲线。 +enum class CharacterFrameEventType { + OpenHitWindow, + CloseHitWindow, + OpenInputBuffer, + CloseInputBuffer, + SetVelocityXY, + SetVelocityZ, + AnimationEvent, + FinishAction, +}; + +/// 单个动作帧上的逻辑事件。 +struct CharacterFrameEventDefinition { + int frame = 0; + CharacterFrameEventType type = CharacterFrameEventType::AnimationEvent; + Vec2 velocityXY = Vec2::Zero(); + float velocityZ = 0.0f; + std::string stringValue; +}; + +/// 取消规则决定动作在什么窗口内可以接下一个动作。 +struct CharacterCancelRuleDefinition { + std::string targetAction; + int beginFrame = 0; + int endFrame = 0; + bool requireGrounded = false; + bool requireAirborne = false; +}; + +/// 逻辑动作定义。actionId 是玩法动作名,animationTag 是资源动作名。 +struct CharacterActionDefinition { + std::string actionId; + std::string animationTag; + int totalFrames = 1; + bool loop = false; + bool canMove = false; + bool canTurn = true; + bool canBeInterrupted = true; + int inputBufferOpenFrame = 0; + float moveSpeedScale = 0.0f; + std::vector cancelRules; + std::vector frameEvents; +}; + +/// 运行期状态标记,承载霸体、无敌等战斗属性。 +struct CharacterStatus { + bool superArmor = false; + bool invincible = false; + bool controlLocked = false; +}; + +/// 外部战斗系统打入角色时携带的受击上下文。 +struct HitContext { + std::string hurtAction = "hurt_light"; + float launchZVelocity = 0.0f; + bool ignoreInterrupt = false; +}; + +/// 短时缓存输入,提供预输入和连段窗口的基础能力。 +class CharacterCommandBuffer { +public: + void Advance(float deltaTime); + void Submit(const CharacterCommand& command); + CharacterIntent BuildIntent() const; + bool HasBuffered(CharacterCommandType type) const; + void Consume(CharacterCommandType type); + void Clear(); + +private: + struct BufferedButton { + CharacterCommandType type = CharacterCommandType::AttackPressed; + float age = 0.0f; + }; + + Vec2 moveAxis_ = Vec2::Zero(); + std::vector buttons_; + float bufferWindowSeconds_ = 0.2f; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h index 815a0b6..6b8a5df 100644 --- a/Game/include/character/CharacterAnimation.h +++ b/Game/include/character/CharacterAnimation.h @@ -24,6 +24,9 @@ public: void 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; + } private: static std::string FormatImgPath(std::string path, Animation::ReplaceData data); diff --git a/Game/include/character/CharacterInputRouter.h b/Game/include/character/CharacterInputRouter.h new file mode 100644 index 0000000..1e6148e --- /dev/null +++ b/Game/include/character/CharacterInputRouter.h @@ -0,0 +1,35 @@ +#pragma once + +#include "character/CharacterActionTypes.h" +#include +#include + +namespace frostbite2D { + +/// 将 SDL 键盘/手柄事件统一转换为角色命令,避免状态机直接依赖设备细节。 +class CharacterInputRouter { +public: + bool OnKeyDown(const KeyEvent& event); + bool OnKeyUp(KeyCode keyCode); + bool OnJoystickAxis(const JoystickAxisEvent& event); + bool OnJoystickButtonDown(const JoystickButtonDownEvent& event); + bool OnJoystickButtonUp(const JoystickButtonUpEvent& event); + + void EmitCommands(CharacterCommandBuffer& commandBuffer); + void ClearFrameState(); + +private: + bool left_ = false; + bool right_ = false; + bool up_ = false; + bool down_ = false; + bool jumpPressed_ = false; + bool attackPressed_ = false; + bool skill1Pressed_ = false; + bool dashPressed_ = false; + float leftStickX_ = 0.0f; + float leftStickY_ = 0.0f; + float joystickDeadZone_ = 0.2f; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterObject.h b/Game/include/character/CharacterObject.h index cd73e99..713cdbe 100644 --- a/Game/include/character/CharacterObject.h +++ b/Game/include/character/CharacterObject.h @@ -1,9 +1,14 @@ #pragma once +#include "character/CharacterActionLibrary.h" #include "character/CharacterAnimation.h" #include "character/CharacterDataLoader.h" #include "character/CharacterEquipmentManager.h" +#include "character/CharacterInputRouter.h" +#include "character/CharacterStateMachine.h" #include +#include +#include #include #include @@ -19,11 +24,32 @@ public: void SetAction(const std::string& actionName); void SetDirection(int direction); void SetCharacterPosition(const Vec2& pos); + void SetWorldPosition(const CharacterWorldPosition& pos); + void PushCommand(const CharacterCommand& command); + void ApplyHit(const HitContext& hit); + + 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; int GetJobId() const { return jobId_; } int GetGrowType() const { return growType_; } int GetDirection() const { return direction_; } const std::string& GetCurrentAction() const { return currentAction_; } + CharacterStateId GetState() const { return stateMachine_.GetState(); } + const CharacterIntent& GetCurrentIntent() const { return currentIntent_; } + const CharacterWorldPosition& GetWorldPosition() const { return motor_.position; } + const CharacterMotor& GetMotor() const { return motor_; } + CharacterMotor& GetMotorMutable() { return motor_; } + float GetLastDeltaTime() const { return lastDeltaTime_; } + const CharacterActionDefinition* FindAction(const std::string& actionId) const; + std::string ResolveAnimationTag(const std::string& preferred, + const std::string& fallback) const; + void PlayAnimationTag(const std::string& actionTag); + void SetFacing(int direction); const character::CharacterConfig* GetConfig() const { return config_ ? &config_.value() : nullptr; @@ -37,7 +63,15 @@ private: std::string currentAction_; std::optional config_; CharacterEquipmentManager equipmentManager_; + CharacterActionLibrary actionLibrary_; + CharacterInputRouter inputRouter_; + CharacterCommandBuffer commandBuffer_; + CharacterIntent currentIntent_; + CharacterStateMachine stateMachine_; + CharacterMotor motor_; + CharacterStatus status_; RefPtr animationManager_ = nullptr; + float lastDeltaTime_ = 0.0f; }; } // namespace frostbite2D diff --git a/Game/include/character/CharacterStateMachine.h b/Game/include/character/CharacterStateMachine.h new file mode 100644 index 0000000..15f3909 --- /dev/null +++ b/Game/include/character/CharacterStateMachine.h @@ -0,0 +1,59 @@ +#pragma once + +#include "character/CharacterActionLibrary.h" + +namespace frostbite2D { + +class CharacterObject; + +/// 角色主状态机。输入先变成意图,再由状态机决定当前大状态和动作推进。 +class CharacterStateMachine { +public: + void Reset(); + void Update(CharacterObject& owner, + CharacterCommandBuffer& commandBuffer, + const CharacterIntent& intent, + float deltaTime); + void ForceHurt(const CharacterActionDefinition* hurtAction); + + CharacterStateId GetState() const { return currentState_; } + const std::string& GetCurrentActionId() const { return currentActionId_; } + const CharacterActionDefinition* GetCurrentActionDefinition() const { + return currentAction_; + } + + bool CanAcceptGroundActions() const; + bool CanBeInterrupted() const; + bool IsMovementLocked() const; + bool CanTurn() const; + +private: + void ChangeState(CharacterObject& owner, CharacterStateId nextState); + void EnterAction(CharacterObject& owner, const CharacterActionDefinition* action); + void UpdateGroundState(CharacterObject& owner, + CharacterCommandBuffer& commandBuffer, + const CharacterIntent& intent); + void UpdateAirState(CharacterObject& owner, + CharacterCommandBuffer& commandBuffer, + const CharacterIntent& intent); + void UpdateActionState(CharacterObject& owner, + CharacterCommandBuffer& commandBuffer, + const CharacterIntent& intent, + float deltaTime); + void UpdateHurtState(CharacterObject& owner, float deltaTime); + bool TryStartAction(CharacterObject& owner, + CharacterCommandBuffer& commandBuffer, + CharacterCommandType commandType, + const std::string& actionId); + void ApplyFrameEvents(CharacterObject& owner, int previousFrame, int currentFrame); + + CharacterStateId currentState_ = CharacterStateId::Idle; + std::string currentActionId_; + const CharacterActionDefinition* currentAction_ = nullptr; + float stateTime_ = 0.0f; + float actionFrameProgress_ = 0.0f; + int actionFrame_ = 0; + float landingTimer_ = 0.0f; +}; + +} // namespace frostbite2D diff --git a/Game/src/character/CharacterActionLibrary.cpp b/Game/src/character/CharacterActionLibrary.cpp new file mode 100644 index 0000000..5fb5ca3 --- /dev/null +++ b/Game/src/character/CharacterActionLibrary.cpp @@ -0,0 +1,442 @@ +#include "character/CharacterActionLibrary.h" +#include +#include +#include +#include +#include +#include + +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(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(value[begin]))) { + ++begin; + } + while (end > begin + && std::isspace(static_cast(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 tokens_; + size_t index_ = 0; + bool valid_ = false; +}; + +// action_list.lst 同时兼容两种格式: +// 1. 标准 PVF #PVF_File + 索引 + `path` +// 2. 早期临时版的 actionId + path 成对写法 +std::vector> parseActionListEntries( + ScriptTokenStream& listStream) { + std::vector> entries; + std::vector 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& 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(actions_.size()), config.jobId); + return true; +} + +std::optional loadCharacterActionLibrary( + const character::CharacterConfig& config) { + CharacterActionLibrary library; + if (!library.LoadForConfig(config)) { + return std::nullopt; + } + return library; +} + +} // namespace frostbite2D diff --git a/Game/src/character/CharacterInputRouter.cpp b/Game/src/character/CharacterInputRouter.cpp new file mode 100644 index 0000000..d28a515 --- /dev/null +++ b/Game/src/character/CharacterInputRouter.cpp @@ -0,0 +1,142 @@ +#include "character/CharacterInputRouter.h" +#include +#include + +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 diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 376ed09..736ad7f 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -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(); 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(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(event)); +} + +bool CharacterObject::OnJoystickButtonDown(const JoystickEvent& event) { + return inputRouter_.OnJoystickButtonDown( + static_cast(event)); +} + +bool CharacterObject::OnJoystickButtonUp(const JoystickEvent& event) { + return inputRouter_.OnJoystickButtonUp( + static_cast(event)); +} + +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 diff --git a/Game/src/character/CharacterStateMachine.cpp b/Game/src/character/CharacterStateMachine.cpp new file mode 100644 index 0000000..b14163a --- /dev/null +++ b/Game/src/character/CharacterStateMachine.cpp @@ -0,0 +1,333 @@ +#include "character/CharacterStateMachine.h" +#include "character/CharacterObject.h" +#include + +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(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(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 diff --git a/Game/src/map/GameMap.cpp b/Game/src/map/GameMap.cpp index 6cd9c92..2c02e1d 100644 --- a/Game/src/map/GameMap.cpp +++ b/Game/src/map/GameMap.cpp @@ -38,6 +38,7 @@ Ptr createMapSprite(const std::string& path, int index) { } // namespace GameMap::GameMap() { + EnableEventReceive(); initLayers(); } diff --git a/Game/src/scene/GameMapTestScene.cpp b/Game/src/scene/GameMapTestScene.cpp index 4a38e54..66de57f 100644 --- a/Game/src/scene/GameMapTestScene.cpp +++ b/Game/src/scene/GameMapTestScene.cpp @@ -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_); }