Table Of Contents
- "Send me a Signal"
- EventManager Class
- Add a Listener
- Triggering a Signal
- Queuing a Signal
- Remove a Listener
- The Complete Code
- What's Next?
Logs: ⬅️
"Send me a Signal"
If you're an old fart like me, you'll probably remember a line from a Chris de Burgh song released in 1982: "Ship to shore, answer my call, Send me a signal, a beacon to bring me home."
What does this have to do with game development? Well, the first thing I code in any game I make is a form of the Observer pattern. It allows you to send signals to trigger events in different parts of your game engine without actually calling the code directly.
⚠️ This technique is only useful in fire-and-forget type situations. If you need a response from your call, you shouldn't use this method. Don't get trapped in the situation where you try to make signals talk to each other. It can become messy and very difficult to bug hunt.
For instance, if you're coding a battle system, there'll be a need to update the health bars of your characters. However, you don't want your battle system to directly update the GUI interface because if the GUI code changes or you want to reuse that battle system code for a different game, then your battle system code will break as soon as the code it depends on changes. Sending signals solves this. (Say that 5 times fast)
At a high level, different objects in your game will register to listen for certain signals. When a particular signal is transmitted, it goes into a queue where it is picked up by the Event Manager and broadcast to every object that registered for that signal. You can even package up some data to send along with the Signal. Let's implement this in the MiniScript language.
EventManager Class
First, let's create the EventManager class that will manage listeners and trigger signals.
EventManager = {
"listeners": {},
"queue": [],
}
As you can see, it doesn't take much to define the properties for the manager. The listeners are stored in a map
data type named listeners
. When an object wants to listen for a signal, they are added to that map
.
📚 In MiniScript,
map
data types are just a form of dictionary or associative array.
As an example, this is what the value of the listeners
map might look like after a couple of listeners are added:
"listeners" : {
"signal_name" : [
{
"listener": some_object,
"callback": some_function,
},
{
"listener": another_object,
"callback": another_function,
}
]
}
So inside the listeners
map, we have a list
associated with the signal's name. Each element inside that list
, contains a map
with data about the listener including the object that is doing the listening and the function to call when the signal is triggered.
📚 In MiniScript, a
list
data type is the equivalent of an array in most languages.
Add a Listener
Let's code the function to add a listener:
EventManager.addListener = function(eventType, listener, callback)
if not self.listeners.hasIndex(eventType) then
self.listeners[eventType] = []
end if
if not self.findListener(eventType, listener, callback) then
l = {
"listener": listener,
"callback": callback,
}
self.listeners[eventType].push l
end if
end function
EventManager.findListener = function(eventType, listener, callback)
if self.listeners.hasIndex(eventType) and self.listeners[eventType].len > 0 then
// Loop through each Listener and find the one with the Callback
place = null
for i in range(self.listeners[eventType].len-1)
l = self.listeners[eventType][i]
if l.listener == listener and l.callback == callback then
place = i
break
end if
end for
return place
end if
return null
end function
The function addListener
ensures a list
exists for the signal name and that the listener object and its callback exist only once in that signal's list
. If an object tries to register the same callback more than once for a signal, it's ignored.
The findListener
function is included here because it helps to do some heavily lifting that will be required again later.
Triggering a Signal / Event
Once a listener has been added, the EventManager will be able to process signals. Signals can be triggered to run either immediately such as in the following code:
EventManager.triggerEvent = function(eventType, data=null)
if self.listeners.hasIndex(eventType) then
for l in self.listeners[eventType]
listener = l.listener
callback = l.callback
if data == null then
listener[callback]
else
listener[callback] data
end if
end for
end if
end function
Or they can be added to a queue where they can be processed at the end of the game loop.
In case a signal doesn't have any listeners, then that signal is simply ignored.
Queuing a Signal / Event
To delay triggering a signal to the end of a frame, we can add the signal to a queue and then process that queue as either the first or last thing we do in our game loop. If it is the first thing we do, more than likely any signals queued during your game loop will have to wait until the next loop / frame to process them. This is why I tend to process my event queue at the end of the game loop so that all queued signals are processed in the same loop that they were queued.
Let's take a look at how to enqueue a signal and process the queue:
EventManager.queueEvent = function(eventType, data=null)
self.queue.push [
eventType,
data,
]
end function
EventManager.processQueue = function
if self.queue and self.queue.len > 0 then
for event in self.queue
self.triggerEvent event[0], event[1]
end for
self.queue = []
end if
end function
Each time processQueue
is called, all signals in the queue list
will be triggered and then the queue is emptied.
Remove a Listener
There are times that you will want to remove a listener from a signal's list. The removeListener
function will do this for you. Most of the heavy lifting is already done by the findListener
function making the removal fairly straightforward.
EventManager.removeListener = function(eventType, listener, callback)
index = self.findListener(eventType, listener, callback)
if index != null then
self.listeners[eventType].remove index
end if
end function
📚 Just a heads up, MiniScript handles
null
and0
values as false. This is why I compareindex != null
because if I just checked if index was considered atrue
/false
value,0
would be treated as afalse
and thus when a listener is stored in the 0th element of the list, it wouldn't get removed.
The Complete Code
Here is the complete code for the event library that I'm using in my game.
import "importUtil"
ensureImport "listUtil"
EventManager = {
"listeners": {},
"queue": [],
}
EventManager.findListener = function(eventType, listener, callback)
if self.listeners.hasIndex(eventType) and self.listeners[eventType].len > 0 then
// Loop through each Listener and find the one with the Callback
place = null
for i in range(self.listeners[eventType].len-1)
l = self.listeners[eventType][i]
if l.listener == listener and l.callback == callback then
place = i
break
end if
end for
return place
end if
return null
end function
EventManager.addListener = function(eventType, listener, callback)
if not self.listeners.hasIndex(eventType) then
self.listeners[eventType] = []
end if
if not self.findListener(eventType, listener, callback) then
l = {
"listener": listener,
"callback": callback,
}
self.listeners[eventType].push l
end if
end function
EventManager.removeListener = function(eventType, listener, callback)
index = self.findListener(eventType, listener, callback)
if index != null then
self.listeners[eventType].remove index
end if
end function
EventManager.triggerEvent = function(eventType, data=null)
if self.listeners.hasIndex(eventType) then
for l in self.listeners[eventType]
listener = l.listener
callback = l.callback
if data == null then
listener[callback]
else
listener[callback] data
end if
end for
end if
end function
EventManager.queueEvent = function(eventType, data=null)
self.queue.push [
eventType,
data,
]
end function
EventManager.processQueue = function
if self.queue and self.queue.len > 0 then
for event in self.queue
self.triggerEvent event[0], event[1]
end for
self.queue = []
end if
end function
What's Next?
My game also has the need to delay triggering events longer than one frame and even independent of the frame count. I'll show you how I improved this library to add that feature in my next CO9T DevLog.
Logs: ⬅️