Godot.NET C#IOC重构(8):敌人野猪

前言

这个实在是拖了太久了,这次速战速决

场景继承

由于我们是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来解决同类敌人的问题的。

热门相关:龙皇缠身:爱妃,来生蛋!   史上第一密探   重生童养媳:枭宠不乖娇妻   全能王妃:偷个王爷生宝宝   人族镇守使