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...
Organizzazione di una scena
Questo articolo tratta argomenti riguardo l'organizzazione efficace dei contenuti di una scena. Quali nodi utilizzare? Dove posizionarli? Come dovrebbero interagire?
Come costruire relazioni in modo efficace
Quando gli utenti di Godot iniziano a creare le proprie scene, spesso si imbattono nel seguente problema:
Creano la loro prima scena e la riempiono di contenuti, solo per poi ritrovarsi a salvare rami della loro scena in scene separate, man mano che comincia ad accumularsi la fastidiosa sensazione di dover suddividere il tutto. Però poi, si accorgono che i riferimenti rigidi, su cui potevano contare prima, non sono più possibili. Riutilizzare la scena in più posti crea problemi perché i percorsi dei nodi non trovano le loro destinazioni e le connessioni dei segnali stabilite nell'editor si rompono.
Per risolvere questi problemi, è necessario istanziare le sotto-scene senza che richiedano dettagli sul loro ambiente. Bisogna potersi fidare che la sotto-scena si creerà da sola, senza che sia pignola su come sarà utilizzata.
One of the biggest things to consider in Object-Oriented Programming (OOP) is maintaining focused, singular-purpose classes with loose coupling to other parts of the codebase. This keeps the size of objects small (for maintainability) and improves their reusability.
Queste migliori pratiche della OOP hanno numerose implicazioni per le migliori pratiche nella struttura delle scene e nell'utilizzo degli script.
Se possibile, progettare scene in modo che non abbiano dipendenze. Vale a dire, creare scene che contengano tutto ciò di cui hanno bisogno al loro interno.
Se una scena deve interagire con un contesto esterno, gli sviluppatori esperti raccomandano l'uso della Dependency injection. Questa tecnica prevede che un'API di alto livello fornisca le dipendenze dell'API di basso livello. Perché fare così? Perché le classi che si basano sul loro ambiente esterno possono inavvertitamente causare bug e comportamenti imprevisti.
Per fare ciò, è necessario esporre i dati e quindi fare affidamento su un contesto padre per inizializzarli:
Connettere a un segnale. Estremamente sicuro, ma dovrebbe essere usato solo per "rispondere" a un comportamento, non per avviarlo. Per convenzione, i nomi dei segnali sono solitamente verbi al passato come "entered" (inserito), "skill_activated" (abilità attivata) o "item_collected" (oggetto raccolto).
# Parent $Child.signal_name.connect(method_on_the_object) # Child signal_name.emit() # Triggers parent-specified behavior.
// Parent GetNode("Child").Connect("SignalName", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child EmitSignal("SignalName"); // Triggers parent-specified behavior.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { // Note that get_node may return a nullptr, which would make calling the connect method crash the engine if "Child" does not exist! // So unless you are 1000% sure get_node will never return a nullptr, it's a good idea to always do a nullptr check. node->connect("signal_name", callable_mp(this, &ObjectWithMethod::method_on_the_object)); } // Child emit_signal("signal_name"); // Triggers parent-specified behavior.
Chiamare un metodo. Utilizzato per avviare un comportamento.
# Parent $Child.method_name = "do" # Child, assuming it has String property 'method_name' and method 'do'. call(method_name) # Call parent-specified method (which child must own).
// Parent GetNode("Child").Set("MethodName", "Do"); // Child Call(MethodName); // Call parent-specified method (which child must own).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("method_name", "do"); } // Child call(method_name); // Call parent-specified method (which child must own).
Inizializzare una proprietà Callable. Più sicuro di un metodo, poiché non è necessario possederlo. Utilizzata per avviare un comportamento.
# Parent $Child.func_property = object_with_method.method_on_the_object # Child func_property.call() # Call parent-specified method (can come from anywhere).
// Parent GetNode("Child").Set("FuncProperty", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child FuncProperty.Call(); // Call parent-specified method (can come from anywhere).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("func_property", Callable(&ObjectWithMethod::method_on_the_object)); } // Child func_property.call(); // Call parent-specified method (can come from anywhere).
Inizializzare un nodo o un altro riferimento di oggetto.
# Parent $Child.target = self # Child print(target) # Use parent-specified node.
// Parent GetNode("Child").Set("Target", this); // Child GD.Print(Target); // Use parent-specified node.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target", this); } // Child UtilityFunctions::print(target);
Inizializzare un percorso di nodo (NodePath).
# Parent $Child.target_path = ".." # Child get_node(target_path) # Use parent-specified NodePath.
// Parent GetNode("Child").Set("TargetPath", NodePath("..")); // Child GetNode(TargetPath); // Use parent-specified NodePath.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target_path", NodePath("..")); } // Child get_node<Node>(target_path); // Use parent-specified NodePath.
Queste opzioni nascondono i punti di accesso al nodo figlio. Ciò a sua volta mantiene il nodo figlio debolmente accoppiato al suo ambiente. È possibile riutilizzarlo in un altro contesto senza ulteriori modifiche alla sua API.
Nota
Sebbene gli esempi sopra riportati illustrino le relazioni genitore-figlio, gli stessi principi si applicano a tutte le relazioni tra oggetti. I nodi che sono fratelli dovrebbero conoscere solo le proprie gerarchie, mentre un antenato comune fa da mediatore tra le loro comunicazioni e i loro riferimenti.
# Parent
$Left.target = $Right.get_node("Receiver")
# Left
var target: Node
func execute():
# Do something with 'target'.
# Right
func _init():
var receiver = Receiver.new()
add_child(receiver)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");
public partial class Left : Node
{
public Node Target = null;
public void Execute()
{
// Do something with 'Target'.
}
}
public partial class Right : Node
{
public Node Receiver = null;
public Right()
{
Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
AddChild(Receiver);
}
}
// Parent
get_node<Left>("Left")->target = get_node<Node>("Right/Receiver");
class Left : public Node {
GDCLASS(Left, Node)
protected:
static void _bind_methods() {}
public:
Node *target = nullptr;
Left() {}
void execute() {
// Do something with 'target'.
}
};
class Right : public Node {
GDCLASS(Right, Node)
protected:
static void _bind_methods() {}
public:
Node *receiver = nullptr;
Right() {
receiver = memnew(Node);
add_child(receiver);
}
};
Gli stessi principi si applicano anche agli oggetti non Node che mantengono dipendenze da altri oggetti. L'oggetto che possiede gli altri oggetti dovrebbe gestire le relazioni tra loro.
Avvertimento
È comunque più importante mantenere i dati internamente (interni a una scena), poiché stabilire una dipendenza da un contesto esterno, anche debolmente accoppiato, significa comunque che il nodo si aspetterà che qualcosa nel suo ambiente sia vero. Le filosofie progettuali del proprio progetto dovrebbero impedire che ciò accada. Se no, le insite responsabilità del codice costringeranno gli sviluppatori a utilizzare la documentazione per tenere traccia delle relazioni tra gli oggetti su scala microscopica; questo è altrimenti noto come "inferno dello sviluppo". Scrivere codice che richiede una documentazione esterna per essere utilizzato in modo sicuro è per definizione soggetto a errori.
Per evitare di creare e gestire tale documentazione, si converte il nodo dipendente ("figlio" sopra) in uno script strumento che implementa _get_configuration_warnings(). Restituendo un PackedStringArray non vuoto, il pannello Scena genererà accanto al nodo un'icona di avvertimento, con le stringhe come suggerimento. Questa è la stessa icona che appare, ad esempio, quando il nodo Area2D non definisce nodi figlio CollisionShape2D. L'editor quindi auto-documenta la scena tramite il codice di script. Non c'è bisogno di duplicare il contenuto tramite la documentazione.
A Graphical User Interface (GUI) like this can better inform project users of critical information about a Node. Does it have external dependencies? Have those dependencies been satisfied? Other programmers, and especially designers and writers, will need clear instructions in the messages telling them what to do to configure it.
Allora perché tutto questo complesso scambio funziona? Beh, perché le scene funzionano meglio quando sono isolate. Se non sono in grado di funzionare da sole, la soluzione migliore è lavorare con altre in modo anonimo (con dipendenze rigide minime, ovvero con un accoppiamento debole). Inevitabilmente, potrebbe essere necessario apportare modifiche a una classe, e se tali modifiche causano interazioni impreviste con altre scene, allora tutto va in fumo. L'obiettivo di questo approccio indiretto è evitare di ritrovarsi in una situazione in cui la modifica di una classe abbia un effetti negativi sulle altre classi che ne dipendono.
Script e scene, in quanto estensioni delle classi del motore, dovrebbero rispettare tutti i principi della OOP. Alcuni esempi includono...
Scegliere una struttura per un albero di nodi
Potresti iniziare a lavorare a un gioco, ma sentirti sopraffatto dalle vaste possibilità che ti si offrono. Potresti sapere cosa vuoi fare, quali sistemi vuoi avere, ma dove li metti tutti? Il modo in cui creerai il tuo gioco dipende sempre da te. Gli alberi di nodi si possono costruire in innumerevoli modi. Se non sei sicuro, questa guida può dare un esempio di una struttura decente da cui cominciare.
Un gioco dovrebbe sempre avere un "punto di ingresso"; un punto da poter considerare con certezza l'inizio delle cose, in modo da poterne seguire la logica mentre prosegue altrove. Questo punto serve anche come panoramica per tutti gli altri dati e della logica del programma. Per le applicazioni tradizionali, questa sarebbe la funzione "main". In Godot, è un nodo Main.
Nodo "Main" (main.gd)
Lo script main.gd fungerà da controller primario del tuo gioco.
Poi avrai un "World" di gioco (2D o 3D). Questo può essere un figlio di Main. Inoltre, avrai bisogno di un'interfaccia grafica principale (GUI) per il tuo gioco che gestisca i vari menu e widget necessari al progetto.
- Nodo "Main" (main.gd)
Node2D/Node3D "World" (game_world.gd)
Control "GUI" (gui.gd)
Quando cambi livello, puoi scambiare i figli del nodo "World". Cambiare scena manualmente ti dà il pieno controllo sul modo in cui cambia il mondo del tuo gioco.
Il passo successivo è considerare quali sistemi di gioco richiede il tuo progetto. Se hai un sistema che...
tiene traccia di tutti i suoi dati internamente
dovrebbe essere accessibile a livello globale
dovrebbe esistere in modo isolato
... allora dovresti creare un nodo autoload 'singleton'.
Nota
Per i giochi più piccoli, un'alternativa più semplice con meno controllo sarebbe quella di avere un singleton "Game" che semplicemente chiama il metodo SceneTree.change_scene_to_file() per scambiare il contenuto della scena principale. Questa struttura mantiene più o meno "World" come nodo principale del gioco.
Ogni GUI dovrebbe anche essere un singleton, una parte transitoria di "World", oppure aggiunta manualmente come figlio diretto della radice. Altrimenti, i nodi della GUI si eliminerebbero anche durante le transizioni tra scene.
If you have systems that modify other systems' data, you should define those as their own scripts or scenes, rather than autoloads. For more information, see Autoloads versus regular nodes.
Ogni sotto-sistema del gioco dovrebbe avere una propria sezione all'interno dello SceneTree. Dovresti utilizzare le relazioni padre-figlio solo nei casi in cui i nodi siano effettivamente elementi dei rispettivi genitori. Rimuovere il genitore significa che anche i figli debbano essere rimossi? In caso contrario, dovrebbe avere un propria posto nella gerarchia come fratello o in un'altra relazione.
Nota
In alcuni casi, è necessario che questi nodi separati anche si posizionino l'uno rispetto all'altro. A questo scopo, puoi utilizzare i nodi RemoteTransform / RemoteTransform2D. Questi consentono a un nodo di destinazione di ereditare in modo condizionale gli elementi scelti della trasformazione dal nodo Remote*. Per assegnare il NodePath all'obiettivo (target), utilizza uno dei seguenti:
Una terza parte affidabile, probabilmente un nodo padre, che fa da mediatore per l'assegnazione.
Un gruppo, per ottenere un riferimento al nodo desiderato (supponendo che ci sarà sempre solo uno degli obiettivi).
Il momento in cui farlo è soggettivo. Il dilemma sorge quando è necessario gestire in dettaglio un nodo che deve muoversi intorno allo SceneTree per preservare se stesso. Per esempio...
Aggiungi un nodo "giocatore" a una "stanza".
Bisogna cambiare stanza, quindi devi eliminare la stanza attuale.
Prima di poter eliminare la stanza, è necessario conservare e/o spostare il giocatore.
Se la memoria non è un problema, puoi...
Creare la nuova stanza.
Muovere il giocatore nella nuova stanza.
Eliminare la vecchia stanza.
Se la memoria è un problema, allora dovrai...
Spostare il giocatore da qualche altro posto nell'albero di scene.
Eliminare la stanza.
Istanziare e aggiungere la nuova stanza.
Ri-aggiungere il giocatore alla nuova stanza.
Il problema è che il giocatore in questo caso è un "caso speciale" in cui gli sviluppatori devono sapere di doverlo gestire in questo modo per il progetto. L'unico modo per condividere queste informazioni in modo affidabile come team è documentarle. Mantenere i dettagli di implementazione nella documentazione è pericoloso. È un peso per la manutenzione, compromette la leggibilità del codice e appesantisce inutilmente il contenuto intellettuale di un progetto.
In un gioco più complesso con risorse più grandi, potrebbe essere più opportuno mantenere il giocatore totalmente da qualche altro posto nello SceneTree. Questo risulta in:
Più coerenza.
Nessun "caso speciale" che debba essere documentato e mantenuto da qualche parte.
Nessuna opportunità che si verifichino errori a causa di questi dettagli non presi in considerazione.
Al contrario, se mai avessi bisogno di un nodo figlio che non erediti la trasformazione del suo genitore, hai le seguenti opzioni:
La soluzione dichiarativa: inserisci un Node tra di loro. Dato che non ha una trasformazione, non passerà questa informazione ai suoi figli.
La soluzione imperativa: utilizza la proprietà
top_levelper il nodo CanvasItem o Node3D. Questo consentirà al nodo di ignorare la trasformazione ereditata.
Nota
Se stai creando un gioco in rete, tieni presente quali nodi e sistemi di gioco sono rilevanti per tutti i giocatori e quali sono pertinenti solo al server autorevole. Ad esempio, non tutti gli utenti devono avere una copia della logica "PlayerController" di ogni giocatore: basta la propria logica. Mantenerli in un ramo separato dal "mondo" può contribuire a semplificare la gestione delle connessioni di gioco e simili.
La chiave dell'organizzazione di una scena è considerare lo SceneTree in termini relazionali piuttosto che spaziali. I nodi dipendono dall'esistenza del genitore? Se no, possono prosperare tutti da soli altrove. Se sono dipendenti, allora è logico che debbano essere figli di quel genitore (e probabilmente parte della scena di quel genitore, se non lo sono già).
Ciò significa che i nodi stessi sono componenti? Niente affatto. Gli alberi di nodi di Godot formano una relazione di aggregazione, non di composizione. Tuttavia, pur mantenendo la flessibilità di spostare i nodi, è comunque preferibile che tali spostamenti non siano necessari normalmente.