NSSTC / sim-ecs

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

Implement Component Observers #67

Open minecrawler opened 1 year ago

minecrawler commented 1 year ago

Components' fields change all the time, and sometimes we want to react to a certain change on a component without having complex logic or writing our own event machinery.

The goals are:

One example could be listening for changes to an object (as a component) which is managed by a thrid-party library, like a physics engine or AI.

A more understandable example could be the implementation of achievements. Whenever a player enables a lever, the state can be observed and inside a system it can be tracked without having to add new logic. Let's say the game wants to track that a player activated a lever three times:

class Lever {
    constructor(public state: LeverState) {}
}

class LeverInteraction {
    constructor(public target: Lever) {}
}

class LeverInteractionEvent {
    constructor(
        public readonly entity: IEntity,
        public readonly lever: Lever,
        public readonly field: string,
        public readonly oldValue: unknown,
        public readonly newValue: unknown,
    ) {}
}

class LeverState {
    active = false
}

// ...

prepWorld.buildEntity().withComponents(
    new Mesh('lever.glb'),
    Interactive,
    Obervable(Lever, LeverInteractionEvent),
    // ...
).build();

// ...

const LeverSystem = createSystem({
    query: queryComponents({leverInteractions: Read(LeverInteraction)}),
})
    .withRunFunction(({query}) =>
        // Change lever state after activation
        query.execute(({leverInteractions}) => leverInteractions.target.state.active = true)
    )
    .build();

const ActivateLeverThreeTimesSystem = createSystem({
    achievementState: Storage({ activatedLevers: 0, unlocked: false }),
    leverInteractionEvent: ReadEvent(LeverInteractionEvent),
})
    .withRunFunction(({achievementState, leverInteractionEvent}) =>
        leverInteractionEvent.execute(event => {
            // Increment number of lever activations this step
            if (event.newValue as boolean == true) {
                achievementState.activatedLevers++;
            }
        });

        // Check if the threshold was reached
        if (!achievementState.unlocked && achievementState.activatedLevers >= 3) {
            achievementState.unlocked = true;
            // unlock achievement!
        }
    )
    .build();

This code is not final, but should demonstrate how such an observer could be used from a user perspective. The implementation can be done using setters in the Object definition. We will not use Proxys, since their performance is abysmal. Also, observers should be used sparingly, since they augment a function call behind a simple value assignment.