Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Logik-Präferenzen

Haben Sie sich jemals gefragt, ob man sich Problem X mit Strategie Y oder Z nähern sollte? Dieser Artikel behandelt eine Vielzahl von Themen im Zusammenhang mit diesen Dilemmas.

Nodes hinzufügen und Propertys bearbeiten: Welches zuerst?

Wenn Sie Nodes zur Laufzeit aus einem Skript initialisieren, müssen Sie möglicherweise Propertys wie den Namen oder die Position des Nodes ändern. Ein häufiges Dilemma ist die Frage, wann Sie diese Werte ändern sollten.

Es ist bewährte Praxis, Werte an einem Node zu ändern, bevor man ihn zum Szenenbaum hinzufügt. Die Setter einiger Propertys enthalten Code, um andere entsprechende Werte zu aktualisieren, und dieser Code kann langsam sein! In den meisten Fällen hat dieser Code keinen Einfluss auf die Performance Ihres Spiels, aber in schweren Anwendungsfällen wie der prozeduralen Generierung kann er Ihr Spiel zum Stillstand bringen.

Aus diesen Gründen ist es immer bewährte Praxis, die Anfangswerte eines Nodes festzulegen, bevor er dem Szenenbaum hinzugefügt wird.

Load vs. Preload

In GDScript gibt es die globale Methode preload. Sie lädt Ressourcen so früh wie möglich um die "Lade"-Operationen im Voraus zu auszuführen und somit das Laden von Ressourcen inmitten von leistungsintensivem Code zu vermeiden.

Ihr Gegenstück, die Methode load, lädt eine Ressource erst, wenn sie die Ladeanweisung erreicht. Das heißt, sie lädt eine Ressource "in-place", was zu Verlangsamungen führen kann, wenn dies mitten in empfindlichen Prozessen geschieht. Die Funktion load() ist auch ein Alias für ResourceLoader.load(path), der für alle Skriptsprachen zugänglich ist.

Wann genau erfolgt das Preloading im Vergleich zum Loading und wann sollte man welches davon verwenden? Sehen wir uns ein Beispiel an:

# my_buildings.gd
extends Node

# Note how constant scripts/scenes have a different naming scheme than
# their property variants.

# This value is a constant, so it spawns when the Script object loads.
# The script is preloading the value. The advantage here is that the editor
# can offer autocompletion since it must be a static path.
const BuildingScn = preload("res://building.tscn")

# 1. The script preloads the value, so it will load as a dependency
#    of the 'my_buildings.gd' script file. But, because this is a
#    property rather than a constant, the object won't copy the preloaded
#    PackedScene resource into the property until the script instantiates
#    with .new().
#
# 2. The preloaded value is inaccessible from the Script object alone. As
#    such, preloading the value here actually does not benefit anyone.
#
# 3. Because the user exports the value, if this script stored on
#    a node in a scene file, the scene instantiation code will overwrite the
#    preloaded initial value anyway (wasting it). It's usually better to
#    provide null, empty, or otherwise invalid default values for exports.
#
# 4. It is when one instantiates this script on its own with .new() that
#    one will load "office.tscn" rather than the exported value.
@export var a_building : PackedScene = preload("office.tscn")

# Uh oh! This results in an error!
# One must assign constant values to constants. Because `load` performs a
# runtime lookup by its very nature, one cannot use it to initialize a
# constant.
const OfficeScn = load("res://office.tscn")

# Successfully loads and only when one instantiates the script! Yay!
var office_scn = load("res://office.tscn")

Durch das Preloading kann das Skript den gesamten Ladevorgang in dem Moment ausführen, in dem das Skript selbst geladen wird. Das Preloading ist nützlich, aber es gibt auch Zeiten, in denen man es nicht wünscht. Um diese Situationen zu unterscheiden, gibt es einige Dinge, die man berücksichtigen kann:

  1. Wenn man nicht bestimmen kann, wann das Skript geladen wird, so wird das Preloading einer Ressource, insbesondere einer Szene oder eines Skripts, eventuell zu weiteren Ladevorgängen führen, die man nicht erwartet. Daraus können unbeabsichtigte Ladezeiten unterschiedlicher Länge zusätzlich zu den Ladevorgängen des ursprünglichen Skripts entstehen.

  2. Wenn etwas anderes den Wert ersetzen könnte (wie die exportierte Initialisierung einer Szene), hat das Preloading des Werts keine Bedeutung. Dieser Punkt ist nicht von Bedeutung, wenn man beabsichtigt, das Skript immer selbst zu erstellen.

  3. Wenn man nur eine andere Klassenressource (Skript oder Szene) "importieren" möchte, ist die Verwendung einer vorher geladenen Konstante oft die beste Vorgehensweise. In Ausnahmefällen sollte man dies jedoch nicht tun:

    1. Wenn sich die 'importierte' Klasse ändern kann, dann sollte sie stattdessen eine Property sein, die entweder mit export oder load() initialisiert wird (und vielleicht sogar erst später initialisiert wird).

    2. Wenn das Skript sehr viele Abhängigkeiten erfordert und man nicht so viel Speicher verbrauchen möchte, kann man verschiedene Abhängigkeiten zur Laufzeit laden und entladen, wenn sich die Umstände ändern. Wenn man Ressourcen in Konstanten per Preloading lädt, wäre die einzige Möglichkeit, diese Ressourcen zu entladen, das Entladen des gesamten Skripts. Wenn es sich stattdessen um geladene Propertys handelt, kann man sie auf null setzen und alle Referenzen auf die Ressource vollständig entfernen (was bei Ressourcen als einem von RefCounted abgeleiteten Typ dazu führt, dass sie sich selbst aus dem Speicher löschen).

Große Levels: statisch vs. dynamisch

Welche Umstände sind am besten geeignet, wenn ein großes Level erstellt wird? Sollten sie das Level als einen statischen Raum erstellen? Oder sollten sie das Level in Stücken laden und den Inhalt der Welt nach Bedarf verschieben?

Nun, die einfache Antwort lautet: "wenn die Performance es erfordert". Das Dilemma, das mit den beiden Optionen verbunden ist, ist eine der uralten Programmierentscheidungen: Optimiert man den Speicher gegenüber der Laufzeit oder umgekehrt?

Die naive Antwort besteht darin, eine statische Ebene zu verwenden, die alles auf einmal lädt. Dies kann jedoch je nach Projekt viel Speicherplatz beanspruchen. Die Vergeudung von Arbeitsspeicher führt dazu, dass Programme langsam laufen oder sogar abstürzen, weil sich der Computer gleichzeitig mit anderen Aufgaben beschäftigt.

Egal was passiert, man sollte größere Szenen in kleinere aufteilen (um die Wiederverwendbarkeit von Assets zu erleichtern). Entwickler können dann einen Node entwerfen, der das Erstellen/Laden und Löschen/Entladen von Ressourcen und Nodes in Echtzeit verwaltet. Spiele mit großen und unterschiedlichen Umgebungen oder prozedural generierten Elementen implementieren diese Strategien häufig, um Speicherverschwendung zu vermeiden.

Auf der anderen Seite ist das entwickeln eines dynamischen Systems komplexer, d.h. es wird mehr programmierte Logik verwendet, was zu Fehlermöglichkeiten und Bugs führt. Wenn man nicht aufpasst, kann man ein System entwickeln, das die technischen Probleme der Anwendung weiter aufbläht.

Die besten Optionen wären demnach...

  1. Statische Levels für kleinere Spiele zu verwenden.

  2. Wenn Sie die Zeit/Ressourcen für ein mittleres/großes Spiel haben, erstellen Sie eine Bibliothek oder ein Plugin, das die Verwaltung von Nodes und Ressourcen übernehmen kann. Wenn es im Laufe der Zeit verfeinert wird, um die Benutzerfreundlichkeit und Stabilität zu verbessern, könnte es sich projektübergreifend zu einem zuverlässigen Tool entwickeln.

  3. Entwerfen Sie die dynamische Logik für ein mittleres/großes Spiel, wenn Sie über diese Programmierfähigkeiten verfügen, jedoch nicht über die Zeit oder die Ressourcen, um den Code zu verfeinern (das Spiel muss fertig werden). Dies könnte möglicherweise später umgestaltet werden, um den Code in ein Plugin auszulagern.

Ein Beispiel für die verschiedenen Möglichkeiten, wie man Szenen zur Laufzeit austauschen kann, finden Sie in der Dokumentation "Szenen manuell ändern".