Godot.NET C#IOC重構(4-7):絲滑運動控制,角色狀態機,滑牆,蹬牆跳

gclove2000發表於2024-04-15

目錄
  • 相關連結
  • 前言
  • 4:加速和減速
    • move_toward,加速移動
  • 5:角色狀態機
    • 有限狀態機
    • 程式碼改動
      • 改動前
      • 改動後
      • 如何寫狀態機
        • 狀態初始量
        • 狀態進入和退出
        • 強制狀態修改
        • 熟練使用非同步
  • 6:滑牆
    • 圖片拼接
    • 碰撞框對應
    • 判斷是否在牆上
  • 7:蹬牆跳
    • 最佳化跳躍手感
  • 總結

相關連結

十分鐘製作橫版動作遊戲|Godot 4 教程《勇者傳說》#0

Godot Engine 4.2 簡體中文文件

GodotNet_LegendOfPaladin C# 重構專案地址

前言

這次來學習一下Godot的運動控制,Godot中內建了很多資料運算的函式,而且是使用C++整合的,使用C# 呼叫,效能方面肯定是沒有問題的。我這個部落格的序號和影片的序號是完全對應的。有時候一節課的知識點比較少,會一次多寫一些

4:加速和減速

move_toward,加速移動

我們可以看到三個引數就是數學中的,起點,終點,導數。所以我們可以填入起始速度,終點速度,加速度。

using Godot;
using GodotNet_LegendOfPaladin2.Utils;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GodotNet_LegendOfPaladin2.SceneModels
{
    public class PlayerSceneModel : ISceneModel
    {
        private PrintHelper printHelper;
        #region 常量
        /// <summary>
        /// 速度
        /// </summary>
        public const float RUN_SPEED = 200;

        /// <summary>
        /// 加速度,為了顯示明顯,20秒內到達RUN_SPEED的速度
        /// </summary>
        public const float ACCELERATION = (float)(RUN_SPEED / 20);

        /// <summary>
        /// 跳躍速度
        /// </summary>
        public const float JUMP_SPEED = -350;

        #endregion


        public override void Process(double delta)
        {
            PlayerMove(delta);
        }

        private void PlayerMove(double delta)
        {
            var velocity = characterBody2D.Velocity;
            velocity.Y += ProjectSettingHelper.Gravity * (float)delta;
            var direction = Input.GetAxis(ProjectSettingHelper.InputMapEnum.move_left.ToString(),
                ProjectSettingHelper.InputMapEnum.move_right.ToString());
            //原本直接賦值
            //velocity.X = direction*RUN_SPEED;
            //現在使用加速度
            velocity.X = Mathf.MoveToward(velocity.X, direction * RUN_SPEED, ACCELERATION);
          ......

        }
    }
}

5:角色狀態機

我們目前做的動畫效果只是單獨的跑動,跳躍,下落,站立。如果我們的動畫邏輯變得複雜起來,我們的角色的狀態的判斷會變得異常的麻煩。會充斥著大量的if,else判斷。這裡就要引入有限狀態機的概念。

有限狀態機

簡單來說就是,狀態只有一個,每個狀態之間的轉化都是有對應的條件才會執行

程式碼改動

改動前



namespace GodotNet_LegendOfPaladin2.SceneModels
{
    public class PlayerSceneModel : ISceneModel
    {
        ......

        public enum AnimationEnum { REST, Idel, Running, Jump, Fall, Land }


        private void PlayerMove(double delta)
        {
            var velocity = characterBody2D.Velocity;
            velocity.Y += ProjectSettingHelper.Gravity * (float)delta;
            var direction = Input.GetAxis(ProjectSettingHelper.InputMapEnum.move_left.ToString(),
                ProjectSettingHelper.InputMapEnum.move_right.ToString());
            //原本直接賦值
            //velocity.X = direction*RUN_SPEED;
            //現在使用加速度
            velocity.X = Mathf.MoveToward(velocity.X, direction * RUN_SPEED, ACCELERATION);

            if (characterBody2D.IsOnFloor())
            {
                

                if (Mathf.IsZeroApprox(direction))
                {
                    PlayAnimation(AnimationEnum.Idel);
                }
                else
                {
                    PlayAnimation(AnimationEnum.Running);
                }
                if (Input.IsActionJustPressed(ProjectSettingHelper.InputMapEnum.jump.ToString())){
                    velocity.Y = JUMP_SPEED;
                    IsLand = false;
                }
            }
            else if (characterBody2D.Velocity.Y > 0)
            {
                PlayAnimation(AnimationEnum.Fall);
            }
            else
            {
                PlayAnimation(AnimationEnum.Jump);
            }

            if (!Mathf.IsZeroApprox(direction))
            {
                sprite2D.FlipH = direction < 0;
            }



            characterBody2D.Velocity = velocity;
            characterBody2D.MoveAndSlide();

        }


        private void PlayAnimation(AnimationEnum animationEnum)
        {
            animationPlayer.Play(animationEnum.ToString());
        }

        ......
    }
}

改動後

using Godot;
using GodotNet_LegendOfPaladin2.Utils;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Godot.TextServer;

namespace GodotNet_LegendOfPaladin2.SceneModels
{
    public class PlayerSceneModel : ISceneModel
    {
        ......

        public PlayerSceneModel(PrintHelper printHelper)
        {

            this.printHelper = printHelper;
            this.printHelper.SetTitle(nameof(PlayerSceneModel));
        }


        public override void Process(double delta)
        {
            PlayerMove(delta);

            SetAnimation();
        }

        /// <summary>
        /// 角色移動
        /// </summary>
        /// <param name="delta"></param>
        private void PlayerMove(double delta)
        {
            var velocity = characterBody2D.Velocity;
            velocity.Y += ProjectSettingHelper.Gravity * (float)delta;
            Direction = Input.GetAxis(ProjectSettingHelper.InputMapEnum.move_left.ToString(),
                ProjectSettingHelper.InputMapEnum.move_right.ToString());
            //原本直接賦值
            //velocity.X = direction*RUN_SPEED;
            //現在使用加速度
            velocity.X = Mathf.MoveToward(velocity.X, Direction * RUN_SPEED, ACCELERATION);

            if(characterBody2D.IsOnFloor() && Input.IsActionJustPressed(ProjectSettingHelper.InputMapEnum.jump.ToString()))
            {
                velocity.Y = JUMP_SPEED;
                AnimationState = AnimationEnum.Jump;
            }
            characterBody2D.Velocity = velocity;
            characterBody2D.MoveAndSlide();

        }

        private void SetAnimation()
        {
            switch (AnimationState)
            {
                case AnimationEnum.Idel:
                    if (!Mathf.IsZeroApprox(Direction))
                    {
                        AnimationState = AnimationEnum.Running;
                    }
                    break;
                case AnimationEnum.Jump:
                    if (characterBody2D.Velocity.Y < 0)
                    {
                        AnimationState = AnimationEnum.Fall;
                        
                    }
                    break;
                case AnimationEnum.Running:
                    if (Mathf.IsZeroApprox(Direction))
                    {
                        AnimationState = AnimationEnum.Idel;
                    }
                    break;
                case AnimationEnum.Fall:
                    if (Mathf.IsZeroApprox(characterBody2D.Velocity.Y))
                    {
                        AnimationState = AnimationEnum.Land;
                        //開啟非同步任務,如果過了400毫秒,仍然是Land,則轉為Idel
                        Task.Run(async () =>
                        {
                            await Task.Delay(400);
                            if(AnimationState == AnimationEnum.Land)
                            {
                                AnimationState = AnimationEnum.Idel;

                            }
                        });
                    }
                    break;
                case AnimationEnum.Land:
                    
                    break;
            }

            if (!Mathf.IsZeroApprox(Direction))
            {
                sprite2D.FlipH = Direction < 0;
            }
            PlayAnimation();
        }

        /// <summary>
        /// 播放動畫
        /// </summary>
        private void PlayAnimation()
        {
            //printHelper.Debug(AnimationState.ToString());

            animationPlayer.Play(AnimationState.ToString());
        }

        /// <summary>
        /// 是否準備好了
        /// </summary>
        public override void Ready()
        {
            characterBody2D = Scene.GetNode<CharacterBody2D>("CharacterBody2D");
            camera2D = characterBody2D.GetNode<Camera2D>("Camera2D");
            sprite2D = characterBody2D.GetNode<Sprite2D>("Sprite2D");
            animationPlayer = characterBody2D.GetNode<AnimationPlayer>("AnimationPlayer");
            printHelper.Debug("載入完成");
            AnimationState = AnimationEnum.Idel;
            PlayAnimation();
        }
    }
}

如何寫狀態機

個人不建議用圖形狀態機,狀態一多就容易成蜘蛛網,而且後期維護困難

狀態初始量

狀態機應該最先想狀態的初始狀態,一般來說是Idel戰力狀態

狀態進入和退出

你進入了一個狀態之後,一定寫個如何退出這個狀態。至少有一個出口

強制狀態修改

有些時候我們需要將狀態強制修改,比如跳躍,無論你當時是什麼狀態,一但按下跳躍就要播放跳躍動畫。

熟練使用非同步

非同步我之前的部落格講解過,Godot出於UI執行緒的安全,不允許在新執行緒裡面對Godot節點進行修改。

Godot UI執行緒,Task非同步和訊息彈窗通知

6:滑牆

圖片拼接

由於我們拿到的圖片是同一個角色,但是分成了兩個圖片,這裡推薦一個圖片拼接網站。

線上圖片拼接


開啟之後確認格式是正確的

碰撞框對應


判斷是否在牆上

characterBody2D也有一個是否在牆上的判斷
characterBody2D.IsOnWall()

7:蹬牆跳

先個一個蹬牆跳的速度

        /// <summary>
        /// 蹬牆跳的速度
        /// </summary>
        public readonly Vector2 WALL_JUMP_VELOCITY = new Vector2(400, -320);
            //如果按下跳躍鍵
           if (Input.IsActionJustPressed(ProjectSettingHelper.InputMapEnum.jump.ToString()))
           {
               
               if (characterBody2D.IsOnFloor())
               {

                   velocity.Y = JUMP_SPEED;
                   AnimationState = AnimationEnum.Jump;
               }
               else if (AnimationState == AnimationEnum.WallSliding)
               {
  
                   velocity = WALL_JUMP_VELOCITY;
                   //獲取牆面的法線的方向
                   velocity.X *= characterBody2D.GetWallNormal().X;
                   AnimationState = AnimationEnum.Jump;

               }
           }

最佳化跳躍手感

我們之前學過一個Mathf.MoveToward,這個其實特別適合做定時器的計算。我們這裡將跳躍的按鍵判斷變成時間計時判斷

      /// <summary>
      /// 跳躍重置時間
      /// </summary>
      public const float JudgeIsJumpTime = 0.5f;
      private float isJumpTime = 0;

      ......
        /// <summary>
        /// 角色移動
        /// </summary>
        /// <param name="delta"></param>
        private void PlayerMove(double delta)
        {
            var velocity = characterBody2D.Velocity;
            velocity.Y += ProjectSettingHelper.Gravity * (float)delta;
            Direction = Input.GetAxis(ProjectSettingHelper.InputMapEnum.move_left.ToString(),
                ProjectSettingHelper.InputMapEnum.move_right.ToString());
            //原本直接賦值
            //velocity.X = direction*RUN_SPEED;
            //現在使用加速度
            velocity.X = Mathf.MoveToward(velocity.X, Direction * RUN_SPEED, ACCELERATION);
            //按下跳躍鍵,就將跳躍時間設定為判斷區間
            if (Input.IsActionJustPressed(ProjectSettingHelper.InputMapEnum.jump.ToString()))
            {
                isJumpTime = JudgeIsJumpTime;
            }
            //慢慢變成0
            isJumpTime = (float)Mathf.MoveToward(isJumpTime,0,delta);

            //如果在跳躍時間的判斷內
            if (isJumpTime != 0)
            {
                
                if (characterBody2D.IsOnFloor())
                {
                    //進行跳躍之後,跳躍時間結束
                    isJumpTime = 0;
                    velocity.Y = JUMP_SPEED;
                    AnimationState = AnimationEnum.Jump;
                }
                else if (AnimationState == AnimationEnum.WallSliding)
                {
                    //進行跳躍之後,跳躍時間結束
                    isJumpTime = 0;
                    velocity = WALL_JUMP_VELOCITY;
                    //獲取牆面的法線的方向
                    velocity.X *= characterBody2D.GetWallNormal().X;
                    AnimationState = AnimationEnum.Jump;

                }
            }

            characterBody2D.Velocity = velocity;
            characterBody2D.MoveAndSlide();

        }

總結

我之後寫的遊戲是回合制戰鬥遊戲,這個只是為了簡單的過一下Godot的基本使用,所以很多的設定我都跳過了。

相關文章