- 相关链接
- 前言
-
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,这个其实特别适合做定时器的计算。我们这里将跳跃的按键判断变成时间计时判断文章来源:https://www.toymoban.com/news/detail-852745.html
/// <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的基本使用,所以很多的设置我都跳过了。文章来源地址https://www.toymoban.com/news/detail-852745.html
到了这里,关于Godot.NET C#IOC重构(4-7):丝滑运动控制,角色状态机,滑墙,蹬墙跳的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!