Event-система в Lua: создаём гибкую архитектуру событий, подписок и сигналов
Научитесь создавать мощную Event-систему в Lua с нуля! Узнайте, как правильно обрабатывать события, настраивать подписки, работать с приоритетами и сигналами. Практические примеры для игровой разработки: от простейшей реализации до полноценной системы с отладкой.
Event-система (система событий) — это один из фундаментальных паттернов программирования, который позволяет различным частям программы общаться между собой, не создавая жёсткой связи. Представьте, что у вас есть игра, где персонаж может получить урон. Вместо того чтобы напрямую вызывать функции обновления здоровья, интерфейса и звуков, мы просто сообщаем: "Произошло событие — персонаж получил урон". А все заинтересованные системы сами на это отреагируют.

Зачем нужны события?
В классическом подходе, если мы хотим, чтобы при получении урона обновился интерфейс, заиграл звук и появилась анимация, нам нужно в функции получения урона прописать вызовы всех этих систем. Это создаёт проблему: наш код становится связанным, его сложно поддерживать и расширять. Event-система решает эту проблему элегантно — она позволяет компонентам программы работать независимо друг от друга.
Допустим, вы разрабатываете игру, где игрок собирает монеты. В простом варианте код может выглядеть так: при сборе монеты мы обновляем счётчик, проигрываем звук, показываем анимацию и проверяем достижения. Всё это находится в одной функции. Но что если через месяц вы захотите добавить систему квестов, которая тоже должна знать о собранных монетах? Придётся снова лезть в код сбора монет и добавлять туда новый вызов. С событиями достаточно просто подписать систему квестов на событие "монета собрана", и она будет получать уведомления автоматически.
Простейшая реализация Event-системы
Начнём с базовой реализации. Event-система в своей основе — это таблица, где ключами являются названия событий, а значениями — списки функций-слушателей, которые должны выполниться при возникновении события.
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
endЭта простая реализация уже работоспособна. Метод subscribe регистрирует функцию-слушателя для конкретного события. Если для данного события ещё нет списка слушателей, мы создаём пустую таблицу. Затем добавляем функцию в этот список. Метод emit вызывает все зарегистрированные слушатели для конкретного события, передавая им все параметры, которые были переданы при вызове emit.
Давайте посмотрим, как это использовать на практике:
-- Подписываемся на событие "player_damaged"
EventSystem:subscribe("player_damaged", function(damage, source)
print("Игрок получил " .. damage .. " урона от " .. 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, "Огненный шар")Как видите, мы можем подписать несколько функций на одно событие, и все они выполнятся по очереди. Каждая функция получает параметры события и может использовать их по-своему. Одна функция выводит сообщение в консоль, другая обновляет интерфейс, третья проигрывает звук. При этом место, где происходит событие (emit), ничего не знает об этих функциях и не зависит от них.
Отписка от событий
Иногда нужно перестать слушать событие. Например, временный эффект закончился, или объект был удалён из игры. Добавим возможность отписки:
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
endВажный момент — для отписки нам нужна ссылка на ту же самую функцию, которую мы подписывали. Анонимные функции отписать не получится, если мы не сохранили на них ссылку:
-- Неправильно: нельзя отписаться
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) -- Работает!Приоритеты событий
В некоторых случаях важен порядок выполнения обработчиков. Например, мы хотим, чтобы проверка на блокирование атаки происходила раньше, чем нанесение урона. Добавим систему приоритетов:
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
endТеперь мы можем указать приоритет при подписке:
EventSystem:subscribe("player_attack", function()
print("Проверка блока")
end, 10)
EventSystem:subscribe("player_attack", function()
print("Нанесение урона")
end, 5)
EventSystem:subscribe("player_attack", function()
print("Анимация удара")
end, 1)
Передача данных и их модификация
Часто нужно не просто уведомить о событии, но и передать данные, которые слушатели могут модифицировать. Например, система брони может уменьшить получаемый урон. Для этого можно передавать таблицу с данными:
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
endТакой подход позволяет слушателям влиять на результат события. Мы можем отменить событие полностью (cancelled = true) или изменить его параметры (уменьшить урон).
Одноразовые подписки
Иногда нужно, чтобы обработчик сработал только один раз. Например, событие "игра загружена" должно обработаться единожды. Добавим метод once:
function EventSystem:once(eventName, callback, priority)
local onceWrapper
onceWrapper = function(...)
callback(...)
self:unsubscribe(eventName, onceWrapper)
end
self:subscribe(eventName, onceWrapper, priority)
endЗдесь мы создаём функцию-обёртку, которая вызывает наш callback, а затем отписывается от события. Использование простое:
EventSystem:once("game_loaded", function()
print("Игра загружена! Это сообщение появится только один раз")
initializeGame()
end)Практический пример: система достижений
Давайте создадим простую систему достижений, используя нашу Event-систему:
Achievements = {
list = {
first_kill = { unlocked = false, name = "Первая кровь" },
coin_collector = { unlocked = false, name = "Коллекционер", 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("🏆 Достижение получено: " .. name)
EventSystem:emit("achievement_unlocked", name)
endТеперь где угодно в коде игры мы можем просто вызывать события, и система достижений будет отслеживать прогресс автоматически:
-- В коде боевой системы
function Enemy:die()
EventSystem:emit("enemy_killed", self)
self:remove()
end
-- В коде сбора предметов
function Coin:collect()
EventSystem:emit("coin_collected")
self:remove()
endИменованные сигналы и типизация
Для больших проектов полезно создать централизованный список всех событий в игре. Это помогает избежать опечаток и делает код более понятным:
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)
end)
EventSystem:emit(Events.PLAYER_DAMAGED, 25)Совет: Такой подход защищает от опечаток — если мы напишем Events.PLAYER_DAMAGD (с опечаткой), Lua выдаст ошибку, а не просто не найдёт событие.
Отладка Event-системы
Когда в проекте много событий, полезно добавить возможность отслеживать их вызовы:
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 = trueОшибки в обработчиках
Важная деталь — если в одном из обработчиков произойдёт ошибка, остальные обработчики не выполнятся. Можно добавить защиту:
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
endТеперь даже если один обработчик упадёт с ошибкой, остальные продолжат работать.
Полная реализация
Вот финальная версия нашей Event-системы со всеми возможностями:
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 EventSystemEvent-система — это мощный инструмент, который делает ваш код более гибким и расширяемым. Вместо жёсткой связи между компонентами вы получаете слабосвязанную архитектуру, где каждая система может работать независимо. Это особенно важно в играх, где взаимодействуют десятки различных систем: графика, звук, физика, искусственный интеллект, интерфейс и многое другое.
Освоить Lua и другие языки программирования с нуля можно в Кодике — нашей образовательной платформе с практическими курсами для начинающих разработчиков.
А ещё у нас есть крутой телеграм канал с дружеским комьюнити, где можно задавать вопросы, делиться своими проектами и общаться с единомышленниками! 🚀