使用信号

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

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

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

Like methods (Callable), signals are a first-class type since Godot 4.0. This means you can pass them around as method arguments directly without having to pass them as strings, which allows for better autocompletion and is less error-prone. See the Signal class reference for a list of what you can do with the Signal type directly.

参见

As mentioned in the introduction, signals are Godot’s version of the observer pattern. You can learn more about it in Game Programming Patterns.

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

备注

对于此项目,我们将遵循 Godot 的命名约定。

  • GDScript:类(节点)使用 PascalCase(大驼峰命名法),变量和函数使用 snake_case(蛇形命名法),常量使用 ALL_CAPS(全大写)(请参阅 GDScript 编写风格指南)。

  • C#:类、导出变量和方法使用 PascalCase(大驼峰命名法),私有字段使用 _camelCase(前缀下划线的小驼峰命名法),局部变量和参数使用 camelCase(小驼峰命名法)(请参阅 C# 风格指南)。连接信号时,请务必准确键入方法名称。

场景设置

要为我们的游戏添加按钮,我们需要新建一个场景,包含一个按钮以及之前课程 创建第一个脚本 编写的 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.webp

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

备注

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

../../_images/signals_advanced_connection_window.png

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

备注

如果你在使用一个外部代码编辑器(例如VS Code),可能会没有自动代码生成。在这种情况下,你需要按照下一部分阐述的方法使用信号连接代码。

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

../../_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 关键字来反转该值。

GDScriptC#

  1. func _on_button_pressed():
  2. set_process(not is_processing())
  1. // We also specified this function name in PascalCase in the editor's connection window.
  2. private void OnButtonPressed()
  3. {
  4. SetProcess(!IsProcessing());
  5. }

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

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

GDScriptC#

  1. func _process(delta):
  2. rotation += angular_speed * delta
  3. var velocity = Vector2.UP.rotated(rotation) * speed
  4. position += velocity * delta
  1. public override void _Process(double delta)
  2. {
  3. Rotation += _angularSpeed * (float)delta;
  4. var velocity = Vector2.Up.Rotated(Rotation) * _speed;
  5. Position += velocity * (float)delta;
  6. }

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

GDScriptC#

  1. extends Sprite2D
  2. var speed = 400
  3. var angular_speed = PI
  4. func _process(delta):
  5. rotation += angular_speed * delta
  6. var velocity = Vector2.UP.rotated(rotation) * speed
  7. position += velocity * delta
  8. func _on_button_pressed():
  9. set_process(not is_processing())
  1. using Godot;
  2. public partial class MySprite2D : Sprite2D
  3. {
  4. private float _speed = 400;
  5. private float _angularSpeed = Mathf.Pi;
  6. public override void _Process(double delta)
  7. {
  8. Rotation += _angularSpeed * (float)delta;
  9. var velocity = Vector2.Up.Rotated(Rotation) * _speed;
  10. Position += velocity * (float)delta;
  11. }
  12. // We also specified this function name in PascalCase in the editor's connection window.
  13. private void OnButtonPressed()
  14. {
  15. SetProcess(!IsProcessing());
  16. }
  17. }

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

用代码连接信号

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

让我们在这里使用一个不同的节点。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()。我们可以将引用存储在变量中。

GDScriptC#

  1. func _ready():
  2. var timer = get_node("Timer")
  1. public override void _Ready()
  2. {
  3. var timer = GetNode<Timer>("Timer");
  4. }

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

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

GDScriptC#

  1. func _ready():
  2. var timer = get_node("Timer")
  3. timer.timeout.connect(_on_timer_timeout)
  1. public override void _Ready()
  2. {
  3. var timer = GetNode<Timer>("Timer");
  4. timer.Timeout += OnTimerTimeout;
  5. }

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

备注

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

GDScriptC#

  1. func _on_timer_timeout():
  2. visible = not visible
  1. private void OnTimerTimeout()
  2. {
  3. Visible = !Visible;
  4. }

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

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

完整脚本

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

GDScriptC#

  1. extends Sprite2D
  2. var speed = 400
  3. var angular_speed = PI
  4. func _ready():
  5. var timer = get_node("Timer")
  6. timer.timeout.connect(_on_timer_timeout)
  7. func _process(delta):
  8. rotation += angular_speed * delta
  9. var velocity = Vector2.UP.rotated(rotation) * speed
  10. position += velocity * delta
  11. func _on_button_pressed():
  12. set_process(not is_processing())
  13. func _on_timer_timeout():
  14. visible = not visible
  1. using Godot;
  2. public partial class MySprite2D : Sprite2D
  3. {
  4. private float _speed = 400;
  5. private float _angularSpeed = Mathf.Pi;
  6. public override void _Ready()
  7. {
  8. var timer = GetNode<Timer>("Timer");
  9. timer.Timeout += OnTimerTimeout;
  10. }
  11. public override void _Process(double delta)
  12. {
  13. Rotation += _angularSpeed * (float)delta;
  14. var velocity = Vector2.Up.Rotated(Rotation) * _speed;
  15. Position += velocity * (float)delta;
  16. }
  17. // We also specified this function name in PascalCase in the editor's connection window.
  18. private void OnButtonPressed()
  19. {
  20. SetProcess(!IsProcessing());
  21. }
  22. private void OnTimerTimeout()
  23. {
  24. Visible = !Visible;
  25. }
  26. }

自定义信号

备注

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

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

GDScriptC#

  1. extends Node2D
  2. signal health_depleted
  3. var health = 10
  1. using Godot;
  2. public partial class MyNode2D : Node2D
  3. {
  4. [Signal]
  5. public delegate void HealthDepletedEventHandler();
  6. private int _health = 10;
  7. }

备注

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

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

../../_images/signals_17_custom_signal.webp

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

GDScriptC#

  1. func take_damage(amount):
  2. health -= amount
  3. if health <= 0:
  4. health_depleted.emit()
  1. public void TakeDamage(int amount)
  2. {
  3. _health -= amount;
  4. if (_health <= 0)
  5. {
  6. EmitSignal(SignalName.HealthDepleted);
  7. }
  8. }

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

GDScriptC#

  1. extends Node2D
  2. signal health_changed(old_value, new_value)
  3. var health = 10
  1. using Godot;
  2. public partial class MyNode : Node
  3. {
  4. [Signal]
  5. public delegate void HealthChangedEventHandler(int oldValue, int newValue);
  6. private int _health = 10;
  7. }

备注

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

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

GDScriptC#

  1. func take_damage(amount):
  2. var old_health = health
  3. health -= amount
  4. health_changed.emit(old_health, health)
  1. public void TakeDamage(int amount)
  2. {
  3. int oldHealth = _health;
  4. _health -= amount;
  5. EmitSignal(SignalName.HealthChanged, oldHealth, _health);
  6. }

总结

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

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

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

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