← Volver al listado de tecnologías

Optimización Específica para Móviles

Por: Artiko
defoldperformancemobileoptimizationfpsmemoria

Optimización Específica para Móviles

Los dispositivos móviles tienen limitaciones únicas de CPU, GPU, memoria y batería. Esta guía te enseñará a optimizar tu juego Defold para obtener el máximo rendimiento en móviles.

Análisis de Performance

1. Profiling en Tiempo Real

Performance Monitor

-- performance_monitor.script
local M = {}

function init(self)
    self.enabled = sys.get_config("project.debug", "0") == "1"

    if self.enabled then
        self.metrics = {
            fps = 0,
            frame_time = 0,
            draw_calls = 0,
            triangles = 0,
            memory_used = 0,
            texture_memory = 0,
            sound_memory = 0
        }

        self.fps_samples = {}
        self.fps_sample_count = 60  -- Samples para promedio
        self.update_interval = 1.0  -- Actualizar cada segundo
        self.update_timer = 0

        -- Crear GUI de debug
        self:create_debug_overlay()

        print("Performance Monitor enabled")
    end
end

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

    -- Calcular FPS
    local current_fps = 1.0 / dt
    table.insert(self.fps_samples, current_fps)

    if #self.fps_samples > self.fps_sample_count then
        table.remove(self.fps_samples, 1)
    end

    -- Calcular promedio de FPS
    local fps_sum = 0
    for _, fps in ipairs(self.fps_samples) do
        fps_sum = fps_sum + fps
    end
    self.metrics.fps = fps_sum / #self.fps_samples
    self.metrics.frame_time = dt * 1000  -- En millisegundos

    -- Actualizar estadísticas periódicamente
    self.update_timer = self.update_timer + dt
    if self.update_timer >= self.update_interval then
        self:update_metrics()
        self:update_debug_overlay()
        self.update_timer = 0
    end

    -- Detectar frame drops
    if dt > 0.033 then  -- Más de 33ms = menos de 30 FPS
        print(string.format("Frame drop detected: %.2fms (%.1f FPS)", dt * 1000, 1/dt))
    end
end

function update_metrics(self)
    -- Obtener estadísticas de render
    local render_stats = profiler.get_render_stats()
    if render_stats then
        self.metrics.draw_calls = render_stats.draw_calls or 0
        self.metrics.triangles = render_stats.triangles or 0
    end

    -- Obtener uso de memoria
    local memory_stats = profiler.get_memory_usage()
    if memory_stats then
        self.metrics.memory_used = memory_stats.total or 0
        self.metrics.texture_memory = memory_stats.textures or 0
        self.metrics.sound_memory = memory_stats.sounds or 0
    end
end

function create_debug_overlay(self)
    -- Esta función se implementaría en un GUI script
    msg.post("main:/debug_overlay#gui", "create_performance_overlay")
end

function update_debug_overlay(self)
    msg.post("main:/debug_overlay#gui", "update_performance_data", self.metrics)
end

-- Función para detectar dispositivos de gama baja
function M.is_low_end_device()
    local sys_info = sys.get_sys_info()
    local platform = sys_info.system_name

    if platform == "Android" then
        -- Detectar Android de gama baja
        local device_model = sys_info.device_model or ""
        local manufacturer = sys_info.manufacturer or ""

        -- Lista de dispositivos conocidos de gama baja
        local low_end_patterns = {
            "Go", "Lite", "Mini", "One", "Core",
            "SM-J", "SM-A0", "SM-G3", "LG-K",
            "HUAWEI Y", "HONOR 7"
        }

        for _, pattern in ipairs(low_end_patterns) do
            if string.find(device_model, pattern) then
                return true
            end
        end

        -- Verificar RAM disponible (aproximado)
        local memory_stats = profiler.get_memory_usage()
        if memory_stats and memory_stats.system_total then
            -- Menos de 2GB de RAM = gama baja
            return memory_stats.system_total < 2147483648
        end

    elseif platform == "iPhone OS" then
        -- Detectar iPhone/iPad antiguos
        local device_model = sys_info.device_model or ""

        -- iPhones anteriores al iPhone 8
        local old_iphones = {
            "iPhone5", "iPhone6", "iPhone7",
            "iPad3", "iPad4", "iPad5", "iPad Air"
        }

        for _, model in ipairs(old_iphones) do
            if string.find(device_model, model) then
                return true
            end
        end
    end

    return false
end

return M

2. Memory Profiler

Memory Tracker

-- memory_tracker.lua
local M = {}

function M.new()
    return {
        enabled = sys.get_config("project.debug", "0") == "1",
        snapshots = {},
        baseline = 0,
        peak_memory = 0,
        allocations = {},
        check_interval = 5.0,  -- Verificar cada 5 segundos
        check_timer = 0
    }
end

function M.update(tracker, dt)
    if not tracker.enabled then return end

    tracker.check_timer = tracker.check_timer + dt

    if tracker.check_timer >= tracker.check_interval then
        M.take_snapshot(tracker)
        tracker.check_timer = 0
    end
end

function M.take_snapshot(tracker, label)
    local memory_stats = profiler.get_memory_usage()
    if not memory_stats then return end

    local snapshot = {
        timestamp = socket.gettime(),
        label = label or "auto",
        total = memory_stats.total,
        lua = memory_stats.lua or 0,
        textures = memory_stats.textures or 0,
        sounds = memory_stats.sounds or 0,
        objects = memory_stats.objects or 0
    }

    table.insert(tracker.snapshots, snapshot)

    -- Actualizar pico de memoria
    if snapshot.total > tracker.peak_memory then
        tracker.peak_memory = snapshot.total
        print(string.format("New memory peak: %.2f MB", snapshot.total / 1024 / 1024))
    end

    -- Mantener solo los últimos 100 snapshots
    if #tracker.snapshots > 100 then
        table.remove(tracker.snapshots, 1)
    end

    M.analyze_memory_trend(tracker)
end

function M.analyze_memory_trend(tracker)
    if #tracker.snapshots < 10 then return end

    local recent = {}
    for i = math.max(1, #tracker.snapshots - 9), #tracker.snapshots do
        table.insert(recent, tracker.snapshots[i].total)
    end

    -- Calcular tendencia (regresión lineal simple)
    local sum_x, sum_y, sum_xy, sum_x2 = 0, 0, 0, 0
    local n = #recent

    for i, memory in ipairs(recent) do
        sum_x = sum_x + i
        sum_y = sum_y + memory
        sum_xy = sum_xy + i * memory
        sum_x2 = sum_x2 + i * i
    end

    local slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x)

    -- Si la pendiente es positiva y significativa, hay memory leak
    local trend_threshold = 1024 * 1024  -- 1MB por snapshot
    if slope > trend_threshold then
        print("WARNING: Potential memory leak detected! Trend: " ..
              string.format("%.2f MB per check", slope / 1024 / 1024))
    end
end

function M.force_garbage_collection(tracker)
    collectgarbage("collect")
    print("Forced garbage collection completed")
    M.take_snapshot(tracker, "gc_forced")
end

return M

Optimización de Renderizado

1. Batch Optimization

Draw Call Reducer

-- draw_call_optimizer.script
local M = {}

function init(self)
    self.atlas_manager = {}
    self.texture_cache = {}
    self.batch_groups = {}

    -- Configuración de batching
    self.max_batch_size = 512  -- Máximo de sprites por batch
    self.z_threshold = 0.01    -- Diferencia mínima de Z para break batch

    print("Draw Call Optimizer initialized")
end

function M.optimize_sprite_batching(sprites)
    -- Agrupar sprites por texture/atlas
    local groups = {}

    for _, sprite in ipairs(sprites) do
        local texture_id = sprite.texture_id
        if not groups[texture_id] then
            groups[texture_id] = {}
        end
        table.insert(groups[texture_id], sprite)
    end

    -- Ordenar cada grupo por Z para minimizar cambios de estado
    for texture_id, group in pairs(groups) do
        table.sort(group, function(a, b)
            return a.z < b.z
        end)
    end

    return groups
end

function M.create_atlas_from_textures(textures, max_size)
    max_size = max_size or 2048

    -- Algoritmo simple de atlas packing
    local atlas = {
        width = max_size,
        height = max_size,
        textures = {},
        used_area = 0
    }

    local current_x, current_y = 0, 0
    local row_height = 0

    for _, texture in ipairs(textures) do
        -- Verificar si la textura cabe en la fila actual
        if current_x + texture.width > max_size then
            -- Nueva fila
            current_x = 0
            current_y = current_y + row_height
            row_height = 0
        end

        -- Verificar si cabe verticalmente
        if current_y + texture.height > max_size then
            print("Atlas full, creating new atlas")
            break
        end

        -- Añadir textura al atlas
        atlas.textures[texture.id] = {
            x = current_x,
            y = current_y,
            width = texture.width,
            height = texture.height,
            u1 = current_x / max_size,
            v1 = current_y / max_size,
            u2 = (current_x + texture.width) / max_size,
            v2 = (current_y + texture.height) / max_size
        }

        current_x = current_x + texture.width
        row_height = math.max(row_height, texture.height)
        atlas.used_area = atlas.used_area + texture.width * texture.height
    end

    atlas.efficiency = atlas.used_area / (max_size * max_size)

    return atlas
end

return M

2. LOD System (Level of Detail)

LOD Manager

-- lod_manager.script
local M = {}

function init(self)
    self.lod_objects = {}
    self.camera_position = vmath.vector3(0, 0, 0)
    self.update_frequency = 0.1  -- Actualizar LOD cada 100ms
    self.update_timer = 0

    -- Configuración de distancias LOD
    self.lod_distances = {
        high = 200,    -- Dentro de 200 pixels = LOD alto
        medium = 500,  -- 200-500 pixels = LOD medio
        low = 1000,    -- 500-1000 pixels = LOD bajo
        cull = 1500    -- Más de 1500 pixels = culling
    }

    print("LOD Manager initialized")
end

function update(self, dt)
    self.update_timer = self.update_timer + dt

    if self.update_timer >= self.update_frequency then
        self:update_lod_levels()
        self.update_timer = 0
    end
end

function M.register_lod_object(self, object_id, position, lod_configs)
    self.lod_objects[object_id] = {
        position = position,
        current_lod = "high",
        configs = lod_configs,
        last_distance = 0
    }
end

function update_lod_levels(self)
    for object_id, lod_object in pairs(self.lod_objects) do
        local distance = vmath.length(lod_object.position - self.camera_position)
        lod_object.last_distance = distance

        local new_lod = self:calculate_lod_level(distance)

        if new_lod ~= lod_object.current_lod then
            self:apply_lod_level(object_id, new_lod)
            lod_object.current_lod = new_lod
        end
    end
end

function calculate_lod_level(self, distance)
    if distance > self.lod_distances.cull then
        return "culled"
    elseif distance > self.lod_distances.low then
        return "low"
    elseif distance > self.lod_distances.medium then
        return "medium"
    else
        return "high"
    end
end

function apply_lod_level(self, object_id, lod_level)
    local lod_object = self.lod_objects[object_id]
    local config = lod_object.configs[lod_level]

    if not config then return end

    if lod_level == "culled" then
        -- Ocultar objeto completamente
        msg.post(object_id, "set_visible", {visible = false})
    else
        -- Aplicar configuración de LOD
        msg.post(object_id, "set_visible", {visible = true})
        msg.post(object_id, "apply_lod_config", config)
    end
end

-- Configuraciones de ejemplo para diferentes LODs
function M.create_sprite_lod_config()
    return {
        high = {
            scale = 1.0,
            animation_fps = 30,
            enable_physics = true,
            texture_quality = "high"
        },
        medium = {
            scale = 0.8,
            animation_fps = 15,
            enable_physics = true,
            texture_quality = "medium"
        },
        low = {
            scale = 0.6,
            animation_fps = 10,
            enable_physics = false,
            texture_quality = "low"
        },
        culled = {
            visible = false
        }
    }
end

return M

Optimización de Memoria

1. Asset Streaming

Asset Loader

-- asset_loader.script
local M = {}

function init(self)
    self.loaded_assets = {}
    self.loading_queue = {}
    self.unload_queue = {}
    self.memory_limit = 100 * 1024 * 1024  -- 100MB limite
    self.current_memory = 0
    self.preload_distance = 500  -- Precargar assets a 500 pixels
    self.unload_distance = 1000  -- Descargar assets a 1000 pixels
end

function M.load_asset_async(self, asset_path, callback)
    if self.loaded_assets[asset_path] then
        -- Asset ya cargado
        if callback then callback(self.loaded_assets[asset_path]) end
        return
    end

    -- Añadir a cola de carga
    table.insert(self.loading_queue, {
        path = asset_path,
        callback = callback,
        priority = 1
    })
end

function M.unload_asset(self, asset_path)
    if self.loaded_assets[asset_path] then
        local asset = self.loaded_assets[asset_path]

        -- Liberar memoria del asset
        if asset.type == "texture" then
            -- Descargar textura
            resource.release(asset.handle)
        elseif asset.type == "sound" then
            -- Descargar sonido
            resource.release(asset.handle)
        end

        self.current_memory = self.current_memory - asset.size
        self.loaded_assets[asset_path] = nil

        print("Unloaded asset: " .. asset_path .. " (freed " .. asset.size .. " bytes)")
    end
end

function update(self, dt)
    -- Procesar cola de carga
    if #self.loading_queue > 0 then
        local asset_to_load = table.remove(self.loading_queue, 1)
        self:process_asset_loading(asset_to_load)
    end

    -- Procesar cola de descarga
    if #self.unload_queue > 0 then
        local asset_to_unload = table.remove(self.unload_queue, 1)
        self:unload_asset(asset_to_unload)
    end

    -- Verificar límite de memoria
    if self.current_memory > self.memory_limit then
        self:force_memory_cleanup()
    end
end

function process_asset_loading(self, load_request)
    local asset_path = load_request.path

    -- Verificar espacio en memoria
    local estimated_size = self:estimate_asset_size(asset_path)
    if self.current_memory + estimated_size > self.memory_limit then
        self:make_memory_space(estimated_size)
    end

    -- Cargar asset
    resource.load(asset_path, function(self, hexdigest, success)
        if success then
            local asset = {
                handle = hexdigest,
                path = asset_path,
                size = estimated_size,
                last_used = socket.gettime(),
                type = self:get_asset_type(asset_path)
            }

            self.loaded_assets[asset_path] = asset
            self.current_memory = self.current_memory + estimated_size

            if load_request.callback then
                load_request.callback(asset)
            end

            print("Loaded asset: " .. asset_path .. " (" .. estimated_size .. " bytes)")
        else
            print("Failed to load asset: " .. asset_path)
        end
    end)
end

function make_memory_space(self, needed_space)
    -- Encontrar assets menos usados recientemente
    local sorted_assets = {}
    for path, asset in pairs(self.loaded_assets) do
        table.insert(sorted_assets, {path = path, asset = asset})
    end

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

    -- Descargar assets hasta liberar espacio suficiente
    local freed_space = 0
    for _, entry in ipairs(sorted_assets) do
        if freed_space >= needed_space then break end

        freed_space = freed_space + entry.asset.size
        table.insert(self.unload_queue, entry.path)
    end
end

function estimate_asset_size(self, asset_path)
    -- Estimación aproximada basada en el tipo de archivo
    local extension = string.lower(string.match(asset_path, "%.([^%.]+)$") or "")

    local size_estimates = {
        png = 512 * 1024,    -- 512KB promedio para PNG
        jpg = 256 * 1024,    -- 256KB promedio para JPG
        wav = 1024 * 1024,   -- 1MB promedio para WAV
        ogg = 256 * 1024,    -- 256KB promedio para OGG
        json = 10 * 1024,    -- 10KB promedio para JSON
        atlas = 2048 * 1024  -- 2MB promedio para atlas
    }

    return size_estimates[extension] or 100 * 1024  -- 100KB por defecto
end

return M

2. Object Pooling Avanzado

Multi-Type Object Pool

-- object_pool.lua
local M = {}

function M.new(factory_url, initial_size, max_size)
    return {
        factory_url = factory_url,
        available = {},
        active = {},
        initial_size = initial_size or 10,
        max_size = max_size or 100,
        total_created = 0,
        stats = {
            created = 0,
            reused = 0,
            destroyed = 0
        }
    }
end

function M.initialize_pool(pool)
    -- Crear objetos iniciales
    for i = 1, pool.initial_size do
        local obj = M.create_object(pool)
        M.return_object(pool, obj)
    end

    print(string.format("Pool initialized: %d objects created", pool.initial_size))
end

function M.get_object(pool)
    local obj

    if #pool.available > 0 then
        -- Reutilizar objeto existente
        obj = table.remove(pool.available)
        pool.stats.reused = pool.stats.reused + 1
    else
        -- Crear nuevo objeto si no excede el límite
        if pool.total_created < pool.max_size then
            obj = M.create_object(pool)
            pool.stats.created = pool.stats.created + 1
        else
            print("Object pool limit reached!")
            return nil
        end
    end

    pool.active[obj.id] = obj

    -- Reset objeto al estado inicial
    M.reset_object(obj)

    return obj
end

function M.return_object(pool, obj)
    if pool.active[obj.id] then
        pool.active[obj.id] = nil

        -- Ocultar y desactivar objeto
        go.set_position(vmath.vector3(10000, 10000, 0), obj.id)
        msg.post(obj.id, "disable")

        table.insert(pool.available, obj)
    end
end

function M.create_object(pool)
    local obj_id = factory.create(pool.factory_url)
    pool.total_created = pool.total_created + 1

    return {
        id = obj_id,
        created_time = socket.gettime(),
        reuse_count = 0
    }
end

function M.reset_object(obj)
    -- Reset propiedades del objeto al estado inicial
    msg.post(obj.id, "enable")
    go.set_scale(vmath.vector3(1, 1, 1), obj.id)
    sprite.set_constant(msg.url(nil, obj.id, "sprite"), "tint", vmath.vector4(1, 1, 1, 1))

    obj.reuse_count = obj.reuse_count + 1
end

function M.cleanup_pool(pool)
    -- Destruir objetos no utilizados para liberar memoria
    local cleanup_count = math.max(0, #pool.available - pool.initial_size)

    for i = 1, cleanup_count do
        local obj = table.remove(pool.available)
        go.delete(obj.id)
        pool.total_created = pool.total_created - 1
        pool.stats.destroyed = pool.stats.destroyed + 1
    end

    if cleanup_count > 0 then
        print(string.format("Pool cleanup: %d objects destroyed", cleanup_count))
    end
end

function M.get_pool_stats(pool)
    return {
        available = #pool.available,
        active = 0,  -- Contar activos
        total_created = pool.total_created,
        stats = pool.stats
    }
end

-- Pool Manager para manejar múltiples pools
local PoolManager = {}

function PoolManager.new()
    return {
        pools = {},
        cleanup_interval = 30.0,  -- Cleanup cada 30 segundos
        cleanup_timer = 0
    }
end

function PoolManager.create_pool(manager, pool_name, factory_url, initial_size, max_size)
    manager.pools[pool_name] = M.new(factory_url, initial_size, max_size)
    M.initialize_pool(manager.pools[pool_name])
end

function PoolManager.get_object(manager, pool_name)
    local pool = manager.pools[pool_name]
    if pool then
        return M.get_object(pool)
    end
    return nil
end

function PoolManager.return_object(manager, pool_name, obj)
    local pool = manager.pools[pool_name]
    if pool then
        M.return_object(pool, obj)
    end
end

function PoolManager.update(manager, dt)
    manager.cleanup_timer = manager.cleanup_timer + dt

    if manager.cleanup_timer >= manager.cleanup_interval then
        for name, pool in pairs(manager.pools) do
            M.cleanup_pool(pool)
        end
        manager.cleanup_timer = 0
    end
end

M.PoolManager = PoolManager

return M

Optimización de CPU

1. Spatial Partitioning

Quad Tree Implementation

-- quadtree.lua
local M = {}

function M.new(bounds, max_objects, max_levels, level)
    return {
        bounds = bounds,  -- {x, y, width, height}
        max_objects = max_objects or 10,
        max_levels = max_levels or 5,
        level = level or 0,
        objects = {},
        nodes = {}  -- Sub-cuadrantes
    }
end

function M.clear(quadtree)
    quadtree.objects = {}

    for i = 1, 4 do
        if quadtree.nodes[i] then
            M.clear(quadtree.nodes[i])
            quadtree.nodes[i] = nil
        end
    end
end

function M.split(quadtree)
    local sub_width = quadtree.bounds.width / 2
    local sub_height = quadtree.bounds.height / 2
    local x = quadtree.bounds.x
    local y = quadtree.bounds.y

    -- Crear 4 sub-cuadrantes
    quadtree.nodes[1] = M.new({x = x + sub_width, y = y, width = sub_width, height = sub_height},
                             quadtree.max_objects, quadtree.max_levels, quadtree.level + 1)
    quadtree.nodes[2] = M.new({x = x, y = y, width = sub_width, height = sub_height},
                             quadtree.max_objects, quadtree.max_levels, quadtree.level + 1)
    quadtree.nodes[3] = M.new({x = x, y = y + sub_height, width = sub_width, height = sub_height},
                             quadtree.max_objects, quadtree.max_levels, quadtree.level + 1)
    quadtree.nodes[4] = M.new({x = x + sub_width, y = y + sub_height, width = sub_width, height = sub_height},
                             quadtree.max_objects, quadtree.max_levels, quadtree.level + 1)
end

function M.get_index(quadtree, rect)
    local index = -1
    local vertical_midpoint = quadtree.bounds.x + quadtree.bounds.width / 2
    local horizontal_midpoint = quadtree.bounds.y + quadtree.bounds.height / 2

    -- El objeto puede caber completamente en los cuadrantes superiores
    local top_quadrant = (rect.y < horizontal_midpoint and rect.y + rect.height < horizontal_midpoint)
    -- El objeto puede caber completamente en los cuadrantes inferiores
    local bottom_quadrant = (rect.y > horizontal_midpoint)

    -- El objeto puede caber completamente en los cuadrantes izquierdos
    if rect.x < vertical_midpoint and rect.x + rect.width < vertical_midpoint then
        if top_quadrant then
            index = 2
        elseif bottom_quadrant then
            index = 3
        end
    -- El objeto puede caber completamente en los cuadrantes derechos
    elseif rect.x > vertical_midpoint then
        if top_quadrant then
            index = 1
        elseif bottom_quadrant then
            index = 4
        end
    end

    return index
end

function M.insert(quadtree, object)
    if quadtree.nodes[1] then
        local index = M.get_index(quadtree, object.bounds)

        if index ~= -1 then
            M.insert(quadtree.nodes[index], object)
            return
        end
    end

    table.insert(quadtree.objects, object)

    if #quadtree.objects > quadtree.max_objects and quadtree.level < quadtree.max_levels then
        if not quadtree.nodes[1] then
            M.split(quadtree)
        end

        local i = 1
        while i <= #quadtree.objects do
            local index = M.get_index(quadtree, quadtree.objects[i].bounds)
            if index ~= -1 then
                local object = table.remove(quadtree.objects, i)
                M.insert(quadtree.nodes[index], object)
            else
                i = i + 1
            end
        end
    end
end

function M.retrieve(quadtree, return_objects, rect)
    local index = M.get_index(quadtree, rect)

    if index ~= -1 and quadtree.nodes[1] then
        M.retrieve(quadtree.nodes[index], return_objects, rect)
    end

    for _, object in ipairs(quadtree.objects) do
        table.insert(return_objects, object)
    end

    return return_objects
end

return M

2. Update Frequency Optimization

Adaptive Update System

-- adaptive_update.script
local M = {}

function init(self)
    self.update_groups = {
        high_frequency = {   -- 60 FPS
            objects = {},
            frequency = 1/60,
            timer = 0
        },
        medium_frequency = { -- 30 FPS
            objects = {},
            frequency = 1/30,
            timer = 0
        },
        low_frequency = {    -- 10 FPS
            objects = {},
            frequency = 1/10,
            timer = 0
        },
        very_low_frequency = { -- 2 FPS
            objects = {},
            frequency = 1/2,
            timer = 0
        }
    }

    self.camera_position = vmath.vector3(0, 0, 0)
end

function update(self, dt)
    -- Actualizar cada grupo según su frecuencia
    for group_name, group in pairs(self.update_groups) do
        group.timer = group.timer + dt

        if group.timer >= group.frequency then
            self:update_group(group_name, group, group.timer)
            group.timer = 0
        end
    end

    -- Reevaluar frecuencias cada segundo
    self.reeval_timer = (self.reeval_timer or 0) + dt
    if self.reeval_timer >= 1.0 then
        self:reevaluate_update_frequencies()
        self.reeval_timer = 0
    end
end

function M.register_object(self, object_id, initial_frequency)
    initial_frequency = initial_frequency or "medium_frequency"

    local object_data = {
        id = object_id,
        position = go.get_position(object_id),
        last_distance = 0,
        importance = 1.0,
        activity_level = 1.0
    }

    table.insert(self.update_groups[initial_frequency].objects, object_data)
end

function update_group(self, group_name, group, dt)
    for _, obj in ipairs(group.objects) do
        -- Enviar update al objeto
        msg.post(obj.id, "adaptive_update", {
            dt = dt,
            frequency_group = group_name
        })

        -- Actualizar posición para cálculos de distancia
        obj.position = go.get_position(obj.id)
        obj.last_distance = vmath.length(obj.position - self.camera_position)
    end
end

function reevaluate_update_frequencies(self)
    local all_objects = {}

    -- Recopilar todos los objetos
    for group_name, group in pairs(self.update_groups) do
        for _, obj in ipairs(group.objects) do
            obj.current_group = group_name
            table.insert(all_objects, obj)
        end
        group.objects = {}  -- Limpiar grupos
    end

    -- Reasignar objetos basado en criterios
    for _, obj in ipairs(all_objects) do
        local new_frequency = self:calculate_optimal_frequency(obj)
        table.insert(self.update_groups[new_frequency].objects, obj)
    end
end

function calculate_optimal_frequency(self, obj)
    local distance = obj.last_distance
    local importance = obj.importance
    local activity = obj.activity_level

    -- Calcular score basado en múltiples factores
    local distance_score = math.max(0, 1 - distance / 1000)  -- Normalizar a 1000 pixels
    local total_score = distance_score * importance * activity

    -- Asignar frecuencia basada en score
    if total_score > 0.8 then
        return "high_frequency"
    elseif total_score > 0.5 then
        return "medium_frequency"
    elseif total_score > 0.2 then
        return "low_frequency"
    else
        return "very_low_frequency"
    end
end

return M

Optimización de Shaders

1. Simplified Mobile Shaders

Mobile-Optimized Vertex Shader

// mobile_sprite.vp
attribute vec4 position;
attribute vec2 texcoord0;
attribute vec4 color;

uniform mat4 view_proj;

varying vec2 var_texcoord0;
varying vec4 var_color;

void main()
{
    gl_Position = view_proj * vec4(position.xyz, 1.0);
    var_texcoord0 = texcoord0;
    var_color = color;
}

Mobile-Optimized Fragment Shader

// mobile_sprite.fp
#ifdef GL_ES
precision mediump float;
#endif

varying vec2 var_texcoord0;
varying vec4 var_color;

uniform sampler2D texture_sampler;

void main()
{
    vec4 tint_pm = var_color;
    vec4 color = texture2D(texture_sampler, var_texcoord0.xy) * tint_pm;

    // Simplified alpha test para mejor performance
    if (color.a < 0.01) {
        discard;
    }

    gl_FragColor = color;
}

2. Shader Performance Monitor

Shader Profiler

-- shader_profiler.script
local M = {}

function init(self)
    self.shader_stats = {}
    self.current_shader = ""
    self.draw_call_count = 0
    self.frame_count = 0
end

function M.track_shader_usage(shader_name)
    if not self.shader_stats[shader_name] then
        self.shader_stats[shader_name] = {
            usage_count = 0,
            total_draw_calls = 0,
            avg_draw_calls_per_frame = 0
        }
    end

    local stats = self.shader_stats[shader_name]
    stats.usage_count = stats.usage_count + 1
    stats.total_draw_calls = stats.total_draw_calls + 1
end

function M.end_frame(self)
    self.frame_count = self.frame_count + 1

    -- Calcular promedios cada 60 frames
    if self.frame_count % 60 == 0 then
        for shader_name, stats in pairs(self.shader_stats) do
            stats.avg_draw_calls_per_frame = stats.total_draw_calls / 60
            stats.total_draw_calls = 0  -- Reset para próximo periodo
        end

        self:report_performance()
    end
end

function report_performance(self)
    print("=== Shader Performance Report ===")
    for shader_name, stats in pairs(self.shader_stats) do
        print(string.format("%s: %.2f draw calls/frame",
              shader_name, stats.avg_draw_calls_per_frame))
    end
end

return M

Configuration Profiles

1. Quality Settings

Graphics Quality Manager

-- quality_settings.lua
local M = {}

M.QUALITY_PRESETS = {
    ultra = {
        shadow_quality = "high",
        texture_quality = "high",
        effect_quality = "high",
        particle_density = 1.0,
        lod_bias = 0.0,
        max_fps = 60,
        vsync = true
    },
    high = {
        shadow_quality = "medium",
        texture_quality = "high",
        effect_quality = "high",
        particle_density = 0.8,
        lod_bias = 0.2,
        max_fps = 60,
        vsync = true
    },
    medium = {
        shadow_quality = "low",
        texture_quality = "medium",
        effect_quality = "medium",
        particle_density = 0.6,
        lod_bias = 0.5,
        max_fps = 30,
        vsync = false
    },
    low = {
        shadow_quality = "off",
        texture_quality = "low",
        effect_quality = "low",
        particle_density = 0.4,
        lod_bias = 1.0,
        max_fps = 30,
        vsync = false
    },
    potato = {
        shadow_quality = "off",
        texture_quality = "very_low",
        effect_quality = "off",
        particle_density = 0.2,
        lod_bias = 2.0,
        max_fps = 20,
        vsync = false
    }
}

function M.auto_detect_quality()
    local performance_monitor = require "main.performance_monitor"

    if performance_monitor.is_low_end_device() then
        return "low"
    end

    -- Test de performance básico
    local test_start = socket.gettime()

    -- Simular carga de trabajo
    for i = 1, 1000000 do
        local result = math.sin(i) * math.cos(i)
    end

    local test_duration = socket.gettime() - test_start

    if test_duration < 0.01 then
        return "ultra"
    elseif test_duration < 0.02 then
        return "high"
    elseif test_duration < 0.05 then
        return "medium"
    else
        return "low"
    end
end

function M.apply_quality_settings(preset_name)
    local preset = M.QUALITY_PRESETS[preset_name]
    if not preset then
        print("Unknown quality preset: " .. preset_name)
        return false
    end

    -- Aplicar configuraciones
    msg.post("@render:", "set_quality_settings", preset)
    msg.post("@system:", "set_fps_limit", {fps = preset.max_fps})
    msg.post("@system:", "set_vsync", {enabled = preset.vsync})

    print("Applied quality preset: " .. preset_name)
    return true
end

return M

Mejores Prácticas para Móviles

1. Checklist de Optimización

2. Targets de Performance

3. Testing en Dispositivos Reales

Esta guía te proporciona las herramientas necesarias para optimizar tu juego Defold para dispositivos móviles, asegurando una experiencia fluida en el mayor rango de dispositivos posible.