Skip to main content

godot 游戏引擎

安装环境

https://godotengine.org/

本次学习采用的是最新版 4.4.1 使用语言为: csharp+GDS

.net 版本为 8

代码编辑器为 vs code

vsc 插件如下

image-20250425185342898

设置 godot 脚本编辑器的启动项

image-20250426142755644

Godot学习

认识界面

启动 godot 创建一个基础的项目

image-20250426142723774

场景和节点

image-20250426142839532

理解游戏思路其实我们开发游戏本质就是 创建无数个场景和节点 进行有趣的交互

image-20250426142941180

godot 中我们的节点以树来称呼, 可以称为节点树 思路与日常的代码开发大致相同, 基于继承

我们的场景其实就是节点树 由无数的节点组成为一个场景

节点中的继承关系

功能类是无法作为节点的 它为我们提供一些好用的功能

我们实际创造的是 node :

image-20250426143431436

image-20250426143332947

脚本

通常我们使用选择 node 并且在 node 的详细页面来创建脚本

当然我们也可以直接在文本区创建脚本然后和 node 绑定

image-20250426143733934

生命周期方法

脚本从生效到销毁的过程中 会有一些比较重要的方法

entertree

节点添加到节点树的时候会调用这个方法 可以用于初始化

正序创建如果有多个节点的话 按照加入节点树的顺序调用

image-20250426144747025

  public override void _EnterTree()
{
base._EnterTree();
}

Ready

在节点加载完成的时候触发, 类似于递归的调用 从最底部的节点逐步回调

初始化推荐在 ready 中使用

image-20250426144850148

public override void _Ready()
{
}

_ExitTree

退出节点树(销毁) 一般这里处理节点销毁的逻辑

   public override void _ExitTree()
{
base._ExitTree();
}

_Process

每一帧都会调用一次这个方法 回传进来帧延时,

可以用来写及时逻辑

public override void _Process(double delta)
{
}

_PhysicsProcess

每一次和物理有关的计算会调用 , 比如起跳 落地等

  public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
}

启动场景

image-20250426145421585

这里的第一个 icon 是启动主场景 ,第二个是当前场景

主场景设置:

image-20250426145500657

我们在方法中加入输出语句,

然后启动场景就可以很清晰的看到生命周期的调用了

   // 1. 节点添加到节点树的时候会调用这个方法 可以用于初始化 正序创建如果有多个节点的话
public override void _EnterTree()
{
base._EnterTree();
GD.Print("_EnterTree");
}

//2. 这个是节点加载完成的时候会触发的方法 也可以用于初始化(推荐)
public override void _Ready()
{
GD.Print("_Ready");
}

//
public override void _Process(double delta)
{
GD.Print("_Process");
}

输出结果

image-20250426145712040

关于销毁

using Godot;
using System;

public partial class MySprite : Sprite2D
{

float timer = 5;
// 1. 节点添加到节点树的时候会调用这个方法 可以用于初始化 正序创建如果有多个节点的话
public override void _EnterTree()
{
base._EnterTree();
GD.Print("_EnterTree");
}

//2. 这个是节点加载完成的时候会触发的方法 也可以用于初始化(推荐)
public override void _Ready()
{
GD.Print("_Ready");
}

//
public override void _Process(double delta)
{
// GD.Print("_Process");
timer -= (float)delta;
// timer归0 销毁节点
if (timer<=0)
{
timer = 100;
this.QueueFree();
}
}


public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
}

public override void _ExitTree()
{
base._ExitTree();
GD.Print("_ExitTree");
}

}

image-20250426160438688

输入

键盘事件

这里我们要用到的是 godot 中的Input对象 来获取 player 的设备输入

比如我们来监听一下 键盘上的按键

using Godot;

public partial class InputTest : Node
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsKeyPressed(Key.B)) GD.Print("按下了b");
}
}

image-20250426163614294

你看我们每一帧都会出触发一次按键 b 但是事实上我们只需要一次 或者是按下的时候去使用

这里就有一个新的方法, _input(inputEvent)

    public override void _Input(InputEvent @event)
{
base._Input(@event);
if (@event is InputEventKey key)
{
if (key.Keycode == Key.V)
{
GD.Print("检测到按下 v 键位");
}
}
}

这里我们再次启动项目 点按 v 键

image-20250426164232684

更多关于 keyinputevent 的检测

   public override void _Input(InputEvent @event)
{
base._Input(@event);
if (@event is InputEventKey key)
{
if (key.Keycode == Key.V)
{
// 判断当前是否持续按压
if (key.IsEcho())
{
GD.Print("持续按压v");
}
// 判断当前是否是按下瞬间
else if (key.IsPressed())
{
GD.Print("按下v");
}
// 判断当前是否是抬起瞬间
else if (key.IsReleased())
{
GD.Print("松开v");
}
}
}
}

image-20250426165305525

鼠标事件

同样的我们也可以照猫画虎来检测鼠标事件

 if(@event is InputEventMouse mouse){
if(mouse.IsEcho()){
GD.Print("长按鼠标");
}else if(mouse.IsPressed()){
GD.Print("按下鼠标");
GD.Print("鼠标位置"+mouse.Position);
GD.Print("按下的鼠标按键"+mouse.ButtonMask);
}else if(mouse.IsReleased()){
GD.Print("松开鼠标");
}
}

效果

image-20250426172237829

输入映射(常用)

我们还有第二种方式 来将按键和虚拟的按键进行绑定

image-20250426172355873

比如跳跃按键

image-20250426172455237

这样就绑定好了, 之前我们倾向于基于event来判断 ,因为在帧生成中判断会有问题, 而虚拟按键中我们就可以使用帧生成方法来判断了

   public override void _Process(double delta)
{
// 判断虚拟按键
if (Input.IsActionJustPressed("跳跃"))
{
GD.Print("按下跳跃");
}

if (Input.IsActionPressed("跳跃"))
{
GD.Print("跳跃中");
}

if (Input.IsActionJustReleased("跳跃"))
{
GD.Print("松开跳跃");
}
}

效果

image-20250426174210989

虚拟按键不止可以监听这种状态 还有特殊的方法可以帮助我们监听,

相当于是一个状态轴

  • 按下 1
  • 松开 0

image-20250426181109892

使用Input.GetActionStrength方法 拿到按下的状态值

    float actionStrength = Input.GetActionStrength("跳跃");
GD.Print(actionStrength);

image-20250426174723526

我们可以发现这是一个 0-1 的轴 只有两种状态能触发 实际上 godot 还给我们准备了有 -1 0 1 的三种状态的情况

左右两个按键代表了-1 和 1 如果都没有按则为 0

很经典的就是我们的向左移动 or 向右移动

添加新的虚拟映射

image-20250426181116740

image-20250426175052191

   float axis = Input.GetAxis("left", "right");
GD.Print("往那边走了 " + axis);

我们通过GetAxis方法来设置这个状态轴 这个时候控制台就可以输出了

image-20250426181009151

通常的游戏开发中我们需要获得上下左右四个方向的位置 godot 也提供了方式

image-20250426181209522

image-20250426181806412

设置好按键上下 就可以去监听了 方法参数的顺序为 左右 上下

  public override void _Process(double delta)
{
Vector2 vector2 = Input.GetVector("left", "right", "up", "down");
GD.Print(vector2);
}

由于一直在输出 效果很难截图完整 只需要正确的设置好映射就直接可以展示的

节点(重要)

我们知道节点都是在节点树上的 他一定是有继承关系的

那么我们该如何获取到父节点 子节点 然后操作呢 比如父子通信之类的

比如我们创建一个新的节点用于学习父子关系 同时创建一个测试节点

image-20250426185734458

sonNode添加脚本然后放入内容 按下左键就释放 sontest 节点

using Godot;
using System;

/*
* 用于学习 节点的父子关系
*
*/
public partial class sonNode : Node
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
// 按下键盘左键拿到节点
if (Input.IsActionJustPressed("left"))
{
// 拿到当前场景的根节点
Node root = this.GetTree().CurrentScene;
Node findChild = root.FindChild("sontest");
findChild.QueueFree();
}
}
}

项目启动后 点击远程

image-20250426185845253

然后按下 a 键 查看是否会让 sontest 消失

image-20250426190015678

可以看到 sontest 也是成功释放了 这是最基础的对节点的操作

变为子节点

接下来我们来尝试将同级节点变为子节点

  public override void _Process(double delta)
{
// 按下键盘左键拿到节点
if (Input.IsActionJustPressed("left"))
{
// 拿到当前场景的根节点
Node root = this.GetTree().CurrentScene;
Node findChild = root.FindChild("sontest");
// findChild.QueueFree();
root.RemoveChild(findChild);
// // 将test加入到当前节点中
this.AddChild(findChild);
}
}

现在的节点结构

image-20250426190228386

按下左键后

这里我们将 node2d 的节点网上挪一个单位, 方便操作

image-20250426190815931

创建节点

接下来我们设置按下右键来创造节点

 public override void _Process(double delta)
{
// 按下键盘左键拿到节点
if (Input.IsActionJustPressed("left"))
{
// 拿到当前场景的根节点
Node root = this.GetTree().CurrentScene;
Node findChild = root.FindChild("sontest");
// findChild.QueueFree();
root.RemoveChild(findChild);
// // 将test加入到当前节点中
this.AddChild(findChild);
}

if (Input.IsActionJustPressed("right"))
{
//获取节点树
Node root = this.GetTree().CurrentScene;
//创建节点
Node2D node2D = new Node2D();
node2D.Name = "CreateNode";
//加入节点树
root.AddChild(node2D);
}
}

启动项目 然后按下键盘 d 键

image-20250426191237215

可以看到我们按一次 d 键就会在根目录下 创建一个节点

更快速的操作

多层级的节点操作 我们之前的代码中直接从 root 节点来往下获取, 其实我们可以使用getParent方法和泛型来快速获取到父节点 ,

image-20250426191839904

我们给 sontest3 加入一个脚本

using Godot;
using System;

public partial class sonTest3 : Node
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("up"))
{
// 可以使用getNode通过树路径快速的拿到节点
Node node = GetNode("/root/Node2D/inputTest");
GD.Print("当前拿到的节点" + node.Name);
}
}
}

这里我们按下 w 就会去获取到对应路径的 node 节点

效果

image-20250426192234716

节点的详细面板

image-20250429163016629

从上到下分别是

  • 控制显示
  • 显示层级
  • 纹理
  • 材质

node2d

image-20250429163207049

  • 坐标向量
  • 旋转角度
  • 缩放
  • 倾斜

节点常用属性

创建一个新的节点

image-20250429171738299

using Godot;
using System;

public partial class Sprite2Dupdate : Sprite2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
//canvas的常用属性
this.Visible = false;
//渲染顺序
this.ZIndex = 10;
this.ZAsRelative = false;
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
Vector2 globalMousePosition = GetGlobalMousePosition();
GD.Print("当前鼠标的位置" + globalMousePosition);
}
}

你会发现我们的设置的svg图标并没有展示 并且控制台实时的打印鼠标位置

Node2d属性调整

using Godot;
using System;

public partial class Sprite2Dupdate : Sprite2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
//canvas的常用属性
this.Visible = true;
//渲染顺序
this.ZIndex = 10;
this.ZAsRelative = false;

//node2d常用的属性
//位置
Position = new Vector2(500, 300);
//旋转
Rotation = 0.2f;
//缩放
Scale = new Vector2(2, 2);
//倾斜
Skew = 30;
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
Vector2 globalMousePosition = GetGlobalMousePosition();
GD.Print("当前鼠标的位置" + globalMousePosition);
}
}

变换后的节点

image-20250429172243033

设置看向某个点

    // Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
Vector2 globalMousePosition = GetGlobalMousePosition();
LookAt(globalMousePosition);
GD.Print("当前鼠标的位置" + globalMousePosition);
}

效果

image-20250429172421358

你就会发现 图标跟着你的鼠标位置不断的旋转方向

精灵节点

三个属性

  • 偏移量
  • 动画(一般动画不会使用这个属性)
  • 缩放

image-20250429172615900

代码设置属性

using Godot;
using System;

public partial class Sprite2Dupdate : Sprite2D
{
[Export] public Texture2D NewtTexture2D;

double timer = 0;

// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
//canvas的常用属性
this.Visible = true;
//渲染顺序
this.ZIndex = 10;
this.ZAsRelative = false;

//node2d常用的属性
//位置
Position = new Vector2(500, 300);
//旋转
Rotation = 0.1f;
//缩放
Scale = new Vector2(2, 2);
//倾斜
// Skew = 30;

//精灵节点的设置
//纹理
Texture = NewtTexture2D;
//中心点 or 左上角
Centered = false;
//偏移
Offset = new Vector2(0, 0);
//反转
//水平
FlipH = false;
//垂直
FlipV = false;
//动画
Hframes = 2;
Vframes = 2;
Frame = 0;
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
// Vector2 globalMousePosition = GetGlobalMousePosition();
// LookAt(globalMousePosition);
// GD.Print("当前鼠标的位置" + globalMousePosition);

// 计时器
timer += delta;
//每一秒更换一个图像
if (timer > 1)
{
//充值计时器
timer = 0;
int index = Frame + 1;
if (index > 3) index = 0;
Frame = index;
}
}
}

效果

image-20250429173510192

启动后你会发现你的图片被分成了四帧动画

节点分组管理

比如 有五个节点 一个player 四个boss 我们通过按键 销毁所有的boss 该如何去操作呢?

创建一个新的场景,并且设置为主场景

image-20250429173719870

我们添加五个精灵节点

image-20250429173849196

大多数的时候 我们的节点名都是动态创建的 那么我们该如何快速的获取同类型的节点呢 ==分组==

image-20250429174055163

创建分组将boss都加入到分组中 , 并且给player01创建一个脚本

using Godot;
using System;
using Godot.Collections;

public partial class Player01 : Sprite2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
//按下右键击杀全部敌人
if (Input.IsActionJustPressed("right"))
{
//获取到分组中的全部节点
var boss = GetTree().GetNodesInGroup("boss");
foreach (Node node in boss)
{
node.QueueFree();
}
}
}
}

启动以后按下右键全部销毁敌人

image-20250429174746806

调用分组节点方法

创建一个boss脚本加入到四个节点中,添加一个自定义方法

    public void test()
{
GD.Print("我是" + Name);
}

我现在需要按下 键盘a 也就是虚拟键盘left 触发所有boss的test方法

using Godot;
using System;
using Godot.Collections;

public partial class Player01 : Sprite2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
//按下右键击杀全部敌人
if (Input.IsActionJustPressed("right"))
{
//获取到分组中的全部节点
var boss = GetTree().GetNodesInGroup("boss");
foreach (Node node in boss)
{
GD.Print("销毁节点", node.Name);
node.QueueFree();
}
}

if (Input.IsActionJustPressed("left"))
{
var boss = GetTree().GetNodesInGroup("boss");
this.GetTree().CallGroup("boss","test");
}
}
}

效果

image-20250429175224516

节点通信(信号)

创建新的游戏场景NodeSignalDemo并且设置为主场景 新增文件夹 NodeSignal 之后我们有关这一小节的节点脚本都将放到这里

image-20250430125819471

静态链接

创建一个button节点并且创建一个button脚本绑定和一个精灵节点 用于学习和演示

点击详细面板的节点可以看到有很多很多信号可以选择 比如按钮的按下 释放 等等

image-20250430132358058

image-20250430130103188

在脚本中新增一个方法

    //按钮按下触发
public void buttonClick()
{
//找到精灵节点并删除
Node2D sp2d = GetNode<Node2D>("/root/NodeSignalDemo/Sprite2D");
sp2d.QueueFree();
}

之后去godot的信号窗口将方法绑定到按钮按下 这里选取可能会出现无法选择的方式 我们可以手动将方法名加入到绑定中

image-20250430131934367

动态链接

通过代码的方式将方法设置到信号中, 先手动的把释放信号断开连接改为用代码动态的连接

代码

using Godot;
using System;

public partial class mybutton : Godot.Button
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
Connect("pressed", new Callable(this, "buttonClick"));
}

//按钮按下触发
public void buttonClick()
{
//找到精灵节点并删除
Node2D sp2d = GetNode<Node2D>("/root/NodeSignalDemo/Sprite2D");
sp2d.QueueFree();
}
}

启动测试 就可以看到效果

自定义信号

基于 cs的委托机制 实现自定义信号 ,我们再创建2个节点

自定义信号的工作方式与内置信号相同:它们显示在“节点”选项卡中,你可以像连接其他信号一样连接到它们。

image-20250430133117676

给A绑定一个脚本 ,然后我们期望按下按键 A 就可以使用自定义的信号

MySignalEventHandler godot会将EventHandler 前的名称获取为信号的名称

using Godot;
using System;

public partial class A : Sprite2D
{
[Signal]
public delegate void MySignalEventHandler();

// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("left"))
{
//触发信号
EmitSignal("MySignal");
}
}
}

我们在A中创建一个委托 并且将信号暴露 这样我们在godot面板中就可以看到我们暴露的信号了 可以向设置系统信号一样设置这个型号

给节点 B 也创建一个脚本 编写一个方法 用于链接信号

using Godot;
using System;

public partial class B : Sprite2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}

public void showSignText()
{
GD.Print("接受到信号了");
}
}

确定链接信号

image-20250430135405599

启动该项目 就可以看到显示结果了

image-20250430135431038

场景

​ 从之前的示例可以知道设置好主场景, 游戏会从主场景开始运行, 现在我们要新创建一个场景,来学习一下场景之间的交互

新建场景在根目录

image-20250429152945472

跳转场景(路径跳转)

创建一个节点, 然后加入一个新的脚本 我们将在这个基点展示场景之间的跳转

  • 我们将game2设置为主场景
  • 然后编写inputgame2脚本
using Godot;
using System;

public partial class InputGame2 : Node2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("left"))
{
// 拿到场景树
SceneTree st = GetTree();
//跳转场景
st.ChangeSceneToFile("res://node.tscn");
}
}
}

image-20250429153255710

node场景 放入一个默认的图片

image-20250429153907938

然后启动项目点击A 就可以查看跳转结果

image-20250429153944990

按下A

image-20250429153958812

可以看到场景成功跳转了

场景跳转(属性跳转)

我们可以使用Attribute的方式将变量导出到godot里进行选择,只需要将节点拖进去就可以了

using Godot;
using System;

public partial class InputGame2 : Node2D
{
[Export] public PackedScene newScene;

// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("left"))
{
// 拿到场景树
SceneTree st = GetTree();
//跳转场景
// st.ChangeSceneToFile("res://node.tscn");
st.ChangeSceneToPacked(newScene);
}
}
}

操作

image-20250429154528548

效果和之前的路径跳转一样, 好处是可以使用变量来进行跳转 对比纯路径更加灵活

实例化场景 (预设体)

例子

比如弹匣里的子弹 我们主场景需要子弹 我们可以加载出子弹的节点 然后根据需要的数量来实例化场景节点并且获取到所需数量的对象

比如node场景的精灵节点 我们需要按下right键位让game2(主场景)拿到这个节点

using Godot;
using System;

public partial class InputGame2 : Node2D
{
[Export] public PackedScene newScene;

// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("left"))
{
// 拿到场景树
SceneTree st = GetTree();
//跳转场景
// st.ChangeSceneToFile("res://node.tscn");
st.ChangeSceneToPacked(newScene);
}

if (Input.IsActionJustPressed("right"))
{
//实例化场景
Node initNode = newScene.Instantiate();
GetTree().CurrentScene.AddChild(initNode);
}
}
}

node

image-20250429155142792

此刻查看我们的运行场景

image-20250429155238063

按下右键 就能看到页面节点发生的变化, 就可以看到效果了 我们拿到了对应的节点

image-20250429155826859

保证设定脚本任意场景都存活

创建一个管理新脚本,我们需要他一直存活在游戏的运行中,

image-20250429160306192

将我们的脚本挂在到项目设置的自动加载中, 这样我们就获得了一个随意切换场景并且保持存活的单利对象

using Godot;
using System;


//用于对游戏做一些设定的管理
public partial class GameManager : Node
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
GD.Print("初始化一次");
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}
}

image-20250429160749345

效果

image-20250429160859217

可以看到就算我切换场景 也不会多次触发ready方法 证明单例是没问题的 , 并且保证代码的存活

向量

向量在开发游戏的道路中尤为基础和关键 我们的未来游戏的方向 瞄准 视线 等等都和向量的使用息息相关

大多数的游戏引擎都封装有对应的函数 这里主要简单复习一下

  • 标量 表示大小的量 如 88 99 100
  • 向量 包含大小 也有方向 比如 前方 100m
  • 向量的模: 只要大小 舍去方向
  • 单位向量: 大小为1的向量
  • 单位化: 把向量转换为单位向量的过程

向量的表示

image-20250429161138386

向量的计算

向量的加法

向量的加法计算思路很简单, 两个向量的起点链接 遵循起点做一个对角线就是结果 形状类似平行四边形

image-20250429161427125

向量的减法

两个向量相减, 比如 a -b 获得的其实就是 从b指向a的向量 反之亦然

image-20250429161800346

向量的点乘

得到两个向量的夹角

计算思路大致就是 $$ \vec{A} \cdot \vec{B} = x_1x_2 + y_1y_2 = n = |\vec{A}||\vec{B}|\cos\theta = \cos\theta $$

总结

到这里godot官方文档的渐进式内容就结束了 这里视频观看的是 bilibili Gamer飞羽的视频配合 c# 菜鸟文档+godot官方文档 接下来是其他内容的笔记

节点动画

使用的是 gamer飞羽公众号提供的2d资源包 地址: https://pan.quark.cn/s/c4723f7548b0

认识动画与选项卡配置动画

新建项目 GameDemo1 并且将资源包加入其中 , 创建一个基础场景 并且创建一个精灵节点

image-20250430140200312

image-20250430140626369

我们将frame设置为8 因为这个攻击的素材有八帧

image-20250430140706378

我们之前的教程有说过 并不会直接的在精灵节点中做动画相关的操作 这里我们选择 动画精灵节点

image-20250430140810545

在选项卡里单击就可以创建动画了

image-20250430140842418

我们可以自行的修改状态 比如动画的名称 帧数 倍速等

image-20250430140912973

控制台的按钮都有悬浮的介绍 不过多的介绍,当我们资源有很多帧的时候 我们可以选择从精灵表中加入动画资源

image-20250430141008811

可以很明显的看到 我们的这个资源是水平 8 帧 垂直 1帧 设置后之后 可以通过选择帧数来创造动画

image-20250430141246429

这里选择资源中的dle动画 设置好帧数之后 作为站立 循环5帧播放 设置好点击播放 就可以看到我们的小骷髅人动了

image-20250430141510369

其他的动画也按照 idle的思路创建

使用AnimationPlayer 来使用动画 这个动画节点支持我们使用动画轨道

image-20250430142052840

image-20250430142120270

给精灵节点添加一个纹理

image-20250430142221438

然后创建动画轨道 我们可以通过轨道来播放

选择节点之后 我们会发现和之前的动画节点是不一样的 这里给我们提供了轨道的选项

新建一个 idle动画

image-20250430142437814

image-20250430142508983

我们可以通过添加轨道来选择节点并且设置关键帧 添加属性轨道

image-20250430142540672

我们可以通过右键 打下关键帧 并且在不同关键帧使用不同的纹理 就可以看到变化 我们可以设置选中节点的所有属性加入到这个轨道中 很有意思

image-20250430142743489

2D光照

全局光照

godot给我们2d也提供了具备光照的节点 这是全局性的光照

image-20250430143214481

效果

image-20250430143251626

点光源

PointLight2D 是点光源 我们给他加入一个纹理让他有体积 方便查看效果 可以看到在点光源的范围中的小骷髅会有被光照的感觉

image-20250430143347159

我们可以通过设置精灵的灯光遮罩层数 来选择哪个节点可以被光照

image-20250430143514880

需要被光照的通道必须匹配才会被这个光源照到

image-20250430143644352

阴影效果

首先我们要能被光照到才能有阴影 这里我们创建一个遮罩节点

image-20250430143631772

创建遮罩体积image-20250430143737592

在光源这里开启支持阴影,之后就可以看出来我们的阴影效果了

image-20250430143827166

同样的我们障碍物和阴影的层数也要相同才可以展示

image-20250430144143541

音频

我们可以通过audioStreamPlayer2D节点来播放音频

image-20250430190853595

我们可以在配置面板对音频进行一些设置 也可以在资源栏右键音频对音频进行一些设置

image-20250430191038831

用代码来配置音频

创建一个精灵节点 加上一个脚本 然后我们希望在创建的时候播放音频

public partial class audioNode : Sprite2D
{
private AudioStreamPlayer audioPlayer;
//我们希望初始化这个节点的时候 就播放音乐
public override void _Ready()
{
audioPlayer = new AudioStreamPlayer();
this.AddChild(audioPlayer);
//加载音频
AudioStream audioStream = GD.Load<AudioStream>("res://2dres/bgm/bg.ogg");
audioPlayer.Stream = audioStream;
audioPlayer.Play();
}
}

当然我们也可以通过 [EXPORT]的方式直接在godot配置版加入音频

碰撞

在游戏中我们经常的会需要去处理碰撞 这里我们来学习godot 开发游戏中最基础的碰撞

基础碰撞

我们先创建两个节点用于演示

image-20250430193702413

我们通过使用 area2d节点来为精灵节点设置碰撞区域, 接着去创建描述碰撞区域的子节点 collisionShape2d

image-20250430193800643

之后再配置卡中选择创建碰撞区域类型就完成碰撞区域的建立了

image-20250430193936573

之后给敌人节点也配置好碰撞区域 打开area2d的配置卡信号区域 我们可以看到两个最常用的信号 用于显示碰撞和离开

image-20250430194056569

我们为player添加一个脚本并且将脚本的关联方法加入到信号绑定, 这个信号会传给我们一个参数 表示在什么区域发生了碰撞

	public void onAreaEnter(Area2D area2D)
{
GD.Print("发生碰撞了"+area2D);
}

image-20250430194817211

射线碰撞

射线碰撞需要添加一个叫 rayCast2d的节点来实现 还是沿用我们的两个节点, 可以看到这个线就是射线碰撞的范围了

这两个配置项就是是否开启碰撞和是否开启和父节点的碰撞

image-20250430195028956

碰撞类型

,默认情况下只检测物理碰撞 勾选 areas开启2d区域的检测

image-20250430195220648

检测碰撞

我们需要添加脚本 来为射线检测提供支持, 这里我们采用触发物理会调用的狗子方法 _PhysicsProcess方法来检测射线 物理帧每一次都执行的区间是固定的 我们的需求是按下键位 拿到当前这一次的检查结果

PS: 这里我预先绑定好了 两个虚拟按键 left和right 对应 A D 你也可以绑定自己需要的键位

using Godot;
using System;

public partial class RayCast2dTest : RayCast2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}

public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
//按下左键 判断射线检测
if (Input.IsActionJustPressed("left"))
{
if (IsColliding())
{
//拿到碰撞的区域
Area2D collider = GetCollider() as Area2D;
GD.Print("检测到了碰撞 "+collider.Name);
}
else
{
GD.Print("没检测到");
}
}
}
}

启动当前的场景去按下左键获取

image-20250430195925439

这里还有一个拓展的节点 ShapeCast2D这个节点可以选择射线的形状 使用的方式和射线节点非常相似

射线也有检测的层级

物理碰撞

这里我们使用 staticBody2D 节点作为地板 来学习物理碰撞 创建节点并且为他添加一个碰撞形状

我们可以为staticBody2D 加入材质 并且可以设置摩擦力

image-20250430200440035

之后我们用RigidBody2d节点 来让精灵节点拥有动态碰撞

image-20250430200648938

这个时候 你启动项目就可以看到 演示节点掉到地板上哈哈

小实现 物理的任务控制

我们之前添加了 虚拟按键 a d 接下来我们来实现一个基于水平轴体的人物移动 给我们的人物刚体添加一个脚本

using Godot;
using System;

public partial class RigidBody2dPlayerControl : RigidBody2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}

public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
//获取水平轴 用于左右移动
float h = Input.GetAxis("left", "right");
//设置速度
LinearVelocity = new Vector2(h * 100, LinearVelocity.Y);
}
}

此刻启动你的游戏 就可以发现你的人物可以移动了 但是是翻滚着移动的

我们需要锁定旋转

using Godot;
using System;

public partial class RigidBody2dPlayerControl : RigidBody2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{ //锁定旋转
LockRotation = true;
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{

}

public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
//获取水平轴 用于左右移动
float h = Input.GetAxis("left", "right");
//设置速度
LinearVelocity = new Vector2(h * 100, LinearVelocity.Y);
}
}

如果我们想实现跳跃 只需要绑定 space 然后修改Y轴就可以了 新增 up绑定空格 ,并且我们需要通过刚体的信号来建立碰撞检测的通知

鼠标悬浮就可以看到介绍 自行了解

image-20250430202137067

代码

using Godot;
using System;

public partial class RigidBody2dPlayerControl : RigidBody2D
{
private bool isGround ;
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
LockRotation = true;
//如果刚体需要碰撞检测需要设置这个选项
ContactMonitor = true;
//记录的次数 默认为0 不修改这个就算绑定了信号也不检测
MaxContactsReported = 1;
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{

}

public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
//获取水平轴 用于左右移动
float h = Input.GetAxis("left", "right");
//设置速度
LinearVelocity = new Vector2(h * 100, LinearVelocity.Y);
if (Input.IsActionJustPressed("up") && isGround ==true)
{
//跳跃
LinearVelocity = new Vector2(LinearVelocity.X,-200);
}


}
public void bodyEntered(Node body)
{
GD.Print("人物落地了");
isGround = true;
}
public void bodyExited(Node body)
{
GD.Print("人物起跳了");
isGround = false;
}
}

然后我们将碰撞和离开两个方法绑定到信号

image-20250430202422344

启动游戏 就可以获得一个简单的任务控制器了

image-20250430202609204

人物控制器

学了一大圈 godot其实给我们提供了专门用于角色的任务控制器 他也是刚体下的一个节点 这个节点的一切下落 移动等 需要我们来控制操作 比如你创建好碰撞和物理 但是你启动界面后 人物还是会在空中

image-20250430202723272

绑定好脚本之后 会发现内置的模版有很多的代码 这其实和我们上一小节实现的代码非常的相似

using Godot;
using System;

public partial class CharacterBody2dPlayer : CharacterBody2D
{
public const float Speed = 300.0f;
public const float JumpVelocity = -400.0f;

public override void _PhysicsProcess(double delta)
{
Vector2 velocity = Velocity;

// Add the gravity.
if (!IsOnFloor())
{
velocity += GetGravity() * (float)delta;
}

// Handle Jump.
if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}

// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
velocity.X = direction.X * Speed;
}
else
{
velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}

Velocity = velocity;
MoveAndSlide();
}
}

UI

godot的ui都基于control节点 我们可以来创建一个ui场景

小案例

比如我们想要将一个ui相对的放到屏幕的某个位置

我们创建一个新的场景 ui 选择用户界面 比如当前这个节点是我想要的小地图 我希望他不论多大的窗口都在右上角显示 就可以这样设置 其他的设置godot的显示的非常完全可以按需去选择

这个相对的锚点其实就是对于宽度和边缘的计算

image-20250430204209914

带纹理的方块

选项卡 提供了非常丰富的选项

image-20250430204621293

显示模式

第一个配置项为忽略模式

第二个配置项是纹理的展示程度

godot为我们提供很详细的中文描述所以不过多的赘述

image-20250430204653367

文字控件

image-20250430204856376

顾名思义 就是可以展示我们的文本了

我们所有对label的设置都支持导入导出 不需要重复设置

image-20250430205016473

效果

image-20250430205158947

按钮节点

我们在之前的节点说过了 带文档的按钮 其实按钮是常用也简单的控件

image-20250430205316267

常用的四个按钮 之后我们使用的时候会展示使用细节

进度条

image-20250501203215853

image-20250501203231310

我们可以用来做加载动画 或者血条

选项卡非常的清晰

  • 最大值
  • 最小值
  • 每次变化多少
  • 当前多少
  • 等等

image-20250501203322137

小实验 制作血条

我们为这个进度条节点 加入一个脚本 , 然后创建一个方法 用于绑定信号, 然后我们的小实验基于按键左右来操作进度条的值

image-20250501203446812

代码

using Godot;
using System;

public partial class ProgressBarTest : ProgressBar
{

public override void _Ready()
{
}

public override void _Process(double delta)
{
var h = Input.GetAxis("left","right");
Value += h*50*delta;
}

public void onValueChanged(float value)
{
GD.Print(value);
}
}

记得将onValueChanged绑定到信号中

效果

image-20250501211924004

之后启动项目 你就可以获得一个由 a d键位控制的进度条了 这里我使用的是虚拟键位 你可以直接使用 key.*的输入来完成

输入框

很多属性可以自己悬浮查看 这里我们主要看一下这个模式 这个是关于内容溢出的模式 none是单行滚动条 另一个选项是自动换行

image-20250501212201640

常用的信号

image-20250501212316733

容器

image-20250501212348068

我现在由好几个空间 我自己手动的去布局 会很麻烦 这个时候我们可以引入一个容器节点 来为我们自动的管理容器的位置模式

image-20250501212503808

我们创造一个水平容器

然后将节点加入到容器中

image-20250501212546532

我们会发现加入的新节点 都按照加入的顺序 水平的排序好位置了 这就是容器为我们做的事情

加入我们想要实现背包 我们该如何选择呢 ? godot给我们提供了格子容器

image-20250501212720213

设定好行数 节点就会自动的去排列

image-20250501212835333

那如果我们空间沾满了需要滚动条呢 godot也提供了对应的滚动条容器 加入就可以使用基本的功能了 很方便

image-20250501213023259

选项列表

类似于节点容器 我们可以在里面创建出所需要的item并且自带响应事件

image-20250501213200255

image-20250501213345395

我们可以通过选项卡的设置 来完成对item展示的控制 同时可以通过信号来接收到item触发的点击回调

比较重要的item信号

using Godot;
using System;

public partial class ItemListTest : ItemList
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
}

// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
}

public void ItemClickedFunc(int index,Vector2 pos,int mbi){
GD.Print("点击触发"+index);
}
}

image-20250501213655848

启动项目 就可以看到点击的结果了

image-20250501213733194

2d相机

创建一个新的 Game场景, 创建一个 2d相机节点, 接下来我们搭建一个角色的基础场景 来学习相机

image-20250501213903228

场景结构

image-20250501214215370

然后我们给角色node加入基础的移动脚本 直接创建脚本实现godot的模版就可以了, 问题就来了 我们启动了之后 一致往右边走 会发现走出一定的距离之后 角色就消失了 这里我们就要用到相机了 我们可以用2d相机绑定到人物上来实现视角的跟随

image-20250501214359527

godot的规则中 父节点会带动子节点移动 那么我们在只有一个相机的情况下 直接将2d相机加入到角色节点下 就实现了效果

相机选项卡中比较重要的设置就是limit

在godot中不希望相机到地图的最左边 left设置为 0 就可以保证不溢出了

image-20250501214722701

地图

基础

瓦片地图 2d中很常用的地图节点 创建新的场景 stumap 并且设置主场景

设置一个全新的tile种子 这样就可以开始设计我们的图块地图了

image-20250501215113815

然后我们导入资源文件中的 tileset 需要注意的是我们每个格子的大小要和tilemap一致 比如图片中都是 16*16px一个格子

image-20250501215445410

之后打开选项卡中的 tilemap就可以看到我们的图片资源 可以选择格子开始绘制了

image-20250501215913185

image-20250501215855374

地图分层

image-20250501215959803

可以选择什么元素展示在什么层中,在编辑中其他层数的元素会变暗 , 运行的时候不会有展示

image-20250501220057094

元素复用

我们可以将画好的地图元素 复制到图案中复用 选中编辑器里的元素 复制到突然即可

image-20250501220353845

元素碰撞

比如我们希望某一个元素块 有物理元素 可以当障碍物使用 , 就可以开启tilemap的物理层 然后选择地图块

image-20250501220733175

为图块添加物理层 然后你就会发现 我们预览其中可以看到图块被添加的碰撞体积

image-20250501220847279

导航寻路

寻路和导航 在任何角色扮演类游戏中都必不可少 接下来学习一下 godot中如何去完成寻路与导航吧

创建一个全新的场景 用于学习导航寻路, 创建一些精灵节点 作为障碍物

image-20250502142346691

想要添加导航 我们需要为场景加入导航区域

image-20250502142437578

创建完成后我们需要再选项卡创建一个绘制多边形

image-20250502142505575

image-20250502142513553

之后就可以绘制导航区域了 同样的这也支持分层, 绘制区域 避开障碍物

image-20250502142632273

创建一个角色控制器

image-20250502142844168

之后为角色导航加入一个 导航代理节点 携带这个代理节点就可以在导航区域使用导航

image-20250502142932678

接下来我们需要实现一个思路 就是让角色移动到我鼠标点击的方向 来看看角色是否可以避开障碍物

using Godot;
using System;

public partial class CharacterBody2dTestNavi : CharacterBody2D
{
//导航使用节点
NavigationAgent2D nav;
public override void _Ready()
{
base._Ready();
//获取 agent节点
nav = GetNode<NavigationAgent2D>("NavigationAgent2D");
}

public override void _PhysicsProcess(double delta)
{
//设置鼠标位置为导航目标点
nav.TargetPosition = GetGlobalMousePosition();
//朝着该方向移动 不断的生成路径点
Vector2 nextPathPosition = (nav.GetNextPathPosition() - this.Position).Normalized();
Velocity = nextPathPosition * 300;
MoveAndSlide();
}
}

基于当前场景运行 就实现了简单的寻路 这里的寻路默认是基于 A*算法

粒子特效

他有两个节点 一个是基于cpu计算 一个是基于显卡计算

image-20250502144456939

通常只要可以使用gpu那么我们都有先用 gpu 性能更加的好

image-20250502144717255

比较重要的就是纹理了 粒子发射实际上就是使用贴图绘制特效 , 选择材质. 创建完之后我们就可以看到粒子的发射了 为什么是向下掉落呢 因为默认是开启了重力

image-20250502144828273

设置纹理

image-20250502145055609

就可以看到发射出来的粒子了 我们可以关闭重力

image-20250502145259685

比如我们想朝着x方向发射粒子

image-20250502145358173

我们可以随意的实验选项卡的选项 来达到各种效果

image-20250502145651037

颜色渐变

image-20250502145825184

我们可以通过设置粒子的展示方式来实现下雪的效果 将持续时间设置5s左右

image-20250502150349258

image-20250502150408733

数据持久化

游戏需要存档 也就是本地持久化 我们需要读档 存档 所以需要以文件的形式 存入到客户端主机里 尤其是我们的引擎是跨平台的 这里我们需要利用godot为我们提供的持久化路径 user:// 它会在不同的系统转为不同的路径

using Godot;
using System;

public partial class localtest : Node2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
string path = ProjectSettings.GlobalizePath("user://1.txt");
GD.Print("路径为" + path);
}

}

创建一个空节点 绑定一个脚本 ,这里我们来演示存储数据

image-20250502150830598

可以看到控制台输出的路径 这就是godot为我们win系统生成的文件持久化路径

image-20250502150939540

这里我们只是展示了一下路径转换并没有创建文件 所有没有 1.TXT

存一个简单的文件

using Godot;
using System;
using System.IO;

public partial class localtest : Node2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
string path = ProjectSettings.GlobalizePath("user://1.txt");
GD.Print("路径为" + path);
File.WriteAllText(path,"唐唐唐唐唐唐唐唐唐唐唐唐唐唐唐唐唐");
}

}

image-20250502151119212

使用json来完成更优质的数据存储

我们使用字典格式来存储为json

using Godot;
using System;
using System.IO;
using Godot.Collections;
using Microsoft.VisualBasic;
using Array = Godot.Collections.Array;

public partial class localtest : Node2D
{
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
//{user:[{"name":"小冷"},{"血量":"100"}]}
//第二层的数据
Dictionary user1 = new Dictionary();
Dictionary user2 = new Dictionary();
user1["name"] = "小冷";
user1["血量"] = "100";
user2["name"] = "猫头";
user2["血量"] = "114514";

//创造一个字典数组
var array = new Array{user1,user2};
Dictionary dic = new Dictionary();
dic["user"] = array;
string stringify = Json.Stringify(dic);
GD.Print(stringify);
}

}

效果

image-20250502152121541

总结

到这里 我们的Godot基础学习与认识 笔记就完结了 之后就可以去尝试学习和创造一些demo 将自己的想象力通过godot实现出来