Singletons (Carregamento Automático)

Introdução

O sistema de cenas de Godot, embora poderoso e flexível, tem uma desvantagem: não existe um método para armazenar informações (por exemplo, pontuação ou inventário de um jogador) que seja necessário em mais de uma cena.

É possível resolver isso com algumas soluções alternativas, mas elas vêm com suas próprias limitações:

  • Embora seja possível que uma cena carregue e descarregue outras cenas como seus filhos para armazenar informações comuns a essas cenas filhas, não será mais possível executar essas cenas sozinhas e esperar que elas funcionem corretamente.
  • Embora a informação possa ser armazenada no disco em ``user: // `` e esta informação possa ser carregada por cenas que a exijam, salvar e carregar continuamente esses dados ao alterar cenas é trabalhoso e pode ser lento.

O Padrão Singleton é uma ferramenta útil para se usar quando é necessário armazenar informações entre cenas. No nosso caso é possível reusar a mesma cena ou classe para múltiplos singletons, contanto que eles tenham diferentes nomes.

Usando esse conceito, você pode criar objetos que:

  • São sempre carregados, não importa qual cena seja aberta no editor
  • Pode armazenar variáveis globais, como informações do jogador, itens, dinheiro etc. e compartilhar informações entre cenas
  • Pode lidar com troca de cenas e transições entre cenas
  • Atua como um singleton, já que o GDScript não suporta variáveis globais por projeto

Carregamento automático de nós e scripts atendem a essa necessidade.

O AutoLoad

Você pode usar o AutoLoad para carregar uma cena ou um script que herda da Classe Node. Nota: ao carregar automaticamente um script, um nó será criado e o script pode referenciá-lo. Este nó será adicionado à viewport raiz antes que qualquer outra cena seja carregada.

../../_images/singleton.png

Para carregar automaticamente uma cena ou script, selecione no menu Projeto -> Configurações do Projeto e mude para a aba “AutoLoad”.

../../_images/autoload_tab.png

Aqui você pode adicionar qualquer número de cenas ou scripts. Cada entrada na lista requer um nome, designada como a propriedade nome do nó. A ordem em que as entradas são adicionadas à árvore da cena global pode ser manipulada usando as setas para cima e para baixo.

../../_images/autoload_example.png

Isso significa que qualquer nó pode acessar um singleton chamado “PlayerVariables” com:

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

Se a coluna “Enable” estiver marcada (default true), então o singleton pode simplesmente ser acessado diretamente:

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

Note que objetos carregados automaticamente (scripts e/ou cenas) são acessadas como qualquer outro nó na árvore de cenas. Se você olhar numa cena executando, você verá os nós carregados automaticamente aparecerem:

../../_images/autoload_runtime.png

Comutador de cena personalizado

Este tutorial explicará como fazer um trocador de cena usando o autoload. Para troca de cena simples, o método SceneTree.change_scene() é suficiente (descrito em Árvore de cena). Mas se você precisar de comportamento mais complexo ao mudar de cenas, esse método fornece mais funcionalidade.

Primeiro faça o download do modelo aqui: autoload.zip e abra-o com Godot.

O projeto contém duas cenas: Scene1.tscn e Scene2.tscn. Cada cena contém um Label exibindo o nome da cena e um botão com seu sinal pressed() conectado. Quando você executar o projeto, ele começará em Scene1.tscn. No entanto, ao pressionar o botão, nada acontecerá.

Global.gd

Mude para a aba “Script” e crie um novo script chamado Global.gd. Tenha certeza que ele herda de :

../../_images/autoload_script.png

O próximo passo é adicionar este script à lista de AutoLoad. Clique em Projeto > Configurações de Projeto no menu, mude para a guia “Carregamento Automático” e selecione o script clicando no botão de procurar ou digitando seu caminho: res://Global.gd. Aperte “Adicionar” para adicioná-lo à lista de autoload:

../../_images/autoload_tutorial1.png

Agora, todas as vezes que você executar qualquer uma das suas cenas, o script sempre será carregado.

Voltando ao nosso script, a cena atual precisa ser usada na função _ready(). Tanto a cena atual (aquela com o botão) quanto global.gd são filhos do nó raiz, mas os nós carregados automaticamente são sempre os primeiros. Isto significa que o último filho da raiz é sempre a cena carregada.

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);
    }
}

Em seguida vem a função para trocar de cena. Esta função libera a cena atual e a substitui pela cena requisitada.

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);
}

Usando Object.call_deferred(), a segunda função irá somente rodar uma vez que todo o código dessa cena has completed. Thus, the current scene will not be removed while it is still being used (i.e. its code is still running).

Finalmente, tudo que resta é completar as funções vazias nas duas cenas:

# Add to 'Scene1.gd'.

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

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

e

# Add to 'Scene2.gd'.

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

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

Rode o projeto e veja se você consegue trocar de cena apertando o botão.

Nota: Quando cenas são pequenas, a transição será instantânea. No entanto, se as cenas são mais complexas, elas levarão um tempo significativo para aparecer. Para aprender a gerenciar isto, veja o próximo tutorial: Background loading