├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── README.md ├── bower.json ├── dist ├── angular-ecs.js ├── angular-ecs.js.map └── angular-ecs.min.js ├── docs-content └── index.ngdoc ├── gulpfile.js ├── karma.conf.js ├── package.json ├── src ├── angular-ecs-Entity.js ├── angular-ecs-Family.js ├── angular-ecs-engine.js ├── angular-ecs.js └── shims.js ├── test ├── .jshintrc └── spec │ ├── angular-ecs-components.js │ ├── angular-ecs-engine.js │ ├── angular-ecs-entities.js │ ├── angular-ecs-families.js │ └── angular-ecs-systems.js └── todo.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | docs 4 | .grunt 5 | .publish 6 | .tmp 7 | .sass-cache 8 | *.swp 9 | *.swo 10 | 11 | .DS_Store 12 | .settings 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "predef": [ 23 | "angular", 24 | "signals" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.8' 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2015-02-25 using generator-angular-component 0.2.3 2 | 'use strict'; 3 | 4 | module.exports = function(grunt) { 5 | 6 | // Configurable paths 7 | var yoConfig = { 8 | livereload: 35729, 9 | src: 'src', 10 | dist: 'dist' 11 | }; 12 | 13 | // Livereload setup 14 | var lrSnippet = require('connect-livereload')({port: yoConfig.livereload}); 15 | var mountFolder = function (connect, dir) { 16 | return connect.static(require('path').resolve(dir)); 17 | }; 18 | 19 | // Load all grunt tasks 20 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 21 | 22 | // Project configuration 23 | grunt.initConfig({ 24 | pkg: grunt.file.readJSON('package.json'), 25 | yo: yoConfig, 26 | meta: { 27 | banner: '/**\n' + 28 | ' * <%= pkg.name %>\n' + 29 | ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 30 | ' * @link <%= pkg.homepage %>\n' + 31 | ' * @author <%= pkg.author.name %> <<%= pkg.author.email %>>\n' + 32 | ' * @license MIT License, http://www.opensource.org/licenses/MIT\n' + 33 | ' */\n' 34 | }, 35 | clean: { 36 | dist: { 37 | files: [{ 38 | dot: true, 39 | src: [ 40 | '.tmp', 41 | '<%= yo.dist %>/*', 42 | '!<%= yo.dist %>/.git*' 43 | ] 44 | }] 45 | }, 46 | server: '.tmp' 47 | }, 48 | watch: { 49 | gruntfile: { 50 | files: '<%= jshint.gruntfile.src %>', 51 | tasks: ['jshint:gruntfile'] 52 | }, 53 | app: { 54 | files: [ 55 | '{.tmp,<%= yo.src %>}/{,*/}*.js' 56 | ], 57 | tasks: ['build'] 58 | }, 59 | test: { 60 | files: '<%= jshint.test.src %>', 61 | tasks: ['jshint:test', 'qunit'] 62 | } 63 | }, 64 | jshint: { 65 | gruntfile: { 66 | options: { 67 | jshintrc: '.jshintrc' 68 | }, 69 | src: 'Gruntfile.js' 70 | }, 71 | src: { 72 | options: { 73 | jshintrc: '.jshintrc' 74 | }, 75 | src: ['<%= yo.src %>/{,*/}*.js'] 76 | }, 77 | test: { 78 | options: { 79 | jshintrc: 'test/.jshintrc' 80 | }, 81 | src: ['test/**/*.js'] 82 | } 83 | }, 84 | karma: { 85 | options: { 86 | configFile: 'karma.conf.js', 87 | browsers: ['PhantomJS'] 88 | }, 89 | unit: { 90 | singleRun: true 91 | }, 92 | server: { 93 | autoWatch: true 94 | } 95 | }, 96 | ngmin: { 97 | options: { 98 | banner: '<%= meta.banner %>' 99 | }, 100 | dist: { 101 | expand: true, 102 | cwd: '.tmp/concat/', 103 | src: '*.js', 104 | dest: '<%= yo.dist %>/' 105 | } 106 | }, 107 | concat: { 108 | options: { 109 | banner: '<%= meta.banner %>', 110 | stripBanners: false 111 | }, 112 | dist: { 113 | src: [ 114 | './<%= yo.src %>/shims.js', 115 | './<%= yo.src %>/<%= pkg.name %>.js', 116 | './<%= yo.src %>/*.js' 117 | ], 118 | dest: '.tmp/concat/<%= pkg.name %>.js' 119 | } 120 | }, 121 | uglify: { 122 | options: { 123 | banner: '<%= meta.banner %>' 124 | }, 125 | dist: { 126 | src: '<%= concat.dist.dest %>', 127 | dest: '<%= yo.dist %>/<%= pkg.name %>.min.js' 128 | } 129 | }, 130 | bump: { 131 | options: { 132 | files: ['package.json','bower.json'], 133 | commitFiles: ['package.json','bower.json','dist/*'], 134 | push: true, 135 | pushTo: 'origin' 136 | } 137 | }, 138 | ngdocs: { 139 | all: ['src/**/*.js'] 140 | }, 141 | connect: { 142 | server: { 143 | options: { 144 | port: 9001, 145 | base: 'docs', 146 | hostname: 'localhost', 147 | open: true 148 | } 149 | } 150 | }, 151 | 'gh-pages': { 152 | options: { 153 | base: 'docs' 154 | }, 155 | src: ['**'] 156 | }, 157 | }); 158 | 159 | grunt.registerTask('test', [ 160 | 'jshint', 161 | 'karma:unit' 162 | ]); 163 | 164 | grunt.registerTask('build', [ 165 | 'clean:dist', 166 | 'concat', 167 | 'ngmin:dist', 168 | 'uglify:dist', 169 | 'ngdocs' 170 | ]); 171 | 172 | grunt.registerTask('release', [ 173 | 'test', 174 | 'bump-only', 175 | 'build', 176 | 'bump-commit' 177 | ]); 178 | 179 | grunt.registerTask('publish', ['test','build','bump-only','uglify','bump-commit','gh-pages']); 180 | 181 | grunt.registerTask('serve', ['build','connect','watch']); 182 | 183 | grunt.registerTask('default', ['build']); 184 | 185 | }; 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-ecs 2 | 3 | [](https://gitter.im/Hypercubed/angular-ecs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | An entity-component-system game framework made specifically for AngularJS. 6 | 7 | > "Make Games, Not Engines" - Everyone 8 | 9 | > "But I..." - Me 10 | 11 | There are many great game engines available for JavaScript. Many include all the pieces needed to develop games in JavaScript; a canvas based rendering engine, optimized and specialized game loop, pixel asset management, dependency injection, and so on. However, when developing a web game using AngularJS you may want to use only some parts of the game engine and leave other parts to Angular. To do this it often means playing tricks on the game engine to cooperate with angularjs. Angular-ecs is a entity-component-system built for and with AngularJS. Angular-ecs was built to play nice with the angular architecture and to feel, as much as possible, like a native part of the angular framework. 12 | 13 | **Watch out, a work in progress** 14 | 15 | ## Getting Started 16 | 17 | Install using bower: 18 | 19 | ``` 20 | bower install --save Hypercubed/angular-ecs 21 | ``` 22 | 23 | ## Design goals 24 | - plays nice with AngularJS services and directives 25 | - uses angular for DI 26 | - take advantage of AngularJs tools 27 | - easy to serialize entities 28 | - feels like part of angular 29 | - don't fight angular or JavaScript 30 | - understand and take advantage of browser optimization 31 | 32 | ## Documentation **WIP** 33 | 34 | * [API](http://hypercubed.github.io/angular-ecs) 35 | * [Getting Started](https://github.com/Hypercubed/angular-ecs/wiki) 36 | 37 | ## Examples 38 | 39 | * [Hypercubed/Epsilon-Prime](https://github.com/Hypercubed/Epsilon-Prime) 40 | * [Hypercubed/angular-ecs-flap](https://github.com/Hypercubed/angular-ecs-flap) 41 | * [Hypercubed/angular-ecs-pong](https://github.com/Hypercubed/angular-ecs-pong) 42 | 43 | ## Acknowledgements 44 | Inspired by [darlingjs/darlingjs](https://github.com/darlingjs/darlingjs) and [brejep/ash-js](https://github.com/brejep/ash-js) and many other great ECS implementations. 45 | 46 | ## License 47 | 48 | [MIT License](http://en.wikipedia.org/wiki/MIT_License) 49 | 50 | Copyright (c) Jayson Harshbarger 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 53 | 54 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 57 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ecs", 3 | "version": "0.0.21", 4 | "dependencies": { 5 | "js-signals": "~1.0.0" 6 | }, 7 | "devDependencies": { 8 | "angular": "latest", 9 | "angular-mocks": "latest", 10 | "jquery": "latest" 11 | }, 12 | "main": [ 13 | "dist/angular-ecs.js" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /dist/angular-ecs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-ecs - An ECS framework built for AngularJS 3 | * @version v0.0.20 4 | * @link https://github.com/Hypercubed/angular-ecs 5 | * @author Jayson Harshbarger <> 6 | * @license 7 | */ 8 | /* global angular:true */ 9 | 10 | // main 11 | 'use strict'; 12 | 13 | (function () { 14 | 15 | 'use strict'; 16 | 17 | /** 18 | * ngdoc overview 19 | * name index 20 | * 21 | * description 22 | * # An entity-component-system game framework made specifically for AngularJS. 23 | * 24 | * ## Why? 25 | * 26 | * There are many great game engines available for JavaScript. Many include all the pieces needed to develop games in JavaScript; a canvas based rendering engine, optimized and specialized game loop, pixel asset management, dependency injection, and so on. However, when developing a web game using AngularJS you may want to use only some parts of the game engine and leave other parts to Angular. To do this it often means playing tricks on the game engine to cooperate with angularjs. Angular-ecs is a entity-component-system built for and with AngularJS. Angular-ecs was built to play nice with the angular architecture and to feel, as much as possible, like a native part of the angular framework. 27 | * 28 | * 29 | */ 30 | 31 | function MapProvider() { 32 | 33 | var map = {}; 34 | 35 | this.register = function (name, constructor) { 36 | if (angular.isObject(name)) { 37 | angular.extend(map, name); 38 | } else { 39 | map[name] = constructor; 40 | } 41 | return this; 42 | }; 43 | 44 | this.$get = ['$injector', function ($injector) { 45 | angular.forEach(map, function (value, key) { 46 | if (angular.isFunction(value)) { 47 | map[key] = $injector.invoke(value, null, null, key); 48 | } 49 | }); 50 | return map; 51 | }]; 52 | } 53 | 54 | angular.module('hc.ngEcs', []) 55 | 56 | /** 57 | * @ngdoc service 58 | * @name hc.ngEcs.$entities 59 | * @description 60 | * Index of {@link hc.ngEcs.Entity:entity entities}. 61 | **/ 62 | .provider('$entities', MapProvider) 63 | 64 | /** 65 | * @ngdoc service 66 | * @name hc.ngEcs.$componentsProvider 67 | * @description 68 | * This provider allows component registration via the register method. 69 | * 70 | **/ 71 | 72 | /** 73 | * @ngdoc 74 | * @name hc.ngEcs.$componentsProvider#$register 75 | * @methodOf hc.ngEcs.$componentsProvider 76 | * 77 | * @description 78 | * Registers a componnet during configuration phase 79 | * 80 | * @param {string|object} name Component name, or an object map of components where the keys are the names and the values are the constructors. 81 | * @param {function()|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation). 82 | */ 83 | 84 | /** 85 | * @ngdoc service 86 | * @name hc.ngEcs.$components 87 | * @description 88 | * Index of components, components are object constructors 89 | * */ 90 | .provider('$components', MapProvider) 91 | 92 | /** 93 | * @ngdoc service 94 | * @name hc.ngEcs.$systemsProvider 95 | * @description 96 | * This provider allows component registration via the register method. 97 | * 98 | **/ 99 | 100 | /** 101 | * @ngdoc 102 | * @name hc.ngEcs.$systemsProvider#$register 103 | * @methodOf hc.ngEcs.$systemsProvider 104 | * 105 | * @description 106 | * Registers a componnet during configuration phase 107 | * 108 | * @param {string|object} name System name, or an object map of systems where the keys are the names and the values are the constructors. 109 | * @param {function()|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation). 110 | */ 111 | 112 | /** 113 | * @ngdoc service 114 | * @name hc.ngEcs.$systems 115 | * @description 116 | * Index of systems, systems are generic objects 117 | * */ 118 | .provider('$systems', MapProvider) 119 | 120 | /** 121 | * @ngdoc service 122 | * @name hc.ngEcs.$families 123 | * @description 124 | * Index of {@link hc.ngEcs.Family:family families}, a family is an array of game entities matching a list of required components. 125 | * */ 126 | .provider('$families', MapProvider); 127 | })(); 128 | 129 | // Entity 130 | (function () { 131 | 132 | 'use strict'; 133 | 134 | angular.module('hc.ngEcs') 135 | 136 | /** 137 | * @ngdoc service 138 | * @name hc.ngEcs.Entity 139 | * @requires hc.ngEcs.$components 140 | * @description 141 | * {@link hc.ngEcs.Entity:entity Entity} factory.. 142 | * 143 | * */ 144 | 145 | .factory('Entity', ['$components', function ($components) { 146 | var _uuid = 0; 147 | function uuid() { 148 | var timestamp = new Date().getUTCMilliseconds(); 149 | return '' + _uuid++ + '_' + timestamp; 150 | } 151 | 152 | /** 153 | * @ngdoc object 154 | * @name hc.ngEcs.Entity:entity 155 | * @description 156 | * An Entity is bag of game properties (components). By convention properties that do not start with a $ or _ are considered compoenets. 157 | * */ 158 | function Entity(id) { 159 | if (false === this instanceof Entity) { 160 | return new Entity(id); 161 | } 162 | this._id = id || uuid(); 163 | 164 | this.$componentAdded = new signals.Signal(); 165 | this.$componentRemoved = new signals.Signal(); 166 | 167 | this.$$signals = {}; 168 | } 169 | 170 | /** 171 | * @ngdoc 172 | * @name hc.ngEcs.Entity:entity#$on 173 | * @methodOf hc.ngEcs.Entity:entity 174 | * 175 | * @description 176 | * Adds an event listener to the entity 177 | * 178 | * @example 179 | *
180 | entity.$on('upgrade', function() { }); 181 | *182 | * @param {string} name Event name to listen on. 183 | * @param {function(event, ...args)} listener Function to call when the event is emitted. 184 | * @returns {function()} Returns a deregistration function for this listener. 185 | */ 186 | Entity.prototype.$on = function (name, listener) { 187 | var sig = this.$$signals[name]; 188 | if (!sig) { 189 | this.$$signals[name] = sig = new signals.Signal(); 190 | } 191 | return sig.add(listener, this); 192 | }; 193 | 194 | /** 195 | * @ngdoc 196 | * @name hc.ngEcs.Entity:entity#$emit 197 | * @methodOf hc.ngEcs.Entity:entity 198 | * 199 | * @description 200 | * Dispatches an event `name` calling notifying 201 | * registered {@link hc.ngEcs.Entity#$on} listeners 202 | * 203 | * @example 204 | *
205 | entity.$emit('upgrade'); 206 | *207 | * @param {string} name Event name to emit. 208 | * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. 209 | * @returns {Entity} The entity 210 | */ 211 | Entity.prototype.$emit = function (name) { 212 | var sig = this.$$signals[name]; 213 | if (!sig) { 214 | return; 215 | } // throw error? 216 | 217 | if (arguments.length > 1) { 218 | var args = Array.prototype.slice.call(arguments, 1); 219 | sig.dispatch.apply(sig, args); 220 | } else { 221 | sig.dispatch(); 222 | } 223 | 224 | return this; 225 | }; 226 | 227 | /** 228 | * @ngdoc 229 | * @name hc.ngEcs.Entity:entity#$add 230 | * @methodOf hc.ngEcs.Entity:entity 231 | * 232 | * @description 233 | * Adds a Component to the entity 234 | * 235 | * @example 236 | *
237 | entity.$add('position', { 238 | x: 1.0, 239 | y: 3.0 240 | }); 241 | *242 | * @param {string} key The name of the Component 243 | * @param {object} [instance] A component instance or a compoent configuration 244 | * @returns {Entity} The entity 245 | */ 246 | Entity.prototype.$add = function (key, instance) { 247 | 248 | if (!key) { 249 | throw new Error('Can\'t add component with undefined key.'); 250 | } 251 | 252 | // remove if exists 253 | if (this[key]) { 254 | this.$remove(key); 255 | } 256 | 257 | instance = angular.isDefined(instance) ? instance : {}; 258 | 259 | // not a component by convention 260 | if (key.charAt(0) === '$' || key.charAt(0) === '_') { 261 | this[key] = instance; 262 | return; // no emit 263 | } 264 | 265 | this[key] = createComponent(this, key, instance); 266 | 267 | this.$componentAdded.dispatch(this, key); 268 | return this; 269 | }; 270 | 271 | function createComponent(e, name, state) { 272 | 273 | // not a registered component 274 | if (!$components.hasOwnProperty(name)) { 275 | return state; 276 | } 277 | 278 | var Type = $components[name]; 279 | 280 | // not valid constructor 281 | if (!angular.isFunction(Type)) { 282 | throw new TypeError('Component constructor may only be an Object or function'); 283 | return; 284 | } 285 | 286 | // already an instance 287 | if (state instanceof Type) { 288 | return state; 289 | } 290 | 291 | // inject 292 | if (Type.$inject) { 293 | return instantiate(Type, e, state); 294 | } 295 | 296 | return angular.extend(new Type(e), state); 297 | } 298 | 299 | function instantiate(Type, e, state) { 300 | var $inject = Type.$inject; 301 | 302 | var length = $inject.length, 303 | args = new Array(length), 304 | i; 305 | 306 | for (i = 0; i < length; ++i) { 307 | args[i] = getValue(e, $inject[i], state); 308 | } 309 | 310 | var instance = Object.create(Type.prototype || null); 311 | Type.apply(instance, args); 312 | return instance; 313 | } 314 | 315 | function getValue(e, key, state) { 316 | if (key === '$parent') { 317 | return e; 318 | } 319 | if (key === '$state') { 320 | return state; 321 | } 322 | //if (key === '$world') { return ngEcs; } // todo 323 | return state[key]; 324 | } 325 | 326 | function isComponent(name) { 327 | return name.charAt(0) !== '$' && name.charAt(0) !== '_'; 328 | } 329 | 330 | /** 331 | * @ngdoc 332 | * @name hc.ngEcs.Entity:entity#$remove 333 | * @methodOf hc.ngEcs.Entity:entity 334 | * 335 | * @description 336 | * Removes a component from the entity 337 | * 338 | * @example 339 | *
340 | entity.$remove('position'); 341 | *342 | * @param {string} key The name of the Component 343 | * @returns {Entity} The entity 344 | */ 345 | Entity.prototype.$remove = function (key) { 346 | // not a component by convention 347 | if (isComponent(key)) { 348 | this.$componentRemoved.dispatch(this, key); 349 | } 350 | delete this[key]; 351 | return this; 352 | }; 353 | 354 | return Entity; 355 | }]); 356 | })(); 357 | 358 | // Entity 359 | (function () { 360 | 361 | 'use strict'; 362 | 363 | /** 364 | * @ngdoc object 365 | * @name hc.ngEcs.Family:family 366 | * @description 367 | * A Family is array of game entities matching a list of required components. 368 | * 369 | **/ 370 | 371 | function Family(require) { 372 | var _this = []; 373 | 374 | /** 375 | * @ngdoc 376 | * @name hc.ngEcs.Family:family#require 377 | * @propertyOf hc.ngEcs.Family:family 378 | * 379 | * @description 380 | * An array of component requirements of this family 381 | */ 382 | Object.defineProperty(_this, 'require', { 383 | enumerable: false, 384 | writable: false, 385 | value: require 386 | }); 387 | 388 | /** 389 | * @ngdoc 390 | * @name hc.ngEcs.Family#entityAdded 391 | * @propertyOf hc.ngEcs.Family:family 392 | * 393 | * @description 394 | * A signal dispatched when an entity is added 395 | */ 396 | Object.defineProperty(_this, 'entityAdded', { 397 | enumerable: false, 398 | value: new signals.Signal() 399 | }); 400 | 401 | /** 402 | * @ngdoc 403 | * @name hc.ngEcs.Family#entityRemoved 404 | * @propertyOf hc.ngEcs.Family:family 405 | * 406 | * @description 407 | * A signal dispatched when an entity is removed 408 | */ 409 | Object.defineProperty(_this, 'entityRemoved', { 410 | enumerable: false, 411 | value: new signals.Signal() 412 | }); 413 | 414 | for (var method in Family.prototype) { 415 | if (Family.prototype.hasOwnProperty(method)) { 416 | Object.defineProperty(_this, method, { 417 | enumerable: false, 418 | value: Family.prototype[method] 419 | }); 420 | } 421 | } 422 | 423 | return _this; 424 | } 425 | 426 | /** 427 | * @ngdoc 428 | * @name hc.ngEcs.Family#isMatch 429 | * @methodOf hc.ngEcs.Family:family 430 | * @param {object} entity the entity to test match. 431 | * @returns {boolean} True if the entity matches this family 432 | * 433 | * @description 434 | * Tests if the entity matches the family requirements 435 | */ 436 | Family.prototype.isMatch = function (entity) { 437 | if (!this.require) { 438 | return true; 439 | } 440 | 441 | return this.require.every(function (d) { 442 | return entity.hasOwnProperty(d); 443 | }); 444 | }; 445 | 446 | /** 447 | * @ngdoc 448 | * @name hc.ngEcs.Family#add 449 | * @methodOf hc.ngEcs.Family:family 450 | * @param {object} entity the entity to add. 451 | * 452 | * @description 453 | * Adds an entity to this family 454 | */ 455 | Family.prototype.add = function (e) { 456 | // check if match? 457 | var index = this.indexOf(e); 458 | if (index < 0) { 459 | this.push(e); 460 | this.entityAdded.dispatch(e); 461 | } 462 | }; 463 | 464 | /** 465 | * @ngdoc 466 | * @name hc.ngEcs.Family#addIfMatch 467 | * @methodOf hc.ngEcs.Family:family 468 | * @param {object} entity the entity to add if it matches the family requirements 469 | * 470 | * @description 471 | * Adds an entity to this family if entity matches requirements 472 | */ 473 | Family.prototype.addIfMatch = function (e) { 474 | if (this.isMatch(e)) { 475 | this.add(e); 476 | } 477 | }; 478 | 479 | /** 480 | * @ngdoc 481 | * @name hc.ngEcs.Family#remove 482 | * @methodOf hc.ngEcs.Family:family 483 | * @param {object} entity the entity to remove 484 | * 485 | * @description 486 | * Removes an entity from this family 487 | */ 488 | Family.prototype.remove = function (e) { 489 | var index = this.indexOf(e); 490 | if (index > -1) { 491 | this.splice(index, 1); 492 | this.entityRemoved.dispatch(e); 493 | } 494 | }; 495 | 496 | /** 497 | * @ngdoc 498 | * @name hc.ngEcs.Family#removeIfMatch 499 | * @methodOf hc.ngEcs.Family:family 500 | * @param {object} entity the entity to remove if it matches the family requirements 501 | * 502 | * @description 503 | * Removes an entity from this family if entity matches requirements 504 | */ 505 | Family.prototype.removeIfMatch = function (e) { 506 | if (this.isMatch(e)) { 507 | this.remove(e); 508 | } 509 | }; 510 | 511 | Family.makeId = function (require) { 512 | if (!require) { 513 | return '::'; 514 | } 515 | if (typeof require === 'string') { 516 | return require; 517 | } 518 | return require.sort().join('::'); 519 | }; 520 | 521 | angular.module('hc.ngEcs') 522 | 523 | /** 524 | * @ngdoc object 525 | * @name hc.ngEcs.Family 526 | * @description 527 | * {@link hc.ngEcs.Family:family Family} factory. 528 | * 529 | * */ 530 | .constant('Family', Family); 531 | })(); 532 | 533 | /* global signals */ 534 | 535 | // engine 536 | (function () { 537 | 538 | 'use strict'; 539 | 540 | angular.module('hc.ngEcs') 541 | 542 | /** 543 | * @ngdoc service 544 | * @name hc.ngEcs.ngEcs 545 | * @requires hc.ngEcs.$components 546 | * @requires hc.ngEcs.$systems 547 | * @requires hc.ngEcs.$entities 548 | * @requires hc.ngEcs.$families 549 | * @requires hc.ngEcs.Entity 550 | * @requires hc.ngEcs.Family 551 | * @description 552 | * ECS engine. Contain System, Components, and Entities. 553 | * */ 554 | .service('ngEcs', ['$rootScope', '$log', '$timeout', '$components', '$systems', '$entities', '$families', 'Entity', 'Family', function ($rootScope, $log, $timeout, $components, $systems, $entities, $families, Entity, Family) { 555 | 556 | var _uuid = 0; 557 | function uuid() { 558 | var timestamp = new Date().getUTCMilliseconds(); 559 | return '' + _uuid++ + '_' + timestamp; 560 | } 561 | 562 | function Ecs(opts) { 563 | this.components = $components; 564 | this.systems = $systems; 565 | this.entities = $entities; 566 | this.families = $families; 567 | 568 | angular.forEach($systems, function (value, key) { 569 | // todo: test this 570 | this.$s(key, value); 571 | }); 572 | 573 | angular.forEach($entities, function (value) { 574 | // todo: test this 575 | this.$e(value); 576 | }); 577 | 578 | //this.$timer = null; 579 | this.$playing = false; 580 | //this.$delay = 1000; 581 | this.$requestId = null; 582 | this.$fps = 60; 583 | this.$interval = 1; 584 | //this.$systemsQueue = []; // make $scenes? Signal? 585 | 586 | this.started = new signals.Signal(); 587 | this.stopped = new signals.Signal(); 588 | 589 | this.updated = new signals.Signal(); 590 | this.rendered = new signals.Signal(); 591 | 592 | this.rendered.add(function () { 593 | $rootScope.$applyAsync(); 594 | }, null, -1); 595 | 596 | angular.extend(this, opts); 597 | } 598 | 599 | Ecs.prototype.constructor = Ecs; 600 | 601 | /** 602 | * @ngdoc service 603 | * @name hc.ngEcs.ngEcs#$c 604 | * @methodOf hc.ngEcs.ngEcs 605 | * 606 | * @description Adds a component contructor 607 | * 608 | * @param {string} key component key 609 | * @param {function|object|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation) or constructor prototype object 610 | */ 611 | Ecs.prototype.$c = function (key, constructor) { 612 | // perhaps add to $components 613 | if (typeof key !== 'string') { 614 | throw new TypeError('A components name is required'); 615 | } 616 | return $components[key] = makeConstructor(key, constructor); 617 | }; 618 | 619 | function makeConstructor(name, O) { 620 | 621 | if (angular.isArray(O)) { 622 | var T = O.pop(); 623 | T.$inject = O; 624 | O = T; 625 | }; 626 | 627 | if (angular.isFunction(O)) { 628 | return O; 629 | } 630 | 631 | if (typeof O !== 'object') { 632 | throw new TypeError('Component constructor may only be an Object or function'); 633 | } 634 | 635 | var Constructor = new Function('return function ' + name + '( instance ){ angular.extend(this, instance); }')(); 636 | 637 | Constructor.prototype = O; 638 | Constructor.prototype.constructor = Constructor; 639 | Constructor.$inject = ['$state']; 640 | 641 | return Constructor; 642 | } 643 | 644 | /** 645 | * @ngdoc service 646 | * @name hc.ngEcs.ngEcs#$f 647 | * @methodOf hc.ngEcs.ngEcs 648 | * 649 | * @description Gets a family 650 | * 651 | * @param {string} require Array of component keys 652 | */ 653 | Ecs.prototype.$f = function (require) { 654 | // perhaps add to $components 655 | var id = Family.makeId(require); 656 | var fam = $families[id]; 657 | if (fam) { 658 | return fam; 659 | } 660 | fam = $families[id] = new Family(require); 661 | onFamilyAdded(fam); 662 | 663 | return fam; 664 | }; 665 | 666 | var isDefined = angular.isDefined; 667 | 668 | /** 669 | * @ngdoc service 670 | * @name hc.ngEcs.ngEcs#$s 671 | * @methodOf hc.ngEcs.ngEcs 672 | * 673 | * @description Adds a system 674 | * 675 | * @param {string} key system key 676 | * @param {object} instance system configuration 677 | */ 678 | Ecs.prototype.$s = function (key, system) { 679 | // perhaps add to $systems 680 | 681 | if (typeof key === 'object') { 682 | system = key; 683 | key = uuid(); 684 | } 685 | 686 | $systems[key] = system; // todo: make a system class? Error if already existing. 687 | 688 | var $priority = system.$priority || 0; 689 | 690 | system.$family = this.$f(system.$require); // todo: later only store id? 691 | 692 | if (system.$addEntity) { 693 | system.$family.entityAdded.add(system.$addEntity, system, $priority); 694 | } 695 | 696 | if (system.$removeEntity) { 697 | system.$family.entityRemoved.add(system.$removeEntity, system, $priority); 698 | } 699 | 700 | this.$$addSystem($systems[key]); 701 | 702 | return system; 703 | }; 704 | 705 | Ecs.prototype.$$addSystem = function (system) { 706 | 707 | var $priority = system.$priority || 0; 708 | 709 | if (isDefined(system.$update)) { 710 | 711 | if (isDefined(system.interval)) { 712 | // add tests for interval 713 | system.acc = isDefined(system.acc) ? system.acc : 0; 714 | system.$$update = function (dt) { 715 | this.acc += dt; 716 | if (this.acc > this.interval) { 717 | if (system.$family.length > 0) { 718 | this.$update(this.interval); 719 | } 720 | this.acc = this.acc - this.interval; 721 | } 722 | }; 723 | } else { 724 | system.$$update = function (dt) { 725 | // can be system prototype 726 | if (system.$family.length > 0) { 727 | this.$update(dt); 728 | } 729 | }; 730 | } 731 | 732 | this.updated.add(system.$$update, system, $priority); 733 | } 734 | 735 | if (isDefined(system.$updateEach)) { 736 | system.$$updateEach = function (time) { 737 | // can be system prototype, bug: updateEach doesn't respect interval 738 | var arr = this.$family, 739 | i = arr.length; 740 | while (i--) { 741 | if (i in arr) { 742 | this.$updateEach(arr[i], time); 743 | } 744 | } 745 | }; 746 | this.updated.add(system.$$updateEach, system, $priority); 747 | } 748 | 749 | if (isDefined(system.$render)) { 750 | this.rendered.add(system.$render, system, $priority); 751 | } 752 | 753 | if (isDefined(system.$renderEach)) { 754 | system.$$renderEach = function () { 755 | var arr = this.$family, 756 | i = arr.length; 757 | while (i--) { 758 | if (i in arr) { 759 | this.$renderEach(arr[i]); 760 | } 761 | } 762 | }; 763 | this.rendered.add(system.$$renderEach, system); 764 | } 765 | 766 | if (isDefined(system.$started)) { 767 | this.started.add(system.$started, system, $priority); 768 | } 769 | 770 | if (isDefined(system.$stopped)) { 771 | this.stopped.add(system.$stopped, system, $priority); 772 | } 773 | 774 | if (isDefined(system.$added)) { 775 | system.$added(); 776 | } 777 | 778 | return this; 779 | }; 780 | 781 | Ecs.prototype.$$removeSystem = function (system) { 782 | // perhaps add to $systems 783 | 784 | if (typeof system === 'string') { 785 | system = $systems[key]; 786 | } 787 | 788 | if (isDefined(system.$$update)) { 789 | this.updated.remove(system.$$update, system); 790 | } 791 | 792 | if (isDefined(system.$$updateEach)) { 793 | this.updated.remove(system.$$updateEach, system); 794 | } 795 | 796 | if (isDefined(system.$render)) { 797 | this.rendered.remove(system.$render, system); 798 | } 799 | 800 | if (isDefined(system.$$renderEach)) { 801 | this.rendered.remove(system.$$renderEach, system); 802 | } 803 | 804 | if (isDefined(system.$started)) { 805 | this.started.remove(system.$started, system); 806 | } 807 | 808 | if (isDefined(system.$stopped)) { 809 | this.stopped.remove(system.$stopped, system); 810 | } 811 | 812 | if (isDefined(system.$removed)) { 813 | system.$removed(); 814 | } 815 | 816 | return this; 817 | }; 818 | 819 | /** 820 | * @ngdoc service 821 | * @name hc.ngEcs.ngEcs#$e 822 | * @methodOf hc.ngEcs.ngEcs 823 | * 824 | * @description Creates and adds an Entity 825 | * @see Entity 826 | * 827 | * @example 828 | *
829 | //config as array 830 | ngEcs.$e('player', ['position','control','collision']); 831 | //or config as object 832 | ngEcs.$e('player', { 833 | position: { x: 0, y: 50 }, 834 | control: {} 835 | collision: {} 836 | }); 837 | *838 | * 839 | * @param {string} id (optional) entity id 840 | * @param {object|array} instance (optional) config object of entity 841 | * @return {Entity} The Entity 842 | */ 843 | Ecs.prototype.$e = function (id, instance) { 844 | //var self = this; 845 | 846 | if (typeof id === 'object') { 847 | instance = id; 848 | id = null; 849 | } 850 | 851 | var e = new Entity(id); 852 | e.$world = this; // get rid of this 853 | 854 | if (Array.isArray(instance)) { 855 | instance.forEach(function (key) { 856 | e.$add(key); 857 | }); 858 | } else { 859 | angular.forEach(instance, function (value, key) { 860 | e.$add(key, value); 861 | }); 862 | } 863 | 864 | onComponentAdded(e); 865 | 866 | e.$componentAdded.add(onComponentAdded, this); 867 | e.$componentRemoved.add(onComponentRemoved, this); 868 | 869 | $entities[e._id] = e; 870 | 871 | return e; 872 | }; 873 | 874 | Ecs.prototype.$$removeEntity = function (e) { 875 | 876 | e.$world = null; 877 | 878 | angular.forEach(e, function (value, key) { 879 | if (key.charAt(0) !== '$' && key.charAt(0) !== '_') { 880 | e.$remove(key); 881 | } 882 | }); 883 | 884 | angular.forEach($families, function (family) { 885 | family.remove(e); 886 | }); 887 | 888 | e.$componentAdded.dispose(); 889 | e.$componentRemoved.dispose(); 890 | 891 | delete this.entities[e._id]; 892 | 893 | return this; 894 | }; 895 | 896 | function onFamilyAdded(family) { 897 | angular.forEach($entities, function (e) { 898 | family.addIfMatch(e); 899 | }); 900 | } 901 | 902 | function onComponentAdded(entity, key) { 903 | angular.forEach($families, function (family) { 904 | if (family.require && key && family.require.indexOf(key) < 0) { 905 | return; 906 | } 907 | family.addIfMatch(entity); 908 | }); 909 | } 910 | 911 | function onComponentRemoved(entity, key) { 912 | angular.forEach($families, function (family) { 913 | if (!family.require || key && family.require.indexOf(key) < 0) { 914 | return; 915 | } 916 | family.removeIfMatch(entity); 917 | }); 918 | } 919 | 920 | /** 921 | * @ngdoc service 922 | * @name hc.ngEcs.ngEcs#$update 923 | * @methodOf hc.ngEcs.ngEcs 924 | * 925 | * @description Calls the update cycle 926 | */ 927 | Ecs.prototype.$update = function (time) { 928 | this.updated.dispatch(time || this.$interval); 929 | }; 930 | 931 | Ecs.prototype.$render = function (time) { 932 | this.rendered.dispatch(time || this.$interval); 933 | }; 934 | 935 | Ecs.prototype.$runLoop = function () { 936 | 937 | window.cancelAnimationFrame(this.$requestId); 938 | 939 | var self = this, 940 | now, 941 | last = window.performance.now(), 942 | dt = 0, 943 | DT = 0, 944 | step; 945 | 946 | function frame() { 947 | if (!self.$playing || self.$paused) { 948 | return; 949 | } 950 | now = window.performance.now(); 951 | DT = Math.min(1, (now - last) / 1000); 952 | dt = dt + DT; 953 | step = 1 / self.$fps; 954 | while (dt > step) { 955 | dt = dt - step; 956 | self.$update(step); 957 | } 958 | self.$render(DT); 959 | 960 | last = now; 961 | self.$requestId = window.requestAnimationFrame(frame); 962 | } 963 | 964 | self.$requestId = window.requestAnimationFrame(frame); 965 | }; 966 | 967 | /** 968 | * @ngdoc service 969 | * @name hc.ngEcs.ngEcs#$start 970 | * @methodOf hc.ngEcs.ngEcs 971 | * 972 | * @description Starts the game loop 973 | */ 974 | Ecs.prototype.$start = function () { 975 | if (this.$playing) { 976 | return; 977 | } 978 | this.$playing = true; 979 | 980 | this.started.dispatch(); 981 | this.$runLoop(); 982 | }; 983 | 984 | /** 985 | * @ngdoc service 986 | * @name hc.ngEcs.ngEcs#$stop 987 | * @methodOf hc.ngEcs.ngEcs 988 | * 989 | * @description Stops the game loop 990 | */ 991 | Ecs.prototype.$stop = function () { 992 | this.$playing = false; 993 | window.cancelAnimationFrame(this.$requestId); 994 | this.stopped.dispatch(); 995 | }; 996 | 997 | Ecs.prototype.$pause = function () { 998 | if (!this.$playing) { 999 | return; 1000 | } 1001 | this.$paused = true; 1002 | }; 1003 | 1004 | Ecs.prototype.$unpause = function () { 1005 | if (!this.$playing || !this.$paused) { 1006 | return; 1007 | } 1008 | this.$paused = false; 1009 | this.$runLoop(); 1010 | }; 1011 | 1012 | var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/; 1013 | function isTypedArray(value) { 1014 | return TYPED_ARRAY_REGEXP.test(Object.prototype.toString.call(value)); 1015 | } 1016 | 1017 | // deep copy objects removing $ props 1018 | // must start with object, 1019 | // skips keys that start with $ 1020 | // navigates down objects but not other times (including arrays) 1021 | Ecs.prototype.$copyState = function ssCopy(src) { 1022 | var dst = {}; 1023 | for (var key in src) { 1024 | if (src.hasOwnProperty(key) && key.charAt(0) !== '$') { 1025 | var s = src[key]; 1026 | if (angular.isObject(s) && !isTypedArray(s) && !angular.isArray(s) && !angular.isDate(s)) { 1027 | dst[key] = ssCopy(s); 1028 | } else if (typeof s !== 'function') { 1029 | dst[key] = s; 1030 | } 1031 | } 1032 | } 1033 | return dst; 1034 | }; 1035 | 1036 | return new Ecs(); 1037 | }]); 1038 | })(); 1039 | 1040 | // shims 1041 | (function () { 1042 | 'use strict'; 1043 | 1044 | window.performance = window.performance || {}; 1045 | 1046 | window.performance.now = (function () { 1047 | return window.performance.now || window.performance.webkitNow || window.performance.msNow || window.performance.mozNow || Date.now || function () { 1048 | return new Date().getTime(); 1049 | }; 1050 | })(); 1051 | 1052 | window.requestAnimationFrame = (function () { 1053 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { 1054 | return setTimeout(function () { 1055 | var time = window.performance.now(); 1056 | callback(time); 1057 | }, 16); 1058 | }; 1059 | })(); 1060 | 1061 | window.cancelAnimationFrame = (function () { 1062 | return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.msCancelAnimationFrame || window.mozCancelAnimationFrame || function (id) { 1063 | clearTimeout(id); 1064 | }; 1065 | })(); 1066 | 1067 | if (!Function.prototype.bind) { 1068 | Function.prototype.bind = function (oThis) { 1069 | if (typeof this !== 'function') { 1070 | // closest thing possible to the ECMAScript 5 1071 | // internal IsCallable function 1072 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 1073 | } 1074 | 1075 | var aArgs = Array.prototype.slice.call(arguments, 1), 1076 | fToBind = this, 1077 | FNOP = function FNOP() {}, 1078 | fBound = function fBound() { 1079 | return fToBind.apply(this instanceof FNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); 1080 | }; 1081 | 1082 | FNOP.prototype = this.prototype; 1083 | fBound.prototype = new FNOP(); 1084 | 1085 | return fBound; 1086 | }; 1087 | } 1088 | })(); 1089 | //# sourceMappingURL=angular-ecs.js.map -------------------------------------------------------------------------------- /dist/angular-ecs.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["angular-ecs.js","angular-ecs-Entity.js","angular-ecs-Family.js","angular-ecs-engine.js","shims.js"],"names":[],"mappings":"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","file":"angular-ecs.js","sourcesContent":["/* global angular:true */\n\n// main\n(function() {\n\n 'use strict';\n\n /**\n * ngdoc overview\n * name index\n *\n * description\n * # An entity-component-system game framework made specifically for AngularJS.\n *\n * ## Why?\n *\n * There are many great game engines available for JavaScript. Many include all the pieces needed to develop games in JavaScript; a canvas based rendering engine, optimized and specialized game loop, pixel asset management, dependency injection, and so on. However, when developing a web game using AngularJS you may want to use only some parts of the game engine and leave other parts to Angular. To do this it often means playing tricks on the game engine to cooperate with angularjs. Angular-ecs is a entity-component-system built for and with AngularJS. Angular-ecs was built to play nice with the angular architecture and to feel, as much as possible, like a native part of the angular framework.\n *\n *\n */\n\n function MapProvider() {\n\n var map = {};\n\n this.register = function(name, constructor) {\n if (angular.isObject(name)) {\n angular.extend(map, name);\n } else {\n map[name] = constructor;\n }\n return this;\n };\n\n this.$get = ['$injector', function($injector) {\n angular.forEach(map, function(value, key) {\n if (angular.isFunction(value)) {\n map[key] = $injector.invoke(value, null, null, key);\n }\n });\n return map;\n }];\n\n }\n\n angular.module('hc.ngEcs',[])\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.$entities\n * @description\n * Index of {@link hc.ngEcs.Entity:entity entities}.\n **/\n .provider('$entities', MapProvider)\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.$componentsProvider\n * @description\n * This provider allows component registration via the register method.\n *\n **/\n\n /**\n * @ngdoc\n * @name hc.ngEcs.$componentsProvider#$register\n * @methodOf hc.ngEcs.$componentsProvider\n *\n * @description\n * Registers a componnet during configuration phase\n *\n * @param {string|object} name Component name, or an object map of components where the keys are the names and the values are the constructors.\n * @param {function()|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation).\n */\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.$components\n * @description\n * Index of components, components are object constructors\n * */\n .provider('$components', MapProvider)\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.$systemsProvider\n * @description\n * This provider allows component registration via the register method.\n *\n **/\n\n /**\n * @ngdoc\n * @name hc.ngEcs.$systemsProvider#$register\n * @methodOf hc.ngEcs.$systemsProvider\n *\n * @description\n * Registers a componnet during configuration phase\n *\n * @param {string|object} name System name, or an object map of systems where the keys are the names and the values are the constructors.\n * @param {function()|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation).\n */\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.$systems\n * @description\n * Index of systems, systems are generic objects\n * */\n .provider('$systems', MapProvider)\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.$families\n * @description\n * Index of {@link hc.ngEcs.Family:family families}, a family is an array of game entities matching a list of required components.\n * */\n .provider('$families', MapProvider);\n\n})();\n","// Entity\n(function() {\n\n 'use strict';\n\n angular.module('hc.ngEcs')\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.Entity\n * @requires hc.ngEcs.$components\n * @description\n * {@link hc.ngEcs.Entity:entity Entity} factory..\n *\n * */\n\n .factory('Entity', function($components) {\n var _uuid = 0;\n function uuid() {\n var timestamp = new Date().getUTCMilliseconds();\n return '' + _uuid++ + '_' + timestamp;\n }\n\n /**\n * @ngdoc object\n * @name hc.ngEcs.Entity:entity\n * @description\n * An Entity is bag of game properties (components). By convention properties that do not start with a $ or _ are considered compoenets.\n * */\n function Entity(id) {\n if(false === (this instanceof Entity)) {\n return new Entity(id);\n }\n this._id = id || uuid();\n\n this.$componentAdded = new signals.Signal();\n this.$componentRemoved = new signals.Signal();\n\n this.$$signals = {};\n\n }\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Entity:entity#$on\n * @methodOf hc.ngEcs.Entity:entity\n *\n * @description\n * Adds an event listener to the entity\n *\n * @example\n *
\n entity.$on('upgrade', function() { });\n *\n * @param {string} name Event name to listen on.\n * @param {function(event, ...args)} listener Function to call when the event is emitted.\n * @returns {function()} Returns a deregistration function for this listener.\n */\n Entity.prototype.$on = function(name, listener) {\n var sig = this.$$signals[name];\n if (!sig) {\n this.$$signals[name] = sig = new signals.Signal();\n }\n return sig.add(listener, this);\n };\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Entity:entity#$emit\n * @methodOf hc.ngEcs.Entity:entity\n *\n * @description\n * Dispatches an event `name` calling notifying\n * registered {@link hc.ngEcs.Entity#$on} listeners\n *\n * @example\n *
\n entity.$emit('upgrade');\n *\n * @param {string} name Event name to emit.\n * @param {...*} args Optional one or more arguments which will be passed onto the event listeners.\n * @returns {Entity} The entity\n */\n Entity.prototype.$emit = function(name) {\n var sig = this.$$signals[name];\n if (!sig) {return;} // throw error?\n\n if (arguments.length > 1) {\n var args = Array.prototype.slice.call(arguments, 1);\n sig.dispatch.apply(sig, args);\n } else {\n sig.dispatch();\n }\n\n return this;\n };\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Entity:entity#$add\n * @methodOf hc.ngEcs.Entity:entity\n *\n * @description\n * Adds a Component to the entity\n *\n * @example\n *
\n entity.$add('position', {\n x: 1.0,\n y: 3.0\n });\n *\n * @param {string} key The name of the Component\n * @param {object} [instance] A component instance or a compoent configuration\n * @returns {Entity} The entity\n */\n Entity.prototype.$add = function(key, instance) {\n\n if (!key) {\n throw new Error('Can\\'t add component with undefined key.');\n }\n\n // remove if exists\n if (this[key]) {\n this.$remove(key);\n }\n\n instance = angular.isDefined(instance) ? instance : {};\n\n // not a component by convention\n if (key.charAt(0) === '$' || key.charAt(0) === '_') {\n this[key] = instance;\n return; // no emit\n }\n\n this[key] = createComponent(this, key, instance);\n\n this.$componentAdded.dispatch(this, key);\n return this;\n };\n\n function createComponent(e, name, state) {\n\n // not a registered component\n if (!$components.hasOwnProperty(name)) {\n return state;\n }\n\n var Type = $components[name];\n\n // not valid constructor\n if (!angular.isFunction(Type)) {\n throw new TypeError('Component constructor may only be an Object or function');\n return;\n }\n\n // already an instance\n if (state instanceof Type) {\n return state;\n }\n\n // inject\n if (Type.$inject) {\n return instantiate(Type, e, state);\n }\n\n return angular.extend(new Type(e), state);\n\n }\n\n function instantiate(Type, e, state) {\n var $inject = Type.$inject;\n\n var length = $inject.length, args = new Array(length), i;\n\n for (i = 0; i < length; ++i) {\n args[i] = getValue(e, $inject[i], state);\n }\n\n var instance = Object.create(Type.prototype || null);\n Type.apply(instance, args);\n return instance;\n }\n\n function getValue(e, key, state) {\n if (key === '$parent') { return e; }\n if (key === '$state') { return state; }\n //if (key === '$world') { return ngEcs; } // todo\n return state[key];\n }\n\n function isComponent(name) {\n return name.charAt(0) !== '$' && name.charAt(0) !== '_';\n }\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Entity:entity#$remove\n * @methodOf hc.ngEcs.Entity:entity\n *\n * @description\n * Removes a component from the entity\n *\n * @example\n *
\n entity.$remove('position');\n *\n * @param {string} key The name of the Component\n * @returns {Entity} The entity\n */\n Entity.prototype.$remove = function(key) {\n // not a component by convention\n if (isComponent(key)) {\n this.$componentRemoved.dispatch(this, key);\n }\n delete this[key];\n return this;\n };\n\n return Entity;\n });\n\n})();\n","// Entity\n(function() {\n\n 'use strict';\n\n /**\n * @ngdoc object\n * @name hc.ngEcs.Family:family\n * @description\n * A Family is array of game entities matching a list of required components.\n *\n **/\n\n function Family(require) {\n var _this = [];\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family:family#require\n * @propertyOf hc.ngEcs.Family:family\n *\n * @description\n * An array of component requirements of this family\n */\n Object.defineProperty(_this, 'require', {\n enumerable: false,\n writable: false,\n value: require\n });\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#entityAdded\n * @propertyOf hc.ngEcs.Family:family\n *\n * @description\n * A signal dispatched when an entity is added\n */\n Object.defineProperty(_this, 'entityAdded', {\n enumerable: false,\n value: new signals.Signal()\n });\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#entityRemoved\n * @propertyOf hc.ngEcs.Family:family\n *\n * @description\n * A signal dispatched when an entity is removed\n */\n Object.defineProperty(_this, 'entityRemoved', {\n enumerable: false,\n value: new signals.Signal()\n });\n\n for (var method in Family.prototype) {\n if (Family.prototype.hasOwnProperty(method)) {\n Object.defineProperty(_this, method, {\n enumerable: false,\n value: Family.prototype[method]\n });\n }\n }\n\n return _this;\n }\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#isMatch\n * @methodOf hc.ngEcs.Family:family\n * @param {object} entity the entity to test match.\n * @returns {boolean} True if the entity matches this family\n *\n * @description\n * Tests if the entity matches the family requirements\n */\n Family.prototype.isMatch = function(entity) {\n if (!this.require) { return true; }\n\n return this.require.every(function(d) {\n return entity.hasOwnProperty(d);\n });\n };\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#add\n * @methodOf hc.ngEcs.Family:family\n * @param {object} entity the entity to add.\n *\n * @description\n * Adds an entity to this family\n */\n Family.prototype.add = function(e) {\n // check if match?\n var index = this.indexOf(e);\n if (index < 0) {\n this.push(e);\n this.entityAdded.dispatch(e);\n }\n };\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#addIfMatch\n * @methodOf hc.ngEcs.Family:family\n * @param {object} entity the entity to add if it matches the family requirements\n *\n * @description\n * Adds an entity to this family if entity matches requirements\n */\n Family.prototype.addIfMatch = function(e) {\n if (this.isMatch(e)) {\n this.add(e);\n }\n };\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#remove\n * @methodOf hc.ngEcs.Family:family\n * @param {object} entity the entity to remove\n *\n * @description\n * Removes an entity from this family\n */\n Family.prototype.remove = function(e) {\n var index = this.indexOf(e);\n if (index > -1) {\n this.splice(index,1);\n this.entityRemoved.dispatch(e);\n }\n };\n\n /**\n * @ngdoc\n * @name hc.ngEcs.Family#removeIfMatch\n * @methodOf hc.ngEcs.Family:family\n * @param {object} entity the entity to remove if it matches the family requirements\n *\n * @description\n * Removes an entity from this family if entity matches requirements\n */\n Family.prototype.removeIfMatch = function(e) {\n if (this.isMatch(e)) {\n this.remove(e);\n }\n };\n\n Family.makeId = function(require) {\n if (!require) { return '::'; }\n if (typeof require === 'string') { return require; }\n return require.sort().join('::');\n };\n\n angular.module('hc.ngEcs')\n\n /**\n * @ngdoc object\n * @name hc.ngEcs.Family\n * @description\n * {@link hc.ngEcs.Family:family Family} factory.\n *\n * */\n .constant('Family', Family);\n\n})();\n","/* global signals */\n\n// engine\n(function() {\n\n 'use strict';\n\n angular.module('hc.ngEcs')\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs\n * @requires hc.ngEcs.$components\n * @requires hc.ngEcs.$systems\n * @requires hc.ngEcs.$entities\n * @requires hc.ngEcs.$families\n * @requires hc.ngEcs.Entity\n * @requires hc.ngEcs.Family\n * @description\n * ECS engine. Contain System, Components, and Entities.\n * */\n .service('ngEcs', function($rootScope, $log, $timeout, $components, $systems, $entities, $families, Entity, Family) {\n\n var _uuid = 0;\n function uuid() {\n var timestamp = new Date().getUTCMilliseconds();\n return '' + _uuid++ + '_' + timestamp;\n }\n\n function Ecs(opts) {\n this.components = $components;\n this.systems = $systems;\n this.entities = $entities;\n this.families = $families;\n\n angular.forEach($systems, function(value, key) { // todo: test this\n this.$s(key, value);\n });\n\n angular.forEach($entities, function(value) { // todo: test this\n this.$e(value);\n });\n\n //this.$timer = null;\n this.$playing = false;\n //this.$delay = 1000;\n this.$requestId = null;\n this.$fps = 60;\n this.$interval = 1;\n //this.$systemsQueue = []; // make $scenes? Signal?\n\n this.started = new signals.Signal();\n this.stopped = new signals.Signal();\n\n this.updated = new signals.Signal();\n this.rendered = new signals.Signal();\n\n this.rendered.add(function() { $rootScope.$applyAsync(); }, null, -1);\n\n angular.extend(this, opts);\n }\n\n Ecs.prototype.constructor = Ecs;\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$c\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Adds a component contructor\n *\n * @param {string} key component key\n * @param {function|object|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation) or constructor prototype object\n */\n Ecs.prototype.$c = function(key, constructor) { // perhaps add to $components\n if (typeof key !== 'string') {\n throw new TypeError('A components name is required');\n }\n return $components[key] = makeConstructor(key, constructor);\n };\n\n function makeConstructor(name, O) {\n\n if (angular.isArray(O)) {\n var T = O.pop();\n T.$inject = O;\n O = T;\n };\n\n if (angular.isFunction(O)) { return O; }\n\n if (typeof O !== 'object') {\n throw new TypeError('Component constructor may only be an Object or function');\n }\n\n var Constructor = new Function(\n 'return function ' + name + '( instance ){ angular.extend(this, instance); }'\n )();\n\n Constructor.prototype = O;\n Constructor.prototype.constructor = Constructor;\n Constructor.$inject = ['$state'];\n\n return Constructor;\n }\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$f\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Gets a family\n *\n * @param {string} require Array of component keys\n */\n Ecs.prototype.$f = function(require) { // perhaps add to $components\n var id = Family.makeId(require);\n var fam = $families[id];\n if (fam) { return fam; }\n fam = $families[id] = new Family(require);\n onFamilyAdded(fam);\n\n return fam;\n };\n\n var isDefined = angular.isDefined;\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$s\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Adds a system\n *\n * @param {string} key system key\n * @param {object} instance system configuration\n */\n Ecs.prototype.$s = function(key, system) { // perhaps add to $systems\n\n if (typeof key === 'object') {\n system = key;\n key = uuid();\n }\n\n $systems[key] = system; // todo: make a system class? Error if already existing.\n\n var $priority = system.$priority || 0;\n\n system.$family = this.$f(system.$require); // todo: later only store id?\n\n if (system.$addEntity) {\n system.$family.entityAdded.add(system.$addEntity, system, $priority);\n }\n\n if (system.$removeEntity) {\n system.$family.entityRemoved.add(system.$removeEntity, system, $priority);\n }\n\n this.$$addSystem($systems[key]);\n\n return system;\n\n };\n\n Ecs.prototype.$$addSystem = function(system) {\n\n var $priority = system.$priority || 0;\n\n if (isDefined(system.$update)) {\n\n if (isDefined(system.interval)) { // add tests for interval\n system.acc = isDefined(system.acc) ? system.acc : 0;\n system.$$update = function(dt) {\n this.acc += dt;\n if (this.acc > this.interval) {\n if (system.$family.length > 0) { this.$update(this.interval); }\n this.acc = this.acc - this.interval;\n }\n };\n } else {\n system.$$update = function(dt) { // can be system prototype\n if (system.$family.length > 0) { this.$update(dt); }\n };\n }\n\n this.updated.add(system.$$update, system, $priority);\n }\n\n if (isDefined(system.$updateEach)) {\n system.$$updateEach = function(time) { // can be system prototype, bug: updateEach doesn't respect interval\n var arr = this.$family,i = arr.length;\n while (i--) {\n if (i in arr) {\n this.$updateEach(arr[i], time);\n }\n }\n };\n this.updated.add(system.$$updateEach, system, $priority);\n }\n\n if (isDefined(system.$render)) {\n this.rendered.add(system.$render, system, $priority);\n }\n\n if (isDefined(system.$renderEach)) {\n system.$$renderEach = function() {\n var arr = this.$family,i = arr.length;\n while (i--) {\n if (i in arr) {\n this.$renderEach(arr[i]);\n }\n }\n };\n this.rendered.add(system.$$renderEach, system);\n }\n\n if (isDefined(system.$started)) {\n this.started.add(system.$started, system, $priority);\n }\n\n if (isDefined(system.$stopped)) {\n this.stopped.add(system.$stopped, system, $priority);\n }\n\n if (isDefined(system.$added)) {\n system.$added();\n }\n\n return this;\n };\n\n Ecs.prototype.$$removeSystem = function(system) { // perhaps add to $systems\n\n if (typeof system === 'string') {\n system = $systems[key];\n }\n\n if (isDefined(system.$$update)) {\n this.updated.remove(system.$$update, system);\n }\n\n if (isDefined(system.$$updateEach)) {\n this.updated.remove(system.$$updateEach, system);\n }\n\n if (isDefined(system.$render)) {\n this.rendered.remove(system.$render, system);\n }\n\n if (isDefined(system.$$renderEach)) {\n this.rendered.remove(system.$$renderEach, system);\n }\n\n if (isDefined(system.$started)) {\n this.started.remove(system.$started, system);\n }\n\n if (isDefined(system.$stopped)) {\n this.stopped.remove(system.$stopped, system);\n }\n\n if (isDefined(system.$removed)) {\n system.$removed();\n }\n\n return this;\n\n };\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$e\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Creates and adds an Entity\n * @see Entity\n *\n * @example\n *
\n //config as array\n ngEcs.$e('player', ['position','control','collision']);\n\n //or config as object\n ngEcs.$e('player', {\n position: { x: 0, y: 50 },\n control: {}\n collision: {}\n });\n *\n *\n * @param {string} id (optional) entity id\n * @param {object|array} instance (optional) config object of entity\n * @return {Entity} The Entity\n */\n Ecs.prototype.$e = function(id, instance) {\n //var self = this;\n\n if (typeof id === 'object') {\n instance = id;\n id = null;\n }\n\n var e = new Entity(id);\n e.$world = this; // get rid of this\n\n if (Array.isArray(instance)) {\n instance.forEach(function(key) {\n e.$add(key);\n });\n } else {\n angular.forEach(instance, function(value, key) {\n e.$add(key, value);\n });\n }\n\n onComponentAdded(e);\n\n e.$componentAdded.add(onComponentAdded, this);\n e.$componentRemoved.add(onComponentRemoved, this);\n\n $entities[e._id] = e;\n\n return e;\n };\n\n Ecs.prototype.$$removeEntity = function(e) {\n\n e.$world = null;\n\n angular.forEach(e, function(value, key) {\n if (key.charAt(0) !== '$' && key.charAt(0) !== '_') {\n e.$remove(key);\n }\n });\n\n angular.forEach($families, function(family) {\n family.remove(e);\n });\n\n e.$componentAdded.dispose();\n e.$componentRemoved.dispose();\n\n delete this.entities[e._id];\n\n return this;\n\n };\n\n function onFamilyAdded(family) {\n angular.forEach($entities, function(e) {\n family.addIfMatch(e);\n });\n }\n\n function onComponentAdded(entity, key) {\n angular.forEach($families, function(family) {\n if (family.require && key && family.require.indexOf(key) < 0) { return; }\n family.addIfMatch(entity);\n });\n }\n\n function onComponentRemoved(entity, key) {\n angular.forEach($families, function(family) {\n if (!family.require || (key && family.require.indexOf(key) < 0)) { return; }\n family.removeIfMatch(entity);\n });\n }\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$update\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Calls the update cycle\n */\n Ecs.prototype.$update = function(time) {\n this.updated.dispatch(time || this.$interval);\n };\n\n Ecs.prototype.$render = function(time) {\n this.rendered.dispatch(time || this.$interval);\n };\n\n Ecs.prototype.$runLoop = function() {\n\n window.cancelAnimationFrame(this.$requestId);\n\n var self = this,\n now,\n last = window.performance.now(),\n dt = 0,\n DT = 0,\n step;\n\n function frame() {\n if (!self.$playing || self.$paused) { return; }\n now = window.performance.now();\n DT = Math.min(1, (now - last) / 1000);\n dt = dt + DT;\n step = 1/self.$fps;\n while(dt > step) {\n dt = dt - step;\n self.$update(step);\n }\n self.$render(DT);\n\n last = now;\n self.$requestId = window.requestAnimationFrame(frame);\n }\n\n self.$requestId = window.requestAnimationFrame(frame);\n };\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$start\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Starts the game loop\n */\n Ecs.prototype.$start = function() {\n if (this.$playing) { return; }\n this.$playing = true;\n\n this.started.dispatch();\n this.$runLoop();\n };\n\n /**\n * @ngdoc service\n * @name hc.ngEcs.ngEcs#$stop\n * @methodOf hc.ngEcs.ngEcs\n *\n * @description Stops the game loop\n */\n Ecs.prototype.$stop = function() {\n this.$playing = false;\n window.cancelAnimationFrame(this.$requestId);\n this.stopped.dispatch();\n };\n\n Ecs.prototype.$pause = function() {\n if (!this.$playing) { return; }\n this.$paused = true;\n };\n\n Ecs.prototype.$unpause = function() {\n if (!this.$playing || !this.$paused) { return; }\n this.$paused = false;\n this.$runLoop();\n };\n\n var TYPED_ARRAY_REGEXP = /^\\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\\]$/;\n function isTypedArray(value) {\n return TYPED_ARRAY_REGEXP.test(Object.prototype.toString.call(value));\n }\n\n // deep copy objects removing $ props\n // must start with object,\n // skips keys that start with $\n // navigates down objects but not other times (including arrays)\n Ecs.prototype.$copyState = function ssCopy(src) {\n var dst = {};\n for (var key in src) {\n if (src.hasOwnProperty(key) && key.charAt(0) !== '$') {\n var s = src[key];\n if (angular.isObject(s) && !isTypedArray(s) && !angular.isArray(s) && !angular.isDate(s)) {\n dst[key] = ssCopy(s);\n } else if (typeof s !== 'function') {\n dst[key] = s;\n }\n }\n }\n return dst;\n }\n\n return new Ecs();\n\n });\n\n})();\n","// shims\n(function () {\n 'use strict';\n\n window.performance = (window.performance || {});\n\n window.performance.now = (function () {\n return (\n window.performance.now ||\n window.performance.webkitNow ||\n window.performance.msNow ||\n window.performance.mozNow ||\n Date.now ||\n function () {\n return new Date().getTime();\n });\n })();\n\n window.requestAnimationFrame = (function () {\n return (\n window.requestAnimationFrame ||\n window.webkitRequestAnimationFrame ||\n window.msRequestAnimationFrame ||\n window.mozRequestAnimationFrame ||\n function (callback) {\n return setTimeout(function () {\n var time = window.performance.now();\n callback(time);\n }, 16);\n });\n })();\n\n window.cancelAnimationFrame = (function () {\n return (\n window.cancelAnimationFrame ||\n window.webkitCancelAnimationFrame ||\n window.msCancelAnimationFrame ||\n window.mozCancelAnimationFrame ||\n function(id) {\n clearTimeout(id);\n });\n })();\n\n if (!Function.prototype.bind) {\n Function.prototype.bind = function(oThis) {\n if (typeof this !== 'function') {\n // closest thing possible to the ECMAScript 5\n // internal IsCallable function\n throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');\n }\n\n var aArgs = Array.prototype.slice.call(arguments, 1),\n fToBind = this,\n FNOP = function() {},\n fBound = function() {\n return fToBind.apply(this instanceof FNOP ? this\n : oThis,\n aArgs.concat(Array.prototype.slice.call(arguments)));\n };\n\n FNOP.prototype = this.prototype;\n fBound.prototype = new FNOP();\n\n return fBound;\n };\n }\n\n})();\n"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /dist/angular-ecs.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-ecs - An ECS framework built for AngularJS 3 | * @version v0.0.20 4 | * @link https://github.com/Hypercubed/angular-ecs 5 | * @author Jayson Harshbarger <> 6 | * @license 7 | */ 8 | "use strict";!function(){function t(){var t={};this.register=function(e,n){return angular.isObject(e)?angular.extend(t,e):t[e]=n,this},this.$get=["$injector",function(e){return angular.forEach(t,function(n,i){angular.isFunction(n)&&(t[i]=e.invoke(n,null,null,i))}),t}]}angular.module("hc.ngEcs",[]).provider("$entities",t).provider("$components",t).provider("$systems",t).provider("$families",t)}(),function(){angular.module("hc.ngEcs").factory("Entity",["$components",function(t){function e(){var t=(new Date).getUTCMilliseconds();return""+s++ +"_"+t}function n(t){return!1==this instanceof n?new n(t):(this._id=t||e(),this.$componentAdded=new signals.Signal,this.$componentRemoved=new signals.Signal,void(this.$$signals={}))}function i(e,n,i){if(!t.hasOwnProperty(n))return i;var o=t[n];if(!angular.isFunction(o))throw new TypeError("Component constructor may only be an Object or function");return i instanceof o?i:o.$inject?r(o,e,i):angular.extend(new o(e),i)}function r(t,e,n){var i,r=t.$inject,a=r.length,s=new Array(a);for(i=0;a>i;++i)s[i]=o(e,r[i],n);var c=Object.create(t.prototype||null);return t.apply(c,s),c}function o(t,e,n){return"$parent"===e?t:"$state"===e?n:n[e]}function a(t){return"$"!==t.charAt(0)&&"_"!==t.charAt(0)}var s=0;return n.prototype.$on=function(t,e){var n=this.$$signals[t];return n||(this.$$signals[t]=n=new signals.Signal),n.add(e,this)},n.prototype.$emit=function(t){var e=this.$$signals[t];if(e){if(arguments.length>1){var n=Array.prototype.slice.call(arguments,1);e.dispatch.apply(e,n)}else e.dispatch();return this}},n.prototype.$add=function(t,e){if(!t)throw new Error("Can't add component with undefined key.");return this[t]&&this.$remove(t),e=angular.isDefined(e)?e:{},"$"===t.charAt(0)||"_"===t.charAt(0)?void(this[t]=e):(this[t]=i(this,t,e),this.$componentAdded.dispatch(this,t),this)},n.prototype.$remove=function(t){return a(t)&&this.$componentRemoved.dispatch(this,t),delete this[t],this},n}])}(),function(){function t(e){var n=[];Object.defineProperty(n,"require",{enumerable:!1,writable:!1,value:e}),Object.defineProperty(n,"entityAdded",{enumerable:!1,value:new signals.Signal}),Object.defineProperty(n,"entityRemoved",{enumerable:!1,value:new signals.Signal});for(var i in t.prototype)t.prototype.hasOwnProperty(i)&&Object.defineProperty(n,i,{enumerable:!1,value:t.prototype[i]});return n}t.prototype.isMatch=function(t){return this.require?this.require.every(function(e){return t.hasOwnProperty(e)}):!0},t.prototype.add=function(t){var e=this.indexOf(t);0>e&&(this.push(t),this.entityAdded.dispatch(t))},t.prototype.addIfMatch=function(t){this.isMatch(t)&&this.add(t)},t.prototype.remove=function(t){var e=this.indexOf(t);e>-1&&(this.splice(e,1),this.entityRemoved.dispatch(t))},t.prototype.removeIfMatch=function(t){this.isMatch(t)&&this.remove(t)},t.makeId=function(t){return t?"string"==typeof t?t:t.sort().join("::"):"::"},angular.module("hc.ngEcs").constant("Family",t)}(),function(){angular.module("hc.ngEcs").service("ngEcs",["$rootScope","$log","$timeout","$components","$systems","$entities","$families","Entity","Family",function(t,e,n,i,r,o,a,s,c){function d(){var t=(new Date).getUTCMilliseconds();return""+m++ +"_"+t}function u(e){this.components=i,this.systems=r,this.entities=o,this.families=a,angular.forEach(r,function(t,e){this.$s(e,t)}),angular.forEach(o,function(t){this.$e(t)}),this.$playing=!1,this.$requestId=null,this.$fps=60,this.$interval=1,this.started=new signals.Signal,this.stopped=new signals.Signal,this.updated=new signals.Signal,this.rendered=new signals.Signal,this.rendered.add(function(){t.$applyAsync()},null,-1),angular.extend(this,e)}function p(t,e){if(angular.isArray(e)){var n=e.pop();n.$inject=e,e=n}if(angular.isFunction(e))return e;if("object"!=typeof e)throw new TypeError("Component constructor may only be an Object or function");var i=new Function("return function "+t+"( instance ){ angular.extend(this, instance); }")();return i.prototype=e,i.prototype.constructor=i,i.$inject=["$state"],i}function h(t){angular.forEach(o,function(e){t.addIfMatch(e)})}function $(t,e){angular.forEach(a,function(n){n.require&&e&&n.require.indexOf(e)<0||n.addIfMatch(t)})}function f(t,e){angular.forEach(a,function(n){!n.require||e&&n.require.indexOf(e)<0||n.removeIfMatch(t)})}function l(t){return w.test(Object.prototype.toString.call(t))}var m=0;u.prototype.constructor=u,u.prototype.$c=function(t,e){if("string"!=typeof t)throw new TypeError("A components name is required");return i[t]=p(t,e)},u.prototype.$f=function(t){var e=c.makeId(t),n=a[e];return n?n:(n=a[e]=new c(t),h(n),n)};var y=angular.isDefined;u.prototype.$s=function(t,e){"object"==typeof t&&(e=t,t=d()),r[t]=e;var n=e.$priority||0;return e.$family=this.$f(e.$require),e.$addEntity&&e.$family.entityAdded.add(e.$addEntity,e,n),e.$removeEntity&&e.$family.entityRemoved.add(e.$removeEntity,e,n),this.$$addSystem(r[t]),e},u.prototype.$$addSystem=function(t){var e=t.$priority||0;return y(t.$update)&&(y(t.interval)?(t.acc=y(t.acc)?t.acc:0,t.$$update=function(e){this.acc+=e,this.acc>this.interval&&(t.$family.length>0&&this.$update(this.interval),this.acc=this.acc-this.interval)}):t.$$update=function(e){t.$family.length>0&&this.$update(e)},this.updated.add(t.$$update,t,e)),y(t.$updateEach)&&(t.$$updateEach=function(t){for(var e=this.$family,n=e.length;n--;)n in e&&this.$updateEach(e[n],t)},this.updated.add(t.$$updateEach,t,e)),y(t.$render)&&this.rendered.add(t.$render,t,e),y(t.$renderEach)&&(t.$$renderEach=function(){for(var t=this.$family,e=t.length;e--;)e in t&&this.$renderEach(t[e])},this.rendered.add(t.$$renderEach,t)),y(t.$started)&&this.started.add(t.$started,t,e),y(t.$stopped)&&this.stopped.add(t.$stopped,t,e),y(t.$added)&&t.$added(),this},u.prototype.$$removeSystem=function(t){return"string"==typeof t&&(t=r[key]),y(t.$$update)&&this.updated.remove(t.$$update,t),y(t.$$updateEach)&&this.updated.remove(t.$$updateEach,t),y(t.$render)&&this.rendered.remove(t.$render,t),y(t.$$renderEach)&&this.rendered.remove(t.$$renderEach,t),y(t.$started)&&this.started.remove(t.$started,t),y(t.$stopped)&&this.stopped.remove(t.$stopped,t),y(t.$removed)&&t.$removed(),this},u.prototype.$e=function(t,e){"object"==typeof t&&(e=t,t=null);var n=new s(t);return n.$world=this,Array.isArray(e)?e.forEach(function(t){n.$add(t)}):angular.forEach(e,function(t,e){n.$add(e,t)}),$(n),n.$componentAdded.add($,this),n.$componentRemoved.add(f,this),o[n._id]=n,n},u.prototype.$$removeEntity=function(t){return t.$world=null,angular.forEach(t,function(e,n){"$"!==n.charAt(0)&&"_"!==n.charAt(0)&&t.$remove(n)}),angular.forEach(a,function(e){e.remove(t)}),t.$componentAdded.dispose(),t.$componentRemoved.dispose(),delete this.entities[t._id],this},u.prototype.$update=function(t){this.updated.dispatch(t||this.$interval)},u.prototype.$render=function(t){this.rendered.dispatch(t||this.$interval)},u.prototype.$runLoop=function(){function t(){if(i.$playing&&!i.$paused){for(e=window.performance.now(),a=Math.min(1,(e-r)/1e3),o+=a,n=1/i.$fps;o>n;)o-=n,i.$update(n);i.$render(a),r=e,i.$requestId=window.requestAnimationFrame(t)}}window.cancelAnimationFrame(this.$requestId);var e,n,i=this,r=window.performance.now(),o=0,a=0;i.$requestId=window.requestAnimationFrame(t)},u.prototype.$start=function(){this.$playing||(this.$playing=!0,this.started.dispatch(),this.$runLoop())},u.prototype.$stop=function(){this.$playing=!1,window.cancelAnimationFrame(this.$requestId),this.stopped.dispatch()},u.prototype.$pause=function(){this.$playing&&(this.$paused=!0)},u.prototype.$unpause=function(){this.$playing&&this.$paused&&(this.$paused=!1,this.$runLoop())};var w=/^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/;return u.prototype.$copyState=function g(t){var e={};for(var n in t)if(t.hasOwnProperty(n)&&"$"!==n.charAt(0)){var i=t[n];!angular.isObject(i)||l(i)||angular.isArray(i)||angular.isDate(i)?"function"!=typeof i&&(e[n]=i):e[n]=g(i)}return e},new u}])}(),function(){window.performance=window.performance||{},window.performance.now=function(){return window.performance.now||window.performance.webkitNow||window.performance.msNow||window.performance.mozNow||Date.now||function(){return(new Date).getTime()}}(),window.requestAnimationFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.mozRequestAnimationFrame||function(t){return setTimeout(function(){var e=window.performance.now();t(e)},16)}}(),window.cancelAnimationFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.msCancelAnimationFrame||window.mozCancelAnimationFrame||function(t){clearTimeout(t)}}(),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),n=this,i=function(){},r=function(){return n.apply(this instanceof i?this:t,e.concat(Array.prototype.slice.call(arguments)))};return i.prototype=this.prototype,r.prototype=new i,r})}(); -------------------------------------------------------------------------------- /docs-content/index.ngdoc: -------------------------------------------------------------------------------- 1 | @ngdoc overview 2 | @name index 3 | @description 4 | 5 | # Why? 6 | 7 | There are many great game engines available for JavaScript. Many include all the pieces needed to develop games in JavaScript; a canvas based rendering engine, optimized and specialized game loop, pixel asset management, dependency injection, and so on. However, when developing a web game using AngularJS you may want to use only some parts of the game engine and leave other parts to Angular. To do this it often means playing tricks on the game engine to cooperate with AngularJS. Angular-ecs is a entity-component-system built for and with AngularJS. Angular-ecs was built to play nice with the angular architecture and to feel, as much as possible, like a native part of the angular framework. 8 | 9 | # What? 10 | 11 | Many ECS engines use linked lists internally to store game objects (entities, systems, etc). They do this to optimize insertion and deletion. These data structures don't play well with angular, especially angular directives. The result of using another ECS engine is the need to constantly push ECS engine objects to JS Arrays for viewing in Angular. I argue that if you are using angular for your view layer this costs outweighs the advantage during insert and delete (this is conjecture on my part, I could be wrong). A second advantage is the ease of serialization. ngEcs objects are easy to serialize to and from localstorage, a database, or rest api. 12 | 13 | # How? 14 | 15 | The Entity-component-system (ECS) pattern is an architectural pattern often used in game development. The ECS pattern follows the Composition over inheritance principle allowing greater flexibility in defining game entities. In ECS every game Entity consists of one or more components which add additional behavior or functionality. Therefore the behavior of an entity can be changed adding or removing components. 16 | 17 | # Quick Start 18 | 19 | ## Terminology 20 | 21 | Entity: An entity is a general purpose object. In ngEcs an entity is a JavaScript object that contains a unique id and properties corresponding to components. 22 | 23 | Component: A component is the raw data and behavior for for one aspect of the game object. In ngEcs an entity is a JavaScript object. 24 | 25 | System: A system is a set of functions an d properties that performs global actions on game entities. 26 | 27 | ## Define a Component 28 | 29 | Components in ngEcs are defined by standard JavaScript Object constructor functions. You define a component using a "protyype" object or with a JS object constructor function. 30 | 31 | The simplest way to define a component constructor is to provide a "prototype" object: 32 | 33 | ```js 34 | ngEcs.$c('position', {x: 0, y: 0}); 35 | ``` 36 | 37 | However, to define component behaviors use a JS object constructor function: 38 | 39 | ```js 40 | function Vector() { 41 | this.x = 0; 42 | this.y = 0; 43 | } 44 | 45 | Vector.prototype.init = function(x,y) { 46 | this.x = x; 47 | this.y = y; 48 | }; 49 | 50 | Vector.prototype.scale = function(s) { 51 | this.x *= s; 52 | this.y *= s; 53 | return this; 54 | }; 55 | 56 | ngEcs.$c('position', Vector); 57 | ngEcs.$c('velocity', Vector); 58 | ``` 59 | 60 | Notice that the same constructor can be used to defined multiple components. 61 | 62 | 63 | ## Define a System 64 | 65 | A system is responsible for updating entities. 66 | 67 | ```js 68 | ngEcs.$s('physics', { 69 | $require: ['position', 'velocity'], 70 | $update: function (dt) { 71 | // called once per update cycle 72 | }, 73 | $updateEach: function (entity, dt) { 74 | // called once per entity per update cycle 75 | position = entity.position; 76 | velocity = entity.velocity; 77 | position.x += velocity.x * dt; 78 | position.y += velocity.y * dt; 79 | }, 80 | $render: function () { 81 | // called once after the update cycle 82 | } 83 | }); 84 | ``` 85 | 86 | Adding a system with a $require will automatically create an associated family: 87 | 88 | ```js 89 | assert($families['position::velocity'].length === 1); 90 | ``` 91 | 92 | You can also listen for entities to be added to the family: 93 | 94 | ```js 95 | ngEcs.$s('physics', { 96 | $require: ['position', 'velocity'], 97 | $update: function (entity, dt) { 98 | // same as above 99 | }, 100 | $updateEach: function (entity, dt) { 101 | // same as above 102 | }, 103 | $render: function () { 104 | // same as above 105 | }, 106 | $entityAdded(entity) { 107 | // This function is called whenever an entity with both 'position' and 108 | // 'velocity' components is added to the world or when a component 109 | // is added that causes the contain both 'position' and 110 | // 'velocity' components 111 | }, 112 | $entityAddedRemoved(entity) { 113 | // This function is called whenever an entity with both 'position' and 114 | // 'velocity' components is removed from the world. It can also be called 115 | // when a 'position' or 'velocity' component is removed from an entity 116 | }); 117 | }); 118 | ``` 119 | 120 | ## Add Entities 121 | 122 | An entity is essentially a container of one or more components. Any property of the entity that does not begin with an underscore or dollar sign is considered a components. There are several ways to create an entity. The simplest method is to provide an array of components names: 123 | 124 | ```js 125 | ngEcs.$e(['position','velocity']); 126 | ``` 127 | 128 | Each value in the array that meets the component naming rules be use to create a new component object using the registered component constructors. 129 | 130 | Another way is to provide a entity template: 131 | 132 | ```js 133 | ngEcs.$e({ 134 | position: {x:0, y:0}, 135 | velocity: {x:0, y:0} 136 | }); 137 | ``` 138 | 139 | This method is great for when the entities state is pulled from browser offline storage or a database. Each property in the template that meets the component naming rules will be use to create a new component object using the registered component constructors. You can also add one at a time using ether a template or the actual constructor. 140 | 141 | ```js 142 | var ball = ngEcs.$e(); 143 | ball.$add('position', new Vector()); // equivalent to ball.$add('position'); 144 | ball.$add('velocity', new Vector()); 145 | ``` 146 | 147 | ## ngEcs 148 | 149 | The engine (`ngEcs`) is the container of all the entities and systems. Calling the `update` method will sequentially update all the systems, in the order they were added. 150 | 151 | ```js 152 | ngEcs.$update(/* interval */); 153 | ``` 154 | 155 | You may also do the same for the `$render` functions: 156 | 157 | ```js 158 | ngEcs.$render(); 159 | ``` 160 | 161 | Or start and stop the engine: 162 | 163 | ```js 164 | ngEcs.$start(); 165 | 166 | //// 167 | 168 | ngEcs.$stop(); 169 | ``` 170 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | // plugins 6 | var connect = require('gulp-connect'), 7 | jshint = require('gulp-jshint'), 8 | uglify = require('gulp-uglify'), 9 | clean = require('gulp-clean'), 10 | rename = require('gulp-rename'), 11 | concat = require('gulp-concat'), 12 | ngAnnotate = require('gulp-ng-annotate'), 13 | header = require('gulp-header'), 14 | karma = require('karma').server, 15 | bump = require('gulp-bump'), 16 | gulpDocs = require('gulp-ngdocs'), 17 | sourcemaps = require('gulp-sourcemaps'), 18 | git = require('gulp-git'), 19 | fs = require('fs'), 20 | runSequence = require('run-sequence'), 21 | babel = require('gulp-babel'), 22 | ghPages = require('gulp-gh-pages'); 23 | 24 | // options 25 | var pkg = require('./package.json'); 26 | var banner = ['/**', 27 | ' * <%= pkg.name %> - <%= pkg.description %>', 28 | ' * @version v<%= pkg.version %>', 29 | ' * @link <%= pkg.homepage %>', 30 | ' * @author <%= pkg.author.name %> <<%= pkg.author.email %>>', 31 | ' * @license <%= pkg.license %>', 32 | ' */', 33 | ''].join('\n'); 34 | 35 | var sourceFiles = ['src/'+pkg.name+'.js', 'src/**/*.js']; 36 | var docFiles = ['docs-content/*.ngdoc', 'src/'+pkg.name+'.js', 'src/**/*.js']; 37 | var distFile = './dist/'+pkg.name+'.js'; 38 | var minFile = './dist/'+pkg.name+'.min.js'; 39 | 40 | gulp.task('connect', ['ngdocs'], function () { 41 | connect.server({ 42 | root: 'docs/', 43 | port: 8888, 44 | livereload: true 45 | }); 46 | }); 47 | 48 | gulp.task('watch', function () { 49 | gulp.watch(docFiles, ['ngdocs']); 50 | }); 51 | 52 | gulp.task('clean', function() { 53 | gulp.src('./dist/*') 54 | .pipe(clean({force: true})); 55 | }); 56 | 57 | gulp.task('lint', function() { 58 | return gulp.src(sourceFiles) 59 | .pipe(jshint('.jshintrc')) 60 | .pipe(jshint.reporter('default')); 61 | }); 62 | 63 | gulp.task('scripts-build', ['clean'], function() { 64 | return gulp.src(sourceFiles) 65 | .pipe(sourcemaps.init()) 66 | .pipe(ngAnnotate({ 67 | sourceMap: true, 68 | gulpWarnings: false 69 | })) 70 | .pipe(concat(pkg.name+'.js')) 71 | .pipe(babel()) 72 | .pipe(header(banner, { pkg : pkg } )) 73 | .pipe(sourcemaps.write('.')) 74 | .pipe(gulp.dest('dist/')); 75 | }); 76 | 77 | gulp.task('scripts-min', ['scripts-build'], function() { 78 | return gulp.src(distFile) 79 | .pipe(rename({suffix: '.min'})) 80 | .pipe(uglify().on('error', function(e) { console.log('\x07',e.message); return this.end(); })) 81 | .pipe(header(banner, { pkg : pkg } )) 82 | .pipe(gulp.dest('./dist/')); 83 | }); 84 | 85 | gulp.task('test', function (done) { 86 | karma.start({ 87 | configFile: __dirname + '/karma.conf.js', 88 | singleRun: true 89 | }, function(){ 90 | done(); 91 | }); 92 | }); 93 | 94 | gulp.task('test-dist', ['scripts-build'], function (done) { 95 | karma.start({ 96 | configFile: __dirname + '/karma.conf.js', 97 | singleRun: true, 98 | files: [ 99 | 'bower_components/angular/angular.js', 100 | 'bower_components/jquery/dist/jquery.js', 101 | 'bower_components/angular-mocks/angular-mocks.js', 102 | 'bower_components/js-signals/dist/signals.js', 103 | distFile, 104 | 'test/spec/*.js' 105 | ], 106 | }, function(){ 107 | done(); 108 | }); 109 | }); 110 | 111 | gulp.task('test-min', ['scripts-min'], function (done) { 112 | karma.start({ 113 | configFile: __dirname + '/karma.conf.js', 114 | singleRun: true, 115 | files: [ 116 | 'bower_components/angular/angular.js', 117 | 'bower_components/jquery/dist/jquery.js', 118 | 'bower_components/angular-mocks/angular-mocks.js', 119 | 'bower_components/js-signals/dist/signals.js', 120 | minFile, 121 | 'test/spec/*.js' 122 | ], 123 | }, function(){ 124 | done(); 125 | }); 126 | }); 127 | 128 | gulp.task('bump-version', function () { 129 | gulp.src('*.json') 130 | .pipe(bump()) 131 | .pipe(gulp.dest('./')); 132 | }); 133 | 134 | gulp.task('commit-changes', function () { 135 | return gulp.src('.') 136 | .pipe(git.commit('Version number', {quiet: false, args: '-a', disableAppendPaths: true})); 137 | }); 138 | 139 | gulp.task('push-changes', function (cb) { 140 | git.push('origin', 'master', cb); 141 | }); 142 | 143 | gulp.task('create-new-tag', function (cb) { 144 | function getPackageJsonVersion () { 145 | return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version; 146 | } 147 | 148 | var version = getPackageJsonVersion(); 149 | git.tag(version, 'Created Tag for version: ' + version, function (error) { 150 | if (error) { 151 | return cb(error); 152 | } 153 | git.push('origin', 'master', {args: '--tags'}, cb); 154 | }); 155 | 156 | }); 157 | 158 | gulp.task('release', ['build'], function (callback) { 159 | runSequence( 160 | 'bump-version', 161 | 'commit-changes', 162 | 'push-changes', 163 | 'create-new-tag', 164 | function (error) { 165 | if (error) { 166 | console.log(error.message); 167 | } else { 168 | console.log('RELEASE FINISHED SUCCESSFULLY'); 169 | } 170 | callback(error); 171 | }); 172 | }); 173 | 174 | gulp.task('ngdocs', [], function () { 175 | return gulp.src(docFiles) 176 | .pipe(gulpDocs.process({ 177 | html5Mode: false 178 | })) 179 | .pipe(gulp.dest('./docs')) 180 | .pipe(connect.reload()); 181 | }); 182 | 183 | gulp.task('deploy', ['ngdocs'], function() { 184 | return gulp.src('./docs/**/*') 185 | .pipe(ghPages()); 186 | }); 187 | 188 | gulp.task('build', ['clean', 'scripts-build', 'scripts-min']); 189 | 190 | gulp.task('default', ['lint', 'test', 'build']); 191 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | 3 | module.exports = function(config) { 4 | 'use strict'; 5 | 6 | config.set({ 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'bower_components/angular/angular.js', 15 | 'bower_components/jquery/dist/jquery.js', 16 | 'bower_components/angular-mocks/angular-mocks.js', 17 | 'bower_components/js-signals/dist/signals.js', 18 | 'src/angular-ecs.js', 19 | 'src/*.js', 20 | 'test/spec/*.js' 21 | ], 22 | 23 | // list of files to exclude 24 | exclude: [], 25 | 26 | // test results reporter to use 27 | // possible values: dots || progress || growl 28 | reporters: ['progress'], 29 | 30 | // web server port 31 | port: 8080, 32 | 33 | // cli runner port 34 | runnerPort: 9100, 35 | 36 | // enable / disable colors in the output (reporters and logs) 37 | colors: true, 38 | 39 | // level of logging 40 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 41 | //logLevel: LOG_INFO, 42 | 43 | // enable / disable watching file and executing tests whenever any file changes 44 | autoWatch: false, 45 | 46 | // Start these browsers, currently available: 47 | // - Chrome 48 | // - ChromeCanary 49 | // - Firefox 50 | // - Opera 51 | // - Safari (only Mac) 52 | // - PhantomJS 53 | // - IE (only Windows) 54 | browsers: ['PhantomJS'], 55 | 56 | // If browser does not capture in given timeout [ms], kill it 57 | captureTimeout: 5000, 58 | 59 | // Continuous Integration mode 60 | // if true, it capture browsers, run tests and exit 61 | singleRun: false, 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ecs", 3 | "version": "0.0.21", 4 | "description": "An ECS framework built for AngularJS", 5 | "keywords": [ 6 | "angular", 7 | "game", 8 | "ecs" 9 | ], 10 | "homepage": "https://github.com/Hypercubed/angular-ecs", 11 | "bugs": "https://github.com/Hypercubed/angular-ecs/issues", 12 | "author": { 13 | "name": "Jayson Harshbarger", 14 | "email": "", 15 | "url": "https://github.com/Hypercubed" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/Hypercubed/angular-ecs.git" 20 | }, 21 | "licenses": [ 22 | { 23 | "type": "MIT" 24 | } 25 | ], 26 | "devDependencies": { 27 | "karma": "~0.12", 28 | "karma-chrome-launcher": "~0.1.0", 29 | "karma-coffee-preprocessor": "~0.1.0", 30 | "karma-firefox-launcher": "~0.1.0", 31 | "karma-html2js-preprocessor": "~0.1.0", 32 | "karma-jasmine": "~0.1.3", 33 | "karma-phantomjs-launcher": "~0.1.0", 34 | "karma-requirejs": "~0.2.0", 35 | "karma-script-launcher": "~0.1.0", 36 | "matchdep": "~0.1.2", 37 | "gulp": "~3.8.11", 38 | "gulp-uglify": "~1.2.0", 39 | "gulp-connect": "~2.2.0", 40 | "gulp-jshint": "~1.10.0", 41 | "gulp-rename": "~1.2.2", 42 | "run-sequence": "~1.1.0", 43 | "gulp-header": "~1.2.2", 44 | "gulp-sourcemaps": "~1.5.2", 45 | "gulp-clean": "~0.3.1", 46 | "gulp-concat": "~2.5.2", 47 | "gulp-bump": "~0.3.0", 48 | "gulp-git": "~1.2.1", 49 | "gulp-ng-annotate": "~0.5.2", 50 | "gulp-babel": "~5.1.0", 51 | "gulp-ngdocs": "~0.2.10", 52 | "gulp-gh-pages": "~0.5.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/angular-ecs-Entity.js: -------------------------------------------------------------------------------- 1 | // Entity 2 | (function() { 3 | 4 | 'use strict'; 5 | 6 | angular.module('hc.ngEcs') 7 | 8 | /** 9 | * @ngdoc service 10 | * @name hc.ngEcs.Entity 11 | * @requires hc.ngEcs.$components 12 | * @description 13 | * {@link hc.ngEcs.Entity:entity Entity} factory.. 14 | * 15 | * */ 16 | 17 | .factory('Entity', function($components) { 18 | var _uuid = 0; 19 | function uuid() { 20 | var timestamp = new Date().getUTCMilliseconds(); 21 | return '' + _uuid++ + '_' + timestamp; 22 | } 23 | 24 | /** 25 | * @ngdoc object 26 | * @name hc.ngEcs.Entity:entity 27 | * @description 28 | * An Entity is bag of game properties (components). By convention properties that do not start with a $ or _ are considered compoenets. 29 | * */ 30 | function Entity(id) { 31 | if(false === (this instanceof Entity)) { 32 | return new Entity(id); 33 | } 34 | this._id = id || uuid(); 35 | 36 | this.$componentAdded = new signals.Signal(); 37 | this.$componentRemoved = new signals.Signal(); 38 | 39 | this.$$signals = {}; 40 | 41 | } 42 | 43 | /** 44 | * @ngdoc 45 | * @name hc.ngEcs.Entity:entity#$on 46 | * @methodOf hc.ngEcs.Entity:entity 47 | * 48 | * @description 49 | * Adds an event listener to the entity 50 | * 51 | * @example 52 | *
53 | entity.$on('upgrade', function() { }); 54 | *55 | * @param {string} name Event name to listen on. 56 | * @param {function(event, ...args)} listener Function to call when the event is emitted. 57 | * @returns {function()} Returns a deregistration function for this listener. 58 | */ 59 | Entity.prototype.$on = function(name, listener) { 60 | var sig = this.$$signals[name]; 61 | if (!sig) { 62 | this.$$signals[name] = sig = new signals.Signal(); 63 | } 64 | return sig.add(listener, this); 65 | }; 66 | 67 | /** 68 | * @ngdoc 69 | * @name hc.ngEcs.Entity:entity#$emit 70 | * @methodOf hc.ngEcs.Entity:entity 71 | * 72 | * @description 73 | * Dispatches an event `name` calling notifying 74 | * registered {@link hc.ngEcs.Entity#$on} listeners 75 | * 76 | * @example 77 | *
78 | entity.$emit('upgrade'); 79 | *80 | * @param {string} name Event name to emit. 81 | * @param {...*} args Optional one or more arguments which will be passed onto the event listeners. 82 | * @returns {Entity} The entity 83 | */ 84 | Entity.prototype.$emit = function(name) { 85 | var sig = this.$$signals[name]; 86 | if (!sig) {return;} // throw error? 87 | 88 | if (arguments.length > 1) { 89 | var args = Array.prototype.slice.call(arguments, 1); 90 | sig.dispatch.apply(sig, args); 91 | } else { 92 | sig.dispatch(); 93 | } 94 | 95 | return this; 96 | }; 97 | 98 | /** 99 | * @ngdoc 100 | * @name hc.ngEcs.Entity:entity#$add 101 | * @methodOf hc.ngEcs.Entity:entity 102 | * 103 | * @description 104 | * Adds a Component to the entity 105 | * 106 | * @example 107 | *
108 | entity.$add('position', { 109 | x: 1.0, 110 | y: 3.0 111 | }); 112 | *113 | * @param {string} key The name of the Component 114 | * @param {object} [instance] A component instance or a compoent configuration 115 | * @returns {Entity} The entity 116 | */ 117 | Entity.prototype.$add = function(key, instance) { 118 | 119 | if (!key) { 120 | throw new Error('Can\'t add component with undefined key.'); 121 | } 122 | 123 | // remove if exists 124 | if (this[key]) { 125 | this.$remove(key); 126 | } 127 | 128 | instance = angular.isDefined(instance) ? instance : {}; 129 | 130 | // not a component by convention 131 | if (key.charAt(0) === '$' || key.charAt(0) === '_') { 132 | this[key] = instance; 133 | return; // no emit 134 | } 135 | 136 | this[key] = createComponent(this, key, instance); 137 | 138 | this.$componentAdded.dispatch(this, key); 139 | return this; 140 | }; 141 | 142 | function createComponent(e, name, state) { 143 | 144 | // not a registered component 145 | if (!$components.hasOwnProperty(name)) { 146 | return state; 147 | } 148 | 149 | var Type = $components[name]; 150 | 151 | // not valid constructor 152 | if (!angular.isFunction(Type)) { 153 | throw new TypeError('Component constructor may only be an Object or function'); 154 | return; 155 | } 156 | 157 | // already an instance 158 | if (state instanceof Type) { 159 | return state; 160 | } 161 | 162 | // inject 163 | if (Type.$inject) { 164 | return instantiate(Type, e, state); 165 | } 166 | 167 | return angular.extend(new Type(e), state); 168 | 169 | } 170 | 171 | function instantiate(Type, e, state) { 172 | var $inject = Type.$inject; 173 | 174 | var length = $inject.length, args = new Array(length), i; 175 | 176 | for (i = 0; i < length; ++i) { 177 | args[i] = getValue(e, $inject[i], state); 178 | } 179 | 180 | var instance = Object.create(Type.prototype || null); 181 | Type.apply(instance, args); 182 | return instance; 183 | } 184 | 185 | function getValue(e, key, state) { 186 | if (key === '$parent') { return e; } 187 | if (key === '$state') { return state; } 188 | //if (key === '$world') { return ngEcs; } // todo 189 | return state[key]; 190 | } 191 | 192 | function isComponent(name) { 193 | return name.charAt(0) !== '$' && name.charAt(0) !== '_'; 194 | } 195 | 196 | /** 197 | * @ngdoc 198 | * @name hc.ngEcs.Entity:entity#$remove 199 | * @methodOf hc.ngEcs.Entity:entity 200 | * 201 | * @description 202 | * Removes a component from the entity 203 | * 204 | * @example 205 | *
206 | entity.$remove('position'); 207 | *208 | * @param {string} key The name of the Component 209 | * @returns {Entity} The entity 210 | */ 211 | Entity.prototype.$remove = function(key) { 212 | // not a component by convention 213 | if (isComponent(key)) { 214 | this.$componentRemoved.dispatch(this, key); 215 | } 216 | delete this[key]; 217 | return this; 218 | }; 219 | 220 | return Entity; 221 | }); 222 | 223 | })(); 224 | -------------------------------------------------------------------------------- /src/angular-ecs-Family.js: -------------------------------------------------------------------------------- 1 | // Entity 2 | (function() { 3 | 4 | 'use strict'; 5 | 6 | /** 7 | * @ngdoc object 8 | * @name hc.ngEcs.Family:family 9 | * @description 10 | * A Family is array of game entities matching a list of required components. 11 | * 12 | **/ 13 | 14 | function Family(require) { 15 | var _this = []; 16 | 17 | /** 18 | * @ngdoc 19 | * @name hc.ngEcs.Family:family#require 20 | * @propertyOf hc.ngEcs.Family:family 21 | * 22 | * @description 23 | * An array of component requirements of this family 24 | */ 25 | Object.defineProperty(_this, 'require', { 26 | enumerable: false, 27 | writable: false, 28 | value: require 29 | }); 30 | 31 | /** 32 | * @ngdoc 33 | * @name hc.ngEcs.Family#entityAdded 34 | * @propertyOf hc.ngEcs.Family:family 35 | * 36 | * @description 37 | * A signal dispatched when an entity is added 38 | */ 39 | Object.defineProperty(_this, 'entityAdded', { 40 | enumerable: false, 41 | value: new signals.Signal() 42 | }); 43 | 44 | /** 45 | * @ngdoc 46 | * @name hc.ngEcs.Family#entityRemoved 47 | * @propertyOf hc.ngEcs.Family:family 48 | * 49 | * @description 50 | * A signal dispatched when an entity is removed 51 | */ 52 | Object.defineProperty(_this, 'entityRemoved', { 53 | enumerable: false, 54 | value: new signals.Signal() 55 | }); 56 | 57 | for (var method in Family.prototype) { 58 | if (Family.prototype.hasOwnProperty(method)) { 59 | Object.defineProperty(_this, method, { 60 | enumerable: false, 61 | value: Family.prototype[method] 62 | }); 63 | } 64 | } 65 | 66 | return _this; 67 | } 68 | 69 | /** 70 | * @ngdoc 71 | * @name hc.ngEcs.Family#isMatch 72 | * @methodOf hc.ngEcs.Family:family 73 | * @param {object} entity the entity to test match. 74 | * @returns {boolean} True if the entity matches this family 75 | * 76 | * @description 77 | * Tests if the entity matches the family requirements 78 | */ 79 | Family.prototype.isMatch = function(entity) { 80 | if (!this.require) { return true; } 81 | 82 | return this.require.every(function(d) { 83 | return entity.hasOwnProperty(d); 84 | }); 85 | }; 86 | 87 | /** 88 | * @ngdoc 89 | * @name hc.ngEcs.Family#add 90 | * @methodOf hc.ngEcs.Family:family 91 | * @param {object} entity the entity to add. 92 | * 93 | * @description 94 | * Adds an entity to this family 95 | */ 96 | Family.prototype.add = function(e) { 97 | // check if match? 98 | var index = this.indexOf(e); 99 | if (index < 0) { 100 | this.push(e); 101 | this.entityAdded.dispatch(e); 102 | } 103 | }; 104 | 105 | /** 106 | * @ngdoc 107 | * @name hc.ngEcs.Family#addIfMatch 108 | * @methodOf hc.ngEcs.Family:family 109 | * @param {object} entity the entity to add if it matches the family requirements 110 | * 111 | * @description 112 | * Adds an entity to this family if entity matches requirements 113 | */ 114 | Family.prototype.addIfMatch = function(e) { 115 | if (this.isMatch(e)) { 116 | this.add(e); 117 | } 118 | }; 119 | 120 | /** 121 | * @ngdoc 122 | * @name hc.ngEcs.Family#remove 123 | * @methodOf hc.ngEcs.Family:family 124 | * @param {object} entity the entity to remove 125 | * 126 | * @description 127 | * Removes an entity from this family 128 | */ 129 | Family.prototype.remove = function(e) { 130 | var index = this.indexOf(e); 131 | if (index > -1) { 132 | this.splice(index,1); 133 | this.entityRemoved.dispatch(e); 134 | } 135 | }; 136 | 137 | /** 138 | * @ngdoc 139 | * @name hc.ngEcs.Family#removeIfMatch 140 | * @methodOf hc.ngEcs.Family:family 141 | * @param {object} entity the entity to remove if it matches the family requirements 142 | * 143 | * @description 144 | * Removes an entity from this family if entity matches requirements 145 | */ 146 | Family.prototype.removeIfMatch = function(e) { 147 | if (this.isMatch(e)) { 148 | this.remove(e); 149 | } 150 | }; 151 | 152 | Family.makeId = function(require) { 153 | if (!require) { return '::'; } 154 | if (typeof require === 'string') { return require; } 155 | return require.sort().join('::'); 156 | }; 157 | 158 | angular.module('hc.ngEcs') 159 | 160 | /** 161 | * @ngdoc object 162 | * @name hc.ngEcs.Family 163 | * @description 164 | * {@link hc.ngEcs.Family:family Family} factory. 165 | * 166 | * */ 167 | .constant('Family', Family); 168 | 169 | })(); 170 | -------------------------------------------------------------------------------- /src/angular-ecs-engine.js: -------------------------------------------------------------------------------- 1 | /* global signals */ 2 | 3 | // engine 4 | (function() { 5 | 6 | 'use strict'; 7 | 8 | angular.module('hc.ngEcs') 9 | 10 | /** 11 | * @ngdoc service 12 | * @name hc.ngEcs.ngEcs 13 | * @requires hc.ngEcs.$components 14 | * @requires hc.ngEcs.$systems 15 | * @requires hc.ngEcs.$entities 16 | * @requires hc.ngEcs.$families 17 | * @requires hc.ngEcs.Entity 18 | * @requires hc.ngEcs.Family 19 | * @description 20 | * ECS engine. Contain System, Components, and Entities. 21 | * */ 22 | .service('ngEcs', function($rootScope, $log, $timeout, $components, $systems, $entities, $families, Entity, Family) { 23 | 24 | var _uuid = 0; 25 | function uuid() { 26 | var timestamp = new Date().getUTCMilliseconds(); 27 | return '' + _uuid++ + '_' + timestamp; 28 | } 29 | 30 | function Ecs(opts) { 31 | this.components = $components; 32 | this.systems = $systems; 33 | this.entities = $entities; 34 | this.families = $families; 35 | 36 | angular.forEach($systems, function(value, key) { // todo: test this 37 | this.$s(key, value); 38 | }); 39 | 40 | angular.forEach($entities, function(value) { // todo: test this 41 | this.$e(value); 42 | }); 43 | 44 | //this.$timer = null; 45 | this.$playing = false; 46 | //this.$delay = 1000; 47 | this.$requestId = null; 48 | this.$fps = 60; 49 | this.$interval = 1; 50 | //this.$systemsQueue = []; // make $scenes? Signal? 51 | 52 | this.started = new signals.Signal(); 53 | this.stopped = new signals.Signal(); 54 | 55 | this.updated = new signals.Signal(); 56 | this.rendered = new signals.Signal(); 57 | 58 | this.rendered.add(function() { $rootScope.$applyAsync(); }, null, -1); 59 | 60 | angular.extend(this, opts); 61 | } 62 | 63 | Ecs.prototype.constructor = Ecs; 64 | 65 | /** 66 | * @ngdoc service 67 | * @name hc.ngEcs.ngEcs#$c 68 | * @methodOf hc.ngEcs.ngEcs 69 | * 70 | * @description Adds a component contructor 71 | * 72 | * @param {string} key component key 73 | * @param {function|object|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation) or constructor prototype object 74 | */ 75 | Ecs.prototype.$c = function(key, constructor) { // perhaps add to $components 76 | if (typeof key !== 'string') { 77 | throw new TypeError('A components name is required'); 78 | } 79 | return $components[key] = makeConstructor(key, constructor); 80 | }; 81 | 82 | function makeConstructor(name, O) { 83 | 84 | if (angular.isArray(O)) { 85 | var T = O.pop(); 86 | T.$inject = O; 87 | O = T; 88 | }; 89 | 90 | if (angular.isFunction(O)) { return O; } 91 | 92 | if (typeof O !== 'object') { 93 | throw new TypeError('Component constructor may only be an Object or function'); 94 | } 95 | 96 | var Constructor = new Function( 97 | 'return function ' + name + '( instance ){ angular.extend(this, instance); }' 98 | )(); 99 | 100 | Constructor.prototype = O; 101 | Constructor.prototype.constructor = Constructor; 102 | Constructor.$inject = ['$state']; 103 | 104 | return Constructor; 105 | } 106 | 107 | /** 108 | * @ngdoc service 109 | * @name hc.ngEcs.ngEcs#$f 110 | * @methodOf hc.ngEcs.ngEcs 111 | * 112 | * @description Gets a family 113 | * 114 | * @param {string} require Array of component keys 115 | */ 116 | Ecs.prototype.$f = function(require) { // perhaps add to $components 117 | var id = Family.makeId(require); 118 | var fam = $families[id]; 119 | if (fam) { return fam; } 120 | fam = $families[id] = new Family(require); 121 | onFamilyAdded(fam); 122 | 123 | return fam; 124 | }; 125 | 126 | var isDefined = angular.isDefined; 127 | 128 | /** 129 | * @ngdoc service 130 | * @name hc.ngEcs.ngEcs#$s 131 | * @methodOf hc.ngEcs.ngEcs 132 | * 133 | * @description Adds a system 134 | * 135 | * @param {string} key system key 136 | * @param {object} instance system configuration 137 | */ 138 | Ecs.prototype.$s = function(key, system) { // perhaps add to $systems 139 | 140 | if (typeof key === 'object') { 141 | system = key; 142 | key = uuid(); 143 | } 144 | 145 | $systems[key] = system; // todo: make a system class? Error if already existing. 146 | 147 | var $priority = system.$priority || 0; 148 | 149 | system.$family = this.$f(system.$require); // todo: later only store id? 150 | 151 | if (system.$addEntity) { 152 | system.$family.entityAdded.add(system.$addEntity, system, $priority); 153 | } 154 | 155 | if (system.$removeEntity) { 156 | system.$family.entityRemoved.add(system.$removeEntity, system, $priority); 157 | } 158 | 159 | this.$$addSystem($systems[key]); 160 | 161 | return system; 162 | 163 | }; 164 | 165 | Ecs.prototype.$$addSystem = function(system) { 166 | 167 | var $priority = system.$priority || 0; 168 | 169 | if (isDefined(system.$update)) { 170 | 171 | if (isDefined(system.interval)) { // add tests for interval 172 | system.acc = isDefined(system.acc) ? system.acc : 0; 173 | system.$$update = function(dt) { 174 | this.acc += dt; 175 | if (this.acc > this.interval) { 176 | if (system.$family.length > 0) { this.$update(this.interval); } 177 | this.acc = this.acc - this.interval; 178 | } 179 | }; 180 | } else { 181 | system.$$update = function(dt) { // can be system prototype 182 | if (system.$family.length > 0) { this.$update(dt); } 183 | }; 184 | } 185 | 186 | this.updated.add(system.$$update, system, $priority); 187 | } 188 | 189 | if (isDefined(system.$updateEach)) { 190 | system.$$updateEach = function(time) { // can be system prototype, bug: updateEach doesn't respect interval 191 | var arr = this.$family,i = arr.length; 192 | while (i--) { 193 | if (i in arr) { 194 | this.$updateEach(arr[i], time); 195 | } 196 | } 197 | }; 198 | this.updated.add(system.$$updateEach, system, $priority); 199 | } 200 | 201 | if (isDefined(system.$render)) { 202 | this.rendered.add(system.$render, system, $priority); 203 | } 204 | 205 | if (isDefined(system.$renderEach)) { 206 | system.$$renderEach = function() { 207 | var arr = this.$family,i = arr.length; 208 | while (i--) { 209 | if (i in arr) { 210 | this.$renderEach(arr[i]); 211 | } 212 | } 213 | }; 214 | this.rendered.add(system.$$renderEach, system); 215 | } 216 | 217 | if (isDefined(system.$started)) { 218 | this.started.add(system.$started, system, $priority); 219 | } 220 | 221 | if (isDefined(system.$stopped)) { 222 | this.stopped.add(system.$stopped, system, $priority); 223 | } 224 | 225 | if (isDefined(system.$added)) { 226 | system.$added(); 227 | } 228 | 229 | return this; 230 | }; 231 | 232 | Ecs.prototype.$$removeSystem = function(system) { // perhaps add to $systems 233 | 234 | if (typeof system === 'string') { 235 | system = $systems[key]; 236 | } 237 | 238 | if (isDefined(system.$$update)) { 239 | this.updated.remove(system.$$update, system); 240 | } 241 | 242 | if (isDefined(system.$$updateEach)) { 243 | this.updated.remove(system.$$updateEach, system); 244 | } 245 | 246 | if (isDefined(system.$render)) { 247 | this.rendered.remove(system.$render, system); 248 | } 249 | 250 | if (isDefined(system.$$renderEach)) { 251 | this.rendered.remove(system.$$renderEach, system); 252 | } 253 | 254 | if (isDefined(system.$started)) { 255 | this.started.remove(system.$started, system); 256 | } 257 | 258 | if (isDefined(system.$stopped)) { 259 | this.stopped.remove(system.$stopped, system); 260 | } 261 | 262 | if (isDefined(system.$removed)) { 263 | system.$removed(); 264 | } 265 | 266 | return this; 267 | 268 | }; 269 | 270 | /** 271 | * @ngdoc service 272 | * @name hc.ngEcs.ngEcs#$e 273 | * @methodOf hc.ngEcs.ngEcs 274 | * 275 | * @description Creates and adds an Entity 276 | * @see Entity 277 | * 278 | * @example 279 | *
280 | //config as array 281 | ngEcs.$e('player', ['position','control','collision']); 282 | 283 | //or config as object 284 | ngEcs.$e('player', { 285 | position: { x: 0, y: 50 }, 286 | control: {} 287 | collision: {} 288 | }); 289 | *290 | * 291 | * @param {string} id (optional) entity id 292 | * @param {object|array} instance (optional) config object of entity 293 | * @return {Entity} The Entity 294 | */ 295 | Ecs.prototype.$e = function(id, instance) { 296 | //var self = this; 297 | 298 | if (typeof id === 'object') { 299 | instance = id; 300 | id = null; 301 | } 302 | 303 | var e = new Entity(id); 304 | e.$world = this; // get rid of this 305 | 306 | if (Array.isArray(instance)) { 307 | instance.forEach(function(key) { 308 | e.$add(key); 309 | }); 310 | } else { 311 | angular.forEach(instance, function(value, key) { 312 | e.$add(key, value); 313 | }); 314 | } 315 | 316 | onComponentAdded(e); 317 | 318 | e.$componentAdded.add(onComponentAdded, this); 319 | e.$componentRemoved.add(onComponentRemoved, this); 320 | 321 | $entities[e._id] = e; 322 | 323 | return e; 324 | }; 325 | 326 | Ecs.prototype.$$removeEntity = function(e) { 327 | 328 | e.$world = null; 329 | 330 | angular.forEach(e, function(value, key) { 331 | if (key.charAt(0) !== '$' && key.charAt(0) !== '_') { 332 | e.$remove(key); 333 | } 334 | }); 335 | 336 | angular.forEach($families, function(family) { 337 | family.remove(e); 338 | }); 339 | 340 | e.$componentAdded.dispose(); 341 | e.$componentRemoved.dispose(); 342 | 343 | delete this.entities[e._id]; 344 | 345 | return this; 346 | 347 | }; 348 | 349 | function onFamilyAdded(family) { 350 | angular.forEach($entities, function(e) { 351 | family.addIfMatch(e); 352 | }); 353 | } 354 | 355 | function onComponentAdded(entity, key) { 356 | angular.forEach($families, function(family) { 357 | if (family.require && key && family.require.indexOf(key) < 0) { return; } 358 | family.addIfMatch(entity); 359 | }); 360 | } 361 | 362 | function onComponentRemoved(entity, key) { 363 | angular.forEach($families, function(family) { 364 | if (!family.require || (key && family.require.indexOf(key) < 0)) { return; } 365 | family.removeIfMatch(entity); 366 | }); 367 | } 368 | 369 | /** 370 | * @ngdoc service 371 | * @name hc.ngEcs.ngEcs#$update 372 | * @methodOf hc.ngEcs.ngEcs 373 | * 374 | * @description Calls the update cycle 375 | */ 376 | Ecs.prototype.$update = function(time) { 377 | this.updated.dispatch(time || this.$interval); 378 | }; 379 | 380 | Ecs.prototype.$render = function(time) { 381 | this.rendered.dispatch(time || this.$interval); 382 | }; 383 | 384 | Ecs.prototype.$runLoop = function() { 385 | 386 | window.cancelAnimationFrame(this.$requestId); 387 | 388 | var self = this, 389 | now, 390 | last = window.performance.now(), 391 | dt = 0, 392 | DT = 0, 393 | step; 394 | 395 | function frame() { 396 | if (!self.$playing || self.$paused) { return; } 397 | now = window.performance.now(); 398 | DT = Math.min(1, (now - last) / 1000); 399 | dt = dt + DT; 400 | step = 1/self.$fps; 401 | while(dt > step) { 402 | dt = dt - step; 403 | self.$update(step); 404 | } 405 | self.$render(DT); 406 | 407 | last = now; 408 | self.$requestId = window.requestAnimationFrame(frame); 409 | } 410 | 411 | self.$requestId = window.requestAnimationFrame(frame); 412 | }; 413 | 414 | /** 415 | * @ngdoc service 416 | * @name hc.ngEcs.ngEcs#$start 417 | * @methodOf hc.ngEcs.ngEcs 418 | * 419 | * @description Starts the game loop 420 | */ 421 | Ecs.prototype.$start = function() { 422 | if (this.$playing) { return; } 423 | this.$playing = true; 424 | 425 | this.started.dispatch(); 426 | this.$runLoop(); 427 | }; 428 | 429 | /** 430 | * @ngdoc service 431 | * @name hc.ngEcs.ngEcs#$stop 432 | * @methodOf hc.ngEcs.ngEcs 433 | * 434 | * @description Stops the game loop 435 | */ 436 | Ecs.prototype.$stop = function() { 437 | this.$playing = false; 438 | window.cancelAnimationFrame(this.$requestId); 439 | this.stopped.dispatch(); 440 | }; 441 | 442 | Ecs.prototype.$pause = function() { 443 | if (!this.$playing) { return; } 444 | this.$paused = true; 445 | }; 446 | 447 | Ecs.prototype.$unpause = function() { 448 | if (!this.$playing || !this.$paused) { return; } 449 | this.$paused = false; 450 | this.$runLoop(); 451 | }; 452 | 453 | var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/; 454 | function isTypedArray(value) { 455 | return TYPED_ARRAY_REGEXP.test(Object.prototype.toString.call(value)); 456 | } 457 | 458 | // deep copy objects removing $ props 459 | // must start with object, 460 | // skips keys that start with $ 461 | // navigates down objects but not other times (including arrays) 462 | Ecs.prototype.$copyState = function ssCopy(src) { 463 | var dst = {}; 464 | for (var key in src) { 465 | if (src.hasOwnProperty(key) && key.charAt(0) !== '$') { 466 | var s = src[key]; 467 | if (angular.isObject(s) && !isTypedArray(s) && !angular.isArray(s) && !angular.isDate(s)) { 468 | dst[key] = ssCopy(s); 469 | } else if (typeof s !== 'function') { 470 | dst[key] = s; 471 | } 472 | } 473 | } 474 | return dst; 475 | } 476 | 477 | return new Ecs(); 478 | 479 | }); 480 | 481 | })(); 482 | -------------------------------------------------------------------------------- /src/angular-ecs.js: -------------------------------------------------------------------------------- 1 | /* global angular:true */ 2 | 3 | // main 4 | (function() { 5 | 6 | 'use strict'; 7 | 8 | /** 9 | * ngdoc overview 10 | * name index 11 | * 12 | * description 13 | * # An entity-component-system game framework made specifically for AngularJS. 14 | * 15 | * ## Why? 16 | * 17 | * There are many great game engines available for JavaScript. Many include all the pieces needed to develop games in JavaScript; a canvas based rendering engine, optimized and specialized game loop, pixel asset management, dependency injection, and so on. However, when developing a web game using AngularJS you may want to use only some parts of the game engine and leave other parts to Angular. To do this it often means playing tricks on the game engine to cooperate with angularjs. Angular-ecs is a entity-component-system built for and with AngularJS. Angular-ecs was built to play nice with the angular architecture and to feel, as much as possible, like a native part of the angular framework. 18 | * 19 | * 20 | */ 21 | 22 | function MapProvider() { 23 | 24 | var map = {}; 25 | 26 | this.register = function(name, constructor) { 27 | if (angular.isObject(name)) { 28 | angular.extend(map, name); 29 | } else { 30 | map[name] = constructor; 31 | } 32 | return this; 33 | }; 34 | 35 | this.$get = ['$injector', function($injector) { 36 | angular.forEach(map, function(value, key) { 37 | if (angular.isFunction(value)) { 38 | map[key] = $injector.invoke(value, null, null, key); 39 | } 40 | }); 41 | return map; 42 | }]; 43 | 44 | } 45 | 46 | angular.module('hc.ngEcs',[]) 47 | 48 | /** 49 | * @ngdoc service 50 | * @name hc.ngEcs.$entities 51 | * @description 52 | * Index of {@link hc.ngEcs.Entity:entity entities}. 53 | **/ 54 | .provider('$entities', MapProvider) 55 | 56 | /** 57 | * @ngdoc service 58 | * @name hc.ngEcs.$componentsProvider 59 | * @description 60 | * This provider allows component registration via the register method. 61 | * 62 | **/ 63 | 64 | /** 65 | * @ngdoc 66 | * @name hc.ngEcs.$componentsProvider#$register 67 | * @methodOf hc.ngEcs.$componentsProvider 68 | * 69 | * @description 70 | * Registers a componnet during configuration phase 71 | * 72 | * @param {string|object} name Component name, or an object map of components where the keys are the names and the values are the constructors. 73 | * @param {function()|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation). 74 | */ 75 | 76 | /** 77 | * @ngdoc service 78 | * @name hc.ngEcs.$components 79 | * @description 80 | * Index of components, components are object constructors 81 | * */ 82 | .provider('$components', MapProvider) 83 | 84 | /** 85 | * @ngdoc service 86 | * @name hc.ngEcs.$systemsProvider 87 | * @description 88 | * This provider allows component registration via the register method. 89 | * 90 | **/ 91 | 92 | /** 93 | * @ngdoc 94 | * @name hc.ngEcs.$systemsProvider#$register 95 | * @methodOf hc.ngEcs.$systemsProvider 96 | * 97 | * @description 98 | * Registers a componnet during configuration phase 99 | * 100 | * @param {string|object} name System name, or an object map of systems where the keys are the names and the values are the constructors. 101 | * @param {function()|array} constructor Component constructor fn (optionally decorated with DI annotations in the array notation). 102 | */ 103 | 104 | /** 105 | * @ngdoc service 106 | * @name hc.ngEcs.$systems 107 | * @description 108 | * Index of systems, systems are generic objects 109 | * */ 110 | .provider('$systems', MapProvider) 111 | 112 | /** 113 | * @ngdoc service 114 | * @name hc.ngEcs.$families 115 | * @description 116 | * Index of {@link hc.ngEcs.Family:family families}, a family is an array of game entities matching a list of required components. 117 | * */ 118 | .provider('$families', MapProvider); 119 | 120 | })(); 121 | -------------------------------------------------------------------------------- /src/shims.js: -------------------------------------------------------------------------------- 1 | // shims 2 | (function () { 3 | 'use strict'; 4 | 5 | window.performance = (window.performance || {}); 6 | 7 | window.performance.now = (function () { 8 | return ( 9 | window.performance.now || 10 | window.performance.webkitNow || 11 | window.performance.msNow || 12 | window.performance.mozNow || 13 | Date.now || 14 | function () { 15 | return new Date().getTime(); 16 | }); 17 | })(); 18 | 19 | window.requestAnimationFrame = (function () { 20 | return ( 21 | window.requestAnimationFrame || 22 | window.webkitRequestAnimationFrame || 23 | window.msRequestAnimationFrame || 24 | window.mozRequestAnimationFrame || 25 | function (callback) { 26 | return setTimeout(function () { 27 | var time = window.performance.now(); 28 | callback(time); 29 | }, 16); 30 | }); 31 | })(); 32 | 33 | window.cancelAnimationFrame = (function () { 34 | return ( 35 | window.cancelAnimationFrame || 36 | window.webkitCancelAnimationFrame || 37 | window.msCancelAnimationFrame || 38 | window.mozCancelAnimationFrame || 39 | function(id) { 40 | clearTimeout(id); 41 | }); 42 | })(); 43 | 44 | if (!Function.prototype.bind) { 45 | Function.prototype.bind = function(oThis) { 46 | if (typeof this !== 'function') { 47 | // closest thing possible to the ECMAScript 5 48 | // internal IsCallable function 49 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 50 | } 51 | 52 | var aArgs = Array.prototype.slice.call(arguments, 1), 53 | fToBind = this, 54 | FNOP = function() {}, 55 | fBound = function() { 56 | return fToBind.apply(this instanceof FNOP ? this 57 | : oThis, 58 | aArgs.concat(Array.prototype.slice.call(arguments))); 59 | }; 60 | 61 | FNOP.prototype = this.prototype; 62 | fBound.prototype = new FNOP(); 63 | 64 | return fBound; 65 | }; 66 | } 67 | 68 | })(); 69 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "predef": [ 23 | "$", 24 | "angular", 25 | "describe", 26 | "beforeEach", 27 | "afterEach", 28 | "inject", 29 | "it", 30 | "expect", 31 | "jasmine", 32 | "spyOn" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/spec/angular-ecs-components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('components', function () { 4 | 5 | var ngEcs, $components; 6 | 7 | beforeEach(module('hc.ngEcs', function() { 8 | 9 | })); 10 | 11 | beforeEach(inject(function(_ngEcs_, _$components_){ 12 | ngEcs = _ngEcs_; 13 | $components = _$components_; 14 | })); 15 | 16 | it('should create components', function () { 17 | var c = ngEcs.$c('test', {}); 18 | 19 | expect($components.test).toBeDefined(); 20 | expect($components.test).toBe(c); 21 | expect($components).toBe(ngEcs.components); 22 | }); 23 | 24 | it('should convert objects to constructor', function () { 25 | var p = {}; 26 | var c = ngEcs.$c('test', p); 27 | 28 | expect(typeof c).toBe('function'); 29 | expect(c.prototype).toBe(p); 30 | expect(c.$inject).toEqual(['$state']); 31 | }); 32 | 33 | it('should create components using prototype', function () { 34 | var c = ngEcs.$c('testComponent', { 35 | testing: 123, 36 | fn: jasmine.createSpy('callback') 37 | }); 38 | var e = ngEcs.$e(['testComponent']); 39 | 40 | e.testComponent.fn(); 41 | expect(e.testComponent.fn.calls.length).toBe(1); 42 | expect(typeof e.testComponent.fn).toBe('function'); 43 | expect(e.testComponent.testing).toBe(123); 44 | 45 | expect(e.testComponent instanceof c); 46 | }); 47 | 48 | it('should create components using constructor', function () { 49 | var MockComponent = jasmine.createSpy('callback'); 50 | 51 | var c = ngEcs.$c('testComponent', MockComponent); 52 | var e = ngEcs.$e(['testComponent']); 53 | 54 | expect(MockComponent.calls.length).toBe(1); 55 | expect(e.testComponent instanceof MockComponent); 56 | expect(e.testComponent.prototype === MockComponent.prototype); 57 | expect(e.testComponent instanceof c); 58 | }); 59 | 60 | it('should not call constructor if already an instance', function () { 61 | var MockComponent = jasmine.createSpy('callback'); 62 | 63 | ngEcs.$c('testComponent', MockComponent); 64 | var e = ngEcs.$e({}); 65 | e.$add('testComponent', new MockComponent()); 66 | 67 | expect(MockComponent.calls.length).toBe(1); 68 | expect(e.testComponent instanceof MockComponent); 69 | expect(e.testComponent.prototype === MockComponent.prototype); 70 | }); 71 | 72 | it('should create components with state', function () { 73 | var MockComponent = jasmine.createSpy('callback'); 74 | 75 | ngEcs.$c('testComponent', MockComponent); 76 | 77 | var e = ngEcs.$e({ 78 | testComponent: { 79 | x: 1, 80 | y: 2, 81 | z: 3 82 | } 83 | }); 84 | 85 | expect(MockComponent.calls.length).toBe(1); 86 | expect(e.testComponent instanceof MockComponent); 87 | expect(e.testComponent.prototype === MockComponent.prototype); 88 | expect(e.testComponent.x).toBe(1); 89 | expect(e.testComponent.y).toBe(2); 90 | expect(e.testComponent.z).toBe(3); 91 | }); 92 | 93 | it('should create injectable components', function () { 94 | var MockComponent = function(x,y) { 95 | this.x = x || 0; 96 | this.y = y || 0; 97 | this.z = null; 98 | }; 99 | MockComponent.$inject = ['x','y']; 100 | 101 | ngEcs.$c('testComponent', MockComponent); 102 | 103 | var e = ngEcs.$e({ 104 | testComponent: { 105 | x: 1, 106 | y: 2, 107 | z: 3 108 | } 109 | }); 110 | 111 | expect(e.testComponent instanceof MockComponent); 112 | expect(e.testComponent.x).toBe(1); 113 | expect(e.testComponent.y).toBe(2); 114 | expect(e.testComponent.z).toBe(null); 115 | }); 116 | 117 | it('should create injectable components with inline notation', function () { 118 | var MockComponent = function(x,y) { 119 | this.x = x || 0; 120 | this.y = y || 0; 121 | this.z = null; 122 | }; 123 | 124 | ngEcs.$c('testComponent', ['x','y', MockComponent]); 125 | 126 | var e = ngEcs.$e({ 127 | testComponent: { 128 | x: 1, 129 | y: 2, 130 | z: 3 131 | } 132 | }); 133 | 134 | expect(e.testComponent instanceof MockComponent); 135 | expect(e.testComponent.x).toBe(1); 136 | expect(e.testComponent.y).toBe(2); 137 | expect(e.testComponent.z).toBe(null); 138 | }); 139 | 140 | it('should create injectable components with defaults', function () { 141 | var MockComponent = function(x,y) { 142 | this.x = x || 0; 143 | this.y = y || 0; 144 | this.z = null; 145 | }; 146 | MockComponent.$inject = ['x','y']; 147 | 148 | ngEcs.$c('testComponent', MockComponent); 149 | 150 | var e = ngEcs.$e(['testComponent']); 151 | 152 | expect(e.testComponent instanceof MockComponent); 153 | expect(e.testComponent.x).toBe(0); 154 | expect(e.testComponent.y).toBe(0); 155 | expect(e.testComponent.z).toBe(null); 156 | }); 157 | 158 | it('should create injectable parent', function () { 159 | var MockComponent = function(x,y,$parent) { 160 | this.x = x || 0; 161 | this.y = y || 0; 162 | this.z = null; 163 | this.$parent = $parent; 164 | }; 165 | MockComponent.$inject = ['x','y','$parent']; 166 | 167 | ngEcs.$c('testComponent', MockComponent); 168 | 169 | var e = ngEcs.$e({ 170 | testComponent: { 171 | x: 1, 172 | y: 2, 173 | z: 3 174 | } 175 | }); 176 | 177 | expect(e.testComponent instanceof MockComponent); 178 | expect(e.testComponent.x).toBe(1); 179 | expect(e.testComponent.y).toBe(2); 180 | expect(e.testComponent.z).toBe(null); 181 | expect(e.testComponent.$parent).toBe(e); 182 | }); 183 | 184 | }); 185 | -------------------------------------------------------------------------------- /test/spec/angular-ecs-engine.js: -------------------------------------------------------------------------------- 1 | /* global spyOn */ 2 | /* global describe */ 3 | /* global beforeEach */ 4 | /* global it */ 5 | /* global jasmine */ 6 | /* global expect */ 7 | /* global xit */ 8 | /* global waitsFor */ 9 | /* global runs */ 10 | 11 | 'use strict'; 12 | 13 | describe('engine', function () { 14 | 15 | var ngEcs, $systems; 16 | 17 | beforeEach(module('hc.ngEcs', function() { 18 | 19 | })); 20 | 21 | beforeEach(inject(function(_ngEcs_, _$systems_){ 22 | ngEcs = _ngEcs_; 23 | $systems = _$systems_; 24 | 25 | ngEcs.$s('test', { 26 | $update: jasmine.createSpy('$update'), 27 | $updateEach: jasmine.createSpy('$updateEach'), 28 | $render: jasmine.createSpy('$render'), 29 | $renderEach: jasmine.createSpy('$renderEach') 30 | }); 31 | 32 | ngEcs.$s('test2', { 33 | $require: ['test2'], 34 | $update: jasmine.createSpy('$update'), 35 | $updateEach: jasmine.createSpy('$updateEach'), 36 | $render: jasmine.createSpy('$render'), 37 | $renderEach: jasmine.createSpy('$renderEach') 38 | }); 39 | 40 | //jasmine.Clock.useMock(); 41 | 42 | })); 43 | 44 | it('should setup engine', function () { 45 | expect(ngEcs).toBeDefined(); 46 | expect(ngEcs.components).toBeDefined(); 47 | expect(ngEcs.systems).toBeDefined(); 48 | expect(ngEcs.entities).toBeDefined(); 49 | }); 50 | 51 | it('should call update', function () { 52 | 53 | ngEcs.$e({ 'test3' :{} }); // needs and entity, with a component (fix this) 54 | ngEcs.$update(); 55 | ngEcs.$update(); 56 | ngEcs.$update(); 57 | 58 | expect($systems.test.$update.calls.length).toBe(3); 59 | expect($systems.test2.$update.calls.length).toBe(0); 60 | }); 61 | 62 | it('should call update and updateEach', function () { 63 | 64 | ngEcs.$e({ 'test' :{} }); // needs and entity, with a component (fix this) 65 | ngEcs.$e({ 'test' :{} }); // needs and entity, with a component (fix this) 66 | 67 | ngEcs.$update(); 68 | ngEcs.$update(); 69 | ngEcs.$update(); 70 | 71 | expect($systems.test.$update.calls.length).toBe(3); 72 | expect($systems.test.$updateEach.calls.length).toBe(6); 73 | 74 | expect($systems.test2.$update.calls.length).toBe(0); 75 | expect($systems.test2.$updateEach.calls.length).toBe(0); 76 | }); 77 | 78 | it('should call update and updateEach on each system', function () { 79 | 80 | ngEcs.$e({ 'test' :{} }); 81 | ngEcs.$e({ 'test' :{} }); 82 | ngEcs.$e({ 'test2' :{} }); 83 | ngEcs.$e({ 'test2' :{} }); 84 | ngEcs.$e({ 'test2' :{} }); 85 | 86 | ngEcs.$update(); 87 | ngEcs.$update(); 88 | 89 | expect($systems.test.$update.calls.length).toBe(2); 90 | expect($systems.test.$updateEach.calls.length).toBe(10); 91 | expect($systems.test2.$update.calls.length).toBe(2); 92 | expect($systems.test2.$updateEach.calls.length).toBe(6); 93 | }); 94 | 95 | it('should not call update and updateEach on removed systems', function () { 96 | 97 | ngEcs.$e({ 'test' :{} }); 98 | ngEcs.$e({ 'test' :{} }); 99 | ngEcs.$e({ 'test2' :{} }); 100 | ngEcs.$e({ 'test2' :{} }); 101 | ngEcs.$e({ 'test2' :{} }); 102 | 103 | ngEcs.$$removeSystem($systems.test2); 104 | 105 | ngEcs.$update(); 106 | ngEcs.$update(); 107 | 108 | expect($systems.test.$update.calls.length).toBe(2); 109 | expect($systems.test.$updateEach.calls.length).toBe(10); 110 | expect($systems.test2.$update.calls.length).toBe(0); 111 | expect($systems.test2.$updateEach.calls.length).toBe(0); 112 | }); 113 | 114 | it('should call render and renderEach on each system', function () { 115 | 116 | ngEcs.$e({ 'test' :{} }); 117 | ngEcs.$e({ 'test' :{} }); 118 | ngEcs.$e({ 'test2' :{} }); 119 | ngEcs.$e({ 'test2' :{} }); 120 | ngEcs.$e({ 'test2' :{} }); 121 | 122 | ngEcs.$$removeSystem($systems.test2); 123 | 124 | ngEcs.$render(); 125 | ngEcs.$render(); 126 | 127 | expect($systems.test.$render.calls.length).toBe(2); 128 | expect($systems.test.$renderEach.calls.length).toBe(10); 129 | expect($systems.test2.$render.calls.length).toBe(0); 130 | expect($systems.test2.$renderEach.calls.length).toBe(0); 131 | }); 132 | 133 | it('should not call render and renderEach on removed system', function () { 134 | 135 | ngEcs.$e({ 'test' :{} }); 136 | ngEcs.$e({ 'test' :{} }); 137 | ngEcs.$e({ 'test2' :{} }); 138 | ngEcs.$e({ 'test2' :{} }); 139 | ngEcs.$e({ 'test2' :{} }); 140 | 141 | ngEcs.$render(); 142 | ngEcs.$render(); 143 | 144 | expect($systems.test.$render.calls.length).toBe(2); 145 | expect($systems.test.$renderEach.calls.length).toBe(10); 146 | expect($systems.test2.$render.calls.length).toBe(2); 147 | expect($systems.test2.$renderEach.calls.length).toBe(6); 148 | }); 149 | 150 | xit('should run game loop', function (done) { 151 | 152 | ngEcs.$e({ 'test' :{} }); 153 | ngEcs.$e({ 'test' :{} }); 154 | ngEcs.$e({ 'test2' :{} }); 155 | ngEcs.$e({ 'test2' :{} }); 156 | ngEcs.$e({ 'test2' :{} }); 157 | 158 | ngEcs.$start(); 159 | 160 | jasmine.Clock.tick(1000/60*4); 161 | 162 | //console.log(1/ngEcs.$fps, $systems.test.$render.mostRecentCall.args[0], 1/60); 163 | 164 | /* expect($systems.test.$update.calls.length).toBe(4); 165 | expect($systems.test.$updateEach.calls.length).toBe(20); 166 | expect($systems.test2.$update.calls.length).toBe(4); 167 | expect($systems.test2.$updateEach.calls.length).toBe(12); */ 168 | 169 | expect($systems.test.$render.calls.length).toBe(4); 170 | expect($systems.test.$renderEach.calls.length).toBe(20); 171 | expect($systems.test2.$render.calls.length).toBe(4); 172 | expect($systems.test2.$renderEach.calls.length).toBe(12); 173 | 174 | }); 175 | 176 | it('should run game loop', function (done) { 177 | 178 | runs(function() { 179 | ngEcs.$e({ 'test' :{} }); 180 | ngEcs.$e({ 'test' :{} }); 181 | ngEcs.$e({ 'test2' :{} }); 182 | ngEcs.$e({ 'test2' :{} }); 183 | ngEcs.$e({ 'test2' :{} }); 184 | 185 | ngEcs.$start(); 186 | }); 187 | 188 | waitsFor(function() { 189 | return ($systems.test2.$render.calls.length === 7); 190 | }, 'Test', 1000); 191 | 192 | runs(function() { 193 | expect($systems.test.$update.mostRecentCall.args[0]).toBe(1/ngEcs.$fps); 194 | expect($systems.test.$render.mostRecentCall.args[0]).toBeCloseTo(0.016); 195 | 196 | expect($systems.test.$update.calls.length).toBe(6); 197 | expect($systems.test.$updateEach.calls.length).toBe(30); 198 | expect($systems.test2.$update.calls.length).toBe(6); 199 | expect($systems.test2.$updateEach.calls.length).toBe(18); 200 | 201 | expect($systems.test.$render.calls.length).toBe(7); 202 | expect($systems.test.$renderEach.calls.length).toBe(5*7); 203 | expect($systems.test2.$render.calls.length).toBe(7); 204 | expect($systems.test2.$renderEach.calls.length).toBe(3*7); 205 | }); 206 | 207 | }); 208 | 209 | it('should run game loop, interval', function (done) { 210 | var sys; 211 | 212 | runs(function() { 213 | 214 | sys = ngEcs.$s('test3', { 215 | interval: 0.03, 216 | $update: jasmine.createSpy('$update'), 217 | $updateEach: jasmine.createSpy('$updateEach'), 218 | $render: jasmine.createSpy('$render'), 219 | $renderEach: jasmine.createSpy('$renderEach') 220 | }); 221 | 222 | ngEcs.$e({ 'test' :{} }); 223 | ngEcs.$e({ 'test' :{} }); 224 | 225 | ngEcs.$start(); 226 | }); 227 | 228 | waitsFor(function() { 229 | if (sys.$render.calls.length === 5) { 230 | ngEcs.$stop(); 231 | return true; 232 | } 233 | return false; 234 | }, 'Test', 1000); 235 | 236 | runs(function() { 237 | expect(sys.$update.mostRecentCall.args[0]).toBe(0.03); 238 | expect(sys.$render.mostRecentCall.args[0]).toBeCloseTo(0.016); 239 | 240 | expect(sys.$update.calls.length).toBe(2); 241 | //expect(sys.$updateEach.calls.length).toBe(5*0.016/0.04*2); This is a bug, updateEach does not respect interval 242 | 243 | expect(sys.$render.calls.length).toBe(5); 244 | expect(sys.$renderEach.calls.length).toBe(2*5); 245 | }); 246 | 247 | }); 248 | 249 | it('should copy state', function () { 250 | 251 | var c = ngEcs.$copyState({ 252 | x: 1, 253 | y: 2, 254 | $z: 3, 255 | _q: 4, 256 | r: function() {}, 257 | s: { 258 | a: 1, 259 | $b: 2, 260 | _c: 3 261 | } 262 | }); 263 | 264 | expect(c.x).toBe(1); 265 | expect(c.y).toBe(2); 266 | expect(c.$z).toBeUndefined(); 267 | expect(c._q).toBe(4); 268 | expect(c.r).toBeUndefined(); 269 | expect(c.s.a).toBe(1); 270 | expect(c.s.a).toBe(1); 271 | expect(c.s.$b).toBeUndefined(); 272 | expect(c.s._c).toBe(3); 273 | }); 274 | 275 | }); 276 | -------------------------------------------------------------------------------- /test/spec/angular-ecs-entities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('entities', function () { 4 | 5 | var ngEcs, $entities; 6 | 7 | var MockComponent, callback; 8 | 9 | function EventComponent(_e) { 10 | _e.$on('call', callback); 11 | } 12 | 13 | beforeEach(module('hc.ngEcs', function() { 14 | 15 | })); 16 | 17 | beforeEach(inject(function(_ngEcs_, _$entities_){ 18 | ngEcs = _ngEcs_; 19 | $entities = _$entities_; 20 | 21 | MockComponent = jasmine.createSpy('callback'); 22 | callback = jasmine.createSpy('callback'); 23 | 24 | ngEcs.$c('mockComponent', MockComponent); 25 | ngEcs.$c('eventComponent', EventComponent); 26 | })); 27 | 28 | it('should start empty', function () { 29 | expect(ngEcs.entities).toBe($entities); 30 | expect(Object.keys($entities).length).toBe(0); 31 | }); 32 | 33 | it('should create entities', function () { 34 | var e = ngEcs.$e(); 35 | expect(Object.keys($entities).length).toBe(1); 36 | expect($entities[e._id]).toBe(e); 37 | }); 38 | 39 | it('should create entities with id', function () { 40 | var e = ngEcs.$e('e1'); 41 | expect(Object.keys(ngEcs.entities).length).toBe(1); 42 | expect($entities.e1).toBe(e); 43 | }); 44 | 45 | it('should delete entities', function () { 46 | var e = ngEcs.$e(); 47 | ngEcs.$$removeEntity(e); 48 | 49 | expect(Object.keys($entities).length).toBe(0); 50 | expect($entities[e._id]).toBeUndefined(); 51 | }); 52 | 53 | it('should create entities with component using array', function () { 54 | var e = ngEcs.$e(['comp','comp2']); 55 | expect(e.comp).toBeDefined(); 56 | expect(e.comp2).toBeDefined(); 57 | }); 58 | 59 | it('should create entities with components using map', function () { 60 | var e = ngEcs.$e({ comp: { x: 1 }, comp2: { y: 2 } }); 61 | expect(e.comp).toBeDefined(); 62 | expect(e.comp2).toBeDefined(); 63 | expect(e.comp.x).toBe(1); 64 | expect(e.comp2.y).toBe(2); 65 | }); 66 | 67 | it('should add components', function () { 68 | var e = ngEcs.$e(); 69 | 70 | e.$add('comp', { x: 1 }); 71 | e.$add('comp2', { y: 2 }); 72 | 73 | expect(e.comp).toBeDefined(); 74 | expect(e.comp2).toBeDefined(); 75 | expect(e.comp.x).toBe(1); 76 | expect(e.comp2.y).toBe(2); 77 | }); 78 | 79 | it('should throw exception on add undefined component', function() { 80 | var e = ngEcs.$e(); 81 | expect(function() { 82 | e.$add(); 83 | }).toThrow(); 84 | }); 85 | 86 | /* it('should invoke callbacks on add', function () { 87 | var e = ngEcs.$e(); 88 | 89 | var called = []; 90 | e.$on('add', function(_e,k) { 91 | expect(_e).toBe(e); 92 | called.push(k); 93 | }); 94 | 95 | e.$add('comp'); 96 | e.$add('comp2'); 97 | 98 | expect(called).toEqual(['comp','comp2']); 99 | }); */ 100 | 101 | /* it('should not invoke callbacks for non-components on add', function () { 102 | var e = ngEcs.$e(); 103 | 104 | var called = []; 105 | e.$on('add', function(_e,k) { 106 | expect(_e).toBe(e); 107 | called.push(k); 108 | }); 109 | 110 | e.$add('comp'); 111 | e.$add('comp2'); 112 | e.$add('_comp'); 113 | e.$add('$comp'); 114 | 115 | expect(called).toEqual(['comp','comp2']); 116 | expect(e.comp).toBeDefined(); 117 | expect(e._comp).toBeDefined(); 118 | expect(e.$comp).toBeDefined(); 119 | }); */ 120 | 121 | it('should remove components', function () { 122 | var e = ngEcs.$e(); 123 | 124 | e.$add('comp', { x: 1 }); 125 | e.$add('comp2', { y: 2 }); 126 | e.$remove('comp'); 127 | 128 | expect(e.comp).toBeUndefined(); 129 | expect(e.comp2).toBeDefined(); 130 | expect(e.comp2.y).toBe(2); 131 | }); 132 | 133 | /* it('should invoke callbacks on remove', function () { 134 | var e = ngEcs.$e(); 135 | 136 | var called = []; 137 | e.$on('remove', function(_e,k) { 138 | expect(_e).toBe(e); 139 | called.push(k); 140 | }); 141 | 142 | e.$add('comp'); 143 | e.$add('comp2'); 144 | e.$remove('comp'); 145 | 146 | expect(called).toEqual(['comp']); 147 | }); */ 148 | 149 | /* it('should not invoke callbacks for non-components on remove', function () { 150 | var e = ngEcs.$e(); 151 | 152 | var called = []; 153 | e.$on('remove', function(_e,k) { 154 | expect(_e).toBe(e); 155 | called.push(k); 156 | }); 157 | 158 | e.$add('comp'); 159 | e.$add('comp2'); 160 | e.$add('_comp'); 161 | e.$add('$comp'); 162 | e.$remove('comp'); 163 | e.$remove('$comp'); 164 | e.$remove('_comp'); 165 | 166 | expect(called).toEqual(['comp']); 167 | }); */ 168 | 169 | it('should be able to add and emit events', function () { 170 | 171 | var e = ngEcs.$e(); 172 | 173 | e.$on('call',callback); 174 | 175 | e.$emit('call', 'arg1'); 176 | e.$emit('call', 'arg2', 'arg3'); 177 | 178 | expect(callback).toHaveBeenCalledWith('arg1'); 179 | expect(callback).toHaveBeenCalledWith('arg2','arg3'); 180 | 181 | }); 182 | 183 | it('should pass entity to constructor', function () { 184 | 185 | var e = ngEcs.$e(); 186 | 187 | e.$add('mockComponent'); 188 | 189 | expect(MockComponent.calls.length).toBe(1); 190 | expect(MockComponent).toHaveBeenCalledWith(e); 191 | expect(e.mockComponent instanceof MockComponent); 192 | expect(e.mockComponent.prototype === MockComponent.prototype); 193 | }); 194 | 195 | it('should be able to add events in constructor', function () { 196 | 197 | var e = ngEcs.$e(['eventComponent']); 198 | 199 | e.$emit('call'); 200 | e.$emit('call'); 201 | 202 | expect(callback.calls.length).toBe(2); 203 | expect(e.eventComponent instanceof EventComponent); 204 | expect(e.eventComponent.prototype === EventComponent.prototype); 205 | }); 206 | 207 | }); 208 | -------------------------------------------------------------------------------- /test/spec/angular-ecs-families.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('families', function () { 4 | 5 | var ngEcs, $systems, $entities, $families; 6 | 7 | beforeEach(module('hc.ngEcs', function() { 8 | 9 | })); 10 | 11 | beforeEach(inject(function(_ngEcs_, _$systems_, _$entities_, _$families_){ 12 | ngEcs = _ngEcs_; 13 | $systems = _$systems_; 14 | $entities = _$entities_; 15 | $families = _$families_; 16 | 17 | ngEcs.$s('system1', { 18 | $require: ['component1'] 19 | }); 20 | 21 | ngEcs.$s('system2', { 22 | $require: ['component2'] 23 | }); 24 | 25 | ngEcs.$s('system3', { 26 | $require: ['component3'] 27 | }); 28 | 29 | ngEcs.$s('system4', { 30 | $require: ['component2','component1'] 31 | }); 32 | 33 | ngEcs.$s('system5', { 34 | $require: ['component1'] 35 | }); 36 | 37 | })); 38 | 39 | it('should create family', function () { 40 | ngEcs.$s('test', {}); 41 | expect($families['::']).toBeDefined(); 42 | expect($families['::'] instanceof Array); 43 | expect($families['::'].length).toBe(0); 44 | }); 45 | 46 | it('should assign entities to families', function () { 47 | 48 | ngEcs.$e(['component1']); 49 | ngEcs.$e(['component1','component2']); 50 | ngEcs.$e(['component2']); 51 | ngEcs.$e(['component2']); 52 | 53 | expect(Object.keys($entities).length).toBe(4); 54 | expect($families.component1.length).toBe(2); 55 | expect($families.component2.length).toBe(3); 56 | expect($families.component3.length).toBe(0); 57 | expect($families['component1::component2'].length).toBe(1); 58 | }); 59 | 60 | it('should be able to add components later', function () { 61 | 62 | var e1 = ngEcs.$e(); 63 | var e2 = ngEcs.$e(); 64 | var e3 = ngEcs.$e(); 65 | var e4 = ngEcs.$e(); 66 | 67 | expect(Object.keys($entities).length).toBe(4); 68 | expect($families.component1.length).toBe(0); 69 | expect($families.component2.length).toBe(0); 70 | expect($families.component3.length).toBe(0); 71 | expect($families['component1::component2'].length).toBe(0); 72 | 73 | e1.$add('component1'); 74 | e2.$add('component1'); 75 | e2.$add('component2'); 76 | e3.$add('component2'); 77 | e4.$add('component2'); 78 | 79 | expect(Object.keys(ngEcs.entities).length).toBe(4); 80 | expect($families.component1.length).toBe(2); 81 | expect($families.component2.length).toBe(3); 82 | expect($families.component3.length).toBe(0); 83 | expect($families['component1::component2'].length).toBe(1); 84 | 85 | }); 86 | 87 | it('should be able to remove components', function () { 88 | 89 | ngEcs.$e(['component1']); 90 | var e2 = ngEcs.$e(['component1','component2']); 91 | ngEcs.$e(['component2']); 92 | ngEcs.$e(['component2']); 93 | 94 | expect(Object.keys($entities).length).toBe(4); 95 | expect($families.component1.length).toBe(2); 96 | expect($families.component2.length).toBe(3); 97 | expect($families.component3.length).toBe(0); 98 | expect($families['component1::component2'].length).toBe(1); 99 | 100 | e2.$remove('component2'); 101 | 102 | expect($families.component1.length).toBe(2); 103 | expect($families.component2.length).toBe(2); 104 | expect($families.component3.length).toBe(0); 105 | expect($families['component1::component2'].length).toBe(0); 106 | 107 | }); 108 | 109 | it('should reuse families', function () { 110 | 111 | expect($systems.system1.$family).toBe($families.component1); 112 | expect($systems.system2.$family).toBe($families.component2); 113 | expect($systems.system1.$family).toBe($systems.system5.$family); 114 | 115 | }); 116 | 117 | it('should create a families', function () { 118 | 119 | var f = ngEcs.$f(['component4']); 120 | 121 | ngEcs.$e(['component4']); 122 | ngEcs.$e(['component4','component5']); 123 | ngEcs.$e(['component5']); 124 | ngEcs.$e(['component5']); 125 | 126 | expect(Object.keys($entities).length).toBe(4); 127 | expect(f.length).toBe(2); 128 | }); 129 | 130 | it('should allow creating a families after entities', function () { 131 | 132 | ngEcs.$e(['component4']); 133 | ngEcs.$e(['component4','component5']); 134 | ngEcs.$e(['component5']); 135 | ngEcs.$e(['component5']); 136 | 137 | var f = ngEcs.$f(['component4']); 138 | 139 | expect(Object.keys($entities).length).toBe(4); 140 | expect(f.length).toBe(2); 141 | }); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /test/spec/angular-ecs-systems.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('systems', function () { 4 | 5 | var ngEcs, $systems, $entities, $families; 6 | 7 | beforeEach(module('hc.ngEcs', function() { 8 | 9 | })); 10 | 11 | beforeEach(inject(function(_ngEcs_, _$systems_, _$entities_, _$families_){ 12 | ngEcs = _ngEcs_; 13 | $systems = _$systems_; 14 | $entities = _$entities_; 15 | $families = _$families_; 16 | 17 | ngEcs.$s('system1', { 18 | $require: ['component1'], 19 | $addEntity: jasmine.createSpy('$addEntity'), 20 | $removeEntity: jasmine.createSpy('$removeEntity') 21 | }); 22 | 23 | ngEcs.$s('system2', { 24 | $require: ['component2'], 25 | $addEntity: jasmine.createSpy('$addEntity'), 26 | $removeEntity: jasmine.createSpy('$removeEntity') 27 | }); 28 | 29 | ngEcs.$s('system3', { 30 | $require: ['component3'], 31 | $addEntity: jasmine.createSpy('$addEntity'), 32 | $removeEntity: jasmine.createSpy('$removeEntity') 33 | }); 34 | 35 | ngEcs.$s('system4', { 36 | $require: ['component2','component1'], 37 | $addEntity: jasmine.createSpy('$addEntity'), 38 | $removeEntity: jasmine.createSpy('$removeEntity') 39 | }); 40 | 41 | ngEcs.$s('system5', { 42 | $require: ['component1'], 43 | $addEntity: jasmine.createSpy('$addEntity'), 44 | $removeEntity: jasmine.createSpy('$removeEntity') 45 | }); 46 | 47 | })); 48 | 49 | it('should create systems', function () { 50 | expect($systems.system1).toBeDefined(); 51 | expect($systems).toBe(ngEcs.systems); 52 | }); 53 | 54 | it('should reference family', function () { 55 | ngEcs.$s('test', {}); 56 | expect($systems.test.$family).toBe($families['::']); 57 | }); 58 | 59 | it('should auto generate system names', function () { 60 | var s = ngEcs.$s({ 61 | test: 123 62 | }); 63 | expect(s.$family).toBe($families['::']); 64 | expect(s.test).toBe(123); 65 | }); 66 | 67 | it('should reference family', function () { 68 | expect($systems.system1.$family).toBe($families.component1); 69 | }); 70 | 71 | it('should create systems and assign entities to families', function () { 72 | 73 | ngEcs.$e(['component1']); 74 | ngEcs.$e(['component1','component2']); 75 | ngEcs.$e(['component2']); 76 | ngEcs.$e(['component2']); 77 | 78 | expect(Object.keys($entities).length).toBe(4); 79 | expect($systems.system1.$family.length).toBe(2); 80 | expect($systems.system2.$family.length).toBe(3); 81 | expect($systems.system3.$family.length).toBe(0); 82 | expect($systems.system4.$family.length).toBe(1); 83 | }); 84 | 85 | it('should call $addEntity', function () { 86 | 87 | ngEcs.$e(['component1']); 88 | ngEcs.$e(['component1','component2']); 89 | ngEcs.$e(['component2']); 90 | ngEcs.$e(['component2']); 91 | 92 | expect(Object.keys(ngEcs.entities).length).toBe(4); 93 | expect($systems.system1.$addEntity.calls.length).toBe(2); 94 | expect($systems.system2.$addEntity.calls.length).toBe(3); 95 | expect($systems.system3.$addEntity.calls.length).toBe(0); 96 | expect($systems.system4.$addEntity.calls.length).toBe(1); 97 | }); 98 | 99 | it('should call $addEntity with entity', function () { 100 | 101 | var e = ngEcs.$e(['component1','component2']); 102 | 103 | expect($systems.system1.$addEntity).toHaveBeenCalledWith(e); 104 | 105 | }); 106 | 107 | it('should call $addEntity with complete entity', function () { 108 | 109 | ngEcs.$s('system6', { 110 | $require: ['component1'], 111 | $addEntity: function(e) { 112 | expect(e.component1).toBeDefined(); 113 | expect(e.component2).toBeDefined(); 114 | } 115 | }); 116 | 117 | var e = ngEcs.$e({ 118 | component1: { x: 1, y: 2 }, 119 | component2: { z: 3 } 120 | }); 121 | 122 | expect($systems.system1.$addEntity).toHaveBeenCalledWith(e); 123 | 124 | }); 125 | 126 | it('should be able to add components later', function () { 127 | 128 | var e1 = ngEcs.$e(); 129 | var e2 = ngEcs.$e(); 130 | var e3 = ngEcs.$e(); 131 | var e4 = ngEcs.$e(); 132 | 133 | expect(Object.keys($entities).length).toBe(4); 134 | expect($systems.system1.$family.length).toBe(0); 135 | expect($systems.system2.$family.length).toBe(0); 136 | expect($systems.system3.$family.length).toBe(0); 137 | expect($systems.system4.$family.length).toBe(0); 138 | expect($systems.system1.$addEntity.calls.length).toBe(0); 139 | expect($systems.system2.$addEntity.calls.length).toBe(0); 140 | expect($systems.system3.$addEntity.calls.length).toBe(0); 141 | expect($systems.system4.$addEntity.calls.length).toBe(0); 142 | 143 | e1.$add('component1'); 144 | e2.$add('component1'); 145 | e2.$add('component2'); 146 | e3.$add('component2'); 147 | e4.$add('component2'); 148 | 149 | expect(Object.keys($entities).length).toBe(4); 150 | expect($systems.system1.$family.length).toBe(2); 151 | expect($systems.system2.$family.length).toBe(3); 152 | expect($systems.system3.$family.length).toBe(0); 153 | expect($systems.system4.$family.length).toBe(1); 154 | expect($systems.system1.$addEntity.calls.length).toBe(2); 155 | expect($systems.system2.$addEntity.calls.length).toBe(3); 156 | expect($systems.system3.$addEntity.calls.length).toBe(0); 157 | expect($systems.system4.$addEntity.calls.length).toBe(1); 158 | 159 | }); 160 | 161 | it('should be able to remove components', function () { 162 | 163 | ngEcs.$e(['component1']); 164 | var e2 = ngEcs.$e(['component1','component2']); 165 | ngEcs.$e(['component2']); 166 | ngEcs.$e(['component2']); 167 | 168 | expect(Object.keys($entities).length).toBe(4); 169 | expect($systems.system1.$family.length).toBe(2); 170 | expect($systems.system2.$family.length).toBe(3); 171 | expect($systems.system3.$family.length).toBe(0); 172 | expect($systems.system4.$family.length).toBe(1); 173 | expect($systems.system1.$addEntity.calls.length).toBe(2); 174 | expect($systems.system2.$addEntity.calls.length).toBe(3); 175 | expect($systems.system3.$addEntity.calls.length).toBe(0); 176 | expect($systems.system4.$addEntity.calls.length).toBe(1); 177 | 178 | e2.$remove('component2'); 179 | 180 | expect($systems.system1.$family.length).toBe(2); 181 | expect($systems.system2.$family.length).toBe(2); 182 | expect($systems.system3.$family.length).toBe(0); 183 | expect($systems.system4.$family.length).toBe(0); 184 | expect($systems.system1.$addEntity.calls.length).toBe(2); 185 | expect($systems.system2.$addEntity.calls.length).toBe(3); 186 | expect($systems.system3.$addEntity.calls.length).toBe(0); 187 | expect($systems.system4.$addEntity.calls.length).toBe(1); 188 | expect($systems.system1.$removeEntity.calls.length).toBe(0); 189 | expect($systems.system2.$removeEntity.calls.length).toBe(1); 190 | expect($systems.system3.$removeEntity.calls.length).toBe(0); 191 | expect($systems.system4.$removeEntity.calls.length).toBe(1); 192 | }); 193 | 194 | it('should be able to remove components within system updateEach', function () { 195 | 196 | ngEcs.$s('systemX', { 197 | $require: ['component2','component1'], 198 | $updateEach: function(e,dt) { 199 | 200 | if (e._id === '2') { 201 | e.$remove('component2'); 202 | } 203 | 204 | } 205 | }); 206 | 207 | spyOn($systems.systemX, '$updateEach').andCallThrough(); 208 | 209 | ngEcs.$e('1', ['component1','component2']); 210 | ngEcs.$e('2', ['component1','component2']); 211 | ngEcs.$e('3', ['component1','component2']); 212 | ngEcs.$e('4', ['component1','component2']); 213 | 214 | expect(Object.keys($entities).length).toBe(4); 215 | expect($systems.systemX.$family.length).toBe(4); 216 | 217 | ngEcs.$update(1); 218 | 219 | expect($systems.systemX.$updateEach.calls.length).toBe(4); 220 | 221 | expect(Object.keys($entities).length).toBe(4); 222 | expect($systems.systemX.$family.length).toBe(3); 223 | }); 224 | 225 | it('should be able to add components within system updateEach', function () { 226 | 227 | ngEcs.$s('systemX', { 228 | $require: ['component1'], 229 | $updateEach: function(e,dt) { 230 | 231 | if (e._id === '2') { 232 | e.$add('component2'); 233 | } 234 | 235 | } 236 | }); 237 | 238 | spyOn($systems.systemX, '$updateEach').andCallThrough(); 239 | 240 | ngEcs.$e('1', ['component1','component2']); 241 | ngEcs.$e('2', ['component1']); 242 | ngEcs.$e('3', ['component1']); 243 | ngEcs.$e('4', ['component1','component2']); 244 | 245 | expect(Object.keys($entities).length).toBe(4); 246 | expect($systems.systemX.$family.length).toBe(4); 247 | expect($systems.system4.$family.length).toBe(2); 248 | 249 | ngEcs.$update(1); 250 | 251 | expect($systems.systemX.$updateEach.calls.length).toBe(4); 252 | 253 | expect(Object.keys($entities).length).toBe(4); 254 | expect($systems.systemX.$family.length).toBe(4); 255 | expect($systems.system4.$family.length).toBe(3); 256 | 257 | }); 258 | 259 | it('should call removeEntity once', function () { 260 | 261 | var e = ngEcs.$e(['component1','component2']); 262 | 263 | e.$remove('component2'); 264 | e.$remove('component1'); 265 | 266 | expect($systems.system4.$removeEntity.calls.length).toBe(1); 267 | }); 268 | 269 | it('should reuse families', function () { 270 | 271 | expect($systems.system1.$family).toBe($systems.system5.$family); 272 | expect($systems.system1.$family).toNotBe($systems.system2.$family); 273 | 274 | }); 275 | 276 | it('should set default priority', function() { 277 | 278 | var i = 0; 279 | 280 | ngEcs.$s('system0', { 281 | $update: function(e,dt) { 282 | expect(i++).toBe(0); 283 | } 284 | }); 285 | 286 | ngEcs.$s('system10', { 287 | $update: function(e,dt) { 288 | expect(i++).toBe(1); 289 | } 290 | }); 291 | 292 | ngEcs.$e(); 293 | 294 | ngEcs.$update(); 295 | 296 | }); 297 | 298 | it('should set $priority', function() { 299 | 300 | var i = 0; 301 | 302 | ngEcs.$s('system0', { 303 | $priority: 10, 304 | $update: function(e,dt) { 305 | expect(i++).toBe(1); 306 | } 307 | }); 308 | 309 | ngEcs.$s('system10', { 310 | $priority: 100, 311 | $update: function(e,dt) { 312 | expect(i++).toBe(0); 313 | } 314 | }); 315 | 316 | ngEcs.$e(); 317 | 318 | ngEcs.$update(); 319 | 320 | }); 321 | 322 | }); 323 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Todo list 2 | 3 | _\( managed using [todo-md](https://github.com/Hypercubed/todo-md) \)_ 4 | 5 | - [x] Switch to gulp 6 | - [-] $world -> $engine? 7 | - [ ] Finish component constructor function 8 | - [x] ngEcs.$c('position', ['x', 'y', Victor]); 9 | - [ ] make $world/$engine injectable 10 | - [x] Normalize components constructors on register 11 | - [ ] Use babel? ES6 modules? Inheritance for systems? 12 | - [ ] Add ecs.$destroy function? 13 | - [ ] ngECSProvider.config 14 | - [ ] Maybe move engine methods to Providers ($entities.add(xxx), $systems.add(xxx), ...) 15 | - [ ] Update Jasmine 16 | - [ ] get render and renderEach to respect interval 17 | - [ ] Use BoostArray? 18 | - [ ] Make a System class 19 | - [-] Serialization helper 20 | - [ ] ngEcs.$copyState 21 | - [-] ngEcs.$copyState(ngEcs.entities) 22 | - [-] ngEcs.$copyState(ngEcs.systems) 23 | - [ ] ngEcs.$copyState(ngEcs) 24 | - [ ] families -> $families 25 | - [ ] componentes -> $components 26 | - [ ] signals -> $signals 27 | - [ ] tests 28 | - [-] System priority 29 | - [ ] test 30 | - [ ] Scene manager 31 | - [x] Add $renderEach 32 | - [x] Use signals for update and render? 33 | - [x] Add tests for update, updateEach, render, renderEach 34 | --------------------------------------------------------------------------------