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

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