What is the Door script in this context?

Godot: El Dilema de los Setters en Tool Scripts

12/03/2022

Valoración: 4.5 (10114 votos)

Imagina que estás en medio del desarrollo de tu próximo gran juego en Godot Engine. Estás creando un nivel complejo, lleno de objetos interactivos: puertas que se abren, palancas que se activan, tesoros que brillan. Para agilizar tu flujo de trabajo, decides usar una de las herramientas más potentes de Godot: los scripts de tipo tool. Estos te permiten ver los cambios de tus nodos personalizados directamente en el editor, una maravilla para el diseño de niveles. Sin embargo, es en este camino hacia la eficiencia donde muchos desarrolladores, tanto novatos como experimentados, se topan con un muro frustrante: el orden de inicialización y los setters.

What is ScriptDoor?
ScriptDoor is an open-market distribution, publishing, and streaming solution that provides an instant ready-made system for enterprise content providers. With ScriptDoor, content providers have control over their material, including product pricing and advertising revenue, and a direct connection to their consumers.

Este artículo profundiza en un problema real y muy común que surge al combinar scripts tool, variables exportadas con setters y la necesidad de manipular nodos hijos. Analizaremos por qué se producen errores aparentemente ilógicos al iniciar el juego y exploraremos tanto las soluciones actuales como las posibles mejoras futuras para el motor que podrían hacer nuestra vida mucho más fácil.

Índice de Contenido

El Poder y el Peligro de los `tool` Scripts

Antes de sumergirnos en el problema, recordemos por qué amamos los scripts tool. Al añadir la anotación tool al principio de un script GDScript, le indicamos al motor que ejecute ese código no solo cuando el juego está en marcha, sino también dentro del propio editor de Godot. Esto transforma tus nodos en herramientas de diseño de niveles vivas.

Un caso de uso clásico es un nodo `Puerta` personalizado. Quieres poder arrastrarlo a tu escena y, desde el panel Inspector, cambiar propiedades como su textura, su dirección (izquierda o derecha) o si está inicialmente abierta o cerrada. Gracias a un script tool, al cambiar estas propiedades, verías la puerta actualizarse visualmente en tiempo real en la ventana del editor. Para lograr esta reactividad, se utilizan comúnmente las variables exportadas con setter:

tool extends Node2D export(bool) var orientada_derecha = true setget set_orientacion func set_orientacion(nuevo_valor): orientada_derecha = nuevo_valor # Aquí iría la lógica para actualizar visualmente la puerta print("La orientación ha cambiado a: ", orientada_derecha)

Cada vez que cambias el valor de `orientada_derecha` en el Inspector, la función `set_orientacion` se ejecuta. Es un sistema poderoso y elegante, pero su poder esconde una sutileza en el ciclo de vida de los nodos que es la raíz de nuestro problema.

El Escenario del Conflicto: Nuestra Puerta Interactiva

Vamos a detallar nuestro caso práctico. Tenemos una escena `Puerta.tscn` con la siguiente estructura:

  • Puerta (Node2D con nuestro script `Door.gd`)
    • Sprite (Sprite)
    • CollisionShape (CollisionShape2D)

El script `Door.gd` tiene dos variables exportadas:

  1. orientada_derecha: Un booleano. Si es `true`, la puerta mira a la derecha. Si es `false`, se voltea y su colisionador se mueve al lado opuesto.
  2. textura_puerta: Una `Texture` para asignársela al nodo Sprite.

La función setter para la orientación podría verse así:

tool extends Node2D export(bool) var orientada_derecha = true setget set_orientacion onready var sprite = $Sprite onready var colisionador = $CollisionShape func set_orientacion(nuevo_valor): orientada_derecha = nuevo_valor if sprite: sprite.flip_h = not orientada_derecha if colisionador: # Suponemos que la posición del colisionador depende de la orientación if orientada_derecha: colisionador.position.x = 20 else: colisionador.position.x = -20

Este código funciona de maravilla en el editor. Cambias la casilla `orientada_derecha` y ¡zas!, la puerta se voltea. El problema llega cuando presionas "Jugar".

El Error Inesperado: `get_node()` en un Nodo Nulo

Al ejecutar el juego, la consola de Godot te grita con un error en rojo: `Invalid call. Nonexistent function 'get_node' in base 'Nil'`. ¿Qué ha pasado? La lógica parece sólida.

Aquí está el quid de la cuestión, el ciclo de vida de un nodo en Godot:

  1. El nodo es creado en memoria.
  2. El motor asigna los valores de sus propiedades exportadas (las que estableciste en el Inspector). ¡Aquí es cuando se llaman los setters por primera vez!
  3. El nodo es añadido al Árbol de Escena activo.
  4. La función _ready() del nodo es llamada, momento en el cual se garantiza que todos sus nodos hijos también existen y están listos.

El problema ocurre en el paso 2. Cuando el juego se inicia, Godot instancia tu escena `Puerta` y, antes de que siquiera forme parte del árbol de la escena principal, intenta establecer el valor de `orientada_derecha`. Esto dispara la ejecución de `set_orientacion()`. Sin embargo, en ese preciso instante, los nodos hijos `Sprite` y `CollisionShape` aún no han sido inicializados ni añadidos como hijos. Por lo tanto, cualquier llamada a `get_node("Sprite")` o `$Sprite` devolverá `null`, y tratar de acceder a una propiedad como `flip_h` en un objeto nulo causa el error que detiene tu juego.

Cabe destacar que la alternativa de usar `_process()` para comprobar cambios constantemente es una pésima idea para los `tool scripts`, ya que obliga al editor a redibujar la escena 60 veces por segundo, consumiendo recursos de la GPU de forma innecesaria. El uso de setters es la vía correcta, pero debemos sortear este obstáculo de inicialización.

La Solución Temporal: Un "Hack" Funcional pero Incómodo

Existe una solución funcional para este problema, aunque requiere añadir código repetitivo que puede sentirse como un parche. Consiste en dos partes:

1. Añadir una guarda en el setter: Modificamos el setter para que solo ejecute su lógica si el nodo ya está dentro del árbol de escena. Esto se logra con la función `is_inside_tree()`.

2. Llamar manualmente al setter en `_ready()`: Como el setter no se ejecutó correctamente al inicio, debemos forzar la configuración inicial una vez que todo esté en su sitio, y el mejor lugar para ello es la función _ready.

What is the Door script in this context?
The Door script is a tool script that the author has been working on. They noticed an issue where they couldn't use `_process ()` in every tool script that reacts to variable changes because it was causing excessive GPU usage due to constant scene redrawing. So they switched to using setters, but encountered another problem.

El código corregido se vería así:

tool extends Node2D export(bool) var orientada_derecha = true setget set_orientacion func _ready(): # Ahora que los hijos existen, llamamos al setter para la configuración inicial set_orientacion(orientada_derecha) func set_orientacion(nuevo_valor): orientada_derecha = nuevo_valor # Guarda: si el nodo no está listo, no hacemos nada todavía. if not is_inside_tree(): return # Ahora es seguro acceder a los hijos $Sprite.flip_h = not orientada_derecha if orientada_derecha: $CollisionShape.position.x = 20 else: $CollisionShape.position.x = -20

Este método funciona. Previene el error al inicio y asegura que la puerta se configure correctamente. Sin embargo, tiene desventajas claras: es repetitivo (imagina hacer esto para 5 o 10 propiedades en docenas de nodos interactivos) y es propenso a errores humanos (es fácil olvidar añadir la llamada en `_ready()`). Se siente como una solución provisional, no como una solución elegante y robusta.

Una Propuesta a Futuro: ¿Setters Diferidos con `onready`?

La comunidad de Godot es consciente de este inconveniente. De hecho, han surgido propuestas para integrar una solución a nivel de lenguaje en GDScript. Una idea interesante es la capacidad de marcar un setter para que su ejecución inicial sea diferida.

Imagina una nueva sintaxis como esta:

export var orientada_derecha: bool onready setget set_orientacion

La palabra clave `onready` (o una similar) antes de `setget` le indicaría al motor que, durante la fase de inicialización del nodo, en lugar de llamar a `set_orientacion()` inmediatamente, debería usar `call_deferred("set_orientacion", valor_inicial)`. La función `call_deferred` pospone la ejecución de una función hasta que el motor esté en un momento seguro de "tiempo muerto" (idle time), lo que ocurre garantizadamente después de que la función `_ready()` haya sido ejecutada y el árbol de escena esté completamente construido.

Esta pequeña adición al lenguaje resolvería el problema de raíz de una manera limpia, declarativa y sin código repetitivo. Eliminaría la necesidad de las guardas `is_inside_tree()` y las llamadas manuales en `_ready()`, haciendo el código más limpio y robusto.

Tabla Comparativa de Enfoques

Para resumir, veamos las ventajas y desventajas de cada método en una tabla.

CaracterísticaEnfoque `_process()`Enfoque Setter + `_ready()`Enfoque `onready setget` (Propuesta)
Rendimiento en EditorMuy MaloExcelenteExcelente
Complejidad de CódigoBajaMedia (repetitivo)Muy Baja
RobustezBajaMedia (propenso a olvidos)Alta
EscalabilidadMalaRegularExcelente

Preguntas Frecuentes (FAQ)

P: ¿Por qué no usar `onready var mi_sprite = $Sprite` y luego usar `mi_sprite` en el setter?

R: Las variables declaradas con `onready` también se inicializan justo antes de que se llame a `_ready()`. Por lo tanto, cuando el setter es invocado por primera vez durante la creación del nodo, la variable `mi_sprite` todavía sería `null`. El problema de tiempo es exactamente el mismo.

P: ¿Este problema solo afecta a los `tool` scripts?

R: No exclusivamente. El orden de inicialización es el mismo para todos los scripts. Sin embargo, el problema es mucho más evidente y común en los `tool scripts` porque su propósito principal es reaccionar a cambios de propiedades exportadas que a menudo afectan a nodos hijos para dar feedback visual en el editor.

P: Mientras no exista una solución integrada, ¿hay alguna otra alternativa al workaround de `is_inside_tree()`?

R: Técnicamente, se podrían explorar otras formas de retrasar la ejecución, como iniciar un `Timer` de un solo uso en `_ready()` o usar `yield(get_tree(), "idle_frame")`. Sin embargo, estos métodos suelen ser más complejos y menos directos que la combinación de la guarda `is_inside_tree()` y la llamada manual en `_ready()`, que sigue siendo el workaround más extendido y comprendido por la comunidad.


En conclusión, el manejo de la inicialización en los setters de los `tool scripts` es una de esas sutilezas de Godot que, una vez entendida, te convierte en un desarrollador más competente. Aunque la solución actual no es perfecta, es funcional y nos permite seguir creando herramientas de editor potentes. La discusión activa sobre mejoras como los setters diferidos demuestra la vitalidad de la comunidad de Godot y su constante búsqueda de un flujo de trabajo más intuitivo y eficiente. Comprender el ciclo de vida de los nodos no es solo resolver un error, es dominar el motor para que trabaje a tu favor.

Si quieres conocer otros artículos parecidos a Godot: El Dilema de los Setters en Tool Scripts puedes visitar la categoría Juegos.

Subir