前言
在我前一个文章,成功实现了玩家动画,并进行
【行走】【跳跃】【攻击】
但是这引申出一个问题,一旦后续我加入了更多状态,【受伤】【死亡】
以及更多的时候,那么他们的逻辑就会更加混乱
因此我们这边用状态机进行我们玩家状态的管理,使用状态机之后执行如下
状态机
什么是状态机?
我相信很多人都听过状态机,但是对于状态机的理解云里雾里
所谓:状态机(State Machine)
是一种管理对象(如游戏角色
)行为模式的编程设计模式🎮。它通过将复杂的行为分解为多个独立的状态
,每个状态定义对象在特定情况下的行为,并根据预设的条件(条件或事件
) 在不同状态间切换
,从而使代码更加清晰、易于维护和扩展。
状态机的原理
在
任意时刻
,一个对象只能处于一个状态
,并且可以根据接收到的输入或事件
,按照预定义的规则从一个状态转换到另一个状态
。每个状态通常包含三个部分:进入状态时的操作(Enter)、状态中的持续操作(Update 或 Physics_Update)以及退出状态时的操作(Exit)
转换到我们的代码上,会通过
Ready
函数方法进行一个初始化状态,并且在Process
函数方法检查输入和触发事件,进行不同的操作切换到不同状态,每一个状态有对应的逻辑切换到别的状态
,实现了状态循环
步骤
要调整成使用状态机,首先,
我们需要有一个状态机统一管理中心,我们叫它StateMachine
然后,我需要有一个可以控制状态的行为约束
,管理进入状态
,退出状态
更新状态
,
然后在我们的状态机StateMachine
获取所有子节点,获取所有状态
并且初始化状态
接着,针对我们需要用到的状态,进行建立各自的状态节点,放到状态机下,进行统一管理
其中,在我的逻辑里面,我为了精简引用,加了一个玩家数据实体类,统一设置引用
节点结构
按照梳理的逻辑,它结构应该需要这样创建
Player (CharacterBody2D)
├── AnimatedSprite2D(挂载Player.cs)
├── CollisionShape2D
├── PlayerEntity(Node2D) (挂载PlayerEntity.cs)
└── StateMachine(Node) (挂载StateMachine.cs)
├── PlayerIdleState(Node) (挂载PlayerIdleState.cs)
├── PlayerRunState (Node)(挂载PlayerRunState.cs)
├── PlayerJumpState (Node)(挂载PlayerJumpState.cs)
├── PlayerAttackState (Node)(挂载PlayerAttackState.cs)
├── PlayerHurtState (Node)(挂载PlayerHurtState.cs)
└── PlayerDeathState (Node)(挂载PlayerDeathState.cs)
└── PlayerFallState(Node)(挂载PlayerFallState.cs)
输入映射&动画调整
当前我们的项目,还没有实现受伤,死亡逻辑,为了模拟进入受伤,死亡,我添加了输入映射
具体怎么添加输入映射,请参考我的前面的文章
针对玩家的动画,进行调整,添加需要的动画,总共需要
attack
:攻击动画
idle
:待机动画
run
:行走动画
hurt
:受伤动画
death
:死亡动画
代码逻辑添加&调整
状态机
首先,我们先添加状态机相关代码
行为约束
using Godot;
public interface IState
{
void Enter();
void Exit();
void Update(double delta);
void PhysicsUpdate(double delta);
}
状态机获取,节点引用,泛型约束
using Godot;
public partial class BaseState<T> : Node, IState where T : Entity
{
protected T Entity;
protected StateMachine StateMachine;
public void SetEntity(T entity)
{
Entity = entity;
}
public void SetStateMachine(StateMachine stateMachine)
{
StateMachine = stateMachine;
}
public virtual void Enter() { }
public virtual void Exit() { }
public virtual void Update(double delta) { }
public virtual void PhysicsUpdate(double delta) { }
}
状态机脚本
,初始化状态,获取所有状态节点,并且获取节点引用
using Godot;
using System;
public partial class StateMachine : Node
{
[Export] private Entity _entity;
[Export] private NodePath _initialStatePath;
private IState _currentState;
private IState _previousState;
public override void _Ready()
{
base._Ready();
foreach (Node child in GetChildren())
{
if (child is BaseState<Entity> baseState)
{
baseState.SetEntity(_entity);
baseState.SetStateMachine(this);
}
else if (child is IState state)
{
//尝试使用动态类型检查
var setEntityMethod = child.GetType().GetMethod("SetEntity");
if (setEntityMethod != null)
{
setEntityMethod.Invoke(child, new object[] { _entity });
}
var setStateMachineMethod = child.GetType().GetMethod("SetStateMachine");
if (setStateMachineMethod != null)
{
setStateMachineMethod.Invoke(child, new object[] { this });
}
}
}
if (_initialStatePath != null && !_initialStatePath.IsEmpty)
{
var initialState = GetNode<Node>(_initialStatePath);
if (initialState is IState state)
{
_currentState = state;
_currentState.Enter();
}
else
{
GD.PrintErr("Initial state does not implement IState interface!");
}
}
else
{
GD.PrintErr("No initial state specified!");
}
}
public override void _Process(double delta)
{
base._Process(delta);
_currentState?.Update(delta);
}
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
_currentState?.PhysicsUpdate(delta);
}
public void TransitionTo<TState>() where TState : Node, IState
{
foreach (Node child in GetChildren())
{
if (child is TState newState)
{
_currentState?.Exit();
_previousState = _currentState;
_currentState = newState;
_currentState.Enter();
return;
}
}
GD.PrintErr($"State {typeof(TState).Name} not found!");
}
public void TransitionToPrevious()
{
if (_previousState != null)
{
_currentState?.Exit();
var temp = _currentState;
_currentState = _previousState;
_previousState = temp;
_currentState.Enter();
}
}
public void SetEntity(Entity entity)
{
_entity = entity;
// 更新所有已存在状态的实体引用
foreach (Node child in GetChildren())
{
if (child is BaseState<Entity> baseState)
{
baseState.SetEntity(_entity);
}
}
}
}
通用引用数据脚本
引用数据脚本基类
using Godot;
public partial class Entity : Node2D
{
[Export] public CharacterBody2D CharacterBody { get; set; }
[Export] public AnimatedSprite2D AnimatedSprite { get; set; }
// 通用属性
[Export] public float Health { get; set; } = 100f;
[Export] public float MaxHealth { get; set; } = 100f;
// 通用方法
public virtual void TakeDamage(float amount)
{
Health -= amount;
if (Health <= 0)
{
Die();
}
}
protected virtual void Die()
{
GD.Print("死亡");
}
}
玩家引用数据脚本
using Godot;
public partial class PlayerEntity : Entity
{
[Export] public int PlayerId { get; set; } = 1;
[Export] public int Score { get; set; } = 0;
[Export] public int Lives { get; set; } = 3;
// 玩家特定的属性
[Export] public bool CanDoubleJump { get; set; } = true;
[Export] public bool HasDash { get; set; } = false;
[Export] public StateMachine StateMachine { get; set; }
// 重写受伤方法
public override void TakeDamage(float amount)
{
base.TakeDamage(amount);
// 玩家特定的受伤逻辑
if (Health > 0)
{
// 触发受伤状态
StateMachine.TransitionTo<PlayerHurtState>();
}
}
protected override void Die()
{
base.Die();
// 玩家特定的死亡逻辑
Lives--;
if (Lives > 0)
{
// 复活逻辑
GD.Print("Player respawns");
}
else
{
// 游戏结束逻辑
GD.Print("Game Over");
}
// 触发死亡状态
StateMachine.TransitionTo<PlayerDeathState>();
}
// 玩家特定方法
public void AddScore(int points)
{
Score += points;
}
}
玩家子状态
这里需要规划我们玩家应该有什么状态,这个需要按照你实际开发的游戏来
如我现在制作的有待机
行走
攻击
受伤
死亡
跳跃
下落
那么需要每一个都有对应的状态节点和脚本
其中,下落和跳跃因为素材限制,不添加动画,在逻辑上统一使用待机动画
待机状态脚本
using Godot;
public partial class PlayerIdleState : BaseState<PlayerEntity>
{
public override void Enter()
{
Entity.AnimatedSprite.Play("idle");
}
public override void PhysicsUpdate(double delta)
{
// 检查是否离开地面
if (!Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerFallState>(); // 改为切换到下落状态
return;
}
if (Entity.CharacterBody.IsOnFloor())
{
Entity.CharacterBody.Velocity = new Vector2(0, Entity.CharacterBody.Velocity.Y);
}
Vector2 direction = GameInputEvent.MovementInput();
if (direction != Vector2.Zero)
{
StateMachine.TransitionTo<PlayerRunState>();
return;
}
if (GameInputEvent.IsJumping() && Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerJumpState>();
return;
}
if (GameInputEvent.IsAttacking())
{
StateMachine.TransitionTo<PlayerAttackState>();
return;
}
if (GameInputEvent.isHurt())
{
StateMachine.TransitionTo<PlayerHurtState>();
return;
}
if (GameInputEvent.isDeath())
{
StateMachine.TransitionTo<PlayerDeathState>();
return;
}
}
}
行走状态脚本
using Godot;
public partial class PlayerRunState : BaseState<PlayerEntity>
{
[Export] public float Speed = 300.0f;
[Export] public float GroundFriction = 0.2f;
public override void Enter()
{
Entity.AnimatedSprite.Play("run");
}
public override void PhysicsUpdate(double delta)
{
// 检查是否离开地面
if (!Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerFallState>();
return;
}
Vector2 direction = GameInputEvent.MovementInput();
// 处理移动
if (direction != Vector2.Zero)
{
float targetSpeed = direction.X * Speed;
Entity.CharacterBody.Velocity = new Vector2(
(float)Mathf.Lerp(Entity.CharacterBody.Velocity.X, targetSpeed, 0.2f),
Entity.CharacterBody.Velocity.Y
);
// 设置朝向
Entity.AnimatedSprite.FlipH = direction.X < 0;
}
else
{
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X * (1 - GroundFriction),
Entity.CharacterBody.Velocity.Y
);
if (Mathf.Abs(Entity.CharacterBody.Velocity.X) < 1.0f)
{
Entity.CharacterBody.Velocity = new Vector2(0, Entity.CharacterBody.Velocity.Y);
StateMachine.TransitionTo<PlayerIdleState>();
return;
}
}
// 状态转换检查
if (GameInputEvent.IsJumping() && Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerJumpState>();
return;
}
if (GameInputEvent.IsAttacking())
{
StateMachine.TransitionTo<PlayerAttackState>();
return;
}
}
}
攻击状态脚本
using Godot;
public partial class PlayerAttackState : BaseState<PlayerEntity>
{
private bool _animationFinished = false;
public override void Enter()
{
_animationFinished = false;
Entity.AnimatedSprite.Play("attack");
Entity.AnimatedSprite.AnimationFinished += OnAnimationFinished;
}
public override void Exit()
{
Entity.AnimatedSprite.AnimationFinished -= OnAnimationFinished;
}
public override void PhysicsUpdate(double delta)
{
if (_animationFinished)
{
Vector2 direction = GameInputEvent.MovementInput();
if (GameInputEvent.IsJumping() && !Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerJumpState>();
}
else if (direction != Vector2.Zero)
{
StateMachine.TransitionTo<PlayerRunState>();
}
else
{
StateMachine.TransitionTo<PlayerIdleState>();
}
}
}
private void OnAnimationFinished()
{
if (Entity.AnimatedSprite.Animation == "attack")
{
_animationFinished = true;
}
}
}
跳跃状态脚本
using Godot;
public partial class PlayerJumpState : BaseState<PlayerEntity>
{
[Export] public float JumpVelocity = -400.0f;
[Export] public float DoubleJumpVelocity = -300.0f;
[Export] public float AirResistance = 0.05f;
[Export] public float Gravity = 1000.0f;
[Export] public float Speed = 300.0f;
private int _jumpCount = 0;
private const int MAX_JUMPS = 2;
public override void Enter()
{
Entity.AnimatedSprite.Play("idle");
if (Entity.CharacterBody.IsOnFloor() || _jumpCount == 0)
{
_jumpCount = 1;
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X,
JumpVelocity
);
}
else if (_jumpCount < MAX_JUMPS && Entity.CanDoubleJump)
{
_jumpCount++;
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X,
DoubleJumpVelocity
);
}
}
public override void PhysicsUpdate(double delta)
{
if (Entity.CharacterBody.Velocity.Y >= 0)
{
// 当开始下落时,切换到下落状态
StateMachine.TransitionTo<PlayerFallState>();
return;
}
// 应用重力
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X,
Entity.CharacterBody.Velocity.Y + Gravity * (float)delta
);
// 处理空中移动
Vector2 direction = GameInputEvent.MovementInput();
if (direction != Vector2.Zero)
{
float targetSpeed = direction.X * Speed;
Entity.CharacterBody.Velocity = new Vector2(
(float)Mathf.Lerp(Entity.CharacterBody.Velocity.X, targetSpeed, 0.1f),
Entity.CharacterBody.Velocity.Y
);
// 设置朝向
Entity.AnimatedSprite.FlipH = direction.X < 0;
}
else
{
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X * (1 - AirResistance),
Entity.CharacterBody.Velocity.Y
);
// 添加速度归零检查
if (Mathf.Abs(Entity.CharacterBody.Velocity.X) < 1.0f)
{
Entity.CharacterBody.Velocity = new Vector2(0, Entity.CharacterBody.Velocity.Y);
}
}
// 状态转换检查
if (Entity.CharacterBody.IsOnFloor())
{
_jumpCount = 0;
if (GameInputEvent.IsAttacking())
{
StateMachine.TransitionTo<PlayerAttackState>();
}
else if (direction != Vector2.Zero)
{
StateMachine.TransitionTo<PlayerRunState>();
}
else
{
StateMachine.TransitionTo<PlayerIdleState>();
}
}
else if (GameInputEvent.IsJumping() && _jumpCount < MAX_JUMPS && Entity.CanDoubleJump)
{
_jumpCount++;
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X,
DoubleJumpVelocity
);
}
}
}
受伤状态脚本
using Godot;
public partial class PlayerHurtState : BaseState<PlayerEntity>
{
private float _hurtTimer = 0f;
private const float HURT_DURATION = 0.5f;
public override void Enter()
{
_hurtTimer = 0f;
Entity.AnimatedSprite.Play("hurt");
}
public override void Update(double delta)
{
_hurtTimer += (float)delta;
if (_hurtTimer >= HURT_DURATION)
{
Vector2 direction = GameInputEvent.MovementInput();
if (GameInputEvent.IsJumping() && !Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerJumpState>();
}
else if (direction != Vector2.Zero)
{
StateMachine.TransitionTo<PlayerRunState>();
}
else
{
StateMachine.TransitionTo<PlayerIdleState>();
}
}
}
}
死亡状态脚本
using Godot;
public partial class PlayerDeathState : BaseState<PlayerEntity>
{
public override void Enter()
{
Entity.AnimatedSprite.Play("death");
// 可以在这里添加死亡逻辑,比如禁用碰撞、显示游戏结束画面等
}
/**
测试用
*/
public override void PhysicsUpdate(double delta)
{
Vector2 direction = GameInputEvent.MovementInput();
if (direction != Vector2.Zero)
{
StateMachine.TransitionTo<PlayerRunState>();
return;
}
if (GameInputEvent.IsJumping() && Entity.CharacterBody.IsOnFloor())
{
StateMachine.TransitionTo<PlayerJumpState>();
return;
}
if (GameInputEvent.IsAttacking())
{
StateMachine.TransitionTo<PlayerAttackState>();
return;
}
if (GameInputEvent.isHurt())
{
StateMachine.TransitionTo<PlayerHurtState>();
return;
}
if (GameInputEvent.isDeath())
{
StateMachine.TransitionTo<PlayerDeathState>();
return;
}
}
}
下落状态脚本
using Godot;
public partial class PlayerFallState : BaseState<PlayerEntity>
{
[Export] public float AirResistance = 0.05f;
[Export] public float Gravity = 1000.0f;
[Export] public float Speed = 300.0f;
public override void Enter()
{
Entity.AnimatedSprite.Play("idle");
}
public override void PhysicsUpdate(double delta)
{
// 应用重力
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X,
Entity.CharacterBody.Velocity.Y + Gravity * (float)delta
);
// 处理空中移动
Vector2 direction = GameInputEvent.MovementInput();
if (direction != Vector2.Zero)
{
float targetSpeed = direction.X * Speed;
Entity.CharacterBody.Velocity = new Vector2(
(float)Mathf.Lerp(Entity.CharacterBody.Velocity.X, targetSpeed, 0.1f),
Entity.CharacterBody.Velocity.Y
);
// 设置朝向
Entity.AnimatedSprite.FlipH = direction.X < 0;
}
else
{
Entity.CharacterBody.Velocity = new Vector2(
Entity.CharacterBody.Velocity.X * (1 - AirResistance),
Entity.CharacterBody.Velocity.Y
);
// 添加速度归零检查
if (Mathf.Abs(Entity.CharacterBody.Velocity.X) < 1.0f)
{
Entity.CharacterBody.Velocity = new Vector2(0, Entity.CharacterBody.Velocity.Y);
}
}
// 状态转换检查
if (Entity.CharacterBody.IsOnFloor())
{
direction = GameInputEvent.MovementInput();
if (GameInputEvent.IsAttacking())
{
StateMachine.TransitionTo<PlayerAttackState>();
}
else if (direction != Vector2.Zero)
{
StateMachine.TransitionTo<PlayerRunState>();
}
else
{
StateMachine.TransitionTo<PlayerIdleState>();
}
}
else if (GameInputEvent.IsJumping() && Entity.CanDoubleJump)
{
StateMachine.TransitionTo<PlayerJumpState>();
}
}
}
玩家脚本&输入映射调整
为配合状态机,我们需要同步修改Player.cs的脚本,和输入映射获取的脚本
using Godot;
using System;
public partial class Player : CharacterBody2D
{
[Export] public Entity EntityComponent { get; set; }
[Export] public StateMachine StateMachine { get; set; }
public override void _Ready()
{
base._Ready();
// 确保EntityComponent正确初始化
if (EntityComponent == null)
{
// 尝试自动查找
EntityComponent = GetNode<Entity>("playerEntity");
}
// 获取状态机并设置实体引用
var stateMachine = GetNode<StateMachine>("StateMachine");
if (stateMachine != null && EntityComponent != null)
{
// 这里可能需要添加一个公共方法来设置状态机的实体
stateMachine.SetEntity(EntityComponent);
}
}
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
MoveAndSlide();
}
}
using Godot;
using System;
public class GameInputEvent
{
public static Vector2 direction;
public static Vector2 MovementInput()
{
if (Input.IsActionPressed("move_left"))
{
direction = Vector2.Left;
}
else if (Input.IsActionPressed("move_right"))
{
direction = Vector2.Right;
}
else
{
direction = Vector2.Zero;
}
return direction;
}
public static bool IsAttacking()
{
return Input.IsActionJustPressed("attack");
}
public static bool IsJumping() {
return Input.IsActionJustPressed("jump");
}
public static bool isMove()
{
return direction != Vector2.Zero;
}
public static bool isHurt() {
return Input.IsActionJustPressed("hurt");
}
public static bool isDeath()
{
return Input.IsActionJustPressed("death");
}
}
配置引用
代码编写好之后请进行
编译
,并按照我前面的玩家角色节点以此创建节点和绑定脚本。
然后我们代码中,我们有设定[Export]
需要获取到玩家角色,和玩家引用数据脚本,需要依次添加引用
当前需要添加引用的是Player
playerEntity
StateMachine
然后
启动游戏
即可
结语
如上我们用godot+c#通过状态机管理我们玩家的不同状态