← Volver al listado de tecnologías

Optimización para Dispositivos Móviles

Por: Artiko
defoldmobileoptimizacionperformanceiosandroid

Optimización para Dispositivos Móviles

Los dispositivos móviles presentan desafíos únicos: batería limitada, memoria restringida, procesadores ARM y pantallas táctiles. En esta lección aprenderás a optimizar tus juegos para obtener el máximo rendimiento móvil.

📱 Análisis de Rendimiento Móvil

Profiler Personalizado

-- mobile_profiler.script
local PROFILER_ENABLED = true -- Desactivar en release

local ProfilerData = {
    frame_time = 0,
    draw_calls = 0,
    texture_memory = 0,
    lua_memory = 0,
    fps_history = {},
    memory_history = {},
    thermal_state = "normal"
}

function init(self)
    if not PROFILER_ENABLED then return end

    self.start_time = socket.gettime()
    self.frame_count = 0
    self.last_memory_check = 0
    self.performance_warnings = {}

    -- Configurar muestreo
    self.sample_interval = 0.5 -- Cada 500ms
    self.last_sample = 0

    msg.post(".", "acquire_input_focus")
end

local function sample_performance(self)
    local current_time = socket.gettime()

    -- FPS calculation
    local fps = 1.0 / (current_time - self.last_frame_time)
    table.insert(ProfilerData.fps_history, fps)

    -- Memory usage
    local lua_mem = collectgarbage("count") * 1024 -- Convert to bytes
    local texture_mem = profiler.get_memory_usage()

    ProfilerData.lua_memory = lua_mem
    ProfilerData.texture_memory = texture_mem

    table.insert(ProfilerData.memory_history, {
        lua = lua_mem,
        texture = texture_mem,
        timestamp = current_time
    })

    -- Keep only last 60 samples (30 seconds)
    if #ProfilerData.fps_history > 60 then
        table.remove(ProfilerData.fps_history, 1)
    end
    if #ProfilerData.memory_history > 60 then
        table.remove(ProfilerData.memory_history, 1)
    end

    -- Performance warnings
    self:check_performance_warnings(fps, lua_mem)
end

local function check_performance_warnings(self, fps, memory)
    -- FPS too low
    if fps < 45 then
        self:add_warning("LOW_FPS", string.format("FPS: %.1f", fps))
    end

    -- Memory usage too high
    local memory_mb = memory / (1024 * 1024)
    if memory_mb > 100 then -- 100MB threshold
        self:add_warning("HIGH_MEMORY", string.format("Memory: %.1fMB", memory_mb))
    end

    -- Thermal throttling detection (iOS specific)
    if sys.get_sys_info().system_name == "iPhone OS" then
        local thermal = self:get_thermal_state()
        if thermal ~= "normal" then
            self:add_warning("THERMAL", "Device overheating: " .. thermal)
        end
    end
end

function update(self, dt)
    if not PROFILER_ENABLED then return end

    self.frame_count = self.frame_count + 1
    local current_time = socket.gettime()

    if current_time - self.last_sample >= self.sample_interval then
        sample_performance(self)
        self.last_sample = current_time
    end

    self.last_frame_time = current_time
end

-- Debug overlay
function on_input(self, action_id, action)
    if action_id == hash("show_profiler") and action.pressed then
        if not self.profiler_visible then
            self:show_profiler_overlay()
        else
            self:hide_profiler_overlay()
        end
    end
end

Detección Automática de Dispositivos

-- device_detection.script
local DEVICE_TIERS = {
    LOW_END = {
        max_texture_size = 1024,
        particle_density = 0.5,
        shadow_quality = "off",
        post_processing = false,
        target_fps = 30
    },
    MID_RANGE = {
        max_texture_size = 2048,
        particle_density = 0.75,
        shadow_quality = "low",
        post_processing = true,
        target_fps = 60
    },
    HIGH_END = {
        max_texture_size = 4096,
        particle_density = 1.0,
        shadow_quality = "high",
        post_processing = true,
        target_fps = 60
    }
}

local function detect_device_tier(self)
    local sys_info = sys.get_sys_info()
    local device_info = {
        system = sys_info.system_name,
        device_model = sys_info.device_model,
        language = sys_info.language,
        territory = sys_info.territory
    }

    -- Obtener información de hardware
    local memory_mb = self:get_total_memory() / (1024 * 1024)
    local gpu_info = self:get_gpu_info()
    local cpu_cores = self:get_cpu_cores()

    -- Clasificar dispositivo
    if memory_mb < 2048 then -- Menos de 2GB RAM
        return "LOW_END", device_info
    elseif memory_mb < 4096 then -- 2-4GB RAM
        return "MID_RANGE", device_info
    else -- 4GB+ RAM
        return "HIGH_END", device_info
    end
end

local function apply_device_settings(self, tier)
    local settings = DEVICE_TIERS[tier]
    if not settings then return end

    print("Aplicando configuración para dispositivo:", tier)

    -- Configurar texturas
    msg.post("texture_manager", "set_max_size", {size = settings.max_texture_size})

    -- Configurar partículas
    msg.post("particle_manager", "set_density", {density = settings.particle_density})

    -- Configurar efectos
    msg.post("render", "set_post_processing", {enabled = settings.post_processing})

    -- FPS target
    msg.post(".", "set_vsync", {enabled = settings.target_fps == 30})

    -- Guardar configuración
    self.current_settings = settings
    self:save_device_settings(tier, settings)
end

local function get_total_memory(self)
    -- Platform-specific memory detection
    local sys_info = sys.get_sys_info()

    if sys_info.system_name == "Android" then
        -- Usar JNI para obtener memoria total
        return self:get_android_memory()
    elseif sys_info.system_name == "iPhone OS" then
        -- Usar device model para estimar memoria
        return self:estimate_ios_memory(sys_info.device_model)
    else
        return 2048 * 1024 * 1024 -- 2GB default
    end
end

-- Función específica para Android
local function get_android_memory(self)
    -- Esta función requeriría extensión nativa
    --[[
    -- En Java/Kotlin:
    ActivityManager actManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
    actManager.getMemoryInfo(memInfo);
    long totalMemory = memInfo.totalMem;
    --]]
    return 3 * 1024 * 1024 * 1024 -- 3GB por defecto
end

🔋 Optimización de Batería

Power Management

-- power_manager.script
local POWER_MODES = {
    PERFORMANCE = {
        target_fps = 60,
        update_frequency = 1.0,
        background_updates = true,
        high_precision_timer = true
    },
    BALANCED = {
        target_fps = 45,
        update_frequency = 0.8,
        background_updates = true,
        high_precision_timer = false
    },
    POWER_SAVE = {
        target_fps = 30,
        update_frequency = 0.5,
        background_updates = false,
        high_precision_timer = false
    }
}

function init(self)
    self.current_mode = "BALANCED"
    self.battery_level = 1.0
    self.is_charging = false
    self.thermal_state = "normal"
    self.last_battery_check = 0

    -- Configurar modo inicial
    self:apply_power_mode(self.current_mode)

    -- Monitorear batería cada 30 segundos
    timer.delay(30, true, function()
        self:check_battery_status()
    end)
end

local function check_battery_status(self)
    -- Obtener nivel de batería (requiere extensión nativa)
    local battery_info = self:get_battery_info()

    self.battery_level = battery_info.level or self.battery_level
    self.is_charging = battery_info.charging or false

    -- Cambiar modo según batería
    if not self.is_charging then
        if self.battery_level < 0.2 then -- Menos del 20%
            self:set_power_mode("POWER_SAVE")
        elseif self.battery_level < 0.5 then -- Menos del 50%
            self:set_power_mode("BALANCED")
        end
    else
        -- Cargando - modo performance permitido
        if self.battery_level > 0.8 then
            self:set_power_mode("PERFORMANCE")
        end
    end

    print(string.format("Batería: %.1f%% (%s) - Modo: %s",
                       self.battery_level * 100,
                       self.is_charging and "Cargando" or "Descargando",
                       self.current_mode))
end

local function apply_power_mode(self, mode_name)
    local mode = POWER_MODES[mode_name]
    if not mode then return end

    self.current_mode = mode_name

    -- Ajustar FPS objetivo
    if mode.target_fps == 30 then
        sys.set_vsync_swap_interval(2) -- 30 FPS en pantalla 60Hz
    else
        sys.set_vsync_swap_interval(1) -- 60 FPS
    end

    -- Configurar actualizaciones
    msg.post("game_manager", "set_update_frequency", {
        frequency = mode.update_frequency
    })

    -- Pausar updates en background si es necesario
    if not mode.background_updates then
        msg.post("background_manager", "pause_updates")
    else
        msg.post("background_manager", "resume_updates")
    end

    -- Configurar timers de precisión
    msg.post("timer_manager", "set_precision", {
        high_precision = mode.high_precision_timer
    })
end

-- Reducir trabajo en background
function on_message(self, message_id, message, sender)
    if message_id == hash("window_event") then
        if message.event == window.WINDOW_EVENT_FOCUS_LOST then
            -- App va a background
            self:enter_background_mode()
        elseif message.event == window.WINDOW_EVENT_FOCUS_GAINED then
            -- App regresa a foreground
            self:exit_background_mode()
        end
    end
end

local function enter_background_mode(self)
    print("Entrando a modo background")

    -- Pausar animaciones no críticas
    msg.post("animation_manager", "pause_decorative")

    -- Reducir frecuencia de audio
    msg.post("audio_manager", "set_background_mode", {enabled = true})

    -- Pausar efectos de partículas
    msg.post("particle_manager", "pause_all")

    -- Reducir rate de network updates
    msg.post("network_manager", "set_update_rate", {rate = 0.2})
end

Thermal Management

-- thermal_manager.script
local THERMAL_STATES = {
    NORMAL = 0,
    FAIR = 1,
    SERIOUS = 2,
    CRITICAL = 3
}

local THERMAL_RESPONSES = {
    [THERMAL_STATES.NORMAL] = {
        target_fps = 60,
        particle_quality = 1.0,
        texture_quality = 1.0,
        post_processing = true
    },
    [THERMAL_STATES.FAIR] = {
        target_fps = 45,
        particle_quality = 0.8,
        texture_quality = 0.9,
        post_processing = true
    },
    [THERMAL_STATES.SERIOUS] = {
        target_fps = 30,
        particle_quality = 0.5,
        texture_quality = 0.7,
        post_processing = false
    },
    [THERMAL_STATES.CRITICAL] = {
        target_fps = 20,
        particle_quality = 0.2,
        texture_quality = 0.5,
        post_processing = false
    }
}

local function monitor_thermal_state(self)
    local thermal_state = self:get_device_thermal_state()

    if thermal_state ~= self.last_thermal_state then
        print("Cambio thermal state:", thermal_state)
        self:apply_thermal_response(thermal_state)
        self.last_thermal_state = thermal_state
    end
end

local function apply_thermal_response(self, thermal_state)
    local response = THERMAL_RESPONSES[thermal_state]
    if not response then return end

    -- Reducir FPS
    self:set_target_fps(response.target_fps)

    -- Reducir calidad de partículas
    msg.post("particle_manager", "set_quality_multiplier", {
        multiplier = response.particle_quality
    })

    -- Reducir resolución de texturas
    msg.post("texture_manager", "set_quality_multiplier", {
        multiplier = response.texture_quality
    })

    -- Desactivar post-processing si es necesario
    if not response.post_processing then
        msg.post("render", "disable_post_processing")
    end

    -- Mostrar warning al usuario si es crítico
    if thermal_state >= THERMAL_STATES.SERIOUS then
        msg.post("ui_manager", "show_thermal_warning", {
            state = thermal_state
        })
    end
end

🖼️ Optimización de Texturas y Assets

Atlas Dinámico

-- dynamic_atlas_manager.script
local MAX_ATLAS_SIZE = 2048
local ATLAS_FORMATS = {
    LOW = {size = 1024, compression = "etc1"},
    MEDIUM = {size = 2048, compression = "etc2"},
    HIGH = {size = 4096, compression = "astc"}
}

function init(self)
    self.atlases = {}
    self.texture_cache = {}
    self.device_tier = self:detect_device_tier()
    self.atlas_format = ATLAS_FORMATS[self.device_tier]

    -- Configurar compresión según dispositivo
    self:configure_texture_compression()
end

local function create_dynamic_atlas(self, textures, atlas_id)
    local atlas_size = self.atlas_format.size
    local packer = self:create_texture_packer(atlas_size)

    local packed_textures = {}

    for _, texture_path in ipairs(textures) do
        local texture_data = resource.load(texture_path)
        local packed_data = packer:pack_texture(texture_data)

        if packed_data then
            packed_textures[texture_path] = packed_data
        else
            print("Warning: No se pudo empacar textura:", texture_path)
        end
    end

    -- Crear atlas
    local atlas_texture = self:generate_atlas_texture(packed_textures, atlas_size)
    self.atlases[atlas_id] = {
        texture = atlas_texture,
        mapping = packed_textures,
        size = atlas_size
    }

    return atlas_id
end

local function configure_texture_compression(self)
    local sys_info = sys.get_sys_info()

    if sys_info.system_name == "Android" then
        -- Detectar soporte GPU
        local gpu_vendor = self:get_gpu_vendor()

        if string.find(gpu_vendor:lower(), "adreno") then
            self.compression_format = "astc"
        elseif string.find(gpu_vendor:lower(), "mali") then
            self.compression_format = "etc2"
        else
            self.compression_format = "etc1" -- Fallback
        end

    elseif sys_info.system_name == "iPhone OS" then
        -- iOS siempre soporta ASTC en A8+
        local device_model = sys_info.device_model
        if self:supports_astc(device_model) then
            self.compression_format = "astc"
        else
            self.compression_format = "pvrtc"
        end
    end

    print("Formato de compresión seleccionado:", self.compression_format)
end

-- Streaming de texturas
local function stream_texture(self, texture_path, priority)
    if self.texture_cache[texture_path] then
        -- Ya está en caché
        return self.texture_cache[texture_path]
    end

    -- Determinar LOD según distancia/importancia
    local lod_level = self:calculate_lod_level(priority)
    local texture_url = self:get_lod_texture_path(texture_path, lod_level)

    -- Cargar asincrónicamente
    resource.load_async(texture_url, function(self, url, resource)
        if resource then
            self.texture_cache[texture_path] = resource
            msg.post(".", "texture_loaded", {path = texture_path, resource = resource})
        end
    end)
end

local function manage_texture_memory(self)
    local current_memory = self:get_texture_memory_usage()
    local memory_limit = self:get_memory_limit()

    if current_memory > memory_limit * 0.8 then -- 80% del límite
        print("Memoria de texturas cerca del límite, liberando...")

        -- Ordenar texturas por último uso
        local sorted_textures = {}
        for path, data in pairs(self.texture_cache) do
            table.insert(sorted_textures, {
                path = path,
                last_used = data.last_used,
                size = data.size
            })
        end

        table.sort(sorted_textures, function(a, b)
            return a.last_used < b.last_used
        end)

        -- Liberar texturas más antiguas
        local freed_memory = 0
        local target_free = memory_limit * 0.2 -- Liberar 20%

        for _, texture in ipairs(sorted_textures) do
            if freed_memory >= target_free then break end

            resource.release(texture.path)
            self.texture_cache[texture.path] = nil
            freed_memory = freed_memory + texture.size

            print("Liberada textura:", texture.path)
        end
    end
end

LOD (Level of Detail) Automático

-- lod_manager.script
local LOD_LEVELS = {
    {distance = 100, scale = 1.0, texture_quality = 1.0},
    {distance = 300, scale = 0.8, texture_quality = 0.8},
    {distance = 600, scale = 0.6, texture_quality = 0.6},
    {distance = 1000, scale = 0.4, texture_quality = 0.4},
    {distance = math.huge, scale = 0.2, texture_quality = 0.2}
}

function init(self)
    self.camera_position = vmath.vector3()
    self.lod_objects = {}
    self.last_lod_update = 0
    self.lod_update_interval = 0.1 -- Actualizar cada 100ms
end

local function register_lod_object(self, object_id, position, importance)
    self.lod_objects[object_id] = {
        position = position,
        importance = importance or 1.0,
        current_lod = 0,
        original_scale = go.get_scale(object_id),
        url = object_id
    }
end

local function update_lod_levels(self)
    local camera_pos = go.get_position("main:/camera")

    for object_id, lod_data in pairs(self.lod_objects) do
        if go.exists(object_id) then
            local distance = vmath.length(camera_pos - lod_data.position)

            -- Ajustar distancia por importancia
            distance = distance / lod_data.importance

            -- Encontrar LOD apropiado
            local new_lod = 0
            for i, lod in ipairs(LOD_LEVELS) do
                if distance <= lod.distance then
                    new_lod = i
                    break
                end
            end

            -- Aplicar LOD si cambió
            if new_lod ~= lod_data.current_lod then
                self:apply_lod_level(object_id, lod_data, new_lod)
                lod_data.current_lod = new_lod
            end
        else
            -- Objeto fue destruido, remover del registro
            self.lod_objects[object_id] = nil
        end
    end
end

local function apply_lod_level(self, object_id, lod_data, lod_level)
    local lod = LOD_LEVELS[lod_level]
    if not lod then return end

    -- Aplicar escala
    local new_scale = lod_data.original_scale * lod.scale
    go.set_scale(new_scale, object_id)

    -- Cambiar textura si es necesario
    if lod.texture_quality < 1.0 then
        msg.post(object_id, "set_texture_quality", {
            quality = lod.texture_quality
        })
    end

    -- Desactivar objeto si está muy lejos
    if lod_level >= #LOD_LEVELS then
        go.set_visible(false, object_id)
    else
        go.set_visible(true, object_id)
    end
end

function update(self, dt)
    local current_time = socket.gettime()

    if current_time - self.last_lod_update >= self.lod_update_interval then
        update_lod_levels(self)
        self.last_lod_update = current_time
    end
end

📐 Optimización de Renderizado

Culling Avanzado

-- culling_manager.script
function init(self)
    self.camera_frustum = {}
    self.visible_objects = {}
    self.culled_objects = {}
    self.occlusion_queries = {}

    -- Configurar culling
    self.frustum_culling = true
    self.occlusion_culling = true
    self.distance_culling = true
    self.max_render_distance = 1000
end

local function calculate_camera_frustum(self)
    local camera_proj = go.get("/camera", "projection")
    local camera_view = go.get("/camera", "view")

    -- Calcular planos del frustum
    self.camera_frustum = {
        near = self:extract_plane(camera_proj, camera_view, "near"),
        far = self:extract_plane(camera_proj, camera_view, "far"),
        left = self:extract_plane(camera_proj, camera_view, "left"),
        right = self:extract_plane(camera_proj, camera_view, "right"),
        top = self:extract_plane(camera_proj, camera_view, "top"),
        bottom = self:extract_plane(camera_proj, camera_view, "bottom")
    }
end

local function is_in_frustum(self, object_bounds)
    for _, plane in pairs(self.camera_frustum) do
        if self:distance_to_plane(plane, object_bounds.center) < -object_bounds.radius then
            return false -- Objeto completamente fuera del plano
        end
    end
    return true
end

local function perform_occlusion_culling(self)
    -- Solo en dispositivos de alta gama
    if self.device_tier ~= "HIGH_END" then return end

    for object_id, object_data in pairs(self.visible_objects) do
        if self:is_occluded(object_data.bounds) then
            -- Objeto ocluido, no renderizar
            go.set_visible(false, object_id)
            self.culled_objects[object_id] = object_data
            self.visible_objects[object_id] = nil
        end
    end
end

local function update_culling(self)
    calculate_camera_frustum(self)

    local camera_pos = go.get_position("main:/camera")

    -- Revisar todos los objetos registrados
    for object_id, object_data in pairs(game.render_objects) do
        local distance = vmath.length(camera_pos - object_data.position)
        local visible = true

        -- Distance culling
        if self.distance_culling and distance > self.max_render_distance then
            visible = false
        end

        -- Frustum culling
        if visible and self.frustum_culling then
            visible = is_in_frustum(self, object_data.bounds)
        end

        -- Actualizar visibilidad
        if visible and not self.visible_objects[object_id] then
            -- Objeto entró en vista
            go.set_visible(true, object_id)
            self.visible_objects[object_id] = object_data
            self.culled_objects[object_id] = nil

        elseif not visible and self.visible_objects[object_id] then
            -- Objeto salió de vista
            go.set_visible(false, object_id)
            self.culled_objects[object_id] = object_data
            self.visible_objects[object_id] = nil
        end
    end

    -- Occlusion culling para objetos visibles
    if self.occlusion_culling then
        perform_occlusion_culling(self)
    end
end

Batch Rendering

-- batch_renderer.script
local MAX_BATCH_SIZE = 1000
local BATCH_TYPES = {
    STATIC_SPRITES = "static_sprites",
    DYNAMIC_SPRITES = "dynamic_sprites",
    PARTICLES = "particles"
}

function init(self)
    self.render_batches = {}
    self.draw_calls = 0
    self.batched_objects = 0

    -- Crear batches por tipo
    for batch_type, _ in pairs(BATCH_TYPES) do
        self.render_batches[batch_type] = {
            vertices = {},
            indices = {},
            textures = {},
            count = 0
        }
    end
end

local function add_to_batch(self, batch_type, object_data)
    local batch = self.render_batches[batch_type]
    if not batch then return false end

    -- Verificar si hay espacio en el batch
    if batch.count >= MAX_BATCH_SIZE then
        self:flush_batch(batch_type)
        batch = self.render_batches[batch_type]
    end

    -- Añadir vértices del objeto al batch
    local vertices = object_data.vertices
    local texture = object_data.texture

    -- Verificar si podemos usar la misma textura
    if #batch.textures > 0 and batch.textures[#batch.textures] ~= texture then
        -- Textura diferente, flush y empezar nuevo batch
        self:flush_batch(batch_type)
        batch = self.render_batches[batch_type]
    end

    -- Añadir datos al batch
    for _, vertex in ipairs(vertices) do
        table.insert(batch.vertices, vertex)
    end

    table.insert(batch.textures, texture)
    batch.count = batch.count + 1
    self.batched_objects = self.batched_objects + 1

    return true
end

local function flush_batch(self, batch_type)
    local batch = self.render_batches[batch_type]
    if batch.count == 0 then return end

    -- Enviar batch a GPU
    render.draw(batch.vertices, batch.indices, batch.textures[1])
    self.draw_calls = self.draw_calls + 1

    -- Limpiar batch
    batch.vertices = {}
    batch.indices = {}
    batch.textures = {}
    batch.count = 0
end

function render(self, camera_world_view, camera_world_proj)
    -- Procesar todos los objetos visibles
    self.draw_calls = 0
    self.batched_objects = 0

    for object_id, object_data in pairs(culling_manager.visible_objects) do
        local batch_type = self:determine_batch_type(object_data)
        add_to_batch(self, batch_type, object_data)
    end

    -- Flush batches restantes
    for batch_type, _ in pairs(BATCH_TYPES) do
        flush_batch(self, batch_type)
    end

    print(string.format("Draw calls: %d, Objetos batched: %d",
                       self.draw_calls, self.batched_objects))
end

🎮 Optimización de Gameplay

Object Pooling Avanzado

-- advanced_object_pool.script
local POOL_CONFIGS = {
    bullet = {
        initial_size = 100,
        max_size = 500,
        expand_by = 50,
        factory = "/factories#bullet_factory"
    },
    enemy = {
        initial_size = 20,
        max_size = 100,
        expand_by = 10,
        factory = "/factories#enemy_factory"
    },
    particle = {
        initial_size = 200,
        max_size = 1000,
        expand_by = 100,
        factory = "/factories#particle_factory"
    }
}

function init(self)
    self.pools = {}
    self.active_objects = {}
    self.pool_stats = {}

    -- Crear pools iniciales
    for pool_name, config in pairs(POOL_CONFIGS) do
        self:create_pool(pool_name, config)
    end
end

local function create_pool(self, pool_name, config)
    local pool = {
        available = {},
        config = config,
        total_created = 0,
        active_count = 0
    }

    -- Pre-crear objetos
    for i = 1, config.initial_size do
        local obj = factory.create(config.factory)
        go.set_position(vmath.vector3(-10000, -10000, 0), obj) -- Fuera de pantalla
        table.insert(pool.available, obj)
        pool.total_created = pool.total_created + 1
    end

    self.pools[pool_name] = pool
    self.pool_stats[pool_name] = {
        peak_usage = 0,
        total_requests = 0,
        cache_hits = 0
    }

    print(string.format("Pool '%s' creado con %d objetos", pool_name, config.initial_size))
end

local function get_from_pool(self, pool_name, position, properties)
    local pool = self.pools[pool_name]
    if not pool then return nil end

    local stats = self.pool_stats[pool_name]
    stats.total_requests = stats.total_requests + 1

    local obj = nil

    if #pool.available > 0 then
        -- Usar objeto del pool
        obj = table.remove(pool.available)
        stats.cache_hits = stats.cache_hits + 1
    else
        -- Pool vacío, crear nuevo si es posible
        if pool.total_created < pool.config.max_size then
            obj = factory.create(pool.config.factory)
            pool.total_created = pool.total_created + 1
            print("Pool expandido:", pool_name, "Total objetos:", pool.total_created)
        else
            print("Warning: Pool '" .. pool_name .. "' alcanzó límite máximo")
            return nil
        end
    end

    -- Configurar objeto
    if obj then
        go.set_position(position or vmath.vector3(), obj)
        if properties then
            for prop, value in pairs(properties) do
                go.set(obj, prop, value)
            end
        end

        self.active_objects[obj] = pool_name
        pool.active_count = pool.active_count + 1

        -- Actualizar estadísticas
        if pool.active_count > stats.peak_usage then
            stats.peak_usage = pool.active_count
        end
    end

    return obj
end

local function return_to_pool(self, obj)
    local pool_name = self.active_objects[obj]
    if not pool_name then return false end

    local pool = self.pools[pool_name]

    -- Resetear objeto
    go.set_position(vmath.vector3(-10000, -10000, 0), obj)
    msg.post(obj, "reset") -- Mensaje personalizado para resetear estado

    -- Devolver al pool
    table.insert(pool.available, obj)
    self.active_objects[obj] = nil
    pool.active_count = pool.active_count - 1

    return true
end

-- Auto-cleanup de objetos inactivos
function update(self, dt)
    -- Verificar objetos que han estado inactivos mucho tiempo
    for obj, pool_name in pairs(self.active_objects) do
        if not go.exists(obj) then
            -- Objeto fue destruido externamente
            self.active_objects[obj] = nil
            self.pools[pool_name].active_count = self.pools[pool_name].active_count - 1
        end
    end
end

-- Estadísticas del pool
local function print_pool_stats(self)
    print("=== POOL STATISTICS ===")
    for pool_name, stats in pairs(self.pool_stats) do
        local pool = self.pools[pool_name]
        local hit_rate = stats.cache_hits / math.max(stats.total_requests, 1) * 100

        print(string.format("%s: Active=%d, Available=%d, Hit Rate=%.1f%%",
                           pool_name, pool.active_count, #pool.available, hit_rate))
    end
end

📱 Adaptación Multi-Dispositivo

Responsive UI System

-- responsive_ui.script
local SCREEN_SIZES = {
    PHONE_SMALL = {width = 320, height = 568, scale = 0.8}, -- iPhone 5
    PHONE_MEDIUM = {width = 375, height = 667, scale = 0.9}, -- iPhone 8
    PHONE_LARGE = {width = 414, height = 896, scale = 1.0}, -- iPhone 11
    TABLET = {width = 768, height = 1024, scale = 1.2}, -- iPad
    TABLET_LARGE = {width = 1024, height = 1366, scale = 1.4} -- iPad Pro
}

function init(self)
    local screen_width, screen_height = window.get_size()
    self.screen_category = self:categorize_screen(screen_width, screen_height)
    self.ui_scale = SCREEN_SIZES[self.screen_category].scale

    print(string.format("Pantalla: %dx%d, Categoría: %s, Escala UI: %.1f",
                       screen_width, screen_height, self.screen_category, self.ui_scale))

    self:adapt_ui_layout()
end

local function categorize_screen(self, width, height)
    local smaller_dimension = math.min(width, height)

    if smaller_dimension < 350 then
        return "PHONE_SMALL"
    elseif smaller_dimension < 400 then
        return "PHONE_MEDIUM"
    elseif smaller_dimension < 500 then
        return "PHONE_LARGE"
    elseif smaller_dimension < 800 then
        return "TABLET"
    else
        return "TABLET_LARGE"
    end
end

local function adapt_ui_layout(self)
    local layouts = {
        PHONE_SMALL = {
            button_size = 50,
            font_size = 12,
            margin = 10,
            joystick_size = 80
        },
        PHONE_MEDIUM = {
            button_size = 60,
            font_size = 14,
            margin = 15,
            joystick_size = 100
        },
        PHONE_LARGE = {
            button_size = 70,
            font_size = 16,
            margin = 20,
            joystick_size = 120
        },
        TABLET = {
            button_size = 90,
            font_size = 20,
            margin = 30,
            joystick_size = 150
        }
    }

    local layout = layouts[self.screen_category]
    if not layout then return end

    -- Aplicar configuración de layout
    msg.post("ui_manager", "set_layout", layout)

    -- Escalar elementos UI existentes
    for _, ui_element in ipairs(gui.get_all_nodes()) do
        local current_scale = gui.get_scale(ui_element)
        gui.set_scale(ui_element, current_scale * self.ui_scale)
    end
end

-- Touch input adaptation
local function adapt_touch_controls(self)
    local touch_configs = {
        PHONE_SMALL = {
            min_touch_size = 44, -- Recomendación iOS
            deadzone = 15,
            sensitivity = 1.2
        },
        TABLET = {
            min_touch_size = 64,
            deadzone = 25,
            sensitivity = 0.8
        }
    }

    local config = touch_configs[self.screen_category]
    if config then
        msg.post("input_manager", "configure_touch", config)
    end
end

📚 Recursos y Referencias

APIs de Optimización en Defold

Herramientas de Profiling

🎯 Ejercicios Propuestos

  1. Profiler Personalizado: Crea un sistema completo de profiling que muestre métricas en tiempo real.

  2. Dynamic Quality Settings: Implementa un sistema que ajuste automáticamente la calidad según el rendimiento.

  3. Asset Streaming: Crea un sistema de streaming que cargue assets bajo demanda.

  4. Thermal Throttling: Implementa respuesta automática al sobrecalentamiento del dispositivo.

  5. Memory Pool Manager: Diseña un sistema avanzado de pools para diferentes tipos de objetos.

La optimización móvil es un arte que requiere equilibrar calidad visual, rendimiento y experiencia de usuario. Dominando estas técnicas podrás crear juegos que funcionen perfectamente en cualquier dispositivo móvil.