Giter Club home page Giter Club logo

becsy's People

Contributors

endel avatar getkey avatar marionebl avatar pkaminski avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

becsy's Issues

Is there an API to see if Becsy is currently running in accessRecentlyDeletedData mode?

If not, I think this could be useful.

Reason: Sometimes I have a function (foo()) that does useful things to entities, in situations where accessRecentlyDeletedData(false) and also when accessRecentlyDeletedData(true). While accessing recently deleted data mode is on, I want to avoid calling entity.remove() on a recently deleted entity, because it's deleted already and an error is thrown in dev mode if I were to try. But I can't see a Becsy-API way of asking if accessRecentlyDeletedData is true or not, to make that check possible.

Alternatives:

  • It's certainly possible for me to wrap accessRecentlyDeletedData() in my own function that also sets a variable of my own to track this, but this leaves open the ability for people to still call accessRecentlyDeletedData() directly and making my new variable incorrect.
  • It's possible to dive past Becsy's API and get the value of this.__dispatcher.registry.includeRecentlyDeleted from any system, but obviously not ideal because that's internal, undocumented and might change in future versions of becsy.
  • It's also possible for me to pass in an indication of whether accessRecentlyDeletedData mode is true into foo() itself at the location where it is called e.g. foo({includeRecentlyDeleted: true}), to then conditionally call entity.remove() inside foo, but this feels like double-handling a piece of state that I know exists already inside of becsy.

Open to any thoughts!

How to use dictionary and other reference type in the component?

I want to use an array, map other reference types as a field in a component.

Which I think I should use @field.ref.

However, want to know a bit more about its behavior.

  1. Is updating the field like so will mark the component as changed?
@component 
export class MyComponent {
    @field.ref declare indexes: {[key: string]: Entity};
}

let myComp = entity.write(MyComponent);
myComp["newItem"] = entity2;
myComp.indexes = myComp.indexes; // Mark the component as changed?
  1. Will I going cause the problem with multi-threading by multiple systems trying to access and modify these ref type fields or referenced Entity?

Ability to run a `dispose` method on systems in a system group

Hey there!

Firstly: Thank you so much for becsy! It's been awesome getting to work with it. I had a bit of an... interesting need which I wanted to ask about ๐Ÿ˜„

In my application, I've setup some system groups to correspond to different concepts:

  • Input handling
  • Capabilities
  • Rendering

...etc

Since I'm in a context where people can navigate away and back to the application, I need a way to dispose of all the event handlers which have been declared in the input system group, so they don't double up / we restart in a clean state.

From an API perspective, I believe this could look something like:

const group = System.group(MouseInputSystem, KeyboardInputSystem);

// Before the world is terminated, clear out all event handlers:
group.dispose();

The assumption here would be that all of the systems have an optional dispose() method attached to them which allows them to handle cleaning up any state / listeners they have attached. Not strongly attached to the idea of course - just how I'd see it fitting into my application neatly!

I'm currently reaching into group.__systems a bit naughtily to achieve this... ๐Ÿ™ˆ

Keen for any solution which you believe would align best with becsy's vision. Also more than happy to contribute it back if you think the above approach makes sense!

Cheers!

Cannot read properties of undefined (reading 'trackedWrites')

I'm seeing an exception thrown from becsy source via exception tracking that I haven't been able to reproduce locally yet

Error: Cannot read properties of undefined (reading 'trackedWrites')
  at UR.write(/~/setup.js:6:5065)
  # cut

If traced it to this piece of minified code (formatted for legibility)

class UR{
  constructor(t){
    z(this,"__registry");
    z(this,"__id");
    z(this,"__valid",!0);
    this.__registry=t
  }
 /* more methods */
  write(t){
    return t.__binding.trackedWrites && this.__registry.trackWrite(this.__id,t),t.__bind(this.__id,!0)
  }
 /* more methods */
}

Scanning the source I think this mirrors
https://github.com/LastOliveGames/becsy/blob/main/src/entity.ts#L232-L240

The code I execute right before this amounts to

execute() {
    this.entities.current.forEach((entity) => {
      const a = entity.write(AComponent);
      const b = entity.write(BComponent);
      const c = entity.read(Component).property;

      b.propertyA = b.propertyA * 2
      b.propertyB =  b.propertyB * 2

      a.propertyA = somethingA;
      a.propertyB = somethingB;
    });
}

Sorry I don't have a better reproduction, I figured bringing it up might help with gathering more context by having your in-depth expertise.

Question: Tracking / accessing entities by their id

Hi! I've been trying out your library, and so far it's worked well for me so far - the reactive queries are great!

For my use case, I had hoped to be able to update individual entities without needing to scan through all of the entities each time to find the element to update. It is fairly simple to maintain a mapping from entities to external elements; but not the other way around.

I've noticed each entity is associated with an __id, but the name implies it is not intended to be a public API. Was this intentional? Is there a recommended way to quickly find a specific entity?

Thanks!

Terminating the world and creating a new one with the same components results in an error

  • Our use case for becsy is inside an SPA react app with multiple routes.
  • Some routes serve content that aren't dependent on the becsy world.

Likewise when returning to routes that do rely on the becsy world we have the need to create a new world with a completely different set of entities.

We have been attempting to terminate the world, and create it again when returning - however terminating and starting a new world with the same components currently results in the following due to some global state for said components:

Uncaught (in promise) Error: Component type XComponent is already in use in another world
at Dispatcher.startFrame (dispatcher.ts:320:13)
    at FrameImpl.begin (schedule.ts:298:21)
    at Dispatcher.execute (dispatcher.ts:296:24)
    at World.execute (world.ts:97:30)

Is there a possibility of supporting this use-case: (terminating becsy, and creating the world again with the same components)?

Attach returns system box instead of system

It looks like when using the System.attach method, the returned value is actually the system box containing the system.

system.ts:218

        CHECK: if (!targetSystem) {
          throw new Error(`Attached system ${targetSystemType.name} not defined in this world`);
        }
        (this.system as any)[prop] = targetSystem;
      }

I think it should be targetSystem.system instead.

I have the code (and associated test fixed) on a fork, do you mind me raising a PR?

Graph.findCycles() does not always find cycles

Consider the following systems with some trivial scheduling constraints that have a cycle:

class SystemA extends System { }

class SystemB extends System {
  constructor() {
    super();
    this.schedule(s => s.after(SystemD));
  }
}

class SystemC extends System {
  constructor() {
    super();
    this.schedule(s => s.after(SystemB));
  }
}

class SystemD extends System {
  constructor() {
    super();
    this.schedule(s => s.after(SystemC));
  }
}

await World.create({
  defs: [
    SystemA,
    SystemB,
    SystemC,
    SystemD,
  ],
});

World.create() will happily accept this despite the cycle between SystemB -> SystemD -> SystemC -> SystemB. This has bitten us on our application where we had such a cycle that went undetected. This caused the dependency graph to be broken causing strange behaviour.

When we rewrote findCycles() using Tarjan's strongly connection component algorithm, it was able to properly detect the cycle:

findCycles() {
    const cycles = [];
    const S = [];
    const index = new Array(this.numVertices).fill(null);
    const lowlink = new Array(this.numVertices).fill(null);
    const onStack = new Array(this.numVertices).fill(false);

    let i = 0;
    const strongconnect = (v) => {
        // Set the depth index for v to the smallest unused index
        index[v] = i;
        lowlink[v] = i;
        i++;
        S.push(v);
        onStack[v] = true;
        
        // Consider successors of v
        for (let w = 0; w < this.numVertices; w++) {
            if (this.hasEdgeBetweenIds(v, w)) {
                if (v === w)
                    console.log('self edge')
                if (index[w] == null) {
                    // Successor w has not yet been visited; recurse on it
                    strongconnect(w);
                    lowlink[v] = Math.min(lowlink[v], lowlink[w]);
                } else if (onStack[w]) {
                    // Successor w is in stack S and hence in the current SCC
                    // If w is not on stack, then (v, w) is an edge pointing to an SCC already found and must be ignored
                    // Note: The next line may look odd - but is correct.
                    // It says w.index not w.lowlink; that is deliberate and from the original paper
                    lowlink[v] = Math.min(lowlink[v], index[w]);
                }
            }
        }
        // If v is a root node, pop the stack and generate an SCC
        if (lowlink[v] === index[v]) {
            const cycle = [];
            let w;
            do {
                w = S.pop();
                onStack[w] = false;
                cycle.push(this.vertices[w]);
            } while (w !== v);

            if (cycle.length > 1) {
                cycles.push(cycle);
            }
        }
    };

    for (let v = 0; v < this.numVertices; v++) {
        if (index[v] == null) {
            strongconnect(v);
        }
    }

    return cycles;
}

Ability to refer to entities by identifiers stored in an external data structure

Hey Piotr!

It's been about a year since I started using becsy and it's still working wonderfully for my use case; thanks again for your sustained effort on this awesome library ๐Ÿ™ I hope for my app to go public soon enough so we can share it with you!

I've come with yet another odd use case, but one I'm hoping you'd have suggestions on ๐Ÿ˜„

I've been trying to build a VisibilitySystem which marks specific elements which are in or out of view as visible using a VisibleComponent. To determine visibility performantly, my system is making use of an external data structure to implement a map of tiles (leveraging tiling as the algorithm), and we're also considering a quadtree for spatial queries later onwards.

My question is: I'm struggling to find a fast way to lookup entities after I query for which ones are visible in my data structure. The problem is exacerbated by our world having a huge number of entities (sometimes upwards of 10000).

The system I'm trying to build is roughly like:

class VisibilitySystem extends System {
  entities = this.query((q) => q.addedOrChangedOrRemoved.with(IdComponent, PositionComponent, SizeComponent));
  camera = this.query((q) => q.addedOrChanged.with(CameraComponent).trackWrites);

  initialize() {
    this.tileMap = new TileMap();
  }

  execute() {
    for (const entity of this.entities.addedOrChangedOrRemoved) {
      const id = entity.read(IdComponent).id;
      const visible = this.tileMap.addElement(
        id, 
        entity.read(PositionComponent), 
        entity.read(SizeComponent)
      );

      // Works great for mutating a single entity when it changes
      if (visible) {
        entity.add(VisibleComponent);
      }
    }

    if (this.camera.addedOrChanged.length) {
      const camera = this.entities.addedOrChanged[0];
      const visibleElementIds = this.tileMap.visibleElements(camera);

      // Question: I want to avoid traversing through all ~10000 elements I have here to save on performance cost
      // and add a `VisibleComponent` to those things which have become visible (and do a lookup to see which
      // elements are no longer visible to remove the component, etc).
    }
  }
}

In such a situation I would typically err for a lookup table of sorts to easily lookup an entity by the id you see above, but I can appreciate that building threading primitives for that may be tricky.

One idea I had whilst thinking through my use case is adding an indexed query flavour to queries. It felt like it'd fit well into the way we're currently interpreting the API in our project, whilst also aligning with becsy's thread-safe vision. One way I could see that playing out is as below - supporting hashed lookup of entities based on user-defined components:

class MySystem {
  entities = this.query(
    (q) => 
      q
        .addedOrChangedOrRemoved
        .with(IdComponent, PositionComponent, SizeComponent))
        .indexed('entitiesById', e => e.read(IdComponent).id)
  );

  execute() {
    // later on, we are able to do a quick lookup & add the `VisibleComponent` to entities
    for (const entities of entitiesWhichAreNowVisible) {
      // O(1) lookup vs O(n) for thousands of elements
      const entity = this.entities.fromIndex('entitiesById', entity);
      entity.add(VisibleComponent);
    }
  }
}

In this way, we'd keep the API surface change pretty small, whilst also fitting into the pre-existing concept of query flavours.

Another pattern distinct to above may be to allow entities in a system to mark themselves as being indexed:

const entity = this.someQuery.current[0];
const entityId = entity.read(IdComponent).id;

entity.index(entityId);

// in another tick of the game loop
const entityId = someListOfIds[0];
const entity = this.queryIndex(entityId);

In either case: Just wanted to share those ideas as I've been thinking about this a little bit in the context of my project ๐Ÿ˜„ Keen for any and all suggestions from your side independent of these of course, including if we're not wielding the existing API surface of becsy well enough / missing something which would suit the above use case.

In short: We need a way to refer to performantly refer to entities by identifiers stored in a specialised data structure.

Thanks in advance for all your help!

ChainAlert: npm package release (0.13.1) has no matching tag in this repo

Dear @lastolivegames/becsy maintainers,
Thank you for your contribution to the open-source community.

This issue was automatically created to inform you a new version (0.13.1) of @lastolivegames/becsy was published without a matching tag in this repo.

As part of our efforts to fight software supply chain attacks, we would like to verify this release is known and intended, and not a result of an unauthorized activity.

If you find this behavior legitimate, kindly close and ignore this issue. Read more

badge

Stricter types for `Query` and `QueryBuilder`?

This one is more of a directional question to ask if you'd want a contribution of this kind.

When using the query system there is a number of invariants checked during runtime, resulting in errors being thrown if violated. Some examples (as per this CodeSandbox)

world.createEntity(A);

/* ... */

const q = this.query(b => b.current.with(A).using(C));

execute = () => {
  // Query 'added' not configured ...
  this.q.added.forEach(() => {});

  this.q.current.forEach((e) => {
    // System didn't mark component B as readable
    const b = e.read(B);
    
    // System didn't mark component A as writable
    const a = e.write(A);
    
    // Entity doesn't have a C component
    const c = e.read(C);
  });
}

While learning the query system and bumping into those I figured they could be mostly covered by the type system - is this something you'd be interested to receive a contribution for?

I've given stricter types for the sub-systems at hand a first shot via this TypeScript playground example - the types are definitely not trivial so maintenance vs. value considerations might come into play.

The suggested types would bump the error site for the cases above up to compile time, e.g.

world.createEntity(A);

/* ... */

const q = this.query(b => b.current.with(A).using(C));

execute = () => {
  // Property added might be undefined
  this.q.added.forEach(() => {});

  this.q.current.forEach((e) => {
    // Argument of type 'typeof B' is not assignable to parameter of type 'typeof A'.
    const b = e.read(B);
    
    // Property 'write' does not exist on type 'ReadableEntity<typeof A>'.
    const a = e.write(A);
    
    // Not covered with types (yet)
    // const c = e.read(C);
  });
}

World.terminate() never resolves

Consider the following:

import { System, World } from '@lastolivegames/becsy';

class SystemA extends System {}

class SystemB extends System {
  constructor() {
    super();
    this.schedule((s) => s.after(SystemA));
  }
}

class SystemC extends System {
  constructor() {
    super();
    this.schedule((s) => s.after(SystemA));
  }
}

const startAndTerminate = async () => {
  console.log('Starting world');

  const world = await World.create({
    defs: [SystemA, SystemB, SystemC],
  });

  await world.terminate();

  console.log('Terminated!');
};

startAndTerminate();

The line console.log('Terminated!') will never execute as world.terminate() never resolves.

This is currently breaking our application as we need to wait for the world to terminate before creating a new world when another page is loaded.

Seems like the bug is in SimplePlan.finalize() on line 82 and 90: https://github.com/LastOliveGames/becsy/blob/d97a10632dcd7df75cf5142da0c4e89d6161fdd6/src/planner.ts#L82;L90

this.graph.traverse() can return an empty array, meaning the line if (!systems) return resolve() never gets executed causing the promise to never resolve. Checking if (!systems?.length) return resolve() instead seems like it would fix the issue.

Command pattern for Systems

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.

System support for deferring work to be done on next execute

This is a question to ask if you would consider a contribution of this kind?

Currently we have a lot of input Systems which listen for events and read/write Entities when an event is emitted (e.g. onMouseDown do x, y, z...). When an event is emitted; naively, anything done in the listener callback is done immediately and not within the systems execute which I've come to understand is undesirable.

We have been setting up ways in our Systems to store operations (for example in a field called pendingUpdates which is drained in execute) with the goal of reading/writing Entities during execute cycles only. This is relatively easy with some abstraction.

I was wondering if you'd consider supporting such a thing in Systems by default?

This could be achieved for instance by providing a new function this.doNext(fn) in the System base class which stores callbacks to be run and cleared during the next Systems execute.

Injecting dependencies into Systems

Hello,

I've been studying Becsy for a few days and it has super interesting concepts. Nice library!
But I am wondering how would you approach the need of passing dependencies into systems.
They are currently automatically constructed so one cannot define construction parameters.

One example that comes into mind is a PixiRenderSystem that takes the Pixi.application as construction parameter.
Maybe we want to inject a canvas 2d object so that we can draw gizmos as an overlay to debug our game.
I think it is a very common use case.

Does anything need to be represented as an entity+component to be accessed by a system ?

Internal error: Should commit log before counting. Please report a bug!

Hi there! First off thanks for such an awesome ECS library. I'm continually surprised by the sophistication and richness of features throughout becsy.

I noticed recently an increasing pattern of:

Internal error: Should commit log before counting. Please report a bug!

I unfortunately don't have a whole lot more to share because it's happening on a remote test machine with no apparent effect to operation and I'm unable to reproduce locally, apologies.

I was wondering if you could share some information about this error, and perhaps provide advice about how to debug the cause?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.