- 前言
- 资源下载
- 添加人物节点
- 运动状态机
- 移动平台
- 单向穿过
- 奇怪的Bug
- Area2D
- BodyEntered
- 死亡区域
- 全局类
- 多线程安全
- TileMap处理
- TileMap分层
前言
这次来学习一下youtube的传奇Unity博主,Breakeys的Godot新手教程。Breakeys是从15岁左右就开始用unity做游戏并在youtube上面发布视频了。他已经在youtube上面发布了讲解450个视频,然后他累了,3年前发布了一个告别视频后离开了。因为前端时间的untiy收费事件,他又回来了。他并没有明确的批评Unity,但是他说游戏的未来应该是像Blender一样的开源社区,而且Godot的完成度远超他的想象。
基本的godot操作我们就不展开说明,我会对操作进行一些进阶的代码替换。会跳过很多步骤,详细的代码可以看我的github仓库:https://github.com/Gclove2000/Brackeys-Godot-Beginner-Tutorial-In-Dotnet
资源下载
Brackeys' Platformer Bundle:https://brackeysgames.itch.io/brackeys-platformer-bundle
添加人物节点
这里比较简单,我就跳过了
运动状态机
因为我之前写过状态机,我这里就直接写代码了。
using Godot;
using GodotGame.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace GodotGame.SceneModels
{public class PlayerSceneModel : ISceneModel{private PrintHelper printHelper;private CharacterBody2D characterBody2D;private Sprite2D sprite2D;private AnimationPlayer animationPlayer;private CollisionShape2D collisionShape2D;public const int SPEED = 300;public const int JUMP_VELOCITY = -400;public enum AnimationEnum { Idel,Run,Roll,Hit,Death}private AnimationEnum animationState = AnimationEnum.Idel;public AnimationEnum AnimationState{get => animationState;set{if(animationState != value){printHelper?.Debug($"[{animationState}] => [{value}]");animationState = value;}}}private bool isFlip = false;public bool IsFlip{get => isFlip;set{if(isFlip != value){var postion = characterBody2D.Scale;postion.X = -1;characterBody2D.Scale = postion;isFlip = value;}}}public PlayerSceneModel(PrintHelper printHelper){this.printHelper = printHelper;printHelper.SetTitle(nameof(PlayerSceneModel));}public override void Ready(){characterBody2D = Scene.GetNode<CharacterBody2D>("CharacterBody2D");sprite2D = Scene.GetNode<Sprite2D>("CharacterBody2D/Sprite2D");animationPlayer = Scene.GetNode<AnimationPlayer>("CharacterBody2D/AnimationPlayer");collisionShape2D = Scene.GetNode<CollisionShape2D>("CharacterBody2D/CollisionShape2D");printHelper.Debug("加载成功!");}public override void Process(double delta){Move(delta);Play();SetAnimation();}private void SetAnimation(){if (!characterBody2D.IsOnFloor()){AnimationState = AnimationEnum.Roll;}switch (AnimationState){case AnimationEnum.Idel:if (!Mathf.IsZeroApprox(characterBody2D.Velocity.X)){AnimationState = AnimationEnum.Run;}break;case AnimationEnum.Run:if (Mathf.IsZeroApprox(characterBody2D.Velocity.X)){AnimationState = AnimationEnum.Idel;}break;case AnimationEnum.Hit:break;case AnimationEnum.Death:break;case AnimationEnum.Roll:if (characterBody2D.IsOnFloor()){AnimationState = AnimationEnum.Idel;}break;}if (!characterBody2D.IsOnFloor()){//printHelper.Debug("跳跃");AnimationState = AnimationEnum.Roll;}}private void Move(double delta){var move = new Vector2(0,0);move = characterBody2D.Velocity;move.Y += (float)(MyGodotSetting.GRAVITY * delta);if (MyGodotSetting.IsActionJustPressed(MyGodotSetting.InputMapEnum.Jump) && characterBody2D.IsOnFloor()){printHelper.Debug("跳跃");move.Y = JUMP_VELOCITY;}var direction = Input.GetAxis(MyGodotSetting.InputMapEnum.Left.ToString(), MyGodotSetting.InputMapEnum.Right.ToString());if(Mathf.IsZeroApprox(direction)){move.X = (float)Mathf.MoveToward(move.X, 0, delta*SPEED);}else{move.X = (float)Mathf.MoveToward(move.X, direction*SPEED, delta * SPEED);IsFlip = direction < 0;}characterBody2D.Velocity = move;characterBody2D.MoveAndSlide();}private void Play(){animationPlayer.Play(AnimationState.ToString());}}
}
移动平台
StaticBody2D和他的子节点都适合用于制作不会移动的节点
单向穿过
如果我们想要一个单向的碰撞体,就可以打开 One Way Collision 这个按钮
奇怪的Bug
如果我们使用Node作为根节点来进行移动,就会导致整个碰撞层的错误,这里我不知道为什么
Area2D
Area2D一般用于制作简单的无碰撞的物体
BodyEntered
之前我说过,在C# 中,不适用信号而改用委托事件的方式,能在C# 内部解决的,就尽量不调用Godot的API。
using Godot;
using GodotGame.SceneScripts;
using GodotGame.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace GodotGame.SceneModels
{public class CoinSceneModel : ISceneModel{private PrintHelper printHelper;private Area2D area2D;private Sprite2D sprite2D;private AnimationPlayer animationPlayer;private CollisionShape2D collisionShape2D;public CoinSceneModel(PrintHelper printHelper) {this.printHelper = printHelper;this.printHelper.SetTitle(nameof(CoinSceneModel));}public override void Process(double delta){}public override void Ready(){area2D = Scene.GetNode<Area2D>("Area2D");sprite2D = Scene.GetNode<Sprite2D>("Area2D/Sprite2D");animationPlayer = Scene.GetNode<AnimationPlayer>("Area2D/AnimationPlayer");collisionShape2D = Scene.GetNode<CollisionShape2D>("Area2D/CollisionShape2D");printHelper.Debug("加载完成");area2D.BodyEntered += Area2D_BodyEntered;}private void Area2D_BodyEntered(Node2D body){printHelper.Debug("有东西进入");if (body is PlayerScene){printHelper.Debug("玩家进入");}if(body.GetParent() is PlayerScene){printHelper.Debug("父节点是玩家的进入");}}}
}
这里的碰撞检测就用到了Godot的一个特性了,如果你使用了继承的脚本重载了节点,这样相当于你新建了一个类型。比如Node2D节点挂载了一个继承Node2D的 PlayerScene,这样Godot就认为你是PlayerScene这个节点,这样方便我们对各种碰撞事件的对象进行判断
但是要注意的是,碰撞的对象只是Player的Area节点,所以还要去找他的父节点才可以找到对应的脚本类型
当然,我们最好也设置一下物理层,这样防止出现额外的碰撞事件。
死亡区域
全局类
这里我们就用全局类来进行代替
using Godot;
using GodotGame.SceneScripts;
using GodotGame.Utils;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace GodotGame.Modules
{[GlobalClass]public partial class DeathArea :Area2D{private PrintHelper printHelper;public DeathArea() {this.printHelper = Program.Services.GetService<PrintHelper>();this.printHelper.SetTitle(nameof(DeathArea));this.BodyEntered += DeathArea_BodyEntered;}private void DeathArea_BodyEntered(Node2D body){printHelper.Debug("Anythiny enter!");//如果玩家进入,则等待0.6秒后重新加载if (body.GetParent() is PlayerScene){printHelper.Debug("You Get Die");Reload();}}/// <summary>/// 为了线程安全,我们只能这么做/// </summary>/// <returns></returns>private async Task Reload(){await Task.Delay(600);GetTree().ReloadCurrentScene();}}
}
多线程安全
线程安全这里就不展开说明了,我们目前暂时还没接触到大量的数学计算。