Skip to content

Instantly share code, notes, and snippets.

@FaultyFunctions
Last active August 1, 2023 13:24
Show Gist options
  • Save FaultyFunctions/cb74284faf33b4b4ca3a02cdf14ce409 to your computer and use it in GitHub Desktop.
Save FaultyFunctions/cb74284faf33b4b4ca3a02cdf14ce409 to your computer and use it in GitHub Desktop.
A simple Event Bus for GameMaker Studio 2.3+
// CHANGE THIS IF YOU WANT TO CALL "global.EventBus" DIFFERENTLY
#macro Event global.EventBus
// ADD YOUR CHANNELS HERE IN BEFORE _SIZE
enum Channel {
GLOBAL,
_SIZE
}
// ADD YOUR GLOBAL EVENTS HERE
enum GlobalEvent {
}
// YOU CAN ALSO PASS IN STRINGS FOR EVENTS BUT IT'S BEST TO DEFINE ENUMS
// FOR YOUR EVENTS. CHANNELS SHOULD STRICTLY BE ENUMS.
global.EventBus = {
throw_errors: true, // Whether to crash the game or not if some funky stuff happens
verbose: false, // Log messages in console of what's going on
prune_threshold: 200, // Change this to have pruning run more or less often
prune_count: 0,
__channels: [],
/// @func on(event, callback, scope, [channel]);
/// @desc subscribe to an event
on: function(event, callback, scope, channel = global) {
__subscribe(event, callback, scope, channel);
},
/// @func once(event, callback, scope, [channel]);
/// @desc subscribe to an event, but remove itself the subscription after being called once
once: function(event, callback, scope, channel = global) {
var _listener = __subscribe(event, callback, scope, channel);
_listener.callOnce = true;
},
/// @func send(event, data, [channel]);
/// @desc send an event with data
send: function(event, data, channel = global) {
if (channel == global) {
channel = Channel.GLOBAL;
}
// CHECK IF CHANNEL EXISTS
if (channel >= array_length(__channels)) {
var _error = "Channel [ " + string(channel) + " ] does not exist! Be sure to add it to the Channel enum.";
if (throw_errors) { throw _error; } else { show_debug_message(_error); }
return;
}
// ENSURE EVENT EXISTS ON GLOBAL EVENTS STRUCT AND HAS LISTENERS
if (!variable_struct_exists(__channels[channel], event)) {
if (verbose) { show_debug_message("No listeners for event: " + string(event)); }
return;
} else if (array_length(__channels[channel][$ event]) == 0) {
if (verbose) { show_debug_message("No listeners for event: " + string(event)); }
return;
}
// SEND EVENT AND EXECUTE ANY CALLBACKS
for (var i = array_length(__channels[channel][$ event]) - 1; i >= 0; --i) {
var _listener = __channels[channel][$ event][i];
// IF OUR WEAK REFERENCES DIED, REMOVE THE INSTANCE AND MOVE ON
if (!weak_ref_alive(_listener.reference)) {
__pruneInstance(channel, event, i);
continue;
}
// EXECUTE CALLBACKS
if (_listener.isInstance) {
if (instance_exists(_listener.reference.ref)) {
method(_listener.reference.ref.id, _listener.callback)(data);
if (_listener.callOnce) {
remove(_listener.reference.ref.id, event, channel);
}
} else {
__pruneInstance(channel, event, i);
}
} else {
method(_listener.reference.ref, _listener.callback)(data);
if (_listener.callOnce) {
remove(_listener.reference.ref, event, channel);
}
}
}
// PRUNE CHECK
if (++prune_count >= prune_threshold) {
__pruneEventBus();
}
},
/// @func fire(event, [channel]);
/// @desc send an event without passing any data
fire: function(event, channel = global) {
send(event, undefined, channel);
},
/// @func remove(instance, event, [channel]);
/// @desc unsubscribe to an event
remove: function(instance, event, channel = global) {
if (channel == global) {
channel = Channel.GLOBAL;
}
// CHECK IF CHANNEL EXISTS
if (channel >= array_length(__channels)) {
var _error = "Channel [ " + string(channel) + " ] does not exist! Be sure to add it to the Channel enum.";
if (throw_errors) { throw _error; } else { show_debug_message(_error); }
return;
}
// ENSURE EVENT EXISTS ON GLOBAL EVENTS STRUCT AND HAS LISTENERS
if (!variable_struct_exists(__channels[channel], event)) {
if (verbose) { show_debug_message("Event [ " + string(event) + " ] does not exist to remove listener from!"); }
return;
} else if (array_length(__channels[channel][$ event]) == 0) {
if (verbose) { show_debug_message("Event [ " + string(event) + " ] has no listeners to remove listener from!"); }
return;
}
// CHECK FOR AND REMOVE INSTANCE FROM EVENT (also prunes that specific event list)
for (var i = array_length(__channels[channel][$ event]) - 1; i >= 0; --i) {
var _listener = __channels[channel][$ event][i];
if (weak_ref_alive(_listener.reference)) {
if (is_numeric(instance) and _listener.isInstance and instance == _listener.reference.ref.id) {
delete __channels[channel][$ event][i]; // Delete struct from memory
array_delete(__channels[channel][$ event], i, 1); // Delete entry in array
} else if (instance == _listener.reference.ref) {
delete __channels[channel][$ event][i]; // Delete struct from memory
array_delete(__channels[channel][$ event], i, 1); // Delete entry in array
}
} else {
__pruneInstance(channel, event, i);
}
}
// DELETE EVENT IF NO MORE LISTENERS
if (array_length(__channels[channel][$ event]) == 0) {
variable_struct_remove(__channels[channel], event);
}
// PRUNE CHECK
if (++prune_count >= prune_threshold) {
__pruneEventBus();
}
},
__subscribe: function(event, callback, scope, channel = global) {
// RECURSE TO SUBSCRIBE FOR MULTIPLE EVENTS AT THE SAME TIME
// AND YES I'M MAKING RECURSE A WORD
if (is_array(event)) {
for (var i = 0; i < array_length(event); ++i) {
on(event[i], callback, scope, channel);
}
return;
}
if (channel == global) {
channel = Channel.GLOBAL;
}
// CHECK IF CHANNEL EXISTS
if (channel >= array_length(__channels)) {
var _error = "Channel [ " + string(channel) + " ] does not exist! Be sure to add it to the Channel enum.";
if (throw_errors) { throw _error; } else { show_debug_message(_error); }
return;
}
// ENSURE WE HAVE A CALLBACK FUNCTION
if (callback == undefined or typeof(callback) != "method") {
var _error = "No callback defined, or callback isn't a function.";
if (throw_errors) { throw _error; } else { show_debug_message(_error); }
return;
}
// ENSURE EVENT ARRAY EXISTS ON CHANNEL
if (!variable_struct_exists(__channels[channel], event)) {
variable_struct_set(__channels[channel], event, []);
}
// CREATE WEAK REF
var _inst_ref = undefined;
with (scope) {
_inst_ref = weak_ref_create(self);
}
// ADD LISTENER TO EVENTS STRUCT
var _listener = {
reference: _inst_ref,
callback: method(undefined, callback),
isInstance: (instanceof(_inst_ref.ref) == "instance"),
callOnce: false
};
array_push(__channels[channel][$ event], _listener);
return _listener;
},
__pruneInstance: function(channel, event, index) {
if (verbose) { show_debug_message("Pruned listener at index [ " + string(index) + " ] from [ " + string(event) + " ] because it no longer exists."); }
array_delete(__channels[channel][$ event], index, 1);
},
__pruneEventBus: function() {
if (verbose) { show_debug_message("Starting prune..."); }
var _pruned_events = 0;
var _pruned_listeners = 0;
for (var i = array_length(__channels) - 1; i >= 0; --i) {
var _channel = __channels[i];
var _event_list = variable_struct_get_names(_channel);
for (var j = array_length(_event_list) - 1; j >= 0; --j) {
var _event = _channel[$ _event_list[j]];
for (var k = array_length(_event) - 1; k >= 0; --k) {
var _listener = _event[k];
if (!weak_ref_alive(_listener.reference)) {
__pruneInstance(i, _event_list[j], k);
delete _listener;
++_pruned_listeners;
}
}
if (array_length(_event) == 0) {
variable_struct_remove(_channel, _event_list[j]);
++_pruned_events;
if (verbose) { show_debug_message("Pruned [ " + string(_event) + " ] due to no listeners."); }
}
}
}
if (verbose) { show_debug_message("Pruned " + string(_pruned_events) + " events and " + string(_pruned_listeners) + " listeners."); }
}
}
// INIT EVENT BUS CHANNELS ARRAY
repeat(Channel._SIZE) {
array_push(global.EventBus.__channels, {});
}
@HaikuJock
Copy link

Brilliant! I like this a lot and would like to use it in a commercial game. What's the license? I sent you a DM on Twitter.

@FaultyFunctions
Copy link
Author

Brilliant! I like this a lot and would like to use it in a commercial game. What's the license? I sent you a DM on Twitter.

I already replied on Twitter but for anyone else coming across this, feel free to use this on any project for free. If you really want you can add a credit for "FaultyFunctions" but it is definitely not required!

@HaikuJock
Copy link

I've found what might be a bug, or it might be intended behaviour.

When I deactivate an instance instance_exists returns false. If an event is triggered that the deactivated instance is listening for, the listener will be pruned. For my purposes this is not desirable behaviour because I want the instance to receive events when it is re-activated. I've removed lines 77 and 78 to stop this happening.

				if (instance_exists(_listener.reference.ref)) {
					method(_listener.reference.ref.id, _listener.callback)(data);
					if (_listener.callOnce) {
						remove(_listener.reference.ref.id, event, channel);
					}
				} else { // line 77
					__pruneInstance(channel, event, i); // line 78
				}

Otherwise this is working very well for me ❤️

@FaultyFunctions
Copy link
Author

I've found what might be a bug, or it might be intended behaviour.

When I deactivate an instance instance_exists returns false. If an event is triggered that the deactivated instance is listening for, the listener will be pruned. For my purposes this is not desirable behaviour because I want the instance to receive events when it is re-activated. I've removed lines 77 and 78 to stop this happening.

				if (instance_exists(_listener.reference.ref)) {
					method(_listener.reference.ref.id, _listener.callback)(data);
					if (_listener.callOnce) {
						remove(_listener.reference.ref.id, event, channel);
					}
				} else { // line 77
					__pruneInstance(channel, event, i); // line 78
				}

Otherwise this is working very well for me ❤️

Hello! Thanks for bringing this up. I guess I didn't really consider this but this is a good thing to point out. Since I don't really use the deactivate instance functions (I use my own personal version of them), The __pruneEventBus function checks if a weak reference is still alive, I'm not 100% sure a weak ref to an instance is still alive if it's been deactivated so you removing that line might be the best solution depending on what weak_ref_alive() returns in the __pruneEventBus function. It's been a long time since I've looked at this code to be honest but thanks for bringing this up. I'll have to do some testing!

@HaikuJock
Copy link

From my tests weak_ref_alive() returns true for deactivated instances and there's already a check for this on line 65.

I'm using instance_deactivate_layer to deactivate the instances in question, and I've assumed that works the same as deactivating a single instance.

Thanks again 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment