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();

        }
    }
}

多线程安全

线程安全这里就不展开说明了,我们目前暂时还没接触到大量的数学计算。

TileMap处理

TileMap分层

热门相关:龙潭大鳄   省省吧!我家富贵发   逆天奇案2粤语   我竟是狂龙   最狂神医赘婿