← Volver al listado de tecnologías

Capítulo 5: GDScript Intermedio - POO, Señales y Patrones

Por: Artiko
godotgdscriptintermediopooseñalesasyncpatrones

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:

Próximo Capítulo

En el siguiente capítulo exploraremos conceptos avanzados de GDScript:

→ 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.