イベントシステムは、プログラムのさまざまな部分が厳密な接続を作成することなく相互に通信できるようにする基本的なプログラミングパターンの1つです。キャラクターがダメージを受ける可能性のあるゲームがあるとします。健康、インターフェース、サウンドの更新機能を直接呼び出す代わりに、「イベントが発生しました—キャラクターがダメージを受けました」と報告します。そして、すべての関係するシステムはそれに反応します。

イベントが必要な理由
従来のアプローチでは、ダメージを受けたときにインターフェースを更新し、サウンドを再生し、アニメーションを表示するには、これらすべてのシステムの呼び出しをダメージ関数に記述する必要があります。これにより、コードが結合され、維持および拡張が難しくなるという問題が発生します。イベントシステムはこの問題をエレガントに解決します。プログラムのコンポーネントが互いに独立して動作することを可能にします。
プレイヤーがコインを集めるゲームを開発しているとします。簡単なバージョンでは、コインを集めるときにカウンターを更新し、サウンドを再生し、アニメーションを表示し、達成度を確認するなど、コードは次のようになります。これらはすべて1つの関数に含まれています。しかし、1か月後に、収集されたコインについても知る必要があるクエストシステムを追加したい場合はどうなりますか?コインコレクションコードに再び入り、そこに新しい呼び出しを追加する必要があります。イベントでは、クエストシステムを「コイン収集」イベントにサインするだけで、自動的に通知を受け取ることができます。
イベントシステムの最も単純な実装
基本的な実装から始めましょう。イベントシステムは基本的に、キーがイベント名、値がイベント発生時に実行されるリスナ関数のリストであるテーブルです。
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("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")ご覧のとおり、1つのイベントに複数の関数を割り当てることができ、それらはすべて順番に実行されます。各関数はイベントパラメータを受け取り、独自の方法でそれらを使用できます。1つの関数はコンソールにメッセージを出力し、もう1つはインターフェースを更新し、3つ目はサウンドを再生します。同時に、イベントが発生する場所(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("Unit check")
end, 10)
EventSystem:subscribe("player_attack", function()
print("Damage")
end, 5)
EventSystem:subscribe("player_attack", function()
print("Impact animation")
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このアプローチにより、リスナーはイベントの結果に影響を与えることができます。イベントを完全にキャンセルするか(キャンセル済み= true )、パラメータを変更するか(ダメージを減らす)ことができます。
1回限りのサブスクリプション
ハンドラーが1回だけ動作する必要がある場合があります。たとえば、「ゲームがロードされた」イベントは1回のみ処理する必要があります。メソッドを追加します once:
function EventSystem:once(eventName, callback, priority)
local onceWrapper
onceWrapper = function(...)
callback(...)
self:unsubscribe(eventName, onceWrapper)
end
self:subscribe(eventName, onceWrapper, priority)
endここでは、コールバックを呼び出し、イベントのサブスクリプションを解除するラッパー関数を作成します。使い方は簡単です。
EventSystem:once("game_loaded", function()
print("Game loaded! This message will only appear once")
initializeGame()
end)実例:達成システム
イベントシステムを使用して、シンプルなアチーブメントシステムを作成しましょう。
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これでゲームコードのどこからでもイベントを呼び出すことができ、アチーブメントシステムが自動的に進行状況を追跡します。
-- В коде боевой системы
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 received: " .. damage)
end)
EventSystem:emit(Events.PLAYER_DAMAGED, 25)ヒント: このアプローチは、入力ミスから保護します。Events.PLAYER_DAMAGD (入力ミス)と入力すると、Lua はイベントを見つけられないだけでなく、エラーを出します。
イベントシステムのデバッグ
プロジェクトにイベントが多い場合は、それらの呼び出しを追跡する機能を追加すると便利です。
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ハンドラのエラー
重要な点は、ハンドラーの1つでエラーが発生した場合、残りのハンドラーが実行されないことです。保護を追加できます。
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これで、1つのハンドラーがエラーで終了しても、残りのハンドラーは動作し続けます。
完全実装
これが、すべての機能を備えたイベントシステムの最終バージョンです。
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イベントシステムは、コードをより柔軟で拡張性の高いものにする強力なツールです。コンポーネント間の厳密な接続の代わりに、各システムが独立して動作できる、ゆるやかに接続されたアーキテクチャーを得ることができます。これは、グラフィックス、サウンド、物理、AI、インターフェースなど、数十の異なるシステムが相互作用するゲームでは特に重要です。
Luaやその他のプログラミング言語をゼロから習得することができます コディケ — 初心者向けの実践的なコースを提供する当社の教育プラットフォーム。
そして、私たちは素晴らしい Telegramチャンネル 質問をしたり、プロジェクトを共有したり、志を同じくする人々と交流したりできるフレンドリーなコミュニティ! 🚀
