From fee35197688a4bc203447f9b19944af4724f5bc1 Mon Sep 17 00:00:00 2001 From: hak33m16 Date: Sun, 6 Mar 2022 12:03:04 -0800 Subject: [PATCH] Refactor API (#12) Refactors the addComponent API to now be called addComponents. It now accepts n-many Component class references, and since users are expected to only add data fields to their components, will auto construct the components for them. Small improvements to the EntityGroup API that allows users to easily check the contents of a query's result, with the addition of get and has helpers. Changes to the component API which now auto-constructs new components for the user, giving them a method that guarantees type-safety without the need for non-null assertions. Updates version. --- README.md | 88 +++++++++++++++++++++------- package.json | 2 +- src/Component.ts | 6 +- src/Entity.ts | 63 ++++++++++++++------ src/EntityManager.ts | 46 +++++++++++---- src/__tests__/Entity.test.ts | 89 ++++++++++++++++++----------- src/__tests__/EntityManager.test.ts | 84 ++++++++++++++++----------- 7 files changed, 259 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 1863843..6971bd9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![Build Status](https://github.com/hak33m16/trecs/workflows/build/badge.svg?branch=master)](https://github.com/hak33m16/trecs/actions?query=workflow%3Abuild+branch%3Amaster) [![codecov](https://codecov.io/gh/hak33m16/trecs/branch/master/graph/badge.svg?token=QG2BOJPZC3)](https://codecov.io/gh/hak33m16/trecs) [![Code Climate](https://codeclimate.com/github/hak33m16/trecs/badges/gpa.svg)](https://codeclimate.com/github/hak33m16/trecs) +[![npm version](https://badge.fury.io/js/trecs.svg)](https://badge.fury.io/js/trecs) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) -[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) [![npm version](https://badge.fury.io/js/trecs.svg)](https://badge.fury.io/js/trecs) +[![Build Status](https://github.com/hak33m16/trecs/workflows/build/badge.svg?branch=master)](https://github.com/hak33m16/trecs/actions?query=workflow%3Abuild+branch%3Amaster) [![codecov](https://codecov.io/gh/hak33m16/trecs/branch/master/graph/badge.svg?token=QG2BOJPZC3)](https://codecov.io/gh/hak33m16/trecs) [![Code Climate](https://codeclimate.com/github/hak33m16/trecs/badges/gpa.svg)](https://codeclimate.com/github/hak33m16/trecs) # TrECS @@ -51,47 +51,60 @@ class Sprite extends Component { } ``` -Components are added using `addComponent` and support chaining: +It's recommended to add components using the `component` helper method: +```ts +const spriteRef = hero.component(Sprite) +spriteRef.image = 'new-image.png' +``` + +The `component` method will either return a reference to the entities instance of that particular type, or it will auto construct it before doing so. + +If you'd like to add multiple components at once, the `addComponents` method is available, but will require that you retrieve them through other accessors afterwards. ```ts -hero.addComponent(new PlayerControlled()).addComponent(new Sprite()); +hero.addComponents(PlayerControlled, Sprite) ``` -Retrieve components in a type-safe way: +Components can then be retrieved with `component`: ```ts hero.component(PlayerControlled).gamepad = 2 hero.component(Sprite).image === 'hero.png'; // true ``` -Entities can be tagged with a string for fast retrieval: +Optionally, there's the `getComponent` method, but it won't auto construct components for you and therefore can't guarantee that what it returns is defined: ```ts -hero.addTag('player'); +hero.getComponent(PlayerControlled)!.gamepad = 2 // note the !. to guarantee non-null +``` -... +In order to check if an entity has a component, use the helper method `hasComponent`: -const hero = Array.from(world.queryTag('player').values())[0] // This syntax will get better, I promise +```ts +hero.hasComponent(PlayerControlled) // true ``` -You can also remove components and tags in much the same way: +A set of components can also be quickly checked: ```ts -hero.removeComponent(Sprite); -hero.removeTag('player'); +if (hero.hasAllComponents(Transform, Sprite)) { ... } ``` -`hasComponent` will efficiently determine if an entity has a specific single -component: +Entities can be tagged with a string for fast retrieval: ```ts -if (hero.hasComponent(Transform)) { ... } +hero.addTag('player'); + +... + +const hero = world.queryTag('player').toArray()[0] ``` -A set of components can also be quickly checked: +You can also remove components and tags in much the same way: ```ts -if (hero.hasAllComponents(Transform, Sprite)) { ... } +hero.removeComponent(Sprite); +hero.removeTag('player'); ``` ### Querying Entities @@ -99,26 +112,57 @@ if (hero.hasAllComponents(Transform, Sprite)) { ... } The entity manager indexes entities and their components, allowing extremely fast queries. -Entity queries return an array of entities. +Entity queries return read-only reference to a group of entities. Get all entities that have a specific set of components: ```ts -const toDraw = entities.queryComponents(Transform, Sprite); +const toDraw = world.queryComponents(Transform, Sprite); ``` Get all entities with a certain tag: ```ts -const enemies = entities.queryTag('enemy'); +const enemies = world.queryTag('enemy'); +``` + +The type of the returned query is also directly iterable: + +```ts +const objects = world.queryComponents(Position, Velocity) +for (const entity of objects) { ... } +``` + +Note that the underlying group can be modified by anything that has a reference to your entity manager. If you need a copy of the results that won't be modified, create an array of the results. + +```ts +const objects = world.queryComponents(Position, Velocity) + +const myCopy = objects.toArray() +// OR +const myCopy = Array.from(objects) ``` ### Removing Entities +To remove an entity from a manager, all of its components, and all of its tags, use `remove`: + ```ts hero.remove(); ``` +To remove a particular component, use `removeComponent`: + +```ts +hero.removeComponent(Sprite) +``` + +To remove a tag, use `removeTag`: + +```ts +hero.removeTag('player') +``` + ### Components As mentioned above, components must extend the base class `Component` for type-safety reasons. It is highly recommended that components are lean data containers, leaving all the heavy lifting for systems. If interface names weren't erased after transpilation, this library would've used them instead of classes. @@ -138,9 +182,9 @@ function PhysicsSystem (world) this.update = function (dt, time) { var candidates = world.queryComponents(Transform, RigidBody); - candidates.forEach(function(entity) { + for (const entity of candidates) { ... - }); + } } } ``` diff --git a/package.json b/package.json index f122011..d1a5dfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trecs", - "version": "0.1.1", + "version": "0.2.0", "description": "A TypeScript-first Entity Component System library.", "keywords": [ "component", diff --git a/src/Component.ts b/src/Component.ts index 9f67230..4b11e1a 100644 --- a/src/Component.ts +++ b/src/Component.ts @@ -1 +1,5 @@ -export class Component {} +export class Component { + // Required to prevent anything with a constructor being + // considered a component class :F: + public static __uniqueComponentProperty: any; +} diff --git a/src/Entity.ts b/src/Entity.ts index 6eb7245..e3a26e9 100644 --- a/src/Entity.ts +++ b/src/Entity.ts @@ -1,7 +1,8 @@ import { Component } from "./Component"; import { EntityManager } from "./EntityManager"; -export interface TypeStore extends Function { +export interface ComponentTypeStore extends Function { + __uniqueComponentProperty: any; new (...args: any[]): T; } @@ -29,27 +30,47 @@ export class Entity { this._componentMap = {}; } - public component(classRef: TypeStore): T | undefined { + /** + * Helper function to safely access components as it is guaranteed + * to return a component of the specified type. + * + * If the specified component type doesn't exist on the entity, it + * will auto-construct it and add it. + * @param classRef type of component + * @returns component of specified type + */ + public component(classRef: ComponentTypeStore): T { + if (!this.hasComponent(classRef)) { + return this.addComponent(classRef); + } + return this._componentMap[classRef.name] as T; + } + + public getComponent( + classRef: ComponentTypeStore + ): T | undefined { return this._componentMap[classRef.name] as T; } - // TODO: Prevent users from being able to pass in - // a component subclass here. Should we instead - // only accept subclasses and return a new component - // construction? Only problem is that this will - // require every component to have an empty constructor, - // but I think that's something I'm ok with enforcing. - // Components should be dumb structs, and nothing else. - // If we go that route, we should also start returning - // the new component here. - public addComponent = (component: Component) => { + public addComponent(classRef: ComponentTypeStore): T { this.assertManagerExists(); + const component = new classRef(); this._manager!.entityAddComponent(this, component); + return component; + } + + public addComponents = ( + ...classRefs: ComponentTypeStore[] + ): Entity => { + this.assertManagerExists(); + classRefs.forEach((clazz) => { + this._manager!.entityAddComponent(this, new clazz()); + }); return this; }; - public removeComponent(classRef: TypeStore) { + public removeComponent(classRef: ComponentTypeStore) { this.assertManagerExists(); this._manager!.entityRemoveComponent(this, classRef); } @@ -59,19 +80,23 @@ export class Entity { this._manager!.entityRemoveAllComponents(this); }; - public hasAllComponents = (...componentClasses: Function[]) => { + public hasAllComponents( + ...classRefs: ComponentTypeStore[] + ) { let hasAllComponents = true; - for (const clazz of componentClasses) { + for (const clazz of classRefs) { hasAllComponents = hasAllComponents && this.hasComponent(clazz); } return hasAllComponents; - }; + } - public hasComponent = (componentClass: Function) => { - return this._componentMap[componentClass.name] !== undefined; - }; + public hasComponent( + classRef: ComponentTypeStore + ): boolean { + return this._componentMap[classRef.name] !== undefined; + } public hasTag = (tag: string) => { return this._tags.has(tag); diff --git a/src/EntityManager.ts b/src/EntityManager.ts index a012649..0cf0cb0 100644 --- a/src/EntityManager.ts +++ b/src/EntityManager.ts @@ -1,5 +1,5 @@ import { Component } from "./Component"; -import { Entity, EntityID, TypeStore } from "./Entity"; +import { Entity, EntityID, ComponentTypeStore } from "./Entity"; interface TagPool { [tag: string]: Map; @@ -26,10 +26,22 @@ class EntityGroup implements Iterable { } } - get(id: EntityID): Entity | undefined { + get(entity: Entity): Entity | undefined { + return this.groupRef?.get(entity!.id); + } + + getById(id: EntityID): Entity | undefined { return this.groupRef?.get(id); } + has(entity: Entity): boolean { + return this.groupRef?.get(entity!.id) !== undefined; + } + + hasById(id: EntityID): boolean { + return this.groupRef?.get(id) !== undefined; + } + size() { return this.groupRef?.size ?? 0; } @@ -150,9 +162,7 @@ export class EntityManager { // Only add this entity to a group index if this component is in the group, // this entity has all the components of the group, and its not already in // the index. - const componentIsInGroup = group.componentClasses.includes( - component.constructor - ); + const componentIsInGroup = group.hasComponent(component.constructor); const entityHasAllComponents = entity.hasAllComponents( ...group.componentClasses ); @@ -180,7 +190,7 @@ export class EntityManager { public entityRemoveComponent( entity: Entity, - classRef: TypeStore + classRef: ComponentTypeStore ) { if (!entity._componentMap[classRef.name]) return; @@ -199,7 +209,9 @@ export class EntityManager { delete entity._componentMap[classRef.name]; } - public queryComponents = (...componentClasses: Function[]) => { + public queryComponents = ( + ...componentClasses: ComponentTypeStore[] + ) => { const group = this.groups.get(this.groupKey(componentClasses)) ?? this.indexGroup(componentClasses); @@ -209,7 +221,9 @@ export class EntityManager { public count = () => this.entities.size; - private indexGroup = (componentClasses: Function[]): Group => { + private indexGroup = ( + componentClasses: ComponentTypeStore[] + ): Group => { const key = this.groupKey(componentClasses); if (this.groups.has(key)) { @@ -251,10 +265,22 @@ export class EntityManager { } export class Group { - public componentClasses: Function[]; + public componentClasses: ComponentTypeStore[]; public entities: Map; - constructor(componentClasses: Function[]) { + public hasComponent(classRef: Function) { + let hasClass = false; + for (const clazz of this.componentClasses) { + if (clazz === classRef) { + hasClass = true; + break; + } + } + + return hasClass; + } + + constructor(componentClasses: ComponentTypeStore[]) { this.componentClasses = componentClasses; this.entities = new Map(); } diff --git a/src/__tests__/Entity.test.ts b/src/__tests__/Entity.test.ts index 9fd840b..faccccf 100644 --- a/src/__tests__/Entity.test.ts +++ b/src/__tests__/Entity.test.ts @@ -20,10 +20,9 @@ describe("Entity", () => { test("can add component to entity, retrieve it, and modifications persist", () => { const entity = entityManager.createEntity(); - const firstComponent = new FirstDummyComponent(); - firstComponent.dummyField = "dummyval1"; - entity.addComponent(firstComponent); - expect(entity.component(FirstDummyComponent)!.dummyField).toEqual( + entity.addComponents(FirstDummyComponent); + entity.component(FirstDummyComponent).dummyField = "dummyval1"; + expect(entity.component(FirstDummyComponent).dummyField).toEqual( "dummyval1" ); @@ -31,8 +30,8 @@ describe("Entity", () => { expect(entity.hasAllComponents(FirstDummyComponent)).toEqual(true); const firstComponentByRef = entity.component(FirstDummyComponent); - firstComponentByRef!.dummyField = "dummyvalnumber2"; - expect(entity.component(FirstDummyComponent)!.dummyField).toEqual( + firstComponentByRef.dummyField = "dummyvalnumber2"; + expect(entity.component(FirstDummyComponent).dummyField).toEqual( "dummyvalnumber2" ); }); @@ -43,7 +42,9 @@ describe("Entity", () => { entity.addTag("testtag"); expect(entity.hasTag("testtag")).toEqual(true); - expect(entityManager.queryTag("testtag")?.get(entity.id)).toEqual(entity); + expect(entityManager.queryTag("testtag")?.getById(entity.id)).toEqual( + entity + ); entity.removeTag("testtag"); expect(entity.hasTag("testtag")).toEqual(false); @@ -51,30 +52,27 @@ describe("Entity", () => { test("can remove component", () => { const entity = entityManager.createEntity(); - // TODO: Figure out why tf it's allowing this - //entity.addComponent(FirstDummyComponent); - const firstComponent = new FirstDummyComponent(); - entity.addComponent(firstComponent); - expect(entity.component(FirstDummyComponent)).toEqual(firstComponent); + + entity.addComponents(FirstDummyComponent); + expect(entity.getComponent(FirstDummyComponent)).not.toBeUndefined(); entity.removeComponent(FirstDummyComponent); - expect(entity.component(FirstDummyComponent)).toEqual(undefined); + expect(entity.getComponent(FirstDummyComponent)).toBeUndefined(); - entity.addComponent(firstComponent); - expect(entity.component(FirstDummyComponent)).toEqual(firstComponent); + entity.addComponents(FirstDummyComponent); + expect(entity.getComponent(FirstDummyComponent)).not.toBeUndefined(); entity.removeAllComponents(); - expect(entity.component(FirstDummyComponent)).toEqual(undefined); + expect(entity.getComponent(FirstDummyComponent)).toBeUndefined(); }); test("removing entity clears existing tags, components, and manager", () => { const entity = entityManager.createEntity(); expect(entity._manager).toBeDefined(); - const firstComponent = new FirstDummyComponent(); - expect(entity.component(FirstDummyComponent)).toEqual(undefined); - entity.addComponent(firstComponent); - expect(entity.component(FirstDummyComponent)).toEqual(firstComponent); + expect(entity.getComponent(FirstDummyComponent)).toBeUndefined(); + entity.addComponents(FirstDummyComponent); + expect(entity.getComponent(FirstDummyComponent)).not.toBeUndefined(); expect(entity.hasTag("testtag")).toEqual(false); entity.addTag("testtag"); @@ -84,7 +82,7 @@ describe("Entity", () => { expect(entity._manager).toEqual(null); // TODO: Should all entity functions throw if it has no manager? expect(entity.hasTag("testtag")).toEqual(false); - expect(entity.component(FirstDummyComponent)).toEqual(undefined); + expect(entity.getComponent(FirstDummyComponent)).toBeUndefined(); }); test("entity throws error when it has no manager", () => { @@ -101,19 +99,14 @@ describe("Entity", () => { test("can add multiple components to entity", () => { const entity = entityManager.createEntity(); - expect(entity.component(FirstDummyComponent)).toEqual(undefined); - expect(entity.component(SecondDummyComponent)).toEqual(undefined); - - const firstComponent = new FirstDummyComponent(); - const secondComponent = new SecondDummyComponent(); - - entity.addComponent(firstComponent); - entity.addComponent(secondComponent); + expect(entity.getComponent(FirstDummyComponent)).toBeUndefined(); + expect(entity.getComponent(SecondDummyComponent)).toBeUndefined(); - console.log(entityManager["groups"]); + entity.addComponents(FirstDummyComponent); + entity.addComponents(SecondDummyComponent); - expect(entity.component(FirstDummyComponent)).toEqual(firstComponent); - expect(entity.component(SecondDummyComponent)).toEqual(secondComponent); + expect(entity.getComponent(FirstDummyComponent)).not.toBeUndefined(); + expect(entity.getComponent(SecondDummyComponent)).not.toBeUndefined(); }); test("removing tag that doesnt exist shouldnt throw", () => { @@ -128,4 +121,36 @@ describe("Entity", () => { test("removing a component that an entity doesnt have shouldnt throw", () => { entityManager.createEntity().removeComponent(FirstDummyComponent); }); + + test("can add multiple components at once that are auto constructed", () => { + const entity = entityManager + .createEntity() + .addComponents(FirstDummyComponent, SecondDummyComponent); + + expect(entity.component(FirstDummyComponent)).not.toBeUndefined(); + expect(entity.component(SecondDummyComponent)).not.toBeUndefined(); + + entity.component(FirstDummyComponent).dummyField = "test"; + entity.component(SecondDummyComponent).secondDummyField = 69; + + expect(entity.component(FirstDummyComponent).dummyField).toEqual("test"); + expect(entity.component(SecondDummyComponent).secondDummyField).toEqual(69); + }); + + test("addComponent returns reference to underlying component", () => { + const entity = entityManager.createEntity(); + const dummyRef = entity.addComponent(FirstDummyComponent); + expect(entity.component(FirstDummyComponent)).toBe(dummyRef); + + dummyRef.dummyField = "test"; + expect(entity.component(FirstDummyComponent).dummyField).toBe("test"); + }); + + test("component() auto constructs missing components", () => { + const entity = entityManager.createEntity(); + expect(entity.getComponent(FirstDummyComponent)).toBeUndefined(); + entity.component(FirstDummyComponent).dummyField = "test"; + expect(entity.getComponent(FirstDummyComponent)).not.toBeUndefined(); + expect(entity.component(FirstDummyComponent).dummyField).toEqual("test"); + }); }); diff --git a/src/__tests__/EntityManager.test.ts b/src/__tests__/EntityManager.test.ts index c7f7678..b285fba 100644 --- a/src/__tests__/EntityManager.test.ts +++ b/src/__tests__/EntityManager.test.ts @@ -55,52 +55,42 @@ describe("EntityManager", () => { test("throws error when adding component type twice", () => { const entity = entityManager.createEntity(); - const firstComponent1 = new FirstDummyComponent(); - const firstComponent2 = new FirstDummyComponent(); - - entity.addComponent(firstComponent1); - expect(() => entity.addComponent(firstComponent2)).toThrow(); + entity.addComponents(FirstDummyComponent); + expect(() => entity.addComponents(FirstDummyComponent)).toThrow(); }); test("allows querying by component", () => { const entity = entityManager.createEntity(); const secondEntity = entityManager.createEntity(); - const firstComponent = new FirstDummyComponent(); - const secondComponent = new SecondDummyComponent(); - - entity.addComponent(firstComponent); - entity.addComponent(secondComponent); + entity.addComponents(FirstDummyComponent, SecondDummyComponent); - const firstComponent2 = new FirstDummyComponent(); - - secondEntity.addComponent(firstComponent2); + secondEntity.addComponents(FirstDummyComponent); const firstQuery = entityManager.queryComponents( FirstDummyComponent, SecondDummyComponent ); - expect(firstQuery.get(entity.id)).toEqual(entity); - expect(firstQuery.get(secondEntity.id)).toBeUndefined(); + expect(firstQuery.getById(entity.id)).toEqual(entity); + expect(firstQuery.getById(secondEntity.id)).toBeUndefined(); const secondQuery = entityManager.queryComponents(FirstDummyComponent); - expect(secondQuery.get(entity.id)).toEqual(entity); - expect(secondQuery.get(secondEntity.id)).toEqual(secondEntity); + expect(secondQuery.getById(entity.id)).toEqual(entity); + expect(secondQuery.getById(secondEntity.id)).toEqual(secondEntity); const thirdQuery = entityManager.queryComponents(SecondDummyComponent); - expect(thirdQuery.get(entity.id)).toEqual(entity); - expect(thirdQuery.get(secondEntity.id)).toBeUndefined(); + expect(thirdQuery.getById(entity.id)).toEqual(entity); + expect(thirdQuery.getById(secondEntity.id)).toBeUndefined(); }); test("clears entities out of component groups when a component is removed", () => { const entity = entityManager.createEntity(); - const firstComponent = new FirstDummyComponent(); - entity.addComponent(firstComponent); + entity.addComponents(FirstDummyComponent); expect(entityManager["groups"].size).toEqual(0); expect( - entityManager.queryComponents(FirstDummyComponent).get(entity.id) + entityManager.queryComponents(FirstDummyComponent).getById(entity.id) ).toEqual(entity); expect(entityManager["groups"].size).toEqual(1); @@ -113,21 +103,21 @@ describe("EntityManager", () => { test("adds components to existing groups", () => { const entity = entityManager.createEntity(); - entity.addComponent(new FirstDummyComponent()); + entity.addComponents(FirstDummyComponent); expect(entityManager["groups"].size).toBe(0); // Create group const query = entityManager.queryComponents(FirstDummyComponent); expect(query.size()).toBe(1); - expect(query.get(entity.id)).toBe(entity); + expect(query.getById(entity.id)).toBe(entity); expect(entityManager["groups"].size).toBe(1); const secondEntity = entityManager.createEntity(); // Should reuse group - secondEntity.addComponent(new FirstDummyComponent()); + secondEntity.addComponents(FirstDummyComponent); expect(entityManager["groups"].size).toBe(1); - expect(query.get(secondEntity.id)).toBe(secondEntity); + expect(query.getById(secondEntity.id)).toBe(secondEntity); }); test("retagging entity should do nothing", () => { @@ -135,10 +125,10 @@ describe("EntityManager", () => { expect(entityManager.queryTag("testtag").size()).toBe(0); entity.addTag("testtag"); - expect(entityManager.queryTag("testtag").get(entity.id)).toBe(entity); + expect(entityManager.queryTag("testtag").getById(entity.id)).toBe(entity); // Should do nothing entity.addTag("testtag"); - expect(entityManager.queryTag("testtag").get(entity.id)).toBe(entity); + expect(entityManager.queryTag("testtag").getById(entity.id)).toBe(entity); }); test("removing entities by tag that doesnt exist should be fine", () => { @@ -161,7 +151,7 @@ describe("EntityManager", () => { test("impossible normally: make sure indexGroup double calls dont overwrite groups", () => { const entity = entityManager.createEntity(); - entity.addComponent(new FirstDummyComponent()); + entity.addComponents(FirstDummyComponent); expect(entityManager["groups"].size).toBe(0); // Create group @@ -181,8 +171,8 @@ describe("EntityManager", () => { const entity1 = entityManager.createEntity(); const entity2 = entityManager.createEntity(); - entity1.addComponent(new FirstDummyComponent()); - entity2.addComponent(new FirstDummyComponent()); + entity1.addComponents(FirstDummyComponent); + entity2.addComponents(FirstDummyComponent); const group = entityManager.queryComponents(FirstDummyComponent); let count = 0; @@ -196,19 +186,19 @@ describe("EntityManager", () => { const componentQuery = entityManager.queryComponents(FirstDummyComponent); expect(componentQuery.size()).toBe(0); - expect(componentQuery.get(0)).toBeUndefined(); + expect(componentQuery.getById(0)).toBeUndefined(); // Tag queries don't always return a group which is why we // use optional chaining in the EntityGroup const tagQuery = entityManager.queryTag("nonexistent"); expect(tagQuery.size()).toBe(0); - expect(tagQuery.get(0)).toBeUndefined(); + expect(tagQuery.getById(0)).toBeUndefined(); }); test("forEach allows us to iterate over entities", () => { const entity = entityManager.createEntity(); - entity.addComponent(new FirstDummyComponent()); + entity.addComponents(FirstDummyComponent); const group = entityManager.queryComponents(FirstDummyComponent); group.forEach((groupEntity) => { @@ -225,4 +215,30 @@ describe("EntityManager", () => { expect(() => tagQuery.forEach(() => {})).not.toThrow(); expect(() => tagQuery.toArray()).not.toThrow(); }); + + test("get/has entity work as expected", () => { + const entity = entityManager.createEntity(); + entity.addComponents(FirstDummyComponent); + + const group = entityManager.queryComponents(FirstDummyComponent); + expect(group.has(entity)).toBe(true); + expect(group.get(entity)).toBe(entity); + + const tagGroup = entityManager.queryTag("nonexistent"); + expect(tagGroup.get(entity)).toBe(undefined); + expect(tagGroup.has(entity)).toBe(false); + }); + + test("get/has by id work as expected", () => { + const entity = entityManager.createEntity(); + entity.addComponents(FirstDummyComponent); + + const group = entityManager.queryComponents(FirstDummyComponent); + expect(group.hasById(entity.id)).toBe(true); + expect(group.getById(entity.id)).toBe(entity); + + const tagGroup = entityManager.queryTag("nonexistent"); + expect(tagGroup.getById(entity.id)).toBe(undefined); + expect(tagGroup.hasById(entity.id)).toBe(false); + }); });