Hey guys, I'm using an ECS(-ish) setup for my game engine. Currently I have "entities" which are integers, "components" which are plain old data types, and "systems" which are modules containing callback functions.
What I want to do is refactor my game engine so each system module can be compiled into its own DLL, and be loaded dynamically at runtime. On first think it seems like I just need to figure out how to expose the callback functions in the DLL to the rest of the engine, and use shared engine resources in the DLL. A typical system module: systemOf kinematics: (DynamicBody, Transform) proc onEngage(entity: Entity) {.kinematics.} = # Do stuff when a new entity with DynamicBody and Transform components spawn proc onUpdate(context: Context) {.kinematics.} = var id: Entity if engines.physics.checkUpdate(id): # Synchronize with the shared physics engine proc onMutate(entity: Entity, transform: Transform) {.kinematics.} = # Update DynamicBody based on Transform updates proc onMutate(entity: Entity, transform: DynamicBody) {.kinematics.} = # <-- overload # Update Transform based on DynamicBody updates Run What I plan to do is somehow generate an exported function in every system that returns a struct of function pointers that point to the functions in the module: type SystemCallbacks = object onEngage: proc(entity: Entity) onUpdate: proc(context: Context) onMutate: array[2, pointer] # huh? proc kinematicsCallbacks*(): SystemCallbacks {.stdcall exportc dynlib.} = # return a struct of function pointers Run As you can see from the above, I am unsure about how to deal with function overloads, short of completely getting rid of them. Currently I generate integer IDs for component types so I can do stuff with them at runtime: type TypeId* = uint16 proc getId(): TypeId {.noinit.} = var id {.global.} : TypeId = 0 result = id id += 1 proc typeId*(T: type): TypeId {.noinit.} = # generates a unique ID per instantation of the implicit generic # source: https://stackoverflow.com/a/2486090 let id {.global.} = getId() result = id Run So I thought maybe I can use a sparse array indexed by `TypeId` to resolve overloads: type SystemCallbacks = object ... onMutate: array[0..TypeId.high, pointer] # hmm Run But therein lies the problem: how do I generate a consistent `TypeId` that can be used in multiple DLLs? A solution I thought of is to create a separate module where I register component types: # componentregistrar.nim import "./components/dynamicbody" import "./components/transform" # creates exported functions that return `TypeId` register(DynamicBody) register(Transform) Run and compile _that_ into yet another DLL, and have the engine and system DLLs use the IDs from that DLL. It's an extra step though, and adds some complexity. Is there a better way?