使用信号

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

Signals are a delegation mechanism built into Godot that allows one game object to react to a change in another without them referencing one another. Using signals limits coupling and keeps your code flexible.

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

备注

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

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

场景设置

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

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

../../_images/signals_01_new_scene.png

在场景停靠栏中,单击 2D 场景按钮。这将添加一个 Node2D 作为我们的根。

../../_images/signals_02_2d_scene.png

在文件系统停靠栏中,单击之前保存的 Sprite.tscn 文件并将其拖动到 Node2D 上以对其进行实例化。

../../_images/signals_03_dragging_scene.png

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

../../_images/signals_04_add_child_node.png

寻找并添加 Button 节点类型。

../../_images/signals_05_add_button.png

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

../../_images/signals_06_drag_button.png

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

../../_images/signals_07_select_tool.png

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

You can also write a label on the Button by editing its Text property in the Inspector. Enter "Toggle motion".

../../_images/signals_08_toggle_motion_text.png

您的场景树和视区应该是类似这样的。

../../_images/signals_09_scene_setup.png

Save your newly created scene. You can then run it with F6. At the moment, the button will be visible, but nothing will happen if you press it.

在编辑器中连接信号

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

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

../../_images/signals_10_node_dock.png

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

../../_images/signals_11_pressed_signals.png

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

../../_images/signals_12_node_connection.png

在那里,您可以将信号连接到 Sprite 节点。节点需要一个接收器方法,当按钮发出信号时,Godot 将调用该函数。编辑器会为您生成一个。按照惯例,我们将这些回调方法命名为“_on_节点名_信号名”。在这里,它将是“_on_Button_pressed”。

备注

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

../../_images/signals_advanced_connection_window.png

The advanced view lets you connect to any node and any built-in function, add arguments to the callback, and set options. You can toggle the mode in the window's bottom-right by clicking the Advanced button.

Click the Connect button to complete the signal connection and jump to the Script workspace. You should see the new method with a connection icon in the left margin.

../../_images/signals_13_signals_connection_icon.png

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

../../_images/signals_14_signals_connection_info.png

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

我们的 Sprite 由于 _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.gd 代码应该是类似下面这样的。

extends Sprite

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 上则是 Alt + 1)。

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

../../_images/signals_15_scene_tree.png

选中 Timer 节点,在“检查器”中勾选 Autostart(自动开启)属性。

../../_images/signals_18_timer_autostart.png

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

../../_images/signals_16_click_script.png

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

  1. 从 Sprite 获取 Timer 的引用。

  2. 调用 Timer 的 connect() 方法。

备注

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

我们想要在场景实例化时连接信号,那么就可以在 Node._ready() 中实现。引擎会在节点完全实例化后自动调用这个函数。

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

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

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

现在,我们可以在 _ready() 函数中将计时器连接到精灵。

func _ready():
    var timer = get_node("Timer")
    timer.connect("timeout", self, "_on_Timer_timeout")

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

func _on_Timer_timeout():
    visible = not visible

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

If you run the scene now, you will see that the sprite blinks on and off, at one second intervals.

完整脚本

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

extends Sprite

var speed = 400
var angular_speed = PI


func _ready():
    var timer = get_node("Timer")
    timer.connect("timeout", self, "_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_signal()

func take_damage(amount):
    health -= amount
    if health <= 0:
        emit_signal("health_depleted")

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

extends Node

signal health_changed(old_value, new_value)

备注

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

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

func take_damage(amount):
    var old_health = health
    health -= amount
    emit_signal("health_changed", old_health, health)

总结

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

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

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

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