Capítulo 5: GDScript Intermedio - POO, Señales y Patrones
Capítulo 5: GDScript Intermedio - POO, Señales y Patrones
Ahora que dominas los fundamentos, es momento de profundizar en conceptos intermedios que te permitirán crear juegos más complejos y mejor organizados. Aprenderás programación orientada a objetos, señales avanzadas, programación asíncrona y patrones de diseño específicos para gamedev.
Programación Orientada a Objetos (POO)
Clases y Objetos
En Godot, cada script es una clase que extiende otra:
# Definir una clase personalizada
class_name Personaje
extends Node2D
# Propiedades (variables de instancia)
var nombre: String = "Sin nombre"
var vida: int = 100
var vida_maxima: int = 100
var nivel: int = 1
var experiencia: int = 0
# Constructor
func _init(p_nombre: String = "Héroe"):
nombre = p_nombre
print("Personaje ", nombre, " creado")
# Métodos
func recibir_dano(cantidad: int) -> void:
vida -= cantidad
vida = max(0, vida) # No puede ser negativo
print(nombre, " recibió ", cantidad, " de daño")
if vida == 0:
morir()
func curar(cantidad: int) -> void:
vida = min(vida + cantidad, vida_maxima)
print(nombre, " se curó ", cantidad, " puntos")
func morir() -> void:
print(nombre, " ha muerto")
queue_free() # Elimina el nodo del árbol
Herencia
La herencia permite crear clases especializadas:
# Clase base: guerrero.gd
class_name Guerrero
extends Personaje
var fuerza: int = 15
var defensa: int = 10
var arma_equipada: String = "Espada"
func _init():
super._init("Guerrero") # Llama al constructor padre
vida_maxima = 150
vida = vida_maxima
func atacar() -> int:
var dano_base = 10
var dano_total = dano_base + fuerza
print(nombre, " ataca con ", arma_equipada, " causando ", dano_total, " de daño")
return dano_total
func defender(dano_entrante: int) -> int:
var dano_reducido = max(1, dano_entrante - defensa)
recibir_dano(dano_reducido)
return dano_reducido
# Sobrescribir método del padre
func morir() -> void:
print("¡El valiente guerrero ", nombre, " ha caído en batalla!")
soltar_items()
super.morir() # Llama al método original
func soltar_items():
print("Soltando espada y escudo...")
# Clase heredada: mago.gd
class_name Mago
extends Personaje
var mana: int = 100
var mana_maximo: int = 100
var poder_magico: int = 20
var hechizos_conocidos: Array[String] = ["Bola de fuego", "Escudo mágico"]
func _init():
super._init("Mago")
vida_maxima = 80 # Menos vida que un guerrero
vida = vida_maxima
func lanzar_hechizo(hechizo: String, objetivo: Node = null) -> void:
if hechizo not in hechizos_conocidos:
print("No conozco ese hechizo")
return
match hechizo:
"Bola de fuego":
if mana >= 20:
mana -= 20
var dano = poder_magico * 2
print("¡Lanzando bola de fuego! Daño: ", dano)
if objetivo and objetivo.has_method("recibir_dano"):
objetivo.recibir_dano(dano)
else:
print("No tengo suficiente mana")
"Escudo mágico":
if mana >= 30:
mana -= 30
print("¡Escudo mágico activado!")
# Lógica del escudo
_:
print("Hechizo no implementado")
func regenerar_mana(cantidad: int):
mana = min(mana + cantidad, mana_maximo)
Encapsulación con Getters y Setters
class_name Jugador
extends CharacterBody2D
# Variable privada (convención con _)
var _vida: int = 100
var _vida_maxima: int = 100
var _nivel: int = 1
var _experiencia: int = 0
var _exp_siguiente_nivel: int = 100
# Señales para notificar cambios
signal vida_cambiada(nueva_vida, vida_maxima)
signal nivel_subido(nuevo_nivel)
signal muerto()
# Getter y setter para vida
var vida: int:
get:
return _vida
set(value):
var vida_anterior = _vida
_vida = clamp(value, 0, _vida_maxima)
if _vida != vida_anterior:
vida_cambiada.emit(_vida, _vida_maxima)
if _vida == 0:
muerto.emit()
# Getter para porcentaje de vida
var vida_porcentaje: float:
get:
return float(_vida) / float(_vida_maxima)
# Propiedad de solo lectura
var esta_muerto: bool:
get:
return _vida <= 0
# Experiencia con auto-levelup
var experiencia: int:
get:
return _experiencia
set(value):
_experiencia = value
while _experiencia >= _exp_siguiente_nivel:
subir_nivel()
func subir_nivel():
_nivel += 1
_experiencia -= _exp_siguiente_nivel
_exp_siguiente_nivel = calcular_exp_necesaria(_nivel)
_vida_maxima += 10
_vida = _vida_maxima # Cura completa al subir de nivel
nivel_subido.emit(_nivel)
print("¡Subiste al nivel ", _nivel, "!")
func calcular_exp_necesaria(nivel: int) -> int:
return nivel * 100 # Fórmula simple
Composición vs Herencia
A veces es mejor usar composición (tener componentes) que herencia:
# Sistema de componentes - componente_salud.gd
class_name ComponenteSalud
extends Node
@export var vida_maxima: int = 100
var vida_actual: int
signal vida_cambiada(nueva_vida, vida_maxima)
signal muerto()
signal danado(cantidad)
signal curado(cantidad)
func _ready():
vida_actual = vida_maxima
func recibir_dano(cantidad: int) -> void:
if vida_actual <= 0:
return
var dano_real = min(cantidad, vida_actual)
vida_actual -= dano_real
danado.emit(dano_real)
vida_cambiada.emit(vida_actual, vida_maxima)
if vida_actual <= 0:
muerto.emit()
func curar(cantidad: int) -> void:
if vida_actual >= vida_maxima:
return
var curacion_real = min(cantidad, vida_maxima - vida_actual)
vida_actual += curacion_real
curado.emit(curacion_real)
vida_cambiada.emit(vida_actual, vida_maxima)
func obtener_porcentaje() -> float:
return float(vida_actual) / float(vida_maxima)
# Usar componentes - enemigo.gd
extends CharacterBody2D
@onready var salud: ComponenteSalud = $ComponenteSalud
@onready var ataque: ComponenteAtaque = $ComponenteAtaque
@onready var ia: ComponenteIA = $ComponenteIA
func _ready():
# Conectar señales del componente
salud.muerto.connect(_on_muerte)
salud.danado.connect(_on_danado)
ia.objetivo_detectado.connect(_on_objetivo_detectado)
func _on_muerte():
print("Enemigo eliminado")
crear_drop()
queue_free()
func _on_danado(cantidad):
mostrar_numero_dano(cantidad)
parpadear()
func _on_objetivo_detectado(objetivo):
ataque.establecer_objetivo(objetivo)
Señales Avanzadas
Señales con Parámetros Tipados
class_name SistemaEventos
extends Node
# Señales con tipos específicos
signal item_recogido(item: Item, cantidad: int)
signal enemigo_eliminado(enemigo: Enemigo, exp_ganada: int, items: Array[Item])
signal quest_completada(quest_id: String, recompensas: Dictionary)
# Emitir con validación
func reportar_item_recogido(item: Item, cantidad: int = 1):
if item == null or cantidad <= 0:
push_error("Datos de item inválidos")
return
item_recogido.emit(item, cantidad)
# Conectar con validación
func conectar_eventos(nodo: Node):
if nodo.has_signal("item_recogido"):
nodo.item_recogido.connect(_on_item_recogido)
Buses de Señales (Event Bus)
# Autoload: EventBus.gd
extends Node
# Señales globales del juego
signal jugador_murio()
signal nivel_completado(tiempo: float, puntuacion: int)
signal boss_derrotado(boss_nombre: String)
signal item_desbloqueado(item_id: String)
signal logro_obtenido(logro_id: String)
# Sistema de suscripción
var suscriptores: Dictionary = {}
func suscribir(evento: String, objeto: Object, metodo: String):
if not suscriptores.has(evento):
suscriptores[evento] = []
suscriptores[evento].append({
"objeto": objeto,
"metodo": metodo
})
func emitir_evento(evento: String, datos: Dictionary = {}):
if not suscriptores.has(evento):
return
for suscriptor in suscriptores[evento]:
if is_instance_valid(suscriptor.objeto):
suscriptor.objeto.call(suscriptor.metodo, datos)
# Uso desde cualquier script
func _ready():
EventBus.jugador_murio.connect(_on_jugador_muerto)
EventBus.nivel_completado.connect(_on_nivel_completado)
Señales One-Shot y Desconexión
extends Node
func _ready():
# Conectar one-shot (se desconecta después de una emisión)
timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
# Conectar con referencia para desconectar después
var conexion = player.vida_cambiada.connect(_on_vida_cambiada)
# Desconectar más tarde
player.vida_cambiada.disconnect(_on_vida_cambiada)
# Verificar si está conectado
if player.vida_cambiada.is_connected(_on_vida_cambiada):
print("Aún conectado")
# Conexión diferida (se ejecuta al final del frame)
func conectar_diferido():
player.muerto.connect(_on_player_muerto, CONNECT_DEFERRED)
Programación Asíncrona con await
Await Básico
# Esperar tiempo
func accion_con_delay():
print("Iniciando...")
await get_tree().create_timer(2.0).timeout
print("2 segundos después")
await get_tree().create_timer(1.0).timeout
print("1 segundo más")
# Esperar señal
func esperar_entrada():
print("Presiona cualquier tecla...")
await self.gui_input
print("¡Tecla presionada!")
# Esperar animación
func reproducir_animacion_completa():
$AnimationPlayer.play("ataque")
await $AnimationPlayer.animation_finished
print("Animación terminada")
Coroutines Complejas
# Sistema de diálogos con await
class_name SistemaDialogo
extends Control
signal dialogo_terminado
signal opcion_seleccionada(indice: int)
func mostrar_dialogo(texto: String, velocidad: float = 0.05):
visible = true
var label = $DialogBox/Text
label.text = ""
# Mostrar texto letra por letra
for letra in texto:
label.text += letra
await get_tree().create_timer(velocidad).timeout
# Permitir saltar el texto
if Input.is_action_just_pressed("ui_accept"):
label.text = texto
break
# Esperar confirmación
await self.gui_input
visible = false
dialogo_terminado.emit()
func mostrar_opciones(pregunta: String, opciones: Array[String]) -> int:
await mostrar_dialogo(pregunta)
# Crear botones de opciones
for i in range(opciones.size()):
var boton = Button.new()
boton.text = opciones[i]
boton.pressed.connect(_on_opcion_presionada.bind(i))
$OpcionesContainer.add_child(boton)
# Esperar selección
var indice_seleccionado = await opcion_seleccionada
# Limpiar botones
for child in $OpcionesContainer.get_children():
child.queue_free()
return indice_seleccionado
func _on_opcion_presionada(indice: int):
opcion_seleccionada.emit(indice)
Secuencias de Acciones
# Secuencia de batalla
func ejecutar_turno_enemigo():
# Animación de preparación
$Sprite2D.modulate = Color.RED
await get_tree().create_timer(0.5).timeout
# Mover hacia el jugador
var tween = create_tween()
tween.tween_property(self, "position", jugador.position, 0.3)
await tween.finished
# Atacar
$AnimationPlayer.play("attack")
await $AnimationPlayer.animation_finished
jugador.recibir_dano(ataque)
# Volver a posición
tween = create_tween()
tween.tween_property(self, "position", posicion_inicial, 0.3)
await tween.finished
$Sprite2D.modulate = Color.WHITE
turno_terminado.emit()
# Cadena de habilidades
func combo_ataque():
await ejecutar_habilidad("golpe_rapido")
await ejecutar_habilidad("golpe_fuerte")
if critico_disponible:
await ejecutar_habilidad("golpe_critico")
print("Combo completado")
Manejo de Archivos
Guardar y Cargar Datos
# Sistema de guardado
class_name SaveGame
extends Resource
@export var nivel_actual: int = 1
@export var posicion_jugador: Vector2
@export var vida_jugador: int = 100
@export var inventario: Array[String] = []
@export var tiempo_jugado: float = 0.0
@export var fecha_guardado: String = ""
static func guardar_juego(slot: int = 1) -> bool:
var save_game = SaveGame.new()
# Recopilar datos del juego
save_game.nivel_actual = GameManager.nivel_actual
save_game.posicion_jugador = GameManager.jugador.position
save_game.vida_jugador = GameManager.jugador.vida
save_game.inventario = GameManager.inventario.items
save_game.tiempo_jugado = GameManager.tiempo_total
save_game.fecha_guardado = Time.get_datetime_string_from_system()
# Guardar archivo
var ruta = "user://savegame_%d.tres" % slot
var resultado = ResourceSaver.save(save_game, ruta)
return resultado == OK
static func cargar_juego(slot: int = 1) -> SaveGame:
var ruta = "user://savegame_%d.tres" % slot
if not FileAccess.file_exists(ruta):
return null
var save_game = load(ruta) as SaveGame
return save_game
static func existe_guardado(slot: int = 1) -> bool:
return FileAccess.file_exists("user://savegame_%d.tres" % slot)
static func borrar_guardado(slot: int = 1) -> bool:
var ruta = "user://savegame_%d.tres" % slot
if FileAccess.file_exists(ruta):
var dir = DirAccess.open("user://")
return dir.remove(ruta) == OK
return false
Archivos JSON
# Guardar configuración en JSON
func guardar_configuracion(config: Dictionary):
var archivo = FileAccess.open("user://config.json", FileAccess.WRITE)
if archivo:
var json_string = JSON.stringify(config, "\t")
archivo.store_string(json_string)
archivo.close()
return true
return false
# Cargar configuración desde JSON
func cargar_configuracion() -> Dictionary:
if not FileAccess.file_exists("user://config.json"):
return obtener_config_default()
var archivo = FileAccess.open("user://config.json", FileAccess.READ)
if archivo:
var json_string = archivo.get_as_text()
archivo.close()
var json = JSON.new()
var parse_result = json.parse(json_string)
if parse_result == OK:
return json.data
else:
push_error("Error parseando JSON: " + json.get_error_message())
return obtener_config_default()
func obtener_config_default() -> Dictionary:
return {
"volumen_master": 1.0,
"volumen_musica": 0.8,
"volumen_fx": 1.0,
"pantalla_completa": false,
"resolucion": "1280x720",
"calidad_graficos": "medio",
"controles": {
"arriba": "W",
"abajo": "S",
"izquierda": "A",
"derecha": "D",
"saltar": "Space",
"atacar": "Mouse1"
}
}
Sistema de Logs
# Logger personalizado
class_name Logger
extends RefCounted
enum LogLevel {
DEBUG,
INFO,
WARNING,
ERROR
}
static var archivo_log: FileAccess
static var nivel_minimo: LogLevel = LogLevel.INFO
static func inicializar():
var fecha = Time.get_datetime_string_from_system().replace(":", "-")
var ruta = "user://logs/game_log_%s.txt" % fecha
# Crear directorio si no existe
var dir = DirAccess.open("user://")
if not dir.dir_exists("logs"):
dir.make_dir("logs")
archivo_log = FileAccess.open(ruta, FileAccess.WRITE)
static func log(nivel: LogLevel, mensaje: String, categoria: String = "General"):
if nivel < nivel_minimo:
return
var timestamp = Time.get_time_string_from_system()
var nivel_str = ["DEBUG", "INFO", "WARNING", "ERROR"][nivel]
var linea = "[%s] [%s] [%s] %s" % [timestamp, nivel_str, categoria, mensaje]
# Escribir en archivo
if archivo_log:
archivo_log.store_line(linea)
archivo_log.flush()
# También imprimir en consola
match nivel:
LogLevel.ERROR:
push_error(linea)
LogLevel.WARNING:
push_warning(linea)
_:
print(linea)
static func debug(mensaje: String, categoria: String = "General"):
log(LogLevel.DEBUG, mensaje, categoria)
static func info(mensaje: String, categoria: String = "General"):
log(LogLevel.INFO, mensaje, categoria)
static func warning(mensaje: String, categoria: String = "General"):
log(LogLevel.WARNING, mensaje, categoria)
static func error(mensaje: String, categoria: String = "General"):
log(LogLevel.ERROR, mensaje, categoria)
Tipos Avanzados y Tipado Estático
Tipado Estático Completo
class_name SistemaInventario
extends Node
# Tipos personalizados
class Item:
var id: String
var nombre: String
var cantidad: int
var tipo: TipoItem
var rareza: Rareza
enum TipoItem {
CONSUMIBLE,
EQUIPABLE,
MATERIAL,
QUEST
}
enum Rareza {
COMUN,
RARO,
EPICO,
LEGENDARIO
}
# Arrays tipados
var items: Array[Item] = []
var slots: Array[SlotInventario] = []
# Funciones con tipos
func agregar_item(item: Item, cantidad: int = 1) -> bool:
var item_existente: Item = buscar_item(item.id)
if item_existente:
item_existente.cantidad += cantidad
return true
else:
if items.size() < capacidad_maxima:
var nuevo_item: Item = item.duplicate()
nuevo_item.cantidad = cantidad
items.append(nuevo_item)
return true
return false
func buscar_item(id: String) -> Item:
for item in items:
if item.id == id:
return item
return null
# Diccionarios tipados (Godot 4.2+)
var estadisticas: Dictionary = {
"fuerza": 10,
"agilidad": 15,
"inteligencia": 8
}
# Funciones lambda tipadas
var filtro_rareza: Callable = func(item: Item) -> bool:
return item.rareza >= Rareza.EPICO
func obtener_items_raros() -> Array[Item]:
return items.filter(filtro_rareza)
Genéricos con Variant
# Pool de objetos genérico
class_name ObjectPool
extends Node
var pool: Array = []
var tipo_objeto: PackedScene
var tamano_maximo: int
func _init(p_tipo: PackedScene, p_tamano: int = 10):
tipo_objeto = p_tipo
tamano_maximo = p_tamano
llenar_pool()
func llenar_pool():
for i in tamano_maximo:
var obj = tipo_objeto.instantiate()
obj.set_process(false)
obj.visible = false
add_child(obj)
pool.append(obj)
func obtener() -> Node:
for obj in pool:
if not obj.visible:
obj.set_process(true)
obj.visible = true
return obj
# Si no hay disponibles, crear uno nuevo
if pool.size() < tamano_maximo * 2: # Límite suave
var obj = tipo_objeto.instantiate()
add_child(obj)
pool.append(obj)
return obj
return null
func devolver(obj: Node):
obj.set_process(false)
obj.visible = false
obj.position = Vector2.ZERO
Patrones de Diseño en Godot
Singleton (Autoload)
# GameManager.gd - Autoload
extends Node
# Variables globales del juego
var puntuacion: int = 0
var nivel_actual: int = 1
var jugador_vidas: int = 3
var tiempo_total: float = 0.0
var configuracion: Dictionary = {}
# Referencias importantes
var jugador: Node = null
var camara: Camera2D = null
var ui_manager: Control = null
# Señales globales
signal juego_pausado()
signal juego_reanudado()
signal cambio_escena()
func _ready():
# Configuración inicial
process_mode = Node.PROCESS_MODE_ALWAYS
cargar_configuracion()
func _notification(what):
if what == NOTIFICATION_WM_CLOSE_REQUEST:
guardar_configuracion()
get_tree().quit()
func cambiar_escena(escena_path: String):
cambio_escena.emit()
await get_tree().create_timer(0.3).timeout
get_tree().change_scene_to_file(escena_path)
func reiniciar_nivel():
get_tree().reload_current_scene()
func pausar_juego():
get_tree().paused = true
juego_pausado.emit()
func reanudar_juego():
get_tree().paused = false
juego_reanudado.emit()
State Machine (Máquina de Estados)
# Estado base
class_name Estado
extends Node
var maquina_estados: MaquinaEstados
var personaje: CharacterBody2D
func entrar():
pass
func salir():
pass
func actualizar(delta: float):
pass
func fisica_actualizar(delta: float):
pass
func manejar_input(evento: InputEvent):
pass
# Máquina de estados
class_name MaquinaEstados
extends Node
@export var estado_inicial: Estado
var estado_actual: Estado
var estados: Dictionary = {}
var personaje: CharacterBody2D
func _ready():
personaje = get_parent()
# Registrar todos los estados hijos
for child in get_children():
if child is Estado:
estados[child.name] = child
child.maquina_estados = self
child.personaje = personaje
# Iniciar con el estado inicial
if estado_inicial:
estado_actual = estado_inicial
estado_actual.entrar()
func _process(delta):
if estado_actual:
estado_actual.actualizar(delta)
func _physics_process(delta):
if estado_actual:
estado_actual.fisica_actualizar(delta)
func _unhandled_input(event):
if estado_actual:
estado_actual.manejar_input(event)
func cambiar_estado(nombre_estado: String):
if not estados.has(nombre_estado):
push_error("Estado no encontrado: " + nombre_estado)
return
if estado_actual:
estado_actual.salir()
estado_actual = estados[nombre_estado]
estado_actual.entrar()
# Estados específicos
class_name EstadoIdle
extends Estado
func entrar():
personaje.velocity = Vector2.ZERO
personaje.get_node("AnimationPlayer").play("idle")
func fisica_actualizar(delta):
if Input.is_action_pressed("ui_left") or Input.is_action_pressed("ui_right"):
maquina_estados.cambiar_estado("Caminar")
if Input.is_action_just_pressed("jump") and personaje.is_on_floor():
maquina_estados.cambiar_estado("Saltar")
Observer Pattern con Señales
# Sistema de logros con Observer
class_name SistemaLogros
extends Node
signal logro_desbloqueado(logro_id: String, logro_data: Dictionary)
var logros: Dictionary = {
"primer_enemigo": {
"nombre": "Primer Sangre",
"descripcion": "Derrota tu primer enemigo",
"desbloqueado": false,
"condicion": {"enemigos_derrotados": 1}
},
"coleccionista": {
"nombre": "Coleccionista",
"descripcion": "Recoge 100 monedas",
"desbloqueado": false,
"condicion": {"monedas_totales": 100}
},
"superviviente": {
"nombre": "Superviviente",
"descripcion": "Sobrevive 5 minutos",
"desbloqueado": false,
"condicion": {"tiempo_supervivencia": 300}
}
}
var estadisticas: Dictionary = {
"enemigos_derrotados": 0,
"monedas_totales": 0,
"tiempo_supervivencia": 0
}
func _ready():
# Suscribirse a eventos del juego
EventBus.enemigo_eliminado.connect(_on_enemigo_eliminado)
EventBus.moneda_recogida.connect(_on_moneda_recogida)
func actualizar_estadistica(stat: String, valor: int):
if estadisticas.has(stat):
estadisticas[stat] += valor
verificar_logros()
func verificar_logros():
for logro_id in logros:
var logro = logros[logro_id]
if logro.desbloqueado:
continue
var cumple_condiciones = true
for condicion in logro.condicion:
if estadisticas.get(condicion, 0) < logro.condicion[condicion]:
cumple_condiciones = false
break
if cumple_condiciones:
desbloquear_logro(logro_id)
func desbloquear_logro(logro_id: String):
if logros.has(logro_id):
logros[logro_id].desbloqueado = true
logro_desbloqueado.emit(logro_id, logros[logro_id])
mostrar_notificacion_logro(logros[logro_id])
func _on_enemigo_eliminado(enemigo):
actualizar_estadistica("enemigos_derrotados", 1)
func _on_moneda_recogida(cantidad):
actualizar_estadistica("monedas_totales", cantidad)
Factory Pattern
# Fábrica de enemigos
class_name FabricaEnemigos
extends Node
var tipos_enemigos: Dictionary = {
"goblin": preload("res://enemies/Goblin.tscn"),
"orco": preload("res://enemies/Orco.tscn"),
"esqueleto": preload("res://enemies/Esqueleto.tscn"),
"dragon": preload("res://enemies/Dragon.tscn")
}
var configuracion_enemigos: Dictionary = {
"goblin": {
"vida": 30,
"velocidad": 100,
"dano": 5,
"experiencia": 10
},
"orco": {
"vida": 50,
"velocidad": 80,
"dano": 10,
"experiencia": 25
},
"esqueleto": {
"vida": 40,
"velocidad": 120,
"dano": 8,
"experiencia": 15
},
"dragon": {
"vida": 200,
"velocidad": 150,
"dano": 30,
"experiencia": 100
}
}
func crear_enemigo(tipo: String, posicion: Vector2) -> Node:
if not tipos_enemigos.has(tipo):
push_error("Tipo de enemigo desconocido: " + tipo)
return null
var enemigo = tipos_enemigos[tipo].instantiate()
enemigo.position = posicion
# Configurar estadísticas
if configuracion_enemigos.has(tipo):
var config = configuracion_enemigos[tipo]
if enemigo.has_method("configurar"):
enemigo.configurar(config)
return enemigo
func crear_oleada(tipos: Array[String], cantidad: int, area: Rect2) -> Array[Node]:
var enemigos = []
for i in cantidad:
var tipo = tipos[randi() % tipos.size()]
var pos = Vector2(
randf_range(area.position.x, area.end.x),
randf_range(area.position.y, area.end.y)
)
var enemigo = crear_enemigo(tipo, pos)
if enemigo:
enemigos.append(enemigo)
return enemigos
Optimización y Performance
Object Pooling
# Pool de proyectiles
class_name PoolProyectiles
extends Node
var pool_balas: Array[Node] = []
var tamano_pool: int = 100
var escena_bala: PackedScene = preload("res://projectiles/Bullet.tscn")
func _ready():
# Pre-crear balas
for i in tamano_pool:
var bala = escena_bala.instantiate()
bala.visible = false
bala.set_physics_process(false)
add_child(bala)
pool_balas.append(bala)
func obtener_bala() -> Node:
# Buscar bala inactiva
for bala in pool_balas:
if not bala.visible:
bala.visible = true
bala.set_physics_process(true)
return bala
# Si no hay disponibles, crear una nueva
var nueva_bala = escena_bala.instantiate()
add_child(nueva_bala)
pool_balas.append(nueva_bala)
return nueva_bala
func devolver_bala(bala: Node):
bala.visible = false
bala.set_physics_process(false)
bala.position = Vector2.ZERO
bala.velocity = Vector2.ZERO
Lazy Loading
class_name RecursoManager
extends Node
var recursos_cargados: Dictionary = {}
var cargando: Dictionary = {}
func obtener_recurso_async(ruta: String) -> Resource:
# Si ya está cargado, devolverlo
if recursos_cargados.has(ruta):
return recursos_cargados[ruta]
# Si se está cargando, esperar
if cargando.has(ruta):
await cargando[ruta]
return recursos_cargados[ruta]
# Cargar asíncronamente
cargando[ruta] = cargar_recurso_async(ruta)
var recurso = await cargando[ruta]
recursos_cargados[ruta] = recurso
cargando.erase(ruta)
return recurso
func cargar_recurso_async(ruta: String) -> Resource:
# Simular carga asíncrona
await get_tree().process_frame
return load(ruta)
func precargar_recursos(rutas: Array[String]):
for ruta in rutas:
obtener_recurso_async(ruta)
Ejercicios Prácticos
Ejercicio 1: Sistema de Combate con Estados
Crea un sistema de combate que use máquina de estados para idle, atacar, defender y esquivar.
Ejercicio 2: Sistema de Guardado Complejo
Implementa un sistema que guarde el estado completo del juego incluyendo posición de enemigos, items en el suelo y tiempo.
Ejercicio 3: Pool Manager Genérico
Crea un manager que pueda manejar pools de diferentes tipos de objetos (balas, enemigos, efectos).
Resumen del Capítulo
Has aprendido conceptos intermedios de GDScript:
- ✅ Programación orientada a objetos completa
- ✅ Herencia y composición
- ✅ Señales avanzadas y event bus
- ✅ Programación asíncrona con await
- ✅ Manejo de archivos y persistencia
- ✅ Patrones de diseño para juegos
- ✅ Optimización con object pooling
Próximo Capítulo
En el siguiente capítulo exploraremos conceptos avanzados de GDScript:
- Metaprogramación
- Plugins y tools
- Networking
- Shaders en GDScript
- Integración con C++
→ Capítulo 6: GDScript Avanzado
Proyecto: Crea un RPG simple con sistema de inventario, guardado/carga, estados de personaje y combate por turnos usando todo lo aprendido.