0
\$\begingroup\$

For example, let's say that the player walks into an enemy and in response, a chain of events should occur (such as: his score goes up, the enemy dies, a cinematic starts, etc.)

I see two "proper" ways of accomplishing this:

Method 1:

There exists a global message bus in the game. Any system can emit events to it, and any other system can listen for some events from it. For example, the system handling the players score would look like this:

messageBus.listen("playerhitsenemy", e => {
  score += e.monsterType;
});

and the collision system may look like this:

messageBus.emit("playerhitsenemy", { monsterType: 5 });

Similarly, other systems can listen for the same event, such that when something happens, the CPU immediately moves from processing the current system to the other system processing this event. That is, the emit call is basically just a list of callbacks that are immediately invoked and sent the message.

Method 2:

The second method, instead, is to encode events / messages as entities themselves. That is, when an event occurs (such as the player hitting something in the collision detection system), this would happen:

let newEntity = new Entity();
newEntity.components.push(new MessageComponent());
newEntity.components.push(new MessagePlayerHitsEnemyComponent(5));
entityAdmin.spawnNewEntitiy(newEntity);

That is, a new "message" entity is created, and it is given the generic Message component and the concrete event component type as well, MessagePlayerHitsEnemy.

Then, all systems interested in these messages would have something like this in their execute function:

for (let entity of entityAdmin.query(MessagePlayerHitsEnemyComponent) {
  let monsterType = entity.getComponent<MessagePlayerHitsEnemyComponent>();
 
  score += monsterType;
}

And then at the end of the frame there would be a system whose responsibility was deleting all messages, like so:

for (let entity of entityAdmin.query(MessageComponent) {
  entityAdmin.remove(entity);
}

I cannot decide which is better. The first method seems simpler and probably more efficient in Javascript, considering it's just immediately invoking all the requested functions. However, the second one seems more "correct", in each system is iteratively processed. That is, first the collision system runs in its entirety, then the scoring system, etc. There is no "jumping around" from system to system during execution. Each system processes everything in its entirety, and then the second system processes everything it's interested in, etc.

So, in essence, the first method seems simpler and more efficient, while the latter method seems more "correct", pure, verbose, debuggable, and extendable. Which one should I pick?

\$\endgroup\$
4
  • \$\begingroup\$ It sounds to me like you correctly understand the trade-offs implied by this choice. Now it's up to you to decide which pros are more valuable to your project and team, and which cons are more painful. If you're making a game that's extremely sensitive to the order in which actions take place, then the simplicity you gain from the first method might not be worth the sacrifice in controllable timing you take from dropping the second. Or, if you're making a game where the precise sequencing isn't so important, the former might be all you need. Only you know your project well enough to decide. \$\endgroup\$
    – DMGregory
    Commented Feb 1, 2021 at 18:34
  • \$\begingroup\$ Well I'm a solo developer and I just started writing this game a couple days ago so I don't feel like I have enough knowledge yet to make an informed decision. \$\endgroup\$ Commented Feb 1, 2021 at 18:38
  • \$\begingroup\$ I know in unity’s ECS system, crating an entity is probably the better bet, since you get to take advantage of all the optimisations “under the hood”. Maybe your JavaScript framework has some similar heavy optimization in its enmity component framework. \$\endgroup\$
    – Adam B
    Commented Feb 2, 2021 at 4:52
  • \$\begingroup\$ With the latter method, what happens if a system that runs later in the frame wants to send a message to a system that ran earlier? It seems like cleanup at the end of the frame can become quite complex if you need to determine which messages have been "fully processed" [consumed by all components that would be interested] \$\endgroup\$
    – Basic
    Commented Dec 12, 2022 at 17:27

1 Answer 1

3
\$\begingroup\$

My approach was to have a PhysicsSystem which works on entities that have a ColliderComponent/PhysicsComponent/TransformComponent, in turn, upon detecting collisions it creates a CollisionComponent for every entity that takes part in a collision. Then there could be followup systems in the chain that work on entities that have a HealthComponent and a CollisionComponent perhaps, or DamageDealerComponent and CollisionComponent. Essentially I just generate a component and ensure the sequence of systems is setup in a way where one system can generate entities for a later system.

\$\endgroup\$

You must log in to answer this question.

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