From looking at the entity system it seems like systems become tightly coupled together. By that I mean one system is directly using another inside itself. This often leads to querying and entity to see if it has x system on it and then call some function on it. I feel like this results in sketti code personally.
I’ve created and event component design. It’s sort of like messages but slightly different. The main idea though is isolated components. Components that don’t know or care about other components. Inside components they have events and actions (functions). The component raises events on things it thinks the outside world would care about. It has actions that allows its internal state to be changed.
The game object still holds a list of components but now it’s also the central hub for connecting components events to other components actions. The nice thing about this is that at a glance you see how that game object works with code that is all on one level. No real logic code just configuration of events. It’s a nice overview and commenting out subscriptions is a nice feature if you want to stop something from happening. Adding new features with this method is very simple as well. Just add the component and hook up the events and your done.
The entity approach treats this differently by directly having systems queried and called directly from other systems resulting in sometimes wasted time if this entity doesn’t have the system you’re looking for and bloated code (relatively speaking).
Events pass data along that the component thinks relevant. Actions receive this data. Because the creator of components might be different people who aren’t communicating or even know anything about each other the variable names sent in the args of the event of one component and the args in the action of another would be different variable names, each subscription has an additional callback function with it. This is called before the actual subscribed action is called and the args are passed to it. This is a mapping callback function because it allows the user of the components to map one variable from the event to another to the action. This obviously only works in a language that allows more dynamic types like a scripting language or even C#, because the args are a dynamic/flexible type to allow this.
There is also a filter function callback on the subscription. This is called after the mapping callback but before the action is called. If this function returns true the action is called if false then it’s not called. This function also gets the args passed. This allows a person to read the args and there are cases that you may not want to call this action based of some value in the args for whatever reason.
An example of this in action is an input component raising an event on key press where it passes the pressed key as the args. You may want different actions raised on different keys being pressed. You can hook up all your actions to this one input key press event but filter which ones get called based on what key was pressed from reading the args in the filter function.
If using a dynamic scripting language all of this can be put into a config file or database since it can all be represented as strings that get converted to objects/events/actions.
From my workings so far this has proven to be the best component approach that allows flexibility, configuration setup, decoupled functionality which leads to easy maintainability and reuse.
To provide a pseudo example is Lua of the event subscription:
inputCompoment.onKeyHit:subscribe(controllerComponent.moveForward, nil, function(args) if args.key == Key.W then return true else return false end)
So when onKeyHit raised from the input component, it’ll first call the last parameter in the subscribe which is the filter function. If it returns true it’ll then call the moveForward action of the controller component. The 2nd parameter to subscribe is nil. That’s the mapping callback but in this case it’s not needed. Here is an example of that if the 2 components were created by different people in isolation and they didn’t coordinate on variable names raised from event and used in action.
onReceiveDamage:subscribe(healthComp.hurt, function(args) args.hurtValue = args.dmgValue end)
The onReceiveDamage event created a variable on the args object called dmgValue but the health component uses a variable called hurtValue. So because this is a dynamic scripting language that assignment creates the variable hurtValue and assigns it the value from the event variable dmgValue. I’ve now easily rested a mapping between 2 isolated components written by different people.
There is also a post callback to subscribe() that’s called now matter what after the others are called. It’s a more rare case but it does come in handy sometimes.
A side effect benefit. Because all action function calls are now handles with the event system it’s a centralized place for all function calls. This gives us logging benefit AND I make those calls all coroutines. So now inside an action you can yield out and do things over time very easily vs having to manage state and then handle those things inside an update function of the component. I find that because of this, hardly any component even need an update function at all making them more efficient.