← Volver al listado de tecnologías
Optimización Específica para Móviles
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
- ✅ Mantener draw calls < 100 por frame
- ✅ Limitar memoria de texturas < 200MB
- ✅ Usar object pooling para objetos frecuentes
- ✅ Implementar LOD system para objetos distantes
- ✅ Optimizar shaders para GPU móviles
- ✅ Usar spatial partitioning para colisiones
- ✅ Configurar quality settings adaptivos
2. Targets de Performance
- FPS: 60 FPS en dispositivos de gama alta, 30 FPS en gama baja
- Memoria: Máximo 512MB para dispositivos de 2GB RAM
- Batería: Minimizar uso de CPU/GPU intensivo
- Temperatura: Evitar throttling térmico
3. Testing en Dispositivos Reales
- Probar en dispositivos de diferentes gamas
- Monitorear temperatura durante sesiones largas
- Verificar performance en condiciones de batería baja
- Testear con múltiples apps en background
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.