使用信号

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

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

例如,您可能在屏幕上有一个代表玩家生命值的生命条。当玩家受到伤害或使用治疗药水时,您希望生命条反映变化。要做到这一点,在 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

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

你也可以通过在检查器中编辑 Text 属性在 Button 上写一个标签。请输入“Toggle motion”。

../../_images/signals_08_toggle_motion_text.png

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

../../_images/signals_09_scene_setup.png

保存新建的场景。然后你就可以使用 F6 来运行。此时按钮是可见的,但是按下之后不会有任何反应。

在编辑器中连接信号

在这里,我们希望将按钮的“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

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

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

../../_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,反之亦然。

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

完整脚本

这就是我们小小的 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 游戏,使用目前为止学到的东西进行实战。