← Volver al listado de tecnologías

Capítulo 6: GDScript Avanzado

Por: Artiko
godotgdscriptavanzadometaprogramaciónoptimización

Capítulo 6: GDScript Avanzado

Introducción

En este capítulo exploraremos las características más avanzadas de GDScript, incluyendo metaprogramación, reflexión, optimización de rendimiento, integración con C++, y patrones arquitectónicos complejos.

1. Metaprogramación y Reflexión

1.1 Reflexión en GDScript

# Inspección de objetos en tiempo de ejecución
class_name MetaExample
extends Node

func _ready():
    # Obtener información de la clase
    var class_info = get_class()
    print("Clase: ", class_info)
    
    # Listar todos los métodos
    var methods = get_method_list()
    for method in methods:
        print("Método: ", method.name)
    
    # Listar todas las propiedades
    var properties = get_property_list()
    for prop in properties:
        if prop.name.begins_with("_"):
            continue
        print("Propiedad: %s - Tipo: %s" % [prop.name, prop.type])
    
    # Verificar si tiene un método
    if has_method("custom_function"):
        call("custom_function")
    
    # Obtener y establecer propiedades dinámicamente
    set("position", Vector2(100, 100))
    var pos = get("position")
    
    # Conectar señales dinámicamente
    if has_signal("custom_signal"):
        connect("custom_signal", self, "_on_custom_signal")

1.2 Creación Dinámica de Objetos

# Factory dinámico con reflexión
class_name DynamicFactory
extends Node

# Registro de clases
var registered_classes = {}

func register_class(class_name: String, class_script: Script):
    registered_classes[class_name] = class_script

func create_instance(class_name: String, params: Dictionary = {}):
    if not registered_classes.has(class_name):
        push_error("Clase no registrada: " + class_name)
        return null
    
    var script = registered_classes[class_name]
    var instance = script.new()
    
    # Configurar propiedades dinámicamente
    for key in params:
        if instance.has_method("set_" + key):
            instance.call("set_" + key, params[key])
        elif key in instance:
            instance.set(key, params[key])
    
    return instance

# Uso
func _ready():
    # Registrar clases
    register_class("Enemy", preload("res://Enemy.gd"))
    register_class("PowerUp", preload("res://PowerUp.gd"))
    
    # Crear instancias dinámicamente
    var enemy = create_instance("Enemy", {
        "health": 100,
        "damage": 20,
        "position": Vector2(500, 300)
    })

1.3 Evaluación Dinámica de Expresiones

# Sistema de expresiones dinámicas
class_name ExpressionEvaluator
extends Node

var expression = Expression.new()

func evaluate_expression(expr_string: String, variables: Dictionary = {}):
    var error = expression.parse(expr_string, variables.keys())
    
    if error != OK:
        push_error("Error parseando expresión: " + expr_string)
        return null
    
    var result = expression.execute(variables.values(), self)
    
    if expression.has_execute_failed():
        push_error("Error ejecutando expresión: " + expr_string)
        return null
    
    return result

# Uso para sistema de condiciones dinámicas
func check_condition(condition: String, context: Dictionary) -> bool:
    var result = evaluate_expression(condition, context)
    return result if result is bool else false

func _ready():
    # Evaluar condiciones complejas
    var player_stats = {
        "health": 75,
        "mana": 50,
        "level": 10
    }
    
    var can_cast = check_condition(
        "health > 50 and mana >= 30 and level >= 5",
        player_stats
    )
    
    print("Puede lanzar hechizo: ", can_cast)

2. Sistemas de Plugins y Extensiones

2.1 Sistema de Plugins Modular

# Sistema de plugins base
class_name PluginSystem
extends Node

# Plugin base
class Plugin:
    var name: String
    var version: String
    var enabled: bool = true
    
    func initialize(host):
        pass
    
    func on_enable():
        pass
    
    func on_disable():
        pass
    
    func process(delta: float):
        pass

# Gestor de plugins
var plugins: Array[Plugin] = []
var plugin_directory = "user://plugins/"

func load_plugins():
    var dir = DirAccess.open(plugin_directory)
    if not dir:
        push_error("No se puede abrir directorio de plugins")
        return
    
    dir.list_dir_begin()
    var file_name = dir.get_next()
    
    while file_name != "":
        if file_name.ends_with(".gd"):
            _load_plugin(plugin_directory + file_name)
        file_name = dir.get_next()

func _load_plugin(path: String):
    var script = load(path)
    if not script:
        push_error("No se puede cargar plugin: " + path)
        return
    
    var plugin = script.new()
    if plugin is Plugin:
        plugin.initialize(self)
        plugins.append(plugin)
        print("Plugin cargado: ", plugin.name)

func enable_plugin(plugin_name: String):
    for plugin in plugins:
        if plugin.name == plugin_name:
            plugin.enabled = true
            plugin.on_enable()
            return

func disable_plugin(plugin_name: String):
    for plugin in plugins:
        if plugin.name == plugin_name:
            plugin.enabled = false
            plugin.on_disable()
            return

func _process(delta):
    for plugin in plugins:
        if plugin.enabled:
            plugin.process(delta)

2.2 Sistema de Hooks y Eventos

# Sistema avanzado de hooks
class_name HookSystem
extends Node

# Diccionario de hooks registrados
var hooks = {}

# Registrar un hook
func register_hook(hook_name: String, callback: Callable, priority: int = 0):
    if not hooks.has(hook_name):
        hooks[hook_name] = []
    
    hooks[hook_name].append({
        "callback": callback,
        "priority": priority
    })
    
    # Ordenar por prioridad
    hooks[hook_name].sort_custom(_sort_by_priority)

func _sort_by_priority(a, b):
    return a.priority > b.priority

# Ejecutar hooks
func execute_hook(hook_name: String, args: Array = []):
    if not hooks.has(hook_name):
        return
    
    var results = []
    for hook in hooks[hook_name]:
        var result = hook.callback.callv(args)
        results.append(result)
        
        # Si un hook retorna false, detener la cadena
        if result is bool and not result:
            break
    
    return results

# Filtro con hooks
func apply_filter(filter_name: String, value, args: Array = []):
    if not hooks.has(filter_name):
        return value
    
    var filtered_value = value
    for hook in hooks[filter_name]:
        var new_args = [filtered_value] + args
        filtered_value = hook.callback.callv(new_args)
    
    return filtered_value

# Ejemplo de uso
func _ready():
    # Registrar hooks
    register_hook("player_damaged", _on_player_damaged, 10)
    register_hook("player_damaged", _check_armor, 20)
    register_hook("player_damaged", _play_damage_sound, 5)
    
    # Registrar filtros
    register_hook("calculate_damage", _apply_resistance, 10)
    register_hook("calculate_damage", _apply_buffs, 5)
    
    # Ejecutar hook
    var damage = 50
    damage = apply_filter("calculate_damage", damage, ["fire"])
    execute_hook("player_damaged", [damage])

func _on_player_damaged(damage):
    print("Jugador recibió daño: ", damage)

func _check_armor(damage):
    print("Verificando armadura...")
    return damage > 10  # Continuar si el daño es significativo

func _play_damage_sound(damage):
    print("Reproduciendo sonido de daño")

func _apply_resistance(damage, damage_type):
    if damage_type == "fire":
        return damage * 0.5  # 50% resistencia al fuego
    return damage

func _apply_buffs(damage, damage_type):
    # Aplicar buffs activos
    return damage * 1.2  # 20% más daño

3. Optimización Avanzada

3.1 Profiling y Métricas

# Sistema de profiling personalizado
class_name Profiler
extends Node

static var instances = {}
static var metrics = {}

class ProfileTimer:
    var name: String
    var start_time: int
    var total_time: int = 0
    var call_count: int = 0
    var min_time: int = 9223372036854775807  # MAX_INT
    var max_time: int = 0
    
    func _init(p_name: String):
        name = p_name
        start_time = Time.get_ticks_usec()
    
    func stop():
        var elapsed = Time.get_ticks_usec() - start_time
        total_time += elapsed
        call_count += 1
        min_time = min(min_time, elapsed)
        max_time = max(max_time, elapsed)
        return elapsed

# Comenzar profiling
static func begin(name: String) -> ProfileTimer:
    var timer = ProfileTimer.new(name)
    if not instances.has(name):
        instances[name] = timer
        metrics[name] = {
            "total_time": 0,
            "call_count": 0,
            "min_time": timer.min_time,
            "max_time": 0,
            "avg_time": 0
        }
    return timer

# Finalizar profiling
static func end(timer: ProfileTimer):
    var elapsed = timer.stop()
    
    if metrics.has(timer.name):
        metrics[timer.name].total_time += elapsed
        metrics[timer.name].call_count += 1
        metrics[timer.name].min_time = min(metrics[timer.name].min_time, elapsed)
        metrics[timer.name].max_time = max(metrics[timer.name].max_time, elapsed)
        metrics[timer.name].avg_time = metrics[timer.name].total_time / metrics[timer.name].call_count

# Reportar métricas
static func report():
    print("\n=== PROFILING REPORT ===")
    for name in metrics:
        var m = metrics[name]
        print("%s:" % name)
        print("  Calls: %d" % m.call_count)
        print("  Total: %.2f ms" % (m.total_time / 1000.0))
        print("  Avg: %.3f ms" % (m.avg_time / 1000.0))
        print("  Min: %.3f ms" % (m.min_time / 1000.0))
        print("  Max: %.3f ms" % (m.max_time / 1000.0))

# Uso
func complex_operation():
    var timer = Profiler.begin("complex_operation")
    
    # Operación costosa
    for i in range(10000):
        var result = sqrt(i) * PI
    
    Profiler.end(timer)

func _ready():
    for i in range(100):
        complex_operation()
    
    Profiler.report()

3.2 Object Pooling Avanzado

# Pool genérico con warmup y estadísticas
class_name AdvancedPool
extends Node

var pool_scene: PackedScene
var available: Array = []
var active: Array = []
var max_size: int = 100
var warmup_size: int = 20
var statistics = {
    "total_created": 0,
    "reuses": 0,
    "peak_usage": 0,
    "current_usage": 0
}

func _init(scene: PackedScene, p_max_size: int = 100, p_warmup: int = 20):
    pool_scene = scene
    max_size = p_max_size
    warmup_size = p_warmup

func warmup():
    for i in range(warmup_size):
        var instance = _create_instance()
        available.append(instance)
    print("Pool warmed up with %d instances" % warmup_size)

func _create_instance():
    statistics.total_created += 1
    var instance = pool_scene.instantiate()
    instance.set_process(false)
    instance.set_physics_process(false)
    instance.visible = false
    add_child(instance)
    return instance

func get_instance():
    var instance
    
    if available.size() > 0:
        instance = available.pop_back()
        statistics.reuses += 1
    elif active.size() < max_size:
        instance = _create_instance()
    else:
        push_warning("Pool exhausted! Max size: %d" % max_size)
        return null
    
    active.append(instance)
    statistics.current_usage = active.size()
    statistics.peak_usage = max(statistics.peak_usage, statistics.current_usage)
    
    instance.set_process(true)
    instance.set_physics_process(true)
    instance.visible = true
    
    if instance.has_method("reset"):
        instance.reset()
    
    return instance

func return_instance(instance):
    if not instance in active:
        push_error("Instance not from this pool")
        return
    
    active.erase(instance)
    available.append(instance)
    
    statistics.current_usage = active.size()
    
    instance.set_process(false)
    instance.set_physics_process(false)
    instance.visible = false
    
    if instance.has_method("on_return_to_pool"):
        instance.on_return_to_pool()

func get_statistics() -> Dictionary:
    statistics.efficiency = float(statistics.reuses) / float(max(statistics.total_created, 1))
    return statistics

func clear_pool():
    for instance in available + active:
        instance.queue_free()
    available.clear()
    active.clear()
    statistics.current_usage = 0

4. Integración con C++

4.1 GDExtension Básico

# Uso de GDExtension desde GDScript
class_name NativeIntegration
extends Node

# Cargar extensión nativa
var native_class

func _ready():
    # Verificar si la extensión está disponible
    if ClassDB.class_exists("MyNativeClass"):
        native_class = ClassDB.instantiate("MyNativeClass")
        
        # Llamar métodos nativos
        var result = native_class.complex_calculation(1000000)
        print("Resultado nativo: ", result)
        
        # Conectar señales desde código nativo
        if native_class.has_signal("calculation_complete"):
            native_class.connect("calculation_complete", _on_calculation_complete)

func _on_calculation_complete(result):
    print("Cálculo completado: ", result)

# Interfaz para comunicación bidireccional
func send_data_to_native(data: Dictionary):
    if native_class and native_class.has_method("process_data"):
        return native_class.process_data(data)
    return null

4.2 Optimización con Threading

# Sistema de threading avanzado
class_name ThreadPool
extends Node

var threads: Array[Thread] = []
var tasks: Array = []
var results: Dictionary = {}
var mutex: Mutex
var semaphore: Semaphore
var running: bool = true
var num_threads: int = OS.get_processor_count()

func _ready():
    mutex = Mutex.new()
    semaphore = Semaphore.new()
    
    # Crear threads del pool
    for i in range(num_threads):
        var thread = Thread.new()
        thread.start(_worker_thread.bind(i))
        threads.append(thread)
    
    print("Thread pool iniciado con %d threads" % num_threads)

func _worker_thread(thread_id: int):
    while running:
        semaphore.wait()  # Esperar por trabajo
        
        if not running:
            break
        
        mutex.lock()
        if tasks.size() == 0:
            mutex.unlock()
            continue
        
        var task = tasks.pop_front()
        mutex.unlock()
        
        # Ejecutar tarea
        var result = task.callable.call()
        
        mutex.lock()
        results[task.id] = result
        mutex.unlock()

func submit_task(callable: Callable, task_id: String = "") -> String:
    if task_id == "":
        task_id = "task_" + str(Time.get_unix_time_from_system())
    
    mutex.lock()
    tasks.append({
        "id": task_id,
        "callable": callable
    })
    mutex.unlock()
    
    semaphore.post()  # Señalar que hay trabajo
    return task_id

func get_result(task_id: String, wait: bool = false):
    while wait:
        mutex.lock()
        if results.has(task_id):
            var result = results[task_id]
            results.erase(task_id)
            mutex.unlock()
            return result
        mutex.unlock()
        OS.delay_msec(10)
    
    mutex.lock()
    if results.has(task_id):
        var result = results[task_id]
        results.erase(task_id)
        mutex.unlock()
        return result
    mutex.unlock()
    return null

func shutdown():
    running = false
    
    # Despertar todos los threads
    for i in range(num_threads):
        semaphore.post()
    
    # Esperar que terminen
    for thread in threads:
        thread.wait_to_finish()
    
    print("Thread pool cerrado")

func _exit_tree():
    shutdown()

5. Arquitecturas Complejas

5.1 Sistema ECS (Entity Component System)

# Implementación de ECS en GDScript
class_name ECS
extends Node

# Componente base
class Component:
    var entity_id: int
    var type: String

# Entidad
class Entity:
    var id: int
    var components: Dictionary = {}
    var active: bool = true
    
    func add_component(component: Component):
        components[component.type] = component
        component.entity_id = id
    
    func get_component(type: String) -> Component:
        return components.get(type)
    
    func has_component(type: String) -> bool:
        return components.has(type)
    
    func remove_component(type: String):
        components.erase(type)

# Sistema base
class System:
    var required_components: Array[String] = []
    var ecs: ECS
    
    func process_entity(entity: Entity, delta: float):
        pass
    
    func can_process(entity: Entity) -> bool:
        for comp_type in required_components:
            if not entity.has_component(comp_type):
                return false
        return true

# Core ECS
var entities: Dictionary = {}
var systems: Array[System] = []
var next_entity_id: int = 0
var component_pools: Dictionary = {}

func create_entity() -> Entity:
    var entity = Entity.new()
    entity.id = next_entity_id
    entities[next_entity_id] = entity
    next_entity_id += 1
    return entity

func destroy_entity(entity_id: int):
    if entities.has(entity_id):
        var entity = entities[entity_id]
        # Retornar componentes a sus pools
        for comp_type in entity.components:
            return_component_to_pool(entity.components[comp_type])
        entities.erase(entity_id)

func add_system(system: System):
    system.ecs = self
    systems.append(system)

func _process(delta):
    for system in systems:
        for entity in entities.values():
            if entity.active and system.can_process(entity):
                system.process_entity(entity, delta)

# Pool de componentes para reutilización
func get_component_from_pool(type: String) -> Component:
    if not component_pools.has(type):
        component_pools[type] = []
    
    if component_pools[type].size() > 0:
        return component_pools[type].pop_back()
    
    # Crear nuevo componente según tipo
    match type:
        "Transform":
            return TransformComponent.new()
        "Velocity":
            return VelocityComponent.new()
        "Health":
            return HealthComponent.new()
        _:
            return Component.new()

func return_component_to_pool(component: Component):
    if not component_pools.has(component.type):
        component_pools[component.type] = []
    component_pools[component.type].append(component)

# Componentes ejemplo
class TransformComponent extends Component:
    var position: Vector2
    var rotation: float
    func _init():
        type = "Transform"

class VelocityComponent extends Component:
    var velocity: Vector2
    var max_speed: float = 500.0
    func _init():
        type = "Velocity"

class HealthComponent extends Component:
    var current: int = 100
    var maximum: int = 100
    func _init():
        type = "Health"

# Sistema de movimiento
class MovementSystem extends System:
    func _init():
        required_components = ["Transform", "Velocity"]
    
    func process_entity(entity: Entity, delta: float):
        var transform = entity.get_component("Transform") as TransformComponent
        var velocity = entity.get_component("Velocity") as VelocityComponent
        
        transform.position += velocity.velocity * delta

5.2 Sistema de Comandos (Command Pattern)

# Sistema de comandos con undo/redo
class_name CommandSystem
extends Node

# Comando base
class Command:
    var timestamp: float
    
    func execute():
        timestamp = Time.get_unix_time_from_system()
    
    func undo():
        pass
    
    func redo():
        execute()
    
    func can_merge_with(other: Command) -> bool:
        return false
    
    func merge(other: Command):
        pass

# Gestor de comandos
var command_history: Array[Command] = []
var current_index: int = -1
var max_history: int = 100
var command_groups: Dictionary = {}

func execute_command(command: Command):
    # Eliminar comandos posteriores al índice actual
    if current_index < command_history.size() - 1:
        command_history.resize(current_index + 1)
    
    # Intentar fusionar con el comando anterior
    if command_history.size() > 0:
        var last_command = command_history[-1]
        if last_command.can_merge_with(command):
            last_command.merge(command)
            return
    
    # Agregar nuevo comando
    command.execute()
    command_history.append(command)
    current_index += 1
    
    # Limitar tamaño del historial
    if command_history.size() > max_history:
        command_history.pop_front()
        current_index -= 1

func undo() -> bool:
    if current_index < 0:
        return false
    
    command_history[current_index].undo()
    current_index -= 1
    return true

func redo() -> bool:
    if current_index >= command_history.size() - 1:
        return false
    
    current_index += 1
    command_history[current_index].redo()
    return true

# Comando compuesto
class CompositeCommand extends Command:
    var commands: Array[Command] = []
    
    func add_command(command: Command):
        commands.append(command)
    
    func execute():
        super.execute()
        for cmd in commands:
            cmd.execute()
    
    func undo():
        # Deshacer en orden inverso
        for i in range(commands.size() - 1, -1, -1):
            commands[i].undo()
    
    func redo():
        for cmd in commands:
            cmd.redo()

# Ejemplo: Comando de movimiento
class MoveCommand extends Command:
    var target: Node2D
    var old_position: Vector2
    var new_position: Vector2
    
    func _init(p_target: Node2D, p_new_position: Vector2):
        target = p_target
        new_position = p_new_position
    
    func execute():
        super.execute()
        old_position = target.position
        target.position = new_position
    
    func undo():
        target.position = old_position
    
    func can_merge_with(other: Command) -> bool:
        if not other is MoveCommand:
            return false
        var other_move = other as MoveCommand
        # Fusionar si es el mismo objeto y los comandos están cerca en tiempo
        return other_move.target == target and \
               abs(other_move.timestamp - timestamp) < 0.5
    
    func merge(other: Command):
        var other_move = other as MoveCommand
        new_position = other_move.new_position
        timestamp = other_move.timestamp

6. Networking Avanzado

6.1 Sistema de Replicación

# Sistema de replicación de estado
class_name NetworkReplication
extends Node

# Propiedades replicadas
class ReplicatedProperty:
    var name: String
    var value
    var last_value
    var interpolate: bool = false
    var reliable: bool = true
    var update_rate: float = 0.1
    var last_update: float = 0.0
    
    func should_replicate() -> bool:
        return value != last_value and \
               Time.get_ticks_msec() / 1000.0 - last_update >= update_rate
    
    func update_sent():
        last_value = value
        last_update = Time.get_ticks_msec() / 1000.0

# Objeto replicado
class ReplicatedObject:
    var id: int
    var owner_id: int
    var properties: Dictionary = {}
    var node: Node
    
    func add_property(prop_name: String, initial_value, config: Dictionary = {}):
        var prop = ReplicatedProperty.new()
        prop.name = prop_name
        prop.value = initial_value
        prop.last_value = initial_value
        prop.interpolate = config.get("interpolate", false)
        prop.reliable = config.get("reliable", true)
        prop.update_rate = config.get("update_rate", 0.1)
        properties[prop_name] = prop
    
    func get_replication_data() -> Dictionary:
        var data = {}
        for prop_name in properties:
            var prop = properties[prop_name]
            if prop.should_replicate():
                data[prop_name] = prop.value
                prop.update_sent()
        return data
    
    func apply_replication_data(data: Dictionary):
        for prop_name in data:
            if properties.has(prop_name):
                var prop = properties[prop_name]
                if prop.interpolate and node:
                    # Interpolar suavemente
                    var tween = node.create_tween()
                    tween.tween_property(node, prop_name, data[prop_name], 0.1)
                else:
                    prop.value = data[prop_name]
                    if node:
                        node.set(prop_name, data[prop_name])

# Gestor de replicación
var replicated_objects: Dictionary = {}
var next_object_id: int = 0
var local_player_id: int = 1

func register_object(node: Node, owner_id: int = -1) -> ReplicatedObject:
    var obj = ReplicatedObject.new()
    obj.id = next_object_id
    obj.owner_id = owner_id if owner_id != -1 else local_player_id
    obj.node = node
    replicated_objects[obj.id] = obj
    next_object_id += 1
    return obj

func unregister_object(object_id: int):
    replicated_objects.erase(object_id)

func get_replication_snapshot() -> Dictionary:
    var snapshot = {}
    for obj_id in replicated_objects:
        var obj = replicated_objects[obj_id]
        var data = obj.get_replication_data()
        if data.size() > 0:
            snapshot[obj_id] = data
    return snapshot

func apply_replication_snapshot(snapshot: Dictionary):
    for obj_id in snapshot:
        if replicated_objects.has(obj_id):
            replicated_objects[obj_id].apply_replication_data(snapshot[obj_id])

# RPC optimizado
@rpc("any_peer", "unreliable_ordered")
func replicate_snapshot(snapshot: Dictionary):
    apply_replication_snapshot(snapshot)

# Enviar actualizaciones
func _physics_process(delta):
    if multiplayer.is_server():
        var snapshot = get_replication_snapshot()
        if snapshot.size() > 0:
            replicate_snapshot.rpc(snapshot)

7. Generación Procedural Avanzada

7.1 Wave Function Collapse

# Implementación de Wave Function Collapse
class_name WaveFunctionCollapse
extends Node

# Celda del grid
class Cell:
    var possibilities: Array = []  # Tiles posibles
    var collapsed: bool = false
    var value: int = -1
    
    func entropy() -> float:
        return log(possibilities.size()) if possibilities.size() > 0 else 0.0

# Reglas de adyacencia
var adjacency_rules: Dictionary = {}
var tile_weights: Dictionary = {}
var grid: Array[Array] = []
var width: int
var height: int

func initialize(p_width: int, p_height: int, tiles: Array):
    width = p_width
    height = p_height
    
    # Inicializar grid
    grid.clear()
    for y in range(height):
        var row: Array[Cell] = []
        for x in range(width):
            var cell = Cell.new()
            cell.possibilities = tiles.duplicate()
            row.append(cell)
        grid.append(row)

func add_adjacency_rule(tile_a: int, tile_b: int, direction: Vector2i):
    if not adjacency_rules.has(tile_a):
        adjacency_rules[tile_a] = {}
    
    var dir_key = "%d,%d" % [direction.x, direction.y]
    if not adjacency_rules[tile_a].has(dir_key):
        adjacency_rules[tile_a][dir_key] = []
    
    adjacency_rules[tile_a][dir_key].append(tile_b)

func collapse():
    while not is_fully_collapsed():
        # Encontrar celda con menor entropía
        var min_entropy_cell = find_minimum_entropy_cell()
        if not min_entropy_cell:
            push_error("No se puede colapsar más")
            break
        
        # Colapsar la celda
        collapse_cell(min_entropy_cell)
        
        # Propagar restricciones
        propagate(min_entropy_cell)

func find_minimum_entropy_cell() -> Vector2i:
    var min_entropy = INF
    var min_cell = Vector2i(-1, -1)
    
    for y in range(height):
        for x in range(width):
            var cell = grid[y][x]
            if not cell.collapsed:
                var entropy = cell.entropy()
                # Agregar ruido para romper empates
                entropy += randf() * 0.001
                
                if entropy < min_entropy:
                    min_entropy = entropy
                    min_cell = Vector2i(x, y)
    
    return min_cell

func collapse_cell(pos: Vector2i):
    var cell = grid[pos.y][pos.x]
    if cell.possibilities.size() == 0:
        return
    
    # Seleccionar tile basado en pesos
    var selected = select_weighted(cell.possibilities)
    cell.value = selected
    cell.possibilities = [selected]
    cell.collapsed = true

func select_weighted(possibilities: Array) -> int:
    var total_weight = 0.0
    for tile in possibilities:
        total_weight += tile_weights.get(tile, 1.0)
    
    var random_value = randf() * total_weight
    var accumulated = 0.0
    
    for tile in possibilities:
        accumulated += tile_weights.get(tile, 1.0)
        if accumulated >= random_value:
            return tile
    
    return possibilities[0]

func propagate(start_pos: Vector2i):
    var stack = [start_pos]
    
    while stack.size() > 0:
        var current = stack.pop_back()
        var current_cell = grid[current.y][current.x]
        
        # Revisar vecinos
        var directions = [
            Vector2i(0, -1),  # Arriba
            Vector2i(1, 0),   # Derecha
            Vector2i(0, 1),   # Abajo
            Vector2i(-1, 0)   # Izquierda
        ]
        
        for dir in directions:
            var neighbor_pos = current + dir
            
            if neighbor_pos.x < 0 or neighbor_pos.x >= width or \
               neighbor_pos.y < 0 or neighbor_pos.y >= height:
                continue
            
            var neighbor = grid[neighbor_pos.y][neighbor_pos.x]
            if neighbor.collapsed:
                continue
            
            var old_possibilities = neighbor.possibilities.size()
            constrain_neighbor(current_cell, neighbor, dir)
            
            if neighbor.possibilities.size() < old_possibilities:
                stack.append(neighbor_pos)

func constrain_neighbor(source: Cell, neighbor: Cell, direction: Vector2i):
    var valid_tiles = []
    
    for source_tile in source.possibilities:
        var dir_key = "%d,%d" % [direction.x, direction.y]
        if adjacency_rules.has(source_tile) and \
           adjacency_rules[source_tile].has(dir_key):
            for valid_tile in adjacency_rules[source_tile][dir_key]:
                if valid_tile not in valid_tiles:
                    valid_tiles.append(valid_tile)
    
    # Filtrar posibilidades del vecino
    var new_possibilities = []
    for tile in neighbor.possibilities:
        if tile in valid_tiles:
            new_possibilities.append(tile)
    
    neighbor.possibilities = new_possibilities

func is_fully_collapsed() -> bool:
    for row in grid:
        for cell in row:
            if not cell.collapsed:
                return false
    return true

func get_result() -> Array[Array]:
    var result: Array[Array] = []
    for row in grid:
        var result_row: Array[int] = []
        for cell in row:
            result_row.append(cell.value)
        result.append(result_row)
    return result

Ejercicios Avanzados

Ejercicio 1: Sistema de Modificadores

Implementa un sistema de modificadores (buffs/debuffs) que:

Ejercicio 2: IA con Behavior Trees

Crea un sistema de Behavior Trees que:

Ejercicio 3: Sistema de Crafting

Diseña un sistema de crafting que:

Recursos Avanzados

Conclusión

Has explorado las características más avanzadas de GDScript, desde metaprogramación hasta integración con código nativo. Estas técnicas te permiten crear sistemas complejos y optimizados para proyectos profesionales.

Recuerda que la complejidad debe justificarse por las necesidades del proyecto. No todas las técnicas avanzadas son necesarias para todos los juegos.

¡Continúa experimentando y creando sistemas innovadores con GDScript!