Godot Breakeys Godot Beginner Tutorial 遊戲開發筆記

gclove2000發表於2024-05-20

目錄
  • 前言
  • 資源下載
  • 新增人物節點
    • 運動狀態機
  • 移動平臺
    • 單向穿過
    • 奇怪的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();

        }
    }
}

多執行緒安全

執行緒安全這裡就不展開說明了,我們目前暫時還沒接觸到大量的數學計算。

TileMap處理

TileMap分層

相關文章