ooflorent Posted September 17, 2013 Share Posted September 17, 2013 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! jerome 1 Quote Link to comment Share on other sites More sharing options...
Ezelia Posted September 19, 2013 Share Posted September 19, 2013 great! I'll read your code as I'm also rolling my own ECS I'll maybe find good ideas Quote Link to comment Share on other sites More sharing options...
whirlibulf Posted September 19, 2013 Share Posted September 19, 2013 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. Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 19, 2013 Author Share Posted September 19, 2013 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 expensiveRetrieving 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! Quote Link to comment Share on other sites More sharing options...
mrspeaker Posted September 19, 2013 Share Posted September 19, 2013 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? Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 19, 2013 Author Share Posted September 19, 2013 This isn't "nice" but this an example.If I created a real game I would have make it different! Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 19, 2013 Author Share Posted September 19, 2013 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 AshI 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! Quote Link to comment Share on other sites More sharing options...
mrspeaker Posted September 19, 2013 Share Posted September 19, 2013 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? Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 19, 2013 Author Share Posted September 19, 2013 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. Quote Link to comment Share on other sites More sharing options...
Mike Posted September 19, 2013 Share Posted September 19, 2013 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. Quote Link to comment Share on other sites More sharing options...
mrspeaker Posted September 20, 2013 Share Posted September 20, 2013 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. ooflorent 1 Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 20, 2013 Author Share Posted September 20, 2013 What a motivating comment! I just pushed a bugfix to Entity#clear(). Today I'm working performances because I got a really strange result: Entity.kill() is faster than Entity.remove(comp) and I don't get why...I suspect FastBitSet to perform badly. Quote Link to comment Share on other sites More sharing options...
mrspeaker Posted September 20, 2013 Share Posted September 20, 2013 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"? Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 20, 2013 Author Share Posted September 20, 2013 I use layers (PIXI's equivalent is DisplayObjectContainer).If you have other questions unrelated to makr.js, please use private messages Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 23, 2013 Author Share Posted September 23, 2013 I just published an update to makr.js!Added entity groupsAdded a new example using PIXI.js (based on Phaser's invaders game)Refactor "balls" example Quote Link to comment Share on other sites More sharing options...
Autarc Posted September 23, 2013 Share Posted September 23, 2013 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. Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 24, 2013 Author Share Posted September 24, 2013 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 problemMoreover 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 slowcomponentName check could be expensiveEach 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. Quote Link to comment Share on other sites More sharing options...
Ezelia Posted September 24, 2013 Share Posted September 24, 2013 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.addadd(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 writevar entity = Entity().add(new CPosition());I can access position component like thisentity.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 Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 24, 2013 Author Share Posted September 24, 2013 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; // ...} Quote Link to comment Share on other sites More sharing options...
Ezelia Posted September 24, 2013 Share Posted September 24, 2013 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. Quote Link to comment Share on other sites More sharing options...
ooflorent Posted September 24, 2013 Author Share Posted September 24, 2013 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 Quote Link to comment Share on other sites More sharing options...
Son Tran-Nguyen Posted May 1, 2014 Share Posted May 1, 2014 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? Quote Link to comment Share on other sites More sharing options...
ooflorent Posted May 2, 2014 Author Share Posted May 2, 2014 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 ! Quote Link to comment Share on other sites More sharing options...
ooflorent Posted January 6, 2015 Author Share Posted January 6, 2015 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 ES6Cleaner APITestsBrowserify supportNo more singletonsComponentRegistry is now automatically calledand more!It would be great to hear some feedback from you guys!Cheers! Pilluminati 1 Quote Link to comment Share on other sites More sharing options...
abiyasa Posted January 7, 2015 Share Posted January 7, 2015 Looks good to me! Do you have any link where we could see the progress? I check the GitHub but the latest commit is still a year ago. I'm curios on how do you use ES6 & which transpiler you're using. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.