feat(animation): 添加动画状态回调支持
refactor(character): 重构角色动作处理逻辑 feat(swordman): 实现剑士基础攻击和技能1处理 refactor(state): 优化状态机与动作上下文管理 feat(input): 改进输入系统支持动作请求队列 refactor(movement): 重构移动系统支持行走/奔跑模式
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -249,7 +249,7 @@ void Animation::Reset() {
|
||||
FlushFrame(0);
|
||||
}
|
||||
|
||||
AniFrame Animation::GetCurrentFrameInfo() {
|
||||
AniFrame Animation::GetCurrentFrameInfo() const {
|
||||
return frames_[currentFrameIndex_];
|
||||
}
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
CharacterActionDefinition definition;
|
||||
definition.actionId = key;
|
||||
actions[key] = std::move(definition);
|
||||
}
|
||||
|
||||
for (const auto& value : parser.parseAll()) {
|
||||
tokens_.push_back(value.toString());
|
||||
}
|
||||
valid_ = true;
|
||||
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");
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? ¤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 "<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: "
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user