Objetivo del sistema de juego
En este capítulo vas a implementar un ciclo de juego completo y coherente: el jugador tiene vida, recolecta monedas que suman puntos, puede reiniciar al caer o recibir daño, y puede ganar al llegar a una meta. Para que el proyecto sea escalable, centralizaremos el estado en un GameManager y conectaremos el resto del juego mediante señales. La interfaz (UI) se actualizará automáticamente cuando cambien vida, monedas o puntuación.
Arquitectura recomendada (quién hace qué)
- GameManager: guarda el estado (vida, monedas, puntos), aplica reglas (daño, sumar puntos, victoria, reinicio) y emite señales para la UI.
- Jugador: detecta eventos (recibe daño, cae) y notifica al GameManager; no decide reglas globales.
- Moneda (Area2D): al ser recogida emite una señal o llama al GameManager para sumar.
- Meta (Area2D): al tocarla, notifica victoria al GameManager.
- UI (Control): muestra vida/monedas/puntos y mensajes; escucha señales del GameManager.
Paso a paso: crear el GameManager (Autoload)
1) Script GameManager.gd
Crea un script llamado GameManager.gd y añádelo como Autoload (Project Settings > Autoload). Así estará disponible como singleton en todo el juego.
extends Node
signal stats_changed(lives: int, coins: int, score: int)
signal game_over()
signal victory()
signal respawned()
@export var max_lives := 3
@export var points_per_coin := 10
var lives: int
var coins: int
var score: int
var spawn_position: Vector2
var current_level_path: String
func _ready() -> void:
reset_run()
func reset_run() -> void:
lives = max_lives
coins = 0
score = 0
emit_signal("stats_changed", lives, coins, score)
func register_level(level_node: Node) -> void:
# Guarda la escena actual para poder recargarla en reinicios “duros”.
current_level_path = level_node.scene_file_path
func register_spawn(pos: Vector2) -> void:
spawn_position = pos
func add_coin(amount := 1) -> void:
coins += amount
score += amount * points_per_coin
emit_signal("stats_changed", lives, coins, score)
func damage(amount := 1) -> void:
lives -= amount
lives = max(lives, 0)
emit_signal("stats_changed", lives, coins, score)
if lives <= 0:
emit_signal("game_over")
return
respawn_player()
func fell_out_of_world() -> void:
# Caer puede contar como daño o reinicio directo; aquí lo tratamos como daño.
damage(1)
func respawn_player() -> void:
# Este método no mueve al jugador directamente si no lo tienes referenciado.
# En su lugar, emitimos una señal para que el nivel/jugador reaccione.
emit_signal("respawned")
func win() -> void:
emit_signal("victory")
func hard_restart_level() -> void:
# Recarga completa de la escena actual (útil para restablecer monedas/enemigos).
if current_level_path != "":
get_tree().change_scene_to_file(current_level_path)2) ¿Por qué señales desde GameManager?
Porque la UI y el nivel no deberían estar consultando constantemente valores (polling). Con señales, el GameManager “empuja” cambios: cada vez que cambian vida/monedas/puntos, la UI se actualiza; cuando hay victoria o game over, el nivel puede pausar, mostrar paneles, etc.
UI: nodos y actualización automática
1) Estructura de UI (Control)
Crea una escena HUD.tscn con raíz Control. Ejemplo de jerarquía:
HUD (Control)TopBar (HBoxContainer)LifeIcon (TextureRect)LivesLabel (Label)CoinIcon (TextureRect)CoinsLabel (Label)ScoreLabel (Label)
CenterMessage (Label)(opcional para “Victoria” / “Game Over”)
Asigna texturas a los TextureRect (icono de corazón, moneda) y deja los Label listos para texto.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
2) Script del HUD escuchando al GameManager
extends Control
@onready var lives_label: Label = $TopBar/LivesLabel
@onready var coins_label: Label = $TopBar/CoinsLabel
@onready var score_label: Label = $TopBar/ScoreLabel
@onready var center_message: Label = $CenterMessage
func _ready() -> void:
center_message.visible = false
GameManager.stats_changed.connect(_on_stats_changed)
GameManager.game_over.connect(_on_game_over)
GameManager.victory.connect(_on_victory)
# Forzamos una primera actualización (por si el HUD aparece después)
_on_stats_changed(GameManager.lives, GameManager.coins, GameManager.score)
func _on_stats_changed(lives: int, coins: int, score: int) -> void:
lives_label.text = str(lives)
coins_label.text = str(coins)
score_label.text = "Puntos: %s" % score
func _on_game_over() -> void:
center_message.visible = true
center_message.text = "Game Over"
func _on_victory() -> void:
center_message.visible = true
center_message.text = "Victoria"Importante: el HUD no calcula nada; solo refleja el estado y reacciona a eventos.
Monedas: recolección con señales
1) Escena Coin.tscn (Area2D)
Crea una moneda como Area2D con CollisionShape2D y un Sprite2D. Configura la colisión para detectar al jugador. Puedes usar un grupo player en el nodo del jugador para identificarlo.
2) Script de la moneda
extends Area2D
signal collected
@export var coin_value := 1
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
if body.is_in_group("player"):
emit_signal("collected")
GameManager.add_coin(coin_value)
queue_free()Aquí se muestran dos enfoques a la vez: la moneda emite collected (útil si quieres efectos locales) y además notifica al GameManager para sumar. Si prefieres desacoplar más, puedes conectar collected desde el nivel y que el nivel llame a GameManager.add_coin().
Daño y reinicio: señal “jugador herido” y caída
1) Jugador: emitir “herido” al recibir daño
En tu jugador, añade una señal y un método para ser dañado. La fuente de daño (enemigo, pinchos, proyectil) solo llama a player.take_damage().
# En el script del jugador
signal hurt(amount: int)
var invulnerable := false
func take_damage(amount := 1) -> void:
if invulnerable:
return
invulnerable = true
emit_signal("hurt", amount)
# Ventana de invulnerabilidad simple
await get_tree().create_timer(0.8).timeout
invulnerable = false2) Nivel: conectar la señal del jugador al GameManager
En la escena del nivel (por ejemplo Level01.tscn), conecta el jugador al GameManager. Esto mantiene al jugador sin lógica global.
# Script del nivel (Node2D)
@onready var player = $Player
@onready var spawn = $SpawnPoint
func _ready() -> void:
GameManager.register_level(self)
GameManager.register_spawn(spawn.global_position)
player.hurt.connect(_on_player_hurt)
GameManager.respawned.connect(_on_respawned)
func _on_player_hurt(amount: int) -> void:
GameManager.damage(amount)
func _on_respawned() -> void:
# Respawn “suave”: reposicionar y limpiar velocidad/estado.
player.global_position = GameManager.spawn_position
if player.has_method("set_velocity"):
player.set_velocity(Vector2.ZERO)
# Alternativa típica si es CharacterBody2D:
if "velocity" in player:
player.velocity = Vector2.ZERONota: si tu jugador es CharacterBody2D, normalmente basta con player.velocity = Vector2.ZERO. Ajusta según tu implementación.
3) Caída fuera del mundo (DeathZone)
Crea un Area2D grande debajo del nivel llamado DeathZone con colisión. Cuando el jugador entra, se considera caída.
# Script en DeathZone (Area2D)
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
if body.is_in_group("player"):
GameManager.fell_out_of_world()Esto evita depender de “si y < -1000” y hace el nivel más controlable.
Condición de victoria: llegar a una meta
1) Escena Goal.tscn (Area2D)
Crea una meta como Area2D con su colisión. Al tocarla el jugador, se dispara la victoria.
extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
if body.is_in_group("player"):
GameManager.win()2) Reacción del nivel ante victoria
Decide qué ocurre al ganar: pausar, mostrar panel, bloquear input, pasar de nivel. Aquí haremos algo simple: pausar y mostrar el mensaje del HUD (ya conectado).
# En el script del nivel
func _ready() -> void:
GameManager.victory.connect(_on_victory)
GameManager.game_over.connect(_on_game_over)
func _on_victory() -> void:
get_tree().paused = true
func _on_game_over() -> void:
get_tree().paused = trueSi pausas el árbol, recuerda que algunos nodos UI podrían necesitar process_mode en Always si quieres animaciones; para texto estático no hace falta.
Reinicio: suave vs recarga de escena
Reinicio suave (recomendado para “perder una vida”)
- Reposiciona al jugador en
SpawnPoint. - Resetea velocidad y estados temporales (invulnerabilidad, salto, etc.).
- No recrea monedas ya recogidas (se mantienen recogidas).
Ya lo implementaste con GameManager.respawned y el handler del nivel.
Reinicio duro (útil para “Game Over” o botón de reinicio)
Recarga la escena del nivel para restablecer monedas/enemigos/objetos a su estado original. Puedes hacerlo al recibir game_over o desde un botón en UI.
# Ejemplo: botón en UI (Control) para reiniciar
func _on_restart_button_pressed() -> void:
get_tree().paused = false
GameManager.reset_run()
GameManager.hard_restart_level()Si usas reinicio duro, asegúrate de llamar a reset_run() para restaurar vida/monedas/puntos.
Pruebas rápidas (checklist) para validar el sistema
1) La UI refleja cambios
- Al iniciar el nivel,
LivesLabelmuestramax_lives, monedas 0 y puntos 0. - Al recoger 1 moneda: monedas +1, puntos +
points_per_coin. - Al recoger varias monedas: el conteo coincide con el número recogido y el puntaje escala correctamente.
2) Daño y respawn
- Al recibir daño: la vida baja en 1 y la UI se actualiza inmediatamente.
- Si la vida sigue > 0: el jugador reaparece en el
SpawnPointy su velocidad queda en cero (no “sale disparado”). - Si hay invulnerabilidad: dos golpes seguidos dentro de 0.8s no deben restar dos vidas.
3) Caída fuera del mundo
- Al entrar en
DeathZone: se aplica la misma regla que daño (vida -1 y respawn). - Repite hasta llegar a 0 vidas: debe dispararse
game_overy mostrarse el mensaje correspondiente.
4) Reinicio restablece el estado
- Tras
Game Over, al ejecutar reinicio duro: vida vuelve amax_lives, monedas 0, puntos 0. - Las monedas vuelven a aparecer (porque la escena se recargó).
- La UI vuelve a los valores iniciales sin necesidad de “refrescar manualmente”.
5) Victoria
- Al tocar la meta: se emite
victory, aparece el mensaje en UI y el juego se pausa (o se bloquea el control, según tu decisión). - Verifica que no se pueda seguir sumando monedas o recibiendo daño si el juego está pausado (si no pausas, desactiva colisiones o input).
Tabla de señales y conexiones sugeridas
| Emisor | Señal | Receptor | Uso |
|---|---|---|---|
| GameManager | stats_changed | HUD | Actualizar labels de vida/monedas/puntos |
| GameManager | respawned | Nivel | Reposicionar jugador en spawn |
| GameManager | game_over | HUD / Nivel | Mostrar mensaje y pausar / reiniciar |
| GameManager | victory | HUD / Nivel | Mostrar mensaje y pausar / cambiar de nivel |
| Jugador | hurt | Nivel | Delegar daño al GameManager |
| Moneda | collected | (Opcional) Nivel | Efectos locales; el puntaje lo maneja GameManager |