lastolivegames / becsy Goto Github PK
View Code? Open in Web Editor NEWA multithreaded Entity Component System (ECS) for TypeScript and JavaScript, inspired by ECSY and bitecs.
License: MIT License
A multithreaded Entity Component System (ECS) for TypeScript and JavaScript, inspired by ECSY and bitecs.
License: MIT License
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:
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.this.__dispatcher.registry.includeRecentlyDeleted
from any system, but obviously not ideal because that's internal, undocumented and might change in future versions of becsy.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!
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.
@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?
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:
...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!
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.
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!
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)?
I'm new to ecs.Why use sparse set rather than archetype?I dont know the benefit of sparse set.
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?
Hello, are you still updating?
this.query(q => q.addedOrChanged.with(DOMRenderable).and.with(Position).track);
Example demo, teypscript version reports error, cannot find attribute track
on QueryBuilder
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;
}
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!
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
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);
});
}
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.
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:
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;
}
Command
objects that extend a certain shape.
send(SomeCommand) โโโโโโโโโโโโโโโ
โ โโโโบโ SubscriberA โ
โผ โ โโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโฌโโโโโ
โ CommandService โ Fork
โโโโโโโโโโโโโโโโโโดโโโโโ
โ โโโโโโโโโโโโโโโ
โโโโบโ SubscriberB โ
โโโโโโโโโโโโโโโ
// 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:
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.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.
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.
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 ?
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?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.