feat(animation): 添加动画状态回调支持

refactor(character): 重构角色动作处理逻辑

feat(swordman): 实现剑士基础攻击和技能1处理

refactor(state): 优化状态机与动作上下文管理

feat(input): 改进输入系统支持动作请求队列

refactor(movement): 重构移动系统支持行走/奔跑模式
This commit is contained in:
2026-04-04 14:45:41 +08:00
parent 1200cf0181
commit 5e80df040b
29 changed files with 1251 additions and 513 deletions

View File

@@ -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();

View File

@@ -249,7 +249,7 @@ void Animation::Reset() {
FlushFrame(0);
}
AniFrame Animation::GetCurrentFrameInfo() {
AniFrame Animation::GetCurrentFrameInfo() const {
return frames_[currentFrameIndex_];
}

View File

@@ -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<std::string, CharacterActionDefinition> actions_;
};

View File

@@ -2,6 +2,9 @@
#include <frostbite2D/types/type_math.h>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
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<std::string> 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<CharacterCancelRuleDefinition> cancelRules;
std::vector<CharacterFrameEventDefinition> frameEvents;
};
using CharacterRuntimeValue = std::variant<bool, int, float, std::string, Vec2>;
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 <typename T>
void SetValue(const std::string& key, T&& value) {
values_[key] = std::forward<T>(value);
}
template <typename T>
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<T>(&it->second);
if (!value) {
return false;
}
outValue = *value;
return true;
}
std::unordered_map<std::string, CharacterRuntimeValue> 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<BufferedButton> buttons_;
float bufferWindowSeconds_ = 0.2f;
};

View File

@@ -5,6 +5,7 @@
#include <frostbite2D/2d/actor.h>
#include <frostbite2D/animation/animation.h>
#include <frostbite2D/base/RefPtr.h>
#include <functional>
#include <map>
#include <string>
#include <vector>
@@ -16,6 +17,8 @@ class CharacterObject;
class CharacterAnimation : public Actor {
public:
using ActionAnimationList = std::map<std::string, std::vector<RefPtr<Animation>>>;
using ActionFrameFlagCallback = std::function<void(int)>;
using ActionEndCallback = std::function<void()>;
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

View File

@@ -3,6 +3,9 @@
#include "character/CharacterActionTypes.h"
#include <frostbite2D/event/joystick_event.h>
#include <frostbite2D/event/key_event.h>
#include <cstdint>
#include <string>
#include <vector>
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<std::string> 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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,66 @@
#pragma once
#include "character/states/CharacterStateBase.h"
#include <string>
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

View File

@@ -1,6 +1,10 @@
#pragma once
#include "character/states/CharacterStateBase.h"
#include "character/states/jobs/swordman/SwordmanActionHandler.h"
#include <memory>
#include <string>
#include <unordered_map>
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<std::string, std::unique_ptr<ISwordmanActionHandler>> handlers_;
};
} // namespace frostbite2D

View File

@@ -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

View File

@@ -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

View File

@@ -1,10 +1,8 @@
#include "character/CharacterActionLibrary.h"
#include <SDL2/SDL.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <frostbite2D/resource/script_parser.h>
#include <algorithm>
#include <cctype>
#include <sstream>
#include <utility>
#include <vector>
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<unsigned char>(value[begin]))) {
++begin;
}
while (end > begin
&& std::isspace(static_cast<unsigned char>(value[end - 1]))) {
while (end > begin && std::isspace(static_cast<unsigned char>(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) {
void addRegisteredAction(std::map<std::string, CharacterActionDefinition>& actions,
const std::string& actionId) {
std::string key = normalizeKey(actionId);
if (key.empty() || actions.count(key) > 0) {
return;
}
ScriptParser parser(*rawData, path_);
if (!parser.isValid()) {
return;
}
for (const auto& value : parser.parseAll()) {
tokens_.push_back(value.toString());
}
valid_ = true;
}
bool IsValid() const { return valid_; }
bool IsEnd() const { return index_ >= tokens_.size(); }
std::string Next() {
if (IsEnd()) {
return {};
}
return tokens_[index_++];
}
private:
std::string path_;
std::vector<std::string> tokens_;
size_t index_ = 0;
bool valid_ = false;
};
// action_list.lst 同时兼容两种格式:
// 1. 标准 PVF #PVF_File + 索引 + `path`
// 2. 早期临时版的 actionId + path 成对写法
bool isNewActionListPath(const std::string& path) {
return path.find("action_logic") != std::string::npos
&& path.size() >= 4
&& path.substr(path.size() - 4) == ".act";
CharacterActionDefinition definition;
definition.actionId = key;
actions[key] = std::move(definition);
}
// action_list.lst only accepts #PVF_File entries like `swordman/action_logic/idle.act`.
std::vector<std::pair<std::string, std::string>> parseActionListEntries(
ScriptTokenStream& listStream) {
std::vector<std::pair<std::string, std::string>> entries;
std::vector<std::string> tokens;
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;
void registerCommonActions(std::map<std::string, CharacterActionDefinition>& actions) {
addRegisteredAction(actions, "idle");
addRegisteredAction(actions, "move");
addRegisteredAction(actions, "run");
addRegisteredAction(actions, "jump");
addRegisteredAction(actions, "fall");
addRegisteredAction(actions, "landing");
addRegisteredAction(actions, "hurt_light");
}
CharacterFrameEventType parseFrameEventType(const std::string& token) {
std::string lower = toLower(token);
if (lower == "open_hit_window") {
return CharacterFrameEventType::OpenHitWindow;
}
if (lower == "close_hit_window") {
return CharacterFrameEventType::CloseHitWindow;
}
if (lower == "open_input_buffer") {
return CharacterFrameEventType::OpenInputBuffer;
}
if (lower == "close_input_buffer") {
return CharacterFrameEventType::CloseInputBuffer;
}
if (lower == "set_velocity_xy") {
return CharacterFrameEventType::SetVelocityXY;
}
if (lower == "set_velocity_z") {
return CharacterFrameEventType::SetVelocityZ;
}
if (lower == "finish_action") {
return CharacterFrameEventType::FinishAction;
}
return CharacterFrameEventType::AnimationEvent;
}
void addOrReplaceAction(std::map<std::string, CharacterActionDefinition>& actions,
CharacterActionDefinition definition) {
if (definition.actionId.empty()) {
return;
}
actions[definition.actionId] = std::move(definition);
void registerSwordmanActions(std::map<std::string, CharacterActionDefinition>& 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<CharacterActionLibrary> loadCharacterActionLibrary(

View File

@@ -1,9 +1,11 @@
#include "character/CharacterAnimation.h"
#include "character/CharacterObject.h"
#include <SDL2/SDL.h>
#include <algorithm>
#include <array>
#include <cstdio>
#include <sstream>
#include <utility>
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<float>(GetCurrentFrameIndex())
/ static_cast<float>(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

View File

@@ -1,45 +1,115 @@
#include "character/CharacterInputRouter.h"
#include <SDL2/SDL_gamecontroller.h>
#include <algorithm>
#include <cmath>
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();
}
commandBuffer.Submit({CharacterCommandType::Move, moveAxis, 0.0f});
if (moveAxis.lengthSquared() >= 0.0001f) {
moveMode = keyboardMoveMode_ == CharacterMoveMode::Run
? CharacterMoveMode::Run
: CharacterMoveMode::Walk;
}
}
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

View File

@@ -2,6 +2,7 @@
#include <SDL2/SDL.h>
#include <frostbite2D/core/application.h>
#include <sstream>
#include <utility>
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 ? &currentActionContext_ : nullptr;
}
CharacterActionContextData* CharacterObject::GetCurrentActionContextMutable() {
return currentActionContext_.valid ? &currentActionContext_ : 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 "<none>";
@@ -273,6 +365,30 @@ void CharacterObject::ReportFatalCharacterError(
<< " currentActionId: "
<< NonEmptyOrPlaceholder(stateMachine_.GetCurrentActionId(), "<none>") << '\n'
<< " currentAnimationTag: " << NonEmptyOrPlaceholder(currentAction_, "<none>") << '\n'
<< " currentActionContext.requestedActionId: "
<< NonEmptyOrPlaceholder(
currentActionContext_.valid ? currentActionContext_.requestedActionId
: std::string(),
"<none>")
<< '\n'
<< " currentActionContext.sourceActionId: "
<< NonEmptyOrPlaceholder(
currentActionContext_.valid ? currentActionContext_.sourceActionId
: std::string(),
"<none>")
<< '\n'
<< " pendingActionContext.requestedActionId: "
<< NonEmptyOrPlaceholder(
pendingActionContext_.valid ? pendingActionContext_.requestedActionId
: std::string(),
"<none>")
<< '\n'
<< " pendingActionContext.sourceActionId: "
<< NonEmptyOrPlaceholder(
pendingActionContext_.valid ? pendingActionContext_.sourceActionId
: std::string(),
"<none>")
<< '\n'
<< " requestedActionId: "
<< NonEmptyOrPlaceholder(requestedActionId, "<none>") << '\n'
<< " requestedAnimationTag: "

View File

@@ -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<ICharacterStateNode> 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

View File

@@ -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

View File

@@ -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

View File

@@ -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<int>(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));

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -0,0 +1,10 @@
#include "character/states/jobs/swordman/SwordmanActionHandler.h"
#include <utility>
namespace frostbite2D {
SwordmanActionHandlerBase::SwordmanActionHandlerBase(std::string actionId)
: actionId_(std::move(actionId)) {
}
} // namespace frostbite2D

View File

@@ -1,16 +1,16 @@
#include "character/states/jobs/swordman/SwordmanAttackState.h"
#include "character/CharacterObject.h"
#include <algorithm>
#include "character/states/jobs/swordman/SwordmanActionHandler.h"
#include "character/states/jobs/swordman/SwordmanBasicAttackHandler.h"
#include "character/states/jobs/swordman/SwordmanSkill1Handler.h"
#include <cmath>
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<SwordmanBasicAttackHandler>());
handlers_.emplace("skill_1", std::make_unique<SwordmanSkill1Handler>());
}
bool SwordmanAttackState::TryEnter(CharacterStateMachine& machine,
CharacterStateContext& context) {
if (TryStartAction(machine, context, CharacterCommandType::Skill1Pressed, "skill_1")) {
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;
}
InvokeHook(machine, owner, action, "on_update");
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);
}
ActionFrameProgress(machine) += context.deltaTime * kLogicFramesPerSecond;
int previousFrame = ActionFrame(machine);
ActionFrame(machine) = std::max(0, static_cast<int>(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)) {
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->totalFrames > 0 && ActionFrame(machine) >= action->totalFrames) {
ClearCurrentAction(machine);
bool started = updateResult.requireBufferedInput
? TryStartAction(machine, context, updateResult.requestedActionId)
: StartAction(machine, context, updateResult.requestedActionId);
(void)started;
return;
}
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

View File

@@ -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<float>(motor.facing);
float slideDistance = slideConfig.neutralDistance;
if (forwardInputStrength >= kDirectionalIntentThreshold) {
slideDistance = slideConfig.forwardDistance;
} else if (forwardInputStrength <= -kDirectionalIntentThreshold) {
slideDistance = 0.0f;
}
motor.StartForcedSlide(Vec2(static_cast<float>(motor.facing), 0.0f),
slideDistance, slideConfig.duration);
}
} // namespace frostbite2D

View File

@@ -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