tutoriales.com

Navegación entre Escenas y Gestión de Estado en Godot 4: Creando un Flujo de Juego Dinámico

Este tutorial te guiará a través de la implementación de la navegación entre escenas y la gestión de estado global en Godot 4. Dominarás cómo cambiar de una escena a otra de forma eficiente y cómo persistir datos importantes a lo largo de tu juego, asegurando una experiencia de usuario fluida y consistente.

Intermedio15 min de lectura13 views24 de marzo de 2026Reportar error

¡Hola, desarrollador! 👋 ¿Estás listo para llevar tus juegos de Godot 4 al siguiente nivel? Un aspecto fundamental en cualquier juego es la capacidad de moverse entre diferentes pantallas o "escenas", ya sean menús, niveles, pantallas de carga o de fin de juego. Pero no solo eso, también es crucial mantener el control sobre la información que persiste a través de estas transiciones, como la puntuación del jugador, el nivel actual o el inventario. En este tutorial, exploraremos a fondo cómo implementar una navegación de escenas robusta y cómo gestionar el estado global de tu juego en Godot 4.

🎯 ¿Por qué es importante la navegación de escenas y el estado global?

Imagina un juego sin menús o sin poder pasar al siguiente nivel. ¡Sería bastante aburrido! La navegación de escenas es el esqueleto que permite a tu juego tener una estructura y un flujo. Es lo que conecta todas las partes individuales de tu proyecto en una experiencia coherente.

Por otro lado, la gestión de estado global es el cerebro. Se encarga de recordar todo lo que es importante para el juego en su conjunto, no solo para una escena específica. Esto puede incluir:

  • Puntuación del jugador 🏆
  • Nivel actual 🗺️
  • Salud o energía ❤️
  • Inventario 🎒
  • Configuración del juego (volumen, dificultad) ⚙️
  • Estado de desbloqueo (niveles completados, logros) ✅

Sin una buena gestión de estado, cada vez que cambias de escena, perderías toda esta información, lo que forzaría a tus jugadores a empezar de cero o a encontrar inconsistencias.


🛠️ Requisitos Previos

Antes de sumergirnos en el código y la configuración, asegúrate de tener lo siguiente:

  • Godot Engine 4.x instalado. Puedes descargarlo desde la página oficial de Godot.
  • Un conocimiento básico de la interfaz de Godot 4.
  • Conocimientos fundamentales de GDScript (variables, funciones, señales).
💡 Consejo: Si eres nuevo en Godot, te recomiendo familiarizarte con los conceptos básicos de nodos y escenas antes de continuar. ¡Hay muchos recursos excelentes para principiantes!

📖 Conceptos Clave en Godot 4

Entender algunos conceptos fundamentales nos ayudará a construir una base sólida.

Nodos y Escenas

En Godot, todo es un nodo. Los nodos se organizan en árboles para formar escenas. Una escena es una colección de nodos que forman una parte funcional del juego (un menú principal, un nivel, un personaje, etc.).

El Árbol de Escenas

Godot gestiona una única escena "activa" a la vez. Esta escena es la raíz del Árbol de Escenas. Cuando cambias de escena, lo que realmente haces es cambiar la raíz de este árbol.

Root SceneA Cambio SceneB Solo una escena activa a la vez
📌 Nota: Los nodos que no se guardan como parte de una escena se consideran "nodos huérfanos" y Godot los gestiona internamente como parte del "escenario" o "Stage".

🚀 Parte 1: Navegación Básica entre Escenas

Vamos a empezar con lo más sencillo: cambiar de una escena a otra.

1. Creando Nuestras Escenas

Para este tutorial, crearemos tres escenas muy simples:

  • MainMenu.tscn: El menú principal del juego.
  • Level1.tscn: El primer nivel del juego.
  • GameOver.tscn: La pantalla de fin de juego.
  1. Crea un nuevo proyecto Godot. Nómbralo "GodotSceneNav".
  2. Crea la escena MainMenu.tscn:
    • Añade un nodo Control como raíz (renómbralo a MainMenu).
    • Añade un Label y establece su Text a "Menú Principal".
    • Añade un Button y establece su Text a "Iniciar Juego".
    • Guarda la escena como res://Scenes/MainMenu.tscn.
  3. Crea la escena Level1.tscn:
    • Añade un nodo Node2D como raíz (renómbralo a Level1).
    • Añade un Label y establece su Text a "Nivel 1".
    • Añade un Button y establece su Text a "Fin de Juego".
    • Guarda la escena como res://Scenes/Level1.tscn.
  4. Crea la escena GameOver.tscn:
    • Añade un nodo Control como raíz (renómbralo a GameOver).
    • Añade un Label y establece su Text a "¡Juego Terminado!".
    • Añade un Button y establece su Text a "Volver al Menú".
    • Guarda la escena como res://Scenes/GameOver.tscn.
💡 Consejo: Organiza tus escenas en una carpeta `Scenes` dentro de tu proyecto para mantener todo ordenado.

2. Implementando el Cambio de Escena

Godot proporciona varios métodos para cambiar de escena. Los dos más comunes son:

  • get_tree().change_scene_to_file(path): Carga una nueva escena desde un archivo *.tscn y la establece como la escena actual.
  • get_tree().change_scene_to_packed(packed_scene): Similar al anterior, pero toma una PackedScene (una escena pre-cargada) como argumento. Esto es más eficiente para transiciones frecuentes.

Vamos a usar change_scene_to_file por ahora, ya que es más sencillo para empezar.

Modificando MainMenu.tscn:

  1. Añade un nuevo script a tu nodo raíz MainMenu (asegúrate de que herede de Control).
  2. Conecta la señal pressed del botón "Iniciar Juego" a una nueva función en el script (_on_Iniciar_Juego_pressed).
# MainMenu.gd
extends Control

func _on_Iniciar_Juego_pressed():
    get_tree().change_scene_to_file("res://Scenes/Level1.tscn")

Modificando Level1.tscn:

  1. Añade un nuevo script a tu nodo raíz Level1 (hereda de Node2D).
  2. Conecta la señal pressed del botón "Fin de Juego" a una nueva función en el script (_on_Fin_de_Juego_pressed).
# Level1.gd
extends Node2D

func _on_Fin_de_Juego_pressed():
    get_tree().change_scene_to_file("res://Scenes/GameOver.tscn")

Modificando GameOver.tscn:

  1. Añade un nuevo script a tu nodo raíz GameOver (hereda de Control).
  2. Conecta la señal pressed del botón "Volver al Menú" a una nueva función en el script (_on_Volver_al_Menu_pressed).
# GameOver.gd
extends Control

func _on_Volver_al_Menu_pressed():
    get_tree().change_scene_to_file("res://Scenes/MainMenu.tscn")

3. Configurando la Escena Principal

Para que tu juego comience en el menú principal, debes configurarlo como la escena principal de tu proyecto:

  1. Ve a Proyecto -> Configuración del Proyecto.
  2. En la pestaña Aplicación -> Ejecutar, haz clic en el icono de la carpeta junto a Escena Principal y selecciona MainMenu.tscn.
  3. Cierra la ventana de configuración.

¡Ahora puedes ejecutar tu proyecto y probar la navegación básica entre escenas! 🚀

🔥 Importante: Godot tiene un límite en la cantidad de escenas que puede cargar en la memoria. Si cambias de escena repetidamente sin una gestión adecuada, podrías experimentar fugas de memoria. Esto nos lleva a la gestión de estado global.

✨ Parte 2: Gestión de Estado Global (Autoload / Singleton)

La gestión de estado global es crucial para persistir datos y lógica de juego entre escenas. En Godot, esto se logra principalmente a través de Autoloads, también conocidos como Singletons (instancias únicas que se cargan automáticamente al inicio del juego).

1. Creando un Singleton de Juego

Vamos a crear un script que manejará la puntuación del jugador y el nivel actual.

  1. Crea un nuevo script llamado GameManager.gd.
  2. Asegúrate de que no extienda de ningún nodo en particular, o extiende de Node para una instancia base.
# GameManager.gd
extends Node

var player_score = 0
var current_level = 1

func add_score(amount: int):
    player_score += amount
    print("Puntuación actual: ", player_score)

func advance_level():
    current_level += 1
    print("Nivel actual: ", current_level)

func reset_game_state():
    player_score = 0
    current_level = 1
    print("Estado del juego reseteado.")

2. Configurando el Script como Autoload

Para que GameManager.gd esté disponible globalmente en todas las escenas:

  1. Ve a Proyecto -> Configuración del Proyecto.
  2. En la pestaña Autocarga:
    • Haz clic en el icono de la carpeta junto a Ruta y selecciona GameManager.gd.
    • En el campo Nombre del Nodo, escribe GameManager (o el nombre que prefieras para referirte a él en tu código). Asegúrate de que Habilitar esté marcado.
    • Haz clic en Añadir.
  3. Cierra la ventana de configuración.

¡Listo! Ahora puedes acceder a GameManager desde cualquier script en tu juego usando su nombre (por ejemplo, GameManager.player_score).

💡 Consejo: Los Autoloads son nodos globales que se añaden automáticamente al árbol de escenas como hijos del nodo "root" oculto. Permanecen activos durante toda la vida del juego.

3. Usando el Singleton en Nuestras Escenas

Vamos a modificar nuestras escenas para interactuar con GameManager.

Modificando Level1.tscn (script Level1.gd):

Queremos que al pasar el nivel, se añada puntuación y se avance el nivel.

  1. Añade un Label a Level1 para mostrar la puntuación.
  2. Añade un Button llamado "Añadir Puntos".
  3. Conecta la señal pressed del botón "Añadir Puntos" a _on_Add_Score_pressed.
# Level1.gd
extends Node2D

@onready var score_label = $ScoreLabel # Asegúrate de tener un Label llamado ScoreLabel

func _ready():
    update_score_display()

func _on_Fin_de_Juego_pressed():
    # Simula que completamos el nivel antes de ir a Game Over
    GameManager.add_score(100)
    GameManager.advance_level()
    get_tree().change_scene_to_file("res://Scenes/GameOver.tscn")

func _on_Add_Score_pressed():
    GameManager.add_score(50) # Añade 50 puntos al presionar el botón
    update_score_display()

func update_score_display():
    score_label.text = "Puntuación: %s | Nivel: %s" % [GameManager.player_score, GameManager.current_level]

Modificando GameOver.tscn (script GameOver.gd):

Queremos mostrar la puntuación final y resetear el estado del juego al volver al menú.

  1. Añade un Label a GameOver para mostrar la puntuación final.
# GameOver.gd
extends Control

@onready var final_score_label = $FinalScoreLabel # Asegúrate de tener un Label llamado FinalScoreLabel

func _ready():
    final_score_label.text = "¡Juego Terminado! Puntuación Final: %s" % GameManager.player_score

func _on_Volver_al_Menu_pressed():
    GameManager.reset_game_state() # Resetea la puntuación y el nivel
    get_tree().change_scene_to_file("res://Scenes/MainMenu.tscn")

Ahora, cuando ejecutes el juego:

  • Inicia en el menú.
  • Ve al Nivel 1, presiona "Añadir Puntos" varias veces.
  • Ve a la pantalla de Fin de Juego, verás la puntuación persistente.
  • Vuelve al Menú, y la puntuación se reseteará.
80% Completado

🔄 Parte 3: Transiciones de Escena Avanzadas (Carga Asíncrona y Pantallas de Carga)

Cambiar de escena instantáneamente puede ser abrupto. A menudo querrás añadir efectos visuales o una pantalla de carga si la escena es compleja.

1. Carga Asíncrona con load_threaded_request

Para escenas grandes, cargar de golpe puede congelar el juego. load_threaded_request permite cargar la escena en un hilo separado, manteniendo la interfaz de usuario responsiva.

Primero, necesitamos una LoadingScreen.tscn:

  1. Crea una nueva escena LoadingScreen.tscn (raíz Control).
  2. Añade un Label para mostrar "Cargando..." y, opcionalmente, una ProgressBar.
  3. Añade un script a LoadingScreen.
# LoadingScreen.gd
extends Control

@onready var progress_bar = $ProgressBar # Si tienes una barra de progreso
@onready var message_label = $MessageLabel # Si tienes un label de mensaje

var target_scene_path: String
var load_thread_rid: RID # Resource ID for the threaded load

func _ready():
    if progress_bar: progress_bar.value = 0
    if message_label: message_label.text = "Cargando..."
    set_process(true) # Habilitar _process para verificar el progreso

func set_target_scene(path: String):
    target_scene_path = path
    load_thread_rid = ResourceLoader.load_threaded_request(path)
    # Opcional: conectar la señal de progreso si quieres actualizaciones más finas
    # ResourceLoader.load_threaded_get_status(load_thread_rid) puede usarse en _process

func _process(delta):
    if load_thread_rid:
        var status = ResourceLoader.load_threaded_get_status(load_thread_rid)
        var progress = ResourceLoader.load_threaded_get_progress(load_thread_rid)

        if progress_bar:
            progress_bar.value = progress.progress * 100

        if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
            if message_label: message_label.text = "Cargando... (%d%%)" % int(progress.progress * 100)
        elif status == ResourceLoader.THREAD_LOAD_LOADED:
            var new_scene = ResourceLoader.load_threaded_get(load_thread_rid)
            if new_scene is PackedScene:
                get_tree().change_scene_to_packed(new_scene) # Cambiar a la escena cargada
            else:
                print("Error: La escena cargada no es PackedScene.")
            load_thread_rid = null # Limpiar el RID
            set_process(false) # Deshabilitar _process
        elif status == ResourceLoader.THREAD_LOAD_FAILED:
            if message_label: message_label.text = "Error al cargar la escena."
            print("Fallo al cargar la escena: ", target_scene_path)
            load_thread_rid = null
            set_process(false)

Ahora, para usar esta pantalla de carga, modificaríamos la forma en que MainMenu va a Level1:

# MainMenu.gd (modificado para carga asíncrona)
extends Control

var LoadingScreen = preload("res://Scenes/LoadingScreen.tscn")

func _on_Iniciar_Juego_pressed():
    var loading_screen_instance = LoadingScreen.instantiate()
    get_tree().root.add_child(loading_screen_instance)
    loading_screen_instance.set_target_scene("res://Scenes/Level1.tscn")
    # No necesitamos cambiar la escena aquí, la pantalla de carga lo hará
    # get_tree().change_scene_to_file("res://Scenes/Level1.tscn") # Eliminar o comentar esta línea

Esto inserta la LoadingScreen en el árbol de escenas encima de la MainMenu, y la LoadingScreen se encarga de cargar la siguiente escena y luego reemplazarse a sí misma.

Paso 1: Jugador presiona 'Iniciar'.
Paso 2: Se instancia y añade `LoadingScreen` al árbol.
Paso 3: `LoadingScreen` inicia carga asíncrona de `Level1`.
Paso 4: Cuando `Level1` está cargado, `LoadingScreen` cambia la escena activa a `Level1` y se elimina.

2. Transiciones con Efectos (Fades)

Para transiciones más pulidas, puedes añadir un efecto de fade in/out. Esto generalmente implica un CanvasLayer con un ColorRect que cambia su opacidad.

Creando un TransitionManager (Singleton):

Podríamos encapsular la lógica de transición en otro Autoload llamado TransitionManager.gd.

  1. Crea un nuevo script TransitionManager.gd que extiende CanvasLayer.
  2. Añádelo como Autoload, con nombre TransitionManager.
  3. En este TransitionManager tendrías un ColorRect como hijo, y scripts para animar su alfa.
# TransitionManager.gd (parte del Autoload)
extends CanvasLayer

@onready var color_rect = $ColorRect
@onready var animation_player = $AnimationPlayer # Asume que tienes un AnimationPlayer

signal transition_finished

func _ready():
    layer = 100 # Asegurarse de que esté encima de todo
    color_rect.color = Color(0, 0, 0, 0) # Empezar transparente
    # Configura AnimationPlayer con animaciones 'fade_in' y 'fade_out'
    # 'fade_in': color_rect.color.a de 0 a 1
    # 'fade_out': color_rect.color.a de 1 a 0
    # Conecta la señal 'animation_finished' del AnimationPlayer a _on_animation_finished

func fade_to_black():
    animation_player.play("fade_in")

func fade_from_black():
    animation_player.play("fade_out")

func _on_animation_finished(anim_name: String):
    if anim_name == "fade_in":
        emit_signal("transition_finished") # Señal para indicar que la pantalla está en negro

Modificando la navegación para usar el TransitionManager:

# Ejemplo: MainMenu.gd modificado para usar TransitionManager
extends Control

func _on_Iniciar_Juego_pressed():
    TransitionManager.fade_to_black()
    await TransitionManager.transition_finished # Espera a que la pantalla se ponga en negro
    get_tree().change_scene_to_file("res://Scenes/Level1.tscn")
    TransitionManager.fade_from_black() # Fade out de la nueva escena

Esto crea una transición suave en lugar de un corte abrupto. Combinar esto con la carga asíncrona es una práctica común para transiciones de nivel complejas.

¿Qué es un CanvasLayer?Un `CanvasLayer` es un nodo que permite renderizar sus hijos en una capa de dibujo separada, por encima o por debajo de la capa de dibujo principal del viewport. Es ideal para elementos de UI, HUDs, y transiciones de pantalla que deben superponerse a todo lo demás.

📝 Consideraciones Adicionales y Buenas Prácticas

Estructura de Proyecto con Singletons

Considera tener varios singletons, cada uno con una responsabilidad clara:

  • GameManager: Puntuación, nivel actual, estado del juego.
  • AudioManager: Gestión de sonido y música global.
  • InputManager: Mapeo de entradas personalizadas (si aplica).
  • SaveLoadManager: Guardar y cargar partidas.
⚠️ Advertencia: No abuses de los singletons. Demasiados singletons pueden llevar a un código difícil de mantener y depurar (acoplamiento fuerte). Úsalos solo para funcionalidades verdaderamente globales.

Gestión de Memoria

  • Cuando cambias de escena con change_scene_to_file o change_scene_to_packed, la escena anterior se libera de la memoria automáticamente (a menos que tengas referencias a ella en singletons). Sin embargo, recursos como texturas grandes o modelos 3D pueden permanecer cargados si son referenciados en singletons o en el nuevo árbol de escenas.
  • Para liberar memoria de forma explícita de recursos no utilizados, puedes usar ResourceLoader.flush_unused_resources() (con precaución) después de un cambio de escena.

Señales Globales

Los singletons son excelentes para emitir señales globales a las que otras escenas o nodos pueden conectarse. Por ejemplo, GameManager podría emitir una señal game_over cuando la salud del jugador llega a cero, y la GameOver.tscn podría conectarse a ella.

Ejemplo en GameManager.gd:

# GameManager.gd (con señal)
extends Node

signal game_over

var player_health = 100

func take_damage(amount: int):
    player_health -= amount
    if player_health <= 0:
        player_health = 0
        emit_signal("game_over")

Luego, en tu Level1.gd (o donde se gestione el jugador), podrías conectar la señal al GameOver directamente o a un método de tu Level1 para manejar la transición.

# Level1.gd (ejemplo de conexión)
extends Node2D

func _ready():
    GameManager.game_over.connect(_on_game_over)

func _on_game_over():
    get_tree().change_scene_to_file("res://Scenes/GameOver.tscn")

Esto promueve un diseño más modular, ya que los nodos no necesitan saber cómo se maneja el fin del juego, solo que el evento game_over ha ocurrido.


🚀 Conclusión

Dominar la navegación entre escenas y la gestión de estado global es fundamental para crear juegos complejos y pulidos en Godot 4. Hemos cubierto desde los conceptos básicos de cambio de escena hasta técnicas avanzadas como la carga asíncrona y el uso de singletons para gestionar datos persistentes. Al aplicar estas técnicas, puedes asegurar un flujo de juego fluido, una gestión de datos eficiente y una experiencia más agradable para tus jugadores.

Recuerda practicar estos conceptos y adaptarlos a las necesidades específicas de tus propios proyectos. ¡El límite es tu imaginación! ¡Feliz desarrollo! 🎮

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!