-
-
Save FaultyFunctions/cb74284faf33b4b4ca3a02cdf14ce409 to your computer and use it in GitHub Desktop.
// 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, {}); | |
} |
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!
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 ❤️
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!
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 👍
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.