1
\$\begingroup\$

If I knew all of the systems at compile time, I could order them myself. However, I intend on having user-defined mods. This adds a level of complexity, in that I (as the framework developer) don't know which mods will be developed, or installed on a particular user's machine.

I've thought about adding some sort of execution ordering

// within core code
systemRegistrar.register(PhysicsSystem.class).before(RenderingSystem.class);

// within third-party mod
systemRegistrar.register(ModSystem.class).before(PhysicsSystem.class);

I've thought about an incidental ordering using some sort of message or event bus

// within core code
// - PhysicsSystem
public void update() {
    // do physics-y stuff
    eventBus.send(new PhysicsSystemUpdatedEvent());
}

// - RenderingSystem
public void handle(PhysicsSystemUpdatedEvent event) {
    // do rendering stuff
    eventBus.send(new RenderingSystemUpdatedEvent());
}

But this blurs the line between what will happen, and what could happen.

Another thought that came into my mind is that we can be a little bit dirty and not care too terribly much about what order the systems get executed in, because even if we render before the physics are applied, the next frame will then render the physics update that was made in the previous frame. It's consistent, but it blurs the line between what happens within a frame.

\$\endgroup\$
1
\$\begingroup\$

In general, I find that it's better to be explicit. If possible, I would advocate for simply fixing the ordering at compile time, as you noted.

But if the nature of your planning modding system is such that you want to introduce the ability for a mod to inject an entirely new class of functionality into the core update ordering, then the next level down I'd suggest would be akin to your first example: explicit ordering of the new system relative to an existing old system. I find it better to keep that ordering in a single place so it's clear and obvious, so I'd expect to see a mod bunch all it's register() (in your case) methods with their associated before() calls at a single point during initialization, rather than implicitly spreading that ordering information around via the more-distributed second example you've provided. This makes it much easier to reason about and to discover when you've accidentally got two things ticking in the wrong order.

\$\endgroup\$
0
\$\begingroup\$

in my ECS solution it would be a mistake to rely on the order of the engines. Instead I use the concept of a "Sequencer" that allows systems to communicate with each other in a given order. Sequencer actually are more complex, allowing conditional branching and loops.

A sequencer is an external dependency that knows the instance of the systems involved. The sequencer is then injected in to the involved systems by constructor.

Inside the System you can then do sequencer.Next(condition); to step to the next system method (the method is defined through an IStep interface that the involved system must implement). The System itself doesn't know about the other systems involved in the sequence, it just triggers the next step. The sequence of steps is actually defined in the sequencer.

I understand this explanation is not enough to give a clear idea of how I solved the issue, but the my main point is that the code shouldn't rely on how the engines are added. It would be a system too rigid that would implode pretty soon.

you can read more about it here: http://www.sebaslab.com/ecs-1-0/ (the sequencer example is toward the end of the article)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.