Jump to content

makr.js — An Entity-Component-System engine


ooflorent
 Share

Recommended Posts

Hi everyone,

 

A few weeks ago Ezelia started a thread about ECS and HTML5 games. As a big fan of this pattern, I decided to build and release my own engine.

It is called makr.js and is available on Github (https://github.com/ooflorent/makrjs) under MIT license.

 

A basic sample is available into examples/

The library must be build using grunt in order to test the sample.

 

Any feedback would be appreciated!

 

Update (2015-01-06):

Version 2 is under active development (more information)

Feedback is welcome!

Link to comment
Share on other sites

Some feedback:

var typeCounter = 0;var TYPE_CLOCK = typeCounter++;var TYPE_POSITION = typeCounter++;var TYPE_VELOCITY = typeCounter++;var TYPE_RADIUS = typeCounter++;var TYPE_COLOR = typeCounter++;

I don't like that you have to manage component types manually. This could be done by the library and prevent potential user errors.

 

Maybe something like:

world.registerComponent(Point);var ball = world.create();ball.add(new Point(5, 10)); //And in your systems:this.registerComponent(Point);

I suppose passing around the constructor function might have performance implications, but it looks nicer to use. Or you could do a hybrid and pass the constructor function to the library, which will then return a library-generated type ID so the user doesn't have to use a type counter.

 

 

Other than that it looks pretty good. What are your future plans for makr.js?

I recently changed whirlibulf from a game engine to a basic library similar to yours.

Link to comment
Share on other sites

I don't like that you have to manage component types manually. This could be done by the library and prevent potential user errors.

 

I don't like this too but:

  • Calling component.constructor (or component.constructor.name) is expensive
  • Retrieving the component internal identifier using a hash is also expensive

 

Or you could do a hybrid and pass the constructor function to the library, which will then return a library-generated type ID so the user doesn't have to use a type counter.

 

That's the best option. At the moment such class is not present in the framework. I'll add one!

 

What are your future plans for makr.js?

 

Currently I'm working on tests and benchmarks (integrating AshJS, ArtemisTS & co with grunt-benchmark is really tedious).

Next step will probably be component-pools, project site and then framework integrations (such as Phaser).

 

If someone has feature requests, my hears are all opened!

Link to comment
Share on other sites

One thing that looks a bit weird to me is this:
 

CollisionSystem.prototype.onBegin = function() {  this._width = document.body.clientWidth;  this._height = document.body.clientHeight;  BallManager.setDimensions(this._width, this._height);};

Why are you referencing a global object inside the system? That doesn't seem "nice". Maybe?

Link to comment
Share on other sites

Benchmark results against Ash:

Running suite Create 1 entity [benchmark/entities-create-1.js]...>> Ash x 315,288 ops/sec ±8.77% (48 runs sampled)>> makr (empty entity pool) x 1,330,425 ops/sec ±3.29% (74 runs sampled)>> makr (full entity pool) x 1,323,492 ops/sec ±0.84% (79 runs sampled)Fastest test is makr (full entity pool) at 0.99x faster than makr (empty entity pool)Running suite Create 200 entities [benchmark/entities-create-200.js]...>> Ash x 1,437 ops/sec ±3.37% (40 runs sampled)>> makr (empty entity pool) x 8,651 ops/sec ±2.44% (27 runs sampled)>> makr (full entity pool) x 8,771 ops/sec ±1.30% (14 runs sampled)Fastest tests are makr (full entity pool),makr (empty entity pool) at 1.01x faster than makr (empty entity pool)Running suite Add 200 entities to 3 systems [benchmark/systems-add-200.js]...>> Ash x 61.38 ops/sec ±1.47% (46 runs sampled)>> Ash (after creation) x 32.76 ops/sec ±1.12% (35 runs sampled)>> makr x 63.89 ops/sec ±3.12% (14 runs sampled)Fastest test is makr at 1.04x faster than AshRunning suite Kill 200 entities added to 3 systems [benchmark/systems-kill-200.js]...>> Ash x 7,900 ops/sec ±2.33% (71 runs sampled)>> makr x 391,082 ops/sec ±5.12% (74 runs sampled)Fastest test is makr at 49.5x faster than AshRunning suite Update 200 entities registered in 3 systems [benchmark/systems-update-200.js]...>> Ash x 99,057 ops/sec ±1.18% (71 runs sampled)>> makr x 575,604 ops/sec ±0.90% (95 runs sampled)Fastest test is makr at 5.8x faster than Ash

I have updated the repository to contain the benchmark suite.

Based on whirlibulf and mrspeaker impressions, I'll rework the provided example to something cleaner.

I'll keep you posted!

Link to comment
Share on other sites

Hey I just had another (veryyy brief) look at your code - and I think I like it. I've used Ash for a few small things, but it felt like there was too much magic going on and it didn't feel really "JavaScripty". When I get some time this week I'm going to dive further into it and test out an example game. I notice you don't have an event system built into it - What's the architectural reason for avoiding this, and what do you do instead?

 

One thing that I never figured out with Ash (and this is more of ECS problem, than a framework problem) was how to do entity-to-map collision and resolution (in a 2D grid-based game). If you're doing it imperatively then it's easy: pass the whole map to the entity's "move" function. If you try to move somewhere but it intersects, then only move part of the way so you are snug up against the edge. Repeat for X and Y axis so you can slide along the walls.

 

But with an ECS this seems, well, not ECS-ish. Do you have any thoughts on how you would handle (or have handled) this?

Link to comment
Share on other sites

There is no event system into makr.js because it does not need it. I suggest you to dive the code to understand why. However, if you need an event system, you can add one inside your game.

 

As for map collisions, you can pass the map to a dedicated system and then test collision for eligible entities.

Link to comment
Share on other sites

Currently I'm working on tests and benchmarks (integrating AshJS, ArtemisTS & co with grunt-benchmark is really tedious).

Next step will probably be component-pools, project site and then framework integrations (such as Phaser).

 

That's something I'll try for sure when it's done. :P

Link to comment
Share on other sites

I'm officially loving makr.js. The code is so straightforward: no magic (well, limited magic that makes sense). I was always annoyed at the inclusion of elaborate event systems that seem to add so much unnecessary complexity.

 

I'm going to try rebuilding an example out of my mini-engine-library-thing to make sure it'll work "in the real world" but my tests this morning leave me pretty confident that I can do anything with it, and it'll sit really well with my way of working.

 

Fantastic stuff.

Link to comment
Share on other sites

I was trying to figure out your code for "floor thirteen" (it's a bit 13kb-ed ;)) how you manage the draw order: how do you ensure the TileMap is rendered first, and the entities correctly overlap the tiles?

 

In my current system I have a "camera" object that renders everything like this:

camera.render([

    map,

    pickups,

    player,

    enemies,

    particles

])

 

The order of the entities is the draw order. If any of the things passed to render are an array, then I sort them first (for depth sorting). But "map" is always drawn first. 

 

How do you make sure the map is "on the ground"?

Link to comment
Share on other sites

As I like the idea of an efficient & flexible ECS while currently tinkering around - I've got a few questions:

 

 

Is there a particular reason or advantage of refering component properties with an additional function call ?

Couldn't you just access the object and its values via "this.Position" ?

 

Althought some components are merely container for simple data, other provide additional methods used in system iterations.

As multiple instance of an entity with the same set of components are created, wouldn't it be better to assign these functions just to the prototype and re-use the definition ?

 

Could you find a proper solution for inter-system/component communication, e.g. inserting another iterating system just at the end of the current loop ? Previous solutions included an additional observer, which got used as messenger to send messages and trigger calls.

Link to comment
Share on other sites

Is there a particular reason or advantage of refering component properties with an additional function call ?

Couldn't you just access the object and its values via "this.Position" ?

 

There are a few reasons justifying this. When I designed makr.js I wanted to build a fast ECS engine with a low memory footprint. Internally, component types are stored as integers. What about the following design choices:

function addComponent(component) {  this._components[component.constructor.name] = component;}

This is what I used for my JS13K entry. The main issue here is caused by JS-minification.

// Before minification// -------------------function Position1(x, y) { /* ... */ }game.Position2 = function(x, y) { /* ... */ }game.Position3 = function Position(x, y) { /* ... */ }console.log(Position1.name); // "Position1"console.log(Position2.name); // undefinedconsole.log(Position3.name); // "Position3"// After minification (using uglify)// ---------------------------------console.log(Position1.name); // "A" (Position1 will be hoisted)console.log(Position2.name); // undefinedconsole.log(Position3.name); // undefined << major problem

Moreover it is possible to have collision between component names.

 

Another solution is to use the constructor property.

function addComponent(component) {  // _components is created by Object.create(null) or {}  this._components[component.constructor] = component;  // Or using ES6 Map  this._componentMap.set(component.constructor, component);}

While Harmony Map is a great to JS, it is badly supported by browsers. Polyfills are slow and consume a lot of memory.

So what about traditional hash? component.constructor will be translated to a string. This string is the function code. The bigger your component constructor is, the longer the string will be. Moreover, string comparison don't perform as fast as integer ones.

 

Here is how entity.[component-name] could be implemented:

function addComponent(component, componentName) {  // Before doing that we need to check is componentName will not override an Entity method  this[componentName] = component;}

There are 3 issues here:

  • Accessing a property using brackets is slow
  • componentName check could be expensive
  • Each time you are adding a new property to the entity a new hidden class is created. This is a performance killer! (v8)

This is why I came up the integer solution.

function addComponent(component, componentType) {  // _components is an array  this._components[componentType] = component;}

This is fast but managing component types could be really tedious. In the samples I created a ComponentRegistry to store component types. While this is more user-friendly than constants is don't really like this object. Personally I use constants:

var __typeCounter = 0;var TYPE_POSITION = __typeCounter++;var TYPE_VELOCITY = __typeCounter++;var TYPE_DISPLAY  = __typeCounter++;

Doing that makes ComponentRegistry useless and also removes the additional function call  ;)

 

Althought some components are merely container for simple data, other provide additional methods used in system iterations.

As multiple instance of an entity with the same set of components are created, wouldn't it be better to assign these functions just to the prototype and re-use the definition ?

 

Templates are a good thing and are simple to implement without modifying makr.js. In my opinion this is user-land responsibility.

 

Could you find a proper solution for inter-system/component communication, e.g. inserting another iterating system just at the end of the current loop ? Previous solutions included an additional observer, which got used as messenger to send messages and trigger calls.

 

Once again, this is user-land responsibility. Such system is not complex to build and to integrate into a game done with makr.js.

Link to comment
Share on other sites

in the ECS I'm building I used @Autrac idea making component accessible throught properties.
I also studied the possibilities you speak about above and finaly decided that component property name will be the resposibility of developer.

I added a possibility to give a custom name.

here is what a component looks like

 

TypeScript version :
 

class CPosition implements IComponent {    static __label__ = 'pos';    constructor(        public x: number = 0,        public y: number = 0,        public z: number = 0        ) { }}

 

 

JS version

    function CPosition(x, y, z) {        this.x = x;        this.y = y;        this.z = z;    }    CPosition.__label__ = 'pos';




if __label__ is defined then it'll be the property name used to access the component, otherwise the constructor name will be used (here 'CPosition')



and here is how I add a component to an entity //Entity.add

add(componentInstance): Entity {    //no custom label => create default    if (!componentInstance.constructor.__label__)        componentInstance.constructor.__label__ = util.getTypeName(componentInstance.constructor);    //get the property name representing the component    var label = componentInstance.constructor.__label__;    if (!this[label]) {        this[label] = componentInstance;                //add entity to a global index of Entity <-> Component correspondance        this.register();    }        //make it chainable    return this;}        get(componentType) {            return this[componentType.__label__];        }

so if I write

var entity = Entity().add(new CPosition());


I can access position component like this

entity.pos.x = 0;entity.pos.y = 10;var z = entity.pos.z;

I can also use a more verbose approach :

var cpos = entity.get(CPosition);cpos.x = 0;cpos.y = 10;var z = entity.pos.z;



I know there is an ugly trick here, but I consider ECS a low level system, so tricks like this can be acceptable if not overused IMO

 

 

 

Link to comment
Share on other sites

As I said previously, you will have performance issues if you have a lot of entities with numerous components (let's say 30).
 
Each time a component will be assigned to an entity, a new internal class will be created by V8.
This is why you should avoid this:

if (!this[label]) {  this[label] = componentInstance;  // ...}
Link to comment
Share on other sites

actually, there is a small performance loss at component creation, but what I needed to optimise is the game loop.
each component is created once, but every component is potentially looked up 60 times per frame , here, making component directly accessible is a big gain compared to its creation.

my approach will also use more memory, but I accept this memory/performance trade off.

 

I consider performance loss at creation / destruction negligable compared to the gameloop.

Link to comment
Share on other sites

my approach will also use more memory, but I accept this memory/performance trade off.

 

I consider performance loss at creation / destruction negligable compared to the gameloop.

That's an important thing: your game engine must fit to your needs!

Since I designed makr.js to handle more than 10,000 entities per tick, I don't want to burn precious time!

 

Anyway, your design approach is perfectly correct  :)

Link to comment
Share on other sites

  • 7 months later...

Sorry for bringing up the old topic!

 

I gave makr.js a try. For some reason, the order of entities is reverse of the order it being passed to onAdded.

 

For example, if I want my player to appear on top of the map tiles, I have to create the player first, and create my tile entities so the rendering system can render them all in the right order. Is this a planned design with makr.js?

Link to comment
Share on other sites

Hi, I really appreciate your feedback ! Your systems should not rely on the entity order but should manage itself the depth sorting. Sorry for giving such a short answer but I am not able to dig more into a solution until Monday...

If you have others questions, feel free to leave me a private message !

Link to comment
Share on other sites

  • 8 months later...

Hello everyone,

 

I'm currently working makr v2. It is an entire rewrite of the library and still focus on blazing fast execution.

It heavily relies on ES6 features. Here is a comparison between system declaration in v1 and v2:

// Beforefunction MovementSystem() {  makr.IteratingSystem.call(this)  this.registerComponent(ComponentRegistry.get(Position))  this.registerComponent(ComponentRegistry.get(Motion))}util.inherits(MovementSystem, makr.IteratingSystem)MovementSystem.prototype.process = function(entity, dt) {  var position = entity.get(ComponentRegistry.get(Position))  var motion = entity.get(ComponentRegistry.get(Motion))  position.x += motion.dx * dt  position.y += motion.dy * dt}// Afterclass MovementSystem extends IteratingSystem.use(Position, Motion) {  updateEntity(entity, dt) {    let [position, motion] = entity.get(Position, Motion)    position.x += motion.dx * dt    position.y += motion.dy * dt  }}

World creation:

// Beforevar world = new makr.World()world.registerSystem(new MovementSystem())world.registerSystem(new CollisionSystem())world.registerSystem(new RenderingSystem())// Afterlet world = new Makr({  types: [Position, Motion, Body, Display],  systems: [    new MovementSystem(),    new CollisionSystem(),    new RenderingSystem()  ]})

Major changes:

  • Uses ES6
  • Cleaner API
  • Tests
  • Browserify support
  • No more singletons
  • ComponentRegistry is now automatically called
  • and more!

It would be great to hear some feedback from you guys!

Cheers!

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...