Le système d'événements est l'un des modèles de programmation fondamentaux qui permet à différentes parties d'un programme de communiquer entre elles sans créer de lien rigide. Imaginez que vous avez un jeu où le personnage peut subir des dégâts. Au lieu d'appeler directement les fonctions de mise à jour de la santé, de l'interface et des sons, nous signalons simplement : « Un événement s'est produit : le personnage a subi des dégâts ». Et tous les systèmes intéressés y réagiront eux-mêmes.

Pourquoi avons-nous besoin d'événements ?
Dans l'approche classique, si nous voulons que l'interface se mette à jour, que le son joue et que l'animation apparaisse lorsque des dégâts sont subis, nous devons écrire les appels de tous ces systèmes dans la fonction de dégâts. Cela crée un problème : notre code devient lié, il est difficile à maintenir et à développer. Le système d'événements résout ce problème avec élégance : il permet aux composants du programme de fonctionner indépendamment les uns des autres.
Supposons que vous développiez un jeu où le joueur collecte des pièces. Dans une version simple, le code peut ressembler à ceci : lors de la collecte d'une pièce, nous mettons à jour le compteur, jouons un son, affichons une animation et vérifions les réalisations. Tout cela est dans une seule fonction. Mais que se passe-t-il si, dans un mois, vous souhaitez ajouter un système de quête qui doit également connaître les pièces collectées ? Vous devrez à nouveau entrer dans le code de collecte de pièces et y ajouter un nouvel appel. Avec les événements, il suffit de s'abonner au système de quête pour l'événement « pièce collectée », et il recevra automatiquement des notifications.
La mise en œuvre la plus simple du système d'événements
Commençons par la mise en œuvre de base. Le système d'événements est essentiellement un tableau où les clés sont les noms des événements et les valeurs sont des listes de fonctions d'écoute qui doivent être exécutées lorsqu'un événement se produit.
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
endCette implémentation simple est déjà opérationnelle. La méthode subscribe enregistre une fonction d'écoute pour un événement spécifique. S'il n'y a pas encore de liste d'auditeurs pour cet événement, nous créons un tableau vide. Ensuite, nous ajoutons la fonction à cette liste. La méthode emit appelle tous les auditeurs enregistrés pour un événement spécifique, en leur transmettant tous les paramètres qui ont été transmis lors de l'appel d'émission.
Voyons comment l'utiliser en pratique :
-- Подписываемся на событие "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")Comme vous pouvez le voir, nous pouvons attribuer plusieurs fonctions à un événement, et elles seront toutes exécutées à tour de rôle. Chaque fonction reçoit les paramètres de l'événement et peut les utiliser à sa manière. Une fonction affiche un message dans la console, une autre met à jour l'interface, une troisième joue un son. Dans ce cas, l'endroit où l'événement se produit (emit) ne sait rien de ces fonctions et n'en dépend pas.
Se désabonner des événements
Parfois, vous devez arrêter d'écouter l'événement. Par exemple, l'effet temporaire est terminé ou l'objet a été supprimé du jeu. Ajoutons la possibilité de se désabonner :
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 point important : pour se désabonner, nous avons besoin d'un lien vers la même fonction que nous avons signée. Les fonctions anonymes ne peuvent pas être désabonnées si nous n'avons pas enregistré de lien vers elles :
-- Неправильно: нельзя отписаться
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és des événements
Dans certains cas, l'ordre d'exécution des gestionnaires est important. Par exemple, nous voulons que la vérification du blocage des attaques se produise avant que les dégâts ne soient infligés. Ajoutons un système de priorités :
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
endNous pouvons maintenant spécifier la priorité lors de l'abonnement :
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)
Transfert et modification des données
Il est souvent nécessaire non seulement de notifier un événement, mais aussi de transmettre des données que les auditeurs peuvent modifier. Par exemple, le système d'armure peut réduire les dégâts subis. Pour ce faire, vous pouvez transférer un tableau avec les données :
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
endCette approche permet aux auditeurs d'influencer le résultat de l'événement. Nous pouvons annuler complètement l'événement (cancelled = true) ou modifier ses paramètres (réduire les dégâts).
Abonnements uniques
Parfois, il est nécessaire que le gestionnaire ne fonctionne qu'une seule fois. Par exemple, l'événement « jeu chargé » doit être traité une seule fois. Ajoutons la méthode once :
function EventSystem:once(eventName, callback, priority)
local onceWrapper
onceWrapper = function(...)
callback(...)
self:unsubscribe(eventName, onceWrapper)
end
self:subscribe(eventName, onceWrapper, priority)
endIci, nous créons une fonction d'enveloppe qui appelle notre rappel, puis se désabonne de l'événement. L'utilisation est simple :
EventSystem:once("game_loaded", function()
print("Game loaded! This message will only appear once")
initializeGame()
end)Exemple pratique : système de réalisations
Créons un système de réalisations simple en utilisant notre système d'événements :
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)
endMaintenant, n'importe où dans le code du jeu, nous pouvons simplement appeler des événements, et le système de réalisations suivra automatiquement les progrès :
-- В коде боевой системы
function Enemy:die()
EventSystem:emit("enemy_killed", self)
self:remove()
end
-- В коде сбора предметов
function Coin:collect()
EventSystem:emit("coin_collected")
self:remove()
endSignaux nommés et typage
Pour les grands projets, il est utile de créer une liste centralisée de tous les événements du jeu. Cela permet d'éviter les fautes de frappe et rend le code plus compréhensible :
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)Conseil : Cette approche protège contre les fautes de frappe - si nous écrivons Events.PLAYER_DAMAGD (avec une faute de frappe), Lua renverra une erreur, et ne trouvera pas simplement l'événement.
Débogage du système d'événements
Lorsque le projet comporte de nombreux événements, il est utile d'ajouter la possibilité de suivre leurs appels :
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 = trueErreurs dans les gestionnaires
Détail important — si une erreur se produit dans l'un des gestionnaires, les autres gestionnaires ne s'exécuteront pas. Vous pouvez ajouter une protection :
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
endDésormais, même si un gestionnaire tombe en erreur, les autres continueront à fonctionner.
Mise en œuvre complète
Voici la version finale de notre système d'événements avec toutes les fonctionnalités :
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 EventSystemLe système d'événements est un outil puissant qui rend votre code plus flexible et extensible. Au lieu d'une connexion rigide entre les composants, vous obtenez une architecture faiblement liée où chaque système peut fonctionner indépendamment. Ceci est particulièrement important dans les jeux où des dizaines de systèmes différents interagissent : graphismes, son, physique, intelligence artificielle, interface et bien plus encore.
Vous pouvez apprendre Lua et d'autres langages de programmation à partir de zéro dans Codique — notre plateforme éducative avec des cours pratiques pour les développeurs débutants.
Et nous avons aussi un super chaîne Telegram avec une communauté amicale où vous pouvez poser des questions, partager vos projets et discuter avec des personnes partageant les mêmes idées ! 🚀
