Last active
August 1, 2023 13:24
-
-
Save FaultyFunctions/cb74284faf33b4b4ca3a02cdf14ce409 to your computer and use it in GitHub Desktop.
A simple Event Bus for GameMaker Studio 2.3+
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, {}); | |
} |
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
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 whatweak_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!