Up to date

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

Organización de la escena

Este artículo cubre temas relacionados con la organización efectiva del contenido de la escena. ¿Qué nodos se deben utilizar? ¿Dónde hay que colocarlos? ¿Cómo deberían interactuar?

Cómo crear relaciones de manera eficiente

Cuando los usuarios de Godot comienzan a crear sus propias escenas, normalmente se encuentran con el siguiente problema:

Crean la primer escena y la llenan con contenido para luego terminar guardando las ramas en escenas separadas a medida que la sensación de que se deben separar cosas comienza a acumularse. Sin embargo, notan que las referencias rígidas de las que dependían, ya no son posibles de usar. Reutilizar la escena en muchos lugares genera problemas porque las rutas de los nodos no llegan a su destino y las conexiones de las señales creadas en el editor se rompen.

Para solucionar esos problemas, uno debe instanciar las sub escenas sin que estas requieran detalles de su entorno. Uno debe ser capaz de confiar de que la sub escena se creará a sí misma sin ser exigente respecto a quién la usa.

Una de las mayores cosas a considerar en POO es mantener clases enfocadas, de propósito simple, con vínculos no estrictos (loose coupling ) respecto a otras partes del proyecto. Esto mantiene pequeño el tamaño de los objetos (para facilitar el mantenimiento) y mejora la reusabilidad.

Estas buenas prácticas POO tienen muchas ramificaciones para buenas prácticas en estructura de escenas y uso de scripts.

Si es posible, las escenas deberían diseñarse para que no tengan dependencias. O lo que es lo mismo, que mantengan todo lo que necesitan dentro de sí mismas.

Si una escena debe interactuar en un contexto externo, los desarrolladores experimentados recomiendan el uso de Inyección de dependencias. Esta técnica implica que una API de alto nivel proporcione las dependencias de la API de bajo nivel. ¿Por qué hacer esto? Porque las clases que dependen de su entorno externo pueden desencadenar errores y comportamientos inesperados sin darse cuenta.

Para hacer esto, uno debe exponer datos y luego confiar en un contexto principal para inicializarlos:

  1. Conectando una señal. Es extremadamente seguro, pero solo debe ser utilizado como "respuesta" a un comportamiento, nunca para iniciarlo. Por convención, los nombres de las señales normalmente están en tiempo pasado, como por ejemplo "entered" (ingresó), "skill_activated" (se activó la habilidad) o "item_collected" (se recolectó el item).

    # Parent
    $Child.signal_name.connect(method_on_the_object)
    
    # Child
    signal_name.emit() # Triggers parent-defined behavior.
    
  2. Llamar a un método. Usado para iniciar un comportamiento.

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-defined method (which child must own).
    
  3. Inicializar una propiedad Callable. Es más seguro que establecer un método como propiedad del método . Se utiliza para iniciar el comportamiento.

    # Parent
    $Child.func_property = object_with_method.method_on_the_object
    
    # Child
    func_property.call() # Call parent-defined method (can come from anywhere).
    
  4. Inicializa un Nodo u otra referencia de Objeto.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
  5. Inicializar un NodePath.

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    

Estas opciones ocultan los puntos de accesos al nodo hijo. Esto a su vez mantiene al nodo hijo acoplado de manera ligera con su entorno. Esto permite reutilizarlo en otro contexto sin ningún cambio adicional en su API.

Nota

Aunque los ejemplos anteriores ilustran las relaciones padre-hijo, los mismos principios aplican a todas las relaciones entre objetos. Los nodos hermanos sólo deben percatarse de sus jerarquías, mientras que un ancestro mediará sus comunicaciones y referencias.

# 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)

Los mismos principios también aplican a los objetos no-Nodos que mantengan dependencias de otros objetos. Cualquiera sea el objeto al que pertenezcan los objetos, deberá administrar las relaciones entre ellos.

Advertencia

Uno debería preferir mantener los datos internos a una escena, pues establecer dependencias en contextos externos, incluso levemente, significará que el Nodo esperará a que alguna condición en su contexto se cumpla. La filosofía de diseño del proyecto debe prevenir que esto pase. De lo contrario, las dependencias inherentes del código, forzará a los desarrolladores a usar documentación para seguir el paso de las relaciones entre objetos a escala microscópica; también conocido como "Infierno de desarrollo". Escribir código que dependa de documentación externa para su uso adecuado es propenso a errores por naturaleza.

Para evitar crear y mantener tal documentación, uno convierte el nodo dependiente ("hijo" de arriba) en una herramienta script que implementa _get_configuration_warning(). Regresando un PackedStringArray no vacío de ello que hará al panel de Escena generar un ícono de advertencia con la cadena como una información por el nodo. Éste es el mismo icono que aparece para nodos tal como el nodo Area2D cuando no tiene nodos descendientes CollisionShape2D definidos. El editor entonces auto-documenta la escena a través del código del script. No es necesaria la duplicación de contenido a través de la documentación.

Una GUI como esta puede informar mejor a los usuarios del proyecto sobre la existencia de información crítica sobre un Nodo. ¿Tiene dependencias externas? ¿se han satisfecho esas dependencias?. Otros programadores, y especialmente los diseñadores y escritores, necesitarán instrucciones claras en los mensajes que les indiquen qué hacer para configurarlo.

¿Entonces, por qué todo esto funciona? Porque las escenas operan mejor cuando trabajan solas. Si no son capaces de funcionar solas al menos deberían intentar trabajar con otras de manera anónima (con mínimas dependencias, bajo acoplamiento) en el peor caso. Inevitablemente, puede que deban hacerse cambios a una clase y esto ocasione que interactúe con otras escenas de maneras no previstas, entonces las cosas se romperán. El objetivo de todo este proceso es para evitar terminar en una situación en donde cambiando una clase resulte en efectos adversos para otras clases que dependen de ella.

Tanto scripts y escenas, como clases de extensión del motor, deben apegarse a todos los principios POO. Ejemplos incluyen...

Eligiendo una estructura de árbol de nodos

Entonces, un desarrollador comienza a trabajar en un juego sólo para detenerse en las vastas posibilidades que tiene ante sí. Es posible que sepan lo que quieren hacer, qué sistemas quieren tener, pero ¿dónde ponerlos a todos? Bueno, la forma en que uno hace su juego siempre depende de ellos. Se pueden construir árboles de nodos de innumerables formas. Pero, para aquellos que no están seguros, esta guía útil puede darles una muestra de una estructura decente para empezar.

Un juego siempre debe tener una especie de "punto de entrada"; en algún lugar el desarrollador puede rastrear definitivamente dónde comienzan las cosas para poder seguir la lógica mientras continúa en otro lugar. Este lugar también sirve como una vista de pájaro de todos los demás datos y la lógica del programa. Para las aplicaciones tradicionales, esta sería la función "principal". En este caso, sería un nodo Principal.

  • Nodo "Main" (main.gd)

El script main.gd debería servir como controlador primario del juego.

Luego, uno tiene el "Mundo" (2D o 3D) del juego. Esto puede ser un hijo de Main. Adicionalmente, se puede necesitar una GUI primaria para el juego, que maneje los menues y widgets que necesita el proyecto.

  • Nodo "Main" (main.gd)
    • Node2D/Node3D "World" (game_world.gd)

    • Control "GUI" (gui.gd)

Cuando se cambian los niveles, entonces se pueden intercambiar los hijos del nodo "World". Cambiar escenas manualmente le da al usuario un mayor control sobre cómo suceden las transiciones del juego.

El siguiente paso es considerar qué sistemas de juego se requieren. Si uno tiene un sistema que...

  1. monitorea todos los datos internamente

  2. debería ser accesible globalmente

  3. debería existir de manera aislada

... entonces se puede crear un nodo autoload 'singleton'.

Nota

Para juegos pequeños, una alternativa simple con menor control podría ser tener un singleton "Game" que simplemente llame al método SceneTree.change_scene_to_file() para intercambiar el contenido de la escena principal. Esta estructura mantiene a "World" como un nodo principal del juego.

Un GUI puede necesitar también un singleton, sean partes transitorias del "World", o agregadas manualmente como hijas de la raíz. De otro modo, los nodos GUI podrían borrarse a si mismos durante escenas de transición.

Si uno de los sistemas modifica datos de otro sistema, se los pueden definir en sus propios scripts en lugar de autoloads. Para más información sobre estas razones, por favor lee la documentación en 'Autoloads vs. Nodos Internos'.

Cada subsistema dentro de tu juego debería tener su propia sección en el Árbol de Escenas. Uno debe usar la relación padre-hijo sólo en casos donde los nodos son efectivamente elementos de sus padres. ¿Remover un padre quiere decir también remover los hijos? Si la respuesta es no, entonces estos deben tener su propio lugar en la jerarquía como hermanos o alguna otra relación.

Nota

En algunos casos, es necesario que esos nodos separados también se posicionen de manera relativa entre sí. Se pueden utilizar los nodos RemoteTransform3D o RemoteTransform2D para ello. Estos permiten que a un nodo objetivo se le apliquen elementos seleccionados del Transform del nodo remoto. Para asignar el target NodePath, usa uno de los siguientes:

  1. Un nodo externo confiable, como un nodo padre, para mediar en la asignación.

  2. Un grupo, para tomar fácilmente una referencia al nodo deseado (asumiendo que siempre será uno de los objetivos).

¿Cuándo se debe hacer esto? Bueno, esto es algo subjetivo. El dilema surge cuando uno debe microgestionar cuando un nodo debe moverse alrededor del SceneTree para preservarse. Por ejemplo...

  • Agregar un nodo "jugador" a un "escenario".

  • Se necesitan cambiar escenarios, entonces se debe eliminar el actual.

  • Antes de que el escenario sea borrado, hay que preservar o mover al jugador.

    ¿Importa la memoria?

    • Si no importa, se pueden crear dos escenarios, mover al jugador y borrar el escenario viejo. No hay problema.

    Si importa, habrá que...

    • Mover el jugador a algún lugar en el árbol de escenas.

    • Borrar el escenario.

    • Instanciar el escenario nuevo.

    • Agrega nuevamente el jugador.

El problema es que en este caso, el jugador es un "caso especial", uno donde los desarrolladores deben saber que habrá que utilizar de este modo al jugador en el proyecto. Como tal, el único modo de compartir esta información de manera confiable en el equipo, es documentarla. Mantener detalles de implementación en la documentación es peligroso, es una carga extra de mantenimiento, afecta la facilidad de lectura del código y engorda el contenido intelectual del proyecto de manera innecesaria.

En un juego más complejo, con recursos más grandes, puede ser mejor simplemente dejar al jugador en alguna otra parte del árbol de escenas. Esto resulta en:

  1. Mayor consistencia.

  2. No hay "casos especiales" que deban ser documentados y mantenidos en algún lugar.

  3. No hay oportunidad de que sucedan esos errores porque algún detalle no se tuvo en cuenta.

En contraste, si se necesita que un hijo no herede el transform del padre, están las siguientes opciones:

  1. La solución declarativa: coloca un Node entre ellos. Como los Node no tienen transform, no pasarán esa información a sus hijos.

  2. La solución imperativa: Usa la propiedad top_level del nodo CanvasItem or Node3D. Esto hará que el nodo ignore el transform heredado.

Nota

Si estás creando un juego en red, ten en mente qué nodos y sistemas de juego serán relevantes para todos los jugadores y cuáles sólo serán pertinentes para el servidor autoritativo. Por ejemplo, no todos los usuarios necesitan tener una copia de la lógica de cada "controlador de jugador". En cambio, sí necesitan el propio. De este modo, mantenerlos en una rama separada del "mundo" puede simplificar la administración de las conexiones del juego entre otras cosas.

La clave para la organización de la escena es considerar el Árbol de Escenas en términos relacionales más que espaciales. ¿Los nodos dependen de la existencia de sus padres? Si no es así, entonces pueden prosperar por sí mismos en otro lugar. Si son dependientes, entonces es lógico que sean hijos de ese padre (y probablemente parte de la escena de ese padre si no lo son ya).

¿Quiere decir esto que los nodos son componentes? Para nada, los árboles de nodos de Godot forman una relación de agregación, no una de composición. Y aunque exista la flexibilidad de mover nodos por el árbol, es mejor cuando esas acciones no son necesarias.