feat(character): 实现角色状态机与输入路由系统
新增角色状态机框架,包含空闲、移动、跳跃、攻击等状态 添加输入路由组件,统一处理键盘和手柄输入 引入动作库管理角色动作定义,支持PVF脚本配置 重构角色对象,整合运动器、状态机和输入处理 修复Actor子节点排序时的迭代安全问题
This commit is contained in:
28
Game/include/character/CharacterActionLibrary.h
Normal file
28
Game/include/character/CharacterActionLibrary.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "character/CharacterActionTypes.h"
|
||||
#include "character/CharacterDataLoader.h"
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/// 管理角色动作定义。优先从 PVF 动作脚本读取,缺失时回退到内置默认动作。
|
||||
class CharacterActionLibrary {
|
||||
public:
|
||||
bool LoadForConfig(const character::CharacterConfig& config);
|
||||
const CharacterActionDefinition* FindAction(const std::string& actionId) const;
|
||||
const CharacterActionDefinition* GetDefaultAction() const;
|
||||
|
||||
private:
|
||||
void BuildFallbackActions(const character::CharacterConfig& config);
|
||||
bool TryLoadPvfActionScripts(const character::CharacterConfig& config);
|
||||
|
||||
std::map<std::string, CharacterActionDefinition> actions_;
|
||||
};
|
||||
|
||||
std::optional<CharacterActionLibrary> loadCharacterActionLibrary(
|
||||
const character::CharacterConfig& config);
|
||||
|
||||
} // namespace frostbite2D
|
||||
199
Game/include/character/CharacterActionTypes.h
Normal file
199
Game/include/character/CharacterActionTypes.h
Normal file
@@ -0,0 +1,199 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/types/type_math.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/// 输入层产出的统一命令,屏蔽键盘与手柄的设备差异。
|
||||
enum class CharacterCommandType {
|
||||
Move,
|
||||
JumpPressed,
|
||||
AttackPressed,
|
||||
Skill1Pressed,
|
||||
DashPressed,
|
||||
};
|
||||
|
||||
/// 单条输入命令。移动命令携带轴值,按钮命令只关心触发时机。
|
||||
struct CharacterCommand {
|
||||
CharacterCommandType type = CharacterCommandType::Move;
|
||||
Vec2 moveAxis = Vec2::Zero();
|
||||
float timestamp = 0.0f;
|
||||
};
|
||||
|
||||
/// 角色在当前帧想要执行的意图,供状态机消费。
|
||||
struct CharacterIntent {
|
||||
float moveX = 0.0f;
|
||||
float moveY = 0.0f;
|
||||
bool wantJump = false;
|
||||
bool wantAttack = false;
|
||||
bool wantSkill1 = false;
|
||||
bool wantDash = false;
|
||||
};
|
||||
|
||||
/// 主状态机的大状态,普攻和技能统一走 Action。
|
||||
enum class CharacterStateId {
|
||||
Idle,
|
||||
Move,
|
||||
Jump,
|
||||
Fall,
|
||||
Landing,
|
||||
Action,
|
||||
Hurt,
|
||||
Dead,
|
||||
};
|
||||
|
||||
/// 角色的逻辑世界坐标。x/y 是地面平面,z 是高度轴。
|
||||
struct CharacterWorldPosition {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
|
||||
/// 当前项目的 2.5D 投影:地面 y 决定站位,高度 z 决定视觉抬升。
|
||||
Vec2 ToScreenPosition() const { return Vec2(x, y - z); }
|
||||
Vec2 ToGroundPosition() const { return Vec2(x, y); }
|
||||
};
|
||||
|
||||
/// 运动器只负责推进坐标和速度,不直接决定状态流转。
|
||||
struct CharacterMotor {
|
||||
CharacterWorldPosition position;
|
||||
Vec2 groundVelocity = Vec2::Zero();
|
||||
float verticalVelocity = 0.0f;
|
||||
bool grounded = true;
|
||||
int facing = 1;
|
||||
float moveSpeed = 220.0f;
|
||||
float jumpSpeed = 520.0f;
|
||||
float gravity = 1600.0f;
|
||||
|
||||
void SetGroundPosition(const Vec2& groundPosition) {
|
||||
position.x = groundPosition.x;
|
||||
position.y = groundPosition.y;
|
||||
}
|
||||
|
||||
void ApplyGroundInput(float moveX, float moveY, float speedScale = 1.0f) {
|
||||
Vec2 input(moveX, moveY);
|
||||
if (input.lengthSquared() > 1.0f) {
|
||||
input = input.normalized();
|
||||
}
|
||||
groundVelocity = input * (moveSpeed * speedScale);
|
||||
if (input.x > 0.01f) {
|
||||
facing = 1;
|
||||
} else if (input.x < -0.01f) {
|
||||
facing = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void StopGroundMovement() { groundVelocity = Vec2::Zero(); }
|
||||
|
||||
void Jump() {
|
||||
grounded = false;
|
||||
verticalVelocity = jumpSpeed;
|
||||
if (position.z < 0.0f) {
|
||||
position.z = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void Update(float deltaTime) {
|
||||
position.x += groundVelocity.x * deltaTime;
|
||||
position.y += groundVelocity.y * deltaTime;
|
||||
|
||||
if (!grounded || position.z > 0.0f || verticalVelocity > 0.0f) {
|
||||
position.z += verticalVelocity * deltaTime;
|
||||
verticalVelocity -= gravity * deltaTime;
|
||||
grounded = false;
|
||||
|
||||
if (position.z <= 0.0f) {
|
||||
position.z = 0.0f;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
}
|
||||
} else {
|
||||
position.z = 0.0f;
|
||||
verticalVelocity = 0.0f;
|
||||
grounded = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// 帧事件是动作定义驱动逻辑的钩子,后续可扩展命中框与位移曲线。
|
||||
enum class CharacterFrameEventType {
|
||||
OpenHitWindow,
|
||||
CloseHitWindow,
|
||||
OpenInputBuffer,
|
||||
CloseInputBuffer,
|
||||
SetVelocityXY,
|
||||
SetVelocityZ,
|
||||
AnimationEvent,
|
||||
FinishAction,
|
||||
};
|
||||
|
||||
/// 单个动作帧上的逻辑事件。
|
||||
struct CharacterFrameEventDefinition {
|
||||
int frame = 0;
|
||||
CharacterFrameEventType type = CharacterFrameEventType::AnimationEvent;
|
||||
Vec2 velocityXY = Vec2::Zero();
|
||||
float velocityZ = 0.0f;
|
||||
std::string stringValue;
|
||||
};
|
||||
|
||||
/// 取消规则决定动作在什么窗口内可以接下一个动作。
|
||||
struct CharacterCancelRuleDefinition {
|
||||
std::string targetAction;
|
||||
int beginFrame = 0;
|
||||
int endFrame = 0;
|
||||
bool requireGrounded = false;
|
||||
bool requireAirborne = false;
|
||||
};
|
||||
|
||||
/// 逻辑动作定义。actionId 是玩法动作名,animationTag 是资源动作名。
|
||||
struct CharacterActionDefinition {
|
||||
std::string actionId;
|
||||
std::string animationTag;
|
||||
int totalFrames = 1;
|
||||
bool loop = false;
|
||||
bool canMove = false;
|
||||
bool canTurn = true;
|
||||
bool canBeInterrupted = true;
|
||||
int inputBufferOpenFrame = 0;
|
||||
float moveSpeedScale = 0.0f;
|
||||
std::vector<CharacterCancelRuleDefinition> cancelRules;
|
||||
std::vector<CharacterFrameEventDefinition> frameEvents;
|
||||
};
|
||||
|
||||
/// 运行期状态标记,承载霸体、无敌等战斗属性。
|
||||
struct CharacterStatus {
|
||||
bool superArmor = false;
|
||||
bool invincible = false;
|
||||
bool controlLocked = false;
|
||||
};
|
||||
|
||||
/// 外部战斗系统打入角色时携带的受击上下文。
|
||||
struct HitContext {
|
||||
std::string hurtAction = "hurt_light";
|
||||
float launchZVelocity = 0.0f;
|
||||
bool ignoreInterrupt = false;
|
||||
};
|
||||
|
||||
/// 短时缓存输入,提供预输入和连段窗口的基础能力。
|
||||
class CharacterCommandBuffer {
|
||||
public:
|
||||
void Advance(float deltaTime);
|
||||
void Submit(const CharacterCommand& command);
|
||||
CharacterIntent BuildIntent() const;
|
||||
bool HasBuffered(CharacterCommandType type) const;
|
||||
void Consume(CharacterCommandType type);
|
||||
void Clear();
|
||||
|
||||
private:
|
||||
struct BufferedButton {
|
||||
CharacterCommandType type = CharacterCommandType::AttackPressed;
|
||||
float age = 0.0f;
|
||||
};
|
||||
|
||||
Vec2 moveAxis_ = Vec2::Zero();
|
||||
std::vector<BufferedButton> buttons_;
|
||||
float bufferWindowSeconds_ = 0.2f;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -24,6 +24,9 @@ public:
|
||||
void SetAction(const std::string& actionName);
|
||||
void SetDirection(int direction);
|
||||
const std::string& GetCurrentAction() const { return currentActionTag_; }
|
||||
bool HasAction(const std::string& actionName) const {
|
||||
return actionAnimations_.count(actionName) > 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string FormatImgPath(std::string path, Animation::ReplaceData data);
|
||||
|
||||
35
Game/include/character/CharacterInputRouter.h
Normal file
35
Game/include/character/CharacterInputRouter.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "character/CharacterActionTypes.h"
|
||||
#include <frostbite2D/event/joystick_event.h>
|
||||
#include <frostbite2D/event/key_event.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/// 将 SDL 键盘/手柄事件统一转换为角色命令,避免状态机直接依赖设备细节。
|
||||
class CharacterInputRouter {
|
||||
public:
|
||||
bool OnKeyDown(const KeyEvent& event);
|
||||
bool OnKeyUp(KeyCode keyCode);
|
||||
bool OnJoystickAxis(const JoystickAxisEvent& event);
|
||||
bool OnJoystickButtonDown(const JoystickButtonDownEvent& event);
|
||||
bool OnJoystickButtonUp(const JoystickButtonUpEvent& event);
|
||||
|
||||
void EmitCommands(CharacterCommandBuffer& commandBuffer);
|
||||
void ClearFrameState();
|
||||
|
||||
private:
|
||||
bool left_ = false;
|
||||
bool right_ = false;
|
||||
bool up_ = false;
|
||||
bool down_ = false;
|
||||
bool jumpPressed_ = false;
|
||||
bool attackPressed_ = false;
|
||||
bool skill1Pressed_ = false;
|
||||
bool dashPressed_ = false;
|
||||
float leftStickX_ = 0.0f;
|
||||
float leftStickY_ = 0.0f;
|
||||
float joystickDeadZone_ = 0.2f;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -1,9 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "character/CharacterActionLibrary.h"
|
||||
#include "character/CharacterAnimation.h"
|
||||
#include "character/CharacterDataLoader.h"
|
||||
#include "character/CharacterEquipmentManager.h"
|
||||
#include "character/CharacterInputRouter.h"
|
||||
#include "character/CharacterStateMachine.h"
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/event/joystick_event.h>
|
||||
#include <frostbite2D/event/key_event.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -19,11 +24,32 @@ public:
|
||||
void SetAction(const std::string& actionName);
|
||||
void SetDirection(int direction);
|
||||
void SetCharacterPosition(const Vec2& pos);
|
||||
void SetWorldPosition(const CharacterWorldPosition& pos);
|
||||
void PushCommand(const CharacterCommand& command);
|
||||
void ApplyHit(const HitContext& hit);
|
||||
|
||||
void OnUpdate(float deltaTime) override;
|
||||
bool OnKeyDown(const KeyEvent& event) override;
|
||||
bool OnKeyUp(const KeyEvent& event) override;
|
||||
bool OnJoystickAxis(const JoystickEvent& event) override;
|
||||
bool OnJoystickButtonDown(const JoystickEvent& event) override;
|
||||
bool OnJoystickButtonUp(const JoystickEvent& event) override;
|
||||
|
||||
int GetJobId() const { return jobId_; }
|
||||
int GetGrowType() const { return growType_; }
|
||||
int GetDirection() const { return direction_; }
|
||||
const std::string& GetCurrentAction() const { return currentAction_; }
|
||||
CharacterStateId GetState() const { return stateMachine_.GetState(); }
|
||||
const CharacterIntent& GetCurrentIntent() const { return currentIntent_; }
|
||||
const CharacterWorldPosition& GetWorldPosition() const { return motor_.position; }
|
||||
const CharacterMotor& GetMotor() const { return motor_; }
|
||||
CharacterMotor& GetMotorMutable() { return motor_; }
|
||||
float GetLastDeltaTime() const { return lastDeltaTime_; }
|
||||
const CharacterActionDefinition* FindAction(const std::string& actionId) const;
|
||||
std::string ResolveAnimationTag(const std::string& preferred,
|
||||
const std::string& fallback) const;
|
||||
void PlayAnimationTag(const std::string& actionTag);
|
||||
void SetFacing(int direction);
|
||||
|
||||
const character::CharacterConfig* GetConfig() const {
|
||||
return config_ ? &config_.value() : nullptr;
|
||||
@@ -37,7 +63,15 @@ private:
|
||||
std::string currentAction_;
|
||||
std::optional<character::CharacterConfig> config_;
|
||||
CharacterEquipmentManager equipmentManager_;
|
||||
CharacterActionLibrary actionLibrary_;
|
||||
CharacterInputRouter inputRouter_;
|
||||
CharacterCommandBuffer commandBuffer_;
|
||||
CharacterIntent currentIntent_;
|
||||
CharacterStateMachine stateMachine_;
|
||||
CharacterMotor motor_;
|
||||
CharacterStatus status_;
|
||||
RefPtr<CharacterAnimation> animationManager_ = nullptr;
|
||||
float lastDeltaTime_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
59
Game/include/character/CharacterStateMachine.h
Normal file
59
Game/include/character/CharacterStateMachine.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "character/CharacterActionLibrary.h"
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class CharacterObject;
|
||||
|
||||
/// 角色主状态机。输入先变成意图,再由状态机决定当前大状态和动作推进。
|
||||
class CharacterStateMachine {
|
||||
public:
|
||||
void Reset();
|
||||
void Update(CharacterObject& owner,
|
||||
CharacterCommandBuffer& commandBuffer,
|
||||
const CharacterIntent& intent,
|
||||
float deltaTime);
|
||||
void ForceHurt(const CharacterActionDefinition* hurtAction);
|
||||
|
||||
CharacterStateId GetState() const { return currentState_; }
|
||||
const std::string& GetCurrentActionId() const { return currentActionId_; }
|
||||
const CharacterActionDefinition* GetCurrentActionDefinition() const {
|
||||
return currentAction_;
|
||||
}
|
||||
|
||||
bool CanAcceptGroundActions() const;
|
||||
bool CanBeInterrupted() const;
|
||||
bool IsMovementLocked() const;
|
||||
bool CanTurn() const;
|
||||
|
||||
private:
|
||||
void ChangeState(CharacterObject& owner, CharacterStateId nextState);
|
||||
void EnterAction(CharacterObject& owner, const CharacterActionDefinition* action);
|
||||
void UpdateGroundState(CharacterObject& owner,
|
||||
CharacterCommandBuffer& commandBuffer,
|
||||
const CharacterIntent& intent);
|
||||
void UpdateAirState(CharacterObject& owner,
|
||||
CharacterCommandBuffer& commandBuffer,
|
||||
const CharacterIntent& intent);
|
||||
void UpdateActionState(CharacterObject& owner,
|
||||
CharacterCommandBuffer& commandBuffer,
|
||||
const CharacterIntent& intent,
|
||||
float deltaTime);
|
||||
void UpdateHurtState(CharacterObject& owner, float deltaTime);
|
||||
bool TryStartAction(CharacterObject& owner,
|
||||
CharacterCommandBuffer& commandBuffer,
|
||||
CharacterCommandType commandType,
|
||||
const std::string& actionId);
|
||||
void ApplyFrameEvents(CharacterObject& owner, int previousFrame, int currentFrame);
|
||||
|
||||
CharacterStateId currentState_ = CharacterStateId::Idle;
|
||||
std::string currentActionId_;
|
||||
const CharacterActionDefinition* currentAction_ = nullptr;
|
||||
float stateTime_ = 0.0f;
|
||||
float actionFrameProgress_ = 0.0f;
|
||||
int actionFrame_ = 0;
|
||||
float landingTimer_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
Reference in New Issue
Block a user