Logikeinstellungen

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 Dilemmata.

Laden im Vergleich zu Vorladen

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

Das Gegenstück, die Methode load lädt eine Ressource erst, wenn sie die load-Anweisung erreicht. Das heißt, es wird eine Ressource an Ort und Stelle geladen, was zu Verlangsamungen führen kann, wenn sie mitten in sensiblen Prozessen auftritt. Die Funktion load ist auch ein Alias für ResourceLoader.load(path) auf den alle Skriptsprachen zugreifen können.

Wann genau erfolgt das Vorladen im Vergleich zum Laden und wann sollte man eines 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(PackedScene) var a_building = 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")
using System;
using Godot;

// C# and other languages have no concept of "preloading".
public class MyBuildings : Node
{
    //This is a read-only field, it can only be assigned when it's declared or during a constructor.
    public readonly PackedScene Building = ResourceLoader.Load<PackedScene>("res://building.tscn");

    public PackedScene ABuilding;

    public override void _Ready()
    {
        // Can assign the value during initialization.
        ABuilding = GD.Load<PackedScene>("res://office.tscn");
    }
}

Durch das Vorladen kann das Skript den gesamten Ladevorgang in dem Moment ausführen, in dem das Skript selbst geladen wird. Das Vorladen 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 werden kann, kann das Vorladen einer Ressource, insbesondere einer Szene oder eines Skripts, zu weiteren Ladevorgängen führen, die man nicht erwartet. Dies kann zu unbeabsichtigten Ladezeiten variabler Länge zusätzlich zu den Ladevorgängen des ursprünglichen Skripts führen.
  2. Wenn etwas anderes den Wert ersetzen könnte (wie die exportierte Initialisierung einer Szene), hat das Vorladen des Werts keine Bedeutung. Dieser Punkt ist kein wesentlicher Faktor, wenn das Skript immer selbst erstellt werden soll.
  3. Wenn man nur eine andere Klassenressource (Skript oder Szene) "importieren" möchte, ist die Verwendung einer vorinstallierten Konstante oft die beste Vorgehensweise. In Ausnahmefällen möchte ich dies jedoch nicht tun:
    1. Wenn sich die 'importierte' Klasse ändern kann, sollte es sich stattdessen um eine Eigenschaft handeln, die entweder mit einem export oder einem load initialisiert wird (und möglicherweise 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 vorlädt, besteht die einzige Möglichkeit diese Ressourcen zu entladen darin, das gesamte Skript zu entladen. Wenn sie stattdessen geladene Eigenschaften sind, können sie auf null gesetzt und alle Verweise auf die Ressource vollständig entfernt werden (was als Referenz -erweiternder Typ dazu führt, dass die Ressourcen sich selbst aus dem Speicher löscht).

Große Levels: statisch im Vergleich zu dynamisch

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

Nun, die einfache Antwort lautet: "Wenn die Leistung es erfordert." Das mit den beiden Optionen verbundene Dilemma ist eine der uralten Entscheidungen beim Programmieren: soll man lieber die Speichernutzung vor der Geschwindigkeit optimieren, oder besser 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 Verschwendung des Arbeitsspeichers der Nutzer führt zu langsam ausgeführten Programmen oder diese stürzen sogar ab, bei dem was der Computer sonst noch gleichzeitig versucht.

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 Codieren 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.

Als solche wären die besten Optionen ...

  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 codieren 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. Codieren 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 erledigt 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 Szenen zur Laufzeit ausgetauscht werden können, finden Sie in der Dokumentation "Szenen manuell ändern".