Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

使用信号

在本课中,我们将介绍信号。它们是节点在发生特定事件时发出的消息,例如按下按钮。其他节点可以连接到该信号,并在事件发生时调用函数。

信号是 Godot 内置的委派机制,允许一个游戏对象对另一个游戏对象的变化做出反应,而无需相互引用。使用信号可以限制耦合,并保持代码的灵活性。

例如,你可能在屏幕上有一个代表玩家生命值的生命条。当玩家受到伤害或使用治疗药水时,你希望生命条反映变化。要做到这一点,在 Godot 中,你会使用到信号。

备注

正如引言中提到的,信号是 Godot 版本的观察者模式。你可以在此处了解有关它的更多信息:https://gameprogrammingpatterns.com/observer.html

现在,我们将使用信号来使上一节课(监听玩家的输入)中的 Godot 图标移动,并通过按下按钮来停止。

场景设置

要为我们的游戏添加按钮,我们需要新建一个“主”场景,包含一个按钮以及之前课程 创建第一个脚本 编写的 sprite_2d.tscn 场景。

通过转到菜单“场景 -> 新建场景”来创建新场景。

../../_images/signals_01_new_scene.webp

在场景面板中,单击“2D 场景”按钮。这样就会添加一个 Node2D 作为我们的根节点。

../../_images/signals_02_2d_scene.webp

在文件系统面板中,单击之前保存的 sprite_2d.tscn 文件并将其拖动到 Node2D 上,对其进行实例化。

../../_images/signals_03_dragging_scene.png

我们想要添加另一个节点作为 Sprite2D 的同级节点。为此,请右键单击 Node2D,然后选择“添加子节点”。

../../_images/signals_04_add_child_node.webp

寻找并添加 Button 节点。

../../_images/signals_05_add_button.webp

该节点默认比较小。在视口中,点击并拖拽该按钮右下角的手柄来调整大小。

../../_images/signals_06_drag_button.png

如果看不到手柄,请确保工具栏中的选择工具处于活动状态。

../../_images/signals_07_select_tool.webp

点击并拖拽按钮使其更接近精灵。

你可以通过修改检查器中的 Text 属性来给 Button 上写一个标签。请输入Toggle motion

../../_images/signals_08_toggle_motion_text.webp

你的场景树和视口应该是类似这样的。

../../_images/signals_09_scene_setup.png

如果你还没保存场景的话,保存新建的场景为 node_2d.tscn。然后你就可以使用 F6`(macOS 则为 :kbd:`Cmd + R)来运行。此时,你可以看到按钮,但是按下之后不会有任何反应。

在编辑器中连接信号

然后,我们希望将按钮的“pressed”信号连接到我们的 Sprite2D,并且我们想要调用一个新函数来打开和关闭其运动。我们需要像我们在上一课中所做的操作一样,将一个脚本附加到 Sprite2D 节点。

你可以在“节点”面板中连接信号。选择 Button 节点,然后在编辑器的右侧,单击检查器旁边名为“节点”的选项卡。

../../_images/signals_10_node_dock.webp

停靠栏显示所选节点上可用的信号列表。

../../_images/signals_11_pressed_signals.webp

双击“pressed”信号,打开节点连接窗口。

../../_images/signals_12_node_connection.png

然后,你可以将信号连接到 Sprite2D 节点。该节点需要一个用于接收按钮信号的函数,当按钮发出信号时,Godot 将调用该函数。编辑器会为你生成一个。按照规范,我们将这些回调方法命名为"_on_node_name_signal_name"。在这里,它被命名为"_on_button_pressed"。

备注

通过编辑器的节点面板连接信号时,可以使用两种模式。简单的一个只允许你连接到附加了脚本的节点,并在它们上面创建一个新的回调函数。

../../_images/signals_advanced_connection_window.png

你可以在高级视图中连接到任何节点和任何内置函数、向回调添加参数、设置选项。你可以单击窗口右下角的“高级”按钮来切换模式。

单击“连接”按钮以完成信号连接并跳转到脚本工作区。你应该会看到新方法,并在左边距中带有连接图标。

../../_images/signals_13_signals_connection_icon.webp

如果单击该图标,将弹出一个窗口并显示有关连接的信息。此功能仅在编辑器中连接节点时可用。

../../_images/signals_14_signals_connection_info.webp

让我们用代码替换带有 pass 关键字的一行,以切换节点的运动。

我们的 Sprite2D 由于 _process() 函数中的代码而移动。Godot 提供了一种打开和关闭处理的方法:Node.set_process() 。Node 的另一个方法 is_processing() ,如果空闲处理处于活动状态,则返回 true。我们可以使用 not 关键字来反转该值。

func _on_button_pressed():
    set_process(not is_processing())

此函数将切换处理,进而切换按下按钮时图标的移动。

在尝试游戏之前,我们需要简化 _process() 函数,以自动移动节点,而不是等待用户输入。将其替换为以下代码,这是我们在两课前看到的代码:

func _process(delta):
    rotation += angular_speed * delta
    var velocity = Vector2.UP.rotated(rotation) * speed
    position += velocity * delta

你的完整的 Sprite_2d.gd 代码应该是类似下面这样的。

extends Sprite2D

var speed = 400
var angular_speed = PI


func _process(delta):
    rotation += angular_speed * delta
    var velocity = Vector2.UP.rotated(rotation) * speed
    position += velocity * delta


func _on_button_pressed():
    set_process(not is_processing())

运行该场景,然后点击按钮,就可以看到精灵开始或停止运行。

用代码连接信号

你可以通过代码连接信号,而不是使用编辑器。这在脚本中创建节点或实例化场景时是必需的。

让我们在这里使用一个不同的节点。Godot 有一个 Timer 节点,可用于实现技能冷却时间、武器重装等。

回到 2D 工作区。你可以点击窗口顶部的“2D”字样,或者按 Ctrl + F1(macOS 上则是 Ctrl + Cmd + 1)。

在“场景”面板中,右键点击 Sprite2D 节点并添加新的子节点。搜索 Timer 并添加对应节点。你的场景现在应该类似这样。

../../_images/signals_15_scene_tree.png

选中 Timer 节点,在“检查器”中勾选 Autostart 属性。

../../_images/signals_18_timer_autostart.png

点击 Sprite2D 旁的脚本图标,返回脚本工作区。

../../_images/signals_16_click_script.png

我们需要执行两个操作来通过代码连接节点:

  1. 从 Sprite2D 获取 Timer 的引用。

  2. 通过 Timer 的“timeout”信号调用 connect() 方法。

备注

要使用代码来连接信号,你需要调用所需监听节点信号的 connect() 方法。这里我们要监听的是 Timer 的“timeout”信号。

我们想要在场景实例化时连接信号,我们可以使用 Node._ready() 内置函数来实现这一点,当节点完全实例化时,引擎会自动调用该函数。

为了获取相对于当前节点的引用,我们使用方法 Node.get_node()。我们可以将引用存储在变量中。

func _ready():
    var timer = get_node("Timer")

get_node() 函数会查看 Sprite2D 的子节点,并按节点的名称获取节点。例如,如果在编辑器中将 Timer 节点重命名为“BlinkingTimer”,则必须将调用更改为 get_node("BlinkingTimer")

现在,我们可以在 _ready() 函数中将Timer连接到Sprite2D。

func _ready():
    var timer = get_node("Timer")
    timer.timeout.connect(_on_timer_timeout)

该行读起来是这样的:我们将计时器的“timeout”信号连接到脚本附加到的节点上。当计时器发出“timeout”时,去调用我们需要定义的函数``_on_timer_timeout()``。让我们将其定义添加到脚本的底部,并使用它来切换 sprite 的可见性。

备注

按照惯例,我们将这些回调方法在 GDScript 中命名为“_on_node_name_signal_name”,在 C# 中命名为“OnNodeNameSignalName”。故此处的GDScript 为“_on_timer_timeout”,C# 为“OnTimerTimeout()”。

func _on_timer_timeout():
    visible = not visible

visible 属性是一个布尔值,用于控制节点的可见性。visible = not visible 行切换该值。如果 visibletrue,它就会变成 false,反之亦然。

如果你现在运行场景,就会看到精灵在闪啊闪的,间隔为一秒。

完整脚本

这就是我们小小的 Godot 图标移动闪烁演示了!这是完整的 sprite_2d.gd 文件,仅供参考。

extends Sprite2D

var speed = 400
var angular_speed = PI


func _ready():
    var timer = get_node("Timer")
    timer.timeout.connect(_on_timer_timeout)


func _process(delta):
    rotation += angular_speed * delta
    var velocity = Vector2.UP.rotated(rotation) * speed
    position += velocity * delta


func _on_button_pressed():
    set_process(not is_processing())


func _on_timer_timeout():
    visible = not visible

自定义信号

备注

本节介绍的是如何定义并使用你自己的信号,不依赖之前课程所创建的项目。

你可以在脚本中定义自定义信号。例如,假设你希望在玩家的生命值为零时通过屏幕显示游戏结束。为此,当他们的生命值达到 0 时,你可以定义一个名为“died”或“health_depleted”的信号。

extends Node2D

signal health_depleted

var health = 10

备注

由于信号表示刚刚发生的事件,我们通常在其名称中使用过去时态的动作动词。

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

../../_images/signals_17_custom_signal.png

要通过代码发出信号,请调用信号的 emit() 方法。

func take_damage(amount):
    health -= amount
    if health <= 0:
        health_depleted.emit()

信号还可以选择声明一个或多个参数。在括号之间指定参数的名称:

extends Node

signal health_changed(old_value, new_value)

var health = 10

备注

这些信号参数显示在编辑器的节点停靠面板中,Godot 可以使用它们为你生成回调函数。但是,发出信号时仍然可以发出任意数量的参数;所以由你来决定是否发出正确的值。

要在发出信号的同时传值,请将它们添加为 emit() 函数的额外参数:

func take_damage(amount):
    var old_health = health
    health -= amount
    health_changed.emit(old_health, health)

总结

Godot 中的任何节点都会在发生特定事件时发出信号,例如按下按钮。其他节点可以连接到单个信号并对所选事件做出反应。

信号有很多用途。有了它们,你可以对进入或退出游戏世界的节点、碰撞、角色进入或离开某个区域、界面元素的大小变化等等做出反应。

例如,代表金币的 Area2D 会在玩家的物理实体进入其碰撞形状时发出 body_entered 信号,让你知道玩家收集到了金币。

在下一节 你的第一个 2D 游戏 中,你将创建一个完整的 2D 游戏,使用目前为止学到的东西进行实战。