Command pattern for Systems
See original GitHub issueHello! Just to start out with I want to say great work with this library, it is very impressive and nice to use!
I want to ask for advice on how to extend Becsy to add a pattern, the ability to send 'Command’s to systems that trigger some behavior.
Motivation: I want an API that let’s me send messages between systems, and also from outside the ECS, to any system. Currently managing input from the DOM inside of systems has become hard to manage, I hope to consolidate DOM events in a separate module and communicate with the ECS through this new API. I also prefer that this is done without using ECS Components, since using them increases complexity of queries, introduces ambiguity in what pattern to use and requires that I create many components that are not meant for the same general purpose of representing long-lived data about an entity.
Currently how I’m using this pattern is by doing the following:
- Extend System to have a new “query” API for commands
protected command<P extends CommandProperties>(
CommandClass: CommandConstructor<P>
): CommandQueue<InstanceType<CommandConstructor<P>>> {
/**
* Apply our callback in response to new commands of the types you are subscribed to..
* @example `private exampleCommands = this.command(ExampleCommand)`
* */
const queue = commandService.subscribe(CommandClass, this.constructor.name);
// Returns a reference to a queue that will be populated by the command service.
return queue;
}
- A system query will subscribe to a service that can be called to send
Command
objects that extend a certain shape. 2a. I am able to send a command from anywhere, e.g. outside the ECS. I am also able to send commands from other systems.
send(SomeCommand) ┌─────────────┐
│ ┌──►│ SubscriberA │
▼ │ └─────────────┘
┌────────────────┬────┘
│ CommandService │ Fork
└────────────────┴────┐
│ ┌─────────────┐
└──►│ SubscriberB │
└─────────────┘
- Our systems will then have access to their own fork of the command queue that can be iterated on.
// In ExampleSystem's..
execute() {
// This dequeues this system's exampleCommands queue one by one
this.exampleCommands.forEachCommand(() => {
console.log("Responding to ExampleCommand!");
});
}
The current implementation seems to work well, although there are a few quirks:
- It would be great if
World
could be extended to include the only reference to theCommandService
, so that it automatically gets disposed when the world is gone. Can’t currently extendWorld
. Right now I have a global singleton that is accessible to anyone, and needs to be explicitly cleaned up when the world is disposed. - In order to send references to entities through a command, the only current way is to send a held entity. I am unsure how reliable this is, but ideally an entity id would be the only thing that gets sent, and if possible that id would then be accessed with O(1) on the Becsy side.
I’m very interested in hearing what your thoughts are regarding this, whether you’d possibly consider implementing such a pattern as part of Becsy, or if not, what patterns you would recommend.
Issue Analytics
- State:
- Created 2 years ago
- Comments:7
I push a new release that should address the issues raised here (and changed my mind about the ordering guarantee):
0.12.2
hasSomeOf
,hasAllOf
,hasAnyOtherThan
, andcountHas
toEntity
.validate(entity)
method to any component type and check for valid combinations of components using thehas
collection of methods, throwing an error if a check fails. All validation methods are executed for all entities whose components have changed after each system executes (not just ones that have a component of the method’s host type), and the system’s read entitlements are bypassed during these checks. Entities are not validated in response to writes, so validators shouldn’t look at fields. Entities are not validated at all in the perf build.Entity.ordinal
.orderBy
. Just pass in a function to transform entities into numeric values and all results will be sorted by the function’s output in ascending order. There are some optimizations to avoid unnecessary sorting, especially in the common case oforderBy(entity => entity.ordinal)
.Cool, that’s some great feedback. The main insight appears to be that it’s useful to constrain which components can, must, or cannot go together on an object. I need to think about it more but being able to declare some kind of rules, have Becsy validate their consistency, and then check them at runtime (in non-perf mode only!) sounds like a great idea. Would be fun to riff together on a possible design if you’re interested!
The big problem you’ll run into here is that in multi-threaded JavaScript only
SharedArrayBuffer
s are shared between threads; there’s no way to share “normal” objects (including systems, arrays, other singletons, etc.). One of the big value-adds of Becsy is that it hides the ugly buffers behind a thin veneer of components and guarantees thread-safe access. I doubt you’d be able to reproduce this (if for no other reason than Becsy not exposing its threading primitives), so you’d effectively need to have all producers and consumers of events in one thread for your approach to work.If you follow the pattern I gave then yes, in practice the command entities will show up in the
added
array in the order they were created. However, this is more of an artifact of Becsy’s log-based design and not yet formally guaranteed. It’s also a bit fragile in general, especially for queries that select on multiple component types that may not all be added at the same time.That said, I think this is a common enough pattern that it’s worth supporting in some way, perhaps by having Becsy keep track of entity creation order and supporting an
orderByTimeOfEntityCreation
modifier on queries. (I was also already planning to have a newcreate
entitlement that would, e.g., allow multiple systems to queue commands in parallel in some situations, which awrite
entitlement wouldn’t.)