Comparing version 0.3.2-a1 to 0.3.2-a2
@@ -38,2 +38,19 @@ declare module 'bitecs' { | ||
type ArrayByType = { | ||
['bool']: boolean[]; | ||
[Types.i8]: Int8Array; | ||
[Types.ui8]: Uint8Array; | ||
[Types.ui8c]: Uint8ClampedArray; | ||
[Types.i16]: Int16Array; | ||
[Types.ui16]: Uint16Array; | ||
[Types.i32]: Int32Array; | ||
[Types.ui32]: Uint32Array; | ||
[Types.f32]: Float32Array; | ||
[Types.f64]: Float64Array; | ||
} | ||
type ComponentType<T extends ISchema> = { | ||
[key in keyof T]: T[key] extends Type ? ArrayByType[T[key]] : T[key] extends ISchema ? ComponentType<T[key]> : unknown; | ||
} | ||
interface IWorld { | ||
@@ -61,3 +78,3 @@ [key: string]: any | ||
export function createWorld (size?: number): IWorld | ||
@@ -68,3 +85,3 @@ export function addEntity (world: IWorld): number | ||
export function registerComponents (world: IWorld, components: IComponent[]): void | ||
export function defineComponent (schema: ISchema): IComponent | ||
export function defineComponent <T extends ISchema>(schema: T): ComponentType<T> | ||
export function addComponent (world: IWorld, component: IComponent, eid: number): void | ||
@@ -71,0 +88,0 @@ export function removeComponent (world: IWorld, component: IComponent, eid: number): void |
@@ -51,2 +51,3 @@ const TYPES_ENUM = { | ||
const $subarray = Symbol('subarray'); | ||
const $tagStore = Symbol('tagStore'); | ||
const $queryShadow = Symbol('queryShadow'); | ||
@@ -68,2 +69,3 @@ const $serializeShadow = Symbol('serializeShadow'); | ||
const length = store[0].length; | ||
const indexType = length < UNSIGNED_MAX.uint8 ? 'ui8' : length < UNSIGNED_MAX.uint16 ? 'ui16' : 'ui32'; | ||
const arrayCount = metadata[$storeArrayCounts][type]; | ||
@@ -80,2 +82,4 @@ const summedLength = Array(arrayCount).fill(0).reduce((a, p) => a + length, 0); // for threaded impl | ||
metadata[$storeSubarrays][type][$serializeShadow] = array.slice(0); | ||
array[$indexType] = TYPES_NAMES[indexType]; | ||
array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT; | ||
let end = 0; | ||
@@ -119,4 +123,5 @@ | ||
const resizeStore = (store, size) => { | ||
if (store[$tagStore]) return; | ||
store[$storeSize] = size; | ||
store[$storeFlattened] = []; | ||
store[$storeFlattened].length = 0; | ||
Object.keys(store[$subarrayCursors]).forEach(k => { | ||
@@ -191,5 +196,15 @@ store[$subarrayCursors][k] = 0; | ||
const createStore = (schema, size = 10000) => { | ||
const createStore = (schema, size) => { | ||
const $store = Symbol('store'); | ||
if (!schema) return {}; | ||
if (!schema || !Object.keys(schema).length) { | ||
// tag component | ||
stores[$store] = { | ||
[$storeSize]: size, | ||
[$tagStore]: true, | ||
[$storeBase]: () => stores[$store] | ||
}; | ||
return stores[$store]; | ||
} | ||
schema = JSON.parse(JSON.stringify(schema)); | ||
@@ -221,4 +236,4 @@ const arrayCounts = {}; | ||
}), {}), | ||
[$storeArrayCounts]: arrayCounts, | ||
[$storeFlattened]: [] | ||
[$storeFlattened]: [], | ||
[$storeArrayCounts]: arrayCounts | ||
}; | ||
@@ -230,2 +245,3 @@ | ||
a[k] = createTypeStore(a[k], size); | ||
createShadows(a[k]); | ||
@@ -235,3 +251,2 @@ a[k][$storeBase] = () => stores[$store]; | ||
metadata[$storeFlattened].push(a[k]); | ||
createShadows(a[k]); | ||
} else if (isArrayType(a[k])) { | ||
@@ -257,12 +272,204 @@ const [type, length] = a[k]; | ||
return stores[$store]; | ||
} // tag component | ||
} | ||
}; | ||
let resized = false; | ||
const setSerializationResized = v => { | ||
resized = v; | ||
}; | ||
stores[$store] = metadata; | ||
const canonicalize = target => { | ||
let componentProps = []; | ||
let changedProps = new Set(); | ||
stores[$store][$storeBase] = () => stores[$store]; | ||
if (Array.isArray(target)) { | ||
componentProps = target.map(p => { | ||
if (typeof p === 'function' && p.name === 'QueryChanged') { | ||
p()[$storeFlattened].forEach(prop => { | ||
changedProps.add(prop); | ||
}); | ||
return p()[$storeFlattened]; | ||
} | ||
return stores[$store]; | ||
if (Object.getOwnPropertySymbols(p).includes($storeFlattened)) { | ||
return p[$storeFlattened]; | ||
} | ||
if (Object.getOwnPropertySymbols(p).includes($storeBase)) { | ||
return p; | ||
} | ||
}).reduce((a, v) => a.concat(v), []); | ||
} | ||
return [componentProps, changedProps]; | ||
}; | ||
const defineSerializer = (target, maxBytes = 20000000) => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps, changedProps] = canonicalize(target); // TODO: calculate max bytes based on target | ||
const buffer = new ArrayBuffer(maxBytes); | ||
const view = new DataView(buffer); | ||
return ents => { | ||
if (resized) { | ||
[componentProps, changedProps] = canonicalize(target); | ||
resized = false; | ||
} | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
if (Object.getOwnPropertySymbols(ents).includes($componentMap)) { | ||
ents = ents[$entityArray]; | ||
} | ||
if (!ents.length) return; | ||
let where = 0; // iterate over component props | ||
for (let pid = 0; pid < componentProps.length; pid++) { | ||
const prop = componentProps[pid]; | ||
const diff = changedProps.has(prop); // write pid | ||
view.setUint8(where, pid); | ||
where += 1; // save space for entity count | ||
const countWhere = where; | ||
where += 4; | ||
let count = 0; // write eid,val | ||
for (let i = 0; i < ents.length; i++) { | ||
const eid = ents[i]; // skip if diffing and no change | ||
if (diff && prop[eid] === prop[$serializeShadow][eid]) { | ||
continue; | ||
} | ||
count++; // write eid | ||
view.setUint32(where, eid); | ||
where += 4; | ||
if (prop[$tagStore]) { | ||
continue; | ||
} // if property is an array | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
const type = prop[eid].constructor.name.replace('Array', ''); | ||
const indexType = prop[eid][$indexType]; | ||
const indexBytes = prop[eid][$indexBytes]; // add space for count of dirty array elements | ||
const countWhere2 = where; | ||
where += 1; | ||
let count2 = 0; // write index,value | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
const value = prop[eid][i]; | ||
if (diff && prop[eid][i] === prop[eid][$serializeShadow][i]) { | ||
continue; | ||
} // write array index | ||
view[`set${indexType}`](where, i); | ||
where += indexBytes; // write value at that index | ||
view[`set${type}`](where, value); | ||
where += prop[eid].BYTES_PER_ELEMENT; | ||
count2++; | ||
} // write total element count | ||
view[`set${indexType}`](countWhere2, count2); | ||
} else { | ||
// regular property values | ||
const type = prop.constructor.name.replace('Array', ''); // set value next [type] bytes | ||
view[`set${type}`](where, prop[eid]); | ||
where += prop.BYTES_PER_ELEMENT; // sync shadow state | ||
prop[$serializeShadow][eid] = prop[eid]; | ||
} | ||
} | ||
view.setUint32(countWhere, count); | ||
} | ||
return buffer.slice(0, where); | ||
}; | ||
}; | ||
const defineDeserializer = target => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps] = canonicalize(target); | ||
return (world, packet) => { | ||
if (resized) { | ||
[componentProps] = canonicalize(target); | ||
resized = false; | ||
} | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
const view = new DataView(packet); | ||
let where = 0; | ||
while (where < packet.byteLength) { | ||
// pid | ||
const pid = view.getUint8(where); | ||
where += 1; // entity count | ||
const entityCount = view.getUint32(where); | ||
where += 4; // typed array | ||
const ta = componentProps[pid]; // Get the properties and set the new state | ||
for (let i = 0; i < entityCount; i++) { | ||
let eid = view.getUint32(where); | ||
where += 4; // if this world hasn't seen this eid yet | ||
if (!world[$entityEnabled][eid]) { | ||
// make a new entity for the data | ||
eid = addEntity(world); | ||
} | ||
const component = ta[$storeBase](); | ||
if (!hasComponent(world, component, eid)) { | ||
addComponent(world, component, eid); | ||
} | ||
if (component[$tagStore]) { | ||
continue; | ||
} | ||
if (ArrayBuffer.isView(ta[eid])) { | ||
const array = ta[eid]; | ||
const count = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; // iterate over count | ||
for (let i = 0; i < count; i++) { | ||
const index = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; | ||
const value = view[`get${array.constructor.name.replace('Array', '')}`](where); | ||
where += array.BYTES_PER_ELEMENT; | ||
ta[eid][index] = value; | ||
} | ||
} else { | ||
const value = view[`get${ta.constructor.name.replace('Array', '')}`](where); | ||
where += ta.BYTES_PER_ELEMENT; | ||
ta[eid] = value; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
const $entityMasks = Symbol('entityMasks'); | ||
@@ -272,7 +479,8 @@ const $entityEnabled = Symbol('entityEnabled'); | ||
const $entityIndices = Symbol('entityIndices'); | ||
const NONE$1 = 2 ** 32; // need a global EID cursor which all worlds and all components know about | ||
const NONE$1 = 2 ** 32; | ||
const defaultSize = 100000; // need a global EID cursor which all worlds and all components know about | ||
// so that world entities can posess entire rows spanning all component tables | ||
let globalEntityCursor = 0; | ||
let globalSize = 10000; | ||
let globalSize = defaultSize; | ||
@@ -287,3 +495,3 @@ let resizeThreshold = () => globalSize - globalSize / 5; | ||
const enabled = world[$entityEnabled]; | ||
const eid = removed.length > 0 ? removed.shift() : globalEntityCursor++; | ||
const eid = removed.length > 0 ? removed.pop() : globalEntityCursor++; | ||
enabled[eid] = 1; | ||
@@ -300,2 +508,3 @@ world[$entityIndices][eid] = world[$entityArray].push(eid) - 1; // if data stores are 80% full | ||
resizeComponents(newSize); | ||
setSerializationResized(true); | ||
console.info(`๐พ bitECS - resizing all worlds from ${size} to ${size + amount}`); | ||
@@ -332,9 +541,2 @@ } | ||
enabled[eid] = 0; // pop swap | ||
// const index = world[$entityIndices][eid] | ||
// const swapped = world[$entityArray].pop() | ||
// if (swapped !== eid) { | ||
// world[$entityArray][index] = swapped | ||
// world[$entityIndices][swapped] = index | ||
// } | ||
// world[$entityIndices][eid] = NONE | ||
@@ -538,3 +740,4 @@ popSwap(world, eid); // Clear entity bitmasks | ||
q.entities.push(eid); | ||
q.indices[eid] = q.entities.length - 1; | ||
q.indices[eid] = q.entities.length - 1; // TODO: pop swap so dupes don't enter | ||
q.entered.push(eid); | ||
@@ -571,3 +774,4 @@ }; | ||
q.toRemove.push(eid); | ||
world[$dirtyQueries].add(q); | ||
world[$dirtyQueries].add(q); // TODO: pop swap so dupes don't enter | ||
q.exited.push(eid); | ||
@@ -582,3 +786,3 @@ }; | ||
const defineComponent = schema => { | ||
const component = createStore(schema); | ||
const component = createStore(schema, defaultSize); | ||
if (schema && Object.keys(schema).length) components.push(component); | ||
@@ -719,177 +923,2 @@ return component; | ||
const canonicalize = target => { | ||
let componentProps = []; | ||
let changedProps = new Set(); | ||
if (Array.isArray(target)) { | ||
componentProps = target.map(p => { | ||
if (typeof p === 'function' && p.name === 'QueryChanged') { | ||
p()[$storeFlattened].forEach(prop => { | ||
changedProps.add(prop); | ||
}); | ||
return p()[$storeFlattened]; | ||
} | ||
if (Object.getOwnPropertySymbols(p).includes($storeFlattened)) { | ||
return p[$storeFlattened]; | ||
} | ||
if (Object.getOwnPropertySymbols(p).includes($storeBase)) { | ||
return p; | ||
} | ||
}).reduce((a, v) => a.concat(v), []); | ||
} | ||
return [componentProps, changedProps]; | ||
}; | ||
const defineSerializer = (target, maxBytes = 20000000) => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps, changedProps] = canonicalize(target); // TODO: calculate max bytes based on target | ||
const buffer = new ArrayBuffer(maxBytes); | ||
const view = new DataView(buffer); | ||
return ents => { | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
if (Object.getOwnPropertySymbols(ents).includes($componentMap)) { | ||
ents = ents[$entityArray]; | ||
} | ||
if (!ents.length) return; | ||
let where = 0; // iterate over component props | ||
for (let pid = 0; pid < componentProps.length; pid++) { | ||
const prop = componentProps[pid]; | ||
const diff = changedProps.has(prop); // write pid | ||
view.setUint8(where, pid); | ||
where += 1; // save space for entity count | ||
const countWhere = where; | ||
where += 4; | ||
let count = 0; // write eid,val | ||
for (let i = 0; i < ents.length; i++) { | ||
const eid = ents[i]; // skip if diffing and no change | ||
if (diff && prop[eid] === prop[$serializeShadow][eid]) { | ||
continue; | ||
} | ||
count++; // write eid | ||
view.setUint32(where, eid); | ||
where += 4; // if property is an array | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
const type = prop[eid].constructor.name.replace('Array', ''); | ||
const indexType = prop[eid][$indexType]; | ||
const indexBytes = prop[eid][$indexBytes]; // add space for count of dirty array elements | ||
const countWhere2 = where; | ||
where += 1; | ||
let count2 = 0; // write index,value | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
const value = prop[eid][i]; | ||
if (diff && prop[eid][i] === prop[eid][$serializeShadow][i]) { | ||
continue; | ||
} // write array index | ||
view[`set${indexType}`](where, i); | ||
where += indexBytes; // write value at that index | ||
view[`set${type}`](where, value); | ||
where += prop[eid].BYTES_PER_ELEMENT; | ||
count2++; | ||
} // write total element count | ||
view[`set${indexType}`](countWhere2, count2); | ||
} else { | ||
// regular property values | ||
const type = prop.constructor.name.replace('Array', ''); // set value next [type] bytes | ||
view[`set${type}`](where, prop[eid]); | ||
where += prop.BYTES_PER_ELEMENT; // sync shadow state | ||
prop[$serializeShadow][eid] = prop[eid]; | ||
} | ||
} | ||
view.setUint32(countWhere, count); | ||
} | ||
return buffer.slice(0, where); | ||
}; | ||
}; | ||
const defineDeserializer = target => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps] = canonicalize(target); | ||
return (world, packet) => { | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
const view = new DataView(packet); | ||
let where = 0; | ||
while (where < packet.byteLength) { | ||
// pid | ||
const pid = view.getUint8(where); | ||
where += 1; // entity count | ||
const entityCount = view.getUint32(where); | ||
where += 4; // typed array | ||
const ta = componentProps[pid]; // Get the properties and set the new state | ||
for (let i = 0; i < entityCount; i++) { | ||
let eid = view.getUint32(where); | ||
where += 4; // if this world hasn't seen this eid yet | ||
if (!world[$entityEnabled][eid]) { | ||
// make a new entity for the data | ||
eid = addEntity(world); | ||
} | ||
const component = ta[$storeBase](); | ||
if (!hasComponent(world, component, eid)) { | ||
addComponent(world, component, eid); | ||
} | ||
if (ArrayBuffer.isView(ta[eid])) { | ||
const array = ta[eid]; | ||
const count = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; // iterate over count | ||
for (let i = 0; i < count; i++) { | ||
const index = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; | ||
const value = view[`get${array.constructor.name.replace('Array', '')}`](where); | ||
where += array.BYTES_PER_ELEMENT; | ||
ta[eid][index] = value; | ||
} | ||
} else { | ||
let value = view[`get${ta.constructor.name.replace('Array', '')}`](where); | ||
where += ta.BYTES_PER_ELEMENT; | ||
ta[eid] = value; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
const pipe = (...fns) => input => { | ||
@@ -896,0 +925,0 @@ if (!input || Array.isArray(input) && input.length === 0) return; |
@@ -55,2 +55,3 @@ 'use strict'; | ||
const $subarray = Symbol('subarray'); | ||
const $tagStore = Symbol('tagStore'); | ||
const $queryShadow = Symbol('queryShadow'); | ||
@@ -72,2 +73,3 @@ const $serializeShadow = Symbol('serializeShadow'); | ||
const length = store[0].length; | ||
const indexType = length < UNSIGNED_MAX.uint8 ? 'ui8' : length < UNSIGNED_MAX.uint16 ? 'ui16' : 'ui32'; | ||
const arrayCount = metadata[$storeArrayCounts][type]; | ||
@@ -84,2 +86,4 @@ const summedLength = Array(arrayCount).fill(0).reduce((a, p) => a + length, 0); // for threaded impl | ||
metadata[$storeSubarrays][type][$serializeShadow] = array.slice(0); | ||
array[$indexType] = TYPES_NAMES[indexType]; | ||
array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT; | ||
let end = 0; | ||
@@ -123,4 +127,5 @@ | ||
const resizeStore = (store, size) => { | ||
if (store[$tagStore]) return; | ||
store[$storeSize] = size; | ||
store[$storeFlattened] = []; | ||
store[$storeFlattened].length = 0; | ||
Object.keys(store[$subarrayCursors]).forEach(k => { | ||
@@ -195,5 +200,15 @@ store[$subarrayCursors][k] = 0; | ||
const createStore = (schema, size = 10000) => { | ||
const createStore = (schema, size) => { | ||
const $store = Symbol('store'); | ||
if (!schema) return {}; | ||
if (!schema || !Object.keys(schema).length) { | ||
// tag component | ||
stores[$store] = { | ||
[$storeSize]: size, | ||
[$tagStore]: true, | ||
[$storeBase]: () => stores[$store] | ||
}; | ||
return stores[$store]; | ||
} | ||
schema = JSON.parse(JSON.stringify(schema)); | ||
@@ -225,4 +240,4 @@ const arrayCounts = {}; | ||
}), {}), | ||
[$storeArrayCounts]: arrayCounts, | ||
[$storeFlattened]: [] | ||
[$storeFlattened]: [], | ||
[$storeArrayCounts]: arrayCounts | ||
}; | ||
@@ -234,2 +249,3 @@ | ||
a[k] = createTypeStore(a[k], size); | ||
createShadows(a[k]); | ||
@@ -239,3 +255,2 @@ a[k][$storeBase] = () => stores[$store]; | ||
metadata[$storeFlattened].push(a[k]); | ||
createShadows(a[k]); | ||
} else if (isArrayType(a[k])) { | ||
@@ -261,12 +276,204 @@ const [type, length] = a[k]; | ||
return stores[$store]; | ||
} // tag component | ||
} | ||
}; | ||
let resized = false; | ||
const setSerializationResized = v => { | ||
resized = v; | ||
}; | ||
stores[$store] = metadata; | ||
const canonicalize = target => { | ||
let componentProps = []; | ||
let changedProps = new Set(); | ||
stores[$store][$storeBase] = () => stores[$store]; | ||
if (Array.isArray(target)) { | ||
componentProps = target.map(p => { | ||
if (typeof p === 'function' && p.name === 'QueryChanged') { | ||
p()[$storeFlattened].forEach(prop => { | ||
changedProps.add(prop); | ||
}); | ||
return p()[$storeFlattened]; | ||
} | ||
return stores[$store]; | ||
if (Object.getOwnPropertySymbols(p).includes($storeFlattened)) { | ||
return p[$storeFlattened]; | ||
} | ||
if (Object.getOwnPropertySymbols(p).includes($storeBase)) { | ||
return p; | ||
} | ||
}).reduce((a, v) => a.concat(v), []); | ||
} | ||
return [componentProps, changedProps]; | ||
}; | ||
const defineSerializer = (target, maxBytes = 20000000) => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps, changedProps] = canonicalize(target); // TODO: calculate max bytes based on target | ||
const buffer = new ArrayBuffer(maxBytes); | ||
const view = new DataView(buffer); | ||
return ents => { | ||
if (resized) { | ||
[componentProps, changedProps] = canonicalize(target); | ||
resized = false; | ||
} | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
if (Object.getOwnPropertySymbols(ents).includes($componentMap)) { | ||
ents = ents[$entityArray]; | ||
} | ||
if (!ents.length) return; | ||
let where = 0; // iterate over component props | ||
for (let pid = 0; pid < componentProps.length; pid++) { | ||
const prop = componentProps[pid]; | ||
const diff = changedProps.has(prop); // write pid | ||
view.setUint8(where, pid); | ||
where += 1; // save space for entity count | ||
const countWhere = where; | ||
where += 4; | ||
let count = 0; // write eid,val | ||
for (let i = 0; i < ents.length; i++) { | ||
const eid = ents[i]; // skip if diffing and no change | ||
if (diff && prop[eid] === prop[$serializeShadow][eid]) { | ||
continue; | ||
} | ||
count++; // write eid | ||
view.setUint32(where, eid); | ||
where += 4; | ||
if (prop[$tagStore]) { | ||
continue; | ||
} // if property is an array | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
const type = prop[eid].constructor.name.replace('Array', ''); | ||
const indexType = prop[eid][$indexType]; | ||
const indexBytes = prop[eid][$indexBytes]; // add space for count of dirty array elements | ||
const countWhere2 = where; | ||
where += 1; | ||
let count2 = 0; // write index,value | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
const value = prop[eid][i]; | ||
if (diff && prop[eid][i] === prop[eid][$serializeShadow][i]) { | ||
continue; | ||
} // write array index | ||
view[`set${indexType}`](where, i); | ||
where += indexBytes; // write value at that index | ||
view[`set${type}`](where, value); | ||
where += prop[eid].BYTES_PER_ELEMENT; | ||
count2++; | ||
} // write total element count | ||
view[`set${indexType}`](countWhere2, count2); | ||
} else { | ||
// regular property values | ||
const type = prop.constructor.name.replace('Array', ''); // set value next [type] bytes | ||
view[`set${type}`](where, prop[eid]); | ||
where += prop.BYTES_PER_ELEMENT; // sync shadow state | ||
prop[$serializeShadow][eid] = prop[eid]; | ||
} | ||
} | ||
view.setUint32(countWhere, count); | ||
} | ||
return buffer.slice(0, where); | ||
}; | ||
}; | ||
const defineDeserializer = target => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps] = canonicalize(target); | ||
return (world, packet) => { | ||
if (resized) { | ||
[componentProps] = canonicalize(target); | ||
resized = false; | ||
} | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
const view = new DataView(packet); | ||
let where = 0; | ||
while (where < packet.byteLength) { | ||
// pid | ||
const pid = view.getUint8(where); | ||
where += 1; // entity count | ||
const entityCount = view.getUint32(where); | ||
where += 4; // typed array | ||
const ta = componentProps[pid]; // Get the properties and set the new state | ||
for (let i = 0; i < entityCount; i++) { | ||
let eid = view.getUint32(where); | ||
where += 4; // if this world hasn't seen this eid yet | ||
if (!world[$entityEnabled][eid]) { | ||
// make a new entity for the data | ||
eid = addEntity(world); | ||
} | ||
const component = ta[$storeBase](); | ||
if (!hasComponent(world, component, eid)) { | ||
addComponent(world, component, eid); | ||
} | ||
if (component[$tagStore]) { | ||
continue; | ||
} | ||
if (ArrayBuffer.isView(ta[eid])) { | ||
const array = ta[eid]; | ||
const count = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; // iterate over count | ||
for (let i = 0; i < count; i++) { | ||
const index = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; | ||
const value = view[`get${array.constructor.name.replace('Array', '')}`](where); | ||
where += array.BYTES_PER_ELEMENT; | ||
ta[eid][index] = value; | ||
} | ||
} else { | ||
const value = view[`get${ta.constructor.name.replace('Array', '')}`](where); | ||
where += ta.BYTES_PER_ELEMENT; | ||
ta[eid] = value; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
const $entityMasks = Symbol('entityMasks'); | ||
@@ -276,7 +483,8 @@ const $entityEnabled = Symbol('entityEnabled'); | ||
const $entityIndices = Symbol('entityIndices'); | ||
const NONE$1 = 2 ** 32; // need a global EID cursor which all worlds and all components know about | ||
const NONE$1 = 2 ** 32; | ||
const defaultSize = 100000; // need a global EID cursor which all worlds and all components know about | ||
// so that world entities can posess entire rows spanning all component tables | ||
let globalEntityCursor = 0; | ||
let globalSize = 10000; | ||
let globalSize = defaultSize; | ||
@@ -291,3 +499,3 @@ let resizeThreshold = () => globalSize - globalSize / 5; | ||
const enabled = world[$entityEnabled]; | ||
const eid = removed.length > 0 ? removed.shift() : globalEntityCursor++; | ||
const eid = removed.length > 0 ? removed.pop() : globalEntityCursor++; | ||
enabled[eid] = 1; | ||
@@ -304,2 +512,3 @@ world[$entityIndices][eid] = world[$entityArray].push(eid) - 1; // if data stores are 80% full | ||
resizeComponents(newSize); | ||
setSerializationResized(true); | ||
console.info(`๐พ bitECS - resizing all worlds from ${size} to ${size + amount}`); | ||
@@ -336,9 +545,2 @@ } | ||
enabled[eid] = 0; // pop swap | ||
// const index = world[$entityIndices][eid] | ||
// const swapped = world[$entityArray].pop() | ||
// if (swapped !== eid) { | ||
// world[$entityArray][index] = swapped | ||
// world[$entityIndices][swapped] = index | ||
// } | ||
// world[$entityIndices][eid] = NONE | ||
@@ -542,3 +744,4 @@ popSwap(world, eid); // Clear entity bitmasks | ||
q.entities.push(eid); | ||
q.indices[eid] = q.entities.length - 1; | ||
q.indices[eid] = q.entities.length - 1; // TODO: pop swap so dupes don't enter | ||
q.entered.push(eid); | ||
@@ -575,3 +778,4 @@ }; | ||
q.toRemove.push(eid); | ||
world[$dirtyQueries].add(q); | ||
world[$dirtyQueries].add(q); // TODO: pop swap so dupes don't enter | ||
q.exited.push(eid); | ||
@@ -586,3 +790,3 @@ }; | ||
const defineComponent = schema => { | ||
const component = createStore(schema); | ||
const component = createStore(schema, defaultSize); | ||
if (schema && Object.keys(schema).length) components.push(component); | ||
@@ -723,177 +927,2 @@ return component; | ||
const canonicalize = target => { | ||
let componentProps = []; | ||
let changedProps = new Set(); | ||
if (Array.isArray(target)) { | ||
componentProps = target.map(p => { | ||
if (typeof p === 'function' && p.name === 'QueryChanged') { | ||
p()[$storeFlattened].forEach(prop => { | ||
changedProps.add(prop); | ||
}); | ||
return p()[$storeFlattened]; | ||
} | ||
if (Object.getOwnPropertySymbols(p).includes($storeFlattened)) { | ||
return p[$storeFlattened]; | ||
} | ||
if (Object.getOwnPropertySymbols(p).includes($storeBase)) { | ||
return p; | ||
} | ||
}).reduce((a, v) => a.concat(v), []); | ||
} | ||
return [componentProps, changedProps]; | ||
}; | ||
const defineSerializer = (target, maxBytes = 20000000) => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps, changedProps] = canonicalize(target); // TODO: calculate max bytes based on target | ||
const buffer = new ArrayBuffer(maxBytes); | ||
const view = new DataView(buffer); | ||
return ents => { | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
if (Object.getOwnPropertySymbols(ents).includes($componentMap)) { | ||
ents = ents[$entityArray]; | ||
} | ||
if (!ents.length) return; | ||
let where = 0; // iterate over component props | ||
for (let pid = 0; pid < componentProps.length; pid++) { | ||
const prop = componentProps[pid]; | ||
const diff = changedProps.has(prop); // write pid | ||
view.setUint8(where, pid); | ||
where += 1; // save space for entity count | ||
const countWhere = where; | ||
where += 4; | ||
let count = 0; // write eid,val | ||
for (let i = 0; i < ents.length; i++) { | ||
const eid = ents[i]; // skip if diffing and no change | ||
if (diff && prop[eid] === prop[$serializeShadow][eid]) { | ||
continue; | ||
} | ||
count++; // write eid | ||
view.setUint32(where, eid); | ||
where += 4; // if property is an array | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
const type = prop[eid].constructor.name.replace('Array', ''); | ||
const indexType = prop[eid][$indexType]; | ||
const indexBytes = prop[eid][$indexBytes]; // add space for count of dirty array elements | ||
const countWhere2 = where; | ||
where += 1; | ||
let count2 = 0; // write index,value | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
const value = prop[eid][i]; | ||
if (diff && prop[eid][i] === prop[eid][$serializeShadow][i]) { | ||
continue; | ||
} // write array index | ||
view[`set${indexType}`](where, i); | ||
where += indexBytes; // write value at that index | ||
view[`set${type}`](where, value); | ||
where += prop[eid].BYTES_PER_ELEMENT; | ||
count2++; | ||
} // write total element count | ||
view[`set${indexType}`](countWhere2, count2); | ||
} else { | ||
// regular property values | ||
const type = prop.constructor.name.replace('Array', ''); // set value next [type] bytes | ||
view[`set${type}`](where, prop[eid]); | ||
where += prop.BYTES_PER_ELEMENT; // sync shadow state | ||
prop[$serializeShadow][eid] = prop[eid]; | ||
} | ||
} | ||
view.setUint32(countWhere, count); | ||
} | ||
return buffer.slice(0, where); | ||
}; | ||
}; | ||
const defineDeserializer = target => { | ||
const isWorld = Object.getOwnPropertySymbols(target).includes($componentMap); | ||
let [componentProps] = canonicalize(target); | ||
return (world, packet) => { | ||
if (isWorld) { | ||
componentProps = []; | ||
target[$componentMap].forEach((c, component) => { | ||
componentProps.push(...component[$storeFlattened]); | ||
}); | ||
} | ||
const view = new DataView(packet); | ||
let where = 0; | ||
while (where < packet.byteLength) { | ||
// pid | ||
const pid = view.getUint8(where); | ||
where += 1; // entity count | ||
const entityCount = view.getUint32(where); | ||
where += 4; // typed array | ||
const ta = componentProps[pid]; // Get the properties and set the new state | ||
for (let i = 0; i < entityCount; i++) { | ||
let eid = view.getUint32(where); | ||
where += 4; // if this world hasn't seen this eid yet | ||
if (!world[$entityEnabled][eid]) { | ||
// make a new entity for the data | ||
eid = addEntity(world); | ||
} | ||
const component = ta[$storeBase](); | ||
if (!hasComponent(world, component, eid)) { | ||
addComponent(world, component, eid); | ||
} | ||
if (ArrayBuffer.isView(ta[eid])) { | ||
const array = ta[eid]; | ||
const count = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; // iterate over count | ||
for (let i = 0; i < count; i++) { | ||
const index = view[`get${array[$indexType]}`](where); | ||
where += array[$indexBytes]; | ||
const value = view[`get${array.constructor.name.replace('Array', '')}`](where); | ||
where += array.BYTES_PER_ELEMENT; | ||
ta[eid][index] = value; | ||
} | ||
} else { | ||
let value = view[`get${ta.constructor.name.replace('Array', '')}`](where); | ||
where += ta.BYTES_PER_ELEMENT; | ||
ta[eid] = value; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
const pipe = (...fns) => input => { | ||
@@ -900,0 +929,0 @@ if (!input || Array.isArray(input) && input.length === 0) return; |
@@ -38,2 +38,19 @@ declare module 'bitecs' { | ||
type ArrayByType = { | ||
['bool']: boolean[]; | ||
[Types.i8]: Int8Array; | ||
[Types.ui8]: Uint8Array; | ||
[Types.ui8c]: Uint8ClampedArray; | ||
[Types.i16]: Int16Array; | ||
[Types.ui16]: Uint16Array; | ||
[Types.i32]: Int32Array; | ||
[Types.ui32]: Uint32Array; | ||
[Types.f32]: Float32Array; | ||
[Types.f64]: Float64Array; | ||
} | ||
type ComponentType<T extends ISchema> = { | ||
[key in keyof T]: T[key] extends Type ? ArrayByType[T[key]] : T[key] extends ISchema ? ComponentType<T[key]> : unknown; | ||
} | ||
interface IWorld { | ||
@@ -61,3 +78,3 @@ [key: string]: any | ||
export function createWorld (size?: number): IWorld | ||
@@ -68,3 +85,3 @@ export function addEntity (world: IWorld): number | ||
export function registerComponents (world: IWorld, components: IComponent[]): void | ||
export function defineComponent (schema: ISchema): IComponent | ||
export function defineComponent <T extends ISchema>(schema: T): ComponentType<T> | ||
export function addComponent (world: IWorld, component: IComponent, eid: number): void | ||
@@ -71,0 +88,0 @@ export function removeComponent (world: IWorld, component: IComponent, eid: number): void |
{ | ||
"name": "bitecs", | ||
"version": "0.3.2a1", | ||
"version": "0.3.2-a2", | ||
"description": "Functional, minimal, data-driven, ultra-high performance ECS library written in Javascript", | ||
@@ -5,0 +5,0 @@ "license": "MPL-2.0", |
@@ -175,3 +175,3 @@ # ๐พ bitECS ๐พ | ||
## โ System | ||
## ๐ธ System | ||
@@ -178,0 +178,0 @@ Systems are functions and are run against a world to update componenet state of entities, or anything else. |
@@ -15,4 +15,4 @@ import { strictEqual } from 'assert' | ||
const eid = addEntity(world) | ||
// console.log(TestComponent) | ||
addComponent(world, TestComponent, eid) | ||
const serialize = defineSerializer(world) | ||
@@ -19,0 +19,0 @@ const deserialize = defineDeserializer(world) |
import assert, { strictEqual } from 'assert' | ||
import { defaultSize } from '../../src/Entity.js' | ||
import { Types } from '../../src/index.js' | ||
@@ -8,5 +9,5 @@ import { createStore, TYPES } from '../../src/Storage.js' | ||
describe('Storage Integration Tests', () => { | ||
it('should default to size of 1MM', () => { | ||
const store = createStore({ value: Types.i8 }) | ||
strictEqual(store.value.length, 10000) | ||
it('should default to size of 100k', () => { | ||
const store = createStore({ value: Types.i8 }, defaultSize) | ||
strictEqual(store.value.length, defaultSize) | ||
}) | ||
@@ -19,5 +20,5 @@ it('should allow custom size', () => { | ||
it('should create a store with ' + type, () => { | ||
const store = createStore({ value: type }) | ||
const store = createStore({ value: type }, defaultSize) | ||
assert(store.value instanceof TYPES[type]) | ||
strictEqual(store.value.length, 10000) | ||
strictEqual(store.value.length, defaultSize) | ||
}) | ||
@@ -45,3 +46,3 @@ }) | ||
it('should create flat stores', () => { | ||
const store = createStore({ value1: Types.i8, value2: Types.ui16, value3: Types.f32 }) | ||
const store = createStore({ value1: Types.i8, value2: Types.ui16, value3: Types.f32 }, defaultSize) | ||
assert(store.value1 != undefined) | ||
@@ -55,5 +56,5 @@ assert(store.value1 instanceof Int8Array) | ||
it('should create nested stores', () => { | ||
const store1 = createStore({ nest: { value: Types.i8 } }) | ||
const store2 = createStore({ nest: { nest: { value: Types.ui32 } } }) | ||
const store3 = createStore({ nest: { nest: { nest: { value: Types.i16 } } } }) | ||
const store1 = createStore({ nest: { value: Types.i8 } }, defaultSize) | ||
const store2 = createStore({ nest: { nest: { value: Types.ui32 } } }, defaultSize) | ||
const store3 = createStore({ nest: { nest: { nest: { value: Types.i16 } } } }, defaultSize) | ||
assert(store1.nest.value instanceof Int8Array) | ||
@@ -60,0 +61,0 @@ assert(store2.nest.nest.value instanceof Uint32Array) |
import assert, { strictEqual } from 'assert' | ||
import { $componentMap } from '../../src/Component.js' | ||
import { $entityEnabled, $entityMasks, resetGlobals, addEntity } from '../../src/Entity.js' | ||
import { $entityEnabled, $entityMasks, resetGlobals, addEntity, defaultSize } from '../../src/Entity.js' | ||
import { $dirtyQueries, $queries, $queryMap } from '../../src/Query.js' | ||
import { createWorld, $size, $bitflag } from '../../src/World.js' | ||
const defaultSize = 10000 | ||
const growAmount = defaultSize + defaultSize / 2 | ||
@@ -9,0 +8,0 @@ |
import assert, { strictEqual } from 'assert' | ||
import { $componentMap } from '../../src/Component.js' | ||
import { $entityEnabled, $entityMasks, resetGlobals, addEntity } from '../../src/Entity.js' | ||
import { $entityEnabled, $entityMasks, resetGlobals, addEntity, defaultSize } from '../../src/Entity.js' | ||
import { $dirtyQueries, $queries, $queryMap } from '../../src/Query.js' | ||
import { createWorld, $size, $bitflag } from '../../src/World.js' | ||
const defaultSize = 10000 | ||
describe('World Integration Tests', () => { | ||
@@ -10,0 +8,0 @@ afterEach(() => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
251028
2097
0