使用信号
在本课中,我们将介绍信号。它们是节点在发生特定事件时发出的消息,例如按下按钮。其他节点可以连接到该信号,并在事件发生时调用函数。
信号是 Godot 内置的委派机制,允许一个游戏对象对另一个游戏对象的变化做出反应,而无需相互引用。使用信号可以限制耦合),并保持代码的灵活性。
例如,你可能在屏幕上有一个代表玩家生命值的生命条。当玩家受到伤害或使用治疗药水时,你希望生命条反映变化。要做到这一点,在 Godot 中,你会使用到信号。
备注
正如引言中提到的,信号是 Godot 版本的观察者模式。你可以在此处了解有关它的更多信息:https://gameprogrammingpatterns.com/observer.html
现在,我们将使用信号来使上一节课(监听玩家的输入)中的 Godot 图标移动,并通过按下按钮来停止。
场景设置
要为我们的游戏添加按钮,我们需要新建一个“主”场景,包含一个按钮以及之前课程 创建第一个脚本 编写的 sprite_2d.tscn
场景。
通过转到菜单“场景 -> 新建场景”来创建新场景。
在场景面板中,单击“2D 场景”按钮。这样就会添加一个 Node2D 作为我们的根节点。
在文件系统面板中,单击之前保存的 sprite_2d.tscn
文件并将其拖动到 Node2D 上,对其进行实例化。
我们想要添加另一个节点作为 Sprite2D 的同级节点。为此,请右键单击 Node2D,然后选择“添加子节点”。
寻找并添加 Button 节点。
该节点默认比较小。在视口中,点击并拖拽该按钮右下角的手柄来调整大小。
如果看不到手柄,请确保工具栏中的选择工具处于活动状态。
点击并拖拽按钮使其更接近精灵。
你可以通过修改检查器中的 Text 属性来给 Button 上写一个标签。请输入Toggle motion
。
你的场景树和视口应该是类似这样的。
如果你还没保存场景的话,保存新建的场景为 node_2d.tscn
。然后你就可以使用 F6`(macOS 则为 :kbd:`Cmd + R)来运行。此时,你可以看到按钮,但是按下之后不会有任何反应。
在编辑器中连接信号
然后,我们希望将按钮的“pressed”信号连接到我们的 Sprite2D,并且我们想要调用一个新函数来打开和关闭其运动。我们需要像我们在上一课中所做的操作一样,将一个脚本附加到 Sprite2D 节点。
你可以在“节点”面板中连接信号。选择 Button 节点,然后在编辑器的右侧,单击检查器旁边名为“节点”的选项卡。
停靠栏显示所选节点上可用的信号列表。
双击“pressed”信号,打开节点连接窗口。
然后,你可以将信号连接到 Sprite2D 节点。该节点需要一个用于接收按钮信号的函数,当按钮发出信号时,Godot 将调用该函数。编辑器会为你生成一个。按照规范,我们将这些回调方法命名为”_on_node_name_signal_name”。在这里,它被命名为”_on_button_pressed”。
备注
通过编辑器的节点面板连接信号时,可以使用两种模式。简单的一个只允许你连接到附加了脚本的节点,并在它们上面创建一个新的回调函数。
你可以在高级视图中连接到任何节点和任何内置函数、向回调添加参数、设置选项。你可以单击窗口右下角的“高级”按钮来切换模式。
单击“连接”按钮以完成信号连接并跳转到脚本工作区。你应该会看到新方法,并在左边距中带有连接图标。
如果单击该图标,将弹出一个窗口并显示有关连接的信息。此功能仅在编辑器中连接节点时可用。
让我们用代码替换带有 pass
关键字的一行,以切换节点的运动。
我们的 Sprite2D 由于 _process()
函数中的代码而移动。Godot 提供了一种打开和关闭处理的方法:Node.set_process() 。Node 的另一个方法 is_processing()
,如果空闲处理处于活动状态,则返回 true
。我们可以使用 not
关键字来反转该值。
GDScriptC#
func _on_button_pressed():
set_process(not is_processing())
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
此函数将切换处理,进而切换按下按钮时图标的移动。
在尝试游戏之前,我们需要简化 _process()
函数,以自动移动节点,而不是等待用户输入。将其替换为以下代码,这是我们在两课前看到的代码:
GDScriptC#
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
代码应该是类似下面这样的。
GDScriptC#
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;
}
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
}
运行该场景,然后点击按钮,就可以看到精灵开始或停止运行。
用代码连接信号
你可以通过代码连接信号,而不是使用编辑器。这在脚本中创建节点或实例化场景时是必需的。
让我们在这里使用一个不同的节点。Godot 有一个 Timer 节点,可用于实现技能冷却时间、武器重装等。
回到 2D 工作区。你可以点击窗口顶部的“2D”字样,或者按 Ctrl + F1(macOS 上则是 Ctrl + Cmd + 1)。
在“场景”面板中,右键点击 Sprite2D 节点并添加新的子节点。搜索 Timer 并添加对应节点。你的场景现在应该类似这样。
选中 Timer 节点,在“检查器”中勾选 Autostart 属性。
点击 Sprite2D 旁的脚本图标,返回脚本工作区。
我们需要执行两个操作来通过代码连接节点:
从 Sprite2D 获取 Timer 的引用。
通过 Timer 的“timeout”信号调用
connect()
方法。
备注
要使用代码来连接信号,你需要调用所需监听节点信号的 connect()
方法。这里我们要监听的是 Timer 的“timeout”信号。
我们想要在场景实例化时连接信号,我们可以使用 Node._ready() 内置函数来实现这一点,当节点完全实例化时,引擎会自动调用该函数。
为了获取相对于当前节点的引用,我们使用方法 Node.get_node()。我们可以将引用存储在变量中。
GDScriptC#
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。
GDScriptC#
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()``。让我们将其定义添加到脚本的底部,并使用它来切换 sprite 的可见性。
备注
按照惯例,我们将这些回调方法在 GDScript 中命名为“_on_node_name_signal_name”,在 C# 中命名为“OnNodeNameSignalName”。故此处的GDScript 为“_on_timer_timeout”,C# 为“OnTimerTimeout()”。
GDScriptC#
func _on_timer_timeout():
visible = not visible
private void OnTimerTimeout()
{
Visible = !Visible;
}
visible
属性是一个布尔值,用于控制节点的可见性。visible = not visible
行切换该值。如果 visible
是 true
,它就会变成 false
,反之亦然。
如果你现在运行场景,就会看到精灵在闪啊闪的,间隔为一秒。
完整脚本
这就是我们小小的 Godot 图标移动闪烁演示了!这是完整的 sprite_2d.gd
文件,仅供参考。
GDScriptC#
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;
}
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
private void OnTimerTimeout()
{
Visible = !Visible;
}
}
自定义信号
备注
本节介绍的是如何定义并使用你自己的信号,不依赖之前课程所创建的项目。
你可以在脚本中定义自定义信号。例如,假设你希望在玩家的生命值为零时通过屏幕显示游戏结束。为此,当他们的生命值达到 0 时,你可以定义一个名为“died”或“health_depleted”的信号。
GDScriptC#
extends Node2D
signal health_depleted
var health = 10
using Godot;
public partial class MyNode2D : Node2D
{
[Signal]
public delegate void HealthDepletedEventHandler();
private int _health = 10;
}
备注
由于信号表示刚刚发生的事件,我们通常在其名称中使用过去时态的动作动词。
自定义信号的工作方式与内置信号相同:它们显示在“节点”选项卡中,你可以像连接其他信号一样连接到它们。
要通过代码发出信号,请调用信号的 emit()
方法。
GDScriptC#
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);
}
}
信号还可以选择声明一个或多个参数。在括号之间指定参数的名称:
GDScriptC#
extends Node
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()
函数的额外参数:
GDScriptC#
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 游戏,使用目前为止学到的东西进行实战。
© 版权所有 2014-present Juan Linietsky, Ariel Manzur and the Godot community (CC BY 3.0). Revision b1c660f7
.
Built with Sphinx using a theme provided by Read the Docs.