Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Preferenze di logica
Ti sei mai chiesto se dovresti affrontare il problema X con la strategia Y o Z? Questo articolo affronta una varietà di argomenti riguardo questi dilemmi.
Aggiungere nodi e modificarne le proprietà: cosa fare prima?
Quando si inizializzano i nodi da uno script in fase di esecuzione, potrebbe essere necessario modificare proprietà come il nome o la posizione del nodo. Un dilemma comune è: quando è opportuno modificare questi valori?
La migliore pratica è modificare i valori su un nodo prima di aggiungerlo all'albero di scene. Alcuni setter delle proprietà hanno codice per aggiornare altri valori corrispondenti, e tale codice può essere lento! Nella maggior parte dei casi, questo codice non ha alcun impatto sulle prestazioni del gioco, ma in casi di uso intensivo come la generazione procedurale, può rallentare moltissimo un gioco.
Per queste ragioni, di solito è consigliabile impostare i valori iniziali di un nodo prima di aggiungerlo all'albero di scene. Esistono alcune eccezioni in cui i valori non possono essere impostati prima di essere aggiunti all'albero di scene, come impostare la posizione globale.
Caricamento (load) vs pre-caricamento (preload)
In GDScript, esiste il metodo globale preload. Esso carica le risorse il prima possibile per anticipare le operazioni di "caricamento" ed evitare di caricare risorse nel mezzo di codice sensibile alle prestazioni.
La sua controparte, il metodo load, carica una risorsa solo quando raggiunge l'istruzione di caricamento. In altre parole, carica una risorsa sul posto, il che può causare rallentamenti quando avviene durante processi sensibili. La funzione load() è anche un alias di ResourceLoader.load(path), accessibile a tutti i linguaggi di scripting.
Allora, quando esattamente avviene il pre-caricamento rispetto al caricamento, e quando si dovrebbe usare l'uno o l'altro? Vediamo un esempio:
# 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 provide any benefit.
#
# 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. Instantiating the script on its own with .new() triggers
# `load("office.tscn")`, ignoring any value set through the export.
@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")
using Godot;
// C# and other languages have no concept of "preloading".
public partial 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");
}
}
using namespace godot;
class MyBuildings : public Node {
GDCLASS(MyBuildings, Node)
public:
const Ref<PackedScene> building = ResourceLoader::get_singleton()->load("res://building.tscn");
Ref<PackedScene> a_building;
virtual void _ready() override {
// Can assign the value during initialization.
a_building = ResourceLoader::get_singleton()->load("res://office.tscn");
}
};
Il pre-caricamento consente allo script di gestire tutto il caricamento nel momento in cui viene caricato. Il pre-caricamento è utile, ma ci sono anche momenti in cui non è desiderato. Ecco alcune considerazioni per decidere quale usare:
Se non è possibile determinare quando lo script verrà caricato, il pre-caricamento di una risorsa, specialmente di una scena o di uno script, potrebbe risultare in ulteriori caricamenti inaspettati. Ciò potrebbe rendere i tempi di caricamento imprevedibili e di durata variabile, che si aggiungono alle operazioni di caricamento nello script originale.
Se qualcos'altro potesse sostituire il valore (come l'inizializzazione esportata di una scena), pre-caricare il valore sarebbe inutile. Questo punto non è un fattore significativo se si intende creare sempre lo script autonomamente.
Se si desidera solo "importare" un'altra risorsa di classe (script o scena), utilizzare una costante precaricata è spesso la soluzione migliore. Tuttavia, in casi eccezionali, potrebbe essere opportuno non farlo:
Se la classe "importata" è suscettibile a modifiche, allora dovrebbe essere una proprietà, inizializzata tramite
@exportoload()(e forse nemmeno inizializzata subito).Se lo script richiede molte dipendenze e non si desidera consumare così tanta memoria, è possibile caricare e scaricare diverse dipendenze in fase di esecuzione, al variare delle circostanze. Se si precaricano risorse nelle costanti, l'unico modo per scaricarle sarebbe scaricare l'intero script. Se invece si caricano nelle proprietà, è possibile impostare tali proprietà a
nulle rimuovere completamente tutti i riferimenti alla risorsa (il che, essendo un tipo che estende RefCounted, farà eliminare le risorse dalla memoria).
Livelli grandi: statico vs dinamico
Se si sta creando un livello di grandi dimensioni, quale soluzione è la più appropriata? È meglio creare il livello come un unico spazio statico? Oppure è meglio caricare il livello a pezzi e cambiare il contenuto del mondo a seconda delle necessità?
Beh, la risposta semplice è: "quando le prestazioni lo necessitano". Il dilemma associato alle due opzioni è una delle antiche scelte di programmazione: si ottimizza prima la memoria rispetto alla velocità, oppure viceversa?
La risposta più ingenua è usare un livello statico che carichi tutto in una volta. Ma, a seconda del progetto, questo potrebbe consumare una grande quantità di memoria. Sprecare la RAM degli utenti può rallentare o addirittura arrestare i programmi in esecuzione, a causa di tutte le altre attività che il computer cerca di eseguire contemporaneamente.
In ogni caso, è opportuno suddividere le scene più grandi in scene più piccole (per favorire la riutilizzabilità dei contenuti). Gli sviluppatori possono quindi progettare un nodo che gestisca la creazione/caricamento e l'eliminazione/scaricamento di risorse e nodi in tempo reale. I giochi con ambienti vasti e variegati o elementi generati proceduralmente spesso implementano queste strategie per evitare di sprecare la memoria.
D'altro parte, creare un sistema dinamico è più complesso, ovvero necessita di più programmazione, il che risulta in maggiori possibilità di errori e bug. Se non si fa attenzione, si rischia di sviluppare un sistema che appesantisce il debito tecnico dell'applicazione.
Pertanto, le opzioni migliori sarebbero...
Utilizzare livelli statici per i giochi più piccoli.
Se si ha tempo o risorse per un gioco di medie o grandi dimensioni, si può creare una libreria o un'estensione per la gestione di nodi e risorse tramite codice. Se perfezionata nel tempo, in modo da migliorarne l'usabilità e la stabilità, potrebbe evolversi in uno strumento affidabile anche per altri progetti.
Use dynamic logic for a medium/large game because one has the coding skills, but not the time or resources to refine the code (game's gotta get done). Could potentially refactor later to outsource the code into a plugin.
Per un esempio dei vari modi in cui è possibile scambiare le scene durante l'esecuzione, consultare la documentazione "Cambiare le scene manualmente".