From 50c66d90ee6a2745adfe727dc1b521f62351993d Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 27 Apr 2022 19:36:31 +0200 Subject: [PATCH] #45 implement scheduling and sync-point creation prefabs --- .../bench/src/libraries/_sim-ecs/schedule.ts | 28 +++++---------- examples/counter.ts | 30 ++++++++++++++-- examples/pong/src/main.ts | 36 +++---------------- examples/pong/src/schedules/default.ts | 29 +++++++++++++++ examples/pong/src/schedules/pause.ts | 19 ++++++++++ src/index.ts | 1 + src/query/query.spec.ts | 1 - src/scheduler/index.ts | 3 ++ src/scheduler/pipeline/stage.spec.ts | 4 +-- src/scheduler/pipeline/stage.ts | 4 +-- src/scheduler/pipeline/sync-point.spec.ts | 13 +++++++ src/scheduler/pipeline/sync-point.ts | 27 +++++++++++++- 12 files changed, 135 insertions(+), 60 deletions(-) create mode 100644 examples/pong/src/schedules/default.ts create mode 100644 examples/pong/src/schedules/pause.ts diff --git a/examples/bench/src/libraries/_sim-ecs/schedule.ts b/examples/bench/src/libraries/_sim-ecs/schedule.ts index 30b0d64..8a67fa7 100644 --- a/examples/bench/src/libraries/_sim-ecs/schedule.ts +++ b/examples/bench/src/libraries/_sim-ecs/schedule.ts @@ -2,11 +2,11 @@ import {buildWorld, createSystem, queryComponents, World, Write} from '../../../ import {IBenchmark} from "../../benchmark.spec"; import {CheckEndSystem, CounterResource} from "./_"; -class A { constructor(public val: number) {} } -class B { constructor(public val: number) {} } -class C { constructor(public val: number) {} } -class D { constructor(public val: number) {} } -class E { constructor(public val: number) {} } +class A { constructor(public val: number = 0) {} } +class B { constructor(public val: number = 0) {} } +class C { constructor(public val: number = 0) {} } +class D { constructor(public val: number = 0) {} } +class E { constructor(public val: number = 0) {} } const ABSystem = createSystem({ @@ -77,35 +77,25 @@ export class Benchmark implements IBenchmark { for (let i = 0; i < 10000; i++) { this.world.buildEntity() - .with(A, 0) - .with(B, 0) + .withAll(A, B) .build(); } for (let i = 0; i < 10000; i++) { this.world.buildEntity() - .with(A, 0) - .with(B, 0) - .with(C, 0) + .withAll(A, B, C) .build(); } for (let i = 0; i < 10000; i++) { this.world.buildEntity() - .with(A, 0) - .with(B, 0) - .with(C, 0) - .with(D, 0) + .withAll(A, B, C, D) .build(); } for (let i = 0; i < 10000; i++) { this.world.buildEntity() - .with(A, 0) - .with(B, 0) - .with(C, 0) - .with(D, 0) - .with(E, 0) + .withAll(A, B, C, D, E) .build(); } } diff --git a/examples/counter.ts b/examples/counter.ts index b3a8409..f8a6eac 100644 --- a/examples/counter.ts +++ b/examples/counter.ts @@ -1,4 +1,4 @@ -import {Actions, buildWorld, createSystem, queryComponents, Write} from "../src"; +import {Actions, buildWorld, createSystem, ISyncPointPrefab, queryComponents, Write} from "../src"; /// a component. @@ -50,10 +50,30 @@ const CounterSystem = createSystem({ }) .build(); +// in order to execute all systems in the right order, we can create prefabs for execution schedules +// we can do so by defining what is happening in between sync-points + +// sync-points are defined moments in the schedule, when no system runs, and the ECS can sync updates, +// like adding and removing entities or components, to all system caches. +// It is useful to keep the amount of these sync-points to a minimum for a speedy execution! +// The ECS will always have one sync-point, which is the "root" sync-point. +// This means that syncing will always happen between each step. + +// for our simple logic, we only need one sync-point, which will be reached after the below plan has been executed +const executionSchedule: ISyncPointPrefab = { + // each sync point contains stages, which are work units with a defined (custom or default) scheduler + stages: [ + // each stage also contains several systems, which are orchestrated by the stage's scheduler + [ + CounterSystem, + ], + ], +}; + /// then, we need a world which will hold our systems and entities const world = buildWorld() - /// we can inform the world about our processing logic by adding the above defined system - .withDefaultScheduling(root => root.addNewStage(stage => stage.addSystem(CounterSystem))) + /// we can inform the world about our processing logic by adding the above defined prefab + .withDefaultScheduling(root => root.fromPrefab(executionSchedule)) /// we can register components types at this level in order to enable saving (serialization) and loading (deserialization) of them .withComponent(CounterInfo) .build(); @@ -61,7 +81,11 @@ const world = buildWorld() /// in order to do something, we still need to add data, which can be processed. /// think of this like filling up your database, whereas each entity is a row and each component is a column world + /// the commands interface is a safe way to mutate the world, since all changes are always buffered until a good moment, + /// for example after each step, all commands are applied + /// doing so makes sure that there are no race conditions between different systems accessing entities simultaneously .commands + /// invoking the entity builder in this way automatically adds the entity to the world .buildEntity() .with(CounterInfo) .build(); diff --git a/examples/pong/src/main.ts b/examples/pong/src/main.ts index e376926..3b579b8 100644 --- a/examples/pong/src/main.ts +++ b/examples/pong/src/main.ts @@ -1,27 +1,19 @@ import {buildWorld, IWorld} from "sim-ecs"; -import {PaddleSystem} from "./systems/paddle"; -import {InputSystem} from "./systems/input"; import {GameStore} from "./models/game-store"; import {MenuState} from "./states/menu"; import {UIItem} from "./components/ui-item"; -import {MenuSystem} from "./systems/menu"; -import {PauseSystem} from "./systems/pause"; import {Paddle} from "./components/paddle"; import {Position} from "./components/position"; -import {BallSystem} from "./systems/ball"; import {Velocity} from "./components/velocity"; -import {RenderUISystem} from "./systems/render-ui"; -import {RenderGameSystem} from "./systems/render-game"; import {Shape} from "./components/shape"; -import {AnimationSystem} from "./systems/animation"; import {ScoreBoard} from "./models/score-board"; import {PaddleTransforms} from "./models/paddle-transforms"; import {Dimensions} from "./models/dimensions"; -import {CollisionSystem} from "./systems/collision"; import {Collision} from "./components/collision"; import {Wall} from "./components/wall"; -import {BeforeStepSystem} from "./systems/before-step"; import {PauseState} from "./states/pause"; +import {defaultSchedule} from "./schedules/default"; +import {pauseSchedule} from "./schedules/pause"; const cleanup = () => { @@ -38,28 +30,8 @@ const cleanup = () => { const createWorld = () => { return buildWorld() - .withDefaultScheduling(root => root - .addNewStage(stage => stage.addSystem(BeforeStepSystem)) - .addNewStage(stage => stage.addSystem(InputSystem)) - .addNewStage(stage => stage - .addSystem(MenuSystem) - .addSystem(PaddleSystem) - .addSystem(PauseSystem)) - .addNewStage(stage => stage.addSystem(CollisionSystem)) - .addNewStage(stage => stage.addSystem(BallSystem)) - .addNewStage(stage => stage.addSystem(AnimationSystem)) - .addNewStage(stage => stage - .addSystem(RenderGameSystem) - .addSystem(RenderUISystem)) - ) - .withStateScheduling(PauseState, root => root - .addNewStage(stage => stage.addSystem(BeforeStepSystem)) - .addNewStage(stage => stage.addSystem(InputSystem)) - .addNewStage(stage => stage.addSystem(PauseSystem)) - .addNewStage(stage => stage - .addSystem(RenderGameSystem) - .addSystem(RenderUISystem)) - ) + .withDefaultScheduling(root => root.fromPrefab(defaultSchedule)) + .withStateScheduling(PauseState, root => root.fromPrefab(pauseSchedule)) .withComponents( Collision, Paddle, diff --git a/examples/pong/src/schedules/default.ts b/examples/pong/src/schedules/default.ts new file mode 100644 index 0000000..0c8ab86 --- /dev/null +++ b/examples/pong/src/schedules/default.ts @@ -0,0 +1,29 @@ +import {ISyncPointPrefab} from "sim-ecs"; +import {BeforeStepSystem} from "../systems/before-step"; +import {MenuSystem} from "../systems/menu"; +import {PaddleSystem} from "../systems/paddle"; +import {PauseSystem} from "../systems/pause"; +import {CollisionSystem} from "../systems/collision"; +import {BallSystem} from "../systems/ball"; +import {AnimationSystem} from "../systems/animation"; +import {RenderGameSystem} from "../systems/render-game"; +import {RenderUISystem} from "../systems/render-ui"; + + +export const defaultSchedule: ISyncPointPrefab = { + stages: [ + [BeforeStepSystem], + [ + MenuSystem, + PaddleSystem, + PauseSystem, + ], + [CollisionSystem], + [BallSystem], + [AnimationSystem], + [ + RenderGameSystem, + RenderUISystem, + ], + ], +}; diff --git a/examples/pong/src/schedules/pause.ts b/examples/pong/src/schedules/pause.ts new file mode 100644 index 0000000..31f27f3 --- /dev/null +++ b/examples/pong/src/schedules/pause.ts @@ -0,0 +1,19 @@ +import {ISyncPointPrefab} from "sim-ecs"; +import {BeforeStepSystem} from "../systems/before-step"; +import {InputSystem} from "../systems/input"; +import {PauseSystem} from "../systems/pause"; +import {RenderGameSystem} from "../systems/render-game"; +import {RenderUISystem} from "../systems/render-ui"; + + +export const pauseSchedule: ISyncPointPrefab = { + stages: [ + [BeforeStepSystem], + [InputSystem], + [PauseSystem], + [ + RenderGameSystem, + RenderUISystem, + ], + ], +}; diff --git a/src/index.ts b/src/index.ts index 7f977fc..13384aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './ecs/ecs-world'; export * from './entity'; export * from './entity-builder'; export * from './query'; +export * from './scheduler'; export * from './serde'; export * from './state'; export * from './system'; diff --git a/src/query/query.spec.ts b/src/query/query.spec.ts index fa85655..aadfe6c 100644 --- a/src/query/query.spec.ts +++ b/src/query/query.spec.ts @@ -73,4 +73,3 @@ export interface IQuery { export interface IComponentsQuery> extends IQuery> {} export interface IEntitiesQuery extends IQuery, IEntity> {} -//export interface IQueryProto | TExistenceQuery> { new(): IQuery } diff --git a/src/scheduler/index.ts b/src/scheduler/index.ts index e36fefc..a31c4d3 100644 --- a/src/scheduler/index.ts +++ b/src/scheduler/index.ts @@ -1 +1,4 @@ export * from "./scheduler"; +export * from "./pipeline/pipeline"; +export * from "./pipeline/stage"; +export * from "./pipeline/sync-point"; diff --git a/src/scheduler/pipeline/stage.spec.ts b/src/scheduler/pipeline/stage.spec.ts index d1b9ed1..e700e7d 100644 --- a/src/scheduler/pipeline/stage.spec.ts +++ b/src/scheduler/pipeline/stage.spec.ts @@ -2,10 +2,10 @@ import {ISystem} from "../../system"; import {TExecutor} from "../../_.spec"; import {World} from "../../world"; -export type TSchedulingAlgorithm = (world: World, systems: ISystem[]) => Promise; +export type TStageSchedulingAlgorithm = (world: World, systems: ISystem[]) => Promise; export interface IStage { - schedulingAlgorithm: TSchedulingAlgorithm + schedulingAlgorithm: TStageSchedulingAlgorithm systems: ISystem[] /** diff --git a/src/scheduler/pipeline/stage.ts b/src/scheduler/pipeline/stage.ts index 2d3bdcd..ebf2c88 100644 --- a/src/scheduler/pipeline/stage.ts +++ b/src/scheduler/pipeline/stage.ts @@ -7,7 +7,7 @@ import type {World} from "../../world"; export * from "./stage.spec"; -export async function defaultSchedulingAlgorithm(world: World, systems: ISystem[]): Promise { +export async function defaultStageSchedulingAlgorithm(world: World, systems: ISystem[]): Promise { let system; for (system of systems) { @@ -25,7 +25,7 @@ export async function defaultSchedulingAlgorithm(world: World, systems: ISystem[ } export class Stage implements IStage { - schedulingAlgorithm = defaultSchedulingAlgorithm; + schedulingAlgorithm = defaultStageSchedulingAlgorithm; systems: ISystem[] = []; addSystem(System: ISystem): Stage { diff --git a/src/scheduler/pipeline/sync-point.spec.ts b/src/scheduler/pipeline/sync-point.spec.ts index bde2c87..6f32c9b 100644 --- a/src/scheduler/pipeline/sync-point.spec.ts +++ b/src/scheduler/pipeline/sync-point.spec.ts @@ -1,4 +1,11 @@ import {IStage} from "./stage.spec"; +import {ISystem} from "../../system"; + +export interface ISyncPointPrefab { + after?: ISyncPointPrefab + before?: ISyncPointPrefab + stages?: ISystem[][] +} export interface ISyncPoint { after?: ISyncPoint @@ -9,4 +16,10 @@ export interface ISyncPoint { * Add a stage to this group */ addNewStage(handler: (stage: IStage) => void): ISyncPoint + + /** + * Create an execution tree from a schedule-prefab + * @param prefab + */ + fromPrefab(prefab: ISyncPointPrefab): ISyncPoint } diff --git a/src/scheduler/pipeline/sync-point.ts b/src/scheduler/pipeline/sync-point.ts index 184f08c..8822f7a 100644 --- a/src/scheduler/pipeline/sync-point.ts +++ b/src/scheduler/pipeline/sync-point.ts @@ -1,5 +1,6 @@ -import {ISyncPoint} from "./sync-point.spec"; +import {ISyncPoint, ISyncPointPrefab} from "./sync-point.spec"; import {IStage, Stage} from "./stage"; +import {ISystem} from "../../system"; export * from "./sync-point.spec"; @@ -14,4 +15,28 @@ export class SyncPoint implements ISyncPoint { handler(stage); return this; } + + fromPrefab({after, before, stages = []}: ISyncPointPrefab): SyncPoint { + this.after = after + ? new SyncPoint().fromPrefab(after) + : undefined; + this.before = before + ? new SyncPoint().fromPrefab(before) + : undefined; + this.stages.length = 0; + + { + let stage: ISystem[]; + let system: ISystem; + for (stage of stages) { + this.addNewStage(newStage => { + for (system of stage) { + newStage.addSystem(system); + } + }); + } + } + + return this; + } }