Git Product home page Git Product logo

ecsy's Introduction

ecsy

NPM package Build Size Dev Dependencies Build Status

ECSY (pronounced as "eck-see") is an highly experimental Entity Component System framework implemented in javascript, aiming to be lightweight, easy to use and with good performance.

For detailed information on the architecture and API please visit the documentation page

Features

  • Framework agnostic
  • Focused on providing a simple but yet efficient API
  • Designed to avoid garbage collection as possible
  • Systems, entities and components are scoped in a world instance
  • Multiple queries per system
  • Reactive support:
    • Support for reactive behaviour on systems (React to changes on entities and components)
    • System can query mutable or immutable components
  • Predictable:
    • Systems will run on the order they were registered or based on the priority defined when registering them
    • Reactive events will not generate a random callback when emited but queued and be processed in order
  • Modern Javascript: ES6, classes, modules,...
  • Pool for components and entities

Goals

Our goal is for ECSY to be a lightweight, simple, and performant ECS library that can be easily extended and encoruages open source collaboration.

ECSY will not ship with features that bind it to a rendering engine or framework. Instead, we encourage the community to build framework specific projects like ecsy-three, ecsy-babylon, and ecsy-two.

ECSY does not adhere strictly to "pure ECS design". We focus on APIs that push users towards good ECS design like putting their logic in systems and data in components. However, we will sometimes break the rules for API ergonomics, performance in a JS context, or integration with non-ECS frameworks.

ECSY is designed for a community driven ecosystem. We encourage users to come up with modular components and systems that can be composed into larger games, apps, and engines.

Examples

Usage

Installing the package via npm:

npm install --save ecsy
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Hello!</title>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      html, body: {
        margin: 0;
        padding: 0;
      }
    </style>

    <script type="module">
      import { World, System, Component, TagComponent, Types } from "https://ecsyjs.github.io/ecsy/build/ecsy.module.js";

      const NUM_ELEMENTS = 50;
      const SPEED_MULTIPLIER = 0.3;
      const SHAPE_SIZE = 50;
      const SHAPE_HALF_SIZE = SHAPE_SIZE / 2;

      // Initialize canvas
      let canvas = document.querySelector("canvas");
      let canvasWidth = canvas.width = window.innerWidth;
      let canvasHeight = canvas.height = window.innerHeight;
      let ctx = canvas.getContext("2d");

      //----------------------
      // Components
      //----------------------

      // Velocity component
      class Velocity extends Component {}

      Velocity.schema = {
        x: { type: Types.Number },
        y: { type: Types.Number }
      };

      // Position component
      class Position extends Component {}

      Position.schema = {
        x: { type: Types.Number },
        y: { type: Types.Number }
      };

      // Shape component
      class Shape extends Component {}

      Shape.schema = {
        primitive: { type: Types.String, default: 'box' }
      };

      // Renderable component
      class Renderable extends TagComponent {}

      //----------------------
      // Systems
      //----------------------

      // MovableSystem
      class MovableSystem extends System {
        // This method will get called on every frame by default
        execute(delta, time) {
          // Iterate through all the entities on the query
          this.queries.moving.results.forEach(entity => {
            var velocity = entity.getComponent(Velocity);
            var position = entity.getMutableComponent(Position);
            position.x += velocity.x * delta;
            position.y += velocity.y * delta;

            if (position.x > canvasWidth + SHAPE_HALF_SIZE) position.x = - SHAPE_HALF_SIZE;
            if (position.x < - SHAPE_HALF_SIZE) position.x = canvasWidth + SHAPE_HALF_SIZE;
            if (position.y > canvasHeight + SHAPE_HALF_SIZE) position.y = - SHAPE_HALF_SIZE;
            if (position.y < - SHAPE_HALF_SIZE) position.y = canvasHeight + SHAPE_HALF_SIZE;
          });
        }
      }

      // Define a query of entities that have "Velocity" and "Position" components
      MovableSystem.queries = {
        moving: {
          components: [Velocity, Position]
        }
      }

      // RendererSystem
      class RendererSystem extends System {
        // This method will get called on every frame by default
        execute(delta, time) {

          ctx.fillStyle = "#d4d4d4";
          ctx.fillRect(0, 0, canvasWidth, canvasHeight);

          // Iterate through all the entities on the query
          this.queries.renderables.results.forEach(entity => {
            var shape = entity.getComponent(Shape);
            var position = entity.getComponent(Position);
            if (shape.primitive === 'box') {
              this.drawBox(position);
            } else {
              this.drawCircle(position);
            }
          });
        }

        drawCircle(position) {
          ctx.beginPath();
          ctx.arc(position.x, position.y, SHAPE_HALF_SIZE, 0, 2 * Math.PI, false);
          ctx.fillStyle= "#39c495";
          ctx.fill();
          ctx.lineWidth = 2;
          ctx.strokeStyle = "#0b845b";
          ctx.stroke();
        }

        drawBox(position) {
          ctx.beginPath();
          ctx.rect(position.x - SHAPE_HALF_SIZE, position.y - SHAPE_HALF_SIZE, SHAPE_SIZE, SHAPE_SIZE);
          ctx.fillStyle= "#e2736e";
          ctx.fill();
          ctx.lineWidth = 2;
          ctx.strokeStyle = "#b74843";
          ctx.stroke();
        }
      }

      // Define a query of entities that have "Renderable" and "Shape" components
      RendererSystem.queries = {
        renderables: { components: [Renderable, Shape] }
      }

      // Create world and register the components and systems on it
      var world = new World();
      world
        .registerComponent(Velocity)
        .registerComponent(Position)
        .registerComponent(Shape)
        .registerComponent(Renderable)
        .registerSystem(MovableSystem)
        .registerSystem(RendererSystem);

      // Some helper functions when creating the components
      function getRandomVelocity() {
        return {
          x: SPEED_MULTIPLIER * (2 * Math.random() - 1),
          y: SPEED_MULTIPLIER * (2 * Math.random() - 1)
        };
      }

      function getRandomPosition() {
        return {
          x: Math.random() * canvasWidth,
          y: Math.random() * canvasHeight
        };
      }

      function getRandomShape() {
         return {
           primitive: Math.random() >= 0.5 ? 'circle' : 'box'
         };
      }

      for (let i = 0; i < NUM_ELEMENTS; i++) {
        world
          .createEntity()
          .addComponent(Velocity, getRandomVelocity())
          .addComponent(Shape, getRandomShape())
          .addComponent(Position, getRandomPosition())
          .addComponent(Renderable)
      }

      // Run!
      function run() {
        // Compute delta and elapsed time
        var time = performance.now();
        var delta = time - lastTime;

        // Run all the systems
        world.execute(delta, time);

        lastTime = time;
        requestAnimationFrame(run);
      }

      var lastTime = performance.now();
      run();
    </script>
  </head>
  <body>
    <canvas width="500" height="500"></canvas>
  </body>
</html>

Try it on glitch

You can also include the hosted javascript directly on your HTML:

<!-- Using UMD (It will expose a global ECSY namespace) -->
<script src="https://ecsyjs.github.io/ecsy/build/ecsy.js"></script>

<!-- Using ES6 modules -->
<script src="https://ecsyjs.github.io/ecsy/build/ecsy.module.js"></script>

ecsy's People

Contributors

aveyder avatar briancw avatar brianpeiris avatar crabmusket avatar dannyfritz avatar davidpeicho avatar deltakosh avatar dependabot[bot] avatar endel avatar fernandojsg avatar fostuk avatar ifnotfr avatar infinitelee avatar johnshaughnessy avatar joshmarinacci avatar kieirra avatar lalalune avatar luizbills avatar nebual avatar netpro2k avatar pirelenito avatar robertlong avatar sheepsteak avatar simonihmig avatar totallyronja avatar wingyplus avatar zeddic 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  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

ecsy's Issues

Object Pooling and Component Managers

During our internal hackathon I noticed that most people were running into some confusion around the entity.addComponent() API.

The current signature is this:

class World {
  registerComponent<C extends Component>(component: ComponentConstructor<C>): this
}

interface Component {
  copy(source: Component): this // Optional
}

class Entity {
  addComponent(component: ComponentConstructor, parameters: Component | {}): this
}

Where the parameters are copied to the instance of the component acquired from the object pool. In the case that it is an instance of Component with a copy() method it uses that otherwise it just does a shallow copy of the object.

I'd like to propose moving to this:

class World {
  registerComponent<C extends Component>(component: ComponentConstructor<C>, componentManager?: ComponentManager<C>): this
}

class ComponentManager<C extends Component> {
  components: C[]
  get(entityId: number): C
  add(entityId: number): C
  has(entityId: number): boolean
  remove(entityId: number): void
}

class Component<T extends Component> {
  clone(): T {
    return new this.constructor().copy(this);
  }
  copy(source: T): this {
    for (const key in source) {
      if (this.hasOwnProperty(key)) {
        const destValue = this[key];
        const srcValue = source[key];
        if (destValue && destValue.copy && srcValue && srcValue.copy) {
          destValue.copy(srcValue);
        } else {
          this[key] = source[key];
        }
      }
    }
  }
}

class Entity {
  addComponent<C extends Component>(component: ComponentConstructor<C>, instance?: C): C
}

The differences are the following:

  1. addComponent() does not implement pooling by default. I think most of us had issues with the default pooling behavior. The shallow copy doesn't work for non-value types like Matrix4, Vector3, etc. which produces undesirable results. I'll let others comment on their experiences with object pooling, but I think the Hubs team was generally in favor of making object pooling a higher level concept and not turned on by default.
  2. addComponent() returns the component instance. In my experience this is more common than adding multiple components to an entity which is why we currently return the instance of the entity rather than the component.
  3. registerComponent() takes an optional ComponentManager which allows you to implement pooling yourself on a per component basis. You can also store your components with different backing data structures such as Map or a BitSet or TypedArray. The default should probably just use a Map or Array I did some benchmarks in HECS that you can look at to determine the best default.
  4. Add clone() to Component. I think it's good to allow people to add arguments to the component constructor. However, this makes it tough to clone components in editors like Spoke. Adding the clone() method lets you specify those constructor parameters or override how the component is instantiated when cloned.
  5. Move Component from an interface to a class with default implementations of clone and copy. These implementations support ThreeJS classes such as Vector3, Matrix4, etc.

Deferred removal could lead to side effect if adding a component on the same tick

If we take the factory example as reference, imagine we remove (deferred) the Name component when clicking the button https://github.com/fernandojsg/ecsy/blob/master/examples/factory/index.html#L108

It will just enter on the next tick because of the Not operator on the name system and it will add the Name component to it, removing it from the original query: https://github.com/fernandojsg/ecsy/blob/master/examples/factory/index.html#L49-L65

The problem is that, that component won't get added correctly as the deferral removal has not been executed yet, so in this line https://github.com/fernandojsg/ecsy/blob/master/src/EntityManager.js#L47 the _ComponentTypes for that entity will still have Name on it, so it will just return.

After that, the deferral removal will come into action and will do the actual removal of the Name component on the entity... resulting in adding that entity again to the Not(Name) query. And so on the next frame, it will get executed and end up there, so we are executing the call twice.

At first I thought about just doing an extra check when adding the component to see if it's on the "to remove" state so we could just go ahead and add the new component. The problem is that when you will get reading the getComponent() on the onRemove query on your system, you will access to the new one, and not the removed one, that it's probably the one you are interested in.

We already have a componentsToRemove attribute on Entity, so I'm thinking about just move the removed components there, so getComponent() will return always a valid (not mark for removal) component, and we could introduce a getRemovedComponent() so people could use it on the onRemoveEvent queries to access the removed component even if the entity has a new alive one already. With that we could also avoid strange side effects usages like calling entity.getMutableComponent() on an marked for remove component.

Helper to create custom type of "array of a custom type"

Currently you can create a new type by calling: createType(), and we have support for the basic type Array.
But what happens if you want to have an array of a custom type?
Currently you could just go ahead and implement a custom type eg: (https://github.com/fernandojsg/ecsy/blob/master/test/unit/createcomponent.test.js#L138-L170)

var Vector3Array = createType({
    create: defaultValue => {
      var v = [];
      if (typeof defaultValue !== "undefined") {
        for (var i = 0; i < defaultValue.length; i++) {
          var value = defaultValue[i];
          v.push(new Vector3(value.x, value.y, value.z));
        }
      }
      return v;
    },
    reset: (src, key, defaultValue) => {
      if (typeof defaultValue !== "undefined") {
        for (var i = 0; i < defaultValue.length; i++) {
          if (i < src[key].length) {
            src[key][i].copy(defaultValue[i]);
          } else {
            var value = defaultValue[i];
            src[key].push(new Vector3(value.x, value.y, value.z));
          }
        }

        // Remove if the number of elements on the default value is lower than the current value
        var diff = src[key].length - defaultValue.length;
        src[key].splice(defaultValue.length - diff + 1, diff);
      } else {
        src[key].length = 0;
      }
    },
    clear: (src, key) => {
      src[key].length = 0;
    }
  });

Or we could have a helper function as:

createArrayType(Vector3)

And it will generate a type definition based similar to the one pasted above based on the Vector3 type.

Separate between init() and config()/queries()/events()

Currently init on Systems is used to initialize the systems and returning the list of queries and events:

export class DemoSystem extends System {
  init() {
    //... init code

    return {
      queries: {
        entities: { components: [ComponentA] }
      },
      events: {
        eventA: "eventA"
      }
    };
  }
}

I believe it could be nice if we divide it into functions or static variables, eg:

export class DemoSystem extends System {
  init() {
    //... init code
  }

  config() {
    return {
      queries: {
        entities: { components: [ComponentA] }
      },
      events: {
        eventA: "eventA"
      }
    };
  }
}

Function Systems

Just an idea, not sure if it's a good one yet. If systems are supposed to be stateless, it may be a good idea to make them a function.

function RotateObjectsSystem(world, queries) {
  const { dt } = queries.clock[0].getComponent(Clock);

  queries.entities.forEach((entity) => {
    
    const rotateX = entity.getComponent(RotateX);
    const transform = entity.getComponent(Transform);

    transform.rotation.x += rotate.speed * dt;
  });
}

RotateObjectsSystem.query = {
  clock: { components: [Clock]},
  entities: { components: [Transform, RotateX] }
};

Making world an entity itself or have an entity instance on it

So we could get rid of singletons for example (https://github.com/fernandojsg/ecsy/issues/30)
Some thoughts:

  • Extend World from Entity seems cool in the way that we could do world.getComponent(Renderer) to get a "singleton component". The problem is that it also inherit all the function from entity, as reset, remove that would be misleading
  • Creating a property directly on World, so you could access it as world.entity.getComponent, or world.singletonEntity.getComponent ? could be another option. We could even implement the bypass functions getComponent, addComponent and so at the world level, just for the sake of avoiding that level of indirection, so users could still be using world.getComponent.
  • Should we keep world.registerSingletonComponent(ComponentA) as a helper for world.entity.addComponent(ComponentA) or just add directly the component to the world and that's all?
  • With this we will be able to access these singleton components in two ways:
    explicitly
execute() {
  this.world.getComponent(Renderer).render();
}

Using queries:

class SystemA extend System
  init() {
    return {
      queries: {
        renderer: { components: [Renderer], single: true, mandatory: true }
      }
    };
  }

  execute(delta) {
    this.queries.renderer.render();
  }
}

Include documentation on the docs folder too?

I remember on aframe we had the documentation as a .md and generates that pretty website. Do you have any suggestion how to include the documentation as getting started, core components and things like that along with the autogenerated docs from the code?
/cc @brianpeiris

create ecsy-three repo

Eventually the three bindings will move to the Three org, so we should create a separate repo now.

initilizeLibrary?

Similar to registerDefaultSystems (https://github.com/fernandojsg/ecsy/issues/67), in this case this exported function will take care of everything needed to get the library to a ready to go state, it will call internally to registerDefaultSystems but also will create entities and add components needed to get the initial setup for you.

Move queries to System static property

I'd like to propose moving the queries object from the return value of the init() function to a static queries property on the System class. This change will make it possible to know what dependencies a system has before it is instantiated which is important for writing system schedulers.

Ex.

class MySystem extends System {
  static queries = {
    foo: { components: [ComponentA, ComponentB] }
    bar: { components: [ComponentB, ComponentC] }
  };
  init() {}
  execute() {}
}

Or without static property support (currently unsupported without babel / typescript)

class MySystem extends System {
  init() {}
  execute() {}
}

MySystem.queries = {
  foo: { components: [ComponentA, ComponentB] }
  bar: { components: [ComponentB, ComponentC] }
};

This proposal is similar to the syntax of React's propTypes or defaultProps.

An alternate proposal could be introducing a static query() method which returns the queries for the system.

Ex.

class MySystem extends System {
  static query() {
    return {
      foo: { components: [ComponentA, ComponentB] }
      bar: { components: [ComponentB, ComponentC] }
    };
  }
  init() {}
  execute() {}
}

I think this should be avoided since the queries object should not change and could be referenced multiple times in the scheduler which would allocate additional objects unnecessarily. One benefit is that static class methods are currently supported in evergreen browsers without transpilation. However, React has made the decision to go forward with this syntax for propTypes and defaultProps and I think we should as well.

One more proposal would be to do nothing to the syntax other than change examples to set queries in the constructor or with a class property:

Ex.

class MySystem extends System {
  constructor(world) {
    super(world);

    this.queries = {
      foo: { components: [ComponentA, ComponentB] }
      bar: { components: [ComponentB, ComponentC] }
    };
  }
  init() {}
  execute() {}
}

Class properties have better support in current browsers. See the Class Fields Proposal.

class MySystem extends System {
  queries = {
    return {
      foo: { components: [ComponentA, ComponentB] }
      bar: { components: [ComponentB, ComponentC] }
    };
  }
  init() {}
  execute() {}
}

A scheduler would then be assumed to operate after a system has been registered with the World (which calls the system's constructor). Custom schedulers could then be implemented by subclassing SystemManager and providing a custom SystemManager when constructing the World.

Asynchronous init on systems

Currently every system is expected to have a synchronous init(), but some systems could be async and it could be nice to have some examples and best practices around it

Dynamic queries

During our last call we were discussing around if queries could be dynamic or should be static. We didn't come up with any use case for dynamic queries (although currently you could create queries dynamically).

Thinking on the way queries are handled, every time you create a new query, you are traversing the list of components/entities to check if they match the query, to build the initial set of entities for that query, and after that you just wait for events to happen to add or remove the entities from that list.

The problem is that the initial traversing when creating the entity could be expensive depending on the size of your world, so overall I don't think it's a good pattern to do that dynamically.
For that reason, I'll just close this issue right now and reopen if we find a specific use case that really needs this feature. I just wanted to have it documented for further discussions or to point people asking for this feature.

Deferred Remove component on system

Imagine we have a system that remove a component for an object on tick:

export class CollisionSystem extends System {
  init() {
    return {
      entities: [Colliding]
    };
  }

  execute(delta) {
    let entities = this.queries.entities;
    for (let i = 0; i < entities.length; i++) {
      let entity = entities[i];
      entity.removeComponent(Colliding);
    }
  }
}

This has two problems:

  • It could break the loop
  • If other systems need that Colliding component on the same tick they won't be able to access to that entity

Ideas:

  • Remove will be always deferred by default
  • A System will be always executed at the end of the systems' tick and it will remove all the flagged components.
  • Optionally we could still have a forceRemove to remove at the same time. But we should find a use case for this before allow it. #4

Add SystemStateComponents

Based on the Unity ECS implementation: https://docs.unity3d.com/Packages/[email protected]/manual/system_state_components.html

TL;DR State Components are components used by a system to hold internal resources for an entity, they are not removed when you delete the entity, you must remove them from your system once you are done with them. It lets you detect Add/Remove events without callback, eg:

class MySystem extends System {
  init() {
    return {
      queries: {
        added: { components: [ComponentA, Not(StateComponentA)] },
        remove: { components: [Not(ComponentA), StateComponentA] },
        normal: { components: [ComponentA, StateComponentA] },
      }
    };
  },
  execute() {
    added.forEach(entity => {
      entity.addStateComponent(StateComponentA, {data});
    });

    remove.forEach(entity => {
      var component = entity.getStateComponent(StateComponentA);
      // free resources for `component`
      entity.removeStateComponent(StateComponentA);
    });

    normal.forEach(entity => {
      // use entity and its components
    });
  }
}

Syntax for queries and events

Currently the syntax for queries and its events is the following:

{
  queries: {
    entities: {
      components: [Rotating, Transform]
      events: {
        added: {
          event: "EntityAdded"
        },
        removed: {
          event: "EntityRemoved"
        },
        changed: {
          event: "EntityChanged"
        },
        rotatingChanged: {
          event: "ComponentChanged",
          components: [Rotating]
        },
        transformChanged: {
          event: "ComponentChanged",
          components: [Transform]
        }
      }
    }
  }
}```

And the way to access the entities on the `execute` method is:
```javascript
execute() {
  // Queries
  this.queries.entities.forEach(entity => {})

  // Events
  this.events.entities.added.forEach(entity => {})
  this.events.entities.removed.forEach(entity => {})
  this.events.entities.changed.forEach(entity => {})
  this.events.entities.rotatingChanged.forEach(entity => {})
  this.events.entities.transformChanged.forEach(entity => {})
}

I'd like to get feedback on using a common path this.queries.entities.* for both type of queries. Something like:

execute() {
  // Queries
  this.queries.entities.results.forEach(entity => {})

  // Events
  this.queries.entities.events.added.forEach(entity => {})
  this.queries.entities.events.removed.forEach(entity => {})
  this.queries.entities.events.changed.forEach(entity => {})
  this.queries.entities.events.rotatingChanged.forEach(entity => {})
  this.queries.entities.events.transformChanged.forEach(entity => {})
}

Initialize singleton components on system registration

Many times we need to store data related to systems internally, a good practice to avoid storing data on the systems could be to use singleton components and add some syntactic sugar for that:

If we create a system in this way:

world.registerSystem(SystemA, {valueA: 1, valueB: 2});

it will create a singleton component named systemA (As if we had previously created a SystemA component with valueA and valueB attributes).
And we could access the component from within the system by referencing this.component:

execute() {
    this.component.valueA += 2;
}

A-Frame style declarative abstraction layer

Hi,

Exciting project!

I really like how you're making this lib reactive and framework agnostic off the bat. It occurs to me though for folks wishing to transition to this from A-Frame it could help to have a declarative abstraction layer over it?

I've been working on an experimental build system for A-Frame using TypeScript, Webpack, and custom elements v1: https://github.com/edsilv/aframe-ts-webpack

Perhaps this could consume ecsy to provide all of the underlying logic, maintaining separation of concerns and giving A-Frame users a way to pick it up quickly?

Get a helper for single value components?

I find myself using a lot of components with a single attribute as for example:

class Scene {
  constructor() {
    this.scene = null;
  }
}

class Parent {
  constructor() {
    this.parent = null;
  }
}

and then I need to do:

entity.getComponent(Scene).scene;
entity.getComponent(Parent).parent;

sometimes I struggle with the naming as is a bit redundant Scene and scene so I was thinking if it could be a good practice to call these single attribute components something like value?

entity.getComponent(Scene).value;
entity.getComponent(Parent).value;

warning: Probably too much level of syntactic sugar here :)
And we could just include a helper like getComponentValue()

entity.getComponentValue(Scene);
entity.getComponentValue(Parent);

awesome-components

There should be a list of 3rd party components people might be interested in. This could be as simple as a markdown doc in the documentation.

Allow forceRemoveComponent

On #3 I exposed the need of deferred remove component method. I opened this one to keep track of possible use cases where we will need to remove right away the component instead of waiting to the end of the whole tick to do it.

Introduce "READ" and "WRITE" modifiers on queries

So we could define if we are going to get mutable or immutable components so we could implement more advanced schedulers using workers for example.

queries: {
  bullets: {components: [Write(Position), Speed]}
}

Read could be implicit if not defined, though

New syntax for events on queries

Currently the syntax for queries is:

{
  balls: {
    components: [Ball, Transform, Rotating],
    events: {
      added: {
        event: "EntityAdded"
      },
      removed: {
        event: "EntityRemoved"
      },
      changed: {
        event: "EntityChanged"
      },
      rotatingChanged: {
        event: "ComponentChanged",
        components: [Rotating]
      },
      transformChanged: {
        event: "ComponentChanged",
        components: [Transform]
      }
    }
  }
}

In order to simplify the way to define that the query will react to events when an entity is added, removed or changed, we defined the following proposal:

{
  queries: {
    balls: {
      components: [Ball, Transform, Rotating],
      added: true,
      removed: true,
      changed: [Any, Ball, Rotating]
    }
  }
}

Where:

  • added: Is a boolean, false by default, that will determine if the query will store a queue of entities added to the query.
  • removed: as added but when an entity has been removed from the query.
  • changed:
    • If true is defined: It will listen for any entity change (= any component on that entity that is part of the query changes).
    • If a list is defined, it will listen if any of the components of the list has changed.

This will help simplify the access to the entity lists as discussed on https://github.com/fernandojsg/ecsy/issues/70 from:

queries.balls
events.balls.added
events.balls.removed
events.balls.changed
events.balls.transformChanged
events.balls.rotatingChanged

to

queries.balls.results
queries.balls.added
queries.balls.removed
queries.balls.changed.Transform
queries.balls.changed.Rotating

Question: who should add the "entities" to the scene?

Hi @fernandojsg, really nice API you got there! Thanks for sharing this.

Following ECS and other design patterns, I'm wondering who's the responsibility of adding an entity to the scene, into the right container? A "controller"? ๐Ÿ‘€

Speaking of THREE.js, an Object3D should be added (.add()) somewhere. Maybe in the root scene, maybe as a child in another object.

Do you have an opinion about this?
Thanks!

Add "single" modifier to the queries

Add a simple syntactic sugar so you can define a single modifier on the queries to define that the query should expect just one item out of that query (as a way to replace singleton too).
Currently it could be just a prettier way to access that entity instead of doing this.queries.renderer[0], but we can imagine that in the future it could help doing some optimizations for this kind of queries in under the hood if needed.
Also together with https://github.com/fernandojsg/ecsy/issues/28 will make using the singleton concept easier.

class TestSystem extends System {
  init() {
    return {
      queries: {
        meshes: { components: [Mesh] }
        renderer: { components: [RendererComponent], single: true }
      }
    };
  }

  execute(delta) {
    let meshes = this.queries.meshes;
    meshes.forEach(mesh => {
      this.queries.renderer.renderMesh(mesh);
    });
  }
}

world.getSystem()

You can create a system with world.registerSystem() so you should also be able to get a reference to the created instance with world.getSystem()

Implement filters on queries

Allow users to define a filter for the queries could be useful to filter by component's' values easily.
The drawback of this is that filter will create a new instance of the array, and the events (onComponentAdded, onComponentChanged,...) should also be modified by the filter

class SystemB extends System {
  init() {
    return {
      queries: {
        entities: {
          components: [FooComponent],
          filter: entity => entity.getComponent(FooComponent).value > 2
        }
      }
    };
  }
  execute() {}
}

Define order when running systems

It should be defined when the system is added to a ECS context/instance.

  • By default the systems will be executed on the order they were added:
world.registerSystem(SystemA);
world.registerSystem(SystemB);
  • You could specify an orderId (Similar to z-index), by default every system has orderId = 0. In case you want to add a system to be executed always at the beginning or at the end no matter how many systems you will be adding later.
world.registerSystem(SystemA, {order: 0});
world.registerSystem(SystemB, {order: -1});
  • In the future we could support order dependencies between systems:
world.registerSystem(SystemA, {before: [systemC]});
...
world.registerSystem(SystemD, {after: [systemE, systemC]});
...

registerDefaultSystems

We expect that sometimes libraries should register some internal systems in order to work properly, and it could be tedious for the user to need to import all of them and register them.
So it could be nice to export a registerDefaultSystems(world) from the libraries that to handle that for you.
In any case, you could still skip using that and import the systems and do that by yourself

Add "mandatory" attribute on the queries

Basically if the query.length === 0 it won't call execute

class TestSystem extends System {
  init() {
    return {
      queries: {
        renderer: { components: [RendererComponent], mandatory: true }
      }
    };
  }

  execute(delta) {
    // Safely because we know query will have at least one element
    this.queries.renderer[0].doWhatever(); 
  }
}

world.createComponent to create components without adding them to an entity

Currently is possible to add a component by passing the component class and an object to replace the default values:

entity.addComponent(ComponentA);
entity.addComponent(ComponentB, {value: 23, other: 'test'});

It would be interesting to be able to request a component without adding it to an entity automatically:

var component = world.createComponent(ComponentB);
component.value = 23;
component.other = 'test';

entity.addComponent(component);

// You could still do this the "old" way
entity.addComponent(ComponentA); // Just some sugar to detect the component

Extend component query behaviour: AND, OR, NOT

Currently the query on the systems will look for entities with all the components on the list.

[Transform, Rotating] => Transform AND Rotating

We should be able to modify the query to do things like:

[ [Transform, Rotating], [Scale], [NOT(Transform), PulsatingColor]]
=> (Transform AND Rotating) OR (Scale) OR (!Transform AND PulsatingColor)

When adding a new component return it instead of the entity itself

Currently addComponent return the entity instead of the component, so you could chain them as:

entity.addComponent(ComponentA).addComponent(ComponentB);

But if you want to set component values directly without passing an object on the addComponent function, it could be nice to return the component directly:

let componentA = entity.addComponent(ComponentA);
componentA.value = 23;

One question would be if we would like that behaviour to be the same as if we would do getMutableComponent so it will trigger the changed. But as we are just creating it, I believe that just triggering the added event should be enough for now.

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.