Audio y Música Dinámica
Audio y Música Dinámica
El audio es fundamental para crear experiencias inmersivas en los videojuegos. En esta lección aprenderás a usar el sistema de audio de Defold para crear música adaptativa, efectos de sonido dinámicos, y sistemas de audio optimizados que funcionen perfectamente en dispositivos móviles.
Sistema de Audio de Defold
Defold utiliza un sistema de audio moderno basado en grupos de sonidos que permite control granular sobre volumen, mezcla y efectos.
Arquitectura de Audio
-- Estructura típica del sistema de audio
audio_system/
├── music/
│ ├── background_music.ogg
│ ├── boss_battle.ogg
│ └── menu_theme.ogg
├── sfx/
│ ├── player_sounds/
│ ├── enemy_sounds/
│ ├── ui_sounds/
│ └── environment_sounds/
├── groups/
│ ├── master.group
│ ├── music.group
│ ├── sfx.group
│ └── voice.group
└── controllers/
├── audio_manager.script
├── music_controller.script
└── sfx_controller.script
Configuración de Grupos de Audio
-- En game.project configurar grupos
[audio]
max_sound_buffers = 32
max_sound_instances = 256
max_sound_instances_per_component = 16
Proyecto Completo: Sistema de Audio Dinámico
Audio Manager Principal
-- controllers/audio_manager.script
local music_controller = require "controllers.music_controller"
local sfx_controller = require "controllers.sfx_controller"
local M = {}
function init(self)
-- Configuración de grupos de audio
self.audio_groups = {
master = "master",
music = "music",
sfx = "sfx",
voice = "voice",
ui = "ui"
}
-- Configuración por defecto
self.settings = {
master_volume = 0.8,
music_volume = 0.6,
sfx_volume = 0.8,
voice_volume = 1.0,
ui_volume = 0.7
}
-- Estado actual
self.current_music_state = "none"
self.music_transition_active = false
-- Inicializar subsistemas
music_controller.init()
sfx_controller.init()
-- Aplicar configuración inicial
apply_audio_settings(self)
print("Audio Manager inicializado")
end
function apply_audio_settings(self)
for group_name, volume in pairs(self.settings) do
local group_hash = hash(group_name:gsub("_volume", ""))
sound.set_group_gain(group_hash, volume)
print("Configurado", group_name, "a", volume)
end
end
function update(self, dt)
-- Actualizar subsistemas
music_controller.update(dt)
sfx_controller.update(dt)
end
-- API pública de configuración
function M.set_master_volume(volume)
M.settings.master_volume = math.max(0, math.min(1, volume))
sound.set_group_gain(hash("master"), M.settings.master_volume)
end
function M.set_music_volume(volume)
M.settings.music_volume = math.max(0, math.min(1, volume))
sound.set_group_gain(hash("music"), M.settings.music_volume)
end
function M.set_sfx_volume(volume)
M.settings.sfx_volume = math.max(0, math.min(1, volume))
sound.set_group_gain(hash("sfx"), M.settings.sfx_volume)
end
function M.mute_all()
for group_name, _ in pairs(M.audio_groups) do
sound.set_group_gain(hash(group_name), 0)
end
M.audio_muted = true
end
function M.unmute_all()
apply_audio_settings(M)
M.audio_muted = false
end
-- Transiciones de música
function M.transition_to_music(new_music_state, transition_time)
if M.current_music_state == new_music_state then
return
end
print("Transición musical:", M.current_music_state, "→", new_music_state)
music_controller.transition_to(new_music_state, transition_time or 2.0)
M.current_music_state = new_music_state
end
-- Control de efectos de sonido
function M.play_sfx(sound_name, options)
return sfx_controller.play(sound_name, options)
end
function M.stop_sfx(sound_id)
sfx_controller.stop(sound_id)
end
-- Guardar/cargar configuración
function M.save_audio_settings()
local save_data = {
audio_settings = M.settings
}
local save_string = json.encode(save_data)
local path = sys.get_save_file("audio", "settings.json")
local file = io.open(path, "w")
if file then
file:write(save_string)
file:close()
print("Configuración de audio guardada")
end
end
function M.load_audio_settings()
local path = sys.get_save_file("audio", "settings.json")
local file = io.open(path, "r")
if file then
local save_string = file:read("*a")
file:close()
local save_data = json.decode(save_string)
if save_data and save_data.audio_settings then
M.settings = save_data.audio_settings
apply_audio_settings(M)
print("Configuración de audio cargada")
end
end
end
return M
Parte 1: Sistema de Música Adaptativa
Music Controller
-- controllers/music_controller.script
local M = {}
-- Configuración de estados musicales
local MUSIC_STATES = {
menu = {
track = "/music/menu_theme.ogg",
loop = true,
volume = 0.8,
crossfade_time = 1.0
},
exploration = {
track = "/music/exploration.ogg",
loop = true,
volume = 0.6,
crossfade_time = 2.0
},
action = {
track = "/music/action_theme.ogg",
loop = true,
volume = 0.9,
crossfade_time = 1.5
},
boss_battle = {
track = "/music/boss_battle.ogg",
loop = true,
volume = 1.0,
crossfade_time = 0.5
},
victory = {
track = "/music/victory.ogg",
loop = false,
volume = 0.8,
crossfade_time = 1.0
},
ambient = {
tracks = {
"/music/ambient_1.ogg",
"/music/ambient_2.ogg",
"/music/ambient_3.ogg"
},
loop = true,
volume = 0.4,
random_selection = true,
crossfade_time = 3.0
}
}
function M.init()
M.current_track = nil
M.current_state = "none"
M.transition_timer = 0
M.transitioning = false
M.fade_in_track = nil
M.fade_out_track = nil
-- Configurar sound components
M.track_a_url = msg.url(".", "music_track_a", "")
M.track_b_url = msg.url(".", "music_track_b", "")
print("Music Controller inicializado")
end
function M.update(dt)
if M.transitioning then
update_music_transition(dt)
end
-- Verificar si la música actual terminó (para tracks no looped)
check_track_completion()
end
function M.transition_to(new_state, transition_time)
if M.current_state == new_state then
return
end
local state_config = MUSIC_STATES[new_state]
if not state_config then
print("Estado musical no encontrado:", new_state)
return
end
-- Configurar transición
M.transitioning = true
M.transition_timer = transition_time or state_config.crossfade_time or 2.0
M.transition_duration = M.transition_timer
M.target_state = new_state
-- Seleccionar track para estados con múltiples opciones
local target_track = select_track_for_state(state_config)
start_track_transition(target_track, state_config)
end
function select_track_for_state(state_config)
if state_config.track then
return state_config.track
elseif state_config.tracks then
if state_config.random_selection then
local index = math.random(1, #state_config.tracks)
return state_config.tracks[index]
else
-- Selección secuencial o basada en lógica del juego
return state_config.tracks[1]
end
end
return nil
end
function start_track_transition(new_track, state_config)
-- Determinar qué track slot usar
local target_url = M.current_track == M.track_a_url and M.track_b_url or M.track_a_url
-- Configurar nuevo track
sound.load(target_url, new_track)
M.fade_out_track = M.current_track
M.fade_in_track = target_url
M.target_volume = state_config.volume
-- Iniciar reproducción del nuevo track con volumen 0
if M.fade_in_track then
sound.set_gain(M.fade_in_track, 0)
sound.play(M.fade_in_track, {delay = 0, gain = 0})
end
print("Iniciando transición musical a:", new_track)
end
function update_music_transition(dt)
M.transition_timer = M.transition_timer - dt
local progress = 1.0 - (M.transition_timer / M.transition_duration)
if progress >= 1.0 then
complete_music_transition()
return
end
-- Aplicar curva de crossfade
local fade_out_volume = (1.0 - progress) * 0.8
local fade_in_volume = progress * M.target_volume
-- Aplicar volúmenes
if M.fade_out_track then
sound.set_gain(M.fade_out_track, fade_out_volume)
end
if M.fade_in_track then
sound.set_gain(M.fade_in_track, fade_in_volume)
end
end
function complete_music_transition()
-- Detener track anterior
if M.fade_out_track then
sound.stop(M.fade_out_track)
end
-- Configurar nuevo track como actual
M.current_track = M.fade_in_track
M.current_state = M.target_state
-- Limpiar transición
M.transitioning = false
M.fade_in_track = nil
M.fade_out_track = nil
print("Transición musical completada a:", M.current_state)
end
function check_track_completion()
if M.current_track and not M.transitioning then
local state_config = MUSIC_STATES[M.current_state]
if state_config and not state_config.loop then
-- Verificar si el track terminó
if not sound.is_playing(M.current_track) then
handle_track_completion()
end
end
end
end
function handle_track_completion()
print("Track completado:", M.current_state)
-- Lógica para después de completar un track
if M.current_state == "victory" then
M.transition_to("menu", 2.0)
elseif M.current_state == "boss_battle" then
-- Podría transicionar a exploration o action según contexto
M.transition_to("exploration", 1.5)
end
end
-- API para control dinámico
function M.pause()
if M.current_track then
sound.pause(M.current_track)
end
end
function M.resume()
if M.current_track then
sound.resume(M.current_track)
end
end
function M.stop()
if M.current_track then
sound.stop(M.current_track)
M.current_track = nil
M.current_state = "none"
end
end
function M.set_volume(volume)
if M.current_track then
sound.set_gain(M.current_track, volume)
end
end
-- Función para cambio dinámico basado en gameplay
function M.evaluate_music_state(game_context)
local new_state = "exploration" -- Default
if game_context.in_combat then
if game_context.boss_fight then
new_state = "boss_battle"
else
new_state = "action"
end
elseif game_context.in_menu then
new_state = "menu"
elseif game_context.player_won then
new_state = "victory"
elseif game_context.ambient_area then
new_state = "ambient"
end
if new_state ~= M.current_state then
M.transition_to(new_state)
end
end
return M
Parte 2: Sistema de Efectos de Sonido
SFX Controller con Variaciones
-- controllers/sfx_controller.script
local M = {}
-- Base de datos de efectos de sonido
local SFX_DATABASE = {
player_jump = {
files = {"/sfx/player/jump_1.ogg", "/sfx/player/jump_2.ogg", "/sfx/player/jump_3.ogg"},
volume = 0.8,
pitch_variance = 0.1,
group = "sfx"
},
player_land = {
files = {"/sfx/player/land_1.ogg", "/sfx/player/land_2.ogg"},
volume = 0.6,
pitch_variance = 0.15,
group = "sfx"
},
enemy_hit = {
files = {"/sfx/combat/hit_1.ogg", "/sfx/combat/hit_2.ogg", "/sfx/combat/hit_3.ogg"},
volume = 0.9,
pitch_variance = 0.2,
group = "sfx"
},
coin_collect = {
files = {"/sfx/items/coin_1.ogg", "/sfx/items/coin_2.ogg"},
volume = 0.7,
pitch_variance = 0.05,
group = "sfx"
},
button_click = {
files = {"/sfx/ui/click.ogg"},
volume = 0.8,
pitch_variance = 0,
group = "ui"
},
explosion = {
files = {"/sfx/effects/explosion_big.ogg"},
volume = 1.0,
pitch_variance = 0.1,
group = "sfx",
priority = "high"
},
footsteps = {
files = {
"/sfx/player/step_grass_1.ogg",
"/sfx/player/step_grass_2.ogg",
"/sfx/player/step_grass_3.ogg"
},
volume = 0.4,
pitch_variance = 0.1,
group = "sfx",
surface_variants = {
stone = {"/sfx/player/step_stone_1.ogg", "/sfx/player/step_stone_2.ogg"},
water = {"/sfx/player/step_water_1.ogg", "/sfx/player/step_water_2.ogg"},
metal = {"/sfx/player/step_metal_1.ogg", "/sfx/player/step_metal_2.ogg"}
}
}
}
function M.init()
M.active_sounds = {}
M.sound_pools = {}
M.next_sound_id = 1
-- Crear pools de sonidos para optimización
create_sound_pools()
print("SFX Controller inicializado")
end
function create_sound_pools()
-- Pre-cargar sonidos frecuentemente usados
for sfx_name, config in pairs(SFX_DATABASE) do
if config.priority == "high" or sfx_name == "footsteps" or sfx_name == "player_jump" then
M.sound_pools[sfx_name] = {}
for i = 1, 3 do -- Pool de 3 instancias por sonido
local sound_id = factory.create("/sfx_factories#generic_sound_factory")
table.insert(M.sound_pools[sfx_name], sound_id)
end
end
end
end
function M.play(sfx_name, options)
local config = SFX_DATABASE[sfx_name]
if not config then
print("SFX no encontrado:", sfx_name)
return nil
end
options = options or {}
-- Seleccionar archivo de sonido
local sound_file = select_sound_file(config, options)
-- Obtener sound component del pool o crear uno nuevo
local sound_url = get_sound_from_pool(sfx_name) or create_new_sound()
-- Configurar propiedades
local final_volume = (options.volume or config.volume) * (options.volume_multiplier or 1.0)
local pitch = 1.0 + (math.random() - 0.5) * config.pitch_variance
-- Cargar y reproducir
sound.load(sound_url, sound_file)
sound.set_gain(sound_url, final_volume)
local play_options = {
delay = options.delay or 0,
gain = final_volume,
pan = options.pan or 0,
speed = pitch
}
sound.play(sound_url, play_options)
-- Trackear sonido activo
local sound_id = M.next_sound_id
M.next_sound_id = M.next_sound_id + 1
M.active_sounds[sound_id] = {
url = sound_url,
sfx_name = sfx_name,
start_time = socket.gettime(),
duration = options.duration or estimate_sound_duration(sound_file)
}
print("Reproduciendo SFX:", sfx_name, "ID:", sound_id)
return sound_id
end
function select_sound_file(config, options)
local files = config.files
-- Variantes de superficie para footsteps
if config.surface_variants and options.surface then
local variant_files = config.surface_variants[options.surface]
if variant_files then
files = variant_files
end
end
-- Selección aleatoria o secuencial
if options.file_index then
return files[options.file_index] or files[1]
else
return files[math.random(1, #files)]
end
end
function get_sound_from_pool(sfx_name)
local pool = M.sound_pools[sfx_name]
if pool and #pool > 0 then
return table.remove(pool)
end
return nil
end
function create_new_sound()
return factory.create("/sfx_factories#generic_sound_factory")
end
function M.stop(sound_id)
local sound_data = M.active_sounds[sound_id]
if sound_data then
sound.stop(sound_data.url)
return_sound_to_pool(sound_data.url, sound_data.sfx_name)
M.active_sounds[sound_id] = nil
end
end
function M.update(dt)
-- Limpiar sonidos terminados
local current_time = socket.gettime()
for sound_id, sound_data in pairs(M.active_sounds) do
if current_time - sound_data.start_time > sound_data.duration then
return_sound_to_pool(sound_data.url, sound_data.sfx_name)
M.active_sounds[sound_id] = nil
end
end
end
function return_sound_to_pool(sound_url, sfx_name)
local pool = M.sound_pools[sfx_name]
if pool and #pool < 5 then -- Limitar tamaño del pool
table.insert(pool, sound_url)
else
go.delete(sound_url)
end
end
-- Funciones de conveniencia para sonidos específicos
function M.play_footstep(surface_type)
return M.play("footsteps", {surface = surface_type or "grass"})
end
function M.play_ui_sound(ui_element)
if ui_element == "button" then
return M.play("button_click")
elseif ui_element == "coin" then
return M.play("coin_collect")
end
end
function M.play_combat_sound(action, intensity)
if action == "hit" then
local volume_multiplier = intensity and (0.5 + intensity * 0.5) or 1.0
return M.play("enemy_hit", {volume_multiplier = volume_multiplier})
elseif action == "explosion" then
return M.play("explosion")
end
end
-- Control de grupos de sonidos
function M.stop_all_sfx()
for sound_id, _ in pairs(M.active_sounds) do
M.stop(sound_id)
end
end
function M.pause_all_sfx()
for _, sound_data in pairs(M.active_sounds) do
sound.pause(sound_data.url)
end
end
function M.resume_all_sfx()
for _, sound_data in pairs(M.active_sounds) do
sound.resume(sound_data.url)
end
end
function estimate_sound_duration(sound_file)
-- Estimación básica - en un proyecto real podrías tener una database con duraciones exactas
return 2.0 -- Valor por defecto conservador
end
return M
Parte 3: Audio Espacial y 3D
3D Audio Controller
-- controllers/spatial_audio.script
local M = {}
function M.init()
M.listener_position = vmath.vector3(0, 0, 0)
M.spatial_sources = {}
print("Spatial Audio Controller inicializado")
end
function M.set_listener_position(position)
M.listener_position = position
-- Actualizar todas las fuentes espaciales
for source_id, source_data in pairs(M.spatial_sources) do
update_spatial_audio(source_id, source_data)
end
end
function M.play_spatial_sound(sfx_name, world_position, options)
options = options or {}
-- Calcular audio espacial
local distance = vmath.length(world_position - M.listener_position)
local max_distance = options.max_distance or 500
local min_distance = options.min_distance or 50
-- Atenuación por distancia
local distance_factor = 1.0
if distance > min_distance then
distance_factor = math.max(0, 1.0 - (distance - min_distance) / (max_distance - min_distance))
end
-- Pan estéreo basado en posición horizontal
local pan = 0
if math.abs(world_position.x - M.listener_position.x) > 10 then
pan = math.max(-1, math.min(1, (world_position.x - M.listener_position.x) / 200))
end
-- Reproducir con configuración espacial
local spatial_options = {
volume_multiplier = distance_factor,
pan = pan,
world_position = world_position
}
for key, value in pairs(options) do
spatial_options[key] = value
end
local sound_id = require("controllers.sfx_controller").play(sfx_name, spatial_options)
-- Trackear como fuente espacial
if sound_id then
M.spatial_sources[sound_id] = {
position = world_position,
max_distance = max_distance,
min_distance = min_distance
}
end
return sound_id
end
function update_spatial_audio(source_id, source_data)
local distance = vmath.length(source_data.position - M.listener_position)
-- Actualizar volumen basado en distancia
local distance_factor = 1.0
if distance > source_data.min_distance then
distance_factor = math.max(0, 1.0 - (distance - source_data.min_distance) /
(source_data.max_distance - source_data.min_distance))
end
-- Actualizar pan
local pan = 0
if math.abs(source_data.position.x - M.listener_position.x) > 10 then
pan = math.max(-1, math.min(1, (source_data.position.x - M.listener_position.x) / 200))
end
-- Aplicar cambios al sonido activo
local sfx_controller = require("controllers.sfx_controller")
local sound_data = sfx_controller.active_sounds[source_id]
if sound_data then
sound.set_gain(sound_data.url, distance_factor * 0.8) -- Factor base de volumen
sound.set_pan(sound_data.url, pan)
end
end
function M.remove_spatial_source(source_id)
M.spatial_sources[source_id] = nil
end
return M
Parte 4: Sistema de Audio Reactivo
Audio que Responde al Gameplay
-- controllers/reactive_audio.script
local audio_manager = require "controllers.audio_manager"
local music_controller = require "controllers.music_controller"
local M = {}
function M.init()
M.game_state = {
health_percentage = 1.0,
stress_level = 0.0,
combat_intensity = 0.0,
environment_type = "normal"
}
M.audio_effects = {
low_health_filter = false,
stress_reverb = 0.0,
combat_mix = false
}
print("Reactive Audio Controller inicializado")
end
function M.update_game_state(new_state)
local old_state = M.game_state
M.game_state = new_state
-- Reaccionar a cambios significativos
check_health_changes(old_state.health_percentage, new_state.health_percentage)
check_stress_changes(old_state.stress_level, new_state.stress_level)
check_combat_changes(old_state.combat_intensity, new_state.combat_intensity)
check_environment_changes(old_state.environment_type, new_state.environment_type)
-- Actualizar efectos de audio
update_audio_effects()
end
function check_health_changes(old_health, new_health)
if new_health < 0.3 and old_health >= 0.3 then
-- Activar filtro de salud baja
M.audio_effects.low_health_filter = true
start_heartbeat_audio()
elseif new_health >= 0.3 and old_health < 0.3 then
-- Desactivar filtro de salud baja
M.audio_effects.low_health_filter = false
stop_heartbeat_audio()
end
end
function check_stress_changes(old_stress, new_stress)
if math.abs(new_stress - old_stress) > 0.1 then
M.audio_effects.stress_reverb = new_stress
update_reverb_effect(new_stress)
end
end
function check_combat_changes(old_intensity, new_intensity)
if new_intensity > 0.5 and old_intensity <= 0.5 then
M.audio_effects.combat_mix = true
apply_combat_audio_mix()
elseif new_intensity <= 0.5 and old_intensity > 0.5 then
M.audio_effects.combat_mix = false
remove_combat_audio_mix()
end
end
function check_environment_changes(old_env, new_env)
if old_env ~= new_env then
transition_environment_audio(old_env, new_env)
end
end
function start_heartbeat_audio()
-- Reproducir latido cardíaco cuando la salud es baja
timer.delay(0, true, function(self, handle, time_elapsed)
if M.audio_effects.low_health_filter then
audio_manager.play_sfx("heartbeat", {volume = 0.3})
else
timer.cancel(handle)
end
end)
end
function stop_heartbeat_audio()
-- El heartbeat se detiene automáticamente con el timer
end
function update_reverb_effect(stress_level)
-- Simular efecto de reverb basado en estrés
-- En un proyecto real, esto se haría con effectos DSP
local reverb_amount = stress_level * 0.5
msg.post("@render:", "set_reverb", {amount = reverb_amount})
end
function apply_combat_audio_mix()
-- Aumentar volumen de SFX y reducir música durante combate
sound.set_group_gain(hash("sfx"), 1.0)
sound.set_group_gain(hash("music"), 0.4)
-- Filtro de paso alto para mayor intensidad
msg.post("@render:", "set_highpass_filter", {enabled = true})
end
function remove_combat_audio_mix()
-- Restaurar mix normal
sound.set_group_gain(hash("sfx"), 0.8)
sound.set_group_gain(hash("music"), 0.6)
msg.post("@render:", "set_highpass_filter", {enabled = false})
end
function transition_environment_audio(old_env, new_env)
-- Cambiar música ambiental basada en entorno
local music_state_map = {
forest = "ambient",
cave = "exploration",
boss_room = "boss_battle",
town = "menu"
}
local new_music_state = music_state_map[new_env] or "exploration"
music_controller.transition_to(new_music_state, 3.0)
-- Ajustar reverb ambiental
local reverb_map = {
cave = 0.8,
forest = 0.3,
town = 0.1,
boss_room = 0.6
}
local reverb_amount = reverb_map[new_env] or 0.2
update_reverb_effect(reverb_amount)
end
-- API para el game manager
function M.on_player_hit(damage_amount)
local intensity = math.min(1.0, damage_amount / 100)
audio_manager.play_sfx("player_hit", {volume_multiplier = intensity})
-- Screen shake audio cue
audio_manager.play_sfx("impact", {pitch_variance = 0.3})
end
function M.on_enemy_killed(enemy_type)
if enemy_type == "boss" then
audio_manager.play_sfx("boss_death")
music_controller.transition_to("victory", 1.0)
else
audio_manager.play_sfx("enemy_death")
end
end
function M.on_level_complete()
music_controller.transition_to("victory", 2.0)
audio_manager.play_sfx("level_complete")
end
function M.on_powerup_collected(powerup_type)
local sfx_map = {
speed = "powerup_speed",
health = "powerup_health",
weapon = "powerup_weapon"
}
local sfx_name = sfx_map[powerup_type] or "powerup_generic"
audio_manager.play_sfx(sfx_name)
end
return M
Ejercicios Avanzados
Ejercicio 1: Sistema de Diálogos
Crea un sistema de diálogos con:
- Voces sintéticas: Generación procedural de voces
- Subtítulos sincronizados: Timing perfecto con audio
- Emociones: Filtros de audio según estado emocional
- Idiomas múltiples: Sistema de localización
Ejercicio 2: Audio Procedural
Implementa generación de audio en tiempo real:
- Pasos adaptativos: Sonido basado en superficie y velocidad
- Clima dinámico: Audio de lluvia/viento que cambia
- Música generativa: Capas que se activan/desactivan
- Efectos de Doppler: Para objetos en movimiento
Ejercicio 3: Sistema de Mixing Inteligente
Crea un mixer que se adapta automáticamente:
- Ducking automático: Música baja cuando hay diálogos
- Compresión dinámica: Volumen adaptado al dispositivo
- EQ contextual: Ecualización según situación de juego
- Análisis espectral: Evitar frecuencias conflictivas
Próximos Pasos
En la lección final aprenderemos sobre publicación y monetización, preparando nuestros juegos para llegar al mercado.
⬅️ Anterior: Efectos y Partículas | Siguiente: Publicación y Monetización ➡️
¡Excelente! Ahora dominas el sistema de audio de Defold y puedes crear experiencias sonoras inmersivas que elevarán la calidad de tus juegos al siguiente nivel.