0
\$\begingroup\$

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?

\$\endgroup\$
1
  • \$\begingroup\$ I described in an answer to another question (here) the idea of using pipelines to organize systems. That might be useful for you. The idea would be to invoke different pipelines depending on the current phase. \$\endgroup\$
    – Theraot
    Apr 11, 2021 at 1:43

1 Answer 1

1
\$\begingroup\$

Flat update loop

The multi-phase design may be the root of the problem, because it creates a dependency graph of its own, misaligned to ECS's dependency graph. The most direct solution would be to express the phases as a single ECS update.

If certain update function needs to be called multiple times (i.e. in different phases) during the update loop, you can pass the phase argument to that function. Even better, you could tell it what exactly you want in terms more specific than a phase. Like this:

updateMovement(playerOnly = true);
//...
updateMovement(playerOnly = false);

To alternate between player's turn and everyone else's, it may be okay to keep phases, but just 2 of them as a simple if-else branch. Alternatively, you may think of an animation queue, that may be played once calculations are complete.

Entity tracking lists

The issue of update sparsity (i.e. updates are skipping lots or most of the entities) in ECS is solved with entity lists. Ideally, you are supposed to use the entity lists from very beginning, to track which entities are relevant to which update function. The lists must be updated every time entity is created, modified or destroyed.

This approach may be applied to other aspects, such an AI or animation state (those may be FSMs). Allocate a list for each possible state (non-enums must be compressed to enums with clever mapping) and move entities between lists according to their current state. You also want to encapsulate the state to automate the list transfers.


These were the core issues, everything below is tangential.


One of they key features of rogue-likes is non-modality: one control scheme is used for all gameplay interaction. Picking item from the shelves and interacting with the cash register to deposit the money is the canonical way of trading, while displaying separate UI is antithetical to that.

Not saying that it's a silver bullet, but so far as a player I find that more enjoyable — partly because an "immersive" design promotes well-thought shopping list of really valuable items, while most shopping UIs welcomed me to the bottomless repositories of garbage.

Non-modality should also play well with the ECS design.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .