├── .npmignore ├── test ├── setup.js └── unit │ ├── entity.test.js │ ├── .jshintrc │ ├── uid.test.js │ ├── system.test.js │ └── ecs.test.js ├── src ├── performance.js ├── utils.js ├── system.js ├── uid.js ├── ecs.js └── entity.js ├── dist ├── performance.js ├── utils.js ├── uid.js ├── system.js ├── ecs.js └── entity.js ├── .gitignore ├── package.json ├── .jshintrc ├── gulpfile.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | gulpfile.js 2 | .jshintrc 3 | test/ 4 | src/ -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | 4 | global.expect = chai.expect; 5 | global.sinon = sinon; 6 | -------------------------------------------------------------------------------- /src/performance.js: -------------------------------------------------------------------------------- 1 | 2 | /*global global */ 3 | 4 | let perf = null, start = Date.now(); 5 | 6 | // use global browser performance module 7 | // for node create a polyfill 8 | if (!global) { 9 | perf = window.performance; 10 | } else { 11 | perf = { 12 | now() { 13 | return Date.now() - start; 14 | } 15 | }; 16 | } 17 | 18 | export default perf; -------------------------------------------------------------------------------- /dist/performance.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | 5 | /*global global */ 6 | 7 | var perf = null, 8 | start = Date.now(); 9 | 10 | // use global browser performance module 11 | // for node create a polyfill 12 | if (!global) { 13 | perf = window.performance; 14 | } else { 15 | perf = { 16 | now: function now() { 17 | return Date.now() - start; 18 | } 19 | }; 20 | } 21 | 22 | exports["default"] = perf; 23 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | export function fastBind(thisArg, methodFunc) { 3 | return function () { 4 | methodFunc.apply(thisArg, arguments); 5 | }; 6 | } 7 | 8 | export function fastSplice(array, startIndex, removeCount) { 9 | let len = array.length; 10 | let removeLen = 0; 11 | 12 | if (startIndex >= len || removeCount === 0) { 13 | return; 14 | } 15 | 16 | removeCount = startIndex + removeCount > len ? 17 | (len - startIndex) : removeCount; 18 | removeLen = len - removeCount; 19 | 20 | for (let i = startIndex; i < len; i += 1) { 21 | array[i] = array[i + removeCount]; 22 | } 23 | 24 | array.length = removeLen; 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Test reports in xunit format 17 | xunit.xml 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | exports.fastBind = fastBind; 5 | exports.fastSplice = fastSplice; 6 | 7 | function fastBind(thisArg, methodFunc) { 8 | return function () { 9 | methodFunc.apply(thisArg, arguments); 10 | }; 11 | } 12 | 13 | function fastSplice(array, startIndex, removeCount) { 14 | var len = array.length; 15 | var removeLen = 0; 16 | 17 | if (startIndex >= len || removeCount === 0) { 18 | return; 19 | } 20 | 21 | removeCount = startIndex + removeCount > len ? len - startIndex : removeCount; 22 | removeLen = len - removeCount; 23 | 24 | for (var i = startIndex; i < len; i += 1) { 25 | array[i] = array[i + removeCount]; 26 | } 27 | 28 | array.length = removeLen; 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yagl-ecs", 3 | "version": "1.0.0", 4 | "author": "Pierre Beaujeu", 5 | "description": "Entity Component System for ES6", 6 | "main": "dist/ecs.js", 7 | "scripts": { 8 | "test": "gulp test", 9 | "prepublish": "gulp build" 10 | }, 11 | "devDependencies": { 12 | "babel": "^5.1.11", 13 | "chai": "^2.2.0", 14 | "del": "^1.1.1", 15 | "gulp": "^3.8.11", 16 | "gulp-babel": "^5.1.0", 17 | "gulp-istanbul": "^0.8.1", 18 | "gulp-jshint": "^1.10.0", 19 | "gulp-load-plugins": "^0.10.0", 20 | "gulp-mocha": "^2.0.1", 21 | "gulp-notify": "^2.2.0", 22 | "gulp-watch": "^4.2.4", 23 | "isparta": "^3.0.3", 24 | "jshint-stylish": "^1.0.1", 25 | "lolex": "^1.3.1", 26 | "mocha": "^2.2.4", 27 | "sinon": "^1.16.1", 28 | "xunit-file": "0.0.6" 29 | }, 30 | "engines": { 31 | "node": ">=0.10.0" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/yagl/ecs.git" 36 | }, 37 | "keywords": [ 38 | "game", 39 | "gamedev", 40 | "ecs", 41 | "entity component system", 42 | "design pattern" 43 | ], 44 | "licence": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /test/unit/entity.test.js: -------------------------------------------------------------------------------- 1 | import Entity from '../../src/entity'; 2 | 3 | describe('Entity', () => { 4 | it('should initialize', () => { 5 | let entity = new Entity(); 6 | 7 | expect(entity.id).to.be.a('number'); 8 | }); 9 | 10 | it('should have an unique id', () => { 11 | let entity1 = new Entity(); 12 | let entity2 = new Entity(); 13 | 14 | expect(entity1.id).to.be.not.equal(entity2.id); 15 | }); 16 | 17 | it('should support default components', () => { 18 | let entity = new Entity(0, [{ 19 | name: 'test', 20 | defaults: {foo: 'bar'} 21 | }]); 22 | 23 | expect(entity.components.test).to.exist.and.to.be.deep.equal({foo: 'bar'}); 24 | }); 25 | 26 | describe('addComponent()', () => { 27 | it('should add a void object when a name is passed', () => { 28 | let entity = new Entity(); 29 | entity.addComponent('test'); 30 | 31 | expect(entity.components.test).to.deep.equal({}); 32 | }); 33 | }); 34 | 35 | describe('updateComponent()', () => { 36 | it('should update an existing component', () => { 37 | let entity = new Entity(); 38 | entity.addComponent('test', {foo: 'bar'}); 39 | 40 | expect(entity.components.test).to.deep.equal({foo: 'bar'}); 41 | 42 | entity.updateComponent('test', {foo: 'foo'}); 43 | 44 | expect(entity.components.test).to.deep.equal({foo: 'foo'}); 45 | }); 46 | }); 47 | 48 | describe('updateComponents()', () => { 49 | it('should update a list of existing component', () => { 50 | let entity = new Entity(); 51 | entity.addComponent('test', {foo: 'bar'}); 52 | 53 | expect(entity.components.test).to.deep.equal({foo: 'bar'}); 54 | 55 | entity.updateComponents({test: {foo: 'foo'}}); 56 | 57 | expect(entity.components.test).to.deep.equal({foo: 'foo'}); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise" : true, 3 | "camelcase" : true, 4 | "curly" : true, 5 | "eqeqeq" : true, 6 | "forin" : false, 7 | "immed" : true, 8 | "indent" : 2, 9 | "latedef" : true, 10 | "newcap" : true, 11 | "noarg" : true, 12 | "noempty" : true, 13 | "nonbsp" : true, 14 | "nonew" : true, 15 | "plusplus" : false, 16 | "quotmark" : "single", 17 | "undef" : true, 18 | "unused" : true, 19 | "strict" : false, 20 | "trailing" : true, 21 | "maxparams" : 4, 22 | "maxdepth" : 2, 23 | "maxstatements" : 15, 24 | "maxcomplexity" : 10, 25 | "maxlen" : 100, 26 | 27 | "asi" : false, 28 | "boss" : true, 29 | "debug" : false, 30 | "eqnull" : false, 31 | "esnext" : true, 32 | "evil" : false, 33 | "expr" : false, 34 | "funcscope" : false, 35 | "globalstrict" : false, 36 | "iterator" : false, 37 | "lastsemic" : false, 38 | "laxbreak" : false, 39 | "laxcomma" : false, 40 | "loopfunc" : false, 41 | "maxerr" : 50, 42 | "multistr" : false, 43 | "notypeof" : false, 44 | "proto" : false, 45 | "scripturl" : false, 46 | "smarttabs" : false, 47 | "shadow" : false, 48 | "sub" : false, 49 | "supernew" : false, 50 | "validthis" : false, 51 | "noyield" : false, 52 | 53 | "browser" : true, 54 | "couch" : false, 55 | "devel" : false, 56 | "dojo" : false, 57 | "jquery" : false, 58 | "mootools" : false, 59 | "node" : false, 60 | "nonstandard" : false, 61 | "prototypejs" : false, 62 | "rhino" : false, 63 | "worker" : false, 64 | "wsh" : false, 65 | "yui" : false 66 | } 67 | -------------------------------------------------------------------------------- /test/unit/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise" : true, 3 | "camelcase" : true, 4 | "curly" : true, 5 | "eqeqeq" : true, 6 | "forin" : false, 7 | "immed" : true, 8 | "indent" : 2, 9 | "latedef" : true, 10 | "newcap" : true, 11 | "noarg" : true, 12 | "noempty" : true, 13 | "nonbsp" : true, 14 | "nonew" : true, 15 | "plusplus" : false, 16 | "quotmark" : "single", 17 | "undef" : true, 18 | "unused" : true, 19 | "strict" : false, 20 | "trailing" : true, 21 | "maxparams" : 4, 22 | "maxdepth" : 2, 23 | "maxstatements" : 15, 24 | "maxcomplexity" : 10, 25 | "maxlen" : 200, 26 | 27 | "asi" : false, 28 | "boss" : false, 29 | "debug" : false, 30 | "eqnull" : false, 31 | "esnext" : true, 32 | "evil" : false, 33 | "expr" : true, 34 | "funcscope" : false, 35 | "globalstrict" : false, 36 | "iterator" : false, 37 | "lastsemic" : false, 38 | "laxbreak" : false, 39 | "laxcomma" : false, 40 | "loopfunc" : false, 41 | "maxerr" : 50, 42 | "multistr" : false, 43 | "notypeof" : false, 44 | "proto" : false, 45 | "scripturl" : false, 46 | "smarttabs" : false, 47 | "shadow" : false, 48 | "sub" : false, 49 | "supernew" : false, 50 | "validthis" : false, 51 | "noyield" : false, 52 | 53 | "browser" : true, 54 | "couch" : false, 55 | "devel" : false, 56 | "dojo" : false, 57 | "jquery" : false, 58 | "mootools" : false, 59 | "node" : false, 60 | "nonstandard" : false, 61 | "prototypejs" : false, 62 | "rhino" : false, 63 | "worker" : false, 64 | "wsh" : false, 65 | "yui" : false, 66 | "globals": { 67 | "console": true, 68 | "sinon": true, 69 | "spy": true, 70 | "stub": true, 71 | "describe": true, 72 | "before": true, 73 | "after": true, 74 | "beforeEach": true, 75 | "afterEach": true, 76 | "it": true, 77 | "expect": true, 78 | "global": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/unit/uid.test.js: -------------------------------------------------------------------------------- 1 | import uid from '../../src/uid'; 2 | 3 | describe('uid', () => { 4 | it('should have a default generator', () => { 5 | expect(uid.DefaultUIDGenerator).to.exist; 6 | }); 7 | 8 | 9 | it('should create a new generator', () => { 10 | let gen = new uid.UIDGenerator(); 11 | 12 | expect(gen.salt).to.be.a('number'); 13 | expect(gen.uidCounter).to.be.equal(0); 14 | }); 15 | 16 | it('should return sequential unique ids', () => { 17 | let gen = new uid.UIDGenerator(); 18 | 19 | let r1 = gen.next(); 20 | let r2 = gen.next(); 21 | 22 | expect(r1).to.be.a('number'); 23 | expect(r2).to.be.a('number'); 24 | expect(r1).to.be.not.equal('r2'); 25 | }); 26 | 27 | it('should return different sequences with different salts', () => { 28 | let gen1 = new uid.UIDGenerator(1); 29 | let gen2 = new uid.UIDGenerator(2); 30 | 31 | let r11 = gen1.next(); 32 | let r12 = gen1.next(); 33 | let r21 = gen2.next(); 34 | let r22 = gen2.next(); 35 | 36 | expect(r11).to.be.a('number'); 37 | expect(r12).to.be.a('number'); 38 | expect(r21).to.be.a('number'); 39 | expect(r22).to.be.a('number'); 40 | 41 | expect(r11).to.be.not.equal(r21); 42 | expect(r12).to.be.not.equal(r22); 43 | }); 44 | 45 | it('should return generator with incremented salts when calling nextGenerator()', () => { 46 | let gen1 = uid.nextGenerator(); 47 | let gen2 = uid.nextGenerator(); 48 | 49 | expect(gen1.salt).to.be.a('number').and.to.be.not.equal(gen2.salt); 50 | }); 51 | 52 | it('should return incremented salts when calling nextSalt()', () => { 53 | let salt1 = uid.nextSalt(); 54 | let salt2 = uid.nextSalt(); 55 | 56 | expect(salt1).to.be.a('number').and.to.be.not.equal(salt2); 57 | }); 58 | 59 | describe('isSaltedBy()', () => { 60 | it('should return true when then id was salted with given salt', () => { 61 | let gen1 = new uid.UIDGenerator(1); 62 | let gen2 = new uid.UIDGenerator(2); 63 | 64 | let r1 = gen1.next(); 65 | let r2 = gen2.next(); 66 | 67 | expect(uid.isSaltedBy(r1, 1)).to.be.equal(true); 68 | expect(uid.isSaltedBy(r2, 1)).to.be.equal(false); 69 | expect(uid.isSaltedBy(r2, 2)).to.be.equal(true); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 3 | var isparta = require('isparta'); 4 | const gulp = require('gulp'); 5 | const $ = require('gulp-load-plugins')(); 6 | 7 | // Remove the built files 8 | gulp.task('clean', function(callback) { 9 | var del = require('del'); 10 | del(['dist'], callback); 11 | }); 12 | 13 | // Send a notification when JSHint fails, 14 | // so that you know your changes didn't build 15 | function ding(file) { 16 | return file.jshint.success ? false : 'JSHint failed'; 17 | }; 18 | 19 | // Lint our source code 20 | gulp.task('lint:src', function() { 21 | return gulp.src(['src/**/*.js', '!src/wrapper.js']) 22 | .pipe($.jshint()) 23 | .pipe($.jshint.reporter('jshint-stylish')) 24 | .pipe($.notify(ding)) 25 | .pipe($.jshint.reporter('fail')); 26 | }); 27 | 28 | // Lint our test code 29 | gulp.task('lint:test', function() { 30 | return gulp.src(['test/unit/**/*.js']) 31 | .pipe($.jshint()) 32 | .pipe($.jshint.reporter('jshint-stylish')) 33 | .pipe($.notify(ding)) 34 | .pipe($.jshint.reporter('fail')); 35 | }); 36 | 37 | // Compile library to ECMAScript 5 38 | gulp.task('build', ['lint:src', 'clean'], function() { 39 | return gulp.src('src/**/*.js') 40 | .pipe($.babel({blacklist: ['useStrict'], modules: 'common'})) 41 | .pipe(gulp.dest('dist')) 42 | }); 43 | 44 | // Create a coverage report 45 | gulp.task('coverage', function(done) { 46 | require('babel/register')({ modules: 'common' }); 47 | gulp.src(['src/**/*.js']) 48 | .pipe($.istanbul({ instrumenter: isparta.Instrumenter })) 49 | .on('finish', function() { 50 | return test() 51 | .pipe($.istanbul.writeReports()) 52 | .on('end', done); 53 | }); 54 | }); 55 | 56 | // Lint and run our tests 57 | gulp.task('test', ['lint:src', 'lint:test'], function() { 58 | require('babel/register')({modules: 'common'}); 59 | return test(); 60 | }); 61 | 62 | // Lint and run our tests. It creates a xunit report also 63 | gulp.task('test:ci', ['lint:src', 'lint:test'], function() { 64 | require('babel/register')({modules: 'common'}); 65 | return test('xunit-file'); 66 | }); 67 | 68 | function test(reporterName) { 69 | reporterName = !reporterName ? 'spec' : reporterName; 70 | return gulp.src(['test/setup.js', 'test/unit/**/*.js'], {read: false}) 71 | .pipe($.mocha({reporter: reporterName, growl: true})); 72 | }; 73 | 74 | // Watch files for changes & reload 75 | gulp.task('watch', function() { 76 | gulp.watch('src/**/*.js', ['lint:src', 'test']); 77 | gulp.watch('test/**/*.js', ['lint:test', 'test']); 78 | }); 79 | 80 | // An alias of test 81 | gulp.task('default', ['test', 'watch']); 82 | -------------------------------------------------------------------------------- /test/unit/system.test.js: -------------------------------------------------------------------------------- 1 | 2 | import System from '../../src/system'; 3 | 4 | function getFakeEntity() { 5 | return { 6 | addSystem: sinon.spy(), 7 | removeSystem: sinon.spy() 8 | }; 9 | } 10 | 11 | describe('System', () => { 12 | it('should initialize', () => { 13 | let system = new System(); 14 | expect(system).to.exist; 15 | }); 16 | 17 | describe('addEntity()', () => { 18 | let entity, system; 19 | 20 | beforeEach(() => { 21 | entity = getFakeEntity(); 22 | system = new System(); 23 | }); 24 | 25 | it('should add an entity to the system', () => { 26 | system.addEntity(entity); 27 | 28 | expect(system.entities.length).to.be.equal(1); 29 | }); 30 | 31 | it('should add the system to entity systems', () => { 32 | system.addEntity(entity); 33 | 34 | expect(entity.addSystem.calledWith(system)).to.be.equal(true); 35 | }); 36 | 37 | it('should call enter() on added entity', () => { 38 | system.enter = sinon.spy(); 39 | 40 | system.addEntity(entity); 41 | 42 | expect(system.enter.calledWith(entity)).to.be.equal(true); 43 | }); 44 | }); 45 | 46 | describe('removeEntity()', () => { 47 | let entity, system; 48 | 49 | beforeEach(() => { 50 | entity = getFakeEntity(); 51 | system = new System(); 52 | 53 | system.addEntity(entity); 54 | }); 55 | 56 | it('should remove an entity from the system', () => { 57 | system.removeEntity(entity); 58 | 59 | expect(system.entities.length).to.be.equal(0); 60 | }); 61 | 62 | it('should remove the system from entity systems', () => { 63 | system.removeEntity(entity); 64 | 65 | expect(entity.removeSystem.calledWith(system)).to.be.equal(true); 66 | }); 67 | 68 | it('should call exit() on removed entity', () => { 69 | system.exit = sinon.spy(); 70 | 71 | system.removeEntity(entity); 72 | 73 | expect(system.exit.calledWith(entity)).to.be.equal(true); 74 | }); 75 | }); 76 | 77 | describe('updateAll()', () => { 78 | it('should call update() on each entity', () => { 79 | let entity1 = getFakeEntity(); 80 | let entity2 = getFakeEntity(); 81 | let system = new System(); 82 | system.update = sinon.spy(); 83 | 84 | system.addEntity(entity1); 85 | system.addEntity(entity2); 86 | system.updateAll(); 87 | 88 | expect(system.update.calledTwice).to.be.equal(true); 89 | expect(system.update.calledWith(entity1)).to.be.equal(true); 90 | expect(system.update.calledWith(entity2)).to.be.equal(true); 91 | }); 92 | 93 | it('should call preUpdate()', () => { 94 | let system = new System(); 95 | system.preUpdate = sinon.spy(); 96 | 97 | system.updateAll(); 98 | 99 | expect(system.preUpdate.called).to.be.equal(true); 100 | }); 101 | 102 | it('should call postUpdate()', () => { 103 | let system = new System(); 104 | system.postUpdate = sinon.spy(); 105 | 106 | system.updateAll(); 107 | 108 | expect(system.postUpdate.called).to.be.equal(true); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/system.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ecs 3 | */ 4 | 5 | import { fastSplice } from './utils'; 6 | 7 | // forced to disable this check for abstract methods 8 | // jshint unused:false 9 | /** 10 | * @class System 11 | * 12 | * @description A system update all eligible entities at a given frequency. 13 | * This class is not meant to be used directly and should be sub-classed to 14 | * define specific logic. 15 | */ 16 | class System { 17 | /** 18 | * @class System 19 | * @constructor 20 | * @param [frequency=1] {Number} Frequency of execution. 21 | */ 22 | constructor(frequency=1) { 23 | /** 24 | * Frequency of update execution, a frequency of `1` run the system every 25 | * update, `2` will run the system every 2 updates, ect. 26 | * @property {Number} frequency 27 | */ 28 | this.frequency = frequency; 29 | 30 | /** 31 | * Entities of the system. 32 | * 33 | * @property {Array[Entity]} entities 34 | */ 35 | this.entities = []; 36 | } 37 | /** 38 | * Add an entity to the system entities. 39 | * 40 | * @param {Entity} entity The entity to add to the system. 41 | */ 42 | addEntity(entity) { 43 | entity.addSystem(this); 44 | this.entities.push(entity); 45 | 46 | this.enter(entity); 47 | } 48 | /** 49 | * Remove an entity from the system entities. exit() handler is executed 50 | * only if the entity actually exists in the system entities. 51 | * 52 | * @param {Entity} entity Reference of the entity to remove. 53 | */ 54 | removeEntity(entity) { 55 | let index = this.entities.indexOf(entity); 56 | 57 | if (index !== -1) { 58 | entity.removeSystem(this); 59 | fastSplice(this.entities, index, 1); 60 | 61 | this.exit(entity); 62 | } 63 | } 64 | /** 65 | * Apply update to each entity of this system. 66 | * 67 | * @method updateAll 68 | */ 69 | updateAll(elapsed) { 70 | this.preUpdate(); 71 | 72 | for (let i = 0, entity; entity = this.entities[i]; i += 1) { 73 | this.update(entity, elapsed); 74 | } 75 | 76 | this.postUpdate(); 77 | } 78 | /** 79 | * dispose the system by exiting all the entities 80 | * 81 | * @method dispose 82 | */ 83 | dispose() { 84 | for (let i = 0, entity; entity = this.entities[i]; i += 1) { 85 | entity.removeSystem(this); 86 | this.exit(entity); 87 | } 88 | } 89 | // methods to be extended by subclasses 90 | /** 91 | * Abstract method to subclass. Called once per update, before entities 92 | * iteration. 93 | * 94 | * @method preUpdate 95 | */ 96 | preUpdate() {} 97 | /** 98 | * Abstract method to subclass. Called once per update, after entities 99 | * iteration. 100 | * 101 | * @method postUpdate 102 | */ 103 | postUpdate() {} 104 | /** 105 | * Abstract method to subclass. Should return true if the entity is eligible 106 | * to the system, false otherwise. 107 | * 108 | * @method test 109 | * @param {Entity} entity The entity to test. 110 | */ 111 | test(entity) { 112 | return false; 113 | } 114 | /** 115 | * Abstract method to subclass. Called when an entity is added to the system. 116 | * 117 | * @method enter 118 | * @param {Entity} entity The added entity. 119 | */ 120 | enter(entity) {} 121 | /** 122 | * Abstract method to subclass. Called when an entity is removed from the system. 123 | * 124 | * @method exit 125 | * @param {Entity} entity The removed entity. 126 | */ 127 | exit(entity) {} 128 | /** 129 | * Abstract method to subclass. Called for each entity to update. This is 130 | * the only method that should actual mutate entity state. 131 | * 132 | * @method update 133 | * @param {Entity} entity The entity to update. 134 | */ 135 | update(entity) {} 136 | } 137 | // jshint unused:true 138 | 139 | export default System; 140 | -------------------------------------------------------------------------------- /src/uid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module uid 3 | */ 4 | /* 5 | * UIDGenerator for multi-instance Entity Component System 6 | * Generate numeric unique ids for ECS entities. The requirements are: 7 | * * generate Numbers for fast comparaison, low storage and bandwidth usage 8 | * * generators can be salted so you can use multiple generators with 9 | * uniqueness guaranty 10 | * * each salted generator can generate reasonable amount of unique ids 11 | */ 12 | 13 | // maximum number of salted generators that can run concurently, once the 14 | // number of allowed generators has been reached the salt of the next 15 | // generator is silently reset to 0 16 | const MAX_SALTS = 10000; 17 | 18 | const MAX_ENTITY_PER_GENERATOR = Math.floor(Number.MAX_SAFE_INTEGER / 19 | MAX_SALTS) - 1; 20 | let currentSalt = 0; 21 | 22 | /** 23 | * Generate unique sequences of Numbers. Can be salted (up to 9999 salts) 24 | * to generate differents ids. 25 | * 26 | * To work properly, ECS needs to associate an unique id with each entity. But 27 | * to preserve efficiency, the unique id must be a Number (more exactly a safe 28 | * integer). 29 | * 30 | * The basic implementation would be an incremented Number to generate a unique 31 | * sequence, but this fails when several ecs instances are running and creating 32 | * entities concurrently (e.g. in a multiplayer networked game). To work around 33 | * this problem, ecs provide UIDGenerator class which allow you to salt your 34 | * generated ids sequence. Two generators with different salts will NEVER 35 | * generate the same ids. 36 | * 37 | * Currently, there is a maxumum of 9999 salts and about 900719925473 uid per 38 | * salt. These limits are hard-coded, but I plan to expose these settings in 39 | * the future. 40 | * 41 | * @class UIDGenerator 42 | */ 43 | class UIDGenerator { 44 | /** 45 | * @constructor 46 | * @class UIDGenerator 47 | * @param {Number} [salt=0] The salt to use for this generator. Number 48 | * between 0 and 9999 (inclusive). 49 | */ 50 | constructor(salt = 0) { 51 | /** 52 | * The salt of this generator. 53 | * @property {Number} salt 54 | */ 55 | this.salt = salt; 56 | 57 | /** 58 | * The counter used to generate unique sequence. 59 | * @property {Number} uidCount 60 | */ 61 | this.uidCounter = 0; 62 | } 63 | /** 64 | * Create a new unique id. 65 | * 66 | * @return {Number} An unique id. 67 | */ 68 | next() { 69 | let nextUid = this.salt + this.uidCounter * MAX_SALTS; 70 | 71 | // if we exceed the number of maximum entities (which is 72 | // very high) reset the counter. 73 | if (++this.uidCounter >= MAX_ENTITY_PER_GENERATOR) { 74 | this.uidCounter = 0; 75 | } 76 | 77 | return nextUid; 78 | } 79 | } 80 | 81 | /** 82 | * @class UID 83 | */ 84 | const UID = { 85 | /** 86 | * A reference to UIDGenerator class. 87 | * 88 | * @property {class} UIDGenerator 89 | */ 90 | UIDGenerator, 91 | /** 92 | * The default generator to use if an entity is created without id or generator instance. 93 | * 94 | * @property {UIDGenerator} DefaultUIDGenerator 95 | */ 96 | DefaultUIDGenerator: new UIDGenerator(currentSalt++), 97 | /** 98 | * Return true if the entity id was salted by given salt 99 | * 100 | * @param {String} entityId Entity id to test 101 | * @param {String} salt Salt to test 102 | * @return {Boolean} true if the id was generated by the salt, false 103 | * otherwise 104 | */ 105 | isSaltedBy: (entityId, salt) => entityId % MAX_SALTS === salt, 106 | /** 107 | * Return the next unique salt. 108 | * 109 | * @method nextSalt 110 | * @return {Number} A unique salt. 111 | */ 112 | nextSalt: () => { 113 | let salt = currentSalt; 114 | 115 | // if we exceed the number of maximum salts, silently reset 116 | // to 1 (since 0 will always be the default generator) 117 | if (++currentSalt > MAX_SALTS - 1) { 118 | currentSalt = 1; 119 | } 120 | 121 | return salt; 122 | }, 123 | /** 124 | * Create a new generator with unique salt. 125 | * 126 | * @method nextGenerator 127 | * @return {UIDGenerator} The created UIDGenerator. 128 | */ 129 | nextGenerator: () => new UIDGenerator(UID.nextSalt()) 130 | }; 131 | 132 | export default UID; -------------------------------------------------------------------------------- /test/unit/ecs.test.js: -------------------------------------------------------------------------------- 1 | import ECS from '../../src/ecs'; 2 | 3 | describe('ECS', () => { 4 | it('should initialize', () => { 5 | let ecs = new ECS(); 6 | 7 | expect(ecs.entities).to.be.an('array'); 8 | expect(ecs.systems).to.be.an('array'); 9 | }); 10 | 11 | describe('getEntityById()', () => { 12 | it('should retrieve an entity by id', () => { 13 | let ecs = new ECS(); 14 | let entity = new ECS.Entity(123); 15 | 16 | ecs.addEntity(entity); 17 | 18 | expect(ecs.getEntityById(123)).to.be.equal(entity); 19 | }); 20 | }); 21 | 22 | describe('update()', () => { 23 | let ecs, entity, system; 24 | 25 | beforeEach(() => { 26 | ecs = new ECS(); 27 | entity = new ECS.Entity(); 28 | system = new ECS.System(); 29 | }); 30 | 31 | it('should give the elapsed time to update methods', (done) => { 32 | system.test = () => true; 33 | system.update = (entity, elapsed) => { 34 | expect(elapsed).to.be.a('number'); 35 | done(); 36 | }; 37 | 38 | ecs.addSystem(system); 39 | ecs.addEntity(entity); 40 | 41 | ecs.update(); 42 | }); 43 | }); 44 | 45 | describe('addSystem()', () => { 46 | let ecs, entity, system; 47 | 48 | beforeEach(() => { 49 | ecs = new ECS(); 50 | entity = new ECS.Entity(); 51 | system = new ECS.System(); 52 | }); 53 | 54 | it('should call enter() when update', () => { 55 | system.test = () => true; 56 | system.enter = sinon.spy(); 57 | ecs.addSystem(system); 58 | ecs.addEntity(entity); 59 | 60 | ecs.update(); 61 | 62 | expect(system.enter.calledWith(entity)).to.be.equal(true); 63 | }); 64 | 65 | it('should call enter() when removing and re-adding a system', () => { 66 | system.test = () => true; 67 | system.enter = sinon.spy(); 68 | ecs.addSystem(system); 69 | ecs.addEntity(entity); 70 | ecs.update(); 71 | 72 | ecs.removeSystem(system); 73 | ecs.update(); 74 | 75 | ecs.addSystem(system); 76 | ecs.update(); 77 | 78 | expect(system.enter.calledTwice).to.be.equal(true); 79 | }); 80 | }); 81 | 82 | describe('removeSystem()', () => { 83 | let ecs, entity, system; 84 | 85 | beforeEach(() => { 86 | ecs = new ECS(); 87 | entity = new ECS.Entity(); 88 | system = new ECS.System(); 89 | }); 90 | 91 | it('should call exit(entity) when removed', () => { 92 | system.test = () => true; 93 | system.exit = sinon.spy(); 94 | 95 | ecs.addSystem(system); 96 | ecs.addEntity(entity); 97 | 98 | ecs.update(); 99 | 100 | ecs.removeSystem(system); 101 | 102 | expect(system.exit.calledWith(entity)).to.be.equal(true); 103 | }); 104 | 105 | it('should call exit(entity) of all systems when removed', () => { 106 | system.test = () => true; 107 | system.exit = sinon.spy(); 108 | 109 | ecs.addSystem(system); 110 | ecs.addEntity(entity); 111 | 112 | ecs.update(); 113 | 114 | ecs.removeSystem(system); 115 | 116 | expect(system.exit.calledWith(entity)).to.be.equal(true); 117 | }); 118 | }); 119 | 120 | describe('removeEntity()', () => { 121 | let ecs, entity, system1, system2; 122 | 123 | beforeEach(() => { 124 | ecs = new ECS(); 125 | entity = new ECS.Entity(); 126 | system1 = new ECS.System(); 127 | system2 = new ECS.System(); 128 | }); 129 | 130 | it('should call exit(entity) when removed', () => { 131 | system1.test = () => true; 132 | system1.exit = sinon.spy(); 133 | 134 | ecs.addSystem(system1); 135 | ecs.addEntity(entity); 136 | 137 | ecs.update(); 138 | 139 | ecs.removeEntity(entity); 140 | 141 | expect(system1.exit.calledWith(entity)).to.be.equal(true); 142 | }); 143 | 144 | it('should call exit(entity) of all systems when removed', () => { 145 | system2.test = () => true; 146 | system2.exit = sinon.spy(); 147 | system1.test = () => true; 148 | system1.exit = sinon.spy(); 149 | 150 | ecs.addSystem(system1); 151 | ecs.addSystem(system2); 152 | ecs.addEntity(entity); 153 | 154 | ecs.update(); 155 | 156 | ecs.removeEntity(entity); 157 | 158 | expect(system1.exit.calledWith(entity)).to.be.equal(true); 159 | expect(system2.exit.calledWith(entity)).to.be.equal(true); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /dist/uid.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { 2 | value: true 3 | }); 4 | 5 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 8 | 9 | /** 10 | * @module uid 11 | */ 12 | /* 13 | * UIDGenerator for multi-instance Entity Component System 14 | * Generate numeric unique ids for ECS entities. The requirements are: 15 | * * generate Numbers for fast comparaison, low storage and bandwidth usage 16 | * * generators can be salted so you can use multiple generators with 17 | * uniqueness guaranty 18 | * * each salted generator can generate reasonable amount of unique ids 19 | */ 20 | 21 | // maximum number of salted generators that can run concurently, once the 22 | // number of allowed generators has been reached the salt of the next 23 | // generator is silently reset to 0 24 | var MAX_SALTS = 10000; 25 | 26 | var MAX_ENTITY_PER_GENERATOR = Math.floor(Number.MAX_SAFE_INTEGER / MAX_SALTS) - 1; 27 | var currentSalt = 0; 28 | 29 | /** 30 | * Generate unique sequences of Numbers. Can be salted (up to 9999 salts) 31 | * to generate differents ids. 32 | * 33 | * To work properly, ECS needs to associate an unique id with each entity. But 34 | * to preserve efficiency, the unique id must be a Number (more exactly a safe 35 | * integer). 36 | * 37 | * The basic implementation would be an incremented Number to generate a unique 38 | * sequence, but this fails when several ecs instances are running and creating 39 | * entities concurrently (e.g. in a multiplayer networked game). To work around 40 | * this problem, ecs provide UIDGenerator class which allow you to salt your 41 | * generated ids sequence. Two generators with different salts will NEVER 42 | * generate the same ids. 43 | * 44 | * Currently, there is a maxumum of 9999 salts and about 900719925473 uid per 45 | * salt. These limits are hard-coded, but I plan to expose these settings in 46 | * the future. 47 | * 48 | * @class UIDGenerator 49 | */ 50 | 51 | var UIDGenerator = (function () { 52 | /** 53 | * @constructor 54 | * @class UIDGenerator 55 | * @param {Number} [salt=0] The salt to use for this generator. Number 56 | * between 0 and 9999 (inclusive). 57 | */ 58 | 59 | function UIDGenerator() { 60 | var salt = arguments.length <= 0 || arguments[0] === undefined ? 0 : arguments[0]; 61 | 62 | _classCallCheck(this, UIDGenerator); 63 | 64 | /** 65 | * The salt of this generator. 66 | * @property {Number} salt 67 | */ 68 | this.salt = salt; 69 | 70 | /** 71 | * The counter used to generate unique sequence. 72 | * @property {Number} uidCount 73 | */ 74 | this.uidCounter = 0; 75 | } 76 | 77 | /** 78 | * @class UID 79 | */ 80 | 81 | /** 82 | * Create a new unique id. 83 | * 84 | * @return {Number} An unique id. 85 | */ 86 | 87 | _createClass(UIDGenerator, [{ 88 | key: "next", 89 | value: function next() { 90 | var nextUid = this.salt + this.uidCounter * MAX_SALTS; 91 | 92 | // if we exceed the number of maximum entities (which is 93 | // very high) reset the counter. 94 | if (++this.uidCounter >= MAX_ENTITY_PER_GENERATOR) { 95 | this.uidCounter = 0; 96 | } 97 | 98 | return nextUid; 99 | } 100 | }]); 101 | 102 | return UIDGenerator; 103 | })(); 104 | 105 | var UID = { 106 | /** 107 | * A reference to UIDGenerator class. 108 | * 109 | * @property {class} UIDGenerator 110 | */ 111 | UIDGenerator: UIDGenerator, 112 | /** 113 | * The default generator to use if an entity is created without id or generator instance. 114 | * 115 | * @property {UIDGenerator} DefaultUIDGenerator 116 | */ 117 | DefaultUIDGenerator: new UIDGenerator(currentSalt++), 118 | /** 119 | * Return true if the entity id was salted by given salt 120 | * 121 | * @param {String} entityId Entity id to test 122 | * @param {String} salt Salt to test 123 | * @return {Boolean} true if the id was generated by the salt, false 124 | * otherwise 125 | */ 126 | isSaltedBy: function isSaltedBy(entityId, salt) { 127 | return entityId % MAX_SALTS === salt; 128 | }, 129 | /** 130 | * Return the next unique salt. 131 | * 132 | * @method nextSalt 133 | * @return {Number} A unique salt. 134 | */ 135 | nextSalt: function nextSalt() { 136 | var salt = currentSalt; 137 | 138 | // if we exceed the number of maximum salts, silently reset 139 | // to 1 (since 0 will always be the default generator) 140 | if (++currentSalt > MAX_SALTS - 1) { 141 | currentSalt = 1; 142 | } 143 | 144 | return salt; 145 | }, 146 | /** 147 | * Create a new generator with unique salt. 148 | * 149 | * @method nextGenerator 150 | * @return {UIDGenerator} The created UIDGenerator. 151 | */ 152 | nextGenerator: function nextGenerator() { 153 | return new UIDGenerator(UID.nextSalt()); 154 | } 155 | }; 156 | 157 | exports["default"] = UID; 158 | module.exports = exports["default"]; -------------------------------------------------------------------------------- /dist/system.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { 2 | value: true 3 | }); 4 | 5 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 8 | 9 | /** 10 | * @module ecs 11 | */ 12 | 13 | var _utils = require('./utils'); 14 | 15 | // forced to disable this check for abstract methods 16 | // jshint unused:false 17 | /** 18 | * @class System 19 | * 20 | * @description A system update all eligible entities at a given frequency. 21 | * This class is not meant to be used directly and should be sub-classed to 22 | * define specific logic. 23 | */ 24 | 25 | var System = (function () { 26 | /** 27 | * @class System 28 | * @constructor 29 | * @param [frequency=1] {Number} Frequency of execution. 30 | */ 31 | 32 | function System() { 33 | var frequency = arguments.length <= 0 || arguments[0] === undefined ? 1 : arguments[0]; 34 | 35 | _classCallCheck(this, System); 36 | 37 | /** 38 | * Frequency of update execution, a frequency of `1` run the system every 39 | * update, `2` will run the system every 2 updates, ect. 40 | * @property {Number} frequency 41 | */ 42 | this.frequency = frequency; 43 | 44 | /** 45 | * Entities of the system. 46 | * 47 | * @property {Array[Entity]} entities 48 | */ 49 | this.entities = []; 50 | } 51 | 52 | // jshint unused:true 53 | 54 | /** 55 | * Add an entity to the system entities. 56 | * 57 | * @param {Entity} entity The entity to add to the system. 58 | */ 59 | 60 | _createClass(System, [{ 61 | key: 'addEntity', 62 | value: function addEntity(entity) { 63 | entity.addSystem(this); 64 | this.entities.push(entity); 65 | 66 | this.enter(entity); 67 | } 68 | 69 | /** 70 | * Remove an entity from the system entities. exit() handler is executed 71 | * only if the entity actually exists in the system entities. 72 | * 73 | * @param {Entity} entity Reference of the entity to remove. 74 | */ 75 | }, { 76 | key: 'removeEntity', 77 | value: function removeEntity(entity) { 78 | var index = this.entities.indexOf(entity); 79 | 80 | if (index !== -1) { 81 | entity.removeSystem(this); 82 | (0, _utils.fastSplice)(this.entities, index, 1); 83 | 84 | this.exit(entity); 85 | } 86 | } 87 | 88 | /** 89 | * Apply update to each entity of this system. 90 | * 91 | * @method updateAll 92 | */ 93 | }, { 94 | key: 'updateAll', 95 | value: function updateAll(elapsed) { 96 | this.preUpdate(); 97 | 98 | for (var i = 0, entity = undefined; entity = this.entities[i]; i += 1) { 99 | this.update(entity, elapsed); 100 | } 101 | 102 | this.postUpdate(); 103 | } 104 | 105 | /** 106 | * dispose the system by exiting all the entities 107 | * 108 | * @method dispose 109 | */ 110 | }, { 111 | key: 'dispose', 112 | value: function dispose() { 113 | for (var i = 0, entity = undefined; entity = this.entities[i]; i += 1) { 114 | entity.removeSystem(this); 115 | this.exit(entity); 116 | } 117 | } 118 | 119 | // methods to be extended by subclasses 120 | /** 121 | * Abstract method to subclass. Called once per update, before entities 122 | * iteration. 123 | * 124 | * @method preUpdate 125 | */ 126 | }, { 127 | key: 'preUpdate', 128 | value: function preUpdate() {} 129 | 130 | /** 131 | * Abstract method to subclass. Called once per update, after entities 132 | * iteration. 133 | * 134 | * @method postUpdate 135 | */ 136 | }, { 137 | key: 'postUpdate', 138 | value: function postUpdate() {} 139 | 140 | /** 141 | * Abstract method to subclass. Should return true if the entity is eligible 142 | * to the system, false otherwise. 143 | * 144 | * @method test 145 | * @param {Entity} entity The entity to test. 146 | */ 147 | }, { 148 | key: 'test', 149 | value: function test(entity) { 150 | return false; 151 | } 152 | 153 | /** 154 | * Abstract method to subclass. Called when an entity is added to the system. 155 | * 156 | * @method enter 157 | * @param {Entity} entity The added entity. 158 | */ 159 | }, { 160 | key: 'enter', 161 | value: function enter(entity) {} 162 | 163 | /** 164 | * Abstract method to subclass. Called when an entity is removed from the system. 165 | * 166 | * @method exit 167 | * @param {Entity} entity The removed entity. 168 | */ 169 | }, { 170 | key: 'exit', 171 | value: function exit(entity) {} 172 | 173 | /** 174 | * Abstract method to subclass. Called for each entity to update. This is 175 | * the only method that should actual mutate entity state. 176 | * 177 | * @method update 178 | * @param {Entity} entity The entity to update. 179 | */ 180 | }, { 181 | key: 'update', 182 | value: function update(entity) {} 183 | }]); 184 | 185 | return System; 186 | })(); 187 | 188 | exports['default'] = System; 189 | module.exports = exports['default']; -------------------------------------------------------------------------------- /src/ecs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entity Component System module 3 | * 4 | * @module ecs 5 | */ 6 | 7 | import Entity from './entity'; 8 | import System from './system'; 9 | import performance from './performance'; 10 | import uid from './uid'; 11 | import { fastSplice } from './utils'; 12 | 13 | /** 14 | * @class ECS 15 | */ 16 | class ECS { 17 | /** 18 | * @constructor 19 | * @class ECS 20 | */ 21 | constructor() { 22 | /** 23 | * Store all entities of the ECS. 24 | * 25 | * @property entities 26 | * @type {Array} 27 | */ 28 | this.entities = []; 29 | 30 | /** 31 | * Store entities which need to be tested at beginning of next tick. 32 | * 33 | * @property entitiesSystemsDirty 34 | * @type {Array} 35 | */ 36 | this.entitiesSystemsDirty = []; 37 | 38 | /** 39 | * Store all systems of the ECS. 40 | * 41 | * @property systems 42 | * @type {Array} 43 | */ 44 | this.systems = []; 45 | 46 | /** 47 | * Count how many updates have been done. 48 | * 49 | * @property updateCounter 50 | * @type {Number} 51 | */ 52 | this.updateCounter = 0; 53 | 54 | this.lastUpdate = performance.now(); 55 | } 56 | /** 57 | * Retrieve an entity by id 58 | * @param {Number} id id of the entity to retrieve 59 | * @return {Entity} The entity if found null otherwise 60 | */ 61 | getEntityById(id) { 62 | for (let i = 0, entity; entity = this.entities[i]; i += 1) { 63 | if (entity.id === id) { 64 | return entity; 65 | } 66 | } 67 | 68 | return null; 69 | } 70 | /** 71 | * Add an entity to the ecs. 72 | * 73 | * @method addEntity 74 | * @param {Entity} entity The entity to add. 75 | */ 76 | addEntity(entity) { 77 | this.entities.push(entity); 78 | entity.addToECS(this); 79 | } 80 | /** 81 | * Remove an entity from the ecs by reference. 82 | * 83 | * @method removeEntity 84 | * @param {Entity} entity reference of the entity to remove 85 | * @return {Entity} the remove entity if any 86 | */ 87 | removeEntity(entity) { 88 | let index = this.entities.indexOf(entity); 89 | let entityRemoved = null; 90 | 91 | // if the entity is not found do nothing 92 | if (index !== -1) { 93 | entityRemoved = this.entities[index]; 94 | 95 | entity.dispose(); 96 | this.removeEntityIfDirty(entityRemoved); 97 | 98 | fastSplice(this.entities, index, 1); 99 | } 100 | 101 | return entityRemoved; 102 | } 103 | /** 104 | * Remove an entity from the ecs by entity id. 105 | * 106 | * @method removeEntityById 107 | * @param {Entity} entityId id of the entity to remove 108 | * @return {Entity} removed entity if any 109 | */ 110 | removeEntityById(entityId) { 111 | for (let i = 0, entity; entity = this.entities[i]; i += 1) { 112 | if (entity.id === entityId) { 113 | entity.dispose(); 114 | this.removeEntityIfDirty(entity); 115 | 116 | fastSplice(this.entities, i, 1); 117 | 118 | return entity; 119 | } 120 | } 121 | } 122 | /** 123 | * Remove an entity from dirty entities by reference. 124 | * 125 | * @private 126 | * @method removeEntityIfDirty 127 | * @param {[type]} entity entity to remove 128 | */ 129 | removeEntityIfDirty(entity) { 130 | let index = this.entitiesSystemsDirty.indexOf(entity); 131 | 132 | if (index !== -1) { 133 | fastSplice(this.entities, index, 1); 134 | } 135 | } 136 | /** 137 | * Add a system to the ecs. 138 | * 139 | * @method addSystem 140 | * @param {System} system system to add 141 | */ 142 | addSystem(system) { 143 | this.systems.push(system); 144 | 145 | // iterate over all entities to eventually add system 146 | for (let i = 0, entity; entity = this.entities[i]; i += 1) { 147 | if (system.test(entity)) { 148 | system.addEntity(entity); 149 | } 150 | } 151 | } 152 | /** 153 | * Remove a system from the ecs. 154 | * 155 | * @method removeSystem 156 | * @param {System} system system reference 157 | */ 158 | removeSystem(system) { 159 | let index = this.systems.indexOf(system); 160 | 161 | if (index !== -1) { 162 | fastSplice(this.systems, index, 1); 163 | system.dispose(); 164 | } 165 | } 166 | /** 167 | * "Clean" entities flagged as dirty by removing unecessary systems and 168 | * adding missing systems. 169 | * 170 | * @private 171 | * @method cleanDirtyEntities 172 | */ 173 | cleanDirtyEntities() { 174 | // jshint maxdepth: 4 175 | 176 | for (let i = 0, entity; entity = this.entitiesSystemsDirty[i]; i += 1) { 177 | for (let s = 0, system; system = this.systems[s]; s += 1) { 178 | // for each dirty entity for each system 179 | let index = entity.systems.indexOf(system); 180 | let entityTest = system.test(entity); 181 | 182 | if (index === -1 && entityTest) { 183 | // if the entity is not added to the system yet and should be, add it 184 | system.addEntity(entity); 185 | } else if (index !== -1 && !entityTest) { 186 | // if the entity is added to the system but should not be, remove it 187 | system.removeEntity(entity); 188 | } 189 | // else we do nothing the current state is OK 190 | } 191 | 192 | entity.systemsDirty = false; 193 | } 194 | // jshint maxdepth: 3 195 | 196 | this.entitiesSystemsDirty = []; 197 | } 198 | /** 199 | * Update the ecs. 200 | * 201 | * @method update 202 | */ 203 | update() { 204 | let now = performance.now(); 205 | let elapsed = now - this.lastUpdate; 206 | 207 | for (let i = 0, system; system = this.systems[i]; i += 1) { 208 | if (this.updateCounter % system.frequency > 0) { 209 | break; 210 | } 211 | 212 | if (this.entitiesSystemsDirty.length) { 213 | // if the last system flagged some entities as dirty check that case 214 | this.cleanDirtyEntities(); 215 | } 216 | 217 | system.updateAll(elapsed); 218 | } 219 | 220 | this.updateCounter += 1; 221 | this.lastUpdate = now; 222 | } 223 | } 224 | 225 | // expose user stuff 226 | ECS.Entity = Entity; 227 | ECS.System = System; 228 | ECS.uid = uid; 229 | 230 | export default ECS; 231 | -------------------------------------------------------------------------------- /src/entity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ecs 3 | */ 4 | 5 | import { UIDGenerator, DefaultUIDGenerator } from './uid'; 6 | import { fastSplice } from './utils'; 7 | 8 | /** 9 | * An entity. 10 | * 11 | * @class Entity 12 | */ 13 | class Entity { 14 | /** 15 | * @class Entity 16 | * @constructor 17 | * 18 | * @param {Number|UIDGenerator} [idOrUidGenerator=null] The entity id if 19 | * a Number is passed. If an UIDGenerator is passed, the entity will use 20 | * it to generate a new id. If nothing is passed, the entity will use 21 | * the default UIDGenerator. 22 | * 23 | * @param {Array[Component]} [components=[]] An array of initial components. 24 | */ 25 | constructor(idOrUidGenerator, components = []) { 26 | /** 27 | * Unique identifier of the entity. 28 | * 29 | * @property {Number} id 30 | */ 31 | this.id = null; 32 | 33 | // initialize id depending on what is the first argument 34 | if (typeof idOrUidGenerator === 'number') { 35 | // if a number was passed then simply set it as id 36 | this.id = idOrUidGenerator; 37 | } else if (idOrUidGenerator instanceof UIDGenerator) { 38 | // if an instance of UIDGenerator was passed then use it to generate 39 | // the id. This allow the user to use multiple UID generators and 40 | // therefore to create entities with unique ids accross a cluster 41 | // or an async environment. See uid.js for more details 42 | this.id = idOrUidGenerator.next(); 43 | } else { 44 | // if nothing was passed simply use the default generator 45 | this.id = DefaultUIDGenerator.next(); 46 | } 47 | 48 | /** 49 | * Systems applied to the entity. 50 | * 51 | * @property {Array[System]} systems 52 | */ 53 | this.systems = []; 54 | 55 | /** 56 | * Indiquate a change in components (a component was removed or added) 57 | * which require to re-compute entity eligibility to all systems. 58 | * 59 | * @property {Boolean} systemsDirty 60 | */ 61 | this.systemsDirty = false; 62 | 63 | /** 64 | * Components of the entity stored as key-value pairs. 65 | * 66 | * @property {Object} components 67 | */ 68 | this.components = {}; 69 | 70 | // components initialisation 71 | for (let i = 0, component; component = components[i]; i += 1) { 72 | // if a getDefaults method is provided, use it. First because let the 73 | // runtime allocate the component is way more faster than using a copy 74 | // function. Secondly because the user may want to provide some kind 75 | // of logic in components initialisation ALTHOUGH these kind of 76 | // initialisation should be done in enter() handler 77 | if (component.getDefaults) { 78 | this.components[component.name] = component.getDefaults(); 79 | } else { 80 | this.components[component.name] = Object.assign({}, 81 | components[i].defaults); 82 | } 83 | } 84 | 85 | /** 86 | * A reference to parent ECS class. 87 | * @property {ECS} ecs 88 | */ 89 | this.ecs = null; 90 | } 91 | /** 92 | * Set the parent ecs reference. 93 | * 94 | * @private 95 | * @param {ECS} ecs An ECS class instance. 96 | */ 97 | addToECS(ecs) { 98 | this.ecs = ecs; 99 | this.setSystemsDirty(); 100 | } 101 | /** 102 | * Set the systems dirty flag so the ECS knows this entity 103 | * needs to recompute eligibility at the beginning of next 104 | * tick. 105 | */ 106 | setSystemsDirty() { 107 | if (!this.systemsDirty && this.ecs) { 108 | this.systemsDirty = true; 109 | 110 | // notify to parent ECS that this entity needs to be tested next tick 111 | this.ecs.entitiesSystemsDirty.push(this); 112 | } 113 | } 114 | /** 115 | * Add a system to the entity. 116 | * 117 | * @private 118 | * @param {System} system The system to add. 119 | */ 120 | addSystem(system) { 121 | this.systems.push(system); 122 | } 123 | /** 124 | * Remove a system from the entity. 125 | * 126 | * @private 127 | * @param {System} system The system reference to remove. 128 | */ 129 | removeSystem(system) { 130 | let index = this.systems.indexOf(system); 131 | 132 | if (index !== -1) { 133 | fastSplice(this.systems, index, 1); 134 | } 135 | } 136 | /** 137 | * Add a component to the entity. WARNING this method does not copy 138 | * components data but assign directly the reference for maximum 139 | * performances. Be sure not to pass the same component reference to 140 | * many entities. 141 | * 142 | * @param {String} name Attribute name of the component to add. 143 | * @param {Object} data Component data. 144 | */ 145 | addComponent(name, data) { 146 | this.components[name] = data || {}; 147 | this.setSystemsDirty(); 148 | } 149 | /** 150 | * Remove a component from the entity. To preserve performances, we 151 | * simple set the component property to `undefined`. Therefore the 152 | * property is still enumerable after a call to removeComponent() 153 | * 154 | * @param {String} name Name of the component to remove. 155 | */ 156 | removeComponent(name) { 157 | if (!this.components[name]) { 158 | return; 159 | } 160 | 161 | this.components[name] = undefined; 162 | this.setSystemsDirty(); 163 | } 164 | /** 165 | * Update a component field by field, NOT recursively. If the component 166 | * does not exists, this method create it silently. 167 | * 168 | * @method updateComponent 169 | * @param {String} name Name of the component 170 | * @param {Object} data Dict of attributes to update 171 | * @example 172 | * entity.addComponent('kite', {vel: 0, pos: {x: 1}}); 173 | * // entity.component.pos is '{vel: 0, pos: {x: 1}}' 174 | * entity.updateComponent('kite', {angle: 90, pos: {y: 1}}); 175 | * // entity.component.pos is '{vel: 0, angle: 90, pos: {y: 1}}' 176 | */ 177 | updateComponent(name, data) { 178 | let component = this.components[name]; 179 | 180 | if (!component) { 181 | this.addComponent(name, data); 182 | } else { 183 | let keys = Object.keys(data); 184 | 185 | for (let i = 0, key; key = keys[i]; i += 1) { 186 | component[key] = data[key]; 187 | } 188 | } 189 | } 190 | /** 191 | * Update a set of components. 192 | * 193 | * @param {Object} componentsData Dict of components to update. 194 | */ 195 | updateComponents(componentsData) { 196 | let components = Object.keys(componentsData); 197 | 198 | for (let i = 0, component; component = components[i]; i += 1) { 199 | this.updateComponent(component, componentsData[component]); 200 | } 201 | } 202 | /** 203 | * Dispose the entity. 204 | * 205 | * @private 206 | */ 207 | dispose() { 208 | for (var i = 0, system; system = this.systems[0]; i += 1) { 209 | system.removeEntity(this); 210 | } 211 | } 212 | } 213 | 214 | export default Entity; 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Entity Component System 2 | ======================= 3 | 4 | > Entity-component-system (ECS) is an architectural pattern that is mostly 5 | > used in game development. An ECS follows the Composition over inheritance 6 | > principle that allows greater flexibility in defining entities where every 7 | > object in a game's scene is an entity (e.g. enemies, bullets, vehicles, 8 | > etc.). 9 | > *Thanks Wikipédia* 10 | 11 | This library implement the entity component system pattern in EcmaScript6. 12 | 13 | ## Features 14 | 15 | * ES6. 16 | * Barebone. No bullshit. No black magic. Take a look at the sources. 17 | * Flexible. You can subclass the Entity or UIDGenerator classes to implement your own logic. e.g. extend the System class in an EventEmitterSystem class to allow inter-system communication! 18 | * Fast. Intelligently batch your entities and systems so that the minimum amount of time is spent on pure iteration. Benchmarks in a near future. 19 | * Fast even for ECS. The eligibility to systems is computed only when components list change, and in most cases the overhead of systems eligibility will be computed once per entity, when added. Therefore there is no overhead for most iterations. [Iteration is often considered as a flaw of ecs pattern](https://en.wikipedia.org/wiki/Entity_component_system#Drawbacks). 20 | 21 | ## Getting started 22 | 23 | Here is a "minimalist" example of what you can do with `yagl-ecs`. This example is not functionnal and not very useful but illustrate how to declare your components, systems and entities and how to cook that: 24 | 25 | ```js 26 | import ECS from 'yagl-ecs'; 27 | // fake class from example with a keyPressed() method 28 | import Keyboard from 'my/game/keyboard'; 29 | 30 | // components definitions 31 | const Position = { 32 | // you can access the component data on each entity with `entity.components.pos` 33 | name: 'pos', 34 | // defaults attributes for the component. If not precised a void object {} 35 | // is assigned instead. 36 | defaults: {x: 0, y: 0} 37 | }; 38 | 39 | // update entity position based on key pressed 40 | class KeyboardControlSystem extends ECS.Sytem { 41 | // called each game loop 42 | update(entity) { 43 | let {pos} = entity.components; 44 | 45 | // update the entity position according to what is pressed 46 | // can be implemented much better :) kiss for example 47 | if (Keyboard.keyPressed('up')) return pos.y -= 1; 48 | if (Keyboard.keyPressed('down')) return pos.y += 1; 49 | if (Keyboard.keyPressed('left')) return pos.x -= 1; 50 | if (Keyboard.keyPressed('right')) return pos.x += 1; 51 | } 52 | } 53 | 54 | // render entities as square 55 | class RenderingSystem extends ECS.System { 56 | // when constructing this system you must pass a canvas context 57 | constructor(ctx) { 58 | this.ctx = ctx; 59 | } 60 | // only entities passing this test will be added to this system 61 | // if omitted, all entities are added 62 | test(entity) { 63 | // the entity must have a position component 64 | return !!entity.components.pos; 65 | } 66 | // called when an entity is added to the system 67 | enter(entity) { 68 | // super useful variable (and comment) 69 | entity.iAmRendered = true; 70 | } 71 | update(entity) { 72 | let {pos} = entity.components; 73 | 74 | this.ctx.fillRect(pos.x - 5, pos.y - 5, 10, 10); 75 | } 76 | // called when an entity is removed the system 77 | exit(entity) { 78 | entity.iAmRendered = false; 79 | } 80 | } 81 | 82 | // This is a debugging system that you will typically use during development 83 | // but that you want to remove in production. Nothing easier with ECS 84 | class DebugSystem extends ECS.System { 85 | // called when an entity is added to the system 86 | enter(entity) { 87 | // attach entity to window so the dev can play with it in the console 88 | // note: don't forget this is an example, no real world code 89 | window.ecsDebugEntity = entity; 90 | } 91 | // There is not update() method. Apart from enter() and exit(), this system 92 | // has no overhead on the game loop 93 | 94 | // called when an entity is removed from the system 95 | exit(entity) { 96 | window.ecsDebugEntity = null; 97 | } 98 | } 99 | 100 | // game loop 101 | let canvas, ctx, ecs; 102 | function gameLoop() { 103 | canvas.width = canvas.width; // reset the canvas - harsh way. 104 | 105 | // iterate through entities and apply elligible system 106 | ecs.update(); 107 | 108 | requestAnimationFrame(gameLoop); 109 | } 110 | 111 | // game initialisation 112 | canvas = document.getElementById('renderer'); 113 | ctx = canvas.getContext('2d'); 114 | 115 | ecs = new ECS(); 116 | 117 | // add the system. you can do this at any time since adding/removing a system 118 | // to the ECS will take into account existing entities 119 | ecs.addSystem(new KeyboardControlSystem()); 120 | ecs.addSystem(new RenderingSystem(ctx)); 121 | if (DEBUG_ENABLED) ecs.addSystem(new DebugSystem()); 122 | 123 | // then you can start to add entities 124 | // note: in this example the keyboard control ALL entities on screen 125 | let entity = new ECS.Entity([Position]); 126 | 127 | // At the beginning we place the entity at the center 128 | // 129 | // updateComponent() is another way to update component data 130 | // IMO I prefer accessing components directly inside system update() method 131 | // because this is faster than method call. Anyway, this form is more 132 | // convenient for entity initialisation because it does a merge with defaults 133 | // component attributes 134 | entity.updateComponent('pos', { 135 | x: canvas.width / 2, 136 | y: canvas.height / 2 137 | }); 138 | 139 | // finally start the game loop 140 | gameLoop(); 141 | ``` 142 | 143 | As soon as I have time, I'll provide a real world live example on [yagl.github.io](yagl.github.io). 144 | 145 | If you have more in-depth questions about how to structure a bigger project with ECS, do not hesitate to contact me. I would be happy to give you tips on components and systems encapsulation (this is very important for ECS pattern, your systems and components must be as atomic as possible). 146 | 147 | ## Documentation 148 | 149 | The full documentation of methods can be found on [yagl.github.io/docs/ecs](yagl.github.io/docs/ecs). Please note that documentation is still a WIP. 150 | 151 | ## Roadmap 152 | 153 | I'll publish the 0.1 by the end of march. Below a list of TODOs even if the target is not 0.1. 154 | 155 | * Maybe more unit test 156 | * Complete and publish documentation 157 | * Maybe create a benchmark to track performance evolution 158 | 159 | ## License 160 | 161 | Copyright (c) 2015 Pierre BEAUJEU 162 | 163 | Permission is hereby granted, free of charge, to any person obtaining a copy 164 | of this software and associated documentation files (the "Software"), to deal 165 | in the Software without restriction, including without limitation the rights 166 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 167 | copies of the Software, and to permit persons to whom the Software is 168 | furnished to do so, subject to the following conditions: 169 | 170 | The above copyright notice and this permission notice shall be included in 171 | all copies or substantial portions of the Software. 172 | 173 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 174 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 175 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 176 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 177 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 178 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 179 | THE SOFTWARE. 180 | -------------------------------------------------------------------------------- /dist/ecs.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { 2 | value: true 3 | }); 4 | 5 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 10 | 11 | /** 12 | * Entity Component System module 13 | * 14 | * @module ecs 15 | */ 16 | 17 | var _entity = require('./entity'); 18 | 19 | var _entity2 = _interopRequireDefault(_entity); 20 | 21 | var _system = require('./system'); 22 | 23 | var _system2 = _interopRequireDefault(_system); 24 | 25 | var _performance = require('./performance'); 26 | 27 | var _performance2 = _interopRequireDefault(_performance); 28 | 29 | var _uid = require('./uid'); 30 | 31 | var _uid2 = _interopRequireDefault(_uid); 32 | 33 | var _utils = require('./utils'); 34 | 35 | /** 36 | * @class ECS 37 | */ 38 | 39 | var ECS = (function () { 40 | /** 41 | * @constructor 42 | * @class ECS 43 | */ 44 | 45 | function ECS() { 46 | _classCallCheck(this, ECS); 47 | 48 | /** 49 | * Store all entities of the ECS. 50 | * 51 | * @property entities 52 | * @type {Array} 53 | */ 54 | this.entities = []; 55 | 56 | /** 57 | * Store entities which need to be tested at beginning of next tick. 58 | * 59 | * @property entitiesSystemsDirty 60 | * @type {Array} 61 | */ 62 | this.entitiesSystemsDirty = []; 63 | 64 | /** 65 | * Store all systems of the ECS. 66 | * 67 | * @property systems 68 | * @type {Array} 69 | */ 70 | this.systems = []; 71 | 72 | /** 73 | * Count how many updates have been done. 74 | * 75 | * @property updateCounter 76 | * @type {Number} 77 | */ 78 | this.updateCounter = 0; 79 | 80 | this.lastUpdate = _performance2['default'].now(); 81 | } 82 | 83 | // expose user stuff 84 | 85 | /** 86 | * Retrieve an entity by id 87 | * @param {Number} id id of the entity to retrieve 88 | * @return {Entity} The entity if found null otherwise 89 | */ 90 | 91 | _createClass(ECS, [{ 92 | key: 'getEntityById', 93 | value: function getEntityById(id) { 94 | for (var i = 0, entity = undefined; entity = this.entities[i]; i += 1) { 95 | if (entity.id === id) { 96 | return entity; 97 | } 98 | } 99 | 100 | return null; 101 | } 102 | 103 | /** 104 | * Add an entity to the ecs. 105 | * 106 | * @method addEntity 107 | * @param {Entity} entity The entity to add. 108 | */ 109 | }, { 110 | key: 'addEntity', 111 | value: function addEntity(entity) { 112 | this.entities.push(entity); 113 | entity.addToECS(this); 114 | } 115 | 116 | /** 117 | * Remove an entity from the ecs by reference. 118 | * 119 | * @method removeEntity 120 | * @param {Entity} entity reference of the entity to remove 121 | * @return {Entity} the remove entity if any 122 | */ 123 | }, { 124 | key: 'removeEntity', 125 | value: function removeEntity(entity) { 126 | var index = this.entities.indexOf(entity); 127 | var entityRemoved = null; 128 | 129 | // if the entity is not found do nothing 130 | if (index !== -1) { 131 | entityRemoved = this.entities[index]; 132 | 133 | entity.dispose(); 134 | this.removeEntityIfDirty(entityRemoved); 135 | 136 | (0, _utils.fastSplice)(this.entities, index, 1); 137 | } 138 | 139 | return entityRemoved; 140 | } 141 | 142 | /** 143 | * Remove an entity from the ecs by entity id. 144 | * 145 | * @method removeEntityById 146 | * @param {Entity} entityId id of the entity to remove 147 | * @return {Entity} removed entity if any 148 | */ 149 | }, { 150 | key: 'removeEntityById', 151 | value: function removeEntityById(entityId) { 152 | for (var i = 0, entity = undefined; entity = this.entities[i]; i += 1) { 153 | if (entity.id === entityId) { 154 | entity.dispose(); 155 | this.removeEntityIfDirty(entity); 156 | 157 | (0, _utils.fastSplice)(this.entities, i, 1); 158 | 159 | return entity; 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * Remove an entity from dirty entities by reference. 166 | * 167 | * @private 168 | * @method removeEntityIfDirty 169 | * @param {[type]} entity entity to remove 170 | */ 171 | }, { 172 | key: 'removeEntityIfDirty', 173 | value: function removeEntityIfDirty(entity) { 174 | var index = this.entitiesSystemsDirty.indexOf(entity); 175 | 176 | if (index !== -1) { 177 | (0, _utils.fastSplice)(this.entities, index, 1); 178 | } 179 | } 180 | 181 | /** 182 | * Add a system to the ecs. 183 | * 184 | * @method addSystem 185 | * @param {System} system system to add 186 | */ 187 | }, { 188 | key: 'addSystem', 189 | value: function addSystem(system) { 190 | this.systems.push(system); 191 | 192 | // iterate over all entities to eventually add system 193 | for (var i = 0, entity = undefined; entity = this.entities[i]; i += 1) { 194 | if (system.test(entity)) { 195 | system.addEntity(entity); 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Remove a system from the ecs. 202 | * 203 | * @method removeSystem 204 | * @param {System} system system reference 205 | */ 206 | }, { 207 | key: 'removeSystem', 208 | value: function removeSystem(system) { 209 | var index = this.systems.indexOf(system); 210 | 211 | if (index !== -1) { 212 | (0, _utils.fastSplice)(this.systems, index, 1); 213 | system.dispose(); 214 | } 215 | } 216 | 217 | /** 218 | * "Clean" entities flagged as dirty by removing unecessary systems and 219 | * adding missing systems. 220 | * 221 | * @private 222 | * @method cleanDirtyEntities 223 | */ 224 | }, { 225 | key: 'cleanDirtyEntities', 226 | value: function cleanDirtyEntities() { 227 | // jshint maxdepth: 4 228 | 229 | for (var i = 0, entity = undefined; entity = this.entitiesSystemsDirty[i]; i += 1) { 230 | for (var s = 0, system = undefined; system = this.systems[s]; s += 1) { 231 | // for each dirty entity for each system 232 | var index = entity.systems.indexOf(system); 233 | var entityTest = system.test(entity); 234 | 235 | if (index === -1 && entityTest) { 236 | // if the entity is not added to the system yet and should be, add it 237 | system.addEntity(entity); 238 | } else if (index !== -1 && !entityTest) { 239 | // if the entity is added to the system but should not be, remove it 240 | system.removeEntity(entity); 241 | } 242 | // else we do nothing the current state is OK 243 | } 244 | 245 | entity.systemsDirty = false; 246 | } 247 | // jshint maxdepth: 3 248 | 249 | this.entitiesSystemsDirty = []; 250 | } 251 | 252 | /** 253 | * Update the ecs. 254 | * 255 | * @method update 256 | */ 257 | }, { 258 | key: 'update', 259 | value: function update() { 260 | var now = _performance2['default'].now(); 261 | var elapsed = now - this.lastUpdate; 262 | 263 | for (var i = 0, system = undefined; system = this.systems[i]; i += 1) { 264 | if (this.updateCounter % system.frequency > 0) { 265 | break; 266 | } 267 | 268 | if (this.entitiesSystemsDirty.length) { 269 | // if the last system flagged some entities as dirty check that case 270 | this.cleanDirtyEntities(); 271 | } 272 | 273 | system.updateAll(elapsed); 274 | } 275 | 276 | this.updateCounter += 1; 277 | this.lastUpdate = now; 278 | } 279 | }]); 280 | 281 | return ECS; 282 | })(); 283 | 284 | ECS.Entity = _entity2['default']; 285 | ECS.System = _system2['default']; 286 | ECS.uid = _uid2['default']; 287 | 288 | exports['default'] = ECS; 289 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/entity.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { 2 | value: true 3 | }); 4 | 5 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 8 | 9 | /** 10 | * @module ecs 11 | */ 12 | 13 | var _uid = require('./uid'); 14 | 15 | var _utils = require('./utils'); 16 | 17 | /** 18 | * An entity. 19 | * 20 | * @class Entity 21 | */ 22 | 23 | var Entity = (function () { 24 | /** 25 | * @class Entity 26 | * @constructor 27 | * 28 | * @param {Number|UIDGenerator} [idOrUidGenerator=null] The entity id if 29 | * a Number is passed. If an UIDGenerator is passed, the entity will use 30 | * it to generate a new id. If nothing is passed, the entity will use 31 | * the default UIDGenerator. 32 | * 33 | * @param {Array[Component]} [components=[]] An array of initial components. 34 | */ 35 | 36 | function Entity(idOrUidGenerator) { 37 | var components = arguments.length <= 1 || arguments[1] === undefined ? [] : arguments[1]; 38 | 39 | _classCallCheck(this, Entity); 40 | 41 | /** 42 | * Unique identifier of the entity. 43 | * 44 | * @property {Number} id 45 | */ 46 | this.id = null; 47 | 48 | // initialize id depending on what is the first argument 49 | if (typeof idOrUidGenerator === 'number') { 50 | // if a number was passed then simply set it as id 51 | this.id = idOrUidGenerator; 52 | } else if (idOrUidGenerator instanceof _uid.UIDGenerator) { 53 | // if an instance of UIDGenerator was passed then use it to generate 54 | // the id. This allow the user to use multiple UID generators and 55 | // therefore to create entities with unique ids accross a cluster 56 | // or an async environment. See uid.js for more details 57 | this.id = idOrUidGenerator.next(); 58 | } else { 59 | // if nothing was passed simply use the default generator 60 | this.id = _uid.DefaultUIDGenerator.next(); 61 | } 62 | 63 | /** 64 | * Systems applied to the entity. 65 | * 66 | * @property {Array[System]} systems 67 | */ 68 | this.systems = []; 69 | 70 | /** 71 | * Indiquate a change in components (a component was removed or added) 72 | * which require to re-compute entity eligibility to all systems. 73 | * 74 | * @property {Boolean} systemsDirty 75 | */ 76 | this.systemsDirty = false; 77 | 78 | /** 79 | * Components of the entity stored as key-value pairs. 80 | * 81 | * @property {Object} components 82 | */ 83 | this.components = {}; 84 | 85 | // components initialisation 86 | for (var i = 0, component = undefined; component = components[i]; i += 1) { 87 | // if a getDefaults method is provided, use it. First because let the 88 | // runtime allocate the component is way more faster than using a copy 89 | // function. Secondly because the user may want to provide some kind 90 | // of logic in components initialisation ALTHOUGH these kind of 91 | // initialisation should be done in enter() handler 92 | if (component.getDefaults) { 93 | this.components[component.name] = component.getDefaults(); 94 | } else { 95 | this.components[component.name] = Object.assign({}, components[i].defaults); 96 | } 97 | } 98 | 99 | /** 100 | * A reference to parent ECS class. 101 | * @property {ECS} ecs 102 | */ 103 | this.ecs = null; 104 | } 105 | 106 | /** 107 | * Set the parent ecs reference. 108 | * 109 | * @private 110 | * @param {ECS} ecs An ECS class instance. 111 | */ 112 | 113 | _createClass(Entity, [{ 114 | key: 'addToECS', 115 | value: function addToECS(ecs) { 116 | this.ecs = ecs; 117 | this.setSystemsDirty(); 118 | } 119 | 120 | /** 121 | * Set the systems dirty flag so the ECS knows this entity 122 | * needs to recompute eligibility at the beginning of next 123 | * tick. 124 | */ 125 | }, { 126 | key: 'setSystemsDirty', 127 | value: function setSystemsDirty() { 128 | if (!this.systemsDirty && this.ecs) { 129 | this.systemsDirty = true; 130 | 131 | // notify to parent ECS that this entity needs to be tested next tick 132 | this.ecs.entitiesSystemsDirty.push(this); 133 | } 134 | } 135 | 136 | /** 137 | * Add a system to the entity. 138 | * 139 | * @private 140 | * @param {System} system The system to add. 141 | */ 142 | }, { 143 | key: 'addSystem', 144 | value: function addSystem(system) { 145 | this.systems.push(system); 146 | } 147 | 148 | /** 149 | * Remove a system from the entity. 150 | * 151 | * @private 152 | * @param {System} system The system reference to remove. 153 | */ 154 | }, { 155 | key: 'removeSystem', 156 | value: function removeSystem(system) { 157 | var index = this.systems.indexOf(system); 158 | 159 | if (index !== -1) { 160 | (0, _utils.fastSplice)(this.systems, index, 1); 161 | } 162 | } 163 | 164 | /** 165 | * Add a component to the entity. WARNING this method does not copy 166 | * components data but assign directly the reference for maximum 167 | * performances. Be sure not to pass the same component reference to 168 | * many entities. 169 | * 170 | * @param {String} name Attribute name of the component to add. 171 | * @param {Object} data Component data. 172 | */ 173 | }, { 174 | key: 'addComponent', 175 | value: function addComponent(name, data) { 176 | this.components[name] = data || {}; 177 | this.setSystemsDirty(); 178 | } 179 | 180 | /** 181 | * Remove a component from the entity. To preserve performances, we 182 | * simple set the component property to `undefined`. Therefore the 183 | * property is still enumerable after a call to removeComponent() 184 | * 185 | * @param {String} name Name of the component to remove. 186 | */ 187 | }, { 188 | key: 'removeComponent', 189 | value: function removeComponent(name) { 190 | if (!this.components[name]) { 191 | return; 192 | } 193 | 194 | this.components[name] = undefined; 195 | this.setSystemsDirty(); 196 | } 197 | 198 | /** 199 | * Update a component field by field, NOT recursively. If the component 200 | * does not exists, this method create it silently. 201 | * 202 | * @method updateComponent 203 | * @param {String} name Name of the component 204 | * @param {Object} data Dict of attributes to update 205 | * @example 206 | * entity.addComponent('kite', {vel: 0, pos: {x: 1}}); 207 | * // entity.component.pos is '{vel: 0, pos: {x: 1}}' 208 | * entity.updateComponent('kite', {angle: 90, pos: {y: 1}}); 209 | * // entity.component.pos is '{vel: 0, angle: 90, pos: {y: 1}}' 210 | */ 211 | }, { 212 | key: 'updateComponent', 213 | value: function updateComponent(name, data) { 214 | var component = this.components[name]; 215 | 216 | if (!component) { 217 | this.addComponent(name, data); 218 | } else { 219 | var keys = Object.keys(data); 220 | 221 | for (var i = 0, key = undefined; key = keys[i]; i += 1) { 222 | component[key] = data[key]; 223 | } 224 | } 225 | } 226 | 227 | /** 228 | * Update a set of components. 229 | * 230 | * @param {Object} componentsData Dict of components to update. 231 | */ 232 | }, { 233 | key: 'updateComponents', 234 | value: function updateComponents(componentsData) { 235 | var components = Object.keys(componentsData); 236 | 237 | for (var i = 0, component = undefined; component = components[i]; i += 1) { 238 | this.updateComponent(component, componentsData[component]); 239 | } 240 | } 241 | 242 | /** 243 | * Dispose the entity. 244 | * 245 | * @private 246 | */ 247 | }, { 248 | key: 'dispose', 249 | value: function dispose() { 250 | for (var i = 0, system; system = this.systems[0]; i += 1) { 251 | system.removeEntity(this); 252 | } 253 | } 254 | }]); 255 | 256 | return Entity; 257 | })(); 258 | 259 | exports['default'] = Entity; 260 | module.exports = exports['default']; --------------------------------------------------------------------------------