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 射线碰撞检测
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来解决同类敌人的问题的。