文章

Godot中节点与信号的基本概念

Godot相较于Unity和UE有诸多不同点,其中很有特色的就是其节点系统与信号系统,此博客简单介绍下这两个系统

Godot中节点与信号的基本概念

一、节点

1.1 节点(Node)是什么

1.1.1 节点的特征与优势

  • 节点是Godot内最常用最基本的开发组件,开发思想接近人类的思想
  • 节点之间可以有父子关系,一个节点可以有多个子节点
  • 大部分节点都具有非常具体的功能,往往直接涉及人类的视听感官,如显示图片、播放音乐、显示模型、模拟物理体等;比如一个有贴图、可以检测敌人以便造成伤害的子弹,我们可以为之创建一个Area2D节点(可以进行区域的检测),然后为之添加子节点CollisionShape2D用于确定区域的形状(类似于Unity中添加xxxCollider2D的Component一样),然后可以添加用于显示子弹贴图的子节点Sprite2D,然后再添加相应代码即可

GodotNodeIntroduce.png

1.1.2 节点在场景树与服务器的控制下运行

  • 场景树是游戏的主循环对象,只有在树下的节点才可以正常发挥其功能(节点初始化时并不在树下,需要手动处理)
  • 服务器是Godot内置的各类Server(服务器的编码接近底层,保障游戏运行效率),节点本身并不具备实际功能,其主要功能由Server实现
  • 节点与Server通过场景树进行协调沟通

1.1.3 节点通过场景组织

  • 场景是若干节点的集合,场景文件是记录若干节点集合的文件(.tscn文件)
  • 游戏实际运行时不存在所谓场景,场景是节点在文件系统中储存和加载的单位
  • 游戏从主场景开始运行

1.2 节点及其组件的调用

1.2.1 通过路径引用

通过路径引用的坏处是如果我们更改了节点名,则代码中的路径会失效;最好只在我们想要访问的节点是当前工作节点(脚本所在节点)的子节点时才使用路径

1.2.1.1 直接使用路径
  • 符号$get_node()函数的简写
  • 我们在Main节点下创建名为Label0Label类型子节点用于显示文字(类似于Unity中的TextMeshPro3D等),然后我们可以在Main的脚本中用$Label0(可以直接把节点拖入代码编辑器)来指向这个节点,并借之调用该节点的一些属性
1
2
3
4
5
6
# 这个是父节点Main的脚本
func _ready()
	# 在节点被创建时,将标签文本内容初始化为字符串
	$Label0.text = "Girls Band Cry"
	# 标签文本的颜色为绿色
	$Label0.modulate = Color.GREEN
  • 对于子节点的子节点,拖入脚本中会产生一条路径(相对路径,从脚本当前路径开始,此处为Main节点上的脚本,而Player位于Main脚本之下)来定位这个对象,比如
1
2
$Player/Weapon
# 等价于get_node("Player/Weapon")
1.2.1.2 将路径保存起来
  • 我们可以将这些引用保存为一个更方便使用的变量,这只需要我们将节点拖入脚本释放的同时按住Ctrl键即可自动生成如下变量定义语句
1
2
3
# 自动产生一个同名的变量
# 因为Godot中节点的创建有着严格的顺序,如果我们打开游戏并试图在Weapon节点存在之前查找他,那么就会报错,所以此处使用@onready来确保所有子节点都被创建完毕后再进行使用
@onready var weapon = $Player/Weapon
  • 我们也可以通过函数get_path()来获取绝对路径
1
2
print(weapon.get_path())
# /root/Main/Player/Weapon

1.2.2 通过@export赋值的方式获取节点

  • 我们可以通过定义一个检查器中可赋值的(特定类型的)节点来引用我们所需要的节点
1
@export var my_node: Sprite2D
  • 通过if检测节点的种类来判断我们是否进行进一步的操作,此时要注意节点类型间的继承关系,如Node2D就是Sprite2D的父类
1
2
3
4
5
6
7
@export var my_node: Sprite2D

func _ready()
	# Sprite2D是Node2D的子类,所以此条判断会被检测为true
	if my_node is Node2D:
		print("Is Node2D")
# Is Node2D

1.2.3 使用%访问独特的唯一名称节点

  • 我们可以将比如GameManager这种我们确定整个游戏只会存在一个的东西设置为一个特殊的唯一节点,这样这个节点右侧就会出现一个%符号,再按一下可以取消该设置

GodotSetAsUniqueName.png

  • 此时我们就可以通过%在任何相同场景中的脚本中访问这个GameManager了
1
@onready var game_manager = %GameManager
  • 注意:必须确保引用GameManager的脚本和GameManager必须处在同一个场景树下

1.3 节点间的通信

1.3.1 父子节点间的通信

向下调用,向上传递信号

  • 观察以下节点结构(节点间的父子关系并不能代表脚本中类之间有继承关系),父节点Main可以调用子节点中的成员变量和函数
  • 子节点无法获取父类的成员变量或者调用其函数,所以需要使用信号来向上传递信息,父节点可以选择性地接收这些信号
1
2
3
4
5
6
7
Main
	Player
		Collider
		Sprite2D
	Enemy
		Collider
		Sprite2D

1.3.2 兄弟节点间的通信

  • 上述节点结构中,PlayerEnemy节点互为兄弟节点,我们若想实现兄弟节点间的信息传递,可以通过共同的父节点,从其中一个兄弟节点获取信号并将其链接到另一个兄弟节点的函数上
  • (我有点没搞懂为什么规范我们这样干,为啥不能直接把兄弟节点的信号直接链接给另一个兄弟节点,非要通过父类节点中转呢,可能是方便统一管理?)

二、信号

2.1 信号(Signal)的基本用法

信号是节点之间可以相互发送的消息,我们使用信号来告知别的节点某事件已经发生,有点类似Unity引擎中的事件(Event)系统

2.1.1 按钮按下的信号

  • 我们以Button节点为例,选中这个节点后我们可以看到该类节点上所有信号的列表

GodotButtonSignal.png

  • Button节点是Main节点的子节点,我们以Button节点上的pressed()信号为例,将其链接到此处的Main节点的脚本上:双击这个pressed()信号,弹出下面的界面

GodotButtonPressedSignal.png

  • 选择Main后点Connect按钮,这将会在Main节点的脚本上创建一个对应的函数
1
2
func _on_button_pressed():
	pass
  • 此函数旁会有一个绿色箭头,这意味着有一个信号链接到了此函数,点击箭头会跳出相关信息

GodotButtonSignalOnScript.png

  • 我们将函数中的pass替换为按下按钮后(即pressed信号被发出)要执行的语句即可

2.1.2 计时器的信号

  • 节点Timer有一个信号timeout(),这个计时器倒数一定时间长度后会发出这个信号,可以用于比如技能冷却等功能的实现

2.2 信号的自定义创建

2.2.1 不传参的信号

  • 比如我们自己的Main节点由子节点Player和子节点计时器Timer组成,我们想让Player脚本中的经验值满了后进行升级,而这个升级需要作为一个信号来触发Main中的技能解锁,总之这个时候我们需要在Player节点脚本处创建一个信号来完成这个信息的传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Player节点脚本中
extends Node

# 创建信号,使得该脚本所属的节点产生新的可选信号,可以在别处调用
signal level_up

# 经验值
var xp := 0

# 计时器的timeout信号链接,实现每隔几秒增加5经验值
func _on_timer_timeout():
	xp += 5
	print(xp)
	# 经验值超过20时升级
	if xp >= 20:
		xp = 0
		print("level up!")
		# 释放升级的信号
		level_up.emit()
  • Main脚本处,可以添加这个信号的链接,除了手动在引擎中添加外,还可以通过代码来链接或者断开链接,以下是另一个节点的脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Main节点脚本中
extends Node

# 保存Player节点的引用
@onready var player = $Player

# 用于储存player中的升级信号
var level_up_signal: Signal

func _ready():
	# 获取player中的升级信号
	level_up_signal = player.level_up
	# 真正与player中的升级信号建立链接,此时_on_player_level_up函数左侧才会出现绿色箭头
	# 若想要断开链接,则同理调用disconnect函数即可
	level_up_signal.connect(_on_player_level_up)

func _on_player_level_up():
	print("new skill unlocked!")
  • 此时我们运行场景,得到以下输出

GodotLevelUpSignal.png

2.2.2 传参的信号

  • 同样还是玩家升级的信号,我们新增要求:玩家在特定等级解锁特定等级的技能,这需要我们将玩家等级记录下来并传递在信号中
  • 我们需要在三个地方写出需要传递的参数列表
  • 第一处:信号定义的参数列表处;第二处:信号emit()函数括号内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Player节点脚本中
extends Node

# 创建可以传参的信号,当然,也可以传多个参数
signal level_up(level: int)

# 经验值
var xp := 0
# 等级
var level := 0

# 计时器的timeout信号链接,实现每隔几秒增加5经验值
func _on_timer_timeout():
	xp += 5
	# 经验值超过20时升级
	if xp >= 20:
		xp = 0
		level += 1
		print("player level: " + str(level))
		# 释放升级的信号,注意这里也要把需要传递的参数传递出去
		level_up.emit(level)
  • 第三处:信号在被建立连接的实现函数的参数列表处
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Main节点脚本中
extends Node

# 保存Player节点的引用
@onready var player = $Player

# 用于储存player中的升级信号
var level_up_signal: Signal

func _ready():
	# 获取player中的升级信号
	level_up_signal = player.level_up
	# 真正与player中的升级信号建立链接,此时_on_player_level_up函数左侧才会出现绿色箭头
	# 若想要断开链接,则同理调用disconnect函数即可
	level_up_signal.connect(_on_player_level_up)

# 这里注意信号定义时如果有传参,则这里也应当同样格式地写出来
func _on_player_level_up(level: int):
	if level == 3:
		print("skill at Lv.3 unlocked!")
	if level == 6:
		print("skill at Lv.6 unlocked!")
  • 运行后得到以下输出

GodotLevelUpSignalWithParameter.png

本文由作者按照 CC BY-NC-SA 4.0 进行授权