10
\$\begingroup\$

So far the entity component systems I've used have worked mostly like Java's artemis:

  • All data in components
  • Stateless independent systems (at least to the degree that they don't require input on initialization) iterating over each entity that contains only the components that particular system is interested in
  • All systems process their entities one tick, then the whole thing starts over.

Now I'm trying to apply this to a turn based game for the first time, with tons of events and responses that must occur in a set order relative to eachother, before the game can move on. An example:

Player A receives damage from a sword. In response to this, A's armor kicks in and lowers the damage taken. A's movement speed is also lowered as a result of getting weaker.

  • The damage taken is what sets off the whole interaction
  • Armor must be computed and applied to the incoming damage before the damage is applied to the player
  • Movement speed reduction cannot be applied to a unit until after the damage has actually been dealt, since it is dependent on the final damage amount.

Events can also trigger other events. Reducing sword damage using armor can cause the sword to shatter (this must take place before the damage reduction is completed), which in turn can cause additional events in response to it, essentially a recursive evaluation of events.

All in all, this seem to lead to a few problems:

  1. Lots of wasted processing cycles: Most systems (save for things that always run, like rendering) simply doesn't have anything worthwile to do when it's not "their turn" to work, and spend most of the time waiting for the game to enter a valid work-state. This litters every such system with checks that keep growing in size the more states are added to the game.
  2. To find out if a system can process entities that is present in the game, they need some way to monitor other unrelated entity/system states (the systaem responsible for dealing damage needs to know if armor has been applied or not). This either muddles the systems with multiple responsibilities, or creates the need for additional systems with no other purpose but to scan the entity collection after each processing cycle and communicate with a set of listeners by telling them when it's okay to do something.

The above two points assume that the systems work on the same set of entities, that end up changing state using flags in their components.

Another way to solve it would be to add/remove components (or create entirely new entities) as a result of a single systems work to progress the games state. This means that whenever a system actually have a matching entity, it knows that it is allowed to process it.

This however make systems responsible for triggering subsequent systems, making it difficult to reason about the programs behaviour since bugs won't show up as the result of a single system interaction. Adding new systems also gets harder since they can't be implemented without knowing exactly how they affect other systems (and previous systems might have to be modified to trigger the states the new system is interested in), kinda defeating the purpose of having separate systems with a single task.

Is this something I'll have to live with? Every single ECS example I've seen have been real time, and it's really easy to see how this one-iteration-per-game-loop works in such cases. And I still need it for rendering, it just seems really unfit for systems that pause most aspects of itself every time something happens.

Is there some design pattern for moving the game state forward that is suitable for this, or should I just move all logic out of the loop and instead trigger it only when needed?

\$\endgroup\$
2
  • \$\begingroup\$ You don't really want to poll for an event to happen. An event only happens when it occurs. Doesn't Artemis allow for systems to communicate with each other? \$\endgroup\$
    – Sidar
    May 4, 2013 at 5:10
  • \$\begingroup\$ It does, but only by coupling them using methods. \$\endgroup\$
    – Aeris130
    May 7, 2013 at 11:12

3 Answers 3

3
\$\begingroup\$

My advice here is coming from past experience on an RPG project where we used a component system. I will say that I hated working in that gameside code because it was spaghetti code. So I'm not offering much of an answer here, just a perspective:

The logic you describe for handling sword damage to a player... it seems one system should be in charge of all of that.

Somewhere, there's a HandleWeaponHit() function. It would access the player entity's ArmorComponent to get the relevant armor. It would access the attacking weapon entity's WeaponComponent to perhaps shatter the weapon. After computing final damage, it would touch the MovementComponent for the player to achieve the speed reduction.

As for wasted processing cycles... HandleWeaponHit() should only be triggered when needed (upon detecting the sword hit).

Maybe the point I'm trying to make is: surely you want a place in code where you can put a breakpoint, hit it, and then proceed to step through all the logic that is supposed to run when a sword hit occurs. In other words, the logic shouldn't be scattered throughout the tick() functions of multiple systems.

\$\endgroup\$
2
  • \$\begingroup\$ Doing it this way would make the hit() function baloon as more behavior are added. Let's say there's an enemy that falls down laughing every time a sword hits a target (any target) within its line of sight. Should HandleWeaponHit really be responsible for triggering that? \$\endgroup\$
    – Aeris130
    May 7, 2013 at 11:14
  • 1
    \$\begingroup\$ You have a tightly enmeshed combat sequence so yes, hit is responsible for triggering effects. Not everything has to be broken out into little systems, let this one system handle this because it really is your "Combat System" and it handles... Combat... \$\endgroup\$ Jun 2, 2013 at 19:42
3
\$\begingroup\$

It's a year old question but now I'm facing the same trubles with my home made game while studying ECS, thus some necromany. Hopefully it will end up in a discussion or at least few comments.

I`m not sure if it violates ECS concepts, but what if:

  • Add an EventBus to let Systems issue/subscribe to event objects (pure data in fact, but not a component I guess)
  • Create Components for each intermediate state

Example:

  • UserInputSystem fires an Attack event with [DamageDealerEntity, DamageReceiverEntity, Skill/Weapon used info]
  • CombatSystem is subscribed to it and calculates evasion chance for DamageReceiver. If evasion fails then it triggers Damage event with same parameters
  • DamageSystem is subscribed to such event and thus triggered
  • DamageSystem uses Strength,BaseWeapon damage, its type etc and writes it to a new IncomingDamageComponent with [DamageDealerEntity, FinalOutgoingDamage, DamageType] and attach it to the damage receiver Entity/Entities
  • DamageSystem fires a OutgoingDamageCalculated
  • ArmorSystem is triggered by it, picks up a receiver Entity or searches by this IncomingDamage aspect in Entities to pick up IncomingDamageComponent (last one could be probably better for multiple atacks with spread) and calculates armor and damage applied to it. Optionally triggers events for sword shattering
  • ArmorSystems removes IncomingDamageComponent in each entity and replaces it with DamageReceivedComponent with final calculated numbers that will afffect HP and Speed reduction from wounds
  • ArmorSystems sends an IncomingDamageCalculated event
  • Speed system is subscribed and recalculates the speed
  • HealthSystem is subscribed and decreases actual HP
  • etc
  • Somehow clean up

Pros:

  • System trigger each other providing intermediate data for complex chain events
  • Decoupling by EventBus

Cons:

  • I feel that I mix two ways of passing things: in event parameers and in the temporary Components. it might be a weak place. In theory to keep things homogeneous I could fire just enum events with no data so that Systems would find the implied parameters in the Entity's components by aspect... Not sure if it is Ok though
  • Not sure how to know if all the potentially interested SystemsHave processed IncomingDamageCalculated so that it could be cleaned up and let next turn happen. Maybe some kind of checks back in CombatSystem...
\$\endgroup\$
2
\$\begingroup\$

Posting the solution I finally settled on, similar to Yakovlev's.

Basically, I ended up using an event system since I found it very intuitive to follow its logic over turns. The system ended up responsible for the in-game units that adhered to turn-based logic (player, monsters, and anything they can interact with), real-time tasks such as rendering and input polling was placed elsewhere.

Systems implement an onEvent method that takes an event and an entity as input, signalling that the entity has received the event. Every system also subscribes to events and entities with a specific set of components. The only point of interaction available to the systems are the entity manager singleton, used to send events to entities and to retrieve components from a specific entity.

When the entity manager receives an event coupled with the entity it is sent to, it places the event at the back of a queue. While there are events in the queue, the foremost event is retrieved and sent to every system that both subscribes to the event and is interested in the component set of the entity receiving the event. Those systems can in turn process the components of the entity, as well as send additional events to the manager.

Example: Player takes damage, so the player entity is sent a damage event. The DamageSystem subscribes to damage events sent to any entity with the health component and has an onEvent(entity, event) method that reduces the health in the entitys component by the amount specified in the event.

This makes it easy to insert an armor system that subscribes to damage events sent to entities with an armor component. Its onEvent method reduces the damage in the event by the armor amount in the component. This means that specifying the order that systems receive events impact game logic, since the armor system must process the damage event before the damage system in order to work.

Sometimes a system has to step outside of the receiving entity however. To continue with my response to Eric Undersander, it would be trivial to add a system that accesses the game map and looks for entities with the FallsDownLaughingComponent within x spaces of the entity receiving damage, and then send a FallDownLaughingEvent to them. This system would have to be scheduled to receive the event after the damage system, if the damage event has not been cancelled at that point, the damage was dealt.

One issue that came up was how to make sure response-events are processed in the order they are sent, given that some responses may spawn additional responses. Example:

The player moves, prompting a movement event being sent to the players entity and picked up by the movement system.

In queue: Movement

If the movement is allowed, the system adjusts the players position. If not (player tried moving into an obstacle), it marks the event as cancelled, causing the entity manager to discard it instead of sending it to subsequent systems. At the end of the list of systems interested in the event is the TurnFinishedSystem, that confirms that the player has spent his turn on moving the character, and that his/her turn is now over. This results in a TurnOver event being sent to the player entity and placed queue.

In queue: TurnOver

Now say that the player stepped on a trap, causing damage. The TrapSystem gets the movement message before the TurnFinishedSystem, so the damage event is sent first. Now the queue instead looks like this:

In queue: Damage, TurnOver

All is fine so far, the damage event will be processed, and then the turn ends. However, what if additional events are sent as a response to the damage? Now the event queue would look like:

In queue: Damage, TurnOver, ResponseToDamage

In other words, the turn would end before any responses to damage were processed.

To solve this I ended up using two methods of sending events: send(event, entity) and respond(event, eventToRespondTo, entity).

Every event keeps a record of previous events in a response chain, and whenever the respond() method is used, the event being responded to (and every event in its response chain) ends up at the head of the chain in the event used to respond with. The intial movement event has no such events. The subsequent damage response has the movement event in its list.

On top of that, a variable-length array is used to contain multiple event queues. Whenever an event is received by the manager, the event is added to a queue at an index in the array that matches the amount of events in the response chain. Thus the initial movement event is added to the queue at [0], and the damage, as well as TurnOver events are added to a separate queue at [1] since they both were sent as responses to the movement.

When the responses to the damage event are sent, those events will contain both the damage event itself, as well as the movement, putting them in a queue at index [2]. As long as index [n] has events in its queue, those events will be processed before moving on to [n-1]. This gives a processing order of:

Movement -> Damage[1] ->ResponseToDamage[2] -> [2] is empty -> TurnOver[1] -> [1] is empty -> [0] is empty

\$\endgroup\$

You must log in to answer this question.

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