diff --git a/Frostbite2D/include/frostbite2D/animation/animation.h b/Frostbite2D/include/frostbite2D/animation/animation.h index bca0c99..3916e3d 100644 --- a/Frostbite2D/include/frostbite2D/animation/animation.h +++ b/Frostbite2D/include/frostbite2D/animation/animation.h @@ -38,7 +38,7 @@ public: public: void FlushFrame(int index); void Reset(); - animation::AniFrame GetCurrentFrameInfo(); + animation::AniFrame GetCurrentFrameInfo() const; void SetFrameIndex(int index); void SetDirection(int direction); void InterpolationLogic(); diff --git a/Frostbite2D/src/frostbite2D/animation/animation.cpp b/Frostbite2D/src/frostbite2D/animation/animation.cpp index 1547ca9..c82e309 100644 --- a/Frostbite2D/src/frostbite2D/animation/animation.cpp +++ b/Frostbite2D/src/frostbite2D/animation/animation.cpp @@ -249,7 +249,7 @@ void Animation::Reset() { FlushFrame(0); } -AniFrame Animation::GetCurrentFrameInfo() { +AniFrame Animation::GetCurrentFrameInfo() const { return frames_[currentFrameIndex_]; } diff --git a/Game/include/character/CharacterActionLibrary.h b/Game/include/character/CharacterActionLibrary.h index 82ac9c1..06a35da 100644 --- a/Game/include/character/CharacterActionLibrary.h +++ b/Game/include/character/CharacterActionLibrary.h @@ -8,7 +8,11 @@ namespace frostbite2D { -/// 管理角色动作定义。优先从 PVF 动作脚本读取,不再自动生成回退动作。 +/// 管理角色显式注册的逻辑动作。 +/// +/// 动画配置只负责提供资源标签,不再自动推导哪些逻辑动作存在。 +/// 基础状态动作和职业技能入口都在代码中显式注册,让技能实现类自己决定 +/// 当前要播放哪个 ani。 class CharacterActionLibrary { public: bool LoadForConfig(const character::CharacterConfig& config); @@ -17,7 +21,7 @@ public: std::string DescribeActionIds() const; private: - bool TryLoadPvfActionScripts(const character::CharacterConfig& config); + bool BuildExplicitActionRegistry(const character::CharacterConfig& config); std::map actions_; }; diff --git a/Game/include/character/CharacterActionTypes.h b/Game/include/character/CharacterActionTypes.h index 4223627..bd8edca 100644 --- a/Game/include/character/CharacterActionTypes.h +++ b/Game/include/character/CharacterActionTypes.h @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include namespace frostbite2D { @@ -10,9 +13,13 @@ namespace frostbite2D { enum class CharacterCommandType { Move, JumpPressed, - AttackPressed, - Skill1Pressed, - DashPressed, + ActionPressed, +}; + +enum class CharacterMoveMode { + None, + Walk, + Run, }; /// 单条输入命令。移动命令携带轴值,按钮命令只关心触发时机。 @@ -20,16 +27,17 @@ struct CharacterCommand { CharacterCommandType type = CharacterCommandType::Move; Vec2 moveAxis = Vec2::Zero(); float timestamp = 0.0f; + CharacterMoveMode moveMode = CharacterMoveMode::None; + std::string actionId; }; /// 角色在当前帧想要执行的意图,供状态机消费。 struct CharacterIntent { float moveX = 0.0f; float moveY = 0.0f; + CharacterMoveMode moveMode = CharacterMoveMode::None; bool wantJump = false; - bool wantAttack = false; - bool wantSkill1 = false; - bool wantDash = false; + std::vector requestedActions; }; /// 主状态机的大状态,普攻和技能统一走 Action。 @@ -59,7 +67,9 @@ struct CharacterWorldPosition { struct CharacterMotor { CharacterWorldPosition position; Vec2 groundVelocity = Vec2::Zero(); + Vec2 forcedSlideVelocity = Vec2::Zero(); float verticalVelocity = 0.0f; + float forcedSlideRemainingTime = 0.0f; bool grounded = true; int facing = 1; float moveSpeed = 220.0f; @@ -85,6 +95,20 @@ struct CharacterMotor { } void StopGroundMovement() { groundVelocity = Vec2::Zero(); } + void ClearForcedSlide() { + forcedSlideVelocity = Vec2::Zero(); + forcedSlideRemainingTime = 0.0f; + } + + void StartForcedSlide(const Vec2& direction, float distance, float duration) { + if (distance <= 0.0f || duration <= 0.0f || direction.lengthSquared() < 0.0001f) { + ClearForcedSlide(); + return; + } + + forcedSlideVelocity = direction.normalized() * (distance / duration); + forcedSlideRemainingTime = duration; + } void Jump() { grounded = false; @@ -97,6 +121,17 @@ struct CharacterMotor { void Update(float deltaTime) { position.x += groundVelocity.x * deltaTime; position.y += groundVelocity.y * deltaTime; + if (forcedSlideRemainingTime > 0.0f && forcedSlideVelocity.lengthSquared() > 0.0f) { + float slideDeltaTime = deltaTime < forcedSlideRemainingTime + ? deltaTime + : forcedSlideRemainingTime; + position.x += forcedSlideVelocity.x * slideDeltaTime; + position.y += forcedSlideVelocity.y * slideDeltaTime; + forcedSlideRemainingTime -= slideDeltaTime; + if (forcedSlideRemainingTime <= 0.0f) { + ClearForcedSlide(); + } + } if (!grounded || position.z > 0.0f || verticalVelocity > 0.0f) { position.z += verticalVelocity * deltaTime; @@ -116,51 +151,166 @@ struct CharacterMotor { } }; -/// 帧事件是动作定义驱动逻辑的钩子,后续可扩展命中框与位移曲线。 -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 是资源动作名。 +/// 逻辑动作定义。这里只保留显式注册的逻辑入口名。 +/// +/// 动画播放统一由状态类/技能类直接调用 `PlayAnimationTag()` 控制, +/// 不再通过动作定义反查 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; }; +using CharacterRuntimeValue = std::variant; + +class CharacterRuntimeBlackboard { +public: + void SetBool(const std::string& key, bool value); + void SetInt(const std::string& key, int value); + void SetFloat(const std::string& key, float value); + void SetString(const std::string& key, const std::string& value); + void SetVec2(const std::string& key, const Vec2& value); + + bool TryGetBool(const std::string& key, bool& outValue) const; + bool TryGetInt(const std::string& key, int& outValue) const; + bool TryGetFloat(const std::string& key, float& outValue) const; + bool TryGetString(const std::string& key, std::string& outValue) const; + bool TryGetVec2(const std::string& key, Vec2& outValue) const; + + bool Has(const std::string& key) const; + void Remove(const std::string& key); + void Clear(); + +private: + template + void SetValue(const std::string& key, T&& value) { + values_[key] = std::forward(value); + } + + template + bool TryGetValue(const std::string& key, T& outValue) const { + auto it = values_.find(key); + if (it == values_.end()) { + return false; + } + + const T* value = std::get_if(&it->second); + if (!value) { + return false; + } + + outValue = *value; + return true; + } + + std::unordered_map values_; +}; + +struct CharacterActionContextData { + bool valid = false; + std::string requestedActionId; + std::string sourceActionId; + CharacterStateId sourceStateId = CharacterStateId::Idle; + bool hasSourceStateId = false; + CharacterRuntimeBlackboard values; + + void Clear() { + valid = false; + requestedActionId.clear(); + sourceActionId.clear(); + sourceStateId = CharacterStateId::Idle; + hasSourceStateId = false; + values.Clear(); + } + + void SetSourceStateId(CharacterStateId stateId) { + sourceStateId = stateId; + hasSourceStateId = true; + } + + void SetBool(const std::string& key, bool value) { values.SetBool(key, value); } + void SetInt(const std::string& key, int value) { values.SetInt(key, value); } + void SetFloat(const std::string& key, float value) { values.SetFloat(key, value); } + void SetString(const std::string& key, const std::string& value) { + values.SetString(key, value); + } + void SetVec2(const std::string& key, const Vec2& value) { values.SetVec2(key, value); } + + bool TryGetBool(const std::string& key, bool& outValue) const { + return values.TryGetBool(key, outValue); + } + bool TryGetInt(const std::string& key, int& outValue) const { + return values.TryGetInt(key, outValue); + } + bool TryGetFloat(const std::string& key, float& outValue) const { + return values.TryGetFloat(key, outValue); + } + bool TryGetString(const std::string& key, std::string& outValue) const { + return values.TryGetString(key, outValue); + } + bool TryGetVec2(const std::string& key, Vec2& outValue) const { + return values.TryGetVec2(key, outValue); + } + + bool Has(const std::string& key) const { return values.Has(key); } + void Remove(const std::string& key) { values.Remove(key); } +}; + +inline void CharacterRuntimeBlackboard::SetBool(const std::string& key, bool value) { + SetValue(key, value); +} + +inline void CharacterRuntimeBlackboard::SetInt(const std::string& key, int value) { + SetValue(key, value); +} + +inline void CharacterRuntimeBlackboard::SetFloat(const std::string& key, float value) { + SetValue(key, value); +} + +inline void CharacterRuntimeBlackboard::SetString(const std::string& key, + const std::string& value) { + SetValue(key, value); +} + +inline void CharacterRuntimeBlackboard::SetVec2(const std::string& key, const Vec2& value) { + SetValue(key, value); +} + +inline bool CharacterRuntimeBlackboard::TryGetBool(const std::string& key, + bool& outValue) const { + return TryGetValue(key, outValue); +} + +inline bool CharacterRuntimeBlackboard::TryGetInt(const std::string& key, int& outValue) const { + return TryGetValue(key, outValue); +} + +inline bool CharacterRuntimeBlackboard::TryGetFloat(const std::string& key, + float& outValue) const { + return TryGetValue(key, outValue); +} + +inline bool CharacterRuntimeBlackboard::TryGetString(const std::string& key, + std::string& outValue) const { + return TryGetValue(key, outValue); +} + +inline bool CharacterRuntimeBlackboard::TryGetVec2(const std::string& key, + Vec2& outValue) const { + return TryGetValue(key, outValue); +} + +inline bool CharacterRuntimeBlackboard::Has(const std::string& key) const { + return values_.find(key) != values_.end(); +} + +inline void CharacterRuntimeBlackboard::Remove(const std::string& key) { + values_.erase(key); +} + +inline void CharacterRuntimeBlackboard::Clear() { + values_.clear(); +} + /// 运行期状态标记,承载霸体、无敌等战斗属性。 struct CharacterStatus { bool superArmor = false; @@ -183,15 +333,20 @@ public: CharacterIntent BuildIntent() const; bool HasBuffered(CharacterCommandType type) const; void Consume(CharacterCommandType type); + bool HasBufferedAction(const std::string& actionId) const; + void ConsumeAction(const std::string& actionId); + void ClearBufferedActions(); void Clear(); private: struct BufferedButton { - CharacterCommandType type = CharacterCommandType::AttackPressed; + CharacterCommandType type = CharacterCommandType::ActionPressed; + std::string actionId; float age = 0.0f; }; Vec2 moveAxis_ = Vec2::Zero(); + CharacterMoveMode moveMode_ = CharacterMoveMode::None; std::vector buttons_; float bufferWindowSeconds_ = 0.2f; }; diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h index 1954d8d..8942889 100644 --- a/Game/include/character/CharacterAnimation.h +++ b/Game/include/character/CharacterAnimation.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,8 @@ class CharacterObject; class CharacterAnimation : public Actor { public: using ActionAnimationList = std::map>>; + using ActionFrameFlagCallback = std::function; + using ActionEndCallback = std::function; bool Init(CharacterObject* parent, const character::CharacterConfig& config, @@ -28,9 +31,19 @@ public: return actionAnimations_.count(actionName) > 0; } std::string DescribeAvailableActions() const; + bool IsCurrentActionFinished() const; + int GetCurrentFrameIndex() const; + int GetCurrentFrameCount() const; + float GetCurrentNormalizedProgress() const; + animation::AniFrame GetCurrentFrameInfo() const; + void SetActionFrameFlagCallback(ActionFrameFlagCallback callback); + void SetActionEndCallback(ActionEndCallback callback); private: static std::string FormatImgPath(std::string path, Animation::ReplaceData data); + Animation* GetPrimaryAnimation(const std::string& actionName) const; + Animation* GetCurrentPrimaryAnimation() const; + void RefreshRuntimeCallbacks(); void CreateAnimationBySlot(const std::string& actionName, const std::string& slotName, @@ -42,6 +55,8 @@ private: ActionAnimationList actionAnimations_; std::string currentActionTag_; int direction_ = 1; + ActionFrameFlagCallback actionFrameFlagCallback_; + ActionEndCallback actionEndCallback_; }; } // namespace frostbite2D diff --git a/Game/include/character/CharacterInputRouter.h b/Game/include/character/CharacterInputRouter.h index 0806e1c..9c5f53d 100644 --- a/Game/include/character/CharacterInputRouter.h +++ b/Game/include/character/CharacterInputRouter.h @@ -3,6 +3,9 @@ #include "character/CharacterActionTypes.h" #include #include +#include +#include +#include namespace frostbite2D { @@ -19,14 +22,31 @@ public: void ClearFrameState(); private: + enum class KeyboardMoveDirection { + None, + Left, + Right, + Up, + Down, + }; + + void QueueAction(const std::string& actionId); + bool IsDirectionPressed(KeyboardMoveDirection direction) const; + bool HasDirectionalInput() const; + void RefreshKeyboardMoveMode(); + bool left_ = false; bool right_ = false; bool up_ = false; bool down_ = false; bool jumpPressed_ = false; - bool attackPressed_ = false; - bool skill1Pressed_ = false; - bool dashPressed_ = false; + std::vector actionRequests_; + CharacterMoveMode keyboardMoveMode_ = CharacterMoveMode::None; + KeyboardMoveDirection runDirection_ = KeyboardMoveDirection::None; + std::uint32_t leftLastTapTimestamp_ = 0; + std::uint32_t rightLastTapTimestamp_ = 0; + std::uint32_t upLastTapTimestamp_ = 0; + std::uint32_t downLastTapTimestamp_ = 0; float leftStickX_ = 0.0f; float leftStickY_ = 0.0f; float joystickDeadZone_ = 0.2f; diff --git a/Game/include/character/CharacterObject.h b/Game/include/character/CharacterObject.h index 0702150..2ecb685 100644 --- a/Game/include/character/CharacterObject.h +++ b/Game/include/character/CharacterObject.h @@ -97,6 +97,26 @@ public: CharacterMotor& GetMotorMutable() { return motor_; } CharacterCommandBuffer& GetCommandBufferMutable() { return commandBuffer_; } float GetLastDeltaTime() const { return lastDeltaTime_; } + const CharacterRuntimeBlackboard& GetRuntimeBlackboard() const { return runtimeBlackboard_; } + CharacterRuntimeBlackboard& GetRuntimeBlackboardMutable() { return runtimeBlackboard_; } + + /// @brief Prepare one-shot data for the next action transition. + /// + /// The returned context is consumed by the next successful `EnterAction()`. + CharacterActionContextData& PreparePendingActionContext( + const std::string& requestedActionId, + const std::string& sourceActionId = std::string(), + CharacterStateId sourceStateId = CharacterStateId::Idle); + void ClearPendingActionContext(); + bool HasPendingActionContext() const { return pendingActionContext_.valid; } + const CharacterActionContextData* GetPendingActionContext() const; + CharacterActionContextData* GetPendingActionContextMutable(); + + /// @brief Read transition metadata for the currently running action. + const CharacterActionContextData* GetCurrentActionContext() const; + CharacterActionContextData* GetCurrentActionContextMutable(); + bool HasCurrentActionContext() const { return currentActionContext_.valid; } + void ClearCurrentActionContext(); /// @brief 按逻辑动作 id 查询动作定义。 const CharacterActionDefinition* FindAction(const std::string& actionId) const; @@ -105,13 +125,24 @@ public: 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); + bool HasAnimationTag(const std::string& actionTag) const; + + /// @brief 查询当前动作主动画是否已经播放完成。 + bool IsCurrentAnimationFinished() const; + + /// @brief 读取当前动作主动画的帧信息,方便状态类/脚本做运行时判断。 + int GetCurrentAnimationFrameIndex() const; + int GetCurrentAnimationFrameCount() const; + float GetCurrentAnimationProgressNormalized() const; + animation::AniFrame GetCurrentAnimationFrameInfo() const; + + /// @brief 绑定当前动作主动画的运行时回调,供后续脚本系统接入。 + void SetAnimationFrameFlagCallback(CharacterAnimation::ActionFrameFlagCallback callback); + void SetAnimationEndCallback(CharacterAnimation::ActionEndCallback callback); /// @brief 输出角色动作/动画的致命错误,并请求应用退出。 void ReportFatalCharacterError(const char* phase, @@ -136,6 +167,9 @@ public: const CharacterEquipmentManager& GetEquipmentManager() const { return equipmentManager_; } private: + void CommitPendingActionContext(const std::string& defaultRequestedActionId, + const std::string& defaultSourceActionId, + CharacterStateId defaultSourceStateId); bool SetActionStrict(const std::string& actionName, const char* phase, const std::string& requestedActionId); @@ -159,7 +193,7 @@ private: /// 负责拼装装备部件,决定分层 Avatar 的表现资源。 CharacterEquipmentManager equipmentManager_; - /// 动作定义表:把逻辑 actionId 映射到帧数据、取消规则、动画标签等。 + /// 动作定义表:保存显式注册的逻辑动作入口。 CharacterActionLibrary actionLibrary_; /// 输入适配层,把键盘/手柄事件收敛成统一命令。 @@ -171,7 +205,16 @@ private: /// 当前帧从命令缓冲整理出的“角色意图”。 CharacterIntent currentIntent_; - /// 状态调度器,负责状态切换、动作进入和帧事件推进。 + /// Character-wide shared runtime data for skills, buffs, and future scripts. + CharacterRuntimeBlackboard runtimeBlackboard_; + + /// One-shot payload prepared before switching into the next action. + CharacterActionContextData pendingActionContext_; + + /// Metadata attached to the action currently running inside Action state. + CharacterActionContextData currentActionContext_; + + /// 状态调度器,负责状态切换与动作进入。 CharacterStateMachine stateMachine_; /// 纯运动数据:位置、速度、落地状态和朝向都在这里维护。 @@ -188,6 +231,8 @@ private: /// 是否允许从事件系统接收输入;AI/剧情接管时可临时关闭。 bool inputEnabled_ = true; + + friend class CharacterStateMachine; }; } // namespace frostbite2D diff --git a/Game/include/character/CharacterStateMachine.h b/Game/include/character/CharacterStateMachine.h index 23b7ca0..f6477d7 100644 --- a/Game/include/character/CharacterStateMachine.h +++ b/Game/include/character/CharacterStateMachine.h @@ -47,18 +47,17 @@ private: const char* phase) const; bool TryStartAction(CharacterObject& owner, CharacterCommandBuffer& commandBuffer, - CharacterCommandType commandType, + const std::string& requestedActionId, const std::string& actionId); + bool StartAction(CharacterObject& owner, + const std::string& requestedActionId, + const std::string& actionId); bool TryStartRegisteredAction(CharacterStateContext& context); - 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; StateRegistry stateRegistry_; friend class CharacterStateBase; diff --git a/Game/include/character/states/CharacterStateBase.h b/Game/include/character/states/CharacterStateBase.h index a1ef881..0f577b6 100644 --- a/Game/include/character/states/CharacterStateBase.h +++ b/Game/include/character/states/CharacterStateBase.h @@ -57,21 +57,26 @@ protected: void ChangeState(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId nextState) const; - void PlayActionById(CharacterObject& owner, const std::string& actionId) const; void InvokeHook(CharacterStateMachine& machine, CharacterObject& owner, const CharacterActionDefinition* action, const char* phase) const; bool TryStartAction(CharacterStateMachine& machine, CharacterStateContext& context, - CharacterCommandType commandType, const std::string& actionId) const; + bool TryStartAction(CharacterStateMachine& machine, + CharacterStateContext& context, + const std::string& requestedActionId, + const std::string& actionId) const; + bool StartAction(CharacterStateMachine& machine, + CharacterStateContext& context, + const std::string& actionId) const; + bool StartAction(CharacterStateMachine& machine, + CharacterStateContext& context, + const std::string& requestedActionId, + const std::string& actionId) const; bool TryStartRegisteredAction(CharacterStateMachine& machine, CharacterStateContext& context) const; - void ApplyFrameEvents(CharacterStateMachine& machine, - CharacterObject& owner, - int previousFrame, - int currentFrame) const; const CharacterActionDefinition* GetCurrentAction(const CharacterStateMachine& machine) const; const std::string& GetCurrentActionId(const CharacterStateMachine& machine) const; @@ -81,9 +86,6 @@ protected: void ClearCurrentAction(CharacterStateMachine& machine) const; float& StateTime(CharacterStateMachine& machine) const; - float& ActionFrameProgress(CharacterStateMachine& machine) const; - int& ActionFrame(CharacterStateMachine& machine) const; - float& LandingTimer(CharacterStateMachine& machine) const; private: CharacterStateId stateId_ = CharacterStateId::Idle; diff --git a/Game/include/character/states/jobs/swordman/SwordmanActionHandler.h b/Game/include/character/states/jobs/swordman/SwordmanActionHandler.h new file mode 100644 index 0000000..f24d3b7 --- /dev/null +++ b/Game/include/character/states/jobs/swordman/SwordmanActionHandler.h @@ -0,0 +1,66 @@ +#pragma once + +#include "character/states/CharacterStateBase.h" +#include + +namespace frostbite2D { + +struct SwordmanActionUpdateResult { + enum class Type { + Running, + Finished, + Transition, + }; + + static SwordmanActionUpdateResult Running() { + return {}; + } + + static SwordmanActionUpdateResult Finished() { + SwordmanActionUpdateResult result; + result.type = Type::Finished; + return result; + } + + static SwordmanActionUpdateResult TransitionTo(const std::string& requestedActionId, + bool requireBufferedInput = true) { + SwordmanActionUpdateResult result; + result.type = Type::Transition; + result.requestedActionId = requestedActionId; + result.requireBufferedInput = requireBufferedInput; + return result; + } + + Type type = Type::Running; + std::string requestedActionId; + bool requireBufferedInput = true; +}; + +class ISwordmanActionHandler { +public: + virtual ~ISwordmanActionHandler() = default; + + virtual const std::string& GetActionId() const = 0; + virtual void OnEnter(CharacterStateContext& context, + const CharacterActionDefinition& action) = 0; + virtual SwordmanActionUpdateResult OnUpdate(CharacterStateContext& context, + const CharacterActionDefinition& action) = 0; + virtual void OnExit(CharacterStateContext& context, + const CharacterActionDefinition& action) { + (void)context; + (void)action; + } +}; + +class SwordmanActionHandlerBase : public ISwordmanActionHandler { +public: + explicit SwordmanActionHandlerBase(std::string actionId); + ~SwordmanActionHandlerBase() override = default; + + const std::string& GetActionId() const override { return actionId_; } + +protected: + std::string actionId_; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/states/jobs/swordman/SwordmanAttackState.h b/Game/include/character/states/jobs/swordman/SwordmanAttackState.h index 443fe5c..ee383c8 100644 --- a/Game/include/character/states/jobs/swordman/SwordmanAttackState.h +++ b/Game/include/character/states/jobs/swordman/SwordmanAttackState.h @@ -1,6 +1,10 @@ #pragma once #include "character/states/CharacterStateBase.h" +#include "character/states/jobs/swordman/SwordmanActionHandler.h" +#include +#include +#include namespace frostbite2D { @@ -18,6 +22,11 @@ public: void OnExit(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId nextState) override; + +private: + ISwordmanActionHandler* FindHandler(const std::string& actionId) const; + + std::unordered_map> handlers_; }; } // namespace frostbite2D diff --git a/Game/include/character/states/jobs/swordman/SwordmanBasicAttackHandler.h b/Game/include/character/states/jobs/swordman/SwordmanBasicAttackHandler.h new file mode 100644 index 0000000..6e6c20f --- /dev/null +++ b/Game/include/character/states/jobs/swordman/SwordmanBasicAttackHandler.h @@ -0,0 +1,24 @@ +#pragma once + +#include "character/states/jobs/swordman/SwordmanActionHandler.h" + +namespace frostbite2D { + +class SwordmanBasicAttackHandler : public SwordmanActionHandlerBase { +public: + SwordmanBasicAttackHandler(); + + void OnEnter(CharacterStateContext& context, + const CharacterActionDefinition& action) override; + SwordmanActionUpdateResult OnUpdate(CharacterStateContext& context, + const CharacterActionDefinition& action) override; + void OnExit(CharacterStateContext& context, + const CharacterActionDefinition& action) override; + +private: + void BeginComboStep(CharacterStateContext& context, int comboStep); + + int comboStep_ = 0; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/states/jobs/swordman/SwordmanSkill1Handler.h b/Game/include/character/states/jobs/swordman/SwordmanSkill1Handler.h new file mode 100644 index 0000000..9023b83 --- /dev/null +++ b/Game/include/character/states/jobs/swordman/SwordmanSkill1Handler.h @@ -0,0 +1,17 @@ +#pragma once + +#include "character/states/jobs/swordman/SwordmanActionHandler.h" + +namespace frostbite2D { + +class SwordmanSkill1Handler : public SwordmanActionHandlerBase { +public: + SwordmanSkill1Handler(); + + void OnEnter(CharacterStateContext& context, + const CharacterActionDefinition& action) override; + SwordmanActionUpdateResult OnUpdate(CharacterStateContext& context, + const CharacterActionDefinition& action) override; +}; + +} // namespace frostbite2D diff --git a/Game/src/character/CharacterActionLibrary.cpp b/Game/src/character/CharacterActionLibrary.cpp index 7e78aa0..5ef0ae1 100644 --- a/Game/src/character/CharacterActionLibrary.cpp +++ b/Game/src/character/CharacterActionLibrary.cpp @@ -1,10 +1,8 @@ #include "character/CharacterActionLibrary.h" -#include -#include -#include #include #include #include +#include #include namespace frostbite2D { @@ -17,152 +15,47 @@ std::string toLower(const std::string& value) { 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]))) { + while (end > begin && std::isspace(static_cast(value[end - 1]))) { --end; } return value.substr(begin, end - begin); } -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); +std::string normalizeKey(const std::string& value) { + return toLower(trim(value)); } -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 成对写法 -bool isNewActionListPath(const std::string& path) { - return path.find("action_logic") != std::string::npos - && path.size() >= 4 - && path.substr(path.size() - 4) == ".act"; -} - -// action_list.lst only accepts #PVF_File entries like `swordman/action_logic/idle.act`. -std::vector> parseActionListEntries( - ScriptTokenStream& listStream) { - std::vector> entries; - std::vector tokens; - auto& pvf = PvfArchive::get(); - while (!listStream.IsEnd()) { - std::string token = trim(listStream.Next()); - if (!token.empty()) { - tokens.push_back(token); - } - } - - for (size_t i = 0; i + 1 < tokens.size(); i += 2) { - std::string rawActionPath = trim(tokens[i + 1]); - std::string actionPath = pvf.normalizePath(rawActionPath); - if (actionPath.empty() || !isNewActionListPath(actionPath)) { - continue; - } - entries.push_back({toLower(fileStem(actionPath)), actionPath}); - } - 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()) { +void addRegisteredAction(std::map& actions, + const std::string& actionId) { + std::string key = normalizeKey(actionId); + if (key.empty() || actions.count(key) > 0) { return; } - actions[definition.actionId] = std::move(definition); + + CharacterActionDefinition definition; + definition.actionId = key; + actions[key] = std::move(definition); +} + +void registerCommonActions(std::map& actions) { + addRegisteredAction(actions, "idle"); + addRegisteredAction(actions, "move"); + addRegisteredAction(actions, "run"); + addRegisteredAction(actions, "jump"); + addRegisteredAction(actions, "fall"); + addRegisteredAction(actions, "landing"); + addRegisteredAction(actions, "hurt_light"); +} + +void registerSwordmanActions(std::map& actions) { + addRegisteredAction(actions, "attack"); + addRegisteredAction(actions, "skill_1"); } } // namespace @@ -182,23 +75,34 @@ void CharacterCommandBuffer::Advance(float deltaTime) { void CharacterCommandBuffer::Submit(const CharacterCommand& command) { if (command.type == CharacterCommandType::Move) { moveAxis_ = command.moveAxis; + moveMode_ = command.moveMode; if (moveAxis_.lengthSquared() > 1.0f) { moveAxis_ = moveAxis_.normalized(); } + if (moveAxis_.lengthSquared() < 0.0001f) { + moveMode_ = CharacterMoveMode::None; + } return; } - buttons_.push_back({command.type, 0.0f}); + buttons_.push_back({command.type, command.actionId, 0.0f}); } CharacterIntent CharacterCommandBuffer::BuildIntent() const { CharacterIntent intent; intent.moveX = moveAxis_.x; intent.moveY = moveAxis_.y; + intent.moveMode = moveMode_; intent.wantJump = HasBuffered(CharacterCommandType::JumpPressed); - intent.wantAttack = HasBuffered(CharacterCommandType::AttackPressed); - intent.wantSkill1 = HasBuffered(CharacterCommandType::Skill1Pressed); - intent.wantDash = HasBuffered(CharacterCommandType::DashPressed); + for (const auto& button : buttons_) { + if (button.type != CharacterCommandType::ActionPressed || button.actionId.empty()) { + continue; + } + if (std::find(intent.requestedActions.begin(), intent.requestedActions.end(), + button.actionId) == intent.requestedActions.end()) { + intent.requestedActions.push_back(button.actionId); + } + } return intent; } @@ -221,20 +125,61 @@ void CharacterCommandBuffer::Consume(CharacterCommandType type) { } } +bool CharacterCommandBuffer::HasBufferedAction(const std::string& actionId) const { + std::string key = normalizeKey(actionId); + if (key.empty()) { + return false; + } + + for (const auto& button : buttons_) { + if (button.type == CharacterCommandType::ActionPressed + && normalizeKey(button.actionId) == key) { + return true; + } + } + return false; +} + +void CharacterCommandBuffer::ConsumeAction(const std::string& actionId) { + std::string key = normalizeKey(actionId); + if (key.empty()) { + return; + } + + auto it = std::find_if(buttons_.begin(), buttons_.end(), + [&key](const BufferedButton& button) { + return button.type == CharacterCommandType::ActionPressed + && normalizeKey(button.actionId) == key; + }); + if (it != buttons_.end()) { + buttons_.erase(it); + } +} + +void CharacterCommandBuffer::ClearBufferedActions() { + buttons_.erase( + std::remove_if(buttons_.begin(), buttons_.end(), + [](const BufferedButton& button) { + return button.type == CharacterCommandType::ActionPressed; + }), + buttons_.end()); +} + void CharacterCommandBuffer::Clear() { moveAxis_ = Vec2::Zero(); + moveMode_ = CharacterMoveMode::None; buttons_.clear(); } bool CharacterActionLibrary::LoadForConfig(const character::CharacterConfig& config) { actions_.clear(); - TryLoadPvfActionScripts(config); - return !actions_.empty(); + return BuildExplicitActionRegistry(config); } const CharacterActionDefinition* CharacterActionLibrary::FindAction( const std::string& actionId) const { - auto it = actions_.find(actionId); + std::string key = normalizeKey(actionId); + auto it = actions_.find(key); if (it != actions_.end()) { return &it->second; } @@ -263,85 +208,15 @@ std::string CharacterActionLibrary::DescribeActionIds() const { return stream.str(); } -bool CharacterActionLibrary::TryLoadPvfActionScripts( +bool CharacterActionLibrary::BuildExplicitActionRegistry( const character::CharacterConfig& config) { - std::string listPath = "character/" + config.jobTag + "/action_logic/action_list.lst"; - ScriptTokenStream listStream(listPath); - if (!listStream.IsValid()) { - return false; + registerCommonActions(actions_); + + if (config.jobTag == "swordman") { + registerSwordmanActions(actions_); } - auto entries = parseActionListEntries(listStream); - if (entries.empty()) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "CharacterActionLibrary: action_list parsed 0 entries from %s", - listPath.c_str()); - } - for (const auto& [actionId, relativePath] : entries) { - if (actionId.empty() || relativePath.empty()) { - continue; - } - - std::string fullActPath = relativePath.rfind("character/", 0) == 0 - ? relativePath - : "character/" + relativePath; - ScriptTokenStream actionStream(fullActPath); - 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(trim(actionStream.Next())); - } else if (token == "[animation tag]") { - definition.animationTag = trim(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(trim(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(trim(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 = trim(actionStream.Next()); - } - definition.frameEvents.push_back(frameEvent); - } - } - - addOrReplaceAction(actions_, std::move(definition)); - } - - return true; + return !actions_.empty(); } std::optional loadCharacterActionLibrary( diff --git a/Game/src/character/CharacterAnimation.cpp b/Game/src/character/CharacterAnimation.cpp index 6203507..71858b2 100644 --- a/Game/src/character/CharacterAnimation.cpp +++ b/Game/src/character/CharacterAnimation.cpp @@ -1,9 +1,11 @@ #include "character/CharacterAnimation.h" #include "character/CharacterObject.h" #include +#include #include #include #include +#include namespace frostbite2D { namespace { @@ -38,6 +40,8 @@ bool CharacterAnimation::Init(CharacterObject* parent, actionAnimations_.clear(); currentActionTag_.clear(); direction_ = 1; + actionFrameFlagCallback_ = nullptr; + actionEndCallback_ = nullptr; for (const auto& [actionName, actionPath] : config.animationPath) { for (const char* slotName : kAvatarParts) { @@ -142,6 +146,56 @@ std::string CharacterAnimation::DescribeAvailableActions() const { return stream.str(); } +Animation* CharacterAnimation::GetPrimaryAnimation(const std::string& actionName) const { + auto it = actionAnimations_.find(actionName); + if (it == actionAnimations_.end()) { + return nullptr; + } + + for (const auto& animation : it->second) { + if (animation && animation->IsUsable()) { + return animation.Get(); + } + } + + for (const auto& animation : it->second) { + if (animation) { + return animation.Get(); + } + } + + return nullptr; +} + +Animation* CharacterAnimation::GetCurrentPrimaryAnimation() const { + if (currentActionTag_.empty()) { + return nullptr; + } + return GetPrimaryAnimation(currentActionTag_); +} + +void CharacterAnimation::RefreshRuntimeCallbacks() { + auto currentIt = actionAnimations_.find(currentActionTag_); + if (currentIt == actionAnimations_.end()) { + return; + } + + Animation* primaryAnimation = GetPrimaryAnimation(currentActionTag_); + for (auto& animation : currentIt->second) { + if (!animation) { + continue; + } + + if (animation.Get() == primaryAnimation) { + animation->changeFrameCallback_ = actionFrameFlagCallback_; + animation->endCallback_ = actionEndCallback_; + } else { + animation->changeFrameCallback_ = nullptr; + animation->endCallback_ = nullptr; + } + } +} + bool CharacterAnimation::SetAction(const std::string& actionName) { auto nextIt = actionAnimations_.find(actionName); if (nextIt == actionAnimations_.end()) { @@ -172,6 +226,7 @@ bool CharacterAnimation::SetAction(const std::string& actionName) { animation->SetVisible(true); } currentActionTag_ = actionName; + RefreshRuntimeCallbacks(); SetDirection(direction_); return true; } @@ -191,4 +246,51 @@ void CharacterAnimation::SetDirection(int direction) { } } +bool CharacterAnimation::IsCurrentActionFinished() const { + const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); + if (!primaryAnimation) { + return false; + } + return !primaryAnimation->IsUsable(); +} + +int CharacterAnimation::GetCurrentFrameIndex() const { + const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); + return primaryAnimation ? primaryAnimation->GetCurrentFrameIndex() : 0; +} + +int CharacterAnimation::GetCurrentFrameCount() const { + const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); + return primaryAnimation ? primaryAnimation->GetTotalFrameCount() : 0; +} + +float CharacterAnimation::GetCurrentNormalizedProgress() const { + int totalFrames = GetCurrentFrameCount(); + if (totalFrames <= 1) { + return IsCurrentActionFinished() ? 1.0f : 0.0f; + } + + float progress = static_cast(GetCurrentFrameIndex()) + / static_cast(totalFrames - 1); + return std::clamp(progress, 0.0f, 1.0f); +} + +animation::AniFrame CharacterAnimation::GetCurrentFrameInfo() const { + const Animation* primaryAnimation = GetCurrentPrimaryAnimation(); + if (!primaryAnimation) { + return animation::AniFrame(); + } + return primaryAnimation->GetCurrentFrameInfo(); +} + +void CharacterAnimation::SetActionFrameFlagCallback(ActionFrameFlagCallback callback) { + actionFrameFlagCallback_ = std::move(callback); + RefreshRuntimeCallbacks(); +} + +void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) { + actionEndCallback_ = std::move(callback); + RefreshRuntimeCallbacks(); +} + } // namespace frostbite2D diff --git a/Game/src/character/CharacterInputRouter.cpp b/Game/src/character/CharacterInputRouter.cpp index c5e5117..cf78ef4 100644 --- a/Game/src/character/CharacterInputRouter.cpp +++ b/Game/src/character/CharacterInputRouter.cpp @@ -1,45 +1,115 @@ #include "character/CharacterInputRouter.h" #include +#include #include namespace frostbite2D { namespace { +constexpr std::uint32_t kKeyboardRunDoubleTapWindowMs = 250; +constexpr float kAnalogRunThreshold = 0.75f; + float applyDeadZone(float value, float deadZone) { return std::fabs(value) >= deadZone ? value : 0.0f; } } // namespace +void CharacterInputRouter::QueueAction(const std::string& actionId) { + if (actionId.empty()) { + return; + } + if (std::find(actionRequests_.begin(), actionRequests_.end(), actionId) + == actionRequests_.end()) { + actionRequests_.push_back(actionId); + } +} + +bool CharacterInputRouter::IsDirectionPressed(KeyboardMoveDirection direction) const { + switch (direction) { + case KeyboardMoveDirection::Left: + return left_; + case KeyboardMoveDirection::Right: + return right_; + case KeyboardMoveDirection::Up: + return up_; + case KeyboardMoveDirection::Down: + return down_; + default: + return false; + } +} + +bool CharacterInputRouter::HasDirectionalInput() const { + return left_ || right_ || up_ || down_; +} + +void CharacterInputRouter::RefreshKeyboardMoveMode() { + if (!HasDirectionalInput()) { + keyboardMoveMode_ = CharacterMoveMode::None; + runDirection_ = KeyboardMoveDirection::None; + return; + } + + if (runDirection_ != KeyboardMoveDirection::None && IsDirectionPressed(runDirection_)) { + keyboardMoveMode_ = CharacterMoveMode::Run; + return; + } + + runDirection_ = KeyboardMoveDirection::None; + keyboardMoveMode_ = CharacterMoveMode::Walk; +} + bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) { + auto handleDirectionPress = [&](KeyboardMoveDirection direction, + bool& pressedFlag, + std::uint32_t& lastTapTimestamp) { + pressedFlag = true; + std::uint32_t now = event.getTimestamp(); + bool isDoubleTap = lastTapTimestamp > 0 + && now >= lastTapTimestamp + && now - lastTapTimestamp <= kKeyboardRunDoubleTapWindowMs; + lastTapTimestamp = now; + + if (isDoubleTap) { + keyboardMoveMode_ = CharacterMoveMode::Run; + runDirection_ = direction; + } else if (keyboardMoveMode_ != CharacterMoveMode::Run + || !IsDirectionPressed(runDirection_)) { + keyboardMoveMode_ = CharacterMoveMode::Walk; + runDirection_ = KeyboardMoveDirection::None; + } + return true; + }; + switch (event.getKeyCode()) { case KeyCode::a: case KeyCode::Left: - left_ = true; - return true; + return handleDirectionPress( + KeyboardMoveDirection::Left, left_, leftLastTapTimestamp_); case KeyCode::d: case KeyCode::Right: - right_ = true; - return true; + return handleDirectionPress( + KeyboardMoveDirection::Right, right_, rightLastTapTimestamp_); case KeyCode::w: case KeyCode::Up: - up_ = true; - return true; + return handleDirectionPress( + KeyboardMoveDirection::Up, up_, upLastTapTimestamp_); case KeyCode::s: case KeyCode::Down: - down_ = true; + return handleDirectionPress( + KeyboardMoveDirection::Down, down_, downLastTapTimestamp_); + case KeyCode::x: + QueueAction("attack"); return true; - case KeyCode::j: - attackPressed_ = true; - return true; - case KeyCode::k: + case KeyCode::c: jumpPressed_ = true; return true; - case KeyCode::l: - skill1Pressed_ = true; + case KeyCode::z: + QueueAction("skill_1"); return true; case KeyCode::LShift: - dashPressed_ = true; + QueueAction("dash"); return true; default: return false; @@ -47,23 +117,25 @@ bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) { } bool CharacterInputRouter::OnKeyUp(const KeyUpEvent& event) { + auto handleDirectionRelease = [&](bool& pressedFlag) { + pressedFlag = false; + RefreshKeyboardMoveMode(); + return true; + }; + switch (event.getKeyCode()) { case KeyCode::a: case KeyCode::Left: - left_ = false; - return true; + return handleDirectionRelease(left_); case KeyCode::d: case KeyCode::Right: - right_ = false; - return true; + return handleDirectionRelease(right_); case KeyCode::w: case KeyCode::Up: - up_ = false; - return true; + return handleDirectionRelease(up_); case KeyCode::s: case KeyCode::Down: - down_ = false; - return true; + return handleDirectionRelease(down_); default: return false; } @@ -88,14 +160,14 @@ bool CharacterInputRouter::OnJoystickButtonDown( jumpPressed_ = true; return true; case SDL_CONTROLLER_BUTTON_X: - attackPressed_ = true; + QueueAction("attack"); return true; case SDL_CONTROLLER_BUTTON_B: case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: - skill1Pressed_ = true; + QueueAction("skill_1"); return true; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: - dashPressed_ = true; + QueueAction("dash"); return true; default: return false; @@ -110,23 +182,41 @@ bool CharacterInputRouter::OnJoystickButtonUp( void CharacterInputRouter::EmitCommands(CharacterCommandBuffer& commandBuffer) { Vec2 moveAxis(leftStickX_, leftStickY_); - if (moveAxis.lengthSquared() < 0.0001f) { + CharacterMoveMode moveMode = CharacterMoveMode::None; + float stickMagnitudeSquared = moveAxis.lengthSquared(); + if (stickMagnitudeSquared >= joystickDeadZone_ * joystickDeadZone_) { + float stickMagnitude = std::sqrt(stickMagnitudeSquared); + if (stickMagnitude > 1.0f) { + moveAxis = moveAxis.normalized(); + stickMagnitude = 1.0f; + } + moveMode = stickMagnitude >= kAnalogRunThreshold + ? CharacterMoveMode::Run + : CharacterMoveMode::Walk; + } else { moveAxis.x = (right_ ? 1.0f : 0.0f) - (left_ ? 1.0f : 0.0f); moveAxis.y = (down_ ? 1.0f : 0.0f) - (up_ ? 1.0f : 0.0f); + if (moveAxis.lengthSquared() > 1.0f) { + moveAxis = moveAxis.normalized(); + } + if (moveAxis.lengthSquared() >= 0.0001f) { + moveMode = keyboardMoveMode_ == CharacterMoveMode::Run + ? CharacterMoveMode::Run + : CharacterMoveMode::Walk; + } } - commandBuffer.Submit({CharacterCommandType::Move, moveAxis, 0.0f}); + commandBuffer.Submit( + {CharacterCommandType::Move, moveAxis, 0.0f, moveMode, std::string()}); if (jumpPressed_) { - commandBuffer.Submit({CharacterCommandType::JumpPressed, Vec2::Zero(), 0.0f}); + commandBuffer.Submit( + {CharacterCommandType::JumpPressed, Vec2::Zero(), 0.0f, + CharacterMoveMode::None, std::string()}); } - 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}); + for (const auto& actionId : actionRequests_) { + commandBuffer.Submit( + {CharacterCommandType::ActionPressed, Vec2::Zero(), 0.0f, + CharacterMoveMode::None, actionId}); } ClearFrameState(); @@ -134,9 +224,7 @@ void CharacterInputRouter::EmitCommands(CharacterCommandBuffer& commandBuffer) { void CharacterInputRouter::ClearFrameState() { jumpPressed_ = false; - attackPressed_ = false; - skill1Pressed_ = false; - dashPressed_ = false; + actionRequests_.clear(); } } // namespace frostbite2D diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 4f2f6d1..8e987bc 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace frostbite2D { namespace { @@ -45,6 +46,9 @@ bool CharacterObject::Construction(int jobId) { actionLibrary_ = CharacterActionLibrary(); commandBuffer_.Clear(); currentIntent_ = CharacterIntent(); + runtimeBlackboard_.Clear(); + pendingActionContext_.Clear(); + currentActionContext_.Clear(); stateMachine_.Reset(); motor_ = CharacterMotor(); status_ = CharacterStatus(); @@ -76,12 +80,10 @@ bool CharacterObject::Construction(int jobId) { } AddChild(animationManager_); - const CharacterActionDefinition* idleAction = RequireAction( - "idle", "CharacterObject::Construction"); - if (!idleAction) { + if (!RequireAction("idle", "CharacterObject::Construction")) { return false; } - if (!PlayActionDefinition(*idleAction, "CharacterObject::Construction")) { + if (!SetActionStrict("rest", "CharacterObject::Construction", "idle")) { return false; } @@ -144,6 +146,68 @@ void CharacterObject::PushCommand(const CharacterCommand& command) { commandBuffer_.Submit(command); } +CharacterActionContextData& CharacterObject::PreparePendingActionContext( + const std::string& requestedActionId, + const std::string& sourceActionId, + CharacterStateId sourceStateId) { + pendingActionContext_.Clear(); + pendingActionContext_.valid = true; + pendingActionContext_.requestedActionId = requestedActionId; + pendingActionContext_.sourceActionId = sourceActionId; + pendingActionContext_.sourceStateId = sourceStateId; + pendingActionContext_.hasSourceStateId = + sourceStateId != CharacterStateId::Idle || !sourceActionId.empty(); + return pendingActionContext_; +} + +void CharacterObject::ClearPendingActionContext() { + pendingActionContext_.Clear(); +} + +const CharacterActionContextData* CharacterObject::GetPendingActionContext() const { + return pendingActionContext_.valid ? &pendingActionContext_ : nullptr; +} + +CharacterActionContextData* CharacterObject::GetPendingActionContextMutable() { + return pendingActionContext_.valid ? &pendingActionContext_ : nullptr; +} + +const CharacterActionContextData* CharacterObject::GetCurrentActionContext() const { + return currentActionContext_.valid ? ¤tActionContext_ : nullptr; +} + +CharacterActionContextData* CharacterObject::GetCurrentActionContextMutable() { + return currentActionContext_.valid ? ¤tActionContext_ : nullptr; +} + +void CharacterObject::ClearCurrentActionContext() { + currentActionContext_.Clear(); +} + +void CharacterObject::CommitPendingActionContext( + const std::string& defaultRequestedActionId, + const std::string& defaultSourceActionId, + CharacterStateId defaultSourceStateId) { + currentActionContext_.Clear(); + if (pendingActionContext_.valid) { + currentActionContext_ = pendingActionContext_; + } else { + currentActionContext_.valid = true; + } + + if (currentActionContext_.requestedActionId.empty()) { + currentActionContext_.requestedActionId = defaultRequestedActionId; + } + if (currentActionContext_.sourceActionId.empty()) { + currentActionContext_.sourceActionId = defaultSourceActionId; + } + if (!currentActionContext_.hasSourceStateId) { + currentActionContext_.SetSourceStateId(defaultSourceStateId); + } + + pendingActionContext_.Clear(); +} + void CharacterObject::ApplyHit(const HitContext& hit) { if (status_.invincible) { return; @@ -224,20 +288,48 @@ const CharacterActionDefinition* CharacterObject::RequireAction( 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; - } - return SetActionStrict(action.animationTag, phase, action.actionId); -} - void CharacterObject::PlayAnimationTag(const std::string& actionTag) { SetActionStrict(actionTag, "CharacterObject::PlayAnimationTag", std::string()); } +bool CharacterObject::HasAnimationTag(const std::string& actionTag) const { + return animationManager_ && animationManager_->HasAction(actionTag); +} + +bool CharacterObject::IsCurrentAnimationFinished() const { + return animationManager_ && animationManager_->IsCurrentActionFinished(); +} + +int CharacterObject::GetCurrentAnimationFrameIndex() const { + return animationManager_ ? animationManager_->GetCurrentFrameIndex() : 0; +} + +int CharacterObject::GetCurrentAnimationFrameCount() const { + return animationManager_ ? animationManager_->GetCurrentFrameCount() : 0; +} + +float CharacterObject::GetCurrentAnimationProgressNormalized() const { + return animationManager_ ? animationManager_->GetCurrentNormalizedProgress() : 0.0f; +} + +animation::AniFrame CharacterObject::GetCurrentAnimationFrameInfo() const { + return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame(); +} + +void CharacterObject::SetAnimationFrameFlagCallback( + CharacterAnimation::ActionFrameFlagCallback callback) { + if (animationManager_) { + animationManager_->SetActionFrameFlagCallback(std::move(callback)); + } +} + +void CharacterObject::SetAnimationEndCallback( + CharacterAnimation::ActionEndCallback callback) { + if (animationManager_) { + animationManager_->SetActionEndCallback(std::move(callback)); + } +} + std::string CharacterObject::DescribeConfiguredAnimationTags() const { if (!config_ || config_->animationPath.empty()) { return ""; @@ -273,6 +365,30 @@ void CharacterObject::ReportFatalCharacterError( << " currentActionId: " << NonEmptyOrPlaceholder(stateMachine_.GetCurrentActionId(), "") << '\n' << " currentAnimationTag: " << NonEmptyOrPlaceholder(currentAction_, "") << '\n' + << " currentActionContext.requestedActionId: " + << NonEmptyOrPlaceholder( + currentActionContext_.valid ? currentActionContext_.requestedActionId + : std::string(), + "") + << '\n' + << " currentActionContext.sourceActionId: " + << NonEmptyOrPlaceholder( + currentActionContext_.valid ? currentActionContext_.sourceActionId + : std::string(), + "") + << '\n' + << " pendingActionContext.requestedActionId: " + << NonEmptyOrPlaceholder( + pendingActionContext_.valid ? pendingActionContext_.requestedActionId + : std::string(), + "") + << '\n' + << " pendingActionContext.sourceActionId: " + << NonEmptyOrPlaceholder( + pendingActionContext_.valid ? pendingActionContext_.sourceActionId + : std::string(), + "") + << '\n' << " requestedActionId: " << NonEmptyOrPlaceholder(requestedActionId, "") << '\n' << " requestedAnimationTag: " diff --git a/Game/src/character/CharacterStateMachine.cpp b/Game/src/character/CharacterStateMachine.cpp index 0a252d1..4d92bbe 100644 --- a/Game/src/character/CharacterStateMachine.cpp +++ b/Game/src/character/CharacterStateMachine.cpp @@ -3,6 +3,15 @@ #include "character/states/CharacterStateRegistry.h" namespace frostbite2D { +namespace { + +bool IsCompatiblePendingActionContext(const CharacterActionContextData* context, + const std::string& requestedActionId) { + return !context || context->requestedActionId.empty() + || context->requestedActionId == requestedActionId; +} + +} // namespace void CharacterStateMachine::Configure(const character::CharacterConfig& config) { ClearRegisteredStates(); @@ -16,9 +25,6 @@ void CharacterStateMachine::Reset() { currentActionId_.clear(); currentAction_ = nullptr; stateTime_ = 0.0f; - actionFrameProgress_ = 0.0f; - actionFrame_ = 0; - landingTimer_ = 0.0f; } void CharacterStateMachine::Update(CharacterObject& owner, @@ -41,14 +47,14 @@ void CharacterStateMachine::ForceHurt(CharacterObject& owner, return; } + owner.ClearPendingActionContext(); + const CharacterActionDefinition* previousAction = currentAction_; currentAction_ = hurtAction; currentActionId_ = hurtAction->actionId; if (currentState_ == CharacterStateId::Hurt) { InvokeStateHook(owner, previousAction, "on_exit"); stateTime_ = 0.0f; - actionFrameProgress_ = 0.0f; - actionFrame_ = 0; CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(), owner.GetLastDeltaTime()}; if (auto* hurtNode = FindStateNode(CharacterStateId::Hurt)) { @@ -66,20 +72,19 @@ bool CharacterStateMachine::CanAcceptGroundActions() const { } 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; + return currentState_ == CharacterStateId::Action + || currentState_ == CharacterStateId::Hurt + || currentState_ == CharacterStateId::Dead; } bool CharacterStateMachine::CanTurn() const { - return !(currentState_ == CharacterStateId::Action && currentAction_ - && !currentAction_->canTurn); + return currentState_ != CharacterStateId::Action + && currentState_ != CharacterStateId::Hurt + && currentState_ != CharacterStateId::Dead; } void CharacterStateMachine::RegisterState(std::unique_ptr stateNode) { @@ -115,6 +120,13 @@ void CharacterStateMachine::ChangeState(CharacterObject& owner, previousNode->OnExit(*this, context, nextState); } + if (previousState == CharacterStateId::Action && nextState != CharacterStateId::Action) { + owner.ClearCurrentActionContext(); + } + if (nextState != CharacterStateId::Action) { + owner.ClearPendingActionContext(); + } + currentState_ = nextState; stateTime_ = 0.0f; if (nextState != CharacterStateId::Action && nextState != CharacterStateId::Hurt) { @@ -135,21 +147,28 @@ void CharacterStateMachine::EnterAction(CharacterObject& owner, return; } - const CharacterActionDefinition* previousAction = currentAction_; - currentAction_ = action; - currentActionId_ = action->actionId; + const std::string previousActionId = currentActionId_; + CharacterStateId previousState = currentState_; if (currentState_ == CharacterStateId::Action) { - InvokeStateHook(owner, previousAction, "on_exit"); - stateTime_ = 0.0f; - actionFrameProgress_ = 0.0f; - actionFrame_ = 0; CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(), owner.GetLastDeltaTime()}; + if (auto* actionNode = FindStateNode(CharacterStateId::Action)) { + actionNode->OnExit(*this, context, CharacterStateId::Action); + } + + currentAction_ = action; + currentActionId_ = action->actionId; + owner.CommitPendingActionContext(currentActionId_, previousActionId, previousState); + stateTime_ = 0.0f; if (auto* actionNode = FindStateNode(CharacterStateId::Action)) { actionNode->OnEnter(*this, context, CharacterStateId::Action); } return; } + + currentAction_ = action; + currentActionId_ = action->actionId; + owner.CommitPendingActionContext(currentActionId_, previousActionId, previousState); ChangeState(owner, CharacterStateId::Action); } @@ -164,16 +183,38 @@ void CharacterStateMachine::InvokeStateHook(CharacterObject& owner, bool CharacterStateMachine::TryStartAction(CharacterObject& owner, CharacterCommandBuffer& commandBuffer, - CharacterCommandType commandType, + const std::string& requestedActionId, const std::string& actionId) { - if (!commandBuffer.HasBuffered(commandType)) { + if (!commandBuffer.HasBufferedAction(requestedActionId)) { + if (IsCompatiblePendingActionContext(owner.GetPendingActionContext(), requestedActionId)) { + owner.ClearPendingActionContext(); + } return false; } + commandBuffer.ConsumeAction(requestedActionId); + return StartAction(owner, requestedActionId, actionId); +} + +bool CharacterStateMachine::StartAction(CharacterObject& owner, + const std::string& requestedActionId, + const std::string& actionId) { + const CharacterActionContextData* pendingActionContext = owner.GetPendingActionContext(); + if (!IsCompatiblePendingActionContext(pendingActionContext, requestedActionId)) { + owner.ReportFatalCharacterError("CharacterStateMachine::StartAction", + "prepared action context does not match requested action", + requestedActionId); + owner.ClearPendingActionContext(); + return false; + } + if (!pendingActionContext) { + owner.PreparePendingActionContext(requestedActionId, currentActionId_, currentState_); + } + const CharacterActionDefinition* action = owner.RequireAction( - actionId, "CharacterStateMachine::TryStartAction"); - commandBuffer.Consume(commandType); + actionId, "CharacterStateMachine::StartAction"); if (!action) { + owner.ClearPendingActionContext(); return false; } @@ -190,34 +231,4 @@ bool CharacterStateMachine::TryStartRegisteredAction(CharacterStateContext& cont return actionNode->TryEnter(*this, context); } -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/character/states/CharacterStateBase.cpp b/Game/src/character/states/CharacterStateBase.cpp index 68eadbf..4672f07 100644 --- a/Game/src/character/states/CharacterStateBase.cpp +++ b/Game/src/character/states/CharacterStateBase.cpp @@ -18,15 +18,6 @@ void CharacterStateBase::ChangeState(CharacterStateMachine& machine, machine.ChangeState(context.owner, nextState); } -void CharacterStateBase::PlayActionById(CharacterObject& owner, - const std::string& actionId) const { - const auto* action = owner.RequireAction(actionId, "CharacterStateBase::PlayActionById"); - if (!action) { - return; - } - owner.PlayActionDefinition(*action, "CharacterStateBase::PlayActionById"); -} - void CharacterStateBase::InvokeHook(CharacterStateMachine& machine, CharacterObject& owner, const CharacterActionDefinition* action, @@ -36,9 +27,29 @@ void CharacterStateBase::InvokeHook(CharacterStateMachine& machine, bool CharacterStateBase::TryStartAction(CharacterStateMachine& machine, CharacterStateContext& context, - CharacterCommandType commandType, const std::string& actionId) const { - return machine.TryStartAction(context.owner, context.commandBuffer, commandType, actionId); + return machine.TryStartAction(context.owner, context.commandBuffer, actionId, actionId); +} + +bool CharacterStateBase::TryStartAction(CharacterStateMachine& machine, + CharacterStateContext& context, + const std::string& requestedActionId, + const std::string& actionId) const { + return machine.TryStartAction( + context.owner, context.commandBuffer, requestedActionId, actionId); +} + +bool CharacterStateBase::StartAction(CharacterStateMachine& machine, + CharacterStateContext& context, + const std::string& actionId) const { + return machine.StartAction(context.owner, actionId, actionId); +} + +bool CharacterStateBase::StartAction(CharacterStateMachine& machine, + CharacterStateContext& context, + const std::string& requestedActionId, + const std::string& actionId) const { + return machine.StartAction(context.owner, requestedActionId, actionId); } bool CharacterStateBase::TryStartRegisteredAction(CharacterStateMachine& machine, @@ -46,13 +57,6 @@ bool CharacterStateBase::TryStartRegisteredAction(CharacterStateMachine& machine return machine.TryStartRegisteredAction(context); } -void CharacterStateBase::ApplyFrameEvents(CharacterStateMachine& machine, - CharacterObject& owner, - int previousFrame, - int currentFrame) const { - machine.ApplyFrameEvents(owner, previousFrame, currentFrame); -} - const CharacterActionDefinition* CharacterStateBase::GetCurrentAction( const CharacterStateMachine& machine) const { return machine.currentAction_; @@ -79,16 +83,4 @@ float& CharacterStateBase::StateTime(CharacterStateMachine& machine) const { return machine.stateTime_; } -float& CharacterStateBase::ActionFrameProgress(CharacterStateMachine& machine) const { - return machine.actionFrameProgress_; -} - -int& CharacterStateBase::ActionFrame(CharacterStateMachine& machine) const { - return machine.actionFrame_; -} - -float& CharacterStateBase::LandingTimer(CharacterStateMachine& machine) const { - return machine.landingTimer_; -} - } // namespace frostbite2D diff --git a/Game/src/character/states/common/FallState.cpp b/Game/src/character/states/common/FallState.cpp index 923499b..87c7b33 100644 --- a/Game/src/character/states/common/FallState.cpp +++ b/Game/src/character/states/common/FallState.cpp @@ -11,7 +11,7 @@ void FallState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - PlayActionById(context.owner, "fall"); + context.owner.PlayAnimationTag("fall"); InvokeHook(machine, context.owner, context.owner.FindAction("fall"), "on_enter"); } @@ -32,12 +32,7 @@ void FallState::OnUpdate(CharacterStateMachine& machine, return; } - if (context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) { - context.commandBuffer.Consume(CharacterCommandType::AttackPressed); - } - if (context.commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) { - context.commandBuffer.Consume(CharacterCommandType::Skill1Pressed); - } + context.commandBuffer.ClearBufferedActions(); } } // namespace frostbite2D diff --git a/Game/src/character/states/common/HurtState.cpp b/Game/src/character/states/common/HurtState.cpp index 497ab6c..55ff269 100644 --- a/Game/src/character/states/common/HurtState.cpp +++ b/Game/src/character/states/common/HurtState.cpp @@ -26,8 +26,6 @@ void HurtState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - ActionFrameProgress(machine) = 0.0f; - ActionFrame(machine) = 0; const CharacterActionDefinition* action = GetCurrentAction(machine); if (!action) { context.owner.ReportFatalCharacterError("HurtState::OnEnter", @@ -35,9 +33,7 @@ void HurtState::OnEnter(CharacterStateMachine& machine, return; } - if (!context.owner.PlayActionDefinition(*action, "HurtState::OnEnter")) { - return; - } + context.owner.PlayAnimationTag(action->actionId); InvokeHook(machine, context.owner, action, "on_enter"); } @@ -56,9 +52,7 @@ void HurtState::OnUpdate(CharacterStateMachine& machine, InvokeHook(machine, owner, action, "on_update"); - ActionFrameProgress(machine) += context.deltaTime * 60.0f; - ActionFrame(machine) = std::max(0, static_cast(ActionFrameProgress(machine))); - if (action->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) { + if (owner.IsCurrentAnimationFinished()) { ClearCurrentAction(machine); context.owner.SetFacing(motor.facing); ChangeState(machine, context, ResolveInterruptedState(context)); diff --git a/Game/src/character/states/common/IdleState.cpp b/Game/src/character/states/common/IdleState.cpp index 4d7461e..0b4ff9e 100644 --- a/Game/src/character/states/common/IdleState.cpp +++ b/Game/src/character/states/common/IdleState.cpp @@ -6,7 +6,8 @@ namespace frostbite2D { namespace { bool HasMoveIntent(const CharacterIntent& intent) { - return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f; + return intent.moveMode != CharacterMoveMode::None + && (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f); } } // namespace @@ -19,7 +20,7 @@ void IdleState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - PlayActionById(context.owner, "idle"); + context.owner.PlayAnimationTag("rest"); InvokeHook(machine, context.owner, context.owner.FindAction("idle"), "on_enter"); } @@ -43,8 +44,6 @@ void IdleState::OnUpdate(CharacterStateMachine& machine, } if (HasMoveIntent(context.intent)) { - motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY); - owner.SetFacing(motor.facing); ChangeState(machine, context, CharacterStateId::Move); return; } diff --git a/Game/src/character/states/common/JumpState.cpp b/Game/src/character/states/common/JumpState.cpp index 32b180f..8e49ff8 100644 --- a/Game/src/character/states/common/JumpState.cpp +++ b/Game/src/character/states/common/JumpState.cpp @@ -11,7 +11,7 @@ void JumpState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - PlayActionById(context.owner, "jump"); + context.owner.PlayAnimationTag("jump"); InvokeHook(machine, context.owner, context.owner.FindAction("jump"), "on_enter"); } @@ -32,12 +32,7 @@ void JumpState::OnUpdate(CharacterStateMachine& machine, return; } - if (context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) { - context.commandBuffer.Consume(CharacterCommandType::AttackPressed); - } - if (context.commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) { - context.commandBuffer.Consume(CharacterCommandType::Skill1Pressed); - } + context.commandBuffer.ClearBufferedActions(); } } // namespace frostbite2D diff --git a/Game/src/character/states/common/LandingState.cpp b/Game/src/character/states/common/LandingState.cpp index 43b54e4..a06b880 100644 --- a/Game/src/character/states/common/LandingState.cpp +++ b/Game/src/character/states/common/LandingState.cpp @@ -5,10 +5,9 @@ namespace frostbite2D { namespace { -constexpr float kLandingDuration = 0.08f; - bool HasMoveIntent(const CharacterIntent& intent) { - return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f; + return intent.moveMode != CharacterMoveMode::None + && (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f); } } // namespace @@ -21,8 +20,7 @@ void LandingState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - LandingTimer(machine) = 0.0f; - PlayActionById(context.owner, "landing"); + context.owner.PlayAnimationTag("landing"); InvokeHook(machine, context.owner, context.owner.FindAction("landing"), "on_enter"); } @@ -35,9 +33,8 @@ void LandingState::OnUpdate(CharacterStateMachine& machine, return; } - LandingTimer(machine) += context.deltaTime; motor.StopGroundMovement(); - if (LandingTimer(machine) < kLandingDuration) { + if (!owner.IsCurrentAnimationFinished()) { return; } @@ -52,8 +49,6 @@ void LandingState::OnUpdate(CharacterStateMachine& machine, } if (HasMoveIntent(context.intent)) { - motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY); - owner.SetFacing(motor.facing); ChangeState(machine, context, CharacterStateId::Move); return; } diff --git a/Game/src/character/states/common/MoveState.cpp b/Game/src/character/states/common/MoveState.cpp index fc3ee96..57f3a44 100644 --- a/Game/src/character/states/common/MoveState.cpp +++ b/Game/src/character/states/common/MoveState.cpp @@ -5,8 +5,33 @@ namespace frostbite2D { namespace { +constexpr float kWalkMoveSpeedScale = 0.65f; +constexpr float kRunMoveSpeedScale = 1.0f; + bool HasMoveIntent(const CharacterIntent& intent) { - return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f; + return intent.moveMode != CharacterMoveMode::None + && (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f); +} + +float ResolveMoveSpeedScale(const CharacterIntent& intent) { + return intent.moveMode == CharacterMoveMode::Run + ? kRunMoveSpeedScale + : kWalkMoveSpeedScale; +} + +const char* ResolveMoveActionId(const CharacterIntent& intent) { + return intent.moveMode == CharacterMoveMode::Run ? "run" : "move"; +} + +const char* ResolveMoveAnimationTag(const CharacterIntent& intent) { + return intent.moveMode == CharacterMoveMode::Run ? "run" : "move"; +} + +void SyncMovePresentation(CharacterObject& owner, const CharacterIntent& intent) { + const char* animationTag = ResolveMoveAnimationTag(intent); + if (owner.GetCurrentAction() != animationTag) { + owner.PlayAnimationTag(animationTag); + } } } // namespace @@ -19,8 +44,18 @@ void MoveState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - PlayActionById(context.owner, "move"); - InvokeHook(machine, context.owner, context.owner.FindAction("move"), "on_enter"); + auto& owner = context.owner; + auto& motor = owner.GetMotorMutable(); + if (!HasMoveIntent(context.intent)) { + motor.StopGroundMovement(); + return; + } + + motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, + ResolveMoveSpeedScale(context.intent)); + owner.SetFacing(motor.facing); + owner.PlayAnimationTag(ResolveMoveAnimationTag(context.intent)); + InvokeHook(machine, owner, owner.FindAction(ResolveMoveActionId(context.intent)), "on_enter"); } void MoveState::OnUpdate(CharacterStateMachine& machine, @@ -42,12 +77,16 @@ void MoveState::OnUpdate(CharacterStateMachine& machine, return; } - motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY); - owner.SetFacing(motor.facing); if (!HasMoveIntent(context.intent)) { motor.StopGroundMovement(); ChangeState(machine, context, CharacterStateId::Idle); + return; } + + motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, + ResolveMoveSpeedScale(context.intent)); + owner.SetFacing(motor.facing); + SyncMovePresentation(owner, context.intent); } } // namespace frostbite2D diff --git a/Game/src/character/states/jobs/swordman/SwordmanActionHandler.cpp b/Game/src/character/states/jobs/swordman/SwordmanActionHandler.cpp new file mode 100644 index 0000000..81dc4ea --- /dev/null +++ b/Game/src/character/states/jobs/swordman/SwordmanActionHandler.cpp @@ -0,0 +1,10 @@ +#include "character/states/jobs/swordman/SwordmanActionHandler.h" +#include + +namespace frostbite2D { + +SwordmanActionHandlerBase::SwordmanActionHandlerBase(std::string actionId) + : actionId_(std::move(actionId)) { +} + +} // namespace frostbite2D diff --git a/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp b/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp index 0581385..bec3fb4 100644 --- a/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp +++ b/Game/src/character/states/jobs/swordman/SwordmanAttackState.cpp @@ -1,16 +1,16 @@ #include "character/states/jobs/swordman/SwordmanAttackState.h" #include "character/CharacterObject.h" -#include +#include "character/states/jobs/swordman/SwordmanActionHandler.h" +#include "character/states/jobs/swordman/SwordmanBasicAttackHandler.h" +#include "character/states/jobs/swordman/SwordmanSkill1Handler.h" #include namespace frostbite2D { namespace { -constexpr float kLogicFramesPerSecond = 60.0f; -constexpr float kMovementActionScale = 0.35f; - bool HasMoveIntent(const CharacterIntent& intent) { - return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f; + return intent.moveMode != CharacterMoveMode::None + && (std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f); } CharacterStateId ResolveGroundState(const CharacterIntent& intent) { @@ -21,22 +21,43 @@ CharacterStateId ResolveGroundState(const CharacterIntent& intent) { SwordmanAttackState::SwordmanAttackState() : CharacterStateBase(CharacterStateId::Action) { + handlers_.emplace("attack", std::make_unique()); + handlers_.emplace("skill_1", std::make_unique()); } bool SwordmanAttackState::TryEnter(CharacterStateMachine& machine, CharacterStateContext& context) { - if (TryStartAction(machine, context, CharacterCommandType::Skill1Pressed, "skill_1")) { - return true; + for (const auto& requestedActionId : context.intent.requestedActions) { + const CharacterActionDefinition* action = context.owner.FindAction(requestedActionId); + if (!action) { + context.owner.ReportFatalCharacterError( + "SwordmanAttackState::TryEnter", + "requested logical action is not registered", + requestedActionId); + return false; + } + + if (!FindHandler(action->actionId)) { + context.owner.ReportFatalCharacterError( + "SwordmanAttackState::TryEnter", + "requested logical action has no swordman action handler", + action->actionId); + return false; + } + + if (TryStartAction(machine, context, requestedActionId, action->actionId)) { + return true; + } } - return TryStartAction(machine, context, CharacterCommandType::AttackPressed, "attack_1"); + + return false; } void SwordmanAttackState::OnEnter(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId previousState) { (void)previousState; - ActionFrameProgress(machine) = 0.0f; - ActionFrame(machine) = 0; + const CharacterActionDefinition* action = GetCurrentAction(machine); if (!action) { context.owner.ReportFatalCharacterError("SwordmanAttackState::OnEnter", @@ -44,9 +65,15 @@ void SwordmanAttackState::OnEnter(CharacterStateMachine& machine, return; } - if (!context.owner.PlayActionDefinition(*action, "SwordmanAttackState::OnEnter")) { + ISwordmanActionHandler* handler = FindHandler(action->actionId); + if (!handler) { + context.owner.ReportFatalCharacterError("SwordmanAttackState::OnEnter", + "current logical action has no swordman handler", + action->actionId); return; } + + handler->OnEnter(context, *action); InvokeHook(machine, context.owner, action, "on_enter"); } @@ -60,44 +87,32 @@ void SwordmanAttackState::OnUpdate(CharacterStateMachine& machine, return; } + ISwordmanActionHandler* handler = FindHandler(action->actionId); + if (!handler) { + owner.ReportFatalCharacterError("SwordmanAttackState::OnUpdate", + "current logical action has no swordman handler", + action->actionId); + return; + } + InvokeHook(machine, owner, action, "on_update"); + SwordmanActionUpdateResult updateResult = handler->OnUpdate(context, *action); + if (updateResult.type == SwordmanActionUpdateResult::Type::Transition) { + if (updateResult.requestedActionId.empty()) { + owner.ReportFatalCharacterError("SwordmanAttackState::OnUpdate", + "handler requested an empty transition action id", + action->actionId); + return; + } - if (action->canMove) { - float moveScale = action->moveSpeedScale > 0.0f ? action->moveSpeedScale : kMovementActionScale; - motor.ApplyGroundInput(context.intent.moveX, context.intent.moveY, moveScale); - } else { - motor.StopGroundMovement(); - } - if (action->canTurn) { - owner.SetFacing(motor.facing); + bool started = updateResult.requireBufferedInput + ? TryStartAction(machine, context, updateResult.requestedActionId) + : StartAction(machine, context, updateResult.requestedActionId); + (void)started; + return; } - ActionFrameProgress(machine) += context.deltaTime * kLogicFramesPerSecond; - int previousFrame = ActionFrame(machine); - ActionFrame(machine) = std::max(0, static_cast(ActionFrameProgress(machine))); - ApplyFrameEvents(machine, owner, previousFrame + 1, ActionFrame(machine)); - - for (const auto& rule : action->cancelRules) { - if (ActionFrame(machine) < rule.beginFrame || ActionFrame(machine) > rule.endFrame) { - continue; - } - if (rule.requireGrounded && !motor.grounded) { - continue; - } - if (rule.requireAirborne && motor.grounded) { - continue; - } - - if (rule.targetAction == "attack_2" - && context.commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) { - if (TryStartAction(machine, context, CharacterCommandType::AttackPressed, rule.targetAction)) { - return; - } - } - } - - if (action->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) { - ClearCurrentAction(machine); + if (updateResult.type == SwordmanActionUpdateResult::Type::Finished) { if (!motor.grounded) { ChangeState(machine, context, CharacterStateId::Fall); } else { @@ -110,7 +125,22 @@ void SwordmanAttackState::OnExit(CharacterStateMachine& machine, CharacterStateContext& context, CharacterStateId nextState) { (void)nextState; - InvokeHook(machine, context.owner, GetCurrentAction(machine), "on_exit"); + + const CharacterActionDefinition* action = GetCurrentAction(machine); + if (action) { + if (ISwordmanActionHandler* handler = FindHandler(action->actionId)) { + handler->OnExit(context, *action); + } + } + InvokeHook(machine, context.owner, action, "on_exit"); +} + +ISwordmanActionHandler* SwordmanAttackState::FindHandler(const std::string& actionId) const { + auto it = handlers_.find(actionId); + if (it == handlers_.end()) { + return nullptr; + } + return it->second.get(); } } // namespace frostbite2D diff --git a/Game/src/character/states/jobs/swordman/SwordmanBasicAttackHandler.cpp b/Game/src/character/states/jobs/swordman/SwordmanBasicAttackHandler.cpp new file mode 100644 index 0000000..938f7ae --- /dev/null +++ b/Game/src/character/states/jobs/swordman/SwordmanBasicAttackHandler.cpp @@ -0,0 +1,110 @@ +#include "character/states/jobs/swordman/SwordmanBasicAttackHandler.h" +#include "character/CharacterObject.h" + +namespace frostbite2D { +namespace { + +constexpr float kAttackChainWindowBegin = 0.35f; +constexpr float kAttackChainWindowEnd = 0.95f; +constexpr float kDirectionalIntentThreshold = 0.25f; + +struct AttackSlideConfig { + float neutralDistance = 0.0f; + float forwardDistance = 0.0f; + float duration = 0.0f; +}; + +constexpr AttackSlideConfig kAttackSlideConfigs[] = { + {0.0f, 0.0f, 0.0f}, + {16.0f, 28.0f, 0.08f}, + {20.0f, 34.0f, 0.09f}, + {24.0f, 40.0f, 0.10f}, +}; + +} // namespace + +SwordmanBasicAttackHandler::SwordmanBasicAttackHandler() + : SwordmanActionHandlerBase("attack") { +} + +void SwordmanBasicAttackHandler::OnEnter(CharacterStateContext& context, + const CharacterActionDefinition& action) { + (void)action; + BeginComboStep(context, 1); +} + +SwordmanActionUpdateResult SwordmanBasicAttackHandler::OnUpdate( + CharacterStateContext& context, + const CharacterActionDefinition& action) { + (void)action; + + context.owner.GetMotorMutable().StopGroundMovement(); + if (comboStep_ == 1) { + float progress = context.owner.GetCurrentAnimationProgressNormalized(); + if (progress >= kAttackChainWindowBegin + && progress <= kAttackChainWindowEnd + && context.commandBuffer.HasBufferedAction(GetActionId())) { + context.commandBuffer.ConsumeAction(GetActionId()); + BeginComboStep(context, 2); + return SwordmanActionUpdateResult::Running(); + } + } + + if (comboStep_ == 2) { + float progress = context.owner.GetCurrentAnimationProgressNormalized(); + if (progress >= kAttackChainWindowBegin + && progress <= kAttackChainWindowEnd + && context.commandBuffer.HasBufferedAction(GetActionId())) { + context.commandBuffer.ConsumeAction(GetActionId()); + BeginComboStep(context, 3); + return SwordmanActionUpdateResult::Running(); + } + } + + return context.owner.IsCurrentAnimationFinished() + ? SwordmanActionUpdateResult::Finished() + : SwordmanActionUpdateResult::Running(); +} + +void SwordmanBasicAttackHandler::OnExit(CharacterStateContext& context, + const CharacterActionDefinition& action) { + (void)context; + (void)action; + comboStep_ = 0; +} + +void SwordmanBasicAttackHandler::BeginComboStep(CharacterStateContext& context, int comboStep) { + comboStep_ = comboStep; + + auto& owner = context.owner; + auto& motor = owner.GetMotorMutable(); + motor.StopGroundMovement(); + + switch (comboStep_) { + case 1: + owner.PlayAnimationTag("attack1"); + break; + case 2: + owner.PlayAnimationTag("attack2"); + break; + case 3: + owner.PlayAnimationTag("attack3"); + break; + default: + return; + } + + const AttackSlideConfig& slideConfig = kAttackSlideConfigs[comboStep_]; + float forwardInputStrength = context.intent.moveX * static_cast(motor.facing); + float slideDistance = slideConfig.neutralDistance; + if (forwardInputStrength >= kDirectionalIntentThreshold) { + slideDistance = slideConfig.forwardDistance; + } else if (forwardInputStrength <= -kDirectionalIntentThreshold) { + slideDistance = 0.0f; + } + + motor.StartForcedSlide(Vec2(static_cast(motor.facing), 0.0f), + slideDistance, slideConfig.duration); +} + +} // namespace frostbite2D diff --git a/Game/src/character/states/jobs/swordman/SwordmanSkill1Handler.cpp b/Game/src/character/states/jobs/swordman/SwordmanSkill1Handler.cpp new file mode 100644 index 0000000..f71f042 --- /dev/null +++ b/Game/src/character/states/jobs/swordman/SwordmanSkill1Handler.cpp @@ -0,0 +1,31 @@ +#include "character/states/jobs/swordman/SwordmanSkill1Handler.h" +#include "SDL_log.h" +#include "character/CharacterObject.h" + +namespace frostbite2D { + +SwordmanSkill1Handler::SwordmanSkill1Handler() + : SwordmanActionHandlerBase("skill_1") { +} + +void SwordmanSkill1Handler::OnEnter(CharacterStateContext& context, + const CharacterActionDefinition& action) { + (void)action; + context.owner.GetMotorMutable().StopGroundMovement(); + context.owner.PlayAnimationTag("Guard"); +} + +SwordmanActionUpdateResult SwordmanSkill1Handler::OnUpdate( + CharacterStateContext& context, + const CharacterActionDefinition& action) { + (void)action; + + context.owner.GetMotorMutable().StopGroundMovement(); + + + return context.owner.IsCurrentAnimationFinished() + ? SwordmanActionUpdateResult::Finished() + : SwordmanActionUpdateResult::Running(); +} + +} // namespace frostbite2D