Das Event-System (Ereignissystem) ist eines der grundlegenden Programmiermuster, mit dem verschiedene Teile eines Programms miteinander kommunizieren können, ohne eine starre Verbindung herzustellen. Stellen Sie sich vor, Sie haben ein Spiel, in dem ein Charakter Schaden erleiden kann. Anstatt direkt die Funktionen zum Aktualisieren von Gesundheit, Benutzeroberfläche und Sounds aufzurufen, melden wir einfach: „Ein Ereignis ist aufgetreten – der Charakter hat Schaden erlitten.“ Und alle interessierten Systeme werden selbst darauf reagieren.

Warum brauchen wir Veranstaltungen?
Wenn wir beim klassischen Ansatz möchten, dass die Benutzeroberfläche aktualisiert wird, der Ton abgespielt wird und die Animation angezeigt wird, wenn Schaden erlitten wird, müssen wir die Aufrufe all dieser Systeme in der Schadensfunktion schreiben. Dies schafft ein Problem: Unser Code wird gebunden, er ist schwer zu warten und zu erweitern. Das Event-System löst dieses Problem elegant - es ermöglicht den Programmkomponenten, unabhängig voneinander zu arbeiten.
Angenommen, Sie entwickeln ein Spiel, in dem der Spieler Münzen sammelt. In einer einfachen Version könnte der Code so aussehen: Wenn wir Münzen sammeln, aktualisieren wir den Zähler, spielen einen Ton ab, zeigen eine Animation und überprüfen die Erfolge. All dies ist in einer Funktion enthalten. Aber was ist, wenn Sie in einem Monat ein Questsystem hinzufügen möchten, das auch über die gesammelten Münzen Bescheid wissen muss? Sie müssen wieder in den Münzsammlungscode einsteigen und dort einen neuen Aufruf hinzufügen. Bei Ereignissen reicht es aus, das Questsystem für das Ereignis "Münze gesammelt" zu signieren, und es wird automatisch benachrichtigt.
Die einfachste Implementierung des Event-Systems
Beginnen wir mit der grundlegenden Implementierung. Das Ereignissystem ist im Grunde eine Tabelle, in der die Schlüssel die Namen der Ereignisse sind und die Werte die Listen der Listener-Funktionen sind, die ausgeführt werden müssen, wenn ein Ereignis auftritt.
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
endDiese einfache Implementierung ist bereits funktionsfähig. Die Methode subscribe registriert eine Listener-Funktion für ein bestimmtes Ereignis. Wenn es für dieses Ereignis noch keine Zuhörerliste gibt, erstellen wir eine leere Tabelle. Dann fügen wir die Funktion zu dieser Liste hinzu. Die Methode emit ruft alle registrierten Listener für ein bestimmtes Ereignis auf und übergibt ihnen alle Parameter, die beim Aufruf von emit übergeben wurden.
Sehen wir uns an, wie man das in der Praxis anwendet:
-- Подписываемся на событие "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")Wie Sie sehen, können wir mehrere Funktionen für ein Ereignis signieren, und alle werden nacheinander ausgeführt. Jede Funktion erhält Ereignisparameter und kann diese auf ihre eigene Weise verwenden. Eine Funktion gibt eine Nachricht an die Konsole aus, eine andere aktualisiert die Benutzeroberfläche, eine dritte spielt einen Ton ab. Gleichzeitig weiß der Ort, an dem das Ereignis (emit) stattfindet, nichts über diese Funktionen und ist nicht von ihnen abhängig.
Abmeldung von Ereignissen
Manchmal muss man aufhören, auf ein Ereignis zu hören. Zum Beispiel ist ein temporärer Effekt beendet oder ein Objekt wurde aus dem Spiel entfernt. Fügen Sie die Möglichkeit zum Abbestellen hinzu:
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
endEin wichtiger Punkt: Um uns abzumelden, benötigen wir einen Link zu derselben Funktion, die wir abonniert haben. Anonyme Funktionen können nicht abgemeldet werden, wenn wir keinen Link zu ihnen gespeichert haben:
-- Неправильно: нельзя отписаться
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) -- Работает!Prioritäten der Ereignisse
In einigen Fällen ist die Reihenfolge der Ausführung der Handler wichtig. Zum Beispiel möchten wir, dass die Überprüfung auf Angriffsblockierung vor dem Schaden erfolgt. Fügen Sie ein Prioritätssystem hinzu:
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
endJetzt können wir die Priorität beim Abonnieren angeben:
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)
Datenübertragung und -änderung
Oft ist es notwendig, nicht nur über ein Ereignis zu informieren, sondern auch Daten zu übertragen, die die Zuhörer ändern können. Zum Beispiel kann das Rüstungssystem den erlittenen Schaden verringern. Dazu können Sie eine Tabelle mit Daten übergeben:
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
endDieser Ansatz ermöglicht es den Zuhörern, das Ergebnis des Ereignisses zu beeinflussen. Wir können das Ereignis vollständig abbrechen (cancelled = true) oder seine Parameter ändern (Schaden reduzieren).
Einmalige Abonnements
Manchmal ist es notwendig, dass der Handler nur einmal ausgelöst wird. Beispielsweise sollte das Ereignis "Spiel geladen" einmal verarbeitet werden. Fügen Sie die Methode once hinzu:
function EventSystem:once(eventName, callback, priority)
local onceWrapper
onceWrapper = function(...)
callback(...)
self:unsubscribe(eventName, onceWrapper)
end
self:subscribe(eventName, onceWrapper, priority)
endHier erstellen wir eine Wrapper-Funktion, die unseren Callback aufruft und sich dann vom Ereignis abmeldet. Die Verwendung ist einfach:
EventSystem:once("game_loaded", function()
print("Game loaded! This message will only appear once")
initializeGame()
end)Praktisches Beispiel: Leistungssystem
Erstellen wir ein einfaches System von Errungenschaften mit unserem Event-System:
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)
endJetzt können wir überall im Spielcode einfach Ereignisse aufrufen, und das Achievement-System verfolgt den Fortschritt automatisch:
-- В коде боевой системы
function Enemy:die()
EventSystem:emit("enemy_killed", self)
self:remove()
end
-- В коде сбора предметов
function Coin:collect()
EventSystem:emit("coin_collected")
self:remove()
endBenannte Signale und Typisierung
Bei großen Projekten ist es hilfreich, eine zentrale Liste aller Ereignisse im Spiel zu erstellen. Dies hilft, Tippfehler zu vermeiden und den Code verständlicher zu machen:
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)Tipp: Dieser Ansatz schützt vor Tippfehlern - wenn wir Events.PLAYER_DAMAGD (mit einem Tippfehler) schreiben, gibt Lua einen Fehler aus und findet das Ereignis nicht.
Debuggen des Event-Systems
Wenn es viele Ereignisse in einem Projekt gibt, ist es hilfreich, die Möglichkeit hinzuzufügen, ihre Aufrufe zu verfolgen:
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 = trueFehler in den Handlern
Ein wichtiges Detail: Wenn in einem der Handler ein Fehler auftritt, werden die anderen Handler nicht ausgeführt. Sie können Schutz hinzufügen:
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
endAuch wenn ein Handler mit einem Fehler ausfällt, arbeiten die anderen weiter.
Vollständige Umsetzung
Hier ist die endgültige Version unseres Event-Systems mit allen Funktionen:
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 EventSystemDas Event-System ist ein leistungsstarkes Tool, das Ihren Code flexibler und erweiterbarer macht. Anstelle einer starren Verbindung zwischen den Komponenten erhalten Sie eine lose gekoppelte Architektur, in der jedes System unabhängig arbeiten kann. Dies ist besonders wichtig bei Spielen, in denen Dutzende verschiedener Systeme interagieren: Grafik, Sound, Physik, künstliche Intelligenz, Benutzeroberfläche und vieles mehr.
Sie können Lua und andere Programmiersprachen von Grund auf erlernen Kodike — unsere Bildungsplattform mit praktischen Kursen für angehende Entwickler.
Und wir haben auch einen coolen Telegram-Kanal mit einer freundlichen Community, in der Sie Fragen stellen, Ihre Projekte teilen und sich mit Gleichgesinnten austauschen können! 🚀
