- 相關連結
- 前言
- 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的基本使用,所以很多的設定我都跳過了。