Godot.NET C#IOC重構(8):敵人野豬

gclove2000發表於2024-05-01

目錄
  • 前言
  • 場景繼承
    • 在SceneModel裡面新增基礎的節點獲取
      • EnemyScene.cs
      • EnemySceneModel.cs
      • Godot Export屬性和Enum
      • Export預設值問題
        • 修改前
          • EnemySceneModel.cs
          • EnemyScene.cs
        • 修改後
          • EnemyScene.cs
          • EnemySceneModel.cs
    • 但是有個問題,有必要這麼寫嗎?
  • 匯入野豬圖片
    • 圖片拼接
  • 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來解決同類敵人的問題的。

相關文章