question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Command pattern for Systems

See original GitHub issue

Hello! 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:

  1. 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;
}
  1. 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 │
                            └─────────────┘
                      
  1. 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 the CommandService, so that it automatically gets disposed when the world is gone. Can’t currently extend World. 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:closed
  • Created 2 years ago
  • Comments:7

github_iconTop GitHub Comments

1reaction
pkaminskicommented, Feb 17, 2022

I push a new release that should address the issues raised here (and changed my mind about the ordering guarantee):

0.12.2

  • Added hasSomeOf, hasAllOf, hasAnyOtherThan, and countHas to Entity.
  • Implemented experimenal global component combination validators. You can add a static validate(entity) method to any component type and check for valid combinations of components using the has 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.
  • Added Entity.ordinal.
  • Implemented query result ordering via 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 of orderBy(entity => entity.ordinal).
1reaction
pkaminskicommented, Feb 16, 2022

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!

I think at a glance it has equivalent thread-safety, but once multi-threading is implemented maybe we’d need to copy the queue/array before running execute().

The big problem you’ll run into here is that in multi-threaded JavaScript only SharedArrayBuffers 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.

I also need the order in which the commands were sent to be respected, is it ensured that this is the case when querying entities?

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 new create entitlement that would, e.g., allow multiple systems to queue commands in parallel in some situations, which a write entitlement wouldn’t.)

Read more comments on GitHub >

github_iconTop Results From Across the Web

Command Pattern - GeeksforGeeks
Definition: The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, ...
Read more >
Design Patterns - Command Pattern - Tutorialspoint
Command pattern is a data driven design pattern and falls under behavioral pattern category. A request is wrapped under an object as command...
Read more >
Command Design Pattern - DigitalOcean
Command Pattern is one of the Behavioral Design Pattern. Command design pattern is used to implement loose coupling in a request-response model.
Read more >
Command Design Pattern - SourceMaking
The Command pattern allows requests to be encapsulated as objects, thereby allowing clients to be parametrized with different requests. The "check" at a...
Read more >
Command - Refactoring.Guru
Command is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found