Godot Breakeys Godot Beginner Tutorial 游戏开发笔记
前言
这次来学习一下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();
}
}
}
多线程安全
线程安全这里就不展开说明了,我们目前暂时还没接触到大量的数学计算。