Singletons (Entwurfsmuster) - AutoLoad

Einführung

Godot's Szenensystem, obwohl mächtig und flexibel, hat einen Nachteil: es gibt keine Möglichkeit Informationen zu speichern die in mehreren Szenen benötigt werden (z.B. ein Spielstand oder Inventar).

Es ist möglich dieses Problem anzugehen mit gewissen Zwischenlösungen, diese bieten jedoch auch Begrenzungen:

  • Es kann eine übergeordnete Szene verwendet werden die andere lädt und entlädt und als 'Kinder' behandelt. Dies bedeutet jedoch auch, dass diese Szenen nicht länger individuell ausgeführt werden können und eine korrekte Funktionsweise erwartet werden kann.
  • Informationen können auf der Festplatte unter Benutzer:// gespeichert und dann von jeglichen Szenen die diese benötigen geladen werden. Jedoch wirkt sich oftmals wiederholendes Speichern und Laden von Daten abwürgend aus und kann die Anwendung verlangsamen.

Das Singleton Muster ist ein nützliches Werkzeug um dauerhafte Informationen zwischen Szenen zu speichern. In diesem Fall ist es möglich die gleiche Szene oder Klasse für mehrere Einzelelemente zu nutzen, solange sie unterschiedliche Namen haben.

Durch die Verwendung dieses Konzepts wird es Ihnen ermöglicht, Objekte zu instanziieren die:

  • immer geladen werden, unabhängig davon welche Szene gerade abgespielt wird.
  • Globale Variablen abspeichern können, wie z.B. Spielerinformationen.
  • Zum umschalten von Szenen und wechseln zwischen Szenen.
  • verhält sich wie ein Singleton, da GDScript keine globalen Variablen unterstützt.

Automatisch ladende Nodes und Skripte können diese Eigenschaften haben.

Bemerkung

Godot macht ein AutoLoad nicht zu einem "echten" Singleton gemäß dem Singleton-Entwurfsmuster. Falls gewünscht, kann es vom Benutzer immer noch mehrmals instanziiert werden.

Autoload

Sie können ein AutoLoad erstellen, um eine Szene oder ein Skript zu laden, das von Node erbt.

Bemerkung

Beim automatischen Laden eines Skripts wird ein :ref:'class_Node' erstellt und das Skript daran angefügt. Dieses Node wird dem Stammansichtsfenster hinzugefügt, bevor weitere Szenen geladen werden.

../../_images/singleton.png

Um eine Szene oder ein Skript zu autoloaden, wählen Sie Projekt > Projekteinstellungen aus dem Menü aus und wechseln auf den AutoLoad-Reiter.

../../_images/autoload_tab.png

Hier kann man eine beliebige Anzahl an Szenen oder Skripten hinzufügen. Jeder Eintrag in der Liste benötigt einen Namen, der als ``name``des Nodes zugewiesen wird. Die Reihenfolge der Einträge, die zum globalen Szenenbaum hinzugefügt werden, kann durch die hoch/runter Pfeiltasten verändert werden.

../../_images/autoload_example.png

Das bedeutet, dass ein Node auf ein Singleton namens "SpielerVariablen" zugreifen kann mit:

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

Wenn die Spalte Enable markiert ist (was die Voreinstellung ist), dann kann auf das Singleton direkt zugegriffen werden, ohne dass get_node() erforderlich ist:

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

Beachte, dass auf automatisch geladene Objekte (Skripte und/oder Szenen) genauso zugegriffen wird wie auf jedes andere Node innerhalb des Szenenbaums. Wenn man sich also den aktuellen Szenenbaum ansieht, sieht man die automatisch geladenen Nodes erscheinen:

../../_images/autoload_runtime.png

Szenenwechsler

Diese Anleitung zeigt das Erstellen eines Szenenumschalters mithilfe von Autoloads. Für das grundlegende Wechseln der Szene kann man die Methode SceneTree.change_scene() verwenden (siehe Szenen-Baum for mehr Details). Wenn Sie jedoch beim Ändern von Szenen ein komplexeres Verhalten benötigen, bietet diese Methode mehr Funktionen.

Um zu beginnen, laden Sie die Vorlage von hier runter: autoload.zip und öffnen diese anschließend in Godot.

Das Projekt enthält zwei Szenen: Scene1.tscn und Scene2.tscn. Jede Szene enthält ein Label, dieses den Szenennamen anzeigt und einen Knopf der mit einem pressed() -Signal verbunden wurde.

Global.gd

Wechsel zum Script Reiter und erstelle ein neues Skript namens Global.gd. Stelle sicher, dass es von Node erbt:

../../_images/autoload_script.png

Der nächste Schritt ist, dieses Skript zur Autoloader-Liste hinzuzufügen. Öffnen Sie Projekt > Projekteinstellungen aus dem Menü, wechseln zum AutoLoad-Reiter und wählen das Skript aus durch das Klicken des "Suchen"-Knopfes, oder geben Sie den folgenden Pfad ein: res://Global.gd. Drücken Sie Hinzufügen um es der AutoLoader-Liste beizufügen:

../../_images/autoload_tutorial1.png

Ab jetzt würde bei jeder Ausführung des Projekts das Skript vorgeladen werden.

Um zum Skript zurückzukehren, muss es die aktuelle Szene in der Funktion _ready() abrufen. Sowohl die aktuelle Szene (die mit der Schaltfläche) als auch Global.gd sind Kinder von root, aber automatisch geladene Nodes sind immer zuerst. Das bedeutet, dass das letzte Kind von root immer die geladene Szene ist.

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

Als nächstes benötigen wir eine Funktion um die Szene zu modifizieren. Diese Funktion muss die aktuelle Szene leeren und diese mit der gewünschten ersetzen.

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

Durch Nutzung von:ref:`Object.call_deferred() <class_Object_method_call_deferred>`wird die zweite Funktion nur ausgeführt, wenn das komplette Programm der aktuellen Szene beendet ist. Daher wird die aktuelle Szene nicht gelöscht, solange sie noch genutzt wird (z.B. Programm läuft noch).

Schlussendlich müssen wir noch die leeren Callback-Funktionen in den zwei Szenen ausfüllen:

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

und

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

Führen Sie das Projekt aus und überprüfen, dass Sie durch die Betätigung des Knopfes zwischen den Szenen hin- und her springen können.

Bemerkung

Beachte: Wenn Szenen kurz sind, ist der Wechsel unverzüglich. Bei komplexeren Szenen, gibt es möglicherweise einen merkbaren Zeitverzug bis zum erscheinen. Wie dies gehandhabt wird ist nachfolgend erklärt: Laden im Hintergrund.

Wenn die Ladezeit relativ kurz ist (weniger als 3 Sekunden oder so), können Sie alternativ eine "Ladeplakette" anzeigen, indem Sie kurz vor dem Ändern der Szene eine Art 2D-Element anzeigen. Sie können es dann direkt nach dem Ändern der Szene ausblenden. Dies kann verwendet werden, um dem Spieler anzuzeigen, dass eine Szene geladen wird.