【2D横版游戏开发】godot+c#实现状态机

前言

在我前一个文章,成功实现了玩家动画,并进行【行走】【跳跃】【攻击】
但是这引申出一个问题,一旦后续我加入了更多状态,【受伤】【死亡】以及更多的时候,那么他们的逻辑就会更加混乱
因此我们这边用状态机进行我们玩家状态的管理,使用状态机之后执行如下

在这里插入图片描述

状态机

  • 什么是状态机?

我相信很多人都听过状态机,但是对于状态机的理解云里雾里
所谓:状态机(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#通过状态机管理我们玩家的不同状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值