I guess it’s that time of the decade for one of my GM tutorials. In this one, we’ll be implementing a little signal system I’ve been using a lot in my recent projects. Obviously, this tutorial is aimed at GameMaker users, but it should be at least a little useful for anyone.
“What is a signal?” I hear you ask. Fear not, dear reader, I will explain as best I can.
A signal, otherwise known as the Publisher/Subscriber pattern, is a very simple, yet powerful, way to create a reaction to an action. It allows you to broadcast a message across your game, at a moment in time, and have everything that is interested in that message react to it.
Why would you want to do this? Let’s use a concrete example to demonstrate. Let’s say that you have a player character in an RPG. That player character can equip different weapons and armour. You also have a special fountain that gives all swords in your players inventory a buff when interacted with. You could have the fountain try to access the players inventory and search through it for all the swords and apply the buff and blah, blah, blah.
That would work, but it creates a little bit of an uncomfortable coupling between the fountain and the players inventory. Do you really want the fountain to need to interact with the inventory? What if you decide to change the format of the inventory after coding the fountain? You’d have to go back to the fountain code and update it, and this could cause unexpected bugs and so on. It requires the programmer to be aware that “If I change the way swords or the inventory work in the game at any point, I also have to go to this fountain in this random level and change the way it applies the buff.” This is no bueno. It’s just making the game needlessly hard to maintain.
It’d be way cooler if you could have the fountain telegraph it’s effect while being inventory agnostic, and the swords in the inventory could pick up on the fountains telegraphing and react to it directly within their code. That’s what signalling is. The fountain doesn’t care about the swords. The swords don’t care about the fountain. All that is cared about is a message that gets broadcast and received.
A signal, in it’s simplest form, requires:
- A broadcaster, which is the thing that sends out the signal.
- Any number of subscribers (even zero), which are the things that are interested in the signal (they are not interested in the broadcaster, just the signal)
- And the actions that the subscribers take after receiving the broadcast of a signal.
Ok, let’s start looking at code.
One Controller to Control Them All.
function SignalController() constructor {
static __listeners = {};
static __add_listener = function(_id, _signal, _callback) {
if (!struct_exists(__listeners, _signal)) {
__listeners[$ _signal] = [];
}
var _listeners = __listeners[$ _signal];
for (var i = 0; i < array_length(_listeners); i += 2) {
if (_listeners[i] == _id) {
return signal_returns.LST_ALREADY_EXISTS;
}
}
array_push(_listeners, _id, _callback);
return signal_returns.LST_ADDED;
}
static __remove_listener_from_signal = function(_id, _signal) {
if (!struct_exists(__listeners, _signal)) {
return signal_returns.SGL_DOES_NOT_EXIST;
}
var _listeners = __listeners[$ _signal];
var _found = false;
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
if (_listeners[i] == _id) {
array_delete(_listeners, i, 2);
_found = true;
break;
}
}
if (!_found) {
return signal_returns.LST_DOES_NOT_EXIST_IN_SIGNAL;
}
return signal_returns.LST_REMOVED_FROM_SIGNAL;
}
static __remove_listener = function(_id) {
var _names = struct_get_names(__listeners);
var _found = false;
for (var i = 0; i < array_length(_names); i++) {
var _listeners = __listeners[$ _names[i]];
for (var j = array_length(_listeners) - 1; j >= 0; j--) {
if (_listeners[j] == _id) {
array_delete(_listeners, j, 2);
_found = true;
break;
}
}
}
if (!_found) {
return signal_returns.LST_DOES_NOT_EXIST;
}
return signal_returns.LST_REMOVED_COMPLETELY;
}
static __signal_send = function(_signal, _signal_data) {
if (!struct_exists(__listeners, _signal)) {
return signal_returns.SGL_NOT_SENT_NO_SGL;
}
var _listeners = __listeners[$ _signal];
if (array_length(_listeners) <= 0) {
return signal_returns.SGL_NOT_SENT_NO_LST;
}
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
var _id = _listeners[i];
with (_id) {
_listeners[i + 1](_signal_data);
}
}
return signal_returns.SGL_SENT;
}
}
Ah shit, it seems complicated! Well, like all programming problems, let’s break it down into byte-sized (heh) pieces.
Lets dive into this big constructor1 function SignalController(). It has a bunch of static2 methods3 and a single static struct (__listeners). Let’s examine the purpose of __listeners first. The idea is that we will use strings for the signals, so a signal might be “attack completed”. Each signal will be added to the __listeners struct as a key pointing to an array, and each “listener” (anything interested in acting when that specific string is broadcast) will be added to the array stored in that key.
The first static method is __add_listener():
static __add_listener = function(_id, _signal, _callback) {
if (!struct_exists(__listeners, _signal)) {
__listeners[$ _signal] = [];
}
var _listeners = __listeners[$ _signal];
for (var i = 0; i < array_length(_listeners); i += 2) {
if (_listeners[i] == _id) {
return signal_returns.LST_ALREADY_EXISTS;
}
}
array_push(_listeners, _id, _callback);
return signal_returns.LST_ADDED;
}
The name kinda says it all. It’s used to add a listener for a signal. We have three arguments for the function:
_idis the id of the thing that is listening for the signal. This can be either a struct or an instance (in the case of a struct, the_idargument would beselfif added from the scope of the struct itself, and in the case of an instance, the_idargument would beidfrom the scope of the instance)._signalis the string that the listener is interested in. I picked strings because I find them to be the easiest to quickly iterate upon on the fly, as you don’t have to create a new enum for each signal or anything like that. There’s not much more to say here, it’s a simple string._callbackis the action that is taken upon receiving the signal. This is always going to be a function and it will execute from the scope of_idwhen triggered.
So, first we check to see if the _signal string already exists as a variable in __listeners. If it doesn’t, we want to add it to __listeners, and we want to add it as an empty array because, remember, we’re going to be adding each listener to the array stored in the signal variable in the __listeners struct (isn’t coding just a bunch of gobbledegook words strung together sometimes?).
Then we create a new local variable _listeners that points to that specific signal array for convenience, and we iterate through the array to see if the listener has already been added.
You might notice that we are iterating through the array two entries at a time (i += 2). Why is that? Well, the reason is that when we store the listener, we store the callback related to that listener immediately afterwards, so the array will contain listener1, callback1, listener2, callback2, listener3, callback3 and so on. Each listener is actually stored every second position (starting at 0), so that’s why we iterate by 2, rather than by 1 (we do this so we don’t have to store arrays in arrays and we’ll squeeze a little speed out of reading/writing).
If we find the listener already added, we’ll return an “error value” (a previously created enum, which we’ll get to later). This isn’t strictly necessary, but it helps with debugging problems, so I’m including it.
If, after the iteration through the array, we have found that the listener does not exist in the array, then we push the listener and the associated callback function to the array one after the other and we return a little success enum value.
Ok, sweet. We can get instances and structs listening for some signals, and we can designate actions (the callbacks) for them to take when they are notified a signal has been broadcast. But wait a minute, we aren’t even able to broadcast a signal, so all this is useless so far…
Let’s remedy that.
static __signal_send = function(_signal, _signal_data) {
if (!struct_exists(__listeners, _signal)) {
return signal_returns.SGL_NOT_SENT_NO_SGL;
}
var _listeners = __listeners[$ _signal];
if (array_length(_listeners) <= 0) {
return signal_returns.SGL_NOT_SENT_NO_LST;
}
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
var _id = _listeners[i];
with (_id) {
_listeners[i + 1](_signal_data);
}
}
return signal_returns.SGL_SENT;
}
Ah, here we go, sweet, sweet broadcasting. This is where the real action happens. We have two arguments:
_signalis the string we want to broadcast out. This is what the listeners are checking for and will execute their callback function when they receive._signal_datais whatever you want it to be. It’s a data packet of any type that you can attach to a signal being broadcast, and when a listeners gets this signal, this data packet will be included as an argument for their callback function. An example usage might be in a card game, a signal is sent out whenever a new card is played. You might have some “trap cards” listening out for the “new card played” signal. You want the traps to activate, but only when the played card is a spell. In this scenario, you would include the card being played as the_signal_dataargument. Then in your trap cards callback function, you can read the argument that is automatically included for the function and check what type the card is and act accordingly.
Ok, firstly, once again, we check to see if the _signal strings exists as a variable in the __listeners struct. If it doesn’t, we know that nothing is listening for the signal (there are no “subscribers” to that signal) and we don’t need to broadcast anything.
If there are listeners, we again grab the array associated with that signal from __listeners and store it in the local variable _listeners. We then check to see if the _listeners array is greater than 0, to make sure we have some listeners added. This is because we can add or remove listeners, so sometimes we have an existing signal variable holding an array in __listeners, but all the listeners in that array have been removed (technically, we could delete the signal variable from the array when removing listeners if there are no listeners left, but I didn’t do that, so here we are).
After that, we iterate through the _listeners array, but from the end to the start, rather than from the start to the end (and again, iterating by 2 instead of 1). I did it this way specifically so that a listener could perform the action of removing itself from listening for that signal in its callback. If we iterated through the array from start to end, then we would get errors if a listener removed itself as an action (for comp-sci reasons that are easily googleable).
For each listener, we grab the id of the listener, run a with() statement to both alter scope to that listener and guarantee its existence, and then we run the callback function for that listener, providing the _signal_data as the argument for the callback. The code there might seem a little confusing to beginners, so I’ll try to break it down line by line.
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
As I said previously, we are looping backwards. Since we a storing both listener and callback for that listener one after the other, we have to iterate by 2, and this means that we need to start at the end of the array minus 1. Arrays start their counting at position 0. Which means that an array with 1 entry will have an array_length() of 1, but that entry is at position 0 in the array. So if we add two entries to an array, and we want to access the first of the two positions from the end of the array, we will run array_length(array), which will give us the number 2, and then we will have to subtract 2 to get to position 0. All of this is a longwinded explanation as to why we have a minus 2 in i = array_length(_listeners) - 2. After we understand that, it should be fairly obvious that we then iterate backwards, 2 at a time, until we have hit less than 0 (or the first entry in the array) at which point we no longer want to iterate through the array.
As each loop goes through, we know that i is pointing to the id of listener, and i + 1 is pointing at the callback function that listener wants to execute. So we get the id with var _id = _listeners[i];. We then set the scope to the id with the with() statement and we get the callback function using _listeners[i + 1];. Since we know it’s a function (unless you’re a fool of a Took and start randomly adding invalid stuff to the listeners array), we can directly run the function using a bit of chaining like this _listeners[i + 1](); and since we want to supply whatever data we have supplied as the _signal_data argument, we want to stick that in the brackets, with the final form of the line ending up as _listeners[i + 1](_signal_data);.
After the loop has run the callbacks for all listeners, we finally return an “all good” enum value (again, not strictly necessary, but it’s nice to be able to check for confirmation of stuff when you run these methods).
Overall, that’s literally all we need for signals. We can now have instances and structs subscribe to a signal, and we can have anything broadcast a signal, and the two will interact appropriately. However, it would be nice to be able to tidy up the listener arrays and even the signal variables if needed, so we don’t just keep adding more and more things to be checked over time (which can end up being a memory leak in reality).
Cleaning Up The Streets.
So let’s go over the last few methods quickly.
static __remove_listener_from_signal = function(_id, _signal) {
if (!struct_exists(__listeners, _signal)) {
return signal_returns.SGL_DOES_NOT_EXIST;
}
var _listeners = __listeners[$ _signal];
var _found = false;
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
if (_listeners[i] == _id) {
array_delete(_listeners, i, 2);
_found = true;
break;
}
}
if (!_found) {
return signal_returns.LST_DOES_NOT_EXIST_IN_SIGNAL;
}
return signal_returns.LST_REMOVED_FROM_SIGNAL;
}
This method is used to remove a listener from a specific signal, and it does much the same as the others we’ve gone over. To begin with, we check to see if the _signal exists in __listeners, and if it does, we store the array reference in the _listeners variable and we loop backwards through it (since we are wanting to delete entries). If the supplied _id argument matches one of the ids stored in the _listeners array, we’ll delete both it and the corresponding callback associated with it using 2 as the number argument for array_delete() (meaning we want to delete 2 positions from the array) and then we’ll break out of the loop (we don’t need to keep checking, since we know we only add listeners to a signal if they haven’t already been added).
We also have the local variable _found. This lets us return different values depending on whether we found the associated id and deleted it, or if it wasn’t found. Again, just a sanity check and not totally necessary but good to have.
static __remove_listener = function(_id) {
var _names = struct_get_names(__listeners);
var _found = false;
for (var i = 0; i < array_length(_names); i++) {
var _listeners = __listeners[$ _names[i]];
for (var j = array_length(_listeners) - 1; j >= 0; j--) {
if (_listeners[j] == _id) {
array_delete(_listeners, j, 2);
_found = true;
}
}
}
if (!_found) {
return signal_returns.LST_DOES_NOT_EXIST;
}
return signal_returns.LST_REMOVED_COMPLETELY;
}
This method is a little bit heftier than the others, since it will search through all the arrays associated with all the signals and check to see if there is a reference to the provided _id in any of them. This is your “Clean up” method. It gets rid of a listener from everything it has subscribed itself to. It’s essentially the same as the __remove_listener_from_signal method, except that it looks through all signals, instead of just one. As you can see, we get the names of all the signal variables that have been added to __listeners using the struct_get_names() function, and then loop through each one, then running a secondary loop through the array stored in each one.
And that’s it. Simple Signals are implemented. Just instantiate the constructor and you’re good. Except of course, we haven’t talked about the enum references scattered throughout. And helper functions which make things a little less verbose to subscribe to, remove from and send signals. Plus, I think it’ll be helpful to include a real world example of how I’m using signals in my Dice Trek game. So let’s go over that.
A Little Help From My Friends.
Ok, signals are done, but we can make it a little easier to use, I’m sure. Let’s go over the total setup I actually have in my games. All of this is in a single script.
First I just setup a macro for a global variable that will hold my instantiated SignalController() struct (the global variable is called global.__signals), since SIGNALS is easier to type than global.__signals.
#macro SIGNALS global.__signals
Then I instantiate a new struct from the big SignalController() constructor we’ve spent previous our time covering into that global variable. We only need one of these. We don’t want to create a new SignalController() for every signal or anything like that, so since it’s a singleton, we can just assign it to a global variable.
global.__signals = new SignalController();
And now we have our enums.
enum signal_returns {
LST_ADDED,
LST_ALREADY_EXISTS,
LST_REMOVED_FROM_SIGNAL,
LST_REMOVED_COMPLETELY,
LST_NOT_SUBSCRIBED_TO_SIGNAL,
LST_DOES_NOT_EXIST_IN_SIGNAL,
LST_DOES_NOT_EXIST,
SGL_DOES_NOT_EXIST,
SGL_NOT_SENT_NO_SGL,
SGL_NOT_SENT_NO_LST,
SGL_SENT
}
Enums are enums are enums. They are just a way of turning numbers into words so they make more sense. That’s the list, and it lets us do some things like this:
var _removed = SignalUnsubscribe(id, "player damaged");
if (_removed == signal_returns.SGL_DOES_NOT_EXIST) {
show_debug_message("Oops, player damaged has not been subscribed to as a signal");
}
else if (_removed == signal_returns.LST_REMOVED_FROM_SIGNAL) {
show_debug_message("I have been successfully removed as a listener from the player damaged signal");
}
Since the signal functions return the enums, we can catch them and check them to see if signal actions were successful or not.
The helper functions are purely just there to expedite the coding process. They don’t do anything but shorten things really. I like my code to be self-documenting and easily parsable, so I often use long variable names for things, and then add shortened helper functions that might be quicker to type or perform multiple actions if necessary (of course, this comes with a performance cost and should be considered within the totality of your project, but most games are easily able to bear the burden). So here’s the helper functions:
function SignalSubscribe(_id, _signal, _callback) {
return SIGNALS.__add_listener(_id, _signal, _callback);
}
function SignalUnsubscribe(_id, _signal) {
return SIGNALS.__remove_listener_from_signal(_id, _signal);
}
function SignalRemove(_id) {
return SIGNALS.__remove_listener(_id);
}
function SignalSend(_signal, _signal_data = undefined) {
return SIGNALS.__signal_send(_signal, _signal_data);
}
I don’t even know if they need much explanation. SignalSubscribe() subscribes a listener to perform the callback when a specific signal is broadcast. SignalUnsubscribe() removes a listener from a specific signal. SignalRemove() removes a listener from all signals and SignalSend() broadcasts a signal with any associated data you might want to be sent along with it.
The overall total codebase in the script would look like this
#macro SIGNALS global.__signals
global.__signals = new SignalController();
enum signal_returns {
LST_ADDED,
LST_ALREADY_EXISTS,
LST_REMOVED_FROM_SIGNAL,
LST_REMOVED_COMPLETELY,
LST_NOT_SUBSCRIBED_TO_SIGNAL,
LST_DOES_NOT_EXIST_IN_SIGNAL,
LST_DOES_NOT_EXIST,
SGL_DOES_NOT_EXIST,
SGL_NOT_SENT_NO_SGL,
SGL_NOT_SENT_NO_LST,
SGL_SENT
}
function SignalSubscribe(_id, _signal, _callback) {
return SIGNALS.__add_listener(_id, _signal, _callback);
}
function SignalUnsubscribe(_id, _signal) {
return SIGNALS.__remove_listener_from_signal(_id, _signal);
}
function SignalRemove(_id) {
return SIGNALS.__remove_listener(_id);
}
function SignalSend(_signal, _signal_data = undefined) {
return SIGNALS.__signal_send(_signal, _signal_data);
}
function SignalController() constructor {
static __listeners = {};
static __add_listener = function(_id, _signal, _callback) {
if (!struct_exists(__listeners, _signal)) {
__listeners[$ _signal] = [];
}
var _listeners = __listeners[$ _signal];
for (var i = 0; i < array_length(_listeners); i += 2) {
if (_listeners[i] == _id) {
return signal_returns.LST_ALREADY_EXISTS;
}
}
array_push(_listeners, _id, _callback);
return signal_returns.LST_ADDED;
}
static __remove_listener_from_signal = function(_id, _signal) {
if (!struct_exists(__listeners, _signal)) {
return signal_returns.SGL_DOES_NOT_EXIST;
}
var _listeners = __listeners[$ _signal];
var _found = false;
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
if (_listeners[i] == _id) {
array_delete(_listeners, i, 2);
_found = true;
break;
}
}
if (!_found) {
return signal_returns.LST_DOES_NOT_EXIST_IN_SIGNAL;
}
return signal_returns.LST_REMOVED_FROM_SIGNAL;
}
static __remove_listener = function(_id) {
var _names = struct_get_names(__listeners);
var _found = false;
for (var i = 0; i < array_length(_names); i++) {
var _listeners = __listeners[$ _names[i]];
for (var j = array_length(_listeners) - 1; j >= 0; j--) {
if (_listeners[j] == _id) {
array_delete(_listeners, j, 2);
_found = true;
break;
}
}
}
if (!_found) {
return signal_returns.LST_DOES_NOT_EXIST;
}
return signal_returns.LST_REMOVED_COMPLETELY;
}
static __signal_send = function(_signal, _signal_data) {
if (!struct_exists(__listeners, _signal)) {
return signal_returns.SGL_NOT_SENT_NO_SGL;
}
var _listeners = __listeners[$ _signal];
if (array_length(_listeners) <= 0) {
return signal_returns.SGL_NOT_SENT_NO_LST;
}
for (var i = array_length(_listeners) - 2; i >= 0; i -= 2) {
var _id = _listeners[i];
with (_id) {
_listeners[i + 1](_signal_data);
}
}
return signal_returns.SGL_SENT;
}
}
We have our ease of use functions and a controller struct that handles the internal details, which are adding and removing listeners and related callbacks both from individual signals and globally, as well as being able to broadcast signals. Everything we could need for a nice little signal system.
Finally, let’s look at some real world examples of signal usage.
In The Trenches.
In Dice Trek, turns are started by rolling your dice. There’s a bit of a rolling animation, some things are calculated, and once everything is ready, I broadcast a “roll finished” signal when my die have finished rolling very simply like this:
SignalSend("roll finished");
Very simple. That sends out the “roll finished” signal and anything subscribed to it will perform the callback they have associated with that signal. There are a number of things that are subscribed to “roll finished”. Firstly, the player instance is subscribed to it, with this code:
SignalSubscribe(id, "roll finished", function() {
for (var i = 0; i < Stats.NUM; i++) {
if (i == Stats.WEAPONS || i == Stats.CREW_MEMBERS) {
continue;
}
if (ship.GetStrengthCurrent(i) > ship.GetStrengthMax(i)) {
ship.SetStrength(i, ship.GetStrengthMax(i));
}
}
dice_roll_timer = dice_roll_timer_max;
can_roll = true;
reflect_damage = false;
if (!is_undefined(special)) {
special.Turn();
}
});
Essentially, once a roll has finished, my player makes sure that if a stats current value is over its max value, it gets reset back to its max value. It also resets the time that the dice roll animation will take (dice_roll_timer = dice_roll_timer_max), it re-enables the ability to roll the dice again (can_roll = true, which is set to false while the dice is rolling), it disables a specific effect that can happen on a turn (reflect_damage = true) and it runs the Turn method for a special if it has a special equipped.
Then the enemy ship is also subscribed to the “roll finished” signal, with this code:
SignalSubscribe(id, "roll finished", function() {
if (just_fired) {
SignalSend("enemy damage");
var _drops = targeting_drops.Drop();
current_target = _drops[0];
if (current_target.value != -1) {
SignalSend("system targeted", current_target.value);
}
just_fired = false;
}
EnemyFireInfoUpdate();
});
Here, when the enemy receives the signal that the roll has finished, it checks to see if it has fired (they won’t fire every turn, and I only want some actions to happen on the turn they fire). If they have, they send out a signal of their own (the “enemy damage”), then they grab a drop from their targeting lootbox, to see which system they want to target from the players ships, and if they end up targeting a system instead of the player themselves (the players target value is -1), they then send another signal, the “system targeted” signal (an on-screen targeting reticle is subscribed to this and will move to target the appropriate system) and it will set just_fired to false ready for the next time it fires.
Finally, I have a bunch of different specials that effect your dice rolls, and they can be subscribed to this signal, so that they can alter the final outcome of the dice roll after the die have finished rolling. One example is a special that guarantees all your Crew Member die will end up matching with the die rolls of your Weapons system:
SignalSubscribe(self, "roll finished", function() {
SignalUnsubscribe(self, "roll finished");
with (obj_ship) {
var _already_placed = [];
for (var i = 0; i < array_length(ship.GetDiceArray(Stats.CREW_MEMBERS)); i++) {
if (i >= array_length(ship.GetDiceArray(Stats.WEAPONS))) {
break;
}
var _pos = i mod array_length(ship.GetDiceArray(Stats.WEAPONS));
var _target_num = ship.GetDice(Stats.CREW_MEMBERS, i).number;
if (array_contains(_already_placed, _target_num)) {
continue;
}
array_push(_already_placed, _target_num);
ship.GetDice(Stats.WEAPONS, _pos).SetNumber(_target_num);
}
}
});
Since specials are only activated when you decide to activate them (unless they are passive), they aren’t constantly subscribed. Instead, I subscribe them to the signal when you press the special activate button (which runs the above code), and they get subscribed to the “roll finished” signal, and then when they receive that signal, they unsubscribe from it, so the next time you roll your die, they won’t perform their signal action (since they are unsubscribed), until you can activate them and they get subscribed again.
So the first thing they do is unsubscribe themselves and then I just do a loop through all the Crew Member die and a secondary loop through all the Weapons system die (modding the incrementor in that second loop so if there’s more crew die than weapons die, it won’t throw an error), and making sure the numbers for each die match.
That’s three different types of actions being performed from a single SignalSend() initiated by the player, and neither the player, nor the instances involved in the actions necessarily need to talk to each other. It’s a very useful system to have setup, and it’s something that I import into almost all my projects when I first start them.
So Long, and Thanks for All the Fish.
Wow, this turned out to be a pretty big one. Hopefully you’ve got at least a little bit of an idea how signals work. Spend some time experimenting and playing around with them and you’ll be sure to find many uses where they come in super handy.
If this tutorial was helpful, then consider dropping me a wishlist on Alchementalist (a spellcrafting, dungeon crawling roguelite), Doggy Dungeons (an adventurous card game where you play as a pet trying to find their way back to their owner) or Dice Trek (an FTL-inspired roguelite where you explore the galaxy, manage ship systems and battle enemies with dice rolls), whichever floats your boat (yes, I am making 3 games at once and yes I am crazier than a cut snake for doing so). Until next time my friends!
