{}const=>[]async()letfn</>var
EntwicklungLua

Event-System in Lua: Erstellen einer flexiblen Architektur von Ereignissen, Abonnements und Signalen

Lernen Sie, wie man ein leistungsstarkes Event-System in Lua von Grund auf neu erstellt! Erfahren Sie, wie Sie Ereignisse richtig verarbeiten, Subskriptionen einrichten und mit Prioritäten und Signalen arbeiten. Praktische Beispiele für die Spieleentwicklung: von der einfachsten Implementierung bis zum vollwertigen System mit Debugging.

К

Kodik

Autor

8 Min. Lesezeit

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.

🔥 100.000+ Schüler sind bereits bei uns

Genug Theorie gelesen?
Zeit zu coden!

Kodik — eine App, in der du durch Praxis programmieren lernst. KI-Mentor, interaktive Lektionen, echte Projekte.

🤖 KI 24/7
🎓 Zertifikate
💰 Kostenlos
🚀 Jetzt starten
Heute beigetreten

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
end

Diese 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
end

Ein 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
end

Jetzt 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
end

Dieser 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)
end

Hier 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)
end

Jetzt 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()
end

Benannte 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 = true

Fehler 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
end

Auch 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 EventSystem

Das 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! 🚀

🎯Hör auf zu zögern

Artikel gefallen?
Zeit zum Üben!

Bei Kodik liest du nicht nur — du schreibst sofort Code. Theorie + Praxis = echte Skills.

Sofortige Praxis
🧠KI erklärt Code
🏆Zertifikat

Keine Registrierung • Keine Karte