NSSTC / sim-ecs

Batteries included TypeScript ECS
https://nsstc.github.io/sim-ecs/
Mozilla Public License 2.0
81 stars 12 forks source link

Run pipeline from pipeline #68

Open minecrawler opened 10 months ago

minecrawler commented 10 months ago

For many games, it's useful to have different schedulers for systems. For example a turn-based games needs a scheduler for looping logic, like rendering, but also needs to process turns whenever a player presses a button.

There are various ways to go about this problem, but I'd like to propose the following and implement it in the near future:

It should be possible to run logic on the same (game) world, in order to circumvent syncing or expensive queries. Also, scheduling should be handled similar to data. We can already define a system schedule easily! So why not take it to the next level and define schedules for all our needs and be able to execute a schedule when we need it from within another schedule? Basically, call a method on Actions which takes a ISyncPointPrefab and executes it.

Example:

const TurnProcessorSystem = createSystem({
    actions: Actions,
    schedules: ReadResource(SchedulesRecord), // SchedulesRecord is typed as Record<string, ISyncPointPrefab>
    turnProcessingSignals: ReadEvents(TurnProcessingSignal), // may hold additional information
})
    .withRunFunction(async ({actions, schedules, turnProcessingSignals}) => {
        // If turn processing was requested - e.g. by clicking on a "End Turn" button ...
        if (turnProcessingSignals.getOne()) {
            // start processing the turn, e.g. run enemy AI
            await actions.executeSchedule(schedules.turnProcessorPipeline);
        }
    })
    .build();

Using this example, I'm not 100% yet, though, if it's a good idea to inject the processing right there, or defer it using Commands, as in

actions.commands.executeSchedule(schedules.turnProcessorPipeline);
dannyfritz commented 10 months ago

Is there a downside to offering both options? Executing a schedule mid-schedule and scheduling a deferred schedule? I lean toward deferred if I had to choose one because executing mid-schedule provides a lot of foot-guns. But I don't really have a good enough view of the space to truly provide insight.

minecrawler commented 10 months ago

I've been thinking about it. The only footgun I can come up with is that a user forgets to await the schedule. That's the same footgun all systems have, since sim-ecs is built around the idea that a step is finished at a certain point, and no async tasks are still running (unmanaged) in the background.

It could be safer to defer it, because then sim-ecs implements the await, but it would mean less control over when exactly the logic is running. Hence for now, I plan to add the execute method on Actions (and on Commands if someone really wants to defer it...).

Other than that, since all system have the same kind of access to the world and should use similar (save) scheduling logic by default, they won't be riskier to execute immediately.

@dannyfritz did you have any other footguns in mind?


Parameter-wise it will be good to add the possibility to set a scheduler, in case the default one isn't the right choice.

dannyfritz commented 10 months ago

I think you're getting at the footgun I'm thinking about. But if you schedule mid-schedule, you might cause the outer query to be out-of-date when it returns to the outer schedule after modifying entities in the inner schedule.

minecrawler commented 10 months ago

Modifying entities, as in adding and removing components and entities, is not immediately possible (as in "there's no API on Actions to do so"). Things which may cause any system during a step to become out-of-date can only be accessed via Commands, which defers the actual modification to a save point to do so (usually the end of a step). See https://nsstc.github.io/sim-ecs/interfaces/ISystemActions.html and https://nsstc.github.io/sim-ecs/interfaces/ICommands.html . In this case, the modification would still be deferred to the end of the step, so that the outer query stays valid.

In order to make immediate changes, it's a better architecture to keep the ECS constant and work with fields on components. This won't trigger expensive modifications and cache updates, leading to better and smoother performance. Since in JS objects are references, making changes to them immediately makes the changes available everywhere. This could be an issue if the scheduler-invoking system does not expect updates to a component. In such a case, using the Commands version of the invoker method might be the better way...