feat(character): 实现角色状态机与输入路由系统
新增角色状态机框架,包含空闲、移动、跳跃、攻击等状态 添加输入路由组件,统一处理键盘和手柄输入 引入动作库管理角色动作定义,支持PVF脚本配置 重构角色对象,整合运动器、状态机和输入处理 修复Actor子节点排序时的迭代安全问题
This commit is contained in:
@@ -164,6 +164,9 @@ public:
|
|||||||
virtual void OnTransformChanged() {};
|
virtual void OnTransformChanged() {};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void markChildrenOrderDirty();
|
||||||
|
void reorderChildrenByZOrder();
|
||||||
|
|
||||||
Actor* parent_;
|
Actor* parent_;
|
||||||
Scene* scene_;
|
Scene* scene_;
|
||||||
ActorList children_;
|
ActorList children_;
|
||||||
@@ -197,6 +200,8 @@ private:
|
|||||||
uint32 nextUpdateListenerId_ = 1;
|
uint32 nextUpdateListenerId_ = 1;
|
||||||
bool iteratingUpdateListeners_ = false;
|
bool iteratingUpdateListeners_ = false;
|
||||||
bool updateListenersDirty_ = false;
|
bool updateListenersDirty_ = false;
|
||||||
|
bool iteratingChildren_ = false;
|
||||||
|
bool childrenOrderDirty_ = false;
|
||||||
|
|
||||||
struct EventListener {
|
struct EventListener {
|
||||||
uint32 id;
|
uint32 id;
|
||||||
|
|||||||
@@ -188,7 +188,13 @@ void Actor::AddChild(Ptr<Actor> child) {
|
|||||||
|
|
||||||
child->SetParent(this);
|
child->SetParent(this);
|
||||||
child->SetScene(scene_);
|
child->SetScene(scene_);
|
||||||
insertChildByZOrder(child);
|
if (iteratingChildren_) {
|
||||||
|
// 正在遍历孩子时不能立即重新插入排序位置,否则会破坏链表迭代状态。
|
||||||
|
children_.PushBack(child);
|
||||||
|
markChildrenOrderDirty();
|
||||||
|
} else {
|
||||||
|
insertChildByZOrder(child);
|
||||||
|
}
|
||||||
child->OnAdded(this);
|
child->OnAdded(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +206,8 @@ void Actor::SetZOrder(int zOrder) {
|
|||||||
zOrder_ = zOrder;
|
zOrder_ = zOrder;
|
||||||
|
|
||||||
if (parent_) {
|
if (parent_) {
|
||||||
parent_->RemoveChild(this);
|
// 不再在这里直接 RemoveChild/AddChild,避免更新阶段改动正在遍历的链表。
|
||||||
parent_->AddChild(this);
|
parent_->markChildrenOrderDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,19 +229,68 @@ void Actor::RemoveAllChildren() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Actor::UpdateChildren(float deltaTime) {
|
void Actor::UpdateChildren(float deltaTime) {
|
||||||
|
iteratingChildren_ = true;
|
||||||
for (auto it = children_.begin(); it != children_.end(); ++it) {
|
for (auto it = children_.begin(); it != children_.end(); ++it) {
|
||||||
if (*it && (*it)->IsVisible()) {
|
if (*it && (*it)->IsVisible()) {
|
||||||
(*it)->Update(deltaTime);
|
(*it)->Update(deltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
iteratingChildren_ = false;
|
||||||
|
|
||||||
|
if (childrenOrderDirty_) {
|
||||||
|
reorderChildrenByZOrder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Actor::RenderChildren() {
|
void Actor::RenderChildren() {
|
||||||
|
if (childrenOrderDirty_ && !iteratingChildren_) {
|
||||||
|
reorderChildrenByZOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
iteratingChildren_ = true;
|
||||||
for (auto it = children_.begin(); it != children_.end(); ++it) {
|
for (auto it = children_.begin(); it != children_.end(); ++it) {
|
||||||
if (*it && (*it)->IsVisible()) {
|
if (*it && (*it)->IsVisible()) {
|
||||||
(*it)->Render();
|
(*it)->Render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
iteratingChildren_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::markChildrenOrderDirty() {
|
||||||
|
childrenOrderDirty_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Actor::reorderChildrenByZOrder() {
|
||||||
|
if (!childrenOrderDirty_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一在安全时机按 zOrder 重建顺序,解决上下移动时频繁改层级导致的卡死。
|
||||||
|
std::vector<Ptr<Actor>> orderedChildren;
|
||||||
|
for (auto it = children_.begin(); it != children_.end(); ++it) {
|
||||||
|
if (*it) {
|
||||||
|
orderedChildren.push_back(*it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(orderedChildren.begin(), orderedChildren.end(),
|
||||||
|
[](const Ptr<Actor>& lhs, const Ptr<Actor>& rhs) {
|
||||||
|
if (!lhs || !rhs) {
|
||||||
|
return static_cast<bool>(lhs);
|
||||||
|
}
|
||||||
|
return lhs->GetZOrder() < rhs->GetZOrder();
|
||||||
|
});
|
||||||
|
|
||||||
|
while (!children_.IsEmpty()) {
|
||||||
|
Ptr<Actor> child = children_.GetFirst();
|
||||||
|
children_.Remove(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& child : orderedChildren) {
|
||||||
|
children_.PushBack(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
childrenOrderDirty_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32 Actor::AddUpdateListener(UpdateCallback callback, UpdatePhase phase) {
|
uint32 Actor::AddUpdateListener(UpdateCallback callback, UpdatePhase phase) {
|
||||||
|
|||||||
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 SetAction(const std::string& actionName);
|
||||||
void SetDirection(int direction);
|
void SetDirection(int direction);
|
||||||
const std::string& GetCurrentAction() const { return currentActionTag_; }
|
const std::string& GetCurrentAction() const { return currentActionTag_; }
|
||||||
|
bool HasAction(const std::string& actionName) const {
|
||||||
|
return actionAnimations_.count(actionName) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::string FormatImgPath(std::string path, Animation::ReplaceData data);
|
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
|
#pragma once
|
||||||
|
|
||||||
|
#include "character/CharacterActionLibrary.h"
|
||||||
#include "character/CharacterAnimation.h"
|
#include "character/CharacterAnimation.h"
|
||||||
#include "character/CharacterDataLoader.h"
|
#include "character/CharacterDataLoader.h"
|
||||||
#include "character/CharacterEquipmentManager.h"
|
#include "character/CharacterEquipmentManager.h"
|
||||||
|
#include "character/CharacterInputRouter.h"
|
||||||
|
#include "character/CharacterStateMachine.h"
|
||||||
#include <frostbite2D/2d/actor.h>
|
#include <frostbite2D/2d/actor.h>
|
||||||
|
#include <frostbite2D/event/joystick_event.h>
|
||||||
|
#include <frostbite2D/event/key_event.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -19,11 +24,32 @@ public:
|
|||||||
void SetAction(const std::string& actionName);
|
void SetAction(const std::string& actionName);
|
||||||
void SetDirection(int direction);
|
void SetDirection(int direction);
|
||||||
void SetCharacterPosition(const Vec2& pos);
|
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 GetJobId() const { return jobId_; }
|
||||||
int GetGrowType() const { return growType_; }
|
int GetGrowType() const { return growType_; }
|
||||||
int GetDirection() const { return direction_; }
|
int GetDirection() const { return direction_; }
|
||||||
const std::string& GetCurrentAction() const { return currentAction_; }
|
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 {
|
const character::CharacterConfig* GetConfig() const {
|
||||||
return config_ ? &config_.value() : nullptr;
|
return config_ ? &config_.value() : nullptr;
|
||||||
@@ -37,7 +63,15 @@ private:
|
|||||||
std::string currentAction_;
|
std::string currentAction_;
|
||||||
std::optional<character::CharacterConfig> config_;
|
std::optional<character::CharacterConfig> config_;
|
||||||
CharacterEquipmentManager equipmentManager_;
|
CharacterEquipmentManager equipmentManager_;
|
||||||
|
CharacterActionLibrary actionLibrary_;
|
||||||
|
CharacterInputRouter inputRouter_;
|
||||||
|
CharacterCommandBuffer commandBuffer_;
|
||||||
|
CharacterIntent currentIntent_;
|
||||||
|
CharacterStateMachine stateMachine_;
|
||||||
|
CharacterMotor motor_;
|
||||||
|
CharacterStatus status_;
|
||||||
RefPtr<CharacterAnimation> animationManager_ = nullptr;
|
RefPtr<CharacterAnimation> animationManager_ = nullptr;
|
||||||
|
float lastDeltaTime_ = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace frostbite2D
|
} // 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
|
||||||
442
Game/src/character/CharacterActionLibrary.cpp
Normal file
442
Game/src/character/CharacterActionLibrary.cpp
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
#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 <vector>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string toLower(const std::string& value) {
|
||||||
|
std::string result = value;
|
||||||
|
std::transform(result.begin(), result.end(), result.begin(),
|
||||||
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int toInt(const std::string& value, int fallback = 0) {
|
||||||
|
try {
|
||||||
|
return std::stoi(value);
|
||||||
|
} catch (...) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float toFloat(const std::string& value, float fallback = 0.0f) {
|
||||||
|
try {
|
||||||
|
return std::stof(value);
|
||||||
|
} catch (...) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTrueToken(const std::string& value) {
|
||||||
|
std::string token = toLower(value);
|
||||||
|
return token == "1" || token == "true" || token == "[true]" || token == "yes";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string trim(const std::string& value) {
|
||||||
|
size_t begin = 0;
|
||||||
|
size_t end = value.size();
|
||||||
|
while (begin < end && std::isspace(static_cast<unsigned char>(value[begin]))) {
|
||||||
|
++begin;
|
||||||
|
}
|
||||||
|
while (end > begin
|
||||||
|
&& std::isspace(static_cast<unsigned char>(value[end - 1]))) {
|
||||||
|
--end;
|
||||||
|
}
|
||||||
|
return value.substr(begin, end - begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string trimBackticks(const std::string& value) {
|
||||||
|
std::string result = trim(value);
|
||||||
|
if (result.size() >= 2 && result.front() == '`' && result.back() == '`') {
|
||||||
|
return result.substr(1, result.size() - 2);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fileStem(const std::string& path) {
|
||||||
|
size_t slashPos = path.find_last_of("/\\");
|
||||||
|
size_t begin = slashPos == std::string::npos ? 0 : slashPos + 1;
|
||||||
|
size_t dotPos = path.find_last_of('.');
|
||||||
|
if (dotPos == std::string::npos || dotPos < begin) {
|
||||||
|
return path.substr(begin);
|
||||||
|
}
|
||||||
|
return path.substr(begin, dotPos - begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScriptTokenStream {
|
||||||
|
public:
|
||||||
|
explicit ScriptTokenStream(const std::string& path)
|
||||||
|
: path_(PvfArchive::get().normalizePath(path)) {
|
||||||
|
auto rawData = PvfArchive::get().getFileRawData(path_);
|
||||||
|
if (!rawData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptParser parser(*rawData, path_);
|
||||||
|
if (!parser.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& value : parser.parseAll()) {
|
||||||
|
tokens_.push_back(value.toString());
|
||||||
|
}
|
||||||
|
valid_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValid() const { return valid_; }
|
||||||
|
bool IsEnd() const { return index_ >= tokens_.size(); }
|
||||||
|
|
||||||
|
std::string Next() {
|
||||||
|
if (IsEnd()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return tokens_[index_++];
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string path_;
|
||||||
|
std::vector<std::string> tokens_;
|
||||||
|
size_t index_ = 0;
|
||||||
|
bool valid_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// action_list.lst 同时兼容两种格式:
|
||||||
|
// 1. 标准 PVF #PVF_File + 索引 + `path`
|
||||||
|
// 2. 早期临时版的 actionId + path 成对写法
|
||||||
|
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;
|
||||||
|
while (!listStream.IsEnd()) {
|
||||||
|
std::string token = trim(listStream.Next());
|
||||||
|
if (!token.empty()) {
|
||||||
|
tokens.push_back(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.empty()) {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens.front() == "#PVF_File") {
|
||||||
|
for (size_t i = 1; i + 1 < tokens.size(); i += 2) {
|
||||||
|
std::string actionPath = trimBackticks(tokens[i + 1]);
|
||||||
|
if (actionPath.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries.push_back({toLower(fileStem(actionPath)), actionPath});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i + 1 < tokens.size(); i += 2) {
|
||||||
|
entries.push_back({toLower(tokens[i]), trimBackticks(tokens[i + 1])});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterFrameEventType parseFrameEventType(const std::string& token) {
|
||||||
|
std::string lower = toLower(token);
|
||||||
|
if (lower == "open_hit_window") {
|
||||||
|
return CharacterFrameEventType::OpenHitWindow;
|
||||||
|
}
|
||||||
|
if (lower == "close_hit_window") {
|
||||||
|
return CharacterFrameEventType::CloseHitWindow;
|
||||||
|
}
|
||||||
|
if (lower == "open_input_buffer") {
|
||||||
|
return CharacterFrameEventType::OpenInputBuffer;
|
||||||
|
}
|
||||||
|
if (lower == "close_input_buffer") {
|
||||||
|
return CharacterFrameEventType::CloseInputBuffer;
|
||||||
|
}
|
||||||
|
if (lower == "set_velocity_xy") {
|
||||||
|
return CharacterFrameEventType::SetVelocityXY;
|
||||||
|
}
|
||||||
|
if (lower == "set_velocity_z") {
|
||||||
|
return CharacterFrameEventType::SetVelocityZ;
|
||||||
|
}
|
||||||
|
if (lower == "finish_action") {
|
||||||
|
return CharacterFrameEventType::FinishAction;
|
||||||
|
}
|
||||||
|
return CharacterFrameEventType::AnimationEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addOrReplaceAction(std::map<std::string, CharacterActionDefinition>& actions,
|
||||||
|
CharacterActionDefinition definition) {
|
||||||
|
if (definition.actionId.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
actions[definition.actionId] = std::move(definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CharacterCommandBuffer::Advance(float deltaTime) {
|
||||||
|
for (auto& button : buttons_) {
|
||||||
|
button.age += deltaTime;
|
||||||
|
}
|
||||||
|
buttons_.erase(
|
||||||
|
std::remove_if(buttons_.begin(), buttons_.end(),
|
||||||
|
[this](const BufferedButton& button) {
|
||||||
|
return button.age > bufferWindowSeconds_;
|
||||||
|
}),
|
||||||
|
buttons_.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCommandBuffer::Submit(const CharacterCommand& command) {
|
||||||
|
if (command.type == CharacterCommandType::Move) {
|
||||||
|
moveAxis_ = command.moveAxis;
|
||||||
|
if (moveAxis_.lengthSquared() > 1.0f) {
|
||||||
|
moveAxis_ = moveAxis_.normalized();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons_.push_back({command.type, 0.0f});
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterIntent CharacterCommandBuffer::BuildIntent() const {
|
||||||
|
CharacterIntent intent;
|
||||||
|
intent.moveX = moveAxis_.x;
|
||||||
|
intent.moveY = moveAxis_.y;
|
||||||
|
intent.wantJump = HasBuffered(CharacterCommandType::JumpPressed);
|
||||||
|
intent.wantAttack = HasBuffered(CharacterCommandType::AttackPressed);
|
||||||
|
intent.wantSkill1 = HasBuffered(CharacterCommandType::Skill1Pressed);
|
||||||
|
intent.wantDash = HasBuffered(CharacterCommandType::DashPressed);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterCommandBuffer::HasBuffered(CharacterCommandType type) const {
|
||||||
|
for (const auto& button : buttons_) {
|
||||||
|
if (button.type == type) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCommandBuffer::Consume(CharacterCommandType type) {
|
||||||
|
auto it = std::find_if(buttons_.begin(), buttons_.end(),
|
||||||
|
[type](const BufferedButton& button) {
|
||||||
|
return button.type == type;
|
||||||
|
});
|
||||||
|
if (it != buttons_.end()) {
|
||||||
|
buttons_.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCommandBuffer::Clear() {
|
||||||
|
moveAxis_ = Vec2::Zero();
|
||||||
|
buttons_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterActionLibrary::LoadForConfig(const character::CharacterConfig& config) {
|
||||||
|
actions_.clear();
|
||||||
|
// 先填一套最小可运行的默认动作,保证资源或 PVF 缺失时角色仍能工作。
|
||||||
|
BuildFallbackActions(config);
|
||||||
|
// 再尝试用 PVF 动作逻辑覆盖默认值。
|
||||||
|
TryLoadPvfActionScripts(config);
|
||||||
|
return !actions_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterActionDefinition* CharacterActionLibrary::FindAction(
|
||||||
|
const std::string& actionId) const {
|
||||||
|
auto it = actions_.find(actionId);
|
||||||
|
if (it != actions_.end()) {
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterActionDefinition* CharacterActionLibrary::GetDefaultAction() const {
|
||||||
|
return FindAction("idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterActionLibrary::BuildFallbackActions(
|
||||||
|
const character::CharacterConfig& config) {
|
||||||
|
auto animationTagOrDefault = [&config](const std::string& actionId,
|
||||||
|
const std::string& fallback) {
|
||||||
|
if (config.animationPath.count(actionId) > 0) {
|
||||||
|
return actionId;
|
||||||
|
}
|
||||||
|
if (config.animationPath.count(fallback) > 0) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (!config.animationPath.empty()) {
|
||||||
|
return config.animationPath.begin()->first;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
CharacterActionDefinition idle;
|
||||||
|
idle.actionId = "idle";
|
||||||
|
idle.animationTag = animationTagOrDefault("rest", "rest");
|
||||||
|
idle.totalFrames = 1;
|
||||||
|
idle.loop = true;
|
||||||
|
idle.canMove = true;
|
||||||
|
addOrReplaceAction(actions_, idle);
|
||||||
|
|
||||||
|
CharacterActionDefinition move;
|
||||||
|
move.actionId = "move";
|
||||||
|
move.animationTag = animationTagOrDefault("run", animationTagOrDefault("walk", "rest"));
|
||||||
|
move.totalFrames = 1;
|
||||||
|
move.loop = true;
|
||||||
|
move.canMove = true;
|
||||||
|
addOrReplaceAction(actions_, move);
|
||||||
|
|
||||||
|
CharacterActionDefinition jump;
|
||||||
|
jump.actionId = "jump";
|
||||||
|
jump.animationTag = animationTagOrDefault("jump", "rest");
|
||||||
|
jump.totalFrames = 18;
|
||||||
|
jump.canTurn = true;
|
||||||
|
addOrReplaceAction(actions_, jump);
|
||||||
|
|
||||||
|
CharacterActionDefinition fall;
|
||||||
|
fall.actionId = "fall";
|
||||||
|
fall.animationTag = animationTagOrDefault("jump", "rest");
|
||||||
|
fall.totalFrames = 1;
|
||||||
|
fall.loop = true;
|
||||||
|
fall.canTurn = true;
|
||||||
|
addOrReplaceAction(actions_, fall);
|
||||||
|
|
||||||
|
CharacterActionDefinition landing;
|
||||||
|
landing.actionId = "landing";
|
||||||
|
landing.animationTag = animationTagOrDefault("rest", "rest");
|
||||||
|
landing.totalFrames = 6;
|
||||||
|
addOrReplaceAction(actions_, landing);
|
||||||
|
|
||||||
|
CharacterActionDefinition attack1;
|
||||||
|
attack1.actionId = "attack_1";
|
||||||
|
attack1.animationTag = animationTagOrDefault("attack1", animationTagOrDefault("attack", "rest"));
|
||||||
|
attack1.totalFrames = 24;
|
||||||
|
attack1.canMove = false;
|
||||||
|
attack1.canTurn = false;
|
||||||
|
attack1.canBeInterrupted = true;
|
||||||
|
attack1.cancelRules.push_back({"attack_2", 12, 22, true, false});
|
||||||
|
addOrReplaceAction(actions_, attack1);
|
||||||
|
|
||||||
|
CharacterActionDefinition attack2;
|
||||||
|
attack2.actionId = "attack_2";
|
||||||
|
attack2.animationTag = animationTagOrDefault("attack2", attack1.animationTag);
|
||||||
|
attack2.totalFrames = 28;
|
||||||
|
attack2.canMove = false;
|
||||||
|
attack2.canTurn = false;
|
||||||
|
attack2.canBeInterrupted = true;
|
||||||
|
addOrReplaceAction(actions_, attack2);
|
||||||
|
|
||||||
|
CharacterActionDefinition skill1;
|
||||||
|
skill1.actionId = "skill_1";
|
||||||
|
skill1.animationTag = animationTagOrDefault("skill1", attack1.animationTag);
|
||||||
|
skill1.totalFrames = 36;
|
||||||
|
skill1.canMove = false;
|
||||||
|
skill1.canTurn = false;
|
||||||
|
skill1.canBeInterrupted = false;
|
||||||
|
addOrReplaceAction(actions_, skill1);
|
||||||
|
|
||||||
|
CharacterActionDefinition hurt;
|
||||||
|
hurt.actionId = "hurt_light";
|
||||||
|
hurt.animationTag = animationTagOrDefault("damage", animationTagOrDefault("hurt", "rest"));
|
||||||
|
hurt.totalFrames = 18;
|
||||||
|
hurt.canMove = false;
|
||||||
|
hurt.canTurn = false;
|
||||||
|
hurt.canBeInterrupted = true;
|
||||||
|
addOrReplaceAction(actions_, hurt);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterActionLibrary::TryLoadPvfActionScripts(
|
||||||
|
const character::CharacterConfig& config) {
|
||||||
|
std::string listPath = "character/" + config.jobTag + "/action_logic/action_list.lst";
|
||||||
|
ScriptTokenStream listStream(listPath);
|
||||||
|
if (!listStream.IsValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entries = parseActionListEntries(listStream);
|
||||||
|
for (const auto& [actionId, relativePath] : entries) {
|
||||||
|
if (actionId.empty() || relativePath.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptTokenStream actionStream(
|
||||||
|
"character/" + config.jobTag + "/action_logic/" + relativePath);
|
||||||
|
if (!actionStream.IsValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterActionDefinition definition;
|
||||||
|
definition.actionId = actionId;
|
||||||
|
while (!actionStream.IsEnd()) {
|
||||||
|
std::string token = toLower(actionStream.Next());
|
||||||
|
if (token.empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == "[action id]") {
|
||||||
|
definition.actionId = toLower(actionStream.Next());
|
||||||
|
} else if (token == "[animation tag]") {
|
||||||
|
definition.animationTag = actionStream.Next();
|
||||||
|
} else if (token == "[total frames]") {
|
||||||
|
definition.totalFrames = toInt(actionStream.Next(), definition.totalFrames);
|
||||||
|
} else if (token == "[loop]") {
|
||||||
|
definition.loop = isTrueToken(actionStream.Next());
|
||||||
|
} else if (token == "[can move]") {
|
||||||
|
definition.canMove = isTrueToken(actionStream.Next());
|
||||||
|
} else if (token == "[can turn]") {
|
||||||
|
definition.canTurn = isTrueToken(actionStream.Next());
|
||||||
|
} else if (token == "[can be interrupted]") {
|
||||||
|
definition.canBeInterrupted = isTrueToken(actionStream.Next());
|
||||||
|
} else if (token == "[move speed scale]") {
|
||||||
|
definition.moveSpeedScale = toFloat(actionStream.Next(), definition.moveSpeedScale);
|
||||||
|
} else if (token == "[cancel rule]") {
|
||||||
|
CharacterCancelRuleDefinition rule;
|
||||||
|
rule.targetAction = toLower(actionStream.Next());
|
||||||
|
rule.beginFrame = toInt(actionStream.Next(), 0);
|
||||||
|
rule.endFrame = toInt(actionStream.Next(), rule.beginFrame);
|
||||||
|
rule.requireGrounded = isTrueToken(actionStream.Next());
|
||||||
|
rule.requireAirborne = isTrueToken(actionStream.Next());
|
||||||
|
definition.cancelRules.push_back(rule);
|
||||||
|
} else if (token == "[frame event]") {
|
||||||
|
CharacterFrameEventDefinition frameEvent;
|
||||||
|
frameEvent.frame = toInt(actionStream.Next(), 0);
|
||||||
|
frameEvent.type = parseFrameEventType(actionStream.Next());
|
||||||
|
if (frameEvent.type == CharacterFrameEventType::SetVelocityXY) {
|
||||||
|
frameEvent.velocityXY.x = toFloat(actionStream.Next(), 0.0f);
|
||||||
|
frameEvent.velocityXY.y = toFloat(actionStream.Next(), 0.0f);
|
||||||
|
} else if (frameEvent.type == CharacterFrameEventType::SetVelocityZ) {
|
||||||
|
frameEvent.velocityZ = toFloat(actionStream.Next(), 0.0f);
|
||||||
|
} else {
|
||||||
|
frameEvent.stringValue = actionStream.Next();
|
||||||
|
}
|
||||||
|
definition.frameEvents.push_back(frameEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (definition.animationTag.empty()) {
|
||||||
|
// 如果动作脚本没有显式指定动画标签,则尝试用同名动作映射。
|
||||||
|
if (config.animationPath.count(actionId) > 0) {
|
||||||
|
definition.animationTag = actionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addOrReplaceAction(actions_, std::move(definition));
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Log("CharacterActionLibrary: loaded %d actions for job %d",
|
||||||
|
static_cast<int>(actions_.size()), config.jobId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<CharacterActionLibrary> loadCharacterActionLibrary(
|
||||||
|
const character::CharacterConfig& config) {
|
||||||
|
CharacterActionLibrary library;
|
||||||
|
if (!library.LoadForConfig(config)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return library;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
142
Game/src/character/CharacterInputRouter.cpp
Normal file
142
Game/src/character/CharacterInputRouter.cpp
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#include "character/CharacterInputRouter.h"
|
||||||
|
#include <SDL2/SDL_gamecontroller.h>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
float applyDeadZone(float value, float deadZone) {
|
||||||
|
return std::fabs(value) >= deadZone ? value : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool CharacterInputRouter::OnKeyDown(const KeyEvent& event) {
|
||||||
|
switch (event.getKeyCode()) {
|
||||||
|
case KeyCode::a:
|
||||||
|
case KeyCode::Left:
|
||||||
|
left_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::d:
|
||||||
|
case KeyCode::Right:
|
||||||
|
right_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::w:
|
||||||
|
case KeyCode::Up:
|
||||||
|
up_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::s:
|
||||||
|
case KeyCode::Down:
|
||||||
|
down_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::j:
|
||||||
|
attackPressed_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::k:
|
||||||
|
jumpPressed_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::l:
|
||||||
|
skill1Pressed_ = true;
|
||||||
|
return true;
|
||||||
|
case KeyCode::LShift:
|
||||||
|
dashPressed_ = true;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterInputRouter::OnKeyUp(KeyCode keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyCode::a:
|
||||||
|
case KeyCode::Left:
|
||||||
|
left_ = false;
|
||||||
|
return true;
|
||||||
|
case KeyCode::d:
|
||||||
|
case KeyCode::Right:
|
||||||
|
right_ = false;
|
||||||
|
return true;
|
||||||
|
case KeyCode::w:
|
||||||
|
case KeyCode::Up:
|
||||||
|
up_ = false;
|
||||||
|
return true;
|
||||||
|
case KeyCode::s:
|
||||||
|
case KeyCode::Down:
|
||||||
|
down_ = false;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterInputRouter::OnJoystickAxis(const JoystickAxisEvent& event) {
|
||||||
|
if (event.getAxis() == SDL_CONTROLLER_AXIS_LEFTX) {
|
||||||
|
leftStickX_ = applyDeadZone(event.getNormalizedValue(), joystickDeadZone_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.getAxis() == SDL_CONTROLLER_AXIS_LEFTY) {
|
||||||
|
leftStickY_ = applyDeadZone(event.getNormalizedValue(), joystickDeadZone_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterInputRouter::OnJoystickButtonDown(
|
||||||
|
const JoystickButtonDownEvent& event) {
|
||||||
|
switch (event.getButton()) {
|
||||||
|
case SDL_CONTROLLER_BUTTON_A:
|
||||||
|
jumpPressed_ = true;
|
||||||
|
return true;
|
||||||
|
case SDL_CONTROLLER_BUTTON_X:
|
||||||
|
attackPressed_ = true;
|
||||||
|
return true;
|
||||||
|
case SDL_CONTROLLER_BUTTON_B:
|
||||||
|
case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER:
|
||||||
|
skill1Pressed_ = true;
|
||||||
|
return true;
|
||||||
|
case SDL_CONTROLLER_BUTTON_LEFTSHOULDER:
|
||||||
|
dashPressed_ = true;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterInputRouter::OnJoystickButtonUp(
|
||||||
|
const JoystickButtonUpEvent& event) {
|
||||||
|
(void)event;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterInputRouter::EmitCommands(CharacterCommandBuffer& commandBuffer) {
|
||||||
|
Vec2 moveAxis(leftStickX_, leftStickY_);
|
||||||
|
if (moveAxis.lengthSquared() < 0.0001f) {
|
||||||
|
moveAxis.x = (right_ ? 1.0f : 0.0f) - (left_ ? 1.0f : 0.0f);
|
||||||
|
moveAxis.y = (down_ ? 1.0f : 0.0f) - (up_ ? 1.0f : 0.0f);
|
||||||
|
}
|
||||||
|
commandBuffer.Submit({CharacterCommandType::Move, moveAxis, 0.0f});
|
||||||
|
|
||||||
|
if (jumpPressed_) {
|
||||||
|
commandBuffer.Submit({CharacterCommandType::JumpPressed, Vec2::Zero(), 0.0f});
|
||||||
|
}
|
||||||
|
if (attackPressed_) {
|
||||||
|
commandBuffer.Submit({CharacterCommandType::AttackPressed, Vec2::Zero(), 0.0f});
|
||||||
|
}
|
||||||
|
if (skill1Pressed_) {
|
||||||
|
commandBuffer.Submit({CharacterCommandType::Skill1Pressed, Vec2::Zero(), 0.0f});
|
||||||
|
}
|
||||||
|
if (dashPressed_) {
|
||||||
|
commandBuffer.Submit({CharacterCommandType::DashPressed, Vec2::Zero(), 0.0f});
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearFrameState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterInputRouter::ClearFrameState() {
|
||||||
|
jumpPressed_ = false;
|
||||||
|
attackPressed_ = false;
|
||||||
|
skill1Pressed_ = false;
|
||||||
|
dashPressed_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
@@ -4,10 +4,18 @@
|
|||||||
namespace frostbite2D {
|
namespace frostbite2D {
|
||||||
|
|
||||||
bool CharacterObject::Construction(int jobId) {
|
bool CharacterObject::Construction(int jobId) {
|
||||||
|
EnableEventReceive();
|
||||||
RemoveAllChildren();
|
RemoveAllChildren();
|
||||||
animationManager_ = nullptr;
|
animationManager_ = nullptr;
|
||||||
config_.reset();
|
config_.reset();
|
||||||
currentAction_.clear();
|
currentAction_.clear();
|
||||||
|
actionLibrary_ = CharacterActionLibrary();
|
||||||
|
commandBuffer_.Clear();
|
||||||
|
currentIntent_ = CharacterIntent();
|
||||||
|
stateMachine_.Reset();
|
||||||
|
motor_ = CharacterMotor();
|
||||||
|
status_ = CharacterStatus();
|
||||||
|
lastDeltaTime_ = 0.0f;
|
||||||
|
|
||||||
auto config = character::loadCharacterConfig(jobId);
|
auto config = character::loadCharacterConfig(jobId);
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@@ -21,6 +29,9 @@ bool CharacterObject::Construction(int jobId) {
|
|||||||
direction_ = 1;
|
direction_ = 1;
|
||||||
config_ = *config;
|
config_ = *config;
|
||||||
equipmentManager_.Init(config_->baseJobConfig);
|
equipmentManager_.Init(config_->baseJobConfig);
|
||||||
|
if (auto actionLibrary = loadCharacterActionLibrary(*config_)) {
|
||||||
|
actionLibrary_ = *actionLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
animationManager_ = MakePtr<CharacterAnimation>();
|
animationManager_ = MakePtr<CharacterAnimation>();
|
||||||
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
|
if (!animationManager_->Init(this, *config_, equipmentManager_)) {
|
||||||
@@ -29,13 +40,16 @@ bool CharacterObject::Construction(int jobId) {
|
|||||||
}
|
}
|
||||||
AddChild(animationManager_);
|
AddChild(animationManager_);
|
||||||
|
|
||||||
if (config_->animationPath.count("rest") > 0) {
|
if (const auto* idleAction = actionLibrary_.GetDefaultAction()) {
|
||||||
|
PlayAnimationTag(idleAction->animationTag);
|
||||||
|
} else if (config_->animationPath.count("rest") > 0) {
|
||||||
SetAction("rest");
|
SetAction("rest");
|
||||||
} else if (!config_->animationPath.empty()) {
|
} else if (!config_->animationPath.empty()) {
|
||||||
SetAction(config_->animationPath.begin()->first);
|
SetAction(config_->animationPath.begin()->first);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetDirection(1);
|
SetWorldPosition({});
|
||||||
|
SetFacing(1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +68,113 @@ void CharacterObject::SetDirection(int direction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CharacterObject::SetCharacterPosition(const Vec2& pos) {
|
void CharacterObject::SetCharacterPosition(const Vec2& pos) {
|
||||||
SetPosition(pos);
|
motor_.SetGroundPosition(pos);
|
||||||
|
SetWorldPosition(motor_.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterObject::SetWorldPosition(const CharacterWorldPosition& pos) {
|
||||||
|
motor_.position = pos;
|
||||||
|
// 逻辑世界坐标是 x/y/z,真正渲染时再投影到屏幕坐标。
|
||||||
|
SetPosition(pos.ToScreenPosition());
|
||||||
|
// 地面 y 继续决定同层对象前后遮挡顺序。
|
||||||
SetZOrder(static_cast<int>(pos.y));
|
SetZOrder(static_cast<int>(pos.y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CharacterObject::PushCommand(const CharacterCommand& command) {
|
||||||
|
commandBuffer_.Submit(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterObject::ApplyHit(const HitContext& hit) {
|
||||||
|
if (status_.invincible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status_.superArmor && !hit.ignoreInterrupt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hit.ignoreInterrupt && !stateMachine_.CanBeInterrupted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterActionDefinition* hurtAction = FindAction(hit.hurtAction);
|
||||||
|
if (!hurtAction) {
|
||||||
|
hurtAction = FindAction("hurt_light");
|
||||||
|
}
|
||||||
|
|
||||||
|
motor_.verticalVelocity = hit.launchZVelocity;
|
||||||
|
if (hit.launchZVelocity > 0.0f) {
|
||||||
|
motor_.grounded = false;
|
||||||
|
}
|
||||||
|
stateMachine_.ForceHurt(hurtAction);
|
||||||
|
PlayAnimationTag(hurtAction ? hurtAction->animationTag
|
||||||
|
: ResolveAnimationTag("damage", "rest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterObject::OnUpdate(float deltaTime) {
|
||||||
|
// 角色主循环顺序固定为:
|
||||||
|
// 输入采样 -> 缓冲推进 -> 意图生成 -> 状态机决策 -> motor 推进 -> 投影到屏幕。
|
||||||
|
lastDeltaTime_ = deltaTime;
|
||||||
|
commandBuffer_.Advance(deltaTime);
|
||||||
|
inputRouter_.EmitCommands(commandBuffer_);
|
||||||
|
currentIntent_ = commandBuffer_.BuildIntent();
|
||||||
|
stateMachine_.Update(*this, commandBuffer_, currentIntent_, deltaTime);
|
||||||
|
motor_.Update(deltaTime);
|
||||||
|
SetWorldPosition(motor_.position);
|
||||||
|
SetFacing(motor_.facing);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterObject::OnKeyDown(const KeyEvent& event) {
|
||||||
|
if (event.isRepeat()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return inputRouter_.OnKeyDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterObject::OnKeyUp(const KeyEvent& event) {
|
||||||
|
return inputRouter_.OnKeyUp(event.getKeyCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterObject::OnJoystickAxis(const JoystickEvent& event) {
|
||||||
|
return inputRouter_.OnJoystickAxis(static_cast<const JoystickAxisEvent&>(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterObject::OnJoystickButtonDown(const JoystickEvent& event) {
|
||||||
|
return inputRouter_.OnJoystickButtonDown(
|
||||||
|
static_cast<const JoystickButtonDownEvent&>(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterObject::OnJoystickButtonUp(const JoystickEvent& event) {
|
||||||
|
return inputRouter_.OnJoystickButtonUp(
|
||||||
|
static_cast<const JoystickButtonUpEvent&>(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterActionDefinition* CharacterObject::FindAction(
|
||||||
|
const std::string& actionId) const {
|
||||||
|
return actionLibrary_.FindAction(actionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string CharacterObject::ResolveAnimationTag(
|
||||||
|
const std::string& preferred,
|
||||||
|
const std::string& fallback) const {
|
||||||
|
// 逻辑动作与资源动作是两层名字:优先选动作定义里的标签,不存在再回退。
|
||||||
|
if (animationManager_ && animationManager_->HasAction(preferred)) {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
if (animationManager_ && animationManager_->HasAction(fallback)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (config_ && !config_->animationPath.empty()) {
|
||||||
|
return config_->animationPath.begin()->first;
|
||||||
|
}
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterObject::PlayAnimationTag(const std::string& actionTag) {
|
||||||
|
SetAction(actionTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterObject::SetFacing(int direction) {
|
||||||
|
motor_.facing = direction >= 0 ? 1 : -1;
|
||||||
|
SetDirection(motor_.facing);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace frostbite2D
|
} // namespace frostbite2D
|
||||||
|
|||||||
333
Game/src/character/CharacterStateMachine.cpp
Normal file
333
Game/src/character/CharacterStateMachine.cpp
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
#include "character/CharacterStateMachine.h"
|
||||||
|
#include "character/CharacterObject.h"
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace frostbite2D {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float kLogicFramesPerSecond = 60.0f;
|
||||||
|
constexpr float kLandingDuration = 0.08f;
|
||||||
|
constexpr float kMovementActionScale = 0.35f;
|
||||||
|
|
||||||
|
bool hasMoveIntent(const CharacterIntent& intent) {
|
||||||
|
return std::abs(intent.moveX) > 0.01f || std::abs(intent.moveY) > 0.01f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CharacterStateMachine::Reset() {
|
||||||
|
currentState_ = CharacterStateId::Idle;
|
||||||
|
currentActionId_.clear();
|
||||||
|
currentAction_ = nullptr;
|
||||||
|
stateTime_ = 0.0f;
|
||||||
|
actionFrameProgress_ = 0.0f;
|
||||||
|
actionFrame_ = 0;
|
||||||
|
landingTimer_ = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::Update(CharacterObject& owner,
|
||||||
|
CharacterCommandBuffer& commandBuffer,
|
||||||
|
const CharacterIntent& intent,
|
||||||
|
float deltaTime) {
|
||||||
|
stateTime_ += deltaTime;
|
||||||
|
|
||||||
|
switch (currentState_) {
|
||||||
|
case CharacterStateId::Idle:
|
||||||
|
case CharacterStateId::Move:
|
||||||
|
case CharacterStateId::Landing:
|
||||||
|
UpdateGroundState(owner, commandBuffer, intent);
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Jump:
|
||||||
|
case CharacterStateId::Fall:
|
||||||
|
UpdateAirState(owner, commandBuffer, intent);
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Action:
|
||||||
|
UpdateActionState(owner, commandBuffer, intent, deltaTime);
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Hurt:
|
||||||
|
UpdateHurtState(owner, deltaTime);
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Dead:
|
||||||
|
owner.GetMotorMutable().StopGroundMovement();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::ForceHurt(const CharacterActionDefinition* hurtAction) {
|
||||||
|
currentState_ = CharacterStateId::Hurt;
|
||||||
|
currentAction_ = hurtAction;
|
||||||
|
currentActionId_ = hurtAction ? hurtAction->actionId : std::string("hurt_light");
|
||||||
|
stateTime_ = 0.0f;
|
||||||
|
actionFrameProgress_ = 0.0f;
|
||||||
|
actionFrame_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterStateMachine::CanAcceptGroundActions() const {
|
||||||
|
return currentState_ == CharacterStateId::Idle
|
||||||
|
|| currentState_ == CharacterStateId::Move
|
||||||
|
|| currentState_ == CharacterStateId::Landing;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterStateMachine::CanBeInterrupted() const {
|
||||||
|
if (currentState_ == CharacterStateId::Action && currentAction_) {
|
||||||
|
return currentAction_->canBeInterrupted;
|
||||||
|
}
|
||||||
|
return currentState_ != CharacterStateId::Dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterStateMachine::IsMovementLocked() const {
|
||||||
|
return currentState_ == CharacterStateId::Action && currentAction_
|
||||||
|
&& !currentAction_->canMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterStateMachine::CanTurn() const {
|
||||||
|
return !(currentState_ == CharacterStateId::Action && currentAction_
|
||||||
|
&& !currentAction_->canTurn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::ChangeState(CharacterObject& owner,
|
||||||
|
CharacterStateId nextState) {
|
||||||
|
if (currentState_ == nextState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState_ = nextState;
|
||||||
|
stateTime_ = 0.0f;
|
||||||
|
if (nextState != CharacterStateId::Action && nextState != CharacterStateId::Hurt) {
|
||||||
|
currentAction_ = nullptr;
|
||||||
|
currentActionId_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (nextState) {
|
||||||
|
case CharacterStateId::Idle:
|
||||||
|
owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest"));
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Move:
|
||||||
|
owner.PlayAnimationTag(owner.ResolveAnimationTag("run", "walk"));
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Jump:
|
||||||
|
case CharacterStateId::Fall:
|
||||||
|
owner.PlayAnimationTag(owner.ResolveAnimationTag("jump", "rest"));
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Landing:
|
||||||
|
landingTimer_ = 0.0f;
|
||||||
|
owner.PlayAnimationTag(owner.ResolveAnimationTag("rest", "rest"));
|
||||||
|
break;
|
||||||
|
case CharacterStateId::Action:
|
||||||
|
case CharacterStateId::Hurt:
|
||||||
|
case CharacterStateId::Dead:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::EnterAction(CharacterObject& owner,
|
||||||
|
const CharacterActionDefinition* action) {
|
||||||
|
// 普攻和技能都走通用 Action 状态,差异由动作定义驱动。
|
||||||
|
currentAction_ = action;
|
||||||
|
currentActionId_ = action ? action->actionId : std::string();
|
||||||
|
currentState_ = CharacterStateId::Action;
|
||||||
|
stateTime_ = 0.0f;
|
||||||
|
actionFrameProgress_ = 0.0f;
|
||||||
|
actionFrame_ = 0;
|
||||||
|
if (action) {
|
||||||
|
owner.PlayAnimationTag(action->animationTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::UpdateGroundState(CharacterObject& owner,
|
||||||
|
CharacterCommandBuffer& commandBuffer,
|
||||||
|
const CharacterIntent& intent) {
|
||||||
|
auto& motor = owner.GetMotorMutable();
|
||||||
|
if (!motor.grounded) {
|
||||||
|
ChangeState(owner, CharacterStateId::Fall);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState_ == CharacterStateId::Landing) {
|
||||||
|
landingTimer_ += owner.GetLastDeltaTime();
|
||||||
|
motor.StopGroundMovement();
|
||||||
|
if (landingTimer_ < kLandingDuration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryStartAction(owner, commandBuffer, CharacterCommandType::Skill1Pressed,
|
||||||
|
"skill_1")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (TryStartAction(owner, commandBuffer, CharacterCommandType::AttackPressed,
|
||||||
|
"attack_1")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (commandBuffer.HasBuffered(CharacterCommandType::JumpPressed)) {
|
||||||
|
commandBuffer.Consume(CharacterCommandType::JumpPressed);
|
||||||
|
motor.Jump();
|
||||||
|
ChangeState(owner, CharacterStateId::Jump);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
motor.ApplyGroundInput(intent.moveX, intent.moveY);
|
||||||
|
owner.SetFacing(motor.facing);
|
||||||
|
if (hasMoveIntent(intent)) {
|
||||||
|
ChangeState(owner, CharacterStateId::Move);
|
||||||
|
} else {
|
||||||
|
motor.StopGroundMovement();
|
||||||
|
ChangeState(owner, CharacterStateId::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::UpdateAirState(CharacterObject& owner,
|
||||||
|
CharacterCommandBuffer& commandBuffer,
|
||||||
|
const CharacterIntent& intent) {
|
||||||
|
auto& motor = owner.GetMotorMutable();
|
||||||
|
motor.ApplyGroundInput(intent.moveX, intent.moveY, 0.75f);
|
||||||
|
owner.SetFacing(motor.facing);
|
||||||
|
|
||||||
|
if (motor.grounded) {
|
||||||
|
ChangeState(owner, CharacterStateId::Landing);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChangeState(owner, motor.verticalVelocity > 0.0f ? CharacterStateId::Jump
|
||||||
|
: CharacterStateId::Fall);
|
||||||
|
|
||||||
|
if (commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
|
||||||
|
commandBuffer.Consume(CharacterCommandType::AttackPressed);
|
||||||
|
}
|
||||||
|
if (commandBuffer.HasBuffered(CharacterCommandType::Skill1Pressed)) {
|
||||||
|
commandBuffer.Consume(CharacterCommandType::Skill1Pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::UpdateActionState(CharacterObject& owner,
|
||||||
|
CharacterCommandBuffer& commandBuffer,
|
||||||
|
const CharacterIntent& intent,
|
||||||
|
float deltaTime) {
|
||||||
|
auto& motor = owner.GetMotorMutable();
|
||||||
|
if (!currentAction_) {
|
||||||
|
ChangeState(owner, CharacterStateId::Idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAction_->canMove) {
|
||||||
|
float moveScale = currentAction_->moveSpeedScale > 0.0f
|
||||||
|
? currentAction_->moveSpeedScale
|
||||||
|
: kMovementActionScale;
|
||||||
|
motor.ApplyGroundInput(intent.moveX, intent.moveY, moveScale);
|
||||||
|
} else {
|
||||||
|
motor.StopGroundMovement();
|
||||||
|
}
|
||||||
|
if (currentAction_->canTurn) {
|
||||||
|
owner.SetFacing(motor.facing);
|
||||||
|
}
|
||||||
|
|
||||||
|
actionFrameProgress_ += deltaTime * kLogicFramesPerSecond;
|
||||||
|
int previousFrame = actionFrame_;
|
||||||
|
actionFrame_ = std::max(0, static_cast<int>(actionFrameProgress_));
|
||||||
|
ApplyFrameEvents(owner, previousFrame + 1, actionFrame_);
|
||||||
|
|
||||||
|
for (const auto& rule : currentAction_->cancelRules) {
|
||||||
|
// 取消窗口由动作定义提供,状态机只负责在允许的帧段里消费缓存输入。
|
||||||
|
if (actionFrame_ < rule.beginFrame || actionFrame_ > rule.endFrame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.requireGrounded && !motor.grounded) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (rule.requireAirborne && motor.grounded) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.targetAction == "attack_2"
|
||||||
|
&& commandBuffer.HasBuffered(CharacterCommandType::AttackPressed)) {
|
||||||
|
if (TryStartAction(owner, commandBuffer, CharacterCommandType::AttackPressed,
|
||||||
|
rule.targetAction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAction_->totalFrames > 0 && actionFrame_ >= currentAction_->totalFrames) {
|
||||||
|
currentAction_ = nullptr;
|
||||||
|
currentActionId_.clear();
|
||||||
|
if (!motor.grounded) {
|
||||||
|
ChangeState(owner, CharacterStateId::Fall);
|
||||||
|
} else if (hasMoveIntent(intent)) {
|
||||||
|
ChangeState(owner, CharacterStateId::Move);
|
||||||
|
} else {
|
||||||
|
ChangeState(owner, CharacterStateId::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::UpdateHurtState(CharacterObject& owner, float deltaTime) {
|
||||||
|
(void)deltaTime;
|
||||||
|
auto& motor = owner.GetMotorMutable();
|
||||||
|
motor.StopGroundMovement();
|
||||||
|
if (!currentAction_) {
|
||||||
|
currentAction_ = owner.FindAction("hurt_light");
|
||||||
|
}
|
||||||
|
|
||||||
|
actionFrameProgress_ += owner.GetLastDeltaTime() * kLogicFramesPerSecond;
|
||||||
|
actionFrame_ = std::max(0, static_cast<int>(actionFrameProgress_));
|
||||||
|
if (currentAction_ && actionFrame_ >= currentAction_->totalFrames) {
|
||||||
|
currentAction_ = nullptr;
|
||||||
|
currentActionId_.clear();
|
||||||
|
if (motor.grounded) {
|
||||||
|
ChangeState(owner, CharacterStateId::Idle);
|
||||||
|
} else {
|
||||||
|
ChangeState(owner, CharacterStateId::Fall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterStateMachine::TryStartAction(CharacterObject& owner,
|
||||||
|
CharacterCommandBuffer& commandBuffer,
|
||||||
|
CharacterCommandType commandType,
|
||||||
|
const std::string& actionId) {
|
||||||
|
if (!commandBuffer.HasBuffered(commandType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterActionDefinition* action = owner.FindAction(actionId);
|
||||||
|
commandBuffer.Consume(commandType);
|
||||||
|
if (!action) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnterAction(owner, action);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterStateMachine::ApplyFrameEvents(CharacterObject& owner,
|
||||||
|
int previousFrame,
|
||||||
|
int currentFrame) {
|
||||||
|
if (!currentAction_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& motor = owner.GetMotorMutable();
|
||||||
|
for (const auto& event : currentAction_->frameEvents) {
|
||||||
|
// 帧事件把“动作进行到第几帧时要做什么”从硬编码里抽出来。
|
||||||
|
if (event.frame < previousFrame || event.frame > currentFrame) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case CharacterFrameEventType::SetVelocityXY:
|
||||||
|
motor.groundVelocity = event.velocityXY;
|
||||||
|
break;
|
||||||
|
case CharacterFrameEventType::SetVelocityZ:
|
||||||
|
motor.verticalVelocity = event.velocityZ;
|
||||||
|
motor.grounded = false;
|
||||||
|
break;
|
||||||
|
case CharacterFrameEventType::FinishAction:
|
||||||
|
actionFrame_ = currentAction_->totalFrames;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace frostbite2D
|
||||||
@@ -38,6 +38,7 @@ Ptr<Sprite> createMapSprite(const std::string& path, int index) {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GameMap::GameMap() {
|
GameMap::GameMap() {
|
||||||
|
EnableEventReceive();
|
||||||
initLayers();
|
initLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ void GameMapTestScene::onEnter() {
|
|||||||
} else {
|
} else {
|
||||||
Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
|
Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f);
|
||||||
character_->SetCharacterPosition(spawnPos);
|
character_->SetCharacterPosition(spawnPos);
|
||||||
|
character_->EnableEventReceive();
|
||||||
|
character_->SetEventPriority(-100);
|
||||||
map_->AddObject(character_);
|
map_->AddObject(character_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user