El sistema de eventos es uno de los patrones de programación fundamentales que permite que diferentes partes de un programa se comuniquen entre sí sin crear una conexión rígida. Imagina que tienes un juego en el que un personaje puede recibir daño. En lugar de llamar directamente a las funciones de actualización de salud, interfaz y sonido, simplemente informamos: "Se ha producido un evento: el personaje ha recibido daño". Y todos los sistemas interesados reaccionarán a esto por sí mismos.

¿Por qué son necesarios los eventos?
En el enfoque clásico, si queremos que la interfaz se actualice, el sonido se reproduzca y la animación aparezca cuando se recibe daño, necesitamos escribir llamadas a todos estos sistemas en la función de daño. Esto crea un problema: nuestro código se vuelve acoplado, es difícil de mantener y ampliar. El sistema de eventos resuelve este problema de manera elegante: permite que los componentes del programa funcionen de forma independiente entre sí.
Digamos que estás desarrollando un juego en el que el jugador recoge monedas. En una versión simple, el código puede verse así: cuando recogemos una moneda, actualizamos el contador, reproducimos el sonido, mostramos la animación y comprobamos los logros. Todo esto está en una sola función. Pero, ¿qué pasa si en un mes quieres añadir un sistema de misiones que también debe saber sobre las monedas recogidas? Tendrás que volver a entrar en el código de recolección de monedas y añadir una nueva llamada. Con los eventos, simplemente suscriba el sistema de misiones al evento "moneda recogida" y recibirá notificaciones automáticamente.
La implementación más sencilla del sistema de eventos
Comencemos con la implementación básica. El sistema de eventos es básicamente una tabla donde las claves son los nombres de los eventos y los valores son listas de funciones de escucha que deben ejecutarse cuando ocurre un evento.
EventSystem = {}
EventSystem.events = {}
function EventSystem:subscribe(eventName, callback)
if not self.events[eventName] then
self.events[eventName] = {}
end
table.insert(self.events[eventName], callback)
end
function EventSystem:emit(eventName, ...)
if self.events[eventName] then
for _, callback in ipairs(self.events[eventName]) do
callback(...)
end
end
endEsta sencilla implementación ya es operativa. El método subscribe registra una función de escucha para un evento específico. Si aún no hay una lista de oyentes para este evento, creamos una tabla vacía. Luego agregamos la función a esta lista. El método emit llama a todos los oyentes registrados para un evento específico, pasándoles todos los parámetros que se pasaron durante la llamada emit.
Veamos cómo usarlo en la práctica:
-- Подписываемся на событие "player_damaged"
EventSystem:subscribe("player_damaged", function(damage, source)
print("Player received " .. damage .. " damage from " .. source)
end)
EventSystem:subscribe("player_damaged", function(damage)
-- Обновляем интерфейс
updateHealthBar(damage)
end)
EventSystem:subscribe("player_damaged", function()
-- Проигрываем звук
playSound("hurt.wav")
end)
-- Где-то в коде игры происходит событие
EventSystem:emit("player_damaged", 25, "Fireball")Como puedes ver, podemos asignar varias funciones a un evento, y todas se ejecutarán por turnos. Cada función recibe los parámetros del evento y puede usarlos a su manera. Una función muestra un mensaje en la consola, otra actualiza la interfaz, la tercera reproduce el sonido. Al mismo tiempo, el lugar donde ocurre el evento (emit) no sabe nada sobre estas funciones y no depende de ellas.
Darse de baja de eventos
A veces hay que dejar de escuchar un evento. Por ejemplo, el efecto temporal ha terminado o el objeto se ha eliminado del juego. Añadiremos la posibilidad de darse de baja:
function EventSystem:unsubscribe(eventName, callback)
if not self.events[eventName] then
return
end
for i, listener in ipairs(self.events[eventName]) do
if listener == callback then
table.remove(self.events[eventName], i)
return
end
end
endUn punto importante: para cancelar la suscripción, necesitamos un enlace a la misma función que suscribimos. Las funciones anónimas no se pueden cancelar si no hemos guardado un enlace a ellas:
-- Неправильно: нельзя отписаться
EventSystem:subscribe("event", function() print("test") end)
EventSystem:unsubscribe("event", function() print("test") end) -- Не сработает!
-- Правильно: сохраняем ссылку
local myCallback = function() print("test") end
EventSystem:subscribe("event", myCallback)
EventSystem:unsubscribe("event", myCallback) -- Работает!Prioridades de eventos
En algunos casos, el orden de ejecución de los controladores es importante. Por ejemplo, queremos que la comprobación de bloqueo de ataque se produzca antes de que se produzca el daño. Añadamos un sistema de prioridades:
function EventSystem:subscribe(eventName, callback, priority)
if not self.events[eventName] then
self.events[eventName] = {}
end
priority = priority or 0
local listener = {
callback = callback,
priority = priority
}
table.insert(self.events[eventName], listener)
-- Сортируем по приоритету (больший приоритет выполняется раньше)
table.sort(self.events[eventName], function(a, b)
return a.priority > b.priority
end)
end
function EventSystem:emit(eventName, ...)
if self.events[eventName] then
for _, listener in ipairs(self.events[eventName]) do
listener.callback(...)
end
end
endAhora podemos especificar la prioridad al suscribirse:
EventSystem:subscribe("player_attack", function()
print("Unit check")
end, 10)
EventSystem:subscribe("player_attack", function()
print("Damage")
end, 5)
EventSystem:subscribe("player_attack", function()
print("Impact animation")
end, 1)
Transferencia y modificación de datos
A menudo es necesario no solo notificar un evento, sino también transmitir datos que los oyentes pueden modificar. Por ejemplo, el sistema de blindaje puede reducir el daño recibido. Para ello, puedes enviar una tabla con los datos:
local eventData = {
damage = 50,
damageType = "fire",
cancelled = false
}
EventSystem:subscribe("before_damage", function(data)
-- Проверяем иммунитет к огню
if player.hasFireImmunity and data.damageType == "fire" then
data.cancelled = true
end
end, 10)
EventSystem:subscribe("before_damage", function(data)
-- Применяем защиту от брони
data.damage = data.damage * (1 - player.armor / 100)
end, 5)
EventSystem:emit("before_damage", eventData)
if not eventData.cancelled then
player.health = player.health - eventData.damage
endEste enfoque permite a los oyentes influir en el resultado del evento. Podemos cancelar el evento por completo (cancelado = verdadero) o cambiar sus parámetros (reducir el daño).
Suscripciones de un solo uso
A veces es necesario que el controlador funcione solo una vez. Por ejemplo, el evento "juego cargado" debe procesarse una vez. Agreguemos el método once:
function EventSystem:once(eventName, callback, priority)
local onceWrapper
onceWrapper = function(...)
callback(...)
self:unsubscribe(eventName, onceWrapper)
end
self:subscribe(eventName, onceWrapper, priority)
endAquí creamos una función de envoltura que llama a nuestro callback y luego se da de baja del evento. El uso es simple:
EventSystem:once("game_loaded", function()
print("Game loaded! This message will only appear once")
initializeGame()
end)Ejemplo práctico: sistema de logros
Vamos a crear un sistema de logros sencillo utilizando nuestro sistema de eventos:
Achievements = {
list = {
first_kill = { unlocked = false, name = "First Blood" },
coin_collector = { unlocked = false, name = "Collector", coins_needed = 100 }
},
coins_collected = 0
}
function Achievements:init()
EventSystem:subscribe("enemy_killed", function()
if not self.list.first_kill.unlocked then
self.list.first_kill.unlocked = true
self:showAchievement(self.list.first_kill.name)
end
end)
EventSystem:subscribe("coin_collected", function()
self.coins_collected = self.coins_collected + 1
if self.coins_collected >= self.list.coin_collector.coins_needed
and not self.list.coin_collector.unlocked then
self.list.coin_collector.unlocked = true
self:showAchievement(self.list.coin_collector.name)
end
end)
end
function Achievements:showAchievement(name)
print("🏆 Achievement received: " .. name)
EventSystem:emit("achievement_unlocked", name)
endAhora, en cualquier parte del código del juego, podemos simplemente llamar a los eventos y el sistema de logros seguirá el progreso automáticamente:
-- В коде боевой системы
function Enemy:die()
EventSystem:emit("enemy_killed", self)
self:remove()
end
-- В коде сбора предметов
function Coin:collect()
EventSystem:emit("coin_collected")
self:remove()
endSeñales con nombre y tipificación
Para proyectos grandes, es útil crear una lista centralizada de todos los eventos del juego. Esto ayuda a evitar errores tipográficos y hace que el código sea más fácil de entender:
Events = {
PLAYER_DAMAGED = "player_damaged",
PLAYER_HEALED = "player_healed",
ENEMY_KILLED = "enemy_killed",
COIN_COLLECTED = "coin_collected",
LEVEL_COMPLETED = "level_completed",
GAME_PAUSED = "game_paused"
}
-- Использование
EventSystem:subscribe(Events.PLAYER_DAMAGED, function(damage)
print("Damage received: " .. damage)
end)
EventSystem:emit(Events.PLAYER_DAMAGED, 25)Consejo: Este enfoque protege contra errores tipográficos: si escribimos Events.PLAYER_DAMAGD (con un error tipográfico), Lua arrojará un error y no solo no encontrará el evento.
Depuración del sistema de eventos
Cuando hay muchos eventos en un proyecto, es útil añadir la capacidad de realizar un seguimiento de sus llamadas:
EventSystem.debug = false
function EventSystem:emit(eventName, ...)
if self.debug then
print("Event fired: " .. eventName)
end
if self.events[eventName] then
for i, listener in ipairs(self.events[eventName]) do
if self.debug then
print(" -> Calling listener #" .. i)
end
listener.callback(...)
end
end
end
-- Включаем отладку
EventSystem.debug = trueErrores en los controladores
Un detalle importante: si se produce un error en uno de los controladores, los demás no se ejecutarán. Puede añadir protección:
function EventSystem:emit(eventName, ...)
if self.events[eventName] then
for _, listener in ipairs(self.events[eventName]) do
local success, error = pcall(listener.callback, ...)
if not success then
print("Error in event handler: " .. tostring(error))
end
end
end
endAhora, incluso si un controlador falla, los demás seguirán funcionando.
Implementación completa
Aquí está la versión final de nuestro sistema de eventos con todas las funciones:
EventSystem = {
events = {},
debug = false
}
function EventSystem:subscribe(eventName, callback, priority)
if not self.events[eventName] then
self.events[eventName] = {}
end
priority = priority or 0
local listener = {
callback = callback,
priority = priority
}
table.insert(self.events[eventName], listener)
table.sort(self.events[eventName], function(a, b)
return a.priority > b.priority
end)
return callback
end
function EventSystem:unsubscribe(eventName, callback)
if not self.events[eventName] then return end
for i, listener in ipairs(self.events[eventName]) do
if listener.callback == callback then
table.remove(self.events[eventName], i)
return true
end
end
return false
end
function EventSystem:once(eventName, callback, priority)
local onceWrapper
onceWrapper = function(...)
callback(...)
self:unsubscribe(eventName, onceWrapper)
end
self:subscribe(eventName, onceWrapper, priority)
return onceWrapper
end
function EventSystem:emit(eventName, ...)
if self.debug then
print("Event: " .. eventName)
end
if self.events[eventName] then
for i, listener in ipairs(self.events[eventName]) do
local success, error = pcall(listener.callback, ...)
if not success then
print("Event handler error in '" .. eventName .. "': " .. tostring(error))
end
end
end
end
function EventSystem:clear(eventName)
if eventName then
self.events[eventName] = nil
else
self.events = {}
end
end
return EventSystemEl sistema de eventos es una herramienta poderosa que hace que tu código sea más flexible y extensible. En lugar de una conexión rígida entre los componentes, se obtiene una arquitectura de baja acoplamiento en la que cada sistema puede funcionar de forma independiente. Esto es especialmente importante en los juegos, donde interactúan decenas de sistemas diferentes: gráficos, sonido, física, inteligencia artificial, interfaz y mucho más.
Puedes aprender Lua y otros lenguajes de programación desde cero en Codice — nuestra plataforma educativa con cursos prácticos para desarrolladores principiantes.
Y también tenemos un genial canal de Telegram con una comunidad amistosa donde puedes hacer preguntas, compartir tus proyectos y conectar con personas de ideas afines. 🚀
