) {
203 | const type = component.type;
204 | if (!this.components[type]) {
205 | return;
206 | }
207 |
208 | const idx = this.components[type].indexOf(component);
209 | if (idx >= 0) {
210 | this.components[type].splice(idx, 1);
211 |
212 | if (this.components[type].length < 1) {
213 | delete this.components[type];
214 | }
215 |
216 | // Informa aos interessados sobre a atualização
217 | this.subscriptions.forEach(cb => cb(this, undefined, component));
218 | }
219 | }
220 | }
221 |
222 | /**
223 | * Force typing
224 | */
225 | export type ComponentClassType = (new (data: P) => Component
) & {
226 |
227 | /**
228 | * Static reference to type id
229 | */
230 | type: number;
231 |
232 | /**
233 | * Get all instances of this component from entity
234 | *
235 | * @param entity
236 | */
237 | allFrom(entity: Entity): Component
[];
238 |
239 | /**
240 | * Get one instance of this component from entity
241 | *
242 | * @param entity
243 | */
244 | oneFrom(entity: Entity): Component
;
245 | }
246 |
247 | /**
248 | * Representation of a component in ECS
249 | */
250 | export abstract class Component {
251 |
252 | /**
253 | * Register a new component class
254 | */
255 | public static register(): ComponentClassType
{
256 | const typeID = SEQ_COMPONENT++;
257 |
258 | class ComponentImpl extends Component
{
259 |
260 | static type = typeID;
261 |
262 | static allFrom(entity: Entity): ComponentImpl[] {
263 | let components: ComponentImpl[] = (entity as any).components[typeID];
264 | return components || [];
265 | }
266 |
267 | static oneFrom(entity: Entity): ComponentImpl | undefined {
268 | let components = ComponentImpl.allFrom(entity);
269 | if (components && components.length > 0) {
270 | return components[0];
271 | }
272 | }
273 |
274 | /**
275 | * Create a new instance of this custom component
276 | *
277 | * @param data
278 | */
279 | constructor(data: P) {
280 | super(typeID, data);
281 | }
282 | }
283 |
284 | return (ComponentImpl as any) as ComponentClassType
;
285 | }
286 |
287 | public type: number;
288 |
289 | public data: T;
290 |
291 | /**
292 | * A component can have attributes. Attributes are secondary values used to save miscellaneous data required by some
293 | * specialized systems.
294 | */
295 | public attr: {
296 | [key: string]: any
297 | } = {};
298 |
299 | constructor(type: number, data: T) {
300 | this.type = type;
301 | this.data = data;
302 | }
303 | }
304 |
305 | /**
306 | * System callback
307 | */
308 | export type EventCallback = (data: any, entities: Iterator) => void;
309 |
310 | /**
311 | * Represents the logic that transforms component data of an entity from its current state to its next state. A system
312 | * runs on entities that have a specific set of component types.
313 | */
314 | export abstract class System {
315 |
316 | /**
317 | * IDs of the types of components this system expects the entity to have before it can act on. If you want to
318 | * create a system that acts on all entities, enter [-1]
319 | */
320 | private readonly componentTypes: number[] = [];
321 |
322 | private readonly callbacks: { [key: string]: Array } = {};
323 |
324 | /**
325 | * Unique identifier of an instance of this system
326 | */
327 | public readonly id: number;
328 |
329 | /**
330 | * The maximum times per second this system should be updated
331 | */
332 | public frequence: number;
333 |
334 | /**
335 | * Reference to the world, changed at runtime during interactions.
336 | */
337 | protected world: ECS = undefined as any;
338 |
339 | /**
340 | * Allows to trigger any event. Systems interested in this event will be notified immediately
341 | *
342 | * Injected by ECS at runtime
343 | *
344 | * @param event
345 | * @param data
346 | */
347 | protected trigger: (event: string, data: any) => void = undefined as any;
348 |
349 | /**
350 | * Invoked before updating entities available for this system. It is only invoked when there are entities with the
351 | * characteristics expected by this system.
352 | *
353 | * @param time
354 | */
355 | public beforeUpdateAll?(time: number): void;
356 |
357 | /**
358 | * Invoked in updates, limited to the value set in the "frequency" attribute
359 | *
360 | * @param time
361 | * @param delta
362 | * @param entity
363 | */
364 | public update?(time: number, delta: number, entity: Entity): void;
365 |
366 | /**
367 | * Invoked after performing update of entities available for this system. It is only invoked when there are entities
368 | * with the characteristics expected by this system.
369 | *
370 | * @param time
371 | */
372 | public afterUpdateAll?(time: number, entities: Entity[]): void;
373 |
374 | /**
375 | * Invoked when an expected feature of this system is added or removed from the entity
376 | *
377 | * @param entity
378 | * @param added
379 | * @param removed
380 | */
381 | public change?(entity: Entity, added?: Component, removed?: Component): void;
382 |
383 | /**
384 | * Invoked when:
385 | * a) An entity with the characteristics (components) expected by this system is added in the world;
386 | * b) This system is added in the world and this world has one or more entities with the characteristics expected by
387 | * this system;
388 | * c) An existing entity in the same world receives a new component at runtime and all of its new components match
389 | * the standard expected by this system.
390 | *
391 | * @param entity
392 | */
393 | public enter?(entity: Entity): void;
394 |
395 | /**
396 | * Invoked when:
397 | * a) An entity with the characteristics (components) expected by this system is removed from the world;
398 | * b) This system is removed from the world and this world has one or more entities with the characteristics
399 | * expected by this system;
400 | * c) An existing entity in the same world loses a component at runtime and its new component set no longer matches
401 | * the standard expected by this system
402 | *
403 | * @param entity
404 | */
405 | public exit?(entity: Entity): void;
406 |
407 | /**
408 | * @param componentTypes IDs of the types of components this system expects the entity to have before it can act on.
409 | * If you want to create a system that acts on all entities, enter [-1]
410 | * @param frequence The maximum times per second this system should be updated. Defaults 0
411 | */
412 | constructor(componentTypes: number[], frequence: number = 0) {
413 | this.id = SEQ_SYSTEM++;
414 | this.componentTypes = componentTypes;
415 | this.frequence = frequence;
416 | }
417 |
418 | /**
419 | * Allows you to search in the world for all entities that have a specific set of components.
420 | *
421 | * @param componentTypes Enter [-1] to list all entities
422 | */
423 | protected query(componentTypes: number[]): Iterator {
424 | return this.world.query(componentTypes);
425 | }
426 |
427 | /**
428 | * Allows the system to listen for a specific event that occurred during any update.
429 | *
430 | * In callback, the system has access to the existing entities in the world that are processed by this system, in
431 | * the form of an Iterator, and the raw data sent by the event trigger.
432 | *
433 | * ATTENTION! The callback method will be invoked immediately after the event fires, avoid heavy processing.
434 | *
435 | * @param event
436 | * @param callback
437 | * @param once Allows you to perform the callback only once
438 | */
439 | protected listenTo(event: string, callback: EventCallback, once?: boolean) {
440 | if (!this.callbacks.hasOwnProperty(event)) {
441 | this.callbacks[event] = [];
442 | }
443 |
444 | if (once) {
445 | let tmp = callback.bind(this);
446 |
447 | callback = (data: any, entities: Iterator) => {
448 |
449 | tmp(data, entities);
450 |
451 | let idx = this.callbacks[event].indexOf(callback);
452 | if (idx >= 0) {
453 | this.callbacks[event].splice(idx, 1);
454 | }
455 | if (this.callbacks[event].length === 0) {
456 | delete this.callbacks[event];
457 | }
458 | }
459 | }
460 |
461 | this.callbacks[event].push(callback);
462 | }
463 | }
464 |
465 | /**
466 | * The very definition of the ECS. Also called Admin or Manager in other implementations.
467 | */
468 | export default class ECS {
469 |
470 | public static System = System;
471 |
472 | public static Entity = Entity;
473 |
474 | public static Component = Component;
475 |
476 | /**
477 | * All systems in this world
478 | */
479 | private systems: System[] = [];
480 |
481 | /**
482 | * All entities in this world
483 | */
484 | private entities: Entity[] = [];
485 |
486 | /**
487 | * Indexes the systems that must be run for each entity
488 | */
489 | private entitySystems: { [key: number]: System[] } = {};
490 |
491 | /**
492 | * Records the last instant a system was run in this world for an entity, using real time
493 | */
494 | private entitySystemLastUpdate: { [key: number]: { [key: number]: number } } = {};
495 |
496 | /**
497 | * Records the last instant a system was run in this world for an entity, using game time
498 | */
499 | private entitySystemLastUpdateGame: { [key: number]: { [key: number]: number } } = {};
500 |
501 | /**
502 | * Saves subscriptions made to entities
503 | */
504 | private entitySubscription: { [key: number]: () => void } = {};
505 |
506 | /**
507 | * Injection for the system trigger method
508 | *
509 | * @param event
510 | * @param data
511 | */
512 | private systemTrigger = (event: string, data: any) => {
513 | this.systems.forEach(system => {
514 |
515 | let callbacks: {
516 | [key: string]: Array
517 | } = (system as any).callbacks;
518 |
519 | if (callbacks.hasOwnProperty(event) && callbacks[event].length > 0) {
520 | this.inject(system);
521 | let entitiesIterator = this.query((system as any).componentTypes);
522 | callbacks[event].forEach(callback => {
523 | callback(data, entitiesIterator);
524 | });
525 | }
526 | })
527 | };
528 |
529 | /**
530 | * Allows you to apply slow motion effect on systems execution. When timeScale is 1, the timestamp and delta
531 | * parameters received by the systems are consistent with the actual timestamp. When timeScale is 0.5, the values
532 | * received by systems will be half of the actual value.
533 | *
534 | * ATTENTION! The systems continue to be invoked obeying their normal frequencies, what changes is only the values
535 | * received in the timestamp and delta parameters.
536 | */
537 | public timeScale: number = 1;
538 |
539 | /**
540 | * Last execution of update method
541 | */
542 | private lastUpdate: number = NOW();
543 |
544 | /**
545 | * The timestamp of the game, different from the real world, is updated according to timeScale. If at no time does
546 | * the timeScale change, the value is the same as the current timestamp.
547 | *
548 | * This value is sent to the systems update method.
549 | */
550 | private gameTime: number = 0;
551 |
552 | constructor(systems?: System[]) {
553 | if (systems) {
554 | systems.forEach(system => {
555 | this.addSystem(system);
556 | });
557 | }
558 | }
559 |
560 | /**
561 | * Remove all entities and systems
562 | */
563 | public destroy() {
564 | this.entities.forEach((entity) => {
565 | this.removeEntity(entity);
566 | });
567 |
568 | this.systems.forEach(system => {
569 | this.removeSystem(system);
570 | });
571 | }
572 |
573 | /**
574 | * Get an entity by id
575 | *
576 | * @param id
577 | */
578 | public getEntity(id: number): Entity | undefined {
579 | return this.entities.find(entity => entity.id === id);
580 | }
581 |
582 | /**
583 | * Add an entity to this world
584 | *
585 | * @param entity
586 | */
587 | public addEntity(entity: Entity) {
588 | if (!entity || this.entities.indexOf(entity) >= 0) {
589 | return;
590 | }
591 |
592 | this.entities.push(entity);
593 | this.entitySystemLastUpdate[entity.id] = {};
594 | this.entitySystemLastUpdateGame[entity.id] = {};
595 |
596 | // Remove subscription
597 | if (this.entitySubscription[entity.id]) {
598 | this.entitySubscription[entity.id]();
599 | }
600 |
601 | // Add new subscription
602 | this.entitySubscription[entity.id] = entity
603 | .subscribe((entity, added, removed) => {
604 | this.onEntityUpdate(entity, added, removed);
605 | this.indexEntity(entity);
606 | });
607 |
608 | this.indexEntity(entity);
609 | }
610 |
611 | /**
612 | * Remove an entity from this world
613 | *
614 | * @param idOrInstance
615 | */
616 | public removeEntity(idOrInstance: number | Entity) {
617 | let entity: Entity = idOrInstance as Entity;
618 | if (typeof idOrInstance === 'number') {
619 | entity = this.getEntity(idOrInstance) as Entity;
620 | }
621 |
622 | if (!entity) {
623 | return;
624 | }
625 |
626 | const idx = this.entities.indexOf(entity);
627 | if (idx >= 0) {
628 | this.entities.splice(idx, 1);
629 | }
630 |
631 | // Remove subscription, if any
632 | if (this.entitySubscription[entity.id]) {
633 | this.entitySubscription[entity.id]();
634 | }
635 |
636 | // Invoke system exit
637 | let systems = this.entitySystems[entity.id];
638 | if (systems) {
639 | systems.forEach(system => {
640 | if (system.exit) {
641 | this.inject(system);
642 | system.exit(entity as Entity);
643 | }
644 | });
645 | }
646 |
647 | // Remove associative indexes
648 | delete this.entitySystems[entity.id];
649 | delete this.entitySystemLastUpdate[entity.id];
650 | delete this.entitySystemLastUpdateGame[entity.id];
651 | }
652 |
653 | /**
654 | * Add a system in this world
655 | *
656 | * @param system
657 | */
658 | public addSystem(system: System) {
659 | if (!system) {
660 | return;
661 | }
662 |
663 | if (this.systems.indexOf(system) >= 0) {
664 | return;
665 | }
666 |
667 | this.systems.push(system);
668 |
669 | // Indexes entities
670 | this.entities.forEach(entity => {
671 | this.indexEntity(entity, system);
672 | });
673 |
674 | // Invokes system enter
675 | this.entities.forEach(entity => {
676 | if (entity.active) {
677 | let systems = this.entitySystems[entity.id];
678 | if (systems && systems.indexOf(system) >= 0) {
679 | if (system.enter) {
680 | this.inject(system);
681 | system.enter(entity);
682 | }
683 | }
684 | }
685 | });
686 | }
687 |
688 | /**
689 | * Remove a system from this world
690 | *
691 | * @param system
692 | */
693 | public removeSystem(system: System) {
694 | if (!system) {
695 | return;
696 | }
697 |
698 | const idx = this.systems.indexOf(system);
699 | if (idx >= 0) {
700 | // Invoke system exit
701 | this.entities.forEach(entity => {
702 | if (entity.active) {
703 | let systems = this.entitySystems[entity.id];
704 | if (systems && systems.indexOf(system) >= 0) {
705 | if (system.exit) {
706 | this.inject(system);
707 | system.exit(entity);
708 | }
709 | }
710 | }
711 | });
712 |
713 | this.systems.splice(idx, 1);
714 |
715 | if ((system as any).world === this) {
716 | (system as any).world = undefined;
717 | (system as any).trigger = undefined;
718 | }
719 |
720 | // Indexes entities
721 | this.entities.forEach(entity => {
722 | this.indexEntity(entity, system);
723 | });
724 | }
725 | }
726 |
727 | /**
728 | * Allows you to search for all entities that have a specific set of components.
729 | *
730 | * @param componentTypes Enter [-1] to list all entities
731 | */
732 | public query(componentTypes: number[]): Iterator {
733 | let index = 0;
734 | let listAll = componentTypes.indexOf(-1) >= 0;
735 |
736 | return new Iterator(() => {
737 | outside:
738 | for (let l = this.entities.length; index < l; index++) {
739 | let entity = this.entities[index];
740 | if (listAll) {
741 | // Prevents unnecessary processing
742 | return entity;
743 | }
744 |
745 | // -1 = All components. Allows to query for all entities in the world.
746 | const entityComponentIDs: number[] = [-1].concat(
747 | Object.keys((entity as any).components).map(v => Number.parseInt(v, 10))
748 | );
749 |
750 | for (var a = 0, j = componentTypes.length; a < j; a++) {
751 | if (entityComponentIDs.indexOf(componentTypes[a]) < 0) {
752 | continue outside;
753 | }
754 | }
755 |
756 | // Entity has all the components
757 | return entity;
758 | }
759 | });
760 | }
761 |
762 |
763 | /**
764 | * Invokes the "update" method of the systems in this world.
765 | */
766 | public update() {
767 | let now = NOW();
768 |
769 | // adds scaledDelta
770 | this.gameTime += (now - this.lastUpdate) * this.timeScale;
771 | this.lastUpdate = now;
772 |
773 | let toCallAfterUpdateAll: {
774 | [key: string]: {
775 | system: System;
776 | entities: Entity[];
777 | }
778 | } = {};
779 |
780 |
781 | this.entities.forEach(entity => {
782 | if (!entity.active) {
783 | // Entidade inativa
784 | return this.removeEntity(entity);
785 | }
786 |
787 | let systems = this.entitySystems[entity.id];
788 | if (!systems) {
789 | return;
790 | }
791 |
792 | const entityLastUpdates = this.entitySystemLastUpdate[entity.id];
793 | const entityLastUpdatesGame = this.entitySystemLastUpdateGame[entity.id];
794 | let elapsed, elapsedScaled, interval;
795 |
796 | systems.forEach(system => {
797 | if (system.update) {
798 | this.inject(system);
799 |
800 | elapsed = now - entityLastUpdates[system.id];
801 | elapsedScaled = this.gameTime - entityLastUpdatesGame[system.id];
802 |
803 |
804 | // Limit FPS
805 | if (system.frequence > 0) {
806 | interval = 1000 / system.frequence;
807 | if (elapsed < interval) {
808 | return;
809 | }
810 |
811 | // adjust for fpsInterval not being a multiple of RAF's interval (16.7ms)
812 | entityLastUpdates[system.id] = now - (elapsed % interval);
813 | entityLastUpdatesGame[system.id] = this.gameTime;
814 | } else {
815 | entityLastUpdates[system.id] = now;
816 | entityLastUpdatesGame[system.id] = this.gameTime;
817 | }
818 |
819 | let id = `_` + system.id;
820 | if (!toCallAfterUpdateAll[id]) {
821 | // Call afterUpdateAll
822 | if (system.beforeUpdateAll) {
823 | system.beforeUpdateAll(this.gameTime);
824 | }
825 |
826 | // Save for afterUpdateAll
827 | toCallAfterUpdateAll[id] = {
828 | system: system,
829 | entities: []
830 | };
831 | }
832 | toCallAfterUpdateAll[id].entities.push(entity);
833 |
834 | // Call update
835 | system.update(this.gameTime, elapsedScaled, entity);
836 | }
837 | });
838 | });
839 |
840 |
841 | // Call afterUpdateAll
842 | for (var attr in toCallAfterUpdateAll) {
843 | if (!toCallAfterUpdateAll.hasOwnProperty(attr)) {
844 | continue;
845 | }
846 |
847 | let system = toCallAfterUpdateAll[attr].system;
848 | if (system.afterUpdateAll) {
849 | this.inject(system);
850 | system.afterUpdateAll(this.gameTime, toCallAfterUpdateAll[attr].entities);
851 | }
852 | }
853 | toCallAfterUpdateAll = {};
854 | }
855 |
856 | /**
857 | * Injects the execution context into the system.
858 | *
859 | * A system can exist on several worlds at the same time, ECS ensures that global methods will always reference the
860 | * currently running world.
861 | *
862 | * @param system
863 | */
864 | private inject(system: System): System {
865 | (system as any).world = this;
866 | (system as any).trigger = this.systemTrigger;
867 | return system;
868 | }
869 |
870 | /**
871 | * When an entity receives or loses components, invoking the change method of the systems
872 | *
873 | * @param entity
874 | */
875 | private onEntityUpdate(entity: Entity, added?: Component, removed?: Component) {
876 | if (!this.entitySystems[entity.id]) {
877 | return;
878 | }
879 |
880 | const toNotify: System[] = this.entitySystems[entity.id].slice(0);
881 |
882 | outside:
883 | for (var idx = toNotify.length - 1; idx >= 0; idx--) {
884 | let system = toNotify[idx];
885 |
886 | // System is listening to updates on entity?
887 | if (system.change) {
888 |
889 | let systemComponentTypes = (system as any).componentTypes;
890 |
891 | // Listen to all component type
892 | if (systemComponentTypes.indexOf(-1) >= 0) {
893 | continue;
894 | }
895 |
896 | if (added && systemComponentTypes.indexOf(added.type) >= 0) {
897 | continue outside;
898 | }
899 |
900 | if (removed && systemComponentTypes.indexOf(removed.type) >= 0) {
901 | continue outside;
902 | }
903 | }
904 |
905 | // dont match
906 | toNotify.splice(idx, 1);
907 | }
908 |
909 | // Notify systems
910 | toNotify.forEach(system => {
911 | system = this.inject(system);
912 | const systemComponentTypes = (system as any).componentTypes;
913 | const all = systemComponentTypes.indexOf(-1) >= 0;
914 | (system.change as any)(
915 | entity,
916 | // Send only the list of components this system expects
917 | all
918 | ? added
919 | : (
920 | added && systemComponentTypes.indexOf(added.type) >= 0
921 | ? added
922 | : undefined
923 | ),
924 | all
925 | ? removed
926 | : (
927 | removed && systemComponentTypes.indexOf(removed.type) >= 0
928 | ? removed
929 | : undefined
930 | )
931 | );
932 | });
933 | }
934 |
935 | private indexEntitySystem = (entity: Entity, system: System) => {
936 | const idx = this.entitySystems[entity.id].indexOf(system);
937 |
938 | // Sistema não existe neste mundo, remove indexação
939 | if (this.systems.indexOf(system) < 0) {
940 | if (idx >= 0) {
941 | this.entitySystems[entity.id].splice(idx, 1);
942 | delete this.entitySystemLastUpdate[entity.id][system.id];
943 | delete this.entitySystemLastUpdateGame[entity.id][system.id];
944 | }
945 | return;
946 | }
947 |
948 | const systemComponentTypes = (system as any).componentTypes;
949 |
950 | for (var a = 0, l = systemComponentTypes.length; a < l; a++) {
951 | // -1 = All components. Allows a system to receive updates from all entities in the world.
952 | let entityComponentIDs: number[] = [-1].concat(
953 | Object.keys((entity as any).components).map(v => Number.parseInt(v, 10))
954 | );
955 | if (entityComponentIDs.indexOf(systemComponentTypes[a]) < 0) {
956 | // remove
957 | if (idx >= 0) {
958 | // Informs the system of relationship removal
959 | if (system.exit) {
960 | this.inject(system);
961 | system.exit(entity);
962 | }
963 |
964 | this.entitySystems[entity.id].splice(idx, 1);
965 | delete this.entitySystemLastUpdate[entity.id][system.id];
966 | delete this.entitySystemLastUpdateGame[entity.id][system.id];
967 | }
968 | return
969 | }
970 | }
971 |
972 | // Entity has all the components this system needs
973 | if (idx < 0) {
974 | this.entitySystems[entity.id].push(system);
975 | this.entitySystemLastUpdate[entity.id][system.id] = NOW();
976 | this.entitySystemLastUpdateGame[entity.id][system.id] = this.gameTime;
977 |
978 | // Informs the system about the new relationship
979 | if (system.enter) {
980 | this.inject(system);
981 | system.enter(entity);
982 | }
983 | }
984 | };
985 |
986 | /**
987 | * Indexes an entity
988 | *
989 | * @param entity
990 | */
991 | private indexEntity(entity: Entity, system?: System) {
992 |
993 | if (!this.entitySystems[entity.id]) {
994 | this.entitySystems[entity.id] = [];
995 | }
996 |
997 | if (system) {
998 | // Index entity for a specific system
999 | this.indexEntitySystem(entity, system);
1000 |
1001 | } else {
1002 | // Indexes the entire entity
1003 | this.systems.forEach((system) => {
1004 | this.indexEntitySystem(entity, system);
1005 | });
1006 | }
1007 | }
1008 | }
1009 |
1010 |
--------------------------------------------------------------------------------
/test/ECS.test.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/test/ECS.test.ts:
--------------------------------------------------------------------------------
1 | import {describe} from "mocha";
2 | import ECS, {Component, Entity, System} from "../src";
3 | import {expect} from "chai";
4 |
5 |
6 | const ComponentA = Component.register();
7 |
8 | const ComponentB = Component.register();
9 |
10 | const ComponentC = Component.register();
11 |
12 | class Entt extends Entity {
13 |
14 | }
15 |
16 | class SysCompA extends System {
17 | constructor() {
18 | super([ComponentA.type]);
19 | }
20 | }
21 |
22 |
23 | describe("ECS", () => {
24 |
25 | describe("Components", () => {
26 |
27 | it("must be unique", () => {
28 | let world = new ECS();
29 |
30 | expect(ComponentA.type).to.not.equal(ComponentB.type);
31 |
32 | let compA = new ComponentA(100);
33 | let compA2 = new ComponentA(100);
34 | let compB = new ComponentB(200);
35 |
36 | expect(ComponentA.type).to.eql(compA.type);
37 | expect(ComponentA.type).to.eql(compA2.type);
38 | expect(compA2.type).to.eql(compA2.type);
39 |
40 | expect(ComponentB.type).to.eql(compB.type);
41 |
42 |
43 | expect(compA.data).to.eql(100);
44 | expect(compB.data).to.eql(200);
45 | });
46 | });
47 |
48 | describe("Entity", () => {
49 |
50 | it("must have unique identifiers", () => {
51 | let world = new ECS();
52 |
53 | let enttA = new Entt();
54 | let enttB = new Entt();
55 | expect(enttA.id).to.not.equal(enttB.id);
56 | expect(enttA.id).to.eql(enttB.id - 1);
57 | });
58 | });
59 |
60 |
61 | describe("System", () => {
62 |
63 | it("must add systems at runtime", () => {
64 |
65 | let sys1 = new SysCompA();
66 |
67 | let world = new ECS([sys1]);
68 |
69 | expect((world as any).systems.length).to.eql(1);
70 |
71 | let sys2 = new SysCompA();
72 | world.addSystem(sys2);
73 | expect((world as any).systems).to.eql([sys1, sys2]);
74 |
75 | // Must ignore undefined
76 | world.addSystem(undefined as any);
77 | expect((world as any).systems).to.eql([sys1, sys2]);
78 |
79 | // Must ignore if exists
80 | world.addSystem(sys2);
81 | expect((world as any).systems).to.eql([sys1, sys2]);
82 | });
83 |
84 | it("must remove systems at runtime", () => {
85 |
86 | let sys1 = new SysCompA();
87 |
88 | let world = new ECS([sys1]);
89 |
90 | expect((world as any).systems.length).to.eql(1);
91 |
92 | let sys2 = new SysCompA();
93 | world.addSystem(sys2);
94 |
95 | expect((world as any).systems).to.eql([sys1, sys2]);
96 |
97 | // Must ignore undefined
98 | world.removeSystem(undefined as any);
99 | expect((world as any).systems).to.eql([sys1, sys2]);
100 |
101 | world.removeSystem(sys1);
102 | expect((world as any).systems).to.eql([sys2]);
103 |
104 | world.removeSystem(sys2);
105 | expect((world as any).systems).to.eql([]);
106 | });
107 |
108 |
109 | it("must be invoked in the expected order", () => {
110 |
111 | let callOrder = 1;
112 |
113 | let enterCalled = 0;
114 | let changedCalled = 0;
115 | let beforeUpdateAllCalled = 0;
116 | let updateCalled = 0;
117 | let afterUpdateAllCalled = 0;
118 | let exitCalled = 0;
119 |
120 | function clear() {
121 | callOrder = 1;
122 | enterCalled = 0;
123 | changedCalled = 0;
124 | beforeUpdateAllCalled = 0;
125 | updateCalled = 0;
126 | afterUpdateAllCalled = 0;
127 | exitCalled = 0;
128 | }
129 |
130 | class Sys extends System {
131 | constructor(type: number) {
132 | super([type]);
133 | }
134 |
135 | beforeUpdateAll(time: number): void {
136 | beforeUpdateAllCalled = callOrder++;
137 | }
138 |
139 | update(time: number, delta: number, entity: Entity): void {
140 | updateCalled = callOrder++;
141 | }
142 |
143 | afterUpdateAll(time: number, entities: Entity[]): void {
144 | afterUpdateAllCalled = callOrder++;
145 | }
146 |
147 | enter(entity: Entity): void {
148 | enterCalled = callOrder++;
149 | }
150 |
151 | exit(entity: Entity): void {
152 | exitCalled = callOrder++;
153 | }
154 |
155 | change(entity: Entity, added?: Component, removed?: Component): void {
156 | changedCalled = callOrder++;
157 | }
158 | }
159 |
160 | let calledSystem = new Sys(ComponentA.type);
161 |
162 | // Control, should never invoke methods of this instance
163 | let notCalledSystem = new Sys(ComponentC.type);
164 |
165 | let entity = new Entt();
166 | let entity2 = new Entt();
167 |
168 | let world = new ECS([calledSystem, notCalledSystem]);
169 |
170 | // init
171 | expect(enterCalled).to.eql(0);
172 | expect(changedCalled).to.eql(0);
173 | expect(beforeUpdateAllCalled).to.eql(0);
174 | expect(updateCalled).to.eql(0);
175 | expect(afterUpdateAllCalled).to.eql(0);
176 | expect(exitCalled).to.eql(0);
177 |
178 |
179 | // update without entities match, do
180 | world.update();
181 |
182 | expect(enterCalled).to.eql(0);
183 | expect(changedCalled).to.eql(0);
184 | expect(beforeUpdateAllCalled).to.eql(0);
185 | expect(updateCalled).to.eql(0);
186 | expect(afterUpdateAllCalled).to.eql(0);
187 | expect(exitCalled).to.eql(0);
188 |
189 | // does nothing, does not have the expected features of the system
190 | world.addEntity(entity);
191 |
192 | expect(enterCalled).to.eql(0);
193 | expect(changedCalled).to.eql(0);
194 | expect(beforeUpdateAllCalled).to.eql(0);
195 | expect(updateCalled).to.eql(0);
196 | expect(afterUpdateAllCalled).to.eql(0);
197 | expect(exitCalled).to.eql(0);
198 |
199 | // expect enter
200 | let componentA = new ComponentA(100);
201 | entity.add(componentA);
202 |
203 | // expect enter
204 | (global as any).setImmediateExec();
205 |
206 | expect(enterCalled).to.eql(1);
207 | expect(changedCalled).to.eql(0);
208 | expect(beforeUpdateAllCalled).to.eql(0);
209 | expect(updateCalled).to.eql(0);
210 | expect(afterUpdateAllCalled).to.eql(0);
211 | expect(exitCalled).to.eql(0);
212 | clear();
213 |
214 | // do nothing, system is not interested in this kind of component
215 | let componentB = new ComponentB(100);
216 | entity.add(componentB);
217 | (global as any).setImmediateExec();
218 |
219 | expect(enterCalled).to.eql(0);
220 | expect(changedCalled).to.eql(0);
221 | expect(beforeUpdateAllCalled).to.eql(0);
222 | expect(updateCalled).to.eql(0);
223 | expect(afterUpdateAllCalled).to.eql(0);
224 | expect(exitCalled).to.eql(0);
225 | clear();
226 |
227 |
228 | // again, do nothing, system is not interested in this kind of component
229 | entity.remove(componentB);
230 | (global as any).setImmediateExec();
231 |
232 | expect(enterCalled).to.eql(0);
233 | expect(changedCalled).to.eql(0);
234 | expect(beforeUpdateAllCalled).to.eql(0);
235 | expect(updateCalled).to.eql(0);
236 | expect(afterUpdateAllCalled).to.eql(0);
237 | expect(exitCalled).to.eql(0);
238 | clear();
239 |
240 | // expect change
241 | let componentA2 = new ComponentA(100);
242 | entity.add(componentA2);
243 | (global as any).setImmediateExec();
244 |
245 | expect(enterCalled).to.eql(0);
246 | expect(changedCalled).to.eql(1);
247 | expect(beforeUpdateAllCalled).to.eql(0);
248 | expect(updateCalled).to.eql(0);
249 | expect(afterUpdateAllCalled).to.eql(0);
250 | expect(exitCalled).to.eql(0);
251 | clear();
252 |
253 | // again, expect change
254 | entity.remove(componentA2);
255 | (global as any).setImmediateExec();
256 |
257 | expect(enterCalled).to.eql(0);
258 | expect(changedCalled).to.eql(1);
259 | expect(beforeUpdateAllCalled).to.eql(0);
260 | expect(updateCalled).to.eql(0);
261 | expect(afterUpdateAllCalled).to.eql(0);
262 | expect(exitCalled).to.eql(0);
263 | clear();
264 |
265 | // expect update, before and after
266 | world.update();
267 |
268 | expect(enterCalled).to.eql(0);
269 | expect(changedCalled).to.eql(0);
270 | expect(beforeUpdateAllCalled).to.eql(1);
271 | expect(updateCalled).to.eql(2);
272 | expect(afterUpdateAllCalled).to.eql(3);
273 | expect(exitCalled).to.eql(0);
274 |
275 | // again
276 | world.update();
277 |
278 | expect(enterCalled).to.eql(0);
279 | expect(changedCalled).to.eql(0);
280 | expect(beforeUpdateAllCalled).to.eql(4);
281 | expect(updateCalled).to.eql(5);
282 | expect(afterUpdateAllCalled).to.eql(6);
283 | expect(exitCalled).to.eql(0);
284 | clear();
285 |
286 |
287 | // on remove entity
288 | world.removeEntity(entity);
289 |
290 | expect(enterCalled).to.eql(0);
291 | expect(changedCalled).to.eql(0);
292 | expect(beforeUpdateAllCalled).to.eql(0);
293 | expect(updateCalled).to.eql(0);
294 | expect(afterUpdateAllCalled).to.eql(0);
295 | expect(exitCalled).to.eql(1);
296 |
297 | });
298 | });
299 |
300 | it("must create without systems", () => {
301 | let world = new ECS();
302 |
303 | expect((world as any).systems).to.eql([]);
304 | });
305 |
306 | it("must create with systems", () => {
307 |
308 | class Sys extends System {
309 | constructor() {
310 | super([-1]);
311 | }
312 | }
313 |
314 | let world = new ECS([new Sys]);
315 |
316 | expect((world as any).systems.length).to.eql(1);
317 | });
318 | });
319 |
--------------------------------------------------------------------------------
/test/Iterator.test.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/test/Iterator.test.ts:
--------------------------------------------------------------------------------
1 | import {describe} from "mocha";
2 | import {Iterator} from "../src";
3 | import {expect} from "chai";
4 |
5 |
6 | describe("Iterator", () => {
7 |
8 | it("must invoke generator asynchronously", () => {
9 | let r = -1;
10 | let c = 0;
11 | let it = new Iterator(i => {
12 | if (i < 5) {
13 | c++;
14 | expect(r).to.eql(c - 2);
15 | return i;
16 | }
17 | return undefined;
18 | });
19 |
20 | it.each(item => {
21 | r = item;
22 | expect(r).to.eql(c - 1);
23 | });
24 | });
25 |
26 | it("must cache data", () => {
27 | let c = 0;
28 | let it = new Iterator(i => {
29 | if (i < 5) {
30 | c++;
31 | return i;
32 | }
33 | return undefined;
34 | });
35 |
36 | // iterate all
37 | it.each(item => {
38 |
39 | });
40 |
41 | expect(c).to.eql(5);
42 |
43 | c = 0;
44 |
45 | // iterate again
46 | it.each(item => {
47 |
48 | });
49 |
50 | expect(c).to.eql(0);
51 | });
52 |
53 | it("each() - should stop returning false", () => {
54 | let c = 0;
55 | let it = new Iterator(i => {
56 | if (i < 5) {
57 | c++;
58 | return i;
59 | }
60 | return undefined;
61 | });
62 |
63 | let r;
64 | it.each(item => {
65 | r = item;
66 | if (item == 2) {
67 | return false
68 | }
69 | });
70 |
71 | expect(r).to.eql(2);
72 | expect(c).to.eql(3);
73 | });
74 |
75 | it("each() - must iterate over all items", () => {
76 | let c = 0;
77 | let it = new Iterator(i => {
78 | if (i < 5) {
79 | c++;
80 | return i;
81 | }
82 | return undefined;
83 | });
84 |
85 | let r;
86 | it.each(item => {
87 | r = item;
88 | });
89 |
90 | expect(r).to.eql(4);
91 | expect(c).to.eql(5);
92 | });
93 |
94 | it("find() - must find an item", () => {
95 | let c = 0;
96 | let it = new Iterator(i => {
97 | if (i < 5) {
98 | c++;
99 | return i;
100 | }
101 | return undefined;
102 | });
103 |
104 | let r = it.find(item => item === 3);
105 |
106 | expect(r).to.eql(3);
107 | expect(c).to.eql(4);
108 | });
109 |
110 | it("find() - should return undefined when not finding an item", () => {
111 | let c = 0;
112 | let it = new Iterator(i => {
113 | if (i < 5) {
114 | c++;
115 | return i;
116 | }
117 | return undefined;
118 | });
119 |
120 | let r = it.find(item => item === 6);
121 |
122 | expect(r).to.eql(undefined);
123 | expect(c).to.eql(5);
124 | });
125 |
126 | it("filter() - must apply informed filter", () => {
127 | let c = 0;
128 | let it = new Iterator(i => {
129 | if (i < 5) {
130 | c++;
131 | return i;
132 | }
133 | return undefined;
134 | });
135 |
136 | let r = it.filter(item => item % 2 == 0);
137 |
138 | expect(r).to.eql([0, 2, 4]);
139 | expect(c).to.eql(5);
140 | });
141 |
142 | it("map() - must map iterator data", () => {
143 | let c = 0;
144 | let it = new Iterator(i => {
145 | if (i < 5) {
146 | c++;
147 | return i;
148 | }
149 | return undefined;
150 | });
151 |
152 | let r = it.map(item => item * 2);
153 |
154 | expect(r).to.eql([0, 2, 4, 6, 8]);
155 | expect(c).to.eql(5);
156 | });
157 | });
158 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | -r test/setup.js
2 | --require ts-node/register
3 | --require source-map-support/register
4 | --full-trace
5 | --bail
6 | test/*.test.ts
7 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | let setImmediateCallbacks = [];
2 |
3 | global.setImmediate = global.requestAnimationFrame = function (fn) {
4 | setImmediateCallbacks.push(fn);
5 | };
6 |
7 | global.setImmediateExec = function () {
8 | setImmediateCallbacks.forEach(fn => fn());
9 | setImmediateCallbacks = [];
10 | };
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "sourceMap": true,
6 | "rootDir": "src",
7 | "declaration": true,
8 | "emitDeclarationOnly": true,
9 | "outDir": "./",
10 | "baseUrl": "./",
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "skipLibCheck": true,
14 | "moduleResolution": "node",
15 | "esModuleInterop": true,
16 | "lib": [
17 | "dom",
18 | "esnext",
19 | "es5"
20 | ],
21 | "paths": {
22 | "*": [
23 | "node_modules/*"
24 | ]
25 | },
26 | "types": [
27 | "node"
28 | ]
29 | },
30 | "exclude": [
31 | "node_modules",
32 | "dist",
33 | "example",
34 | "test"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/v-release.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Altera a versão para RELEASE antes do build
3 | */
4 | const fs = require('fs');
5 | const http = require('http');
6 | const semver = require('semver');
7 |
8 | var options = {
9 | hostname: 'registry.npmjs.org',
10 | port: 80,
11 | path: '/ecs-lib',
12 | method: 'GET',
13 | headers: {
14 | 'Accept': 'application/json'
15 | }
16 | };
17 |
18 | var req = http.request(options, function (res) {
19 | var json = '';
20 | res.setEncoding('utf8');
21 | res.on('data', function (data) {
22 | json += data;
23 |
24 | });
25 |
26 | res.on('end', function () {
27 | let info = JSON.parse(json);
28 |
29 | // No formato '0.1.0', '0.2.0'
30 | var release = '0.0.0';
31 |
32 | var version = '';
33 | for (var v in info.versions) {
34 | if (semver.prerelease(v)) {
35 | continue;
36 | } else if (semver.gt(v, release)) {
37 | release = v;
38 | }
39 | }
40 |
41 | // Numero da nova versão SNAPHSOT (pre)
42 | var version = semver.inc(release, 'minor');
43 | console.log('Incrementing ecs-lib version to: "' + version + '"');
44 |
45 | var packageJson = require('./package.json');
46 |
47 | packageJson.version = version;
48 |
49 | fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 4));
50 | });
51 | });
52 |
53 | req.on('error', function (e) {
54 | throw e;
55 | });
56 |
57 | req.end();
58 |
--------------------------------------------------------------------------------
/v-snapshot.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Altera a versão para SNAPSHOT antes do build
3 | */
4 | const fs = require('fs');
5 | const http = require('http');
6 | const semver = require('semver');
7 |
8 | var options = {
9 | hostname: 'registry.npmjs.org',
10 | port: 80,
11 | path: '/ecs-lib',
12 | method: 'GET',
13 | headers: {
14 | 'Accept': 'application/json'
15 | }
16 | };
17 |
18 | var req = http.request(options, function (res) {
19 | var json = '';
20 | res.setEncoding('utf8');
21 | res.on('data', function (data) {
22 | json += data;
23 |
24 | });
25 |
26 | res.on('end', function () {
27 | let info = JSON.parse(json);
28 |
29 | // No formato '0.1.0', '0.2.0'
30 | var release = '0.0.0';
31 |
32 | // No formato '0.1.0-pre.0', '0.1.0-pre.1', '0.1.0-pre.2'
33 | var snapshot = '0.0.0';
34 | var version = '';
35 | for (var v in info.versions) {
36 | if (semver.prerelease(v)) {
37 | // Pre-release
38 | if (semver.gt(v, snapshot)) {
39 | snapshot = v;
40 | }
41 | } else if (semver.gt(v, release)) {
42 | release = v;
43 | }
44 | }
45 |
46 | // Se não possui snapshot da versão atual, zera para garantir o uso do preminor
47 | if (semver.gt(release, snapshot)) {
48 | snapshot = '0.0.0';
49 | }
50 |
51 | // Numero da nova versão SNAPHSOT (pre)
52 | // Se já possui um prerelease, apenas incrementa a versão do snapshot
53 | // Ex. Se existir '0.1.0-pre.0', a proxima será '0.1.0-pre.1'
54 | if (snapshot != '0.0.0') {
55 | version = semver.inc(snapshot, 'prerelease', 'pre');
56 | } else {
57 | version = semver.inc(release, 'preminor', 'pre');
58 | }
59 |
60 | console.log('Incrementing ecs-lib version to: "' + version + '"');
61 |
62 | var packageJson = require('./package.json');
63 |
64 | packageJson.version = version;
65 |
66 | fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, 4));
67 | });
68 | });
69 |
70 | req.on('error', function (e) {
71 | throw e;
72 | });
73 |
74 | req.end();
75 |
--------------------------------------------------------------------------------