Optimización para Dispositivos Móviles
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
profiler.get_memory_usage()- Memoria GPU utilizadasys.get_sys_info()- Información del sistemaresource.load_async()- Carga asíncrona de recursosrender.set_render_target()- Control de render targets
Herramientas de Profiling
- Xcode Instruments - Profiling en iOS
- Android Studio Profiler - Profiling en Android
- RenderDoc - Análisis de renderizado
- Unity Profiler - Herramienta genérica útil
Links Útiles
🎯 Ejercicios Propuestos
-
Profiler Personalizado: Crea un sistema completo de profiling que muestre métricas en tiempo real.
-
Dynamic Quality Settings: Implementa un sistema que ajuste automáticamente la calidad según el rendimiento.
-
Asset Streaming: Crea un sistema de streaming que cargue assets bajo demanda.
-
Thermal Throttling: Implementa respuesta automática al sobrecalentamiento del dispositivo.
-
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.