Currently I am evaluating how to refactor my core game logic since I tried to design most of it in pure ECS but isn't really completely feasible the way I intended it.
My game at its core is a turn-based roguelike with multiple (implicit) phases during a turn:
- Player Action Phase: Movement, Attack or Item Usage
- If the chosen action was Attack or Item Usage, handle it right away
- Otherwise, queue a Movement intent
- Movement Phase: All units that decided to move will move simultaneously
- Attack Phase: All enemy units that haven't moved will attack one-by-one
- End Phase: Resolve status effects
These phases have been difficult to design with ECS. Certain systems have checks whether the should run right now with a bunch of tag components. Examples include:
- The BehaviorSystem updates enemy entities, that are currently thinking (applied after any player actions have been resolved), but have neither finished their turn yet and are not suppressed by anything (usually used to await animations).
- The AttackerSelectionSystem that only runs when no movement or attack animation is currently playing and then only picks enemies that still have to attack, sorts them by initiative and tags the next enemy to start his attack during the next frame.
Eventually my current architecture won't scale well with more and more systems to come, especially with different kinds of game states such as game elements that are UI only, in-town/hub maps where I don't need any of these combat/turn-based elements and finally the instance/dungeon areas where I will need them.
My idea is now to include a FSM in my Game class decoupled from the ECS itself to handle my current approach to invoke systems:
auto Game::Implementation::updateSystems(const sf::Time DeltaTime) -> void {
updatePlayerControllers(Registry, InputManager, Tilemap);
updateBehaviors(Registry, Tilemap);
updateMovementAnimations(Registry, DeltaTime);
updateAttackerSelection(Registry);
updateAttackAnimations(Registry, DeltaTime);
updateDamageEvents(Registry, ContentManager);
updateFloatingCombatTextAnimations(Registry, DeltaTime);
updateEndOfTurn(Registry);
updateLocalToWorldTransforms(Registry);
updateSpriteRendererTransforms(Registry);
updateEntityRemovals(Registry);
}
Now I am evaluating how to implement this in a clean way. I fear with too many states I might end up with spaghetti scheduling using weird if/switch constructs. Alternatively I could add a check at the beginning of each system whether the current state of the FSM/game allows them to even run.
Finally a most sophisticated approach would most likely be a dependency graph with conditions that will schedule the systems to run each frame.
Are there any common patterns and approaches to handle this scheduling based on game/turn state?