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

✨ classic: added definePrefab and registerPrefab #138

Merged
merged 8 commits into from
May 29, 2024
5 changes: 5 additions & 0 deletions packages/classic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"name": "Kris Baumgartner",
"email": "kjbaumgartner@gmail.com",
"url": "https://github.com/krispya"
},
{
"name": "Pietro Paolo Vismara",
"email": "pp.vismara@gmail.com",
"url": "https://github.com/pietrovismara"
}
],
"scripts": {
Expand Down
60 changes: 38 additions & 22 deletions packages/classic/src/component/Component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { queryAddEntity, queryRemoveEntity, queryCheckEntity, query, removeQuery } from '../query/Query.js';
import {
queryAddEntity,
queryRemoveEntity,
queryCheckEntity,
query,
removeQuery,
} from '../query/Query.js';
import { $bitflag, $size } from '../world/symbols.js';
import { $entityMasks, $entityComponents } from '../entity/symbols.js';
import { Component, ComponentNode, ComponentType } from './types.js';
Expand All @@ -11,8 +17,15 @@ import { Schema } from '../storage/types.js';
import { createStore, resetStoreFor } from '../storage/Storage.js';
import { Prefab, getEntityComponents, getGlobalSize } from '../entity/Entity.js';
import { IsA, Pair, Wildcard, getRelationTargets } from '../relation/Relation.js';
import { $isPairComponent, $relation, $pairTarget, $relationTargetEntities, $exclusiveRelation } from '../relation/symbols.js';

import {
$isPairComponent,
$relation,
$pairTarget,
$relationTargetEntities,
$exclusiveRelation,
} from '../relation/symbols.js';
import { PrefabToken } from '../prefab/types.js';
import { $worldToPrefab } from '../prefab/symbols.js';

/**
* Defines a new component store.
Expand Down Expand Up @@ -103,29 +116,32 @@ export const hasComponent = (world: World, component: Component, eid: number): b
return (mask & bitflag) === bitflag;
};

const recursivelyInherit = (world: World, baseEid: number, inheritedEid: number) => {
// inherit type
addComponent(world, IsA(inheritedEid), baseEid)
const recursivelyInherit = (world: World, baseEid: number, inheritedEid: number | PrefabToken) => {
if (inheritedEid instanceof Object) {
inheritedEid = inheritedEid[$worldToPrefab].get(world)!;
}

// inherit type
addComponent(world, IsA(inheritedEid), baseEid);
// inherit components
const components = getEntityComponents(world, inheritedEid)
const components = getEntityComponents(world, inheritedEid);
for (const component of components) {
if (component === Prefab) {
continue
continue;
}
addComponent(world, component, baseEid)
addComponent(world, component, baseEid);
// TODO: inherit values for structs other than SoA
const keys = Object.keys(component)
const keys = Object.keys(component);
for (const key of keys) {
component[key][baseEid] = component[key][inheritedEid]
component[key][baseEid] = component[key][inheritedEid];
}
}

const inheritedTargets = getRelationTargets(world, IsA, inheritedEid)
const inheritedTargets = getRelationTargets(world, IsA, inheritedEid);
for (const inheritedEid2 of inheritedTargets) {
recursivelyInherit(world, baseEid, inheritedEid2)
recursivelyInherit(world, baseEid, inheritedEid2);
}
}
};

/**
* Adds a component to an entity
Expand Down Expand Up @@ -158,7 +174,7 @@ export const addComponent = (world: World, component: Component, eid: number, re
// Remove this entity from toRemove if it exists in this query.
queryNode.toRemove.remove(eid);
const match = queryCheckEntity(world, queryNode, eid);

if (match) queryAddEntity(queryNode, eid);
else queryRemoveEntity(world, queryNode, eid);
});
Expand All @@ -177,22 +193,22 @@ export const addComponent = (world: World, component: Component, eid: number, re
addComponent(world, Pair(relation, Wildcard), eid);
const target = component[$pairTarget];
addComponent(world, Pair(Wildcard, target), eid);

// if it's an exclusive relation, remove the old target
if (relation[$exclusiveRelation] === true) {
const oldTarget = getRelationTargets(world, relation, eid)[0];
removeComponent(world, relation(oldTarget), eid);
}

// mark entity as a relation target
world[$relationTargetEntities].add(target)
world[$relationTargetEntities].add(target);

// if it's the IsA relation, add the inheritance chain of relations
if (relation === IsA) {
// recursively travel up the chain of relations
const inheritedTargets = getRelationTargets(world, IsA, eid)
const inheritedTargets = getRelationTargets(world, IsA, eid);
for (const inherited of inheritedTargets) {
recursivelyInherit(world, eid, inherited)
recursivelyInherit(world, eid, inherited);
}
}
}
Expand Down Expand Up @@ -253,7 +269,7 @@ export const removeComponent = (world: World, component: Component, eid: number,
if (component[$isPairComponent]) {
// check if eid is still a subject of any relation or not
if (query(world, [Wildcard(eid)]).length === 0) {
world[$relationTargetEntities].remove(eid)
world[$relationTargetEntities].remove(eid);
// TODO: cleanup query by hash
// removeQueryByHash(world, [Wildcard(eid)])
}
Expand All @@ -263,8 +279,8 @@ export const removeComponent = (world: World, component: Component, eid: number,
removeComponent(world, Pair(Wildcard, target), eid);

// remove wildcard relation if eid has no other relations
const relation = component[$relation]
const otherTargets = getRelationTargets(world, relation, eid)
const relation = component[$relation];
const otherTargets = getRelationTargets(world, relation, eid);
if (otherTargets.length === 0) {
removeComponent(world, Pair(relation, Wildcard), eid);
}
Expand Down
8 changes: 0 additions & 8 deletions packages/classic/src/entity/Entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,7 @@ export const flushRemovedEntities = (world: World) => {
recycled.length = 0;
};

// TODO: definePrefab?
export const Prefab = defineComponent();
export const addPrefab = (world: World) => {
const eid = addEntity(world);

addComponent(world, Prefab, eid);

return eid;
};

/**
* Adds a new entity to the specified world.
Expand Down
5 changes: 4 additions & 1 deletion packages/classic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export {
worlds,
} from './world/World.js';
export {
addPrefab,
addEntity,
removeEntity,
setDefaultSize,
Expand Down Expand Up @@ -51,6 +50,7 @@ export { parentArray } from './storage/Storage.js';
export { TYPES_ENUM as Types } from './constants/Constants.js';
export { pipe } from './utils/pipe.js';
export * from './relation/Relation.js';
export * from './prefab/Prefab.js';

// Types
export * from './component/types.js';
Expand All @@ -61,6 +61,7 @@ export * from './system/types.js';
export * from './world/types.js';
export * from './utils/types.js';
export * from './relation/types.js';
export * from './prefab/types.js';

// Symbols
import * as worldSymbols from './world/symbols.js';
Expand All @@ -69,6 +70,7 @@ import * as componentSymbols from './component/symbols.js';
import * as querySymbols from './query/symbols.js';
import * as storageSymbols from './storage/symbols.js';
import * as relationSymbols from './relation/symbols.js';
import * as prefabSymbols from './prefab/symbols.js';
import { archetypeHash } from './query/utils.js';

export const SYMBOLS = {
Expand All @@ -78,4 +80,5 @@ export const SYMBOLS = {
...querySymbols,
...storageSymbols,
...relationSymbols,
...prefabSymbols,
};
43 changes: 43 additions & 0 deletions packages/classic/src/prefab/Prefab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { addComponent, addComponents } from '../component/Component';
import { Component } from '../component/types';
import { defineHiddenProperties } from '../utils/defineHiddenProperty';
import { World } from '../world/types';
import { Prefab, addEntity } from '../entity/Entity';
import { $prefabComponents, $worldToPrefab } from './symbols';
import { PrefabToken } from './types';

export const definePrefab = (components: Component[] = []) => {
const prefab = {};

defineHiddenProperties(prefab, {
[$prefabComponents]: components,
[$worldToPrefab]: new Map(),
});

return prefab as PrefabToken;
};

export const registerPrefab = (world: World, prefab: PrefabToken) => {
if (prefab[$worldToPrefab].has(world)) {
return prefab[$worldToPrefab].get(world)!;
}

const eid = addPrefab(world);

addComponents(world, prefab[$prefabComponents], eid);

prefab[$worldToPrefab].set(world, eid);

return eid;
};

export const registerPrefabs = (world: World, prefabs: PrefabToken[]) =>
prefabs.map((prefab) => registerPrefab(world, prefab));

export const addPrefab = (world: World) => {
const eid = addEntity(world);

addComponent(world, Prefab, eid);

return eid;
};
2 changes: 2 additions & 0 deletions packages/classic/src/prefab/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const $prefabComponents = Symbol('prefabComponents');
export const $worldToPrefab = Symbol('worldToPrefab');
8 changes: 8 additions & 0 deletions packages/classic/src/prefab/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component } from '../component/types';
import { World } from '../world/types';
import { $prefabComponents, $worldToPrefab } from './symbols';

export type PrefabToken = {
[$prefabComponents]: Component[];
[$worldToPrefab]: Map<World, number>;
};
125 changes: 78 additions & 47 deletions packages/classic/src/relation/Relation.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,87 @@
import { Schema, World, entityExists, getEntityComponents, removeEntity } from "..";
import { ComponentType, defineComponent } from "..";
import { $schema } from "../component/symbols";
import { defineHiddenProperty } from "../utils/defineHiddenProperty";
import { $pairsMap, $isPairComponent, $relation, $pairTarget, $onTargetRemoved, $autoRemoveSubject, $exclusiveRelation } from "./symbols";
import { RelationOptions, RelationType } from "./types";
import { Schema, World, entityExists, getEntityComponents, registerPrefab, removeEntity } from '..';
import { ComponentType, defineComponent } from '..';
import { $schema } from '../component/symbols';
import { $worldToPrefab } from '../prefab/symbols';
import { defineHiddenProperty } from '../utils/defineHiddenProperty';
import {
$pairsMap,
$isPairComponent,
$relation,
$pairTarget,
$onTargetRemoved,
$autoRemoveSubject,
$exclusiveRelation,
} from './symbols';
import { RelationOptions, RelationTarget, RelationType } from './types';

function createOrGetRelationComponent<T extends Schema>(relation: (target: number | string) => ComponentType<T>, schema: T, pairsMap: Map<any, any>, target: string | number) {
if (!pairsMap.has(target)) {
const component = defineComponent(schema) as any;
defineHiddenProperty(component, $isPairComponent, true);
defineHiddenProperty(component, $relation, relation);
defineHiddenProperty(component, $pairTarget, target);
pairsMap.set(target, component);
}
return pairsMap.get(target) as ComponentType<T>;
function createOrGetRelationComponent<T extends Schema>(
relation: (target: RelationTarget) => ComponentType<T>,
schema: T,
pairsMap: Map<any, any>,
target: RelationTarget
) {
if (!pairsMap.has(target)) {
const component = defineComponent(schema) as any;
defineHiddenProperty(component, $isPairComponent, true);
defineHiddenProperty(component, $relation, relation);
defineHiddenProperty(component, $pairTarget, target);
pairsMap.set(target, component);
}
return pairsMap.get(target) as ComponentType<T>;
}

export const defineRelation =
<T extends Schema>(schema: T = {} as T, options?: RelationOptions): RelationType<T> => {
const pairsMap = new Map();
const relation = function (target: number | string) {
if (target === undefined) throw Error("Relation target is undefined")
if (target === '*') target = Wildcard
return createOrGetRelationComponent<T>(relation, schema, pairsMap, target)
}
defineHiddenProperty(relation, $pairsMap, pairsMap)
defineHiddenProperty(relation, $schema, schema)
defineHiddenProperty(relation, $exclusiveRelation, options && options.exclusive)
defineHiddenProperty(relation, $autoRemoveSubject, options && options.autoRemoveSubject)
defineHiddenProperty(relation, $onTargetRemoved, options ? options.onTargetRemoved : undefined)
return relation as RelationType<T>
}
export const defineRelation = <T extends Schema>(
schema: T = {} as T,
options?: RelationOptions
): RelationType<T> => {
const pairsMap = new Map();
const relation = function (target: RelationTarget) {
if (target === undefined) throw Error('Relation target is undefined');
if (target === '*') target = Wildcard;
return createOrGetRelationComponent<T>(relation, schema, pairsMap, target);
};
defineHiddenProperty(relation, $pairsMap, pairsMap);
defineHiddenProperty(relation, $schema, schema);
defineHiddenProperty(relation, $exclusiveRelation, options && options.exclusive);
defineHiddenProperty(relation, $autoRemoveSubject, options && options.autoRemoveSubject);
defineHiddenProperty(relation, $onTargetRemoved, options ? options.onTargetRemoved : undefined);
return relation as RelationType<T>;
};

export const Pair = <T extends Schema>(relation: RelationType<T>, target: number | string): ComponentType<T> => {
if (relation === undefined) throw Error("Relation is undefined")
if (target === undefined) throw Error("Relation target is undefined")
if (target === '*') target = Wildcard
export const Pair = <T extends Schema>(
relation: RelationType<T>,
target: RelationTarget
): ComponentType<T> => {
if (relation === undefined) throw Error('Relation is undefined');
if (target === undefined) throw Error('Relation target is undefined');
if (target === '*') target = Wildcard;

const pairsMap = (relation as RelationType<T>)[$pairsMap]
const schema = (relation as RelationType<T>)[$schema] as T
const pairsMap = (relation as RelationType<T>)[$pairsMap];
const schema = (relation as RelationType<T>)[$schema] as T;

return createOrGetRelationComponent<T>(relation, schema, pairsMap, target)
return createOrGetRelationComponent<T>(relation, schema, pairsMap, target);
};

export const Wildcard: RelationType<any> | string = defineRelation()
export const IsA: RelationType<any> = defineRelation()
export const Wildcard: RelationType<any> | string = defineRelation();
export const IsA: RelationType<any> = defineRelation();

export const getRelationTargets = (world: World, relation:RelationType<any>, eid: number) => {
const components = getEntityComponents(world, eid);
const targets = []
for (const c of components) {
if (c[$relation] === relation && c[$pairTarget] !== Wildcard) targets.push(c[$pairTarget])
}
return targets
}
export const getRelationTargets = (world: World, relation: RelationType<any>, eid: number) => {
const components = getEntityComponents(world, eid);
const targets = [];
for (const c of components) {
if (c[$relation] === relation && c[$pairTarget] !== Wildcard) {
if (c[$pairTarget] instanceof Object) {
// It's a prefab
let eid = c[$pairTarget][$worldToPrefab].get(world);
if (eid == null) {
// The prefab was not registered yet with this world
eid = registerPrefab(world, c[$pairTarget]);
}
targets.push(eid);
} else {
targets.push(c[$pairTarget]);
}
}
}
return targets;
};
Loading