Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typesafe API for defining Components and creating Entities #72

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/Component.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,22 @@ Position.properties = {
coord: '0x0'
};
```

# TypedComponent

There's an additional API for creating typed Components, which have typed `.properties` defined for the Component class and fields of those types on the instances. This uses the mixin pattern in TypeScript.

To create a `TypedComponent`:

```ts
class Position extends ApeECS.TypedComponent({x: 0, y: 0}) {};
```

This creates a `Position` class with properties typed `{x: number, y: number}`.
A `TypedComponent` can have `properties` typed as a superset of the initial properties. For example:

```ts
class Position extends ApeECS.TypedComponent<{x: number, y?: number}>({x: 0}) {};
```

These types are used in the [world.createEntityTypesafe](./World.md#createEntityTypesafe) API and [entity.addComponent](./Entity.md#addComponent).
10 changes: 10 additions & 0 deletions docs/Entity.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ entity.addComponent({

Setting a key makes the `Component` instance accessible as a property of the `Entity`.

👆 Using the api `addComponent({type: Point})` (using the `Point` class rather than a string) will enforce type checking for that component in TypeScript.
```ts
entity.addComponent({
type: Point,
x: 123,
y: 'three',
// ^ error
})
```

💭 It can sometimes be useful to set a custom id for an `Entity`, but there may not be a valid usecase for a new `Component`. You should generally only specify the `id` in `addComponent` if you're restoring a previous `Component` from `getObject`.

👀 See [world.createEntity](./World.md#createEntity) for another perspective on `Component` instance definitions.
Expand Down
31 changes: 31 additions & 0 deletions docs/World.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,37 @@ const playerEntity = world.createEntity({

💭 **Ape ECS** uses a very fast unique id generator for `Components` and `Entities` if you don't specify a given id upon creation. Look at the code in [src/util.js](../src/util.js).

## createEntityTypesafe

Create a new Entity, including its type-checked `Components`. This API is slightly reduced from `createEntity`, in that it does not `c`.
The `type` of the `Component` is used to check the types of the initial arguments, and `type` must be a Component class.

```ts
class Position extends TypedComponent<{x: number, y?: number}> {};
class Texture extends TypedComponent<{filePath: string}> {};
class Flag extends TypedComponent() {};

const playerEntity = world.createEntityTypesafe({
id: 'Player', // optional
tags: ['Character', 'Visible'], //optional
components: [ // optional
{
type: Flag,
},
{
type: Position,
x: 15,
z: 1
// ^ errors
},
{
type: Texture,
filePath: "/assets/img.png",
}
]
});
```

## getObject

Retrieves a serializable object that includes all of the Entities and their Components in the World.
Expand Down
22 changes: 11 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"url": "https://github.com/fritzy/ape-ecs/issues"
},
"homepage": "https://github.com/fritzy/ape-ecs#readme",
"dependencies": {},
"devDependencies": {
"@hapi/eslint-plugin-hapi": "^4.3.5",
"@types/chai": "^4.2.12",
Expand All @@ -42,7 +41,7 @@
"markdown-link-check": "^3.8.3",
"mocha": "^8.1.2",
"nyc": "^15.1.0",
"prettier": "^2.2.0",
"prettier": "^2.3.2",
"ts-node": "^9.0.0",
"typescript": "^4.0.2",
"webpack": "^4.43.0",
Expand Down
6 changes: 4 additions & 2 deletions src/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ class Entity {
}

addComponent(properties) {
const type = properties.type;
let type = properties.type;
if (typeof type !== 'string') {
type = type.name;
}
const pool = this.world.componentPool.get(type);
if (pool === undefined) {
throw new Error(`Component "${type}" has not been registered.`);
Expand Down Expand Up @@ -163,7 +166,6 @@ class Entity {
}

destroy() {

if (this.destroyed) return;
if (this.world.refs[this.id]) {
for (const ref of this.world.refs[this.id]) {
Expand Down
52 changes: 39 additions & 13 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ export declare class Query {
trackAdded: boolean;
trackRemoved: boolean;
from(...entities: (Entity | string)[]): Query;
fromReverse<T extends typeof Component>(
fromReverse<T extends ComponentClass>(
entity: Entity | string,
componentName: string | T
): Query;
fromAll(...types: (string | (new () => Component))[]): Query;
fromAny(...types: (string | (new () => Component))[]): Query;
not(...types: (string | (new () => Component))[]): Query;
only(...types: (string | (new () => Component))[]): Query;
fromAll(...types: (string | ComponentClass)[]): Query;
fromAny(...types: (string | ComponentClass)[]): Query;
not(...types: (string | ComponentClass)[]): Query;
only(...types: (string | ComponentClass)[]): Query;
persist(trackAdded?: boolean, trackRemoved?: boolean): Query;
refresh(): Query;
execute(filter?: IQueryExecuteConfig): Set<Entity>;
Expand All @@ -90,12 +90,14 @@ export interface IComponentUpdate {
[others: string]: any;
}

// in order to reference the class rather than the instance
interface ComponentClass {
new (): Component;
}
type DefaultProperties = {};
type Constructor<T> = { new (...args: any[]): T };
type ComponentClass<T = DefaultProperties> = Constructor<Component<T>>;
export function TypedComponent<TProperties extends DefaultProperties>(
properties?: TProperties
): Constructor<Component<TProperties>> & Constructor<Component & TProperties>;

export declare class Component {
export declare class Component<TProperties extends DefaultProperties = {}> {
preInit(initial: any): any;
init(initial: any): void;
get type(): string;
Expand All @@ -108,6 +110,7 @@ export declare class Component {
entity: Entity;
id: string;
update(values?: IComponentUpdate): void;
properties: TProperties;
[name: string]: any;
static properties: Object;
static serialize: Boolean;
Expand Down Expand Up @@ -188,6 +191,16 @@ export interface IEntityObject {
// export interface IWorldSubscriptions {
// [name: string]: System;
// }
type TypedComponentConfig<T> = T extends Component<infer TProperties>
? {
type: Constructor<T>;
key?: string;
} & TProperties
: never;

export type TypedComponentConfigVal<T> = T extends Component<infer TProperties>
? { type: Constructor<T>; id?: string; entity?: string } & TProperties
: never;

export declare class Entity {
types: IEntityByType;
Expand All @@ -205,8 +218,10 @@ export declare class Entity {
getComponents<T extends Component>(type: { new (): T }): Set<T>;
addTag(tag: string): void;
removeTag(tag: string): void;
addComponent(
properties: IComponentConfig | IComponentObject
addComponent<T>(
properties: T extends Component<infer TProperties>
? TypedComponentConfig<T>
: IComponentConfig | IComponentObject
): Component | undefined;
removeComponent(component: Component | string): boolean;
getObject(componentIds?: boolean): IEntityObject;
Expand All @@ -228,6 +243,14 @@ export interface IEntityConfig {
c?: IComponentConfigValObject;
}

export type TypedEntityConfig<TComponents extends readonly any[]> = {
id?: string;
tags?: string[];
components: {
[K in keyof TComponents]: TypedComponentConfigVal<TComponents[K]>;
};
};

export interface IPoolStat {
active: number;
pooled: number;
Expand Down Expand Up @@ -257,7 +280,7 @@ export declare class World {
registerTags(...tags: string[]): void;

// Both options allow the passing of a class that extends Component
registerComponent<T extends typeof Component>(
registerComponent<T extends ComponentClass | string>(
klass: T,
spinup?: number
): void;
Expand All @@ -266,6 +289,9 @@ export declare class World {
logStats(freq: number, callback?: Function): void;

createEntity(definition: IEntityConfig | IEntityObject): Entity;
createEntityTypesafe<T extends readonly any[]>(
definition: TypedEntityConfig<[...T]>
): Entity;
getObject(): IEntityObject[];
createEntities(definition: IEntityConfig[] | IEntityObject[]): void;
copyTypes(world: World, types: string[]): void;
Expand Down
11 changes: 10 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
const { EntityRef, EntitySet, EntityObject } = require('./entityrefs');
const Component = require('./component');

function TypedComponent(props) {
const typedClass = class TypedComponent extends Component {};
typedClass.properties = { ...props };
return typedClass;
}

module.exports = {
World: require('./world'),
System: require('./system'),
Component: require('./component'),
Entity: require('./entity'),
Component,
TypedComponent,
EntityRef,
EntitySet,
EntityObject
Expand Down
10 changes: 10 additions & 0 deletions src/world.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ module.exports = class World {
this.componentPool.set(name, new ComponentPool(this, name, spinup));
}

createEntityTypesafe(definition) {
definition = {
...definition,
components: definition.components.map((c) => {
return { ...c, type: c.type.name };
})
};
return this.createEntity(definition);
}

createEntity(definition) {
return this.entityPool.get(definition);
}
Expand Down
Loading