Its a "headache" that separates the concerns better. It separates component ownership better. It separates component usages better. It separates the system dependencies better. If separating concerns is multiple headaches for you, then put everything in a god class.
The way I see it, these 'systems' are God classes. They typically handle large amounts of functionality and cut across various concerns in order to make various components work together.
I prefer encapsulating behaviour inside the components themselves, the way Unity does it. Sure, occasionally you need to add a coordinating component on top. For me, trying to decide how to split along 'system' lines just doesn't make any sense because each system is usually so broad as to be full of conditionals or so narrow as to probably be a good candidate to be specific to one component. And how do you decide what goes in individual components if there are an arbitrary number of different systems acting upon the component? Why have components at all in that case, and not just a long property list?
I'm not sure you're using "system" to mean the same thing thing Chubu is, or that I would. When I think of "components" and systems" in the context of ECS, I think of a "system" as an object that owns the data and provides the behaviour for all components of a particular type. It's essentially the same pairing of data and behaviour that drives OO, but the behaviour is moved to the "system" level to cut down on the need for dependency management on individual data, and the state of a particular "game object" is spread across multiple systems. If your systems look like god objects, it's probably time to split them out into smaller sub-systems that actually map to a particular bit of a data.
In my own code - which does
not use an ECS - a "system" is simply an aggregation of game state and some methods to act on it collectively. With this kind of "system", you generally don't have to think too hard about both which systems are orthogonal and which components are orthogonal because those are the same problem. "Component" may not be the best word here. I actually prefer the word "datum" because "component" implies that the component owns its own behaviour and exists as a separate "thing" in isolation, but with this approach it doesn't. The design process for a data-oriented "system" should ideally go something like:
- determine what the desired behaviour is
- determine what data is needed to implement the behaviour
- implement a system that owns the needed data and applies the desired behaviour to the data
With this approach, roughly-speaking each system should match to a particular feature of the game, though sometimes multiple systems need to use the same data, which means that you can split the shared data off into its own system. In general, systems exist to perform specific tasks. A system owns the data it needs to perform a particular task.
Here are some examples of my systems:
SpriteObjectSystem - owns all the data and behaviour needed to animate and render a sprite
CharacterSystem - owns all the data and behaviour needed to represent a character; each datum is essentially a character sheet that other gameplay systems can examine to drive their behaviour
MovementSystem - owns all data relevant to character movement; which characters are moving and how far, in what direction, and whether the movement was voluntary or not
CombatSystem - owns combat state data like how long a particular character has left in its swing, what attack it's using, etc.
CollisionSystem - owns data needed to test for collisions between characters and the terrain and characters and other characters
Updating the systems looks something like this (though in the actual game this update is spread across several "layers" that serve to aggregate related systems):
// somewhat abridged so as not to show the systems I didn't mention above
characters.RegenerateHealth(dt);
characters.ResetDisabledActions();
movement.ApplyAnimations(characters, spriteObjects);
movement.ComputeNextPositions(dt, characters, spriteObjects, collision);
movement.ApplyMovement(dt, characters, spriteObjects);
collision.ClearColliders();
characters.BuildCharacterColliders(collision, spriteObjects);
combat.BuildHitboxes(characters, spriteObjects);
combat.UpdateCombat(dt, characters, movement);
combat.ResolveAttacks();
characters.SetNextAnimations(spriteObjects);
spriteObjects.Tick(dt);
Note that each specific sub-task a subsystem carries out is its own method, rather than throwing everything in a big clunky "Update" function. This means that each method only needs to reference the specific sub-systems it needs, making dependency management substantially easier than having each system store references to one another. In addition, if I want to insert a step in between two particular tasks, I can easily do so. I can even put these into a job system if I want.