单例(自动加载)

简介

Godot的场景系统虽然功能强大且灵活,但有一个缺点:没有一种方法可以用于存储多个场景所需的信息(例如 player 的分数或库存).

可以通过一些变通方法来解决此问题,但是它们有其自身的局限性:

  • 您可以使用”主”场景,加载和卸载其他场景作为其子级.但是,这意味着您不能再单独运行这些场景并期望它们正常工作.

  • 信息可以存储在 user:// 下的磁盘上,然后由需要它的场景加载,但是经常保存和加载数据很麻烦并且可能很慢.

单例模式 是一个解决需要在场景之间存储持久性信息的常见用例的实用工具.在我们的示例中,只要多个单例具有不同的名称,就可以复用相同的场景或类.

利用这个概念,你可以创建这样的对象:

  • 无论当前运行哪个场景,始终加载.

  • 可以存储全局变量,如玩家信息.

  • 可以处理切换场景和场景间过渡.

  • 行为 类似单例模式,因为GDScript在设计上不支持全局变量.

自动加载的节点和脚本可以为我们提供这些特征.

注解

根据单例设计模式,Godot不会使AutoLoad成为一个 “真正的” 单例.如果需要,它仍然可以被用户实例化多次.

自动加载

你可以创建一个AutoLoad来加载一个场景或一个继承自 Node 的脚本.

注解

您可以使用 自动加载(AutoLoad) 加载继承自 Node 的场景或脚本.注意:自动加载脚本时,将创建一个节点并将脚本附加到该节点.在加载任何其他场景之前,此节点将添加到根视图.

../../_images/singleton.png

要自动加载场景或脚本,请从菜单中选择 项目 -> 项目设置(Project -> Project Settings) ,然后切换到 自动加载(AutoLoad) 选项卡.

../../_images/autoload_tab.png

您可以在此处添加任意数量的场景或脚本.列表中的每个条目都需要一个名称,该名称被分配为节点的 name 属性.使用向上/向下箭头键可以操纵将条目添加到全局场景树时的顺序.

../../_images/autoload_example.png

这意味着,任何节点都可以访问并使用一个名为 “PlayerVariables” 的单例:

GDScript

C#

  1. var player_vars = get_node("/root/PlayerVariables")
  2. player_vars.health -= 10
  1. var playerVariables = (PlayerVariables)GetNode("/root/PlayerVariables");
  2. playerVariables.Health -= 10; // Instance field.

如果 Enable 列被选中(这是默认的),那么可以直接访问单例而不需要 get_node():

GDScript

C#

  1. PlayerVariables.health -= 10
  1. // Static members can be accessed by using the class name.
  2. PlayerVariables.Health -= 10;

请注意,就像场景树中的任何其他节点一样,访问自动加载对象(脚本和/或场景)的方式也是如此.实际上,如果查看正在运行的场景树,则会看到自动加载的节点出现:

../../_images/autoload_runtime.png

自定义场景切换器

本教程将演示如何使用自动加载功能构建场景切换器.可以将 SceneTree.change_scene() 方法(有关详细信息,请参见 Using SceneTree )用于基本的场景切换.但是,如果在更改场景时需要更复杂的行为,则此方法可提供更多功能.

首先,请从 autoload.zip 下载模板并在Godot中将其打开.

该项目包含两个场景: Scene1.tscnScene2.tscn.每个场景都包含一个显示场景名称的标签和一个连接了 pressed() 信号的按钮.当您运行该项目时,它将从 Scene1.tscn 开始.但是,按下按钮不会执行任何操作.

Global.gd

切换到 脚本(Script) 选项卡,并创建一个名为 Global.gd 的新脚本.确保它从 Node 继承:

../../_images/autoload_script.png

下一步是将此脚本添加到自动加载列表中.从菜单中打开 项目 > 项目设置(Project > Project Settings) ,切换到 自动加载(AutoLoad) 选项卡,然后单击文件浏览按钮或键入其路径: res://Global.gd .按 添加(Add) 将其添加到自动加载列表:

../../_images/autoload_tutorial1.png

现在,无论何时在项目中运行任何场景,该脚本都将始终加载.

返回到脚本,它需要在 _ready() 函数中获取当前场景.当前场景(带有按钮的场景)和 global.gd 都是 root 的子代,但是自动加载的节点始终是第一个.这意味着 root 的最后一个子项始终是加载的场景.

GDScript

C#

  1. extends Node
  2. var current_scene = null
  3. func _ready():
  4. var root = get_tree().get_root()
  5. current_scene = root.get_child(root.get_child_count() - 1)
  1. using Godot;
  2. using System;
  3. public class Global : Godot.Node
  4. {
  5. public Node CurrentScene { get; set; }
  6. public override void _Ready()
  7. {
  8. Viewport root = GetTree().GetRoot();
  9. CurrentScene = root.GetChild(root.GetChildCount() - 1);
  10. }
  11. }

现在我们需要一个用于更改场景的函数.此函数需要释放当前场景,并将其替换为请求的场景.

GDScript

C#

  1. func goto_scene(path):
  2. # This function will usually be called from a signal callback,
  3. # or some other function in the current scene.
  4. # Deleting the current scene at this point is
  5. # a bad idea, because it may still be executing code.
  6. # This will result in a crash or unexpected behavior.
  7. # The solution is to defer the load to a later time, when
  8. # we can be sure that no code from the current scene is running:
  9. call_deferred("_deferred_goto_scene", path)
  10. func _deferred_goto_scene(path):
  11. # It is now safe to remove the current scene
  12. current_scene.free()
  13. # Load the new scene.
  14. var s = ResourceLoader.load(path)
  15. # Instance the new scene.
  16. current_scene = s.instance()
  17. # Add it to the active scene, as child of root.
  18. get_tree().get_root().add_child(current_scene)
  19. # Optionally, to make it compatible with the SceneTree.change_scene() API.
  20. get_tree().set_current_scene(current_scene)
  1. public void GotoScene(string path)
  2. {
  3. // This function will usually be called from a signal callback,
  4. // or some other function from the current scene.
  5. // Deleting the current scene at this point is
  6. // a bad idea, because it may still be executing code.
  7. // This will result in a crash or unexpected behavior.
  8. // The solution is to defer the load to a later time, when
  9. // we can be sure that no code from the current scene is running:
  10. CallDeferred(nameof(DeferredGotoScene), path);
  11. }
  12. public void DeferredGotoScene(string path)
  13. {
  14. // It is now safe to remove the current scene
  15. CurrentScene.Free();
  16. // Load a new scene.
  17. var nextScene = (PackedScene)GD.Load(path);
  18. // Instance the new scene.
  19. CurrentScene = nextScene.Instance();
  20. // Add it to the active scene, as child of root.
  21. GetTree().GetRoot().AddChild(CurrentScene);
  22. // Optionally, to make it compatible with the SceneTree.change_scene() API.
  23. GetTree().SetCurrentScene(CurrentScene);
  24. }

使用 Object.call_deferred(),第二个函数将仅在当前场景中的所有代码完成后运行.因此,当前场景在仍在使用(即其代码仍在运行)时不会被删除.

最后,我们需要在两个场景中填充空的回调函数:

GDScript

C#

  1. # Add to 'Scene1.gd'.
  2. func _on_Button_pressed():
  3. Global.goto_scene("res://Scene2.tscn")
  1. // Add to 'Scene1.cs'.
  2. public void OnButtonPressed()
  3. {
  4. var global = (Global)GetNode("/root/Global");
  5. global.GotoScene("res://Scene2.tscn");
  6. }

以及

GDScript

C#

  1. # Add to 'Scene2.gd'.
  2. func _on_Button_pressed():
  3. Global.goto_scene("res://Scene1.tscn")
  1. // Add to 'Scene2.cs'.
  2. public void OnButtonPressed()
  3. {
  4. var global = (Global)GetNode("/root/Global");
  5. global.GotoScene("res://Scene1.tscn");
  6. }

运行该项目,并测试您可以通过按下按钮来切换场景.

注解

注意:当场景较小时,过渡是瞬时的.但是,如果您的场景比较复杂,则可能需要花费相当长的时间才能显示出来.要了解如何处理此问题,请参阅下一个教程: 后台加载.

另外,如果加载时间相对较短(少于3秒左右),你可以在改变场景之前,通过显示某种2D元素来显示一个 “加载中图标”,然后在改变场景后隐藏它.这能让玩家知道场景正在载入中.