编写脚本(续)

处理

Godot中的多个动作是由回调或虚函数触发的,因此无需编写始终运行的代码。

但是,在每一帧上都需要处理脚本仍然很常见。有两种处理类型:空闲处理和物理处理。

当在脚本中找到 Node._process() 方法时,将激活空闲处理。可以通过 Node.set_process() 函数来打开或关闭。

这个方法将在每次绘制帧时被调用:

GDScript

C#

  1. func _process(delta):
  2. # Do something...
  3. pass
  1. public override void _Process(float delta)
  2. {
  3. // Do something...
  4. }

重要的是要记住,调用 _process() 的频率取决于应用程序运行的每秒帧数(FPS)。该速率会随着时间和设备的不同而变化。

To help manage this variability, the delta parameter contains the time elapsed in seconds as a floating-point number since the previous call to _process().

此参数可用于确保事物始终花费相同的时间,从而与游戏的FPS无关。

For example, movement is often multiplied with a time delta to make movement speed both constant and independent of the frame rate.

_physics_process() 进行物理处理是相似的,但应将其用于必须在每个物理步骤之前进行的处理,例如控制角色。它始终在物理步骤之前运行,并且以固定的时间间隔调用:默认为每秒60次。您可以从“项目设置”的“物理”->“通用”->“物理Fps”下更改时间间隔。

然而,函数 _process() 不与物理同步。它的帧率不是恒定的,并且取决于硬件和游戏优化。在单线程游戏中它的执行是在的物理步骤之后完成的。

在作品中查看 _process() 函数的一种简单方法是创建具有单个Label节点的且带有以下脚本的场景:

GDScript

C#

  1. extends Label
  2. var accum = 0
  3. func _process(delta):
  4. accum += delta
  5. text = str(accum) # 'text' is a built-in label property.
  1. public class CustomLabel : Label
  2. {
  3. private float _accum;
  4. public override void _Process(float delta)
  5. {
  6. _accum += delta;
  7. Text = _accum.ToString(); // 'Text' is a built-in label property.
  8. }
  9. }

这将显示一个每帧增加的计数器。

分组

Godot中的编组的工作方式类似于您可能在其他软件中遇到的标记。一个节点可以根据需要添加到任意多个编组。这是组织大型场景的一个有用特性。有两种方法可以向编组中添加节点。第一个是从UI,使用 节点 面板下的 Groups 按钮:

../../_images/groups_in_nodes.png

第二种方法是从代码。下面的脚本会在当前节点出现在场景树中后立即将其添加到 enemies 编组中。

GDScript

C#

  1. func _ready():
  2. add_to_group("enemies")
  1. public override void _Ready()
  2. {
  3. base._Ready();
  4. AddToGroup("enemies");
  5. }

这样,如果发现玩家潜入秘密基地,则可以使用 SceneTree.call_group() 发出警报声来通知所有敌人:

GDScript

C#

  1. func _on_discovered(): # This is a purely illustrative function.
  2. get_tree().call_group("enemies", "player_was_discovered")
  1. public void _OnDiscovered() // This is a purely illustrative function.
  2. {
  3. GetTree().CallGroup("enemies", "player_was_discovered");
  4. }

上面的代码在 enemies 编组的每个成员上调用函数 player_was_discovered

也可以通过调用 SceneTree.get_nodes_in_group() 获得 enemies 节点的完整列表:

GDScript

C#

  1. var enemies = get_tree().get_nodes_in_group("enemies")
  1. var enemies = GetTree().GetNodesInGroup("enemies");

SceneTree 类提供了许多有用的方法,例如与场景、其节点层次结构、及节点编组交互。它使您可以轻松切换场景或重新加载场景,退出游戏或暂停和取消暂停游戏。它还有一些有趣的信号,如果您有空可以详细去查看!

通知

Godot 有一个通知系统。这个是非常底层的虚函数,通常不需要放入脚本代码里。只需要知道有这个系统的存在。例如,您可以在您的脚本里添加 Object._notification() 函数:

GDScript

C#

  1. func _notification(what):
  2. match what:
  3. NOTIFICATION_READY:
  4. print("This is the same as overriding _ready()...")
  5. NOTIFICATION_PROCESS:
  6. print("This is the same as overriding _process()...")
  1. public override void _Notification(int what)
  2. {
  3. base._Notification(what);
  4. switch (what)
  5. {
  6. case NotificationReady:
  7. GD.Print("This is the same as overriding _Ready()...");
  8. break;
  9. case NotificationProcess:
  10. var delta = GetProcessDeltaTime();
  11. GD.Print("This is the same as overriding _Process()...");
  12. break;
  13. }
  14. }

:ref:`类参考手册 <toc-class-ref>`每个类的文档里都说明了它们能接收的通知。不过,GDScript在大多数情况下都提供了更简单的可重写函数。

可重载函数

如下所述的这些可重载函数,可以应用于节点:

GDScript

C#

  1. func _enter_tree():
  2. # When the node enters the Scene Tree, it becomes active
  3. # and this function is called. Children nodes have not entered
  4. # the active scene yet. In general, it's better to use _ready()
  5. # for most cases.
  6. pass
  7. func _ready():
  8. # This function is called after _enter_tree, but it ensures
  9. # that all children nodes have also entered the Scene Tree,
  10. # and became active.
  11. pass
  12. func _exit_tree():
  13. # When the node exits the Scene Tree, this function is called.
  14. # Children nodes have all exited the Scene Tree at this point
  15. # and all became inactive.
  16. pass
  17. func _process(delta):
  18. # This function is called every frame.
  19. pass
  20. func _physics_process(delta):
  21. # This is called every physics frame.
  22. pass
  1. public override void _EnterTree()
  2. {
  3. // When the node enters the Scene Tree, it becomes active
  4. // and this function is called. Children nodes have not entered
  5. // the active scene yet. In general, it's better to use _ready()
  6. // for most cases.
  7. base._EnterTree();
  8. }
  9. public override void _Ready()
  10. {
  11. // This function is called after _enter_tree, but it ensures
  12. // that all children nodes have also entered the Scene Tree,
  13. // and became active.
  14. base._Ready();
  15. }
  16. public override void _ExitTree()
  17. {
  18. // When the node exits the Scene Tree, this function is called.
  19. // Children nodes have all exited the Scene Tree at this point
  20. // and all became inactive.
  21. base._ExitTree();
  22. }
  23. public override void _Process(float delta)
  24. {
  25. // This function is called every frame.
  26. base._Process(delta);
  27. }
  28. public override void _PhysicsProcess(float delta)
  29. {
  30. // This is called every physics frame.
  31. base._PhysicsProcess(delta);
  32. }

如前所述,最好使用这些函数代替通知系统。

创建节点

要通过代码创建节点,请像其他任何基于类的数据类型一样,调用 .new() 方法。 例如:

GDScript

C#

  1. var s
  2. func _ready():
  3. s = Sprite.new() # Create a new sprite!
  4. add_child(s) # Add it as a child of this node.
  1. private Sprite _sprite;
  2. public override void _Ready()
  3. {
  4. base._Ready();
  5. _sprite = new Sprite(); // Create a new sprite!
  6. AddChild(_sprite); // Add it as a child of this node.
  7. }

要删除节点,无论是在场景内还是场景外,都必须使用 free()

GDScript

C#

  1. func _someaction():
  2. s.free() # Immediately removes the node from the scene and frees it.
  1. public void _SomeAction()
  2. {
  3. _sprite.Free(); // Immediately removes the node from the scene and frees it.
  4. }

当一个节点被释放时, 它也会释放其所有子节点。因此, 手动删除节点比看起来简单得多。释放基节点, 那么子树中的其他所有东西都会随之消失。

当我们要删除当前“阻塞”的节点时,可能会发生这种情况,因为该节点正在发出信号或正在调用函数。这会导致游戏崩溃。使用调试器运行Godot通常能捕获这种情况并向您发出警告。

删除节点的最安全方法是使用 Node.queue_free()。 这将在空闲期间安全地删除节点。

GDScript

C#

  1. func _someaction():
  2. s.queue_free() # Removes the node from the scene and frees it when it becomes safe to do so.
  1. public void _SomeAction()
  2. {
  3. _sprite.QueueFree(); // Removes the node from the scene and frees it when it becomes safe to do so.
  4. }

实例化场景

从代码实例化场景分两个步骤完成。第一步是从硬盘驱动器加载场景:

GDScript

C#

  1. var scene = load("res://myscene.tscn") # Will load when the script is instanced.
  1. var scene = GD.Load<PackedScene>("res://myscene.tscn"); // Will load when the script is instanced.

预加载可以更方便,因为它是在解析时发生的(仅适用于GDScript):

GDScript

  1. var scene = preload("res://myscene.tscn") # Will load when parsing the script.

但是 scene 还不是一个节点。它被打包在一个称为 PackedScene 的特殊资源中。要想创建实际的节点,就必须调用函数 PackedScene.instance()。这将返回可以添加到活动场景的节点树:

GDScript

C#

  1. var node = scene.instance()
  2. add_child(node)
  1. var node = scene.Instance();
  2. AddChild(node);

此两步过程的优点在于,打包的场景可以保持加载状态并可以随时使用,以便您可以根据需要创建尽可能多的实例。这对于在活动场景中快速实例化多个敌人、子弹、和其他实体特别有用。

将脚本注册为类

Godot有一个“脚本类”的功能,可以使用编辑器注册单个脚本。默认情况下,您只能通过直接加载文件访问未命名的脚本。

您可以命名脚本,并在编辑器中使用 class_name 关键字并后跟类名将其注册为类型。您可以添加逗号和可选的图像路径以用作图标。然后,您将在节点或资源创建对话框中找到您的新类型。

GDScript

  1. extends Node
  2. # Declare the class name here
  3. class_name ScriptName, "res://path/to/optional/icon.svg"
  4. func _ready():
  5. var this = ScriptName # reference to the script
  6. var cppNode = MyCppNode.new() # new instance of a class named MyCppNode
  7. cppNode.queue_free()

../../_images/script_class_nativescript_example.png

警告

在Godot 3.1中:

  • 只有GDScript和NativeScript,即C ++和其他GDNative支持的语言,可以注册脚本。
  • 只有GDScript为每个命名脚本创建全局变量。