Синглтоны (Автозагрузка)

Введение

Система сцен Godot, будучи мощной и гибкой, имеет недостаток: отсутствует способ хранения информации (например, счет игрока или инвентарь), которая необходима для нескольких сцен.

Можно решить эту проблему с помощью некоторых обходных путей, но у них есть свои ограничения:

  • Вы можете использовать "мастер-сцену", которая загружает и выгружает другие сцены в качестве своих детей. Однако это означает, что вы больше не можете запускать эти сцены по отдельности и ожидать от них корректной работы.

  • Информация может храниться на диске в user://, а затем загружаться сценами, которые требуют этого, но увы сохранение и загрузка данных часто является громоздкой и может быть медленной.

Шаблон Singleton - полезный инструмент для решения распространенного варианта использования, когда вам необходимо хранить постоянную информацию между сценами. В нашем случае можно повторно использовать одну и ту же сцену или класс для нескольких синглтонов, если они имеют разные имена.

Используя эту концепцию, вы можете создавать объекты, которые:

  • Всегда загружаются, независимо от того, какая сцена в данный момент запущена.

  • Могут хранить глобальные переменные, такие как информация об игроке.

  • Могут обрабатывать переключения между сценами и переходы между сценами.

  • Действуют как синглтон, так как GDScript не поддерживает глобальные переменные по дизайну.

Узлы и скрипты автоматической загрузки могут дать нам эти характеристики.

Примечание

Godot не сделает AutoLoad "истинным" синглтоном в соответствии с шаблоном проектирования синглтона. При желании он может быть использован пользователем более одного раза.

Автозагрузка

Вы можете создать AutoLoad для загрузки сцены или скрипта, унаследованного от Node.

Примечание

При автозагрузке скрипта будет создан Node, и скрипт будет прикреплен к нему. Этот узел будет добавлен к корневому окну просмотра перед загрузкой любых других сцен.

../../_images/singleton.png

Чтобы автоматически загрузить сцену или скрипт, выберите Project > Project Settings в меню и перейдите на вкладку AutoLoad.

../../_images/autoload_tab.png

Здесь вы можете добавить любое количество сцен или скриптов. Каждая запись в списке требует имя, которое присваивается свойству узла name. Порядок расположения записей в дереве глобальной сцены может быть изменен с помощью клавиш со стрелками вверх/вниз.

../../_images/autoload_example.png

Это означает, что любой узел может получить доступ к синглтону с именем "PlayerVariables":

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

Если столбец Enable отмечен (по умолчанию), то к синглтону можно получить доступ напрямую, не требуя get_node():

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

Обратите внимание, что доступ к объектам автозагрузки (скриптам и/или сценам) осуществляется так же, как и к любому другому узлу дерева сцен. На самом деле, если вы посмотрите на дерево выполняющихся сцен, вы увидите, что появились узлы автозагрузки:

../../_images/autoload_runtime.png

Пользовательский переключатель сцены

В этом руководстве будет продемонстрировано создание переключателя сцен с использованием автозагрузки. Для базового переключения сцен вы можете использовать метод SceneTree.change_scene() (подробности см. в Дерево сцены). Однако, если вам нужно более сложное поведение при смене сцен, этот метод предоставляет больше функциональных возможностей.

Для начала скачайте шаблон здесь: autoload.zip и откройте его в Godot.

Проект содержит две сцены: Scene1.tscn and Scene2.tscn. Каждая сцена содержит Label, отображающий название сцены и кнопку с подключенным сигналом 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 являются дочерними корня, но узлы автозагрузки всегда загружаются первыми. Это означает, что последним дочерним элементом корня всегда будет загруженная сцена.

extends Node

var current_scene = null

func _ready():
    var root = get_tree().get_root()
    current_scene = root.get_child(root.get_child_count() - 1)
using Godot;
using System;

public class Global : Godot.Node
{
    public Node CurrentScene { get; set; }

    public override void _Ready()
    {
        Viewport root = GetTree().GetRoot();
        CurrentScene = root.GetChild(root.GetChildCount() - 1);
    }
}

Теперь нам нужна функция для смены сцены. Эта функция должна освободить (очистить) текущую сцену и заменить ее на нужную нам.

func goto_scene(path):
    # This function will usually be called from a signal callback,
    # or some other function in the current scene.
    # Deleting the current scene at this point is
    # a bad idea, because it may still be executing code.
    # This will result in a crash or unexpected behavior.

    # The solution is to defer the load to a later time, when
    # we can be sure that no code from the current scene is running:

    call_deferred("_deferred_goto_scene", path)


func _deferred_goto_scene(path):
    # It is now safe to remove the current scene
    current_scene.free()

    # Load the new scene.
    var s = ResourceLoader.load(path)

    # Instance the new scene.
    current_scene = s.instance()

    # Add it to the active scene, as child of root.
    get_tree().get_root().add_child(current_scene)

    # Optionally, to make it compatible with the SceneTree.change_scene() API.
    get_tree().set_current_scene(current_scene)
public void GotoScene(string path)
{
    // This function will usually be called from a signal callback,
    // or some other function from the current scene.
    // Deleting the current scene at this point is
    // a bad idea, because it may still be executing code.
    // This will result in a crash or unexpected behavior.

    // The solution is to defer the load to a later time, when
    // we can be sure that no code from the current scene is running:

    CallDeferred(nameof(DeferredGotoScene), path);
}

public void DeferredGotoScene(string path)
{
    // It is now safe to remove the current scene
    CurrentScene.Free();

    // Load a new scene.
    var nextScene = (PackedScene)GD.Load(path);

    // Instance the new scene.
    CurrentScene = nextScene.Instance();

    // Add it to the active scene, as child of root.
    GetTree().GetRoot().AddChild(CurrentScene);

    // Optionally, to make it compatible with the SceneTree.change_scene() API.
    GetTree().SetCurrentScene(CurrentScene);
}

Используя Object.call_deferred(), вторая функция будет запускаться только после завершения всего кода из текущей сцены. Таким образом, текущая сцена не будет удалена, пока она все еще используется (то есть ее код все еще выполняется).

Наконец, нам нужно заполнить пустые функции обратного вызова в двух сценах:

# Add to 'Scene1.gd'.

func _on_Button_pressed():
    Global.goto_scene("res://Scene2.tscn")
// Add to 'Scene1.cs'.

public void OnButtonPressed()
{
    var global = GetNode<Global>("/root/Global");
    global.GotoScene("res://Scene2.tscn");
}

и

# Add to 'Scene2.gd'.

func _on_Button_pressed():
    Global.goto_scene("res://Scene1.tscn")
// Add to 'Scene2.cs'.

public void OnButtonPressed()
{
    var global = GetNode<Global>("/root/Global");
    global.GotoScene("res://Scene1.tscn");
}

Запустите проект и проверьте, что вы можете переключаться между сценами, нажав кнопку.

Примечание

Когда сцены маленькие, переход происходит мгновенно. Однако, если ваши сцены более сложные, для их отображения может потребоваться значительное время. Чтобы узнать, как с этим справиться, см. следующий учебник: Background loading.

В качестве альтернативы, если время загрузки относительно невелико (менее 3 секунд или около того), вы можете отобразить "табличку загрузки", показав какой-либо 2D-элемент непосредственно перед изменением сцены. Затем вы можете скрыть его сразу после изменения сцены. Это можно использовать, чтобы указать игроку, что сцена загружается.