├── .gitignore ├── lib ├── index.js ├── object-pool.js ├── entity-component-system.js ├── object-pool-test.js ├── entity-component-system-test.js ├── entity-pool.js └── entity-pool-test.js ├── package.json ├── LICENSE ├── .eslintrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | EntityComponentSystem: require("./entity-component-system"), 3 | EntityPool: require("./entity-pool") 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entity-component-system", 3 | "version": "4.0.5", 4 | "description": "An implementation of the Entity component system (ECS) pattern used commonly in video games.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "build": "npm run lint && npm test", 9 | "test": "tape 'lib/**/*-test.js' | tap-spec" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ericlathrop/entity-component-system.git" 14 | }, 15 | "keywords": [ 16 | "game", 17 | "video game", 18 | "ecs", 19 | "entity component system" 20 | ], 21 | "author": "Eric Lathrop (http://ericlathrop.com/)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/ericlathrop/entity-component-system/issues" 25 | }, 26 | "homepage": "https://github.com/ericlathrop/entity-component-system", 27 | "devDependencies": { 28 | "eslint": "^3.3.0", 29 | "tap-spec": "^4.1.1", 30 | "tape": "^4.2.2" 31 | }, 32 | "dependencies": { 33 | "present": "^1.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Eric Lathrop 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/object-pool.js: -------------------------------------------------------------------------------- 1 | function ObjectPool(factory, size) { 2 | if (typeof factory !== "function") { 3 | throw new TypeError("ObjectPool expects a factory function, got ", factory); 4 | } 5 | if (size && size < 1 || size === 0) { 6 | throw new RangeError("ObjectPool expects an initial size greater than zero"); 7 | } 8 | this.factory = factory; 9 | this.size = size || 1; 10 | this.dead = []; 11 | 12 | for (var i = 0; i < size; i++) { 13 | this.dead.push(factory()); 14 | } 15 | } 16 | ObjectPool.prototype.alloc = function() { 17 | var factory = this.factory; 18 | var obj; 19 | if (this.dead.length > 0) { 20 | obj = this.dead.pop(); 21 | } else { 22 | obj = factory(); 23 | /* we assume the number "alive" (not stored here) 24 | * must be equal to this.size, so by creating 25 | * that many more objects, (including obj above), 26 | * we double the size of the pool. 27 | */ 28 | for (var i = 0; i < this.size - 1; i++) { 29 | this.dead.push(factory()); 30 | } 31 | this.size *= 2; 32 | } 33 | return obj; 34 | }; 35 | ObjectPool.prototype.free = function(obj) { 36 | this.dead.push(obj); 37 | }; 38 | 39 | module.exports = ObjectPool; 40 | -------------------------------------------------------------------------------- /lib/entity-component-system.js: -------------------------------------------------------------------------------- 1 | var present = require("present"); 2 | 3 | function EntityComponentSystem() { 4 | this.systems = []; 5 | this.systemNames = []; 6 | this.systemTimes = []; 7 | this.runCount = 0; 8 | } 9 | EntityComponentSystem.prototype.add = function(code) { 10 | this.systems.push(code); 11 | this.systemNames.push(code.name); 12 | this.systemTimes.push(0); 13 | }; 14 | EntityComponentSystem.prototype.addEach = function(code, search) { 15 | this.systems.push(function(entities, elapsed) { 16 | var keys = entities.find(search); 17 | for (var i = 0; i < keys.length; i++) { 18 | code(keys[i], elapsed); 19 | } 20 | }); 21 | this.systemNames.push(code.name); 22 | this.systemTimes.push(0); 23 | }; 24 | EntityComponentSystem.prototype.run = function(entities, elapsed) { 25 | for (var i = 0; i < this.systems.length; i++) { 26 | var start = present(); 27 | this.systems[i](entities, elapsed); 28 | var end = present(); 29 | this.systemTimes[i] += end - start; 30 | } 31 | this.runCount++; 32 | }; 33 | EntityComponentSystem.prototype.runs = function() { 34 | return this.runCount; 35 | }; 36 | EntityComponentSystem.prototype.timings = function() { 37 | return this.systemNames.map(function(name, i) { 38 | return { 39 | name: name, 40 | time: this.systemTimes[i] 41 | }; 42 | }.bind(this)); 43 | }; 44 | EntityComponentSystem.prototype.resetTimings = function() { 45 | this.systemTimes = this.systemTimes.map(function() { 46 | return 0; 47 | }); 48 | }; 49 | 50 | module.exports = EntityComponentSystem; 51 | -------------------------------------------------------------------------------- /lib/object-pool-test.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | 3 | var ObjectPool = require("./object-pool"); 4 | 5 | test("objects get reused after being returned to pool", function(t) { 6 | t.plan(1); 7 | 8 | var pool = new ObjectPool(function() { 9 | return {}; 10 | }); 11 | 12 | var a = pool.alloc(); 13 | pool.alloc(); 14 | pool.free(a); 15 | var b = pool.alloc(); 16 | 17 | t.equal(a, b); 18 | }); 19 | 20 | test("allocated objects match result of factory function", function(t) { 21 | t.plan(1); 22 | 23 | function factory() { 24 | return { 25 | a: 1, 26 | b: { 27 | c: "hello world" 28 | }, 29 | d: [3, 2, 1] 30 | }; 31 | } 32 | 33 | var pool = new ObjectPool(factory); 34 | 35 | var allocated = pool.alloc(); 36 | var factoryResult = factory(); 37 | 38 | t.deepEqual(allocated, factoryResult); 39 | }); 40 | 41 | test("ObjectPool constructor creates number of objects specified", function(t) { 42 | t.plan(4); 43 | 44 | var size = 3; 45 | 46 | var pool = new ObjectPool(function() { 47 | return {}; 48 | }, size); 49 | var dead = pool.dead; 50 | 51 | for (var i = 0; i < size; i++) { 52 | t.ok(dead[i]); 53 | } 54 | t.notOk(dead[size], "no more objects created than specified"); 55 | }); 56 | 57 | test("object pool size doubles when limit is exceeded", function(t) { 58 | t.plan(6); 59 | 60 | var size = 2; 61 | 62 | var pool = new ObjectPool(function() { 63 | return {}; 64 | }, size); 65 | 66 | t.equal(pool.size, 2); 67 | pool.alloc(); // 1 68 | t.equal(pool.size, 2); 69 | pool.alloc(); // 2 70 | t.equal(pool.size, 2); 71 | pool.alloc(); // 3 72 | t.equal(pool.size, 4); 73 | pool.alloc(); // 4 74 | t.equal(pool.size, 4); 75 | pool.alloc(); // 5 76 | t.equal(pool.size, 8); 77 | }); 78 | -------------------------------------------------------------------------------- /lib/entity-component-system-test.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | 3 | var ECS = require("./entity-component-system"); 4 | var Pool = require("./entity-pool"); 5 | var present = require("present"); 6 | 7 | test("run with system and entities calls system with entities", function(t) { 8 | t.plan(1); 9 | 10 | var entities = [{}]; 11 | var ecs = new ECS(); 12 | var done = function(arg) { 13 | t.deepEqual(arg, entities); 14 | }; 15 | ecs.add(done); 16 | ecs.run(entities); 17 | }); 18 | 19 | test("run with each system and array of entities calls system with each entity", function(t) { 20 | t.plan(2); 21 | 22 | var entities = new Pool(); 23 | var id = entities.create(); 24 | entities.setComponent(id, "name", "jimmy"); 25 | 26 | var ecs = new ECS(); 27 | var done = function(arg, arg2) { 28 | t.deepEqual(arg, id); 29 | t.deepEqual(arg2, "arg2"); 30 | }; 31 | ecs.addEach(done, "name"); 32 | ecs.run(entities, "arg2"); 33 | }); 34 | 35 | test("runs returns number of runs", function(t) { 36 | t.plan(1); 37 | 38 | var ecs = new ECS(); 39 | ecs.run(); 40 | t.equal(ecs.runs(), 1); 41 | }); 42 | 43 | function waitForTimeToChange() { 44 | var start = present(); 45 | while (present() === start) {} // eslint-disable-line no-empty 46 | } 47 | 48 | test("timings returns timing information for each system", function(t) { 49 | t.plan(2); 50 | 51 | var ecs = new ECS(); 52 | ecs.add(waitForTimeToChange); 53 | ecs.run(); 54 | var timings = ecs.timings(); 55 | t.equal(timings[0].name, "waitForTimeToChange"); 56 | t.ok(timings[0].time > 0, "should be greater than 0"); 57 | }); 58 | 59 | test("resetTimings resets timing information to zero", function(t) { 60 | t.plan(1); 61 | 62 | var ecs = new ECS(); 63 | ecs.add(waitForTimeToChange); 64 | ecs.run(); 65 | ecs.resetTimings(); 66 | var timings = ecs.timings(); 67 | t.equal(timings[0].time, 0); 68 | }); 69 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended" 7 | ], 8 | "globals": { 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [], 18 | "rules": { 19 | "camelcase": 2, 20 | "comma-spacing": 2, 21 | "consistent-return": 2, 22 | "curly": [2, "all"], 23 | "dot-notation": [2, { "allowKeywords": true }], 24 | "eol-last": 2, 25 | "eqeqeq": 2, 26 | "indent": [2, 2, {"SwitchCase": 1 }], 27 | "jsx-quotes": [2, "prefer-double"], 28 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 29 | "keyword-spacing": 2, 30 | "linebreak-style": [2, "unix"], 31 | "new-cap": [2, {"capIsNewExceptions": ["Map", "List"]}], 32 | "new-parens": 2, 33 | "no-alert": 2, 34 | "no-array-constructor": 2, 35 | "no-caller": 2, 36 | "no-catch-shadow": 2, 37 | "no-console": 0, 38 | "no-eval": 2, 39 | "no-extend-native": 2, 40 | "no-extra-bind": 2, 41 | "no-extra-parens": [2, "functions"], 42 | "no-implied-eval": 2, 43 | "no-iterator": 2, 44 | "no-label-var": 2, 45 | "no-labels": 2, 46 | "no-lone-blocks": 2, 47 | "no-loop-func": 2, 48 | "no-multi-spaces": 2, 49 | "no-multi-str": 2, 50 | "no-native-reassign": 2, 51 | "no-new": 2, 52 | "no-new-func": 2, 53 | "no-new-object": 2, 54 | "no-new-wrappers": 2, 55 | "no-octal-escape": 2, 56 | "no-process-exit": 2, 57 | "no-proto": 2, 58 | "no-return-assign": 2, 59 | "no-script-url": 2, 60 | "no-sequences": 2, 61 | "no-shadow": 2, 62 | "no-shadow-restricted-names": 2, 63 | "no-spaced-func": 2, 64 | "no-trailing-spaces": 2, 65 | "no-undef-init": 2, 66 | "no-underscore-dangle": 2, 67 | "no-unused-expressions": 2, 68 | "no-with": 2, 69 | "quotes": [2, "double"], 70 | "semi": [2, "always"], 71 | "semi-spacing": [2, {"before": false, "after": true}], 72 | "space-infix-ops": 2, 73 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 74 | "strict": [2, "function"], 75 | "yoda": [2, "never"] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | ## [4.0.5] - 2017-02-28 8 | ### Fixed 9 | - EntityPool.removeComponent() now fires "remove" callback before resetting the 10 | removed component. 11 | 12 | ## [4.0.4] - 2016-08-14 13 | ### Fixed 14 | - Prevent entity from being in search multiple times when calling 15 | EntityPool.setComponent() with a primitive value 16 | 17 | ## [4.0.3] - 2016-08-14 18 | ### Fixed 19 | - More specific error message to aid in debugging 20 | 21 | ## [4.0.2] - 2016-08-14 22 | ### Fixed 23 | - Change from NoSuchComponentPoolException to Error so stack traces work 24 | 25 | ## [4.0.1] - 2016-08-14 26 | ### Fixed 27 | - NoSuchComponentPoolException is now a proper Error 28 | 29 | ## [4.0.0] - 2016-08-14 30 | ### Added 31 | - `EntityPool.registerComponent` registers a component factory, enabling object 32 | pooling. 33 | - `EntityPool.addComponent` adds a component from the object pool to an entity. 34 | Replaces `set`. 35 | ### Changed 36 | - `EntityPool.get` was renamed to `getComponent` 37 | - `EntityPool.remove` was renamed to `removeComponent` 38 | - `EntityPool.set` was renamed to `setComponent`, and should now only be called with primitive values. This makes it harder to break the object pooling behavior, requiring you to use `get` to manipulate components without accidentally allocating new ones. 39 | 40 | 41 | ## [3.0.0] - 2016-06-04 42 | ### Changed 43 | - Change dynamic arguments in `EntityComponentSystem.run()` to fixed arguments for dramatic speed improvement 44 | 45 | ## [2.2.0] - 2015-12-19 46 | ### Added 47 | - Add timing APIs so you can see which system is slow 48 | 49 | ## [2.1.0] - 2015-12-17 50 | ### Added 51 | - The `onRemoveComponent` callback is now invoked with the old component value that was removed 52 | 53 | ## [2.0.1] - 2015-12-17 54 | ### Fixed 55 | - Bumped version number because I accidentally published a beta version as 2.0.0 56 | 57 | ## [2.0.0] - 2015-12-17 58 | ### Added 59 | - `EntityPool` 60 | ### Removed 61 | - `run()` no longer returns timings of systems 62 | 63 | ## [1.2.2] - 2015-12-08 64 | ### Changed 65 | - Adopted a [code of conduct](CODE_OF_CONDUCT.md) 66 | - Switch from jshint to eslint 67 | - Update development dependencies 68 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at eric@ericlathrop.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ 51 | -------------------------------------------------------------------------------- /lib/entity-pool.js: -------------------------------------------------------------------------------- 1 | var ObjectPool = require("./object-pool"); 2 | 3 | function EntityPool() { 4 | this.entities = {}; 5 | this.nextId = 0; 6 | this.entityPool = new ObjectPool(function() { 7 | return { id: this.nextId++ }; 8 | }.bind(this)); 9 | this.componentPools = {}; 10 | this.resetFunctions = {}; 11 | this.searchToComponents = {}; 12 | this.componentToSearches = {}; 13 | this.searchResults = {}; 14 | this.callbacks = {}; 15 | } 16 | EntityPool.prototype.create = function() { 17 | var entity = this.entityPool.alloc(); 18 | this.entities[entity.id] = entity; 19 | return entity.id; 20 | }; 21 | EntityPool.prototype.destroy = function(id) { 22 | var entity = this.entities[id]; 23 | Object.keys(entity).forEach(function(component) { 24 | if (component === "id") { 25 | return; 26 | } 27 | this.removeComponent(id, component); 28 | }.bind(this)); 29 | delete this.entities[id]; 30 | this.entityPool.free(entity); 31 | }; 32 | EntityPool.prototype.registerComponent = function(component, factory, reset, size) { 33 | this.componentPools[component] = new ObjectPool(factory, size); 34 | this.resetFunctions[component] = reset; 35 | }; 36 | // private 37 | EntityPool.prototype.resetComponent = function(id, component) { 38 | var reset = this.resetFunctions[component]; 39 | if (typeof reset === "function") { 40 | reset(this.entities[id][component]); 41 | } 42 | }; 43 | EntityPool.prototype.getComponent = function(id, component) { 44 | return this.entities[id][component]; 45 | }; 46 | EntityPool.prototype.removeComponent = function(id, component) { 47 | var oldValue = this.entities[id][component]; 48 | if (oldValue === undefined) { 49 | return; 50 | } 51 | 52 | for (var i = 0; i < this.componentToSearches[component].length; i++) { 53 | var search = this.componentToSearches[component][i]; 54 | removeFromArray(this.searchResults[search], id); 55 | } 56 | 57 | this.fireCallback("remove", id, component, oldValue); 58 | 59 | if (!isPrimitive(oldValue)) { 60 | this.resetComponent(id, component); 61 | this.componentPools[component].free(oldValue); 62 | } 63 | delete this.entities[id][component]; 64 | }; 65 | EntityPool.prototype.addComponent = function(id, component) { 66 | if (!this.componentPools[component]) { 67 | throw new Error( 68 | "You can't call EntityPool.prototype.addComponent(" + id + ", \"" + component + "\") " + 69 | "for a component name that hasn't been registered with " + 70 | "EntityPool.prototype.registerComponent(component, factory[, reset][, size])." 71 | ); 72 | } 73 | 74 | var predefinedValue = this.entities[id][component]; 75 | if (predefinedValue && !isPrimitive(predefinedValue)) { 76 | this.resetComponent(id, component); 77 | return predefinedValue; 78 | } 79 | 80 | var value = this.componentPools[component].alloc(); 81 | this.setComponentValue(id, component, value); 82 | 83 | return value; 84 | }; 85 | EntityPool.prototype.setComponent = function(id, component, value) { 86 | if (!isPrimitive(value)) { 87 | throw new TypeError( 88 | "You can't call EntityPool.prototype.setComponent(" + id + ", \"" + component + "\", " + JSON.stringify(value) + ") with " + 89 | "a value that isn't of a primitive type (i.e. null, undefined, boolean, " + 90 | "number, string, or symbol). For objects or arrays, use " + 91 | "EntityPool.prototype.addComponent(id, component) and modify " + 92 | "the result it returns." 93 | ); 94 | } 95 | 96 | if (!isPrimitive(this.entities[id][component])) { 97 | throw new Error( 98 | "You can't set a non-primitive type component \"" + component + "\" to a primitive value. " + 99 | "If you must do this, remove the existing component first with " + 100 | "EntityPool.prototype.removeComponent(id, component)." 101 | ); 102 | } 103 | 104 | if (typeof value === "undefined") { 105 | this.removeComponent(id, component); 106 | } else { 107 | this.setComponentValue(id, component, value); 108 | } 109 | }; 110 | // private 111 | EntityPool.prototype.setComponentValue = function(id, component, value) { 112 | var existingValue = this.entities[id][component]; 113 | if (typeof existingValue !== "undefined" && existingValue === value) { 114 | return; 115 | } 116 | 117 | this.entities[id][component] = value; 118 | if (typeof existingValue === "undefined") { 119 | if (this.searchToComponents[component] === undefined) { 120 | this.mapSearch(component, [component]); 121 | } 122 | for (var i = 0; i < this.componentToSearches[component].length; i++) { 123 | var search = this.componentToSearches[component][i]; 124 | if (objectHasProperties(this.searchToComponents[search], this.entities[id])) { 125 | this.searchResults[search].push(id); 126 | } 127 | } 128 | this.fireCallback("add", id, component, value); 129 | } 130 | }; 131 | // private 132 | EntityPool.prototype.addCallback = function(type, component, callback) { 133 | this.callbacks[type] = this.callbacks[type] || {}; 134 | this.callbacks[type][component] = this.callbacks[type][component] || []; 135 | this.callbacks[type][component].push(callback); 136 | }; 137 | // private 138 | EntityPool.prototype.fireCallback = function(type, id, component) { 139 | if (this.callbackQueue) { 140 | this.callbackQueue.push(Array.prototype.slice.call(arguments, 0)); 141 | return; 142 | } 143 | var cbs = this.callbacks[type] || {}; 144 | var ccbs = cbs[component] || []; 145 | var args = Array.prototype.slice.call(arguments, 3); 146 | for (var i = 0; i < ccbs.length; i++) { 147 | ccbs[i].apply(this, [id, component].concat(args)); 148 | } 149 | }; 150 | // private 151 | EntityPool.prototype.fireQueuedCallbacks = function() { 152 | var queue = this.callbackQueue || []; 153 | delete this.callbackQueue; 154 | for (var i = 0; i < queue.length; i++) { 155 | this.fireCallback.apply(this, queue[i]); 156 | } 157 | }; 158 | 159 | EntityPool.prototype.onAddComponent = function(component, callback) { 160 | this.addCallback("add", component, callback); 161 | }; 162 | EntityPool.prototype.onRemoveComponent = function(component, callback) { 163 | this.addCallback("remove", component, callback); 164 | }; 165 | EntityPool.prototype.find = function(search) { 166 | return this.searchResults[search] || []; 167 | }; 168 | // private 169 | EntityPool.prototype.mapSearch = function(search, components) { 170 | if (this.searchToComponents[search] !== undefined) { 171 | throw "the search \"" + search + "\" was already registered"; 172 | } 173 | 174 | this.searchToComponents[search] = components.slice(0); 175 | 176 | for (var i = 0; i < components.length; i++) { 177 | var c = components[i]; 178 | if (this.componentToSearches[c] === undefined) { 179 | this.componentToSearches[c] = [search]; 180 | } else { 181 | this.componentToSearches[c].push(search); 182 | } 183 | } 184 | 185 | this.searchResults[search] = []; 186 | }; 187 | EntityPool.prototype.registerSearch = function(search, components) { 188 | this.mapSearch(search, components); 189 | this.searchResults[search] = objectValues(this.entities) 190 | .filter(objectHasProperties.bind(undefined, components)) 191 | .map(entityId); 192 | }; 193 | 194 | EntityPool.prototype.load = function(entities) { 195 | this.callbackQueue = []; 196 | entities.forEach(function(entity) { 197 | var id = entity.id; 198 | var allocatedEntity = this.entityPool.alloc(); 199 | allocatedEntity.id = id; 200 | this.entities[id] = allocatedEntity; 201 | if (this.nextId <= id) { 202 | this.nextId = id + 1; 203 | } 204 | Object.keys(entity).forEach(function(component) { 205 | if (component === "id") { 206 | return; 207 | } 208 | var valueToLoad = entity[component]; 209 | if (isPrimitive(valueToLoad)) { 210 | this.setComponent(id, component, valueToLoad); 211 | return; 212 | } 213 | var newComponentObject = this.addComponent(id, component); 214 | Object.keys(valueToLoad).forEach(function(key) { 215 | newComponentObject[key] = valueToLoad[key]; 216 | }); 217 | }.bind(this)); 218 | }.bind(this)); 219 | this.fireQueuedCallbacks(); 220 | }; 221 | 222 | EntityPool.prototype.save = function() { 223 | return objectValues(this.entities); 224 | }; 225 | 226 | function removeFromArray(array, item) { 227 | var i = array.indexOf(item); 228 | if (i !== -1) { 229 | array.splice(i, 1); 230 | } 231 | return array; 232 | } 233 | 234 | function entityId(entity) { 235 | return entity.id; 236 | } 237 | function objectHasProperties(properties, obj) { 238 | return properties.every(Object.prototype.hasOwnProperty.bind(obj)); 239 | } 240 | 241 | function objectValues(obj) { 242 | return Object.keys(obj).map(function(key) { 243 | return obj[key]; 244 | }); 245 | } 246 | 247 | /* returns true if the value is a primitive 248 | * type a.k.a. null, undefined, boolean, 249 | * number, string, or symbol. 250 | */ 251 | function isPrimitive(value) { 252 | return typeof value !== "object" || value === null; 253 | } 254 | 255 | module.exports = EntityPool; 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # entity-component-system 2 | 3 | An implementation of the [Entity Component 4 | System](https://en.wikipedia.org/wiki/Entity_component_system) (ECS) pattern 5 | used commonly in video games. 6 | 7 | ECS is a way of organizing a system using composition instead of inheritance. It 8 | allows you to turn behaviors on and off by adding and removing components to 9 | entities. 10 | 11 | This module manages the running of a list of "systems" over a collection of 12 | entities. 13 | 14 | * An "entity" is a logical object in a game. 15 | * A "component" is a chunk of data attached to an entity. 16 | * A "system" is a function that runs on all entities with specific components. 17 | 18 | The only way to make changes to an entity is to create/edit/delete components 19 | attached to it. 20 | 21 | 22 | # Example 23 | 24 | This is an example game loop: 25 | 26 | ```javascript 27 | var EntityComponentSystem = require("entity-component-system").EntityComponentSystem; 28 | var EntityPool = require("entity-component-system").EntityPool; 29 | 30 | var ecs = new EntityComponentSystem(); 31 | 32 | function drawBackground(entities, elapsed) { /* ... */ } 33 | ecs.add(drawBackground); 34 | 35 | function drawEntity(entity, elapsed) { /* ... */ } 36 | ecs.addEach(drawEntity, "sprite"); // only run on entities with a "sprite" component 37 | 38 | var entities = new EntityPool(); 39 | function spriteFactory() { return { "image": null }; } 40 | entities.registerComponent("sprite", spriteFactory); 41 | entities.load(/* some JSON */); 42 | 43 | var lastTime = -1; 44 | var render = function(time) { 45 | if (this.lastTime === -1) { 46 | this.lastTime = time; 47 | } 48 | var elapsed = time - this.lastTime; 49 | this.lastTime = time; 50 | 51 | ecs.run(entities, elapsed); 52 | window.requestAnimationFrame(render); 53 | }; 54 | window.requestAnimationFrame(render); 55 | ``` 56 | 57 | # EntityComponentSystem 58 | 59 | An `EntityComponentSystem` holds the systems (code) and allows you to `run` 60 | them on the entities inside an `EntityPool`. 61 | 62 | ## add(system) 63 | 64 | Adds a "system" function to the ECS so it will be called once every time `run` 65 | is called. 66 | 67 | * `system` is a function that operates on all entities. `system` has the format: 68 | 69 | ```javascript 70 | function mySystem(entityPool, elapsedTime) { /* ... */ } 71 | ``` 72 | 73 | * `entityPool` is the `EntityPool` of entities to operate on. 74 | * `elapsedTime` is the elapsed time since the last call to `run`. 75 | 76 | 77 | ## addEach(system, search) 78 | 79 | Adds a "system" function to the ECS so it will be called once for each entity 80 | returned from `EntityPool.find(search)` in the `EntityPool` passed to `run`. 81 | 82 | * `system` is a function that operates on a single entity matching `search`. 83 | `system` has the format: 84 | 85 | ```javascript 86 | function mySystem(entityId, elapsedTime) { /* ... */ } 87 | ``` 88 | 89 | * `entityId` is the id of an entity to operate on. 90 | * `elapsedTime` is the elapsed time since the last call to `run`. 91 | 92 | * `search` is the name of a search that was previously registered with `registerSearch`. `system` is invoked once for every entity in the results of `search`. 93 | 94 | ## run(entityPool, elapsedTime) 95 | 96 | Invokes all systems in the order they were added to the `EntityComponentSystem`. 97 | 98 | * `entityPool` is the collection of entities to operate on. 99 | * `elapsedTime` is the time passed since you last called `run`. 100 | 101 | ## runs() 102 | 103 | Returns the number of times `run` was called. 104 | 105 | ## timings() 106 | 107 | Returns an array of each system's name and time it ran in milliseconds. The 108 | system names are gathered from the names of functions passed to `add` and 109 | `addEach`. An example return value: 110 | 111 | ```json 112 | { 113 | "drawBackground": 0.02, 114 | "drawEntity": 5.00 115 | } 116 | ``` 117 | 118 | ## resetTimings() 119 | 120 | Resets the timing information and number of runs back to zero. 121 | 122 | # EntityPool 123 | 124 | An `EntityPool` holds the entities and components for an 125 | `EntityComponentSystem`. `EntityPool` provides ways to add, remove, modify, and 126 | search for entities. `EntityPool` also has hooks where you can provide callbacks 127 | to be notified of changes. 128 | 129 | `EntityPool` also implements the [Object Pool 130 | pattern](http://gameprogrammingpatterns.com/object-pool.html) to reduce 131 | stuttering caused by garbage collection. 132 | 133 | ## create() 134 | 135 | Creates a new entity, and returns the entity's id. 136 | 137 | ```javascript 138 | var player = entities.create(); // => 1 139 | ``` 140 | 141 | ## destroy(id) 142 | 143 | Removes all the components for an entity, and deletes the entity. The 144 | `onRemoveComponent` callbacks are fired for each component that is removed. 145 | 146 | * `id` is the id of the entity to destroy. 147 | 148 | ```javascript 149 | entities.destroy(player); 150 | ``` 151 | 152 | ## registerComponent(component, factory, reset, size) 153 | 154 | Registers a component type. 155 | 156 | * `component` is the name of the component to register. 157 | * `factory` is a factory function which returns a newly allocated instance of 158 | the component. For example: 159 | 160 | ```javascript 161 | function createPosition() { 162 | return { 163 | x: 0, 164 | y: 0 165 | }; 166 | } 167 | ``` 168 | 169 | * `reset` is an optional function which alters a previously used component 170 | instance to a clean state so it can be reused on a new entity. For example: 171 | 172 | ```javascript 173 | function resetPosition(position) { 174 | position.x = 0; 175 | position.y = 0; 176 | } 177 | ``` 178 | 179 | * `size` is an optional number of instances to allocate initially. 180 | 181 | ## addComponent(id, component) 182 | 183 | Adds a new component to an entity, and returns it. If the component is newly added, 184 | the `onAddComponent` callbacks are fired. If the component already existed, it is reset. 185 | 186 | * `id` is the id of the entity to add the component to. 187 | * `component` is the name of the component to add. 188 | 189 | ```javascript 190 | var sprite = entities.addComponent(player, "sprite"); 191 | sprite.image = "something.png"; 192 | ``` 193 | 194 | ## getComponent(id, component) 195 | 196 | Returns the component value for an entity. 197 | 198 | * `id` is the id of the entity to get the component from. 199 | * `component` is the name of the component to get. 200 | 201 | ```javascript 202 | var sprite = entities.getComponent(player, "sprite"); 203 | sprite.image = "something.png"; 204 | ``` 205 | 206 | ## setComponent(id, component, value) 207 | 208 | Sets a primitive value for a component. To change a component that holds an 209 | object, use `getComponent` instead. 210 | 211 | * `id` is the id of the entity to set the component on. 212 | * `component` is the name of the component to set. 213 | * `value` is the primitive value to set. 214 | 215 | ```javascript 216 | entities.setComponent(player, "health", 100); 217 | ``` 218 | 219 | ## removeComponent(id, component) 220 | 221 | Removes a component from an entity. The `onRemoveComponent` callbacks are fired 222 | for the removed component. 223 | 224 | * `id` is the id of the entity to remove the component from. 225 | * `component` is the name of the component to remove. 226 | 227 | ```javascript 228 | entities.removeComponent(player, "health"); 229 | ``` 230 | 231 | ## onAddComponent(component, callback) 232 | 233 | Registers a callback to be called when `component` is added to any entity. 234 | `callback` looks like: 235 | 236 | ```javascript 237 | function myAddCallback(id, component, value) { /* ... */ } 238 | ``` 239 | 240 | ## onRemoveComponent(component, callback) 241 | 242 | Registers a callback to be called when `component` is removed from any entity. 243 | `callback` looks like: 244 | 245 | ```javascript 246 | function myRemoveCallback(id, component, removedValue) { /* ... */ } 247 | ``` 248 | 249 | ## registerSearch(search, components) 250 | 251 | Registers a named search for entities that have all components listed in the 252 | `components` array. 253 | 254 | * `search` is the name of the search to register. 255 | * `components` is an array of component names that an entity must possess to be 256 | included in the results. 257 | 258 | ```javascript 259 | entities.registerSearch("collectables", ["size", "collisions"]); 260 | ``` 261 | 262 | ## find(search) 263 | 264 | Returns a list of entity ids for all entities that match the search. See 265 | `registerSearch`. 266 | 267 | * `search` is the name of the search previously registered with 268 | `registerSearch`. 269 | 270 | ```javascript 271 | var collectables = entities.find("collectables"); // => [1, 2, 3, ...] 272 | ``` 273 | 274 | ## load(entities) 275 | 276 | Load entities into an entity pool from an array of objects. `load` should only 277 | be used to fill an empty `EntityPool`. 278 | 279 | * `entities` is some JSON-compatible object returned by `save`. The format looks 280 | like: 281 | 282 | ```json 283 | [ 284 | { 285 | "id": 1, 286 | "componentName": "componentValue" 287 | }, 288 | { 289 | "id": 2, 290 | "componentName": "componentValue" 291 | } 292 | ] 293 | ``` 294 | 295 | ## save() 296 | 297 | Returns an object suitable for saving all entities in the `EntityPool` to a JSON 298 | file. See `load()`. 299 | 300 | # Install 301 | 302 | With [npm](https://www.npmjs.com/) do: 303 | 304 | ``` 305 | npm install --save entity-component-system 306 | ``` 307 | 308 | # License 309 | 310 | MIT 311 | -------------------------------------------------------------------------------- /lib/entity-pool-test.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | 3 | var EntityPool = require("./entity-pool"); 4 | 5 | test("create returns an entity id", function(t) { 6 | t.plan(1); 7 | 8 | var pool = new EntityPool(); 9 | var id = pool.create(); 10 | t.equal(typeof id, "number"); 11 | }); 12 | 13 | test("create returns different ids each time", function(t) { 14 | t.plan(1); 15 | 16 | var pool = new EntityPool(); 17 | var id1 = pool.create(); 18 | var id2 = pool.create(); 19 | t.notEqual(id1, id2); 20 | }); 21 | 22 | test("destroy deletes a whole entity", function(t) { 23 | t.plan(1); 24 | 25 | t.throws(function() { 26 | var pool = new EntityPool(); 27 | var id = pool.create(); 28 | pool.destroy(id); 29 | pool.getComponent(id, "id"); 30 | }); 31 | }); 32 | 33 | test("getComponent with id returns the id", function(t) { 34 | t.plan(1); 35 | 36 | var pool = new EntityPool(); 37 | var id = pool.create(); 38 | var result = pool.getComponent(id, "id"); 39 | t.equal(result, id); 40 | }); 41 | 42 | test("entity ids get reused", function(t) { 43 | t.plan(1); 44 | 45 | var pool = new EntityPool(); 46 | var a = pool.create(); 47 | pool.create(); 48 | pool.destroy(a); 49 | var b = pool.create(); 50 | 51 | t.equal(a, b); 52 | }); 53 | 54 | test("addComponent can get fetched with getComponent", function(t) { 55 | t.plan(1); 56 | 57 | var pool = new EntityPool(); 58 | var id = pool.create(); 59 | pool.registerComponent("prop", function () { 60 | return {}; 61 | }); 62 | 63 | var a = pool.addComponent(id, "prop"); 64 | var b = pool.getComponent(id, "prop"); 65 | 66 | t.equal(a, b); 67 | }); 68 | test("if a component already exists on an entity, addComponent returns it UNLESS it's primitive", function(t) { 69 | t.plan(2); 70 | 71 | var pool = new EntityPool(); 72 | var id = pool.create(); 73 | pool.setComponent(id, "prop", "hello world"); 74 | pool.registerComponent("prop", function () { 75 | return {}; 76 | }); 77 | 78 | var a = pool.getComponent(id, "prop"); 79 | var b = pool.addComponent(id, "prop"); 80 | var c = pool.addComponent(id, "prop"); 81 | 82 | t.equal(b, c, "returns old component if old component is object"); 83 | t.notEqual(a, b, "returns new component if old component is primitive"); 84 | }); 85 | test("addComponent resets the existing component", function(t) { 86 | t.plan(2); 87 | 88 | var pool = new EntityPool(); 89 | var id = pool.create(); 90 | 91 | pool.registerComponent("info", function() { 92 | return { location: "Louisville" }; 93 | }, function(c) { 94 | c.location = "Louisville"; 95 | }); 96 | 97 | pool.addComponent(id, "info").location = "Pittsburgh"; 98 | var a = pool.getComponent(id, "info").location; 99 | var b = pool.addComponent(id, "info").location; 100 | 101 | t.notEqual(a, b); 102 | t.equal(b, "Louisville", "factory reset is run on addComponent if component exists"); 103 | }); 104 | test("addComponent returns a component matching the provided factory function", function(t) { 105 | t.plan(1); 106 | 107 | var pool = new EntityPool(); 108 | var id = pool.create(); 109 | pool.registerComponent("info", function () { 110 | return { location: "Louisville" }; 111 | }); 112 | 113 | var value = pool.addComponent(id, "info"); 114 | 115 | t.equal(value.location, "Louisville"); 116 | }); 117 | test("updating the value returned by addComponent persists in EntityPool", function(t) { 118 | t.plan(1); 119 | 120 | var pool = new EntityPool(); 121 | var id = pool.create(); 122 | pool.registerComponent("info", function () { 123 | return { location: "Louisville" }; 124 | }); 125 | 126 | var a = pool.addComponent(id, "info"); 127 | var locationA = a.location = "Pittsburgh"; 128 | var b = pool.getComponent(id, "info"); 129 | var locationB = b.location; 130 | 131 | t.equal(locationA, locationB); 132 | }); 133 | 134 | test("setComponent with component can be fetched with getComponent", function(t) { 135 | t.plan(1); 136 | 137 | var pool = new EntityPool(); 138 | var id = pool.create(); 139 | pool.setComponent(id, "name", "jimmy"); 140 | var name = pool.getComponent(id, "name"); 141 | 142 | t.equal(name, "jimmy"); 143 | }); 144 | test("setComponent with same component twice isn't in search twice", function(t) { 145 | t.plan(1); 146 | 147 | var pool = new EntityPool(); 148 | var id = pool.create(); 149 | pool.setComponent(id, "name", "jimmy"); 150 | pool.setComponent(id, "name", "bobby"); 151 | var results = pool.find("name"); 152 | 153 | t.equal(results.length, 1); 154 | }); 155 | test("setComponent with existing component to undefined removes from search", function(t) { 156 | t.plan(1); 157 | 158 | var pool = new EntityPool(); 159 | var id = pool.create(); 160 | pool.setComponent(id, "name", "jimmy"); 161 | pool.setComponent(id, "name", undefined); 162 | var results = pool.find("name"); 163 | 164 | t.equal(results.length, 0); 165 | }); 166 | test("setComponent fails if passed value is not primitive", function(t) { 167 | t.plan(1); 168 | 169 | var pool = new EntityPool(); 170 | var id = pool.create(); 171 | 172 | t.throws(function() { 173 | pool.setComponent(id, "info", { 174 | location: "Louisville" 175 | }); 176 | }, TypeError, "should throw TypeError"); 177 | }); 178 | test("setComponent fails if already existing value is not primitive", function(t) { 179 | t.plan(1); 180 | 181 | var pool = new EntityPool(); 182 | var id = pool.create(); 183 | pool.registerComponent("prop", function () { 184 | return {}; 185 | }); 186 | pool.addComponent(id, "prop"); 187 | 188 | t.throws(function() { 189 | pool.setComponent(id, "prop", "hello world"); 190 | }); 191 | }); 192 | 193 | 194 | test("removeComponent with component makes it unable to be fetched", function(t) { 195 | t.plan(1); 196 | 197 | var pool = new EntityPool(); 198 | var id = pool.create(); 199 | pool.setComponent(id, "name", "jimmy"); 200 | pool.removeComponent(id, "name"); 201 | var name = pool.getComponent(id, "name"); 202 | 203 | t.equal(name, undefined); 204 | }); 205 | test("removeComponent resets the component", function(t) { 206 | t.plan(2); 207 | 208 | var pool = new EntityPool(); 209 | var id = pool.create(); 210 | 211 | pool.registerComponent("info", function () { 212 | return { location: "Louisville" }; 213 | }, function(c) { 214 | c.location = "Louisville"; 215 | }); 216 | 217 | var value = pool.addComponent(id, "info"); 218 | var a = value.location = "Pittsburgh"; 219 | pool.removeComponent(id, "info"); 220 | var b = value.location; 221 | 222 | t.notEqual(a, b); 223 | t.equal(b, "Louisville", "factory reset is run on removeComponent"); 224 | }); 225 | 226 | test("find with no matching components returns empty list", function(t) { 227 | t.plan(1); 228 | 229 | var pool = new EntityPool(); 230 | var id = pool.create(); 231 | pool.setComponent(id, "name", "jimmy"); 232 | var results = pool.find("does-not-exist"); 233 | 234 | t.deepEqual(results, []); 235 | }); 236 | 237 | test("find with matching component returns list with entities", function(t) { 238 | t.plan(1); 239 | 240 | var pool = new EntityPool(); 241 | var id = pool.create(); 242 | pool.setComponent(id, "name", "jimmy"); 243 | var id2 = pool.create(); 244 | pool.setComponent(id2, "name", "amy"); 245 | var results = pool.find("name"); 246 | 247 | t.deepEqual(results, [id, id2]); 248 | }); 249 | 250 | test("find with deleted component returns empty list", function(t) { 251 | t.plan(1); 252 | 253 | var pool = new EntityPool(); 254 | var id = pool.create(); 255 | pool.setComponent(id, "name", "jimmy"); 256 | pool.removeComponent(id, "name"); 257 | var results = pool.find("name"); 258 | 259 | t.deepEqual(results, []); 260 | }); 261 | 262 | test("registerSearch with two components and entities already added returns entities", function(t) { 263 | t.plan(1); 264 | 265 | var pool = new EntityPool(); 266 | var id = pool.create(); 267 | pool.setComponent(id, "name", "jimmy"); 268 | pool.setComponent(id, "age", 8); 269 | pool.registerSearch("peopleWithAge", ["name", "age"]); 270 | var results = pool.find("peopleWithAge"); 271 | 272 | t.deepEqual(results, [id]); 273 | }); 274 | 275 | test("registerSearch with two components and entities added after returns entities", function(t) { 276 | t.plan(1); 277 | 278 | var pool = new EntityPool(); 279 | var id = pool.create(); 280 | pool.registerSearch("peopleWithAge", ["name", "age"]); 281 | pool.setComponent(id, "name", "jimmy"); 282 | pool.setComponent(id, "age", 8); 283 | var results = pool.find("peopleWithAge"); 284 | 285 | t.deepEqual(results, [id]); 286 | }); 287 | 288 | test("registerSearch twice with same name throws", function(t) { 289 | t.plan(1); 290 | 291 | t.throws(function() { 292 | var pool = new EntityPool(); 293 | pool.registerSearch("search", ["search"]); 294 | pool.registerSearch("search", ["search"]); 295 | }); 296 | }); 297 | 298 | test("removeComponent removes an entity from a complex search", function(t) { 299 | t.plan(1); 300 | 301 | var pool = new EntityPool(); 302 | var id = pool.create(); 303 | pool.registerSearch("peopleWithAge", ["name", "age"]); 304 | pool.setComponent(id, "name", "jimmy"); 305 | pool.setComponent(id, "age", 8); 306 | pool.removeComponent(id, "age"); 307 | var results = pool.find("peopleWithAge"); 308 | 309 | t.deepEqual(results, []); 310 | }); 311 | 312 | test("load creates an entity", function(t) { 313 | t.plan(1); 314 | 315 | var pool = new EntityPool(); 316 | pool.load([{ id: 1, name: "jimmy" }]); 317 | var name = pool.getComponent(1, "name"); 318 | 319 | t.deepEqual(name, "jimmy"); 320 | }); 321 | 322 | test("load increments next id", function(t) { 323 | t.plan(1); 324 | 325 | var pool = new EntityPool(); 326 | pool.load([{ id: 1, name: "jimmy" }]); 327 | var id = pool.create(); 328 | 329 | t.deepEqual(id, 2); 330 | }); 331 | 332 | test("save returns entities", function(t) { 333 | t.plan(1); 334 | 335 | var pool = new EntityPool(); 336 | var id = pool.create(); 337 | pool.setComponent(id, "name", "jimmy"); 338 | var output = pool.save(); 339 | 340 | t.deepEqual(output, [{ id: id, name: "jimmy"}]); 341 | }); 342 | 343 | test("onAddComponent with callback is called after set", function(t) { 344 | t.plan(6); 345 | 346 | var pool = new EntityPool(); 347 | var id = pool.create(); 348 | function callback(entity, component, value) { 349 | t.equal(entity, id); 350 | t.equal(component, "name"); 351 | t.equal(value, "jimmy"); 352 | } 353 | pool.onAddComponent("name", callback); 354 | pool.onAddComponent("name", callback); 355 | 356 | pool.setComponent(id, "name", "jimmy"); 357 | }); 358 | 359 | test("onAddComponent with callback is not called on modifying existing component", function(t) { 360 | t.plan(3); 361 | 362 | var pool = new EntityPool(); 363 | var id = pool.create(); 364 | function callback(entity, component, value) { 365 | t.equal(entity, id); 366 | t.equal(component, "name"); 367 | t.equal(value, "jimmy"); 368 | } 369 | pool.onAddComponent("name", callback); 370 | 371 | pool.setComponent(id, "name", "jimmy"); 372 | pool.setComponent(id, "name", "salazar"); 373 | }); 374 | 375 | test("onAddComponent with callback is not called until all data is loaded", function(t) { 376 | t.plan(1); 377 | 378 | var pool = new EntityPool(); 379 | function callback(entity) { 380 | t.equal(pool.getComponent(entity, "age"), 28); 381 | } 382 | pool.onAddComponent("name", callback); 383 | pool.load([{ id: 1, name: "jimmy", age: 28 }]); 384 | }); 385 | test("onRemoveComponent with callback is called on removing component", function(t) { 386 | t.plan(3); 387 | 388 | var pool = new EntityPool(); 389 | pool.registerComponent("info", function () { 390 | return { location: "Louisville" }; 391 | }, function(c) { 392 | c.location = "Louisville"; 393 | }); 394 | 395 | var id = pool.create(); 396 | var info = pool.addComponent(id, "info"); 397 | info.location = "Detroit"; 398 | function callback(entity, component, value) { 399 | t.equal(entity, id); 400 | t.equal(component, "info"); 401 | t.equal(value.location, "Detroit"); 402 | } 403 | pool.onRemoveComponent("info", callback); 404 | 405 | pool.removeComponent(id, "info"); 406 | }); 407 | 408 | test("onRemoveComponent with callback isn't called on removing nonexistant component", function(t) { 409 | t.plan(3); 410 | 411 | var pool = new EntityPool(); 412 | var id = pool.create(); 413 | pool.setComponent(id, "name", "jimmy"); 414 | function callback(entity, component, value) { 415 | t.equal(entity, id); 416 | t.equal(component, "name"); 417 | t.equal(value, "jimmy"); 418 | } 419 | pool.onRemoveComponent("name", callback); 420 | 421 | pool.removeComponent(id, "name"); 422 | pool.removeComponent(id, "name"); 423 | }); 424 | --------------------------------------------------------------------------------