Godot.NET C#IOC重构(4-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的基本使用,所以很多的设置我都跳过了。

热门相关:隐婚99天:首长,请矜持   美漫大幻想   美女总裁之贴身高手   史上最强赘婿   明尊