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, {}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
From my tests
weak_ref_alive()
returnstrue
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 👍