Skip to content

Instantly share code, notes, and snippets.

@FaultyFunctions
Last active August 1, 2023 13:24
A simple Event Bus for GameMaker Studio 2.3+
global.EventBus = {
error_messages: true,
verbose: false,
prune_threshold: 200,
prune_count: 0,
__events: {},
/// @func addListener(instance, event, callback);
addListener: function(instance, event, callback) {
// ENSURE EVENT IS A STRING
if (typeof(event) != "string") {
if (error_messages) { show_debug_message("Attempting to listen for event that is not a string!"); }
return;
}
// ENSURE WE HAVE A CALLBACK FUNCTION
if (callback == undefined or typeof(callback) != "method") {
if (error_messages) { show_debug_message("No callback defined, or callback isn't a method."); }
return;
}
// ENSURE EVENT EXISTS ON GLOBAL EVENTS STRUCT
if (!variable_struct_exists(__events, event)) {
variable_struct_set(__events, event, []);
}
// CREATE WEAK REF
var _inst_ref = undefined;
with (instance) {
_inst_ref = weak_ref_create(self);
}
// ADD LISTENER TO EVENTS STRUCT
array_push(__events[$ event], {
reference: _inst_ref,
callback: method(undefined, callback),
isInstance: (instanceof(_inst_ref.ref) == "instance")
});
},
/// @func send(event, data)
send: function(event, data) {
// ENSURE EVENT IS A STRING
if (typeof(event) != "string") {
if (error_messages) { show_debug_message("Given event is not a string!"); }
return;
}
// ENSURE EVENT EXISTS ON GLOBAL EVENTS STRUCT AND HAS LISTENERS
if (!variable_struct_exists(__events, event)) {
if (error_messages and verbose) { show_debug_message("No listeners for event: " + event); }
return;
} else if (array_length(__events[$ event]) == 0) {
if (error_messages and verbose) { show_debug_message("No listeners for event: " + event); }
return;
}
// SEND EVENT AND EXECUTE ANY CALLBACKS
for (var i = 0; i < array_length(__events[$ event]); ++i) {
var _listener = __events[$ event][i];
// IF OUR WEAK REFERENCES DIED, REMOVE THE INSTANCE AND MOVE ON
if (!weak_ref_alive(_listener.reference)) {
__pruneInstance(event, i);
continue;
}
// EXECUTE CALLBACKS
if (_listener.isInstance) {
if (instance_exists(_listener.reference.ref)) {
with (_listener.reference.ref) {
_listener.callback(data);
}
} else {
__pruneInstance(event, i);
}
} else {
method(_listener.reference.ref, _listener.callback)(data);
}
}
// PRUNE CHECK
if (++prune_count >= prune_threshold) {
__pruneEventBus();
}
},
/// @func removeListener(instance, event)
removeListener: function(instance, event) {
// ENSURE EVENT IS A STRING
if (typeof(event) != "string") {
if (error_messages) { show_debug_message("Attempting to remove event that is not a string!"); }
return;
}
// ENSURE EVENT EXISTS ON GLOBAL EVENTS STRUCT AND HAS LISTENERS
if (!variable_struct_exists(__events, event)) {
if (error_messages and verbose) { show_debug_message("Event [ " + event + " ] has no listeners to remove!"); }
return;
} else if (array_length(__events[$ event]) == 0) {
if (error_messages and verbose) { show_debug_message("Event [ " + event + " ] has no listeners to remove!"); }
return;
}
// CHECK FOR AND REMOVE INSTANCE FROM EVENT
for (var i = array_length(__events[$ event]) - 1; i >= 0; --i) {
var _listener = __events[$ event][i];
if (weak_ref_alive(_listener.reference)) {
if (instance == _listener.reference.ref) {
delete __events[$ event][i]; // Delete struct from memory
array_delete(__events[$ event], i, 1); // Delete entry in array
}
} else {
__pruneInstance(event, i);
}
}
// PRUNE EVENT IF NO MORE LISTENERS
if (array_length(__events[$ event]) == 0) {
variable_struct_remove(__events, event);
}
// PRUNE CHECK
if (++prune_count >= prune_threshold) {
__pruneEventBus();
}
},
__pruneInstance: function(event, index) {
if (verbose) { show_debug_message("Instance no longer exists! Pruning..."); }
array_delete(__events[$ event], index, 1);
},
__pruneEventBus: function() {
if (verbose) { show_debug_message("Starting prune..."); }
var _prunedEvents = 0;
var _prunedListeners = 0;
var _eventList = variable_struct_get_names(__events);
for (var i = array_length(_eventList) - 1; i >= 0; --i) {
var _event = _eventList[i];
// REMOVE IF NO LISTENERS
if (array_length(__events[$ _event]) == 0) {
variable_struct_remove(__events, _event);
_prunedEvents++;
if (verbose) { show_debug_message("Pruned [ " + _event + " ] due to no listeners."); }
continue;
}
// ENTER EACH EVENT AND REMOVE DEAD REFERENCES
for (var j = array_length(__events[$ _event]) - 1; j >= 0; --j) {
var _listener = __events[$ _event][j];
if (!weak_ref_alive(_listener.reference)) {
delete _listener;
array_delete(__events[$ _event], j, 1);
_prunedListeners++;
}
}
}
if (verbose) { show_debug_message("Pruned " + string(_prunedEvents) + " events and " + string(_prunedListeners) + " listeners."); }
}
}
@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