ID:1879080
 
Hello all,
Its been awhile since the last time I looked in on BYOND, and it seems like there have been some fairly exciting recent developments.

I'm just curious if anyone has implemented an entity component framework (see http://gameprogrammingpatterns.com/component.html if you're unfamiliar with the pattern).
An entity framework can easily be created in DM, the grunt work absolutely needs to be done via an event triggering library. I have not released this guy yet, but it works pretty well:

//THIS DEFINE IS THE SHORTHAND FOR ALL OF THE EVENT CODE.
//IF YOU MAKE CHANGES TO THE EVENT DATUM, SERIALIZE YOUR CHANGES HERE INTO A SINGLE LINE
#define EVENT_HANDLER var/tmp/list/listeners;proc/AddEventListener(Event,Object){if(listeners==null){listeners=list();}var/list/l;if(!(Event in listeners)){l=list();listeners[Event]=l;}else{l=listeners[Event];}l|=Object;return 1};proc/RemoveEventListener(Event,Object){if(listeners){var/list/l=listeners[Event];if(l){.=l.Remove(Object);if(l.len==0){listeners.Remove(Event);if(listeners.len==0)listeners=null;}return .}}return 0;};proc/TriggerEvent(Event){if(listeners){var/list/l=listeners[Event];if(l){if(args.len>1){var/list/nargs = args.Copy(2,0);for(var/datum/d in l){. = call(d,Event)(arglist(nargs));if(.!=void)return .;}}else{for(var/datum/d in l){. = call(d,Event)();if(.!=void)return .;}}}}return void;}Cleanup(){listeners=null;..();}

var/datum/void = new()

//a generic interface for dealing with event handlers
//do not initialize these. Only use them as casting interfaces.
event_handler
var
tmp
list/listeners

proc
//add an event listener by event string and object reference
//Event is a string describing the event
//Object is an object reference describing the listener
AddEventListener(Event,Object)
//if the listeners list is null, create it
if(listeners==null)
listeners = list()
//if the event sublist is null, create is
var/list/l
if(!(Event in listeners))
l = list()
listeners[Event] = l
else
l = listeners[Event]
//only add the object to the list if it's not already present
l |= Object
//return successful
return 1

//remove an event listener by event string and object reference
//Event is a string describing the event
//Object is an object reference describing the listener
RemoveEventListener(Event,Object)
//if listeners isn't null
if(listeners)
//if the event sublist isn't null
if(Event in listeners)
var/list/l = listeners[Event]
//attempt to remove the object and store the result of the removal
. = l.Remove(Object)
//if the event sublist is now empty, remove it
if(l.len==0)
listeners.Remove(Event)
//if there are no longer any sublists in the listeners list, null it out
if(listeners.len==0)
listeners = null
//return the result of the removal
return .
return 0

//trigger an event by name
//this will loop through all listeners subscribed to the event and call their event function
TriggerEvent(Event)
//if listeners isn't null
if(listeners)
//if the event sublist isn't null
if(Event in listeners)
var/list/l = listeners[Event]
//if there are additional arguments
if(args.len>1)
//copy the remaining arguments into a list
var/list/nargs = args.Copy(2,0)
//loop through the listeners in the event sublist and call their event function with the supplied arguments
for(var/datum/d in l)
. = call(d,Event)(arglist(nargs))
//if the last object returned anything other than void, stop iterating and return the value
if(.!=void)
return .
else
//loop through the listeners in the event sublist and call their event function with no arguments
for(var/datum/d in l)
. = call(d,Event)()
//if the last object returned anything other than void, stop iterating and return the value
if(.!=void)
return .
//if we made it through all of the listeners, return void
return void

Cleanup()
listeners = null
..()


And here's an example of how to use it:

#define EVENT_ONENTER       1
#define EVENT_ONEXIT 2
#define EVENT_ONENTERED 4
#define EVENT_ONEXITED 8
#define EVENT_ONCROSS 16
#define EVENT_ONUNCROSS 32
#define EVENT_CROSS 64
#define EVENT_UNCROSS 128
#define EVENT_ONCROSSED 256
#define EVENT_ONUNCROSSED 512
#define EVENT_CROSSED 1024
#define EVENT_UNCROSSED 2048
#define EVENT_MOVE 4096
#define EVENT_MOVED 8192
#define EVENT_BUMP 16384
#define EVENT_BUMPED 32768

#define EVENT_OVERLAP (EVENT_CROSS|EVENT_UNCROSS|EVENT_ONCROSS|EVENT_ONUNCROSS)
#define EVENT_OVERLAPPED (EVENT_CROSSED|EVENT_UNCROSSED|EVENT_ONCROSSED|EVENT_ONUNCROSSED)
#define EVENT_TURFCHANGE (EVENT_ONENTER|EVENT_ONEXIT)
#define EVENT_TURFCHANGED (EVENT_ONENTERED|EVENT_ONEXITED)
#define EVENT_COLLISION (EVENT_BUMP|EVENT_BUMPED)

trigger
parent_type = /atom/movable
EVENT_HANDLER

var/tmp
event_flags = 0

Cross(atom/movable/o)
. = TriggerEvent("eventCross",src,o)
if(.==void)
. = ..()

Uncross(atom/movable/o)
. = TriggerEvent("eventUncross",src,o)
if(.==void)
. = ..()

onCross(atom/movable/o)
. = TriggerEvent("eventOnCross",src,o)
if(.==void)
. = ..()

onUncross(atom/movable/o)
. = TriggerEvent("eventOnUncross",src,o)
if(.==void)
. = ..()

Crossed(atom/movable/o)
. = TriggerEvent("eventCrossed",src,o)
if(.==void)
. = ..()

Uncrossed(atom/movable/o)
. = TriggerEvent("eventUncrossed",src,o)
if(.==void)
. = ..()

onCrossed(atom/movable/o)
. = TriggerEvent("eventOnCrossed",src,o)
if(.==void)
. = ..()

onUncrossed(atom/movable/o)
. = TriggerEvent("eventOnUncrossed",src,o)
if(.==void)
. = ..()

onEnter(atom/o,atom/oldloc)
. = TriggerEvent("eventOnEnter",src,o,oldloc)
if(.==void)
. = ..()

onExit(atom/o,atom/newloc)
. = TriggerEvent("eventOnExit",src,o,newloc)
if(.==void)
. = ..()

onEntered(atom/o,atom/oldloc)
. = TriggerEvent("eventOnEntered",src,o,oldloc)
if(.==void)
. = ..()

onExited(atom/o,atom/newloc)
. = TriggerEvent("eventOnExited",src,o,newloc)
if(.==void)
. = ..()

Move(atom/NewLoc,Dir=0,step_x=0,step_y=0)
. = TriggerEvent("eventMove",src,NewLoc,Dir,step_x,step_y)
if(.==void)
. = ..()

Moved(atom/OldLoc,oDir,oSx,oSy)
. = TriggerEvent("eventMoved",src,OldLoc,oDir,oSx,oSy)
if(.==void)
. = ..()

Bump(atom/Obstacle)
. = TriggerEvent("eventBump",src,Obstacle)
if(.==void)
. = ..()

Bumped(atom/movable/Bumper)
. = TriggerEvent("eventBumped",src,Bumper)
if(.==void)
. = ..()

New(loc,Flags=null,Listener=null,Properties=null)
for(var/v in Properties)
vars[v] = Properties[v]

if(bound_x)
pixel_x += bound_x
if(bound_y)
pixel_y += bound_y

if(Listener)
event_flags = Flags
if(Flags&EVENT_ONENTER)
AddEventListener("eventOnEnter",Listener)
if(Flags&EVENT_ONEXIT)
AddEventListener("eventOnExit",Listener)
if(Flags&EVENT_ONENTERED)
AddEventListener("eventOnEntered",Listener)
if(Flags&EVENT_ONEXITED)
AddEventListener("eventOnExited",Listener)
if(Flags&EVENT_ONCROSS)
AddEventListener("eventOnCross",Listener)
if(Flags&EVENT_ONUNCROSS)
AddEventListener("eventOnUncross",Listener)
if(Flags&EVENT_ONCROSSED)
AddEventListener("eventOnCrossed",Listener)
if(Flags&EVENT_ONUNCROSSED)
AddEventListener("eventOnUncrossed",Listener)
if(Flags&EVENT_CROSS)
AddEventListener("eventCross",Listener)
if(Flags&EVENT_UNCROSS)
AddEventListener("eventUncross",Listener)
if(Flags&EVENT_CROSSED)
AddEventListener("eventCrossed",Listener)
if(Flags&EVENT_UNCROSSED)
AddEventListener("eventUncrossed",Listener)
if(Flags&EVENT_MOVE)
AddEventListener("eventMove",Listener)
if(Flags&EVENT_MOVED)
AddEventListener("eventMoved",Listener)
if(Flags&EVENT_BUMP)
AddEventListener("eventBump",Listener)
if(Flags&EVENT_BUMPED)
AddEventListener("eventBumped",Listener)
..()


The flag business could be left out if you'd like.

Simply define a proc named "event[name]" on whatever object you want to link up to an event handler, and process your event there.

This approach is actually quite nice if you know what you are doing with it.
That looks fairly elegant. The only thing I'd be concerned about is passing around the event names as strings-- that seems like it could come back and bite you in the rear end.
Thanks for sharing!
The only thing I'd be concerned about is passing around the event names as strings-- that seems like it could come back and bite you in the rear end.

Well, there's not much choice otherwise. An associative list is the best approach, and we need those strings for the call(). Since the event handler can't know ahead of time what it needs to call without requiring a lot of overrides all over your code, passing strings is genuinely the best way to handle things.

Though I suppose if function pointers in DM were a thing, that'd probably be more ideal.
Yeah, fair enough. I'm still re-acquainting myself with DM, so I'm not really on top of the limitations of the collections. Is it possible to use constants as the index for associative lists in DM?
In response to Bdjewkes
You can use any value other than numbers, such as strings, object instances, and type paths, as the key in a key=value pair. Keys are kept in the order they were first assigned, so a numerical index would be used as a positional index.