Skip to content

Commit

Permalink
Typesafe API for defining Components and creating Entities
Browse files Browse the repository at this point in the history
  • Loading branch information
anderoonies committed Jul 11, 2021
1 parent ed9c34c commit a52a2a2
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 190 deletions.
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 `typeof properties === {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.
25 changes: 25 additions & 0 deletions docs/World.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,31 @@ 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 take `components`, only `c`.
The `type` of the `Component` is used to check the types of the initial arguments.

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

const playerEntity = world.createEntityTypesafe({
id: 'Player', // optional
tags: ['Character', 'Visible'], //optional
c: [ // optional
{
type: Position,
x: 15
},
{
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
60 changes: 49 additions & 11 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,15 @@ export interface IComponentUpdate {
[others: string]: any;
}

// in order to reference the class rather than the instance
interface ComponentClass {
new (): Component;
}
type DefaultProperties = Record<string, any>;
type Constructor<T> = new (...args: any[]) => T;
type ComponentClass<T = DefaultProperties> = ClassType<Component<T>>;
type ClassType<T> = { new (): T };
export function TypedComponent<TProperties extends DefaultProperties>(
properties: TProperties
): ClassType<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 +111,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 +192,22 @@ export interface IEntityObject {
// export interface IWorldSubscriptions {
// [name: string]: System;
// }
type TypedComponentConfig<T = any> = T extends Component<infer TProperties>
? {
type: ClassType<T>;
key?: string;
} & TProperties
: never;

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

export declare class Entity {
types: IEntityByType;
Expand All @@ -208,6 +228,9 @@ export declare class Entity {
addComponent(
properties: IComponentConfig | IComponentObject
): Component | undefined;
addTypedComponent<T extends Component>(
properties: TypedComponentConfig<T>
): Component | undefined;
removeComponent(component: Component | string): boolean;
getObject(componentIds?: boolean): IEntityObject;
destroy(): void;
Expand All @@ -228,6 +251,18 @@ export interface IEntityConfig {
c?: IComponentConfigValObject;
}

export type TypedEntityConfig<TComponents extends Component[] = []> = {
id?: string;
tags?: string[];
c?: {
[TComponent in keyof TComponents]: TComponents[TComponent] extends Component<
infer TProperties
>
? TypedComponentConfigVal<TComponents[TComponent]>
: never;
};
};

export interface IPoolStat {
active: number;
pooled: number;
Expand Down Expand Up @@ -257,7 +292,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 +301,9 @@ export declare class World {
logStats(freq: number, callback?: Function): void;

createEntity(definition: IEntityConfig | IEntityObject): Entity;
createEntityTypesafe<T extends Array<Component>>(
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,
c: definition.c.map((c) => {
return { ...c, type: c.type.name };
})
};
return this.createEntity(definition);
}

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

0 comments on commit a52a2a2

Please sign in to comment.