Skip to content

Commit

Permalink
Refactor API (#12)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hak33m16 authored Mar 6, 2022
1 parent 0a17002 commit fee3519
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 119 deletions.
88 changes: 66 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -51,74 +51,118 @@ 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

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.
Expand All @@ -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) {
...
});
}
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trecs",
"version": "0.1.1",
"version": "0.2.0",
"description": "A TypeScript-first Entity Component System library.",
"keywords": [
"component",
Expand Down
6 changes: 5 additions & 1 deletion src/Component.ts
Original file line number Diff line number Diff line change
@@ -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;
}
63 changes: 44 additions & 19 deletions src/Entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Component } from "./Component";
import { EntityManager } from "./EntityManager";

export interface TypeStore<T> extends Function {
export interface ComponentTypeStore<T> extends Function {
__uniqueComponentProperty: any;
new (...args: any[]): T;
}

Expand Down Expand Up @@ -29,27 +30,47 @@ export class Entity {
this._componentMap = {};
}

public component<T extends Component>(classRef: TypeStore<T>): 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<T extends Component>(classRef: ComponentTypeStore<T>): T {
if (!this.hasComponent(classRef)) {
return this.addComponent(classRef);
}
return this._componentMap[classRef.name] as T;
}

public getComponent<T extends Component>(
classRef: ComponentTypeStore<T>
): 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<T extends Component>(classRef: ComponentTypeStore<T>): T {
this.assertManagerExists();
const component = new classRef();
this._manager!.entityAddComponent(this, component);
return component;
}

public addComponents = (
...classRefs: ComponentTypeStore<Component>[]
): Entity => {
this.assertManagerExists();
classRefs.forEach((clazz) => {
this._manager!.entityAddComponent(this, new clazz());
});

return this;
};

public removeComponent<T extends Component>(classRef: TypeStore<T>) {
public removeComponent<T extends Component>(classRef: ComponentTypeStore<T>) {
this.assertManagerExists();
this._manager!.entityRemoveComponent(this, classRef);
}
Expand All @@ -59,19 +80,23 @@ export class Entity {
this._manager!.entityRemoveAllComponents(this);
};

public hasAllComponents = (...componentClasses: Function[]) => {
public hasAllComponents<T extends Component>(
...classRefs: ComponentTypeStore<T>[]
) {
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<T extends Component>(
classRef: ComponentTypeStore<T>
): boolean {
return this._componentMap[classRef.name] !== undefined;
}

public hasTag = (tag: string) => {
return this._tags.has(tag);
Expand Down
46 changes: 36 additions & 10 deletions src/EntityManager.ts
Original file line number Diff line number Diff line change
@@ -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<EntityID, Entity>;
Expand All @@ -26,10 +26,22 @@ class EntityGroup implements Iterable<Entity> {
}
}

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;
}
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -180,7 +190,7 @@ export class EntityManager {

public entityRemoveComponent<T extends Component>(
entity: Entity,
classRef: TypeStore<T>
classRef: ComponentTypeStore<T>
) {
if (!entity._componentMap[classRef.name]) return;

Expand All @@ -199,7 +209,9 @@ export class EntityManager {
delete entity._componentMap[classRef.name];
}

public queryComponents = (...componentClasses: Function[]) => {
public queryComponents = (
...componentClasses: ComponentTypeStore<Component>[]
) => {
const group =
this.groups.get(this.groupKey(componentClasses)) ??
this.indexGroup(componentClasses);
Expand All @@ -209,7 +221,9 @@ export class EntityManager {

public count = () => this.entities.size;

private indexGroup = (componentClasses: Function[]): Group => {
private indexGroup = (
componentClasses: ComponentTypeStore<Component>[]
): Group => {
const key = this.groupKey(componentClasses);

if (this.groups.has(key)) {
Expand Down Expand Up @@ -251,10 +265,22 @@ export class EntityManager {
}

export class Group {
public componentClasses: Function[];
public componentClasses: ComponentTypeStore<Component>[];
public entities: Map<EntityID, Entity>;

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<Component>[]) {
this.componentClasses = componentClasses;
this.entities = new Map();
}
Expand Down
Loading

0 comments on commit fee3519

Please sign in to comment.