- 前言
- 場景繼承
- 在SceneModel裡面新增基礎的節點獲取
- EnemyScene.cs
- EnemySceneModel.cs
- Godot Export屬性和Enum
- Export預設值問題
- 修改前
- EnemySceneModel.cs
- EnemyScene.cs
- 修改後
- EnemyScene.cs
- EnemySceneModel.cs
- 修改前
- 但是有個問題,有必要這麼寫嗎?
- 在SceneModel裡面新增基礎的節點獲取
- 匯入野豬圖片
- 圖片拼接
- RayCast2D 射線碰撞檢測
- 碰撞檢測
- 碰撞層
- 碰撞層命名
- 狀態機
- 狀態機的朝向問題和載入問題
- 碰撞檢測
- 總結
前言
這個實在是拖了太久了,這次速戰速決
場景繼承
由於我們是C# ,C# 有更強的繼承關係,所以我們直接繼承即可
在SceneModel裡面新增基礎的節點獲取
EnemyScene.cs
using Godot;
using GodotNet_LegendOfPaladin2.SceneModels;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GodotNet_LegendOfPaladin2.SceneScripts
{
public partial class EnemyScene : Node2D
{
[Export]
public EnemySceneModel.DirectionEnum Direction
{
get => Model.Direction;
set => Model.Direction = value;
}
public EnemySceneModel Model { get; private set; }
public EnemyScene()
{
Model = Program.Services.GetService<EnemySceneModel>();
Model.Scene = this;
}
public override void _Ready()
{
Model.Ready();
base._Ready();
}
public override void _Process(double delta)
{
Model.Process(delta);
base._Process(delta);
}
}
}
EnemySceneModel.cs
public class EnemySceneModel : ISceneModel
{
private PrintHelper printHelper;
private CharacterBody2D characterBody2D;
private CollisionShape2D collisionShape2D;
private Sprite2D sprite2D;
private AnimationPlayer animationPlayer;
public enum DirectionEnum
{
Left, Right
}
public DirectionEnum Direction { get; set; }
public EnemySceneModel(PrintHelper printHelper)
{
this.printHelper = printHelper;
printHelper.SetTitle(nameof(EnemySceneModel));
}
public EnemySceneModel() { }
public override void Process(double delta)
{
}
public override void Ready()
{
characterBody2D = Scene.GetNode<CharacterBody2D>("CharacterBody2D");
collisionShape2D = characterBody2D.GetNode<CollisionShape2D>("CollisionShape2D");
sprite2D = characterBody2D.GetNode<Sprite2D>("Sprite2D");
animationPlayer = characterBody2D.GetNode<AnimationPlayer>("AnimationPlayer");
printHelper.Debug("載入成功!");
printHelper.Debug($"當前朝向是:{Direction}");
}
}
Godot Export屬性和Enum
Godot C# 是可以匯出Enum的
public enum DirectionEnum
{
Left, Right
}
。。。。。。
[Export]
public EnemySceneModel.DirectionEnum Direction
我測試過,輸入left還是right都能正確的獲取到的
Export預設值問題
如果使用我的Scenes+SceneModels框架,就沒有預設值了
修改前
EnemySceneModel.cs
/// <summary>
/// 最大速度
/// </summary>
public int MaxSpeed { get; set; } = 180;
/// <summary>
/// 加速度
/// </summary>
public int AccelerationSpeed { get; set; } = 2000;
EnemyScene.cs
[Export]
public int MaxSpeed
{
get => Model.MaxSpeed;
set => Model.MaxSpeed = value;
}
[Export]
public int AccelerationSpeed
{
get => Model.AccelerationSpeed;
set => Model.AccelerationSpeed = value;
}
這樣是不行的,沒有預設值的
修改後
EnemyScene.cs
[Export]
public int MaxSpeed = 180;
[Export]
public int AccelerationSpeed = 2000;
EnemySceneModel.cs
/// <summary>
/// 最大速度
/// </summary>
public int MaxSpeed { get; set; }
/// <summary>
/// 加速度
/// </summary>
public int AccelerationSpeed { get; set; }
但是有個問題,有必要這麼寫嗎?
我測試過,如果場景有節點的話,這麼寫是不行的。感覺還不如每個Enemy都單獨寫一個好一些。
匯入野豬圖片
圖片拼接
我們開啟資源包,可以看到野豬的圖片被分割了。我們肯定是喜歡儘可能的用一張圖片
線上圖片拼接
將圖片整合在一個資料夾裡面,進行數字編號
然後和我說要註冊會員,我肯定是不會付的,然後我又找了個網站
做好圖 圖片線上拼接
這個就簡單多了,直接拼接下載就行了,也不用輸入寬度
匯入成功!
RayCast2D 射線碰撞檢測
Godot Engine 4.2 簡體中文文件 所有類 RayCast2D
RayCast2D是專門用於碰撞檢測的碰撞線,是隻有方向和長度,沒有寬度的線段。
碰撞檢測
我們碰撞檢測得檢測三個物體,牆,地面,玩家
碰撞層
Godot 給我們提供了32個碰撞層。
碰撞分為Layer和Mask,簡單來說就是類似於正極和負極。得碰撞的Layer和被碰撞的Mask是同一層才會發生碰撞。
碰撞層命名
碰撞層預設是序號1-32,我們可以給碰撞層進行命名。一般我們層的命名的順序是,層號越低,越底層。一般我們的一樓是給環境,二樓給玩家,三樓給敵人。
一般我們先給選擇Layer,然後再思考他會和哪些層發生碰撞。
- 環境:Layer1,
- 玩家:layer2,Mask1。只會和環境碰撞,但是不會和敵人碰撞。不然怪物就穿不過去了。
- 敵人:Layer3,Mask1,Mask2。會和環境,但是敵人直接是不相互碰撞的。
狀態機
狀態機只負責狀態,不負責移動。
public enum AnimationEnum
{
Hit, Idle, Run, Walk
}
public AnimationEnum Animation = AnimationEnum.Idle;
/// <summary>
/// 動畫持續時間
/// </summary>
private float animationDuration = 0;
/// <summary>
/// Animation型別
/// </summary>
public int AnimationType { get; set; }
public void PlayAnimation()
{
var animationStr = string.Format("{0}_{1}", AnimationType, Animation);
printHelper.Debug($"播放動畫,{animationStr}");
animationPlayer.Play(animationStr);
}
public void SetAnimation()
{
//如果檢測到玩家,就直接跑起來
if (PlayerCheck.IsColliding())
{
Animation = AnimationEnum.Run;
animationDuration = 0;
}
switch (Animation)
{
//如果站立時間大於2秒,則開始散步
case AnimationEnum.Idle:
if (animationDuration > 2)
{
Animation = AnimationEnum.Walk;
animationDuration = 0;
}
break;
//如果檢測到牆或者沒檢測到地面或者動畫時間超過4秒,則開始walk
case AnimationEnum.Walk:
if (WallCheck.IsColliding() || !FloorCheck.IsColliding() || animationDuration > 4)
{
Animation = AnimationEnum.Idle;
animationDuration = 0;
}
break;
//跑動不會立刻停下,當持續時間大於2秒後站立發呆
case AnimationEnum.Run:
if(animationDuration > 2)
{
Animation = AnimationEnum.Idle;
animationDuration = 0;
}
break;
}
}
建議所有的線性計算都用Mathf.MoveToward,如果直接用 time += delta容易造成溢位的Bug。
完整程式碼
using Godot;
using GodotNet_LegendOfPaladin2.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GodotNet_LegendOfPaladin2.SceneModels
{
public class EnemySceneModel : ISceneModel
{
private PrintHelper printHelper;
private CharacterBody2D characterBody2D;
private CollisionShape2D collisionShape2D;
private Sprite2D sprite2D;
private AnimationPlayer animationPlayer;
public RayCast2D WallCheck { get; private set; }
public RayCast2D FloorCheck { get; private set; }
public RayCast2D PlayerCheck { get; private set; }
public enum DirectionEnum
{
Left, Right
}
public DirectionEnum Direction { get; set; }
public enum AnimationEnum
{
Hit, Idle, Run, Walk
}
public AnimationEnum Animation = AnimationEnum.Idle;
/// <summary>
/// 動畫持續時間
/// </summary>
private float animationDuration = 0;
/// <summary>
/// 最大速度
/// </summary>
public int MaxSpeed { get; set; }
/// <summary>
/// 加速度
/// </summary>
public int AccelerationSpeed { get; set; }
/// <summary>
/// Animation型別
/// </summary>
public int AnimationType { get; set; }
public EnemySceneModel(PrintHelper printHelper)
{
this.printHelper = printHelper;
printHelper.SetTitle(nameof(EnemySceneModel));
}
public EnemySceneModel() { }
public override void Process(double delta)
{
animationDuration = (float)Mathf.MoveToward(animationDuration, 99, delta);
SetAnimation();
}
public override void Ready()
{
characterBody2D = Scene.GetNode<CharacterBody2D>("CharacterBody2D");
collisionShape2D = characterBody2D.GetNode<CollisionShape2D>("CollisionShape2D");
sprite2D = characterBody2D.GetNode<Sprite2D>("Sprite2D");
animationPlayer = characterBody2D.GetNode<AnimationPlayer>("AnimationPlayer");
WallCheck = Scene.GetNode<RayCast2D>("CharacterBody2D/RayCast/WallCheck");
FloorCheck = Scene.GetNode<RayCast2D>("CharacterBody2D/RayCast/FloorCheck");
PlayerCheck = Scene.GetNode<RayCast2D>("CharacterBody2D/RayCast/PlayerCheck");
PlayAnimation();
printHelper.Debug("載入成功!");
printHelper.Debug($"當前朝向是:{Direction}");
}
public void PlayAnimation()
{
var animationStr = string.Format("{0}_{1}", AnimationType, Animation);
//printHelper.Debug($"播放動畫,{animationStr}");
animationPlayer.Play(animationStr);
}
public void SetAnimation()
{
//如果檢測到玩家,就直接跑起來
if (PlayerCheck.IsColliding())
{
//printHelper.Debug("檢測到玩家,開始奔跑");
Animation = AnimationEnum.Run;
animationDuration = 0;
}
switch (Animation)
{
//如果站立時間大於2秒,則開始散步
case AnimationEnum.Idle:
if (animationDuration > 2)
{
printHelper.Debug("站立時間過長,開始移動");
Animation = AnimationEnum.Walk;
animationDuration = 0;
}
break;
//如果檢測到牆或者沒檢測到地面或者動畫時間超過4秒,則開始walk
case AnimationEnum.Walk:
if (WallCheck.IsColliding() || !FloorCheck.IsColliding() || animationDuration > 4)
{
Animation = AnimationEnum.Idle;
animationDuration = 0;
printHelper.Debug("開始閒置");
}
break;
//跑動不會立刻停下,當持續時間大於2秒後站立發呆
case AnimationEnum.Run:
if(animationDuration > 2)
{
printHelper.Debug("追逐時間到達上限,停止");
Animation = AnimationEnum.Idle;
animationDuration = 0;
}
break;
}
PlayAnimation();
}
}
}
狀態機的朝向問題和載入問題
using Bogus;
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 EnemySceneModel : ISceneModel
{
private PrintHelper printHelper;
private CharacterBody2D characterBody2D;
private CollisionShape2D collisionShape2D;
private Sprite2D sprite2D;
private AnimationPlayer animationPlayer;
public RayCast2D WallCheck { get; private set; }
public RayCast2D FloorCheck { get; private set; }
public RayCast2D PlayerCheck { get; private set; }
public enum DirectionEnum
{
Left = -1, Right = 1
}
//設定正向的方向
private DirectionEnum direction = DirectionEnum.Right;
public DirectionEnum Direction
{
get => direction;
//這個是一個生命週期的問題,屬性的設定比樹節點的載入更早
//,所以我們會在Ready裡面使用Direction = Direction來觸發get函式
set
{
if (characterBody2D != null && direction != value)
{
printHelper.Debug($"設定朝向,{value}");
var scale = characterBody2D.Scale;
//注意反轉是X=-1。比如你左反轉到右是X=-1,你右又反轉到左也是X=-1。不是X=-1就是左,X=1就是右。
scale.X = -1;
characterBody2D.Scale = scale;
direction = value;
}
}
}
public enum AnimationEnum
{
Hit, Idle, Run, Walk
}
public AnimationEnum Animation = AnimationEnum.Idle;
/// <summary>
/// 動畫持續時間
/// </summary>
private float animationDuration = 0;
/// <summary>
/// 最大速度
/// </summary>
public int MaxSpeed { get; set; }
/// <summary>
/// 加速度
/// </summary>
public int AccelerationSpeed { get; set; }
/// <summary>
/// Animation型別
/// </summary>
public int AnimationType { get; set; }
public EnemySceneModel(PrintHelper printHelper)
{
this.printHelper = printHelper;
printHelper.SetTitle(nameof(EnemySceneModel));
}
public EnemySceneModel() { }
public override void Process(double delta)
{
animationDuration = (float)Mathf.MoveToward(animationDuration, 99, delta);
SetAnimation();
Move(delta);
Direction = Direction;
}
public override void Ready()
{
characterBody2D = Scene.GetNode<CharacterBody2D>("CharacterBody2D");
collisionShape2D = characterBody2D.GetNode<CollisionShape2D>("CollisionShape2D");
sprite2D = characterBody2D.GetNode<Sprite2D>("Sprite2D");
animationPlayer = characterBody2D.GetNode<AnimationPlayer>("AnimationPlayer");
WallCheck = Scene.GetNode<RayCast2D>("CharacterBody2D/RayCast/WallCheck");
FloorCheck = Scene.GetNode<RayCast2D>("CharacterBody2D/RayCast/FloorCheck");
PlayerCheck = Scene.GetNode<RayCast2D>("CharacterBody2D/RayCast/PlayerCheck");
PlayAnimation();
printHelper.Debug("載入成功!");
printHelper.Debug($"當前朝向是:{Direction}");
Direction = Direction;
}
#region 動畫狀態機
public void PlayAnimation()
{
var animationStr = string.Format("{0}_{1}", AnimationType, Animation);
//printHelper.Debug($"播放動畫,{animationStr}");
animationPlayer.Play(animationStr);
}
public void SetAnimation()
{
//如果檢測到玩家,就直接跑起來
if (PlayerCheck.IsColliding())
{
//printHelper.Debug("檢測到玩家,開始奔跑");
Animation = AnimationEnum.Run;
animationDuration = 0;
}
switch (Animation)
{
//如果站立時間大於2秒,則開始散步
case AnimationEnum.Idle:
if (animationDuration > 2)
{
printHelper.Debug("站立時間過長,開始移動");
Animation = AnimationEnum.Walk;
animationDuration = 0;
//如果撞牆,則反轉
if (WallCheck.IsColliding() || !FloorCheck.IsColliding())
{
if(Direction == DirectionEnum.Left)
{
Direction = DirectionEnum.Right;
}
else
{
Direction = DirectionEnum.Left;
}
}
//Direction = Direction;
}
break;
//如果檢測到牆或者沒檢測到地面或者動畫時間超過4秒,則開始walk
case AnimationEnum.Walk:
if ((WallCheck.IsColliding() || !FloorCheck.IsColliding()) || animationDuration > 4)
{
Animation = AnimationEnum.Idle;
animationDuration = 0;
printHelper.Debug("開始閒置");
}
break;
//跑動不會立刻停下,當持續時間大於2秒後站立發呆
case AnimationEnum.Run:
if (animationDuration > 2)
{
printHelper.Debug("追逐時間到達上限,停止");
Animation = AnimationEnum.Idle;
animationDuration = 0;
}
break;
}
PlayAnimation();
}
#endregion
#region 物體移動
public void Move(double delta)
{
var velocity = characterBody2D.Velocity;
velocity.Y += ProjectSettingHelper.Gravity * (float)delta;
switch (Animation)
{
case AnimationEnum.Idle:
velocity.X = 0;
break;
case AnimationEnum.Walk:
velocity.X = MaxSpeed / 3;
break;
case AnimationEnum.Run:
velocity.X = MaxSpeed;
break;
}
velocity.X = velocity.X * (int)Direction;
characterBody2D.Velocity = velocity;
//printHelper.Debug(JsonConvert.SerializeObject(characterBody2D.Velocity));
characterBody2D.MoveAndSlide();
}
#endregion
}
}
總結
這次解決了簡單的敵人載入的問題,我暫時不瞭解這個場景繼承有什麼用。可能是我的IOC寫的太麻煩了,如果真繼承還是挺麻煩的。後面寫多了看看怎麼解決。我是打算用TypeID來解決同類敵人的問題的。