Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
使用信号
在本课中,我们将介绍信号。它们是节点在发生特定事件时发出的消息,例如按下按钮。其他节点可以连接到该信号,并在事件发生时调用函数。
信号是 Godot 内置的委派机制,允许一个游戏对象对另一个游戏对象的变化做出反应,而无需相互引用。使用信号可以限制耦合,并保持代码的灵活性。
例如,你可能在屏幕上有一个代表玩家生命值的生命条。当玩家受到伤害或使用治疗药水时,你希望生命条反映变化。要做到这一点,在 Godot 中,你会使用到信号。
从 Godot 4.0 开始,信号和方法(Callable)一样,都成为了一等类型。这意味着你可以直接把信号当作方法的参数使用,无需以字符串的形式传参,这样能够更好地实现自动补全、更不容易出错。使用 Signal 类型能够直接实现的功能见 Signal 类参考手册。
参见
正如引言中提到的,信号是 Godot 版本的观察者模式。你可以在 Game Programming Patterns 了解更多相关信息。
现在,我们将使用信号来使上一节课(监听玩家的输入)中的 Godot 图标移动,并通过按下按钮来停止。
备注
对于此项目,我们将遵循 Godot 的命名约定。
GDScript:类(节点)使用 PascalCase(大驼峰命名法),变量和函数使用 snake_case(蛇形命名法),常量使用 ALL_CAPS(全大写)(请参阅 GDScript 编写风格指南)。
C#:类、导出变量和方法使用 PascalCase(大驼峰命名法),私有字段使用 _camelCase(前缀下划线的小驼峰命名法),局部变量和参数使用 camelCase(小驼峰命名法)(请参阅 C# 风格指南)。连接信号时,请务必准确键入方法名称。
场景设置
要为我们的游戏添加按钮,我们需要新建一个场景,包含一个按钮以及之前课程 创建第一个脚本 编写的 sprite_2d.tscn 场景。
主菜单选择 ,创建新场景。
在场景面板中,单击 按钮,即可添加一个 Node2D 作为场景根节点。
在文件系统面板中,单击之前保存的 sprite_2d.tscn 文件并将其拖动到 Node2D 上,对其进行实例化。
我们希望添加另一个节点作为 Sprite2D 的同级节点。为此,请右键点击 Node2D,然后选择 。
寻找并添加 Button 节点。
该节点默认比较小。在视口中,点击并拖拽该按钮右下角的手柄来调整大小。
如果看不到手柄,请确保工具栏中的选择工具处于活动状态。
点击并拖拽按钮使其更接近精灵。
你也可以在 Inspector 中编辑 Button 的 Text 属性来显示文字。请输入 Toggle motion。
你的场景树和视口应该是类似这样的。
如果你还没保存场景的话,保存新建的场景为 node_2d.tscn。然后你就可以使用 F6 (macOS 则为 Cmd + R )来运行。此时,你可以看到按钮,但是按下之后不会有任何反应。
在编辑器中连接信号
然后,我们希望将按钮的“pressed”信号连接到我们的 Sprite2D,并且我们想要调用一个新函数来打开和关闭其运动。我们需要像我们在上一课中所做的操作一样,将一个脚本附加到 Sprite2D 节点。
您可以在 Signals 面板中连接信号。选中 Button 节点,然后在编辑器右侧点击紧邻 Inspector 旁边的 Signals 。
停靠栏显示所选节点上可用的信号列表。
双击“pressed”信号,打开节点连接窗口。
然后,你可以将信号连接到 Sprite2D 节点。该节点需要一个用于接收按钮信号的函数,当按钮发出信号时,Godot 将调用该函数。编辑器会为你生成一个。按照规范,我们将这些回调方法命名为"_on_node_name_signal_name"。在这里,它被命名为"_on_button_pressed"。
备注
在通过编辑器的“信号”工具栏连接信号时,您可以采用两种模式。其中一种较为简单,仅允许您将信号连接到已附加脚本的节点上,并在这些节点上创建一个新的回调函数。
高级视图允许您连接到任意节点和内置函数,为回调添加参数并设置选项。您可以通过点击窗口右下角的 按钮来切换此模式。
备注
如果你在使用一个外部代码编辑器(例如VS Code),可能会没有自动代码生成。在这种情况下,你需要按照下一部分阐述的方法使用信号连接代码。
点击 按钮以完成信号连接,并跳转至 Script 工作区。您应在左侧边栏看到带有连接图标的新增方法。
如果单击该图标,将弹出一个窗口并显示有关连接的信息。此功能仅在编辑器中连接节点时可用。
让我们用代码替换带有 pass 关键字的一行,以切换节点的运动。
我们的 Sprite2D 由于 _process() 函数中的代码而移动。Godot 提供了一种打开和关闭处理的方法:Node.set_process() 。Node 的另一个方法 is_processing() ,如果空闲处理处于活动状态,则返回 true。我们可以使用 not 关键字来反转该值。
func _on_button_pressed():
set_process(not is_processing())
// We also specified this function name in PascalCase in the editor's connection window.
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
此函数将切换处理,进而切换按下按钮时图标的移动。
在尝试游戏之前,我们需要简化 _process() 函数,以自动移动节点,而不是等待用户输入。将其替换为以下代码,这是我们在两课前看到的代码:
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
public override void _Process(double delta)
{
Rotation += _angularSpeed * (float)delta;
var velocity = Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)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())
using Godot;
public partial class MySprite2D : Sprite2D
{
private float _speed = 400;
private float _angularSpeed = Mathf.Pi;
public override void _Process(double delta)
{
Rotation += _angularSpeed * (float)delta;
var velocity = Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)delta;
}
// We also specified this function name in PascalCase in the editor's connection window.
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
}
按下 F6 键运行当前场景(macOS为 Cmd + R ),然后点击按钮,就可以看到精灵开始或停止运动。
用代码连接信号
你可以通过代码连接信号,而不是使用编辑器。这在脚本中创建节点或实例化场景时是必需的。
让我们在这里使用一个不同的节点。Godot 有一个 Timer 节点,可用于实现技能冷却时间、武器重装等。
回到 2D 工作区。你可以点击窗口顶部的“2D”字样,或者按 Ctrl + F1(macOS 上则是 Ctrl + Cmd + 1)。
在“场景”面板中,右键点击 Sprite2D 节点并添加新的子节点。搜索 Timer 并添加对应节点。你的场景现在应该类似这样。
选中 Timer 节点后,前往 Inspector,并启用 Autostart 属性。
点击 Sprite2D 旁边的脚本图标,即可跳转回脚本工作区。
我们需要执行两个操作,通过代码将节点连接起来:
从 Sprite2D 获取对 Timer 的引用。
在 Timer 的 "timeout" 信号上调用
connect()方法。
备注
要通过代码连接信号,需要调用您希望监听的信号的 connect() 方法。在此例中,我们希望监听 Timer 的 "timeout" 信号。
我们想要在场景实例化时连接信号,我们可以使用 Node._ready() 内置函数来实现这一点,当节点完全实例化时,引擎会自动调用该函数。
为了获取相对于当前节点的引用,我们使用方法 Node.get_node()。我们可以将引用存储在变量中。
func _ready():
var timer = get_node("Timer")
public override void _Ready()
{
var timer = GetNode<Timer>("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)
public override void _Ready()
{
var timer = GetNode<Timer>("Timer");
timer.Timeout += OnTimerTimeout;
}
该行读起来是这样的:我们将计时器的“timeout”信号连接到脚本附加到的节点上。当计时器发出 timeout 时,去调用我们需要定义的函数 _on_timer_timeout()。让我们将其定义添加到脚本的底部,并使用它来切换精灵的可见性。
备注
按照惯例,我们将这些回调方法在 GDScript 中命名为“_on_node_name_signal_name”,在 C# 中命名为“OnNodeNameSignalName”。故此处的GDScript 为“_on_timer_timeout”,C# 为“OnTimerTimeout()”。
func _on_timer_timeout():
visible = not visible
private void OnTimerTimeout()
{
Visible = !Visible;
}
visible 属性是一个布尔值,用于控制节点的可见性。visible = not visible 行切换该值。如果 visible 是 true,它就会变成 false,反之亦然。
如果你现在运行 Node2D 场景,就会看到精灵在闪啊闪的,间隔为一秒。
完整脚本
这就是我们小小的 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
using Godot;
public partial class MySprite2D : Sprite2D
{
private float _speed = 400;
private float _angularSpeed = Mathf.Pi;
public override void _Ready()
{
var timer = GetNode<Timer>("Timer");
timer.Timeout += OnTimerTimeout;
}
public override void _Process(double delta)
{
Rotation += _angularSpeed * (float)delta;
var velocity = Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)delta;
}
// We also specified this function name in PascalCase in the editor's connection window.
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
private void OnTimerTimeout()
{
Visible = !Visible;
}
}
自定义信号
备注
本节介绍的是如何定义并使用你自己的信号,不依赖之前课程所创建的项目。
您可以在脚本中定义自定义信号。例如,假设您希望在玩家的生命值为零时通过屏幕显示游戏结束。为此,当他们的生命值达到 0 时,您可以定义一个名为“died”或“health_depleted”的信号。
extends Node2D
signal health_depleted
var health = 10
using Godot;
public partial class MyNode2D : Node2D
{
[Signal]
public delegate void HealthDepletedEventHandler();
private int _health = 10;
}
备注
由于信号表示刚刚发生的事件,我们通常在其名称中使用过去时态的动作动词。
您的自定义信号与内置信号的工作方式相同:它们会显示在 Signals 标签页中,并且可以像其他信号一样进行连接。
要通过代码发出信号,请调用信号的 emit() 方法。
func take_damage(amount):
health -= amount
if health <= 0:
health_depleted.emit()
public void TakeDamage(int amount)
{
_health -= amount;
if (_health <= 0)
{
EmitSignal(SignalName.HealthDepleted);
}
}
信号还可以选择声明一个或多个参数。在括号之间指定参数的名称:
extends Node2D
signal health_changed(old_value, new_value)
var health = 10
using Godot;
public partial class MyNode : Node
{
[Signal]
public delegate void HealthChangedEventHandler(int oldValue, int newValue);
private int _health = 10;
}
备注
信号参数会出现在编辑器的“信号”工具栏中,Godot 可以利用这些参数为您生成回调函数。不过,在发出信号时,您仍可以传递任意数量的参数。所以,您需要自行确定要传递的正确值。
要在发出信号的同时传值,请将它们添加为 emit() 函数的额外参数:
func take_damage(amount):
var old_health = health
health -= amount
health_changed.emit(old_health, health)
public void TakeDamage(int amount)
{
int oldHealth = _health;
_health -= amount;
EmitSignal(SignalName.HealthChanged, oldHealth, _health);
}
总结
Godot 中的任何节点都会在发生特定事件时发出信号,例如按下按钮。其他节点可以连接到单个信号并对所选事件做出反应。
信号有很多用途。有了它们,你可以对进入或退出游戏世界的节点、碰撞、角色进入或离开某个区域、界面元素的大小变化等等做出反应。
例如,代表金币的 Area2D 会在玩家的物理实体进入其碰撞形状时发出 body_entered 信号,让你知道玩家收集到了金币。
在下一节 你的第一个 2D 游戏 中,你将创建一个完整的 2D 游戏,使用目前为止学到的东西进行实战。