Comparing version 0.3.2 to 0.3.3-a
@@ -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'); | ||
@@ -64,11 +65,53 @@ const $serializeShadow = Symbol('serializeShadow'); | ||
const resizeRecursive = (store, size) => { | ||
const resizeSubarray = (metadata, store, size) => { | ||
const cursors = metadata[$subarrayCursors]; | ||
const type = store[$storeType]; | ||
const length = store[0].length; | ||
const indexType = length < UNSIGNED_MAX.uint8 ? 'ui8' : length < UNSIGNED_MAX.uint16 ? 'ui16' : 'ui32'; | ||
const arrayCount = metadata[$storeArrayCounts][type]; | ||
const summedLength = Array(arrayCount).fill(0).reduce((a, p) => a + length, 0); // for threaded impl | ||
// const summedBytesPerElement = Array(arrayCount).fill(0).reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0) | ||
// const totalBytes = roundToMultiple4(summedBytesPerElement * summedLength * size) | ||
// const buffer = new ArrayBuffer(totalBytes) | ||
const array = new TYPES[type](summedLength * size); | ||
array.set(metadata[$storeSubarrays][type]); | ||
metadata[$storeSubarrays][type] = array; | ||
metadata[$storeSubarrays][type][$queryShadow] = array.slice(0); | ||
metadata[$storeSubarrays][type][$serializeShadow] = array.slice(0); | ||
array[$indexType] = TYPES_NAMES[indexType]; | ||
array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT; | ||
let end = 0; | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
store[eid] = metadata[$storeSubarrays][type].subarray(from, to); | ||
store[eid].from = from; | ||
store[eid].to = to; | ||
store[eid][$queryShadow] = metadata[$storeSubarrays][type][$queryShadow].subarray(from, to); | ||
store[eid][$serializeShadow] = metadata[$storeSubarrays][type][$serializeShadow].subarray(from, to); | ||
store[eid][$subarray] = true; | ||
store[eid][$indexType] = array[$indexType]; | ||
store[eid][$indexBytes] = array[$indexBytes]; | ||
end = to; | ||
} | ||
cursors[type] = end; | ||
}; | ||
const resizeRecursive = (metadata, store, size) => { | ||
Object.keys(store).forEach(key => { | ||
const ta = store[key]; | ||
if (ta[$subarray]) return;else if (ArrayBuffer.isView(ta)) { | ||
if (Array.isArray(ta)) { | ||
resizeSubarray(metadata, ta, size); | ||
store[$storeFlattened].push(ta); | ||
} else if (ArrayBuffer.isView(ta)) { | ||
store[key] = resize(ta, size); | ||
store[$storeFlattened].push(store[key]); | ||
store[key][$queryShadow] = resize(ta[$queryShadow], size); | ||
store[key][$serializeShadow] = resize(ta[$serializeShadow], size); | ||
} else if (typeof ta === 'object') { | ||
resizeRecursive(store[key], size); | ||
resizeRecursive(metadata, store[key], size); | ||
} | ||
@@ -78,46 +121,17 @@ }); | ||
const resizeSubarrays = (metadata, size) => { | ||
Object.keys(metadata[$subarrayCursors]).forEach(k => { | ||
metadata[$subarrayCursors][k] = 0; | ||
}); | ||
const cursors = metadata[$subarrayCursors]; | ||
metadata[$storeFlattened].filter(store => !ArrayBuffer.isView(store)).forEach(store => { | ||
const type = store[$storeType]; | ||
const length = store[0].length; | ||
const arrayCount = metadata[$storeArrayCounts][type]; | ||
const summedLength = Array(arrayCount).fill(0).reduce((a, p) => a + length, 0); // for threaded impl | ||
// const summedBytesPerElement = Array(arrayCount).fill(0).reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0) | ||
// const totalBytes = roundToMultiple4(summedBytesPerElement * summedLength * size) | ||
// const buffer = new ArrayBuffer(totalBytes) | ||
const array = new TYPES[type](summedLength * size); | ||
array.set(metadata[$storeSubarrays][type]); | ||
metadata[$storeSubarrays][type] = array; | ||
metadata[$storeSubarrays][type][$queryShadow] = array.slice(0); | ||
metadata[$storeSubarrays][type][$serializeShadow] = array.slice(0); | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
store[eid] = metadata[$storeSubarrays][type].subarray(from, to); | ||
store[eid].from = from; | ||
store[eid].to = to; | ||
store[eid][$queryShadow] = metadata[$storeSubarrays][type][$queryShadow].subarray(from, to); | ||
store[eid][$serializeShadow] = metadata[$storeSubarrays][type][$serializeShadow].subarray(from, to); | ||
store[eid][$subarray] = true; | ||
store[eid][$indexType] = array[$indexType]; | ||
store[eid][$indexBytes] = array[$indexBytes]; | ||
} | ||
}); | ||
}; | ||
const resizeStore = (store, size) => { | ||
if (store[$tagStore]) return; | ||
store[$storeSize] = size; | ||
resizeRecursive(store, size); | ||
resizeSubarrays(store, size); | ||
store[$storeFlattened].length = 0; | ||
Object.keys(store[$subarrayCursors]).forEach(k => { | ||
store[$subarrayCursors][k] = 0; | ||
}); | ||
resizeRecursive(store, store, size); // resizeSubarrays(store, size) | ||
}; | ||
const resetStoreFor = (store, eid) => { | ||
store[$storeFlattened].forEach(ta => { | ||
if (ArrayBuffer.isView(ta)) ta[eid] = 0;else ta[eid].fill(0); | ||
}); | ||
if (store[$storeFlattened]) { | ||
store[$storeFlattened].forEach(ta => { | ||
if (ArrayBuffer.isView(ta)) ta[eid] = 0;else ta[eid].fill(0); | ||
}); | ||
} | ||
}; | ||
@@ -183,5 +197,15 @@ | ||
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)); | ||
@@ -213,4 +237,4 @@ const arrayCounts = {}; | ||
}), {}), | ||
[$storeArrayCounts]: arrayCounts, | ||
[$storeFlattened]: [] | ||
[$storeFlattened]: [], | ||
[$storeArrayCounts]: arrayCounts | ||
}; | ||
@@ -222,2 +246,3 @@ | ||
a[k] = createTypeStore(a[k], size); | ||
createShadows(a[k]); | ||
@@ -227,3 +252,2 @@ a[k][$storeBase] = () => stores[$store]; | ||
metadata[$storeFlattened].push(a[k]); | ||
createShadows(a[k]); | ||
} else if (isArrayType(a[k])) { | ||
@@ -249,12 +273,223 @@ 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]); | ||
}); | ||
} | ||
let world; | ||
if (Object.getOwnPropertySymbols(ents).includes($componentMap)) { | ||
world = ents; | ||
ents = ents[$entityArray]; | ||
} else { | ||
world = eidToWorld.get(ents[0]); | ||
} | ||
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 entity doesn't have this component | ||
if (!hasComponent(world, prop[$storeBase](), eid)) { | ||
continue; | ||
} // 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; | ||
const newEntities = new Map(); | ||
while (where < packet.byteLength) { | ||
// pid | ||
const pid = view.getUint8(where); | ||
where += 1; // entity count | ||
const entityCount = view.getUint32(where); | ||
where += 4; // component property | ||
const prop = componentProps[pid]; // Get the entities and set their prop values | ||
for (let i = 0; i < entityCount; i++) { | ||
let eid = view.getUint32(where); | ||
where += 4; | ||
let newEid = newEntities.get(eid); | ||
if (newEid !== undefined) { | ||
eid = newEid; | ||
} // if this world hasn't seen this eid yet | ||
if (!world[$entityEnabled][eid]) { | ||
// make a new entity for the data | ||
const newEid = addEntity(world); | ||
newEntities.set(eid, newEid); | ||
eid = newEid; | ||
} | ||
const component = prop[$storeBase](); | ||
if (!hasComponent(world, component, eid)) { | ||
addComponent(world, component, eid); // console.log('hi',eid) | ||
} | ||
if (component[$tagStore]) { | ||
continue; | ||
} | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
const array = prop[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; | ||
prop[eid][index] = value; | ||
} | ||
} else { | ||
const value = view[`get${prop.constructor.name.replace('Array', '')}`](where); | ||
where += prop.BYTES_PER_ELEMENT; | ||
prop[eid] = value; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
const $entityMasks = Symbol('entityMasks'); | ||
@@ -264,26 +499,16 @@ 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; // removed eids should also be global to prevent memory leaks | ||
let globalEntityCursor = 0; | ||
let globalSize = defaultSize; | ||
let resizeThreshold = () => globalSize - globalSize / 5; | ||
const getGlobalSize = () => globalSize; // removed eids should also be global to prevent memory leaks | ||
const removed = []; | ||
const getEntityCursor = () => globalEntityCursor; | ||
const resizeWorld = (world, size) => { | ||
world[$size] = size; | ||
world[$componentMap].forEach(c => { | ||
resizeStore(c.store, size); | ||
}); | ||
world[$queryMap].forEach(q => { | ||
q.indices = resize(q.indices, size); | ||
q.enabled = resize(q.enabled, size); | ||
}); | ||
world[$entityEnabled] = resize(world[$entityEnabled], size); | ||
world[$entityIndices] = resize(world[$entityIndices], size); | ||
for (let i = 0; i < world[$entityMasks].length; i++) { | ||
const masks = world[$entityMasks][i]; | ||
world[$entityMasks][i] = resize(masks, size); | ||
} | ||
}; | ||
const eidToWorld = new Map(); | ||
const addEntity = world => { | ||
@@ -293,11 +518,15 @@ const enabled = world[$entityEnabled]; | ||
enabled[eid] = 1; | ||
world[$entityIndices][eid] = world[$entityArray].push(eid) - 1; // if data stores are 80% full | ||
world[$entityIndices][eid] = world[$entityArray].push(eid) - 1; | ||
eidToWorld.set(eid, world); // if data stores are 80% full | ||
if (globalEntityCursor >= world[$warningSize]) { | ||
if (globalEntityCursor >= resizeThreshold()) { | ||
// grow by half the original size rounded up to a multiple of 4 | ||
const size = world[$size]; | ||
const size = globalSize; | ||
const amount = Math.ceil(size / 2 / 4) * 4; | ||
resizeWorld(world, size + amount); | ||
world[$warningSize] = world[$size] - world[$size] / 5; | ||
console.info(`๐พ bitECS - resizing world from ${size} to ${size + amount}`); | ||
const newSize = size + amount; | ||
globalSize = newSize; | ||
resizeWorlds(newSize); | ||
resizeComponents(newSize); | ||
setSerializationResized(true); | ||
console.info(`๐พ bitECS - resizing all worlds from ${size} to ${size + amount}`); | ||
} | ||
@@ -307,2 +536,16 @@ | ||
}; | ||
const popSwap = (world, eid) => { | ||
// 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$1; | ||
}; | ||
const removeEntity = (world, eid) => { | ||
@@ -321,12 +564,4 @@ const enabled = world[$entityEnabled]; // Check if entity is already removed | ||
const index = world[$entityIndices][eid]; | ||
const swapped = world[$entityArray].pop(); | ||
popSwap(world, eid); // Clear entity bitmasks | ||
if (swapped !== eid) { | ||
world[$entityArray][index] = swapped; | ||
world[$entityIndices][swapped] = index; | ||
} | ||
world[$entityIndices][eid] = NONE$1; // Clear entity bitmasks | ||
for (let i = 0; i < world[$entityMasks].length; i++) world[$entityMasks][i][eid] = 0; | ||
@@ -439,6 +674,3 @@ }; | ||
} | ||
}; // const queryHooks = (q) => { | ||
// while (q.entered.length) if (q.enter) { q.enter(q.entered.shift()) } else q.entered.length = 0 | ||
// while (q.exited.length) if (q.exit) { q.exit(q.exited.shift()) } else q.exited.length = 0 | ||
// } | ||
}; | ||
@@ -531,3 +763,4 @@ const diff = q => { | ||
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); | ||
@@ -564,3 +797,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); | ||
@@ -570,3 +804,11 @@ }; | ||
const $componentMap = Symbol('componentMap'); | ||
const defineComponent = schema => createStore(schema); | ||
const components = []; | ||
const resizeComponents = size => { | ||
components.forEach(component => resizeStore(component, size)); | ||
}; | ||
const defineComponent = schema => { | ||
const component = createStore(schema, defaultSize); | ||
if (schema && Object.keys(schema).length) components.push(component); | ||
return component; | ||
}; | ||
const incrementBitflag = world => { | ||
@@ -624,3 +866,3 @@ world[$bitflag] *= 2; | ||
}; | ||
const removeComponent = (world, component, eid, reset = false) => { | ||
const removeComponent = (world, component, eid, reset = true) => { | ||
const { | ||
@@ -644,6 +886,26 @@ generationId, | ||
const $size = Symbol('size'); | ||
const $warningSize = Symbol('warningSize'); | ||
const $resizeThreshold = Symbol('resizeThreshold'); | ||
const $bitflag = Symbol('bitflag'); | ||
const createWorld = (size = 10000) => { | ||
const worlds = []; | ||
const resizeWorlds = size => { | ||
worlds.forEach(world => { | ||
world[$size] = size; | ||
world[$queryMap].forEach(q => { | ||
q.indices = resize(q.indices, size); | ||
q.enabled = resize(q.enabled, size); | ||
}); | ||
world[$entityEnabled] = resize(world[$entityEnabled], size); | ||
world[$entityIndices] = resize(world[$entityIndices], size); | ||
for (let i = 0; i < world[$entityMasks].length; i++) { | ||
const masks = world[$entityMasks][i]; | ||
world[$entityMasks][i] = resize(masks, size); | ||
} | ||
world[$resizeThreshold] = world[$size] - world[$size] / 5; | ||
}); | ||
}; | ||
const createWorld = () => { | ||
const world = {}; | ||
const size = getGlobalSize(); | ||
world[$size] = size; | ||
@@ -659,8 +921,17 @@ world[$entityEnabled] = new Uint8Array(size); | ||
world[$dirtyQueries] = new Set(); | ||
world[$warningSize] = size - size / 5; | ||
worlds.push(world); | ||
return world; | ||
}; | ||
const defineSystem = update => { | ||
const defineSystem = (fn1, fn2) => { | ||
const update = fn2 !== undefined ? fn2 : fn1; | ||
const create = fn2 !== undefined ? fn1 : undefined; | ||
const init = new Set(); | ||
const system = world => { | ||
if (create && !init.has(world)) { | ||
create(world); | ||
init.add(world); | ||
} | ||
update(world); | ||
@@ -678,178 +949,4 @@ commitRemovals(world); | ||
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 = 20_000_000) => { | ||
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 => { | ||
if (!input || Array.isArray(input) && input.length === 0) return; | ||
fns = Array.isArray(fns[0]) ? fns[0] : fns; | ||
@@ -860,3 +957,8 @@ let tmp = input; | ||
const fn = fns[i]; | ||
tmp = fn(tmp); | ||
if (Array.isArray(tmp)) { | ||
tmp = tmp.reduce((a, v) => a.concat(fn(v)), []); | ||
} else { | ||
tmp = fn(tmp); | ||
} | ||
} | ||
@@ -863,0 +965,0 @@ |
@@ -55,2 +55,3 @@ 'use strict'; | ||
const $subarray = Symbol('subarray'); | ||
const $tagStore = Symbol('tagStore'); | ||
const $queryShadow = Symbol('queryShadow'); | ||
@@ -68,11 +69,53 @@ const $serializeShadow = Symbol('serializeShadow'); | ||
const resizeRecursive = (store, size) => { | ||
const resizeSubarray = (metadata, store, size) => { | ||
const cursors = metadata[$subarrayCursors]; | ||
const type = store[$storeType]; | ||
const length = store[0].length; | ||
const indexType = length < UNSIGNED_MAX.uint8 ? 'ui8' : length < UNSIGNED_MAX.uint16 ? 'ui16' : 'ui32'; | ||
const arrayCount = metadata[$storeArrayCounts][type]; | ||
const summedLength = Array(arrayCount).fill(0).reduce((a, p) => a + length, 0); // for threaded impl | ||
// const summedBytesPerElement = Array(arrayCount).fill(0).reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0) | ||
// const totalBytes = roundToMultiple4(summedBytesPerElement * summedLength * size) | ||
// const buffer = new ArrayBuffer(totalBytes) | ||
const array = new TYPES[type](summedLength * size); | ||
array.set(metadata[$storeSubarrays][type]); | ||
metadata[$storeSubarrays][type] = array; | ||
metadata[$storeSubarrays][type][$queryShadow] = array.slice(0); | ||
metadata[$storeSubarrays][type][$serializeShadow] = array.slice(0); | ||
array[$indexType] = TYPES_NAMES[indexType]; | ||
array[$indexBytes] = TYPES[indexType].BYTES_PER_ELEMENT; | ||
let end = 0; | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
store[eid] = metadata[$storeSubarrays][type].subarray(from, to); | ||
store[eid].from = from; | ||
store[eid].to = to; | ||
store[eid][$queryShadow] = metadata[$storeSubarrays][type][$queryShadow].subarray(from, to); | ||
store[eid][$serializeShadow] = metadata[$storeSubarrays][type][$serializeShadow].subarray(from, to); | ||
store[eid][$subarray] = true; | ||
store[eid][$indexType] = array[$indexType]; | ||
store[eid][$indexBytes] = array[$indexBytes]; | ||
end = to; | ||
} | ||
cursors[type] = end; | ||
}; | ||
const resizeRecursive = (metadata, store, size) => { | ||
Object.keys(store).forEach(key => { | ||
const ta = store[key]; | ||
if (ta[$subarray]) return;else if (ArrayBuffer.isView(ta)) { | ||
if (Array.isArray(ta)) { | ||
resizeSubarray(metadata, ta, size); | ||
store[$storeFlattened].push(ta); | ||
} else if (ArrayBuffer.isView(ta)) { | ||
store[key] = resize(ta, size); | ||
store[$storeFlattened].push(store[key]); | ||
store[key][$queryShadow] = resize(ta[$queryShadow], size); | ||
store[key][$serializeShadow] = resize(ta[$serializeShadow], size); | ||
} else if (typeof ta === 'object') { | ||
resizeRecursive(store[key], size); | ||
resizeRecursive(metadata, store[key], size); | ||
} | ||
@@ -82,46 +125,17 @@ }); | ||
const resizeSubarrays = (metadata, size) => { | ||
Object.keys(metadata[$subarrayCursors]).forEach(k => { | ||
metadata[$subarrayCursors][k] = 0; | ||
}); | ||
const cursors = metadata[$subarrayCursors]; | ||
metadata[$storeFlattened].filter(store => !ArrayBuffer.isView(store)).forEach(store => { | ||
const type = store[$storeType]; | ||
const length = store[0].length; | ||
const arrayCount = metadata[$storeArrayCounts][type]; | ||
const summedLength = Array(arrayCount).fill(0).reduce((a, p) => a + length, 0); // for threaded impl | ||
// const summedBytesPerElement = Array(arrayCount).fill(0).reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0) | ||
// const totalBytes = roundToMultiple4(summedBytesPerElement * summedLength * size) | ||
// const buffer = new ArrayBuffer(totalBytes) | ||
const array = new TYPES[type](summedLength * size); | ||
array.set(metadata[$storeSubarrays][type]); | ||
metadata[$storeSubarrays][type] = array; | ||
metadata[$storeSubarrays][type][$queryShadow] = array.slice(0); | ||
metadata[$storeSubarrays][type][$serializeShadow] = array.slice(0); | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
store[eid] = metadata[$storeSubarrays][type].subarray(from, to); | ||
store[eid].from = from; | ||
store[eid].to = to; | ||
store[eid][$queryShadow] = metadata[$storeSubarrays][type][$queryShadow].subarray(from, to); | ||
store[eid][$serializeShadow] = metadata[$storeSubarrays][type][$serializeShadow].subarray(from, to); | ||
store[eid][$subarray] = true; | ||
store[eid][$indexType] = array[$indexType]; | ||
store[eid][$indexBytes] = array[$indexBytes]; | ||
} | ||
}); | ||
}; | ||
const resizeStore = (store, size) => { | ||
if (store[$tagStore]) return; | ||
store[$storeSize] = size; | ||
resizeRecursive(store, size); | ||
resizeSubarrays(store, size); | ||
store[$storeFlattened].length = 0; | ||
Object.keys(store[$subarrayCursors]).forEach(k => { | ||
store[$subarrayCursors][k] = 0; | ||
}); | ||
resizeRecursive(store, store, size); // resizeSubarrays(store, size) | ||
}; | ||
const resetStoreFor = (store, eid) => { | ||
store[$storeFlattened].forEach(ta => { | ||
if (ArrayBuffer.isView(ta)) ta[eid] = 0;else ta[eid].fill(0); | ||
}); | ||
if (store[$storeFlattened]) { | ||
store[$storeFlattened].forEach(ta => { | ||
if (ArrayBuffer.isView(ta)) ta[eid] = 0;else ta[eid].fill(0); | ||
}); | ||
} | ||
}; | ||
@@ -187,5 +201,15 @@ | ||
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)); | ||
@@ -217,4 +241,4 @@ const arrayCounts = {}; | ||
}), {}), | ||
[$storeArrayCounts]: arrayCounts, | ||
[$storeFlattened]: [] | ||
[$storeFlattened]: [], | ||
[$storeArrayCounts]: arrayCounts | ||
}; | ||
@@ -226,2 +250,3 @@ | ||
a[k] = createTypeStore(a[k], size); | ||
createShadows(a[k]); | ||
@@ -231,3 +256,2 @@ a[k][$storeBase] = () => stores[$store]; | ||
metadata[$storeFlattened].push(a[k]); | ||
createShadows(a[k]); | ||
} else if (isArrayType(a[k])) { | ||
@@ -253,12 +277,223 @@ 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]); | ||
}); | ||
} | ||
let world; | ||
if (Object.getOwnPropertySymbols(ents).includes($componentMap)) { | ||
world = ents; | ||
ents = ents[$entityArray]; | ||
} else { | ||
world = eidToWorld.get(ents[0]); | ||
} | ||
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 entity doesn't have this component | ||
if (!hasComponent(world, prop[$storeBase](), eid)) { | ||
continue; | ||
} // 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; | ||
const newEntities = new Map(); | ||
while (where < packet.byteLength) { | ||
// pid | ||
const pid = view.getUint8(where); | ||
where += 1; // entity count | ||
const entityCount = view.getUint32(where); | ||
where += 4; // component property | ||
const prop = componentProps[pid]; // Get the entities and set their prop values | ||
for (let i = 0; i < entityCount; i++) { | ||
let eid = view.getUint32(where); | ||
where += 4; | ||
let newEid = newEntities.get(eid); | ||
if (newEid !== undefined) { | ||
eid = newEid; | ||
} // if this world hasn't seen this eid yet | ||
if (!world[$entityEnabled][eid]) { | ||
// make a new entity for the data | ||
const newEid = addEntity(world); | ||
newEntities.set(eid, newEid); | ||
eid = newEid; | ||
} | ||
const component = prop[$storeBase](); | ||
if (!hasComponent(world, component, eid)) { | ||
addComponent(world, component, eid); // console.log('hi',eid) | ||
} | ||
if (component[$tagStore]) { | ||
continue; | ||
} | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
const array = prop[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; | ||
prop[eid][index] = value; | ||
} | ||
} else { | ||
const value = view[`get${prop.constructor.name.replace('Array', '')}`](where); | ||
where += prop.BYTES_PER_ELEMENT; | ||
prop[eid] = value; | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
const $entityMasks = Symbol('entityMasks'); | ||
@@ -268,26 +503,16 @@ 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; // removed eids should also be global to prevent memory leaks | ||
let globalEntityCursor = 0; | ||
let globalSize = defaultSize; | ||
let resizeThreshold = () => globalSize - globalSize / 5; | ||
const getGlobalSize = () => globalSize; // removed eids should also be global to prevent memory leaks | ||
const removed = []; | ||
const getEntityCursor = () => globalEntityCursor; | ||
const resizeWorld = (world, size) => { | ||
world[$size] = size; | ||
world[$componentMap].forEach(c => { | ||
resizeStore(c.store, size); | ||
}); | ||
world[$queryMap].forEach(q => { | ||
q.indices = resize(q.indices, size); | ||
q.enabled = resize(q.enabled, size); | ||
}); | ||
world[$entityEnabled] = resize(world[$entityEnabled], size); | ||
world[$entityIndices] = resize(world[$entityIndices], size); | ||
for (let i = 0; i < world[$entityMasks].length; i++) { | ||
const masks = world[$entityMasks][i]; | ||
world[$entityMasks][i] = resize(masks, size); | ||
} | ||
}; | ||
const eidToWorld = new Map(); | ||
const addEntity = world => { | ||
@@ -297,11 +522,15 @@ const enabled = world[$entityEnabled]; | ||
enabled[eid] = 1; | ||
world[$entityIndices][eid] = world[$entityArray].push(eid) - 1; // if data stores are 80% full | ||
world[$entityIndices][eid] = world[$entityArray].push(eid) - 1; | ||
eidToWorld.set(eid, world); // if data stores are 80% full | ||
if (globalEntityCursor >= world[$warningSize]) { | ||
if (globalEntityCursor >= resizeThreshold()) { | ||
// grow by half the original size rounded up to a multiple of 4 | ||
const size = world[$size]; | ||
const size = globalSize; | ||
const amount = Math.ceil(size / 2 / 4) * 4; | ||
resizeWorld(world, size + amount); | ||
world[$warningSize] = world[$size] - world[$size] / 5; | ||
console.info(`๐พ bitECS - resizing world from ${size} to ${size + amount}`); | ||
const newSize = size + amount; | ||
globalSize = newSize; | ||
resizeWorlds(newSize); | ||
resizeComponents(newSize); | ||
setSerializationResized(true); | ||
console.info(`๐พ bitECS - resizing all worlds from ${size} to ${size + amount}`); | ||
} | ||
@@ -311,2 +540,16 @@ | ||
}; | ||
const popSwap = (world, eid) => { | ||
// 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$1; | ||
}; | ||
const removeEntity = (world, eid) => { | ||
@@ -325,12 +568,4 @@ const enabled = world[$entityEnabled]; // Check if entity is already removed | ||
const index = world[$entityIndices][eid]; | ||
const swapped = world[$entityArray].pop(); | ||
popSwap(world, eid); // Clear entity bitmasks | ||
if (swapped !== eid) { | ||
world[$entityArray][index] = swapped; | ||
world[$entityIndices][swapped] = index; | ||
} | ||
world[$entityIndices][eid] = NONE$1; // Clear entity bitmasks | ||
for (let i = 0; i < world[$entityMasks].length; i++) world[$entityMasks][i][eid] = 0; | ||
@@ -443,6 +678,3 @@ }; | ||
} | ||
}; // const queryHooks = (q) => { | ||
// while (q.entered.length) if (q.enter) { q.enter(q.entered.shift()) } else q.entered.length = 0 | ||
// while (q.exited.length) if (q.exit) { q.exit(q.exited.shift()) } else q.exited.length = 0 | ||
// } | ||
}; | ||
@@ -535,3 +767,4 @@ const diff = q => { | ||
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); | ||
@@ -568,3 +801,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); | ||
@@ -574,3 +808,11 @@ }; | ||
const $componentMap = Symbol('componentMap'); | ||
const defineComponent = schema => createStore(schema); | ||
const components = []; | ||
const resizeComponents = size => { | ||
components.forEach(component => resizeStore(component, size)); | ||
}; | ||
const defineComponent = schema => { | ||
const component = createStore(schema, defaultSize); | ||
if (schema && Object.keys(schema).length) components.push(component); | ||
return component; | ||
}; | ||
const incrementBitflag = world => { | ||
@@ -628,3 +870,3 @@ world[$bitflag] *= 2; | ||
}; | ||
const removeComponent = (world, component, eid, reset = false) => { | ||
const removeComponent = (world, component, eid, reset = true) => { | ||
const { | ||
@@ -648,6 +890,26 @@ generationId, | ||
const $size = Symbol('size'); | ||
const $warningSize = Symbol('warningSize'); | ||
const $resizeThreshold = Symbol('resizeThreshold'); | ||
const $bitflag = Symbol('bitflag'); | ||
const createWorld = (size = 10000) => { | ||
const worlds = []; | ||
const resizeWorlds = size => { | ||
worlds.forEach(world => { | ||
world[$size] = size; | ||
world[$queryMap].forEach(q => { | ||
q.indices = resize(q.indices, size); | ||
q.enabled = resize(q.enabled, size); | ||
}); | ||
world[$entityEnabled] = resize(world[$entityEnabled], size); | ||
world[$entityIndices] = resize(world[$entityIndices], size); | ||
for (let i = 0; i < world[$entityMasks].length; i++) { | ||
const masks = world[$entityMasks][i]; | ||
world[$entityMasks][i] = resize(masks, size); | ||
} | ||
world[$resizeThreshold] = world[$size] - world[$size] / 5; | ||
}); | ||
}; | ||
const createWorld = () => { | ||
const world = {}; | ||
const size = getGlobalSize(); | ||
world[$size] = size; | ||
@@ -663,8 +925,17 @@ world[$entityEnabled] = new Uint8Array(size); | ||
world[$dirtyQueries] = new Set(); | ||
world[$warningSize] = size - size / 5; | ||
worlds.push(world); | ||
return world; | ||
}; | ||
const defineSystem = update => { | ||
const defineSystem = (fn1, fn2) => { | ||
const update = fn2 !== undefined ? fn2 : fn1; | ||
const create = fn2 !== undefined ? fn1 : undefined; | ||
const init = new Set(); | ||
const system = world => { | ||
if (create && !init.has(world)) { | ||
create(world); | ||
init.add(world); | ||
} | ||
update(world); | ||
@@ -682,178 +953,4 @@ commitRemovals(world); | ||
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 = 20_000_000) => { | ||
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 => { | ||
if (!input || Array.isArray(input) && input.length === 0) return; | ||
fns = Array.isArray(fns[0]) ? fns[0] : fns; | ||
@@ -864,3 +961,8 @@ let tmp = input; | ||
const fn = fns[i]; | ||
tmp = fn(tmp); | ||
if (Array.isArray(tmp)) { | ||
tmp = tmp.reduce((a, v) => a.concat(fn(v)), []); | ||
} else { | ||
tmp = fn(tmp); | ||
} | ||
} | ||
@@ -867,0 +969,0 @@ |
@@ -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.2", | ||
"version": "0.3.3-a", | ||
"description": "Functional, minimal, data-driven, ultra-high performance ECS library written in Javascript", | ||
@@ -5,0 +5,0 @@ "license": "MPL-2.0", |
@@ -13,3 +13,3 @@ # ๐พ bitECS ๐พ | ||
| ๐ Zero dependencies | ๐ Node or browser | | ||
| ๐ค `~5kb` gzipped | ๐ท TypeScript support | | ||
| ๐ค `~4kb` gzipped | ๐ท TypeScript support | | ||
| โค Made with love | ๐บ [glMatrix](https://github.com/toji/gl-matrix) support | | ||
@@ -176,3 +176,3 @@ | ||
## โ System | ||
## ๐ธ System | ||
@@ -179,0 +179,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, resizeWorld, 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 | ||
@@ -14,10 +13,4 @@ | ||
}) | ||
it('should resize on-demand', () => { | ||
it('should resize automatically at 80% of 10k', () => { | ||
const world = createWorld() | ||
resizeWorld(world, growAmount) | ||
strictEqual(world[$entityMasks][0].length, growAmount) | ||
strictEqual(world[$entityEnabled].length, growAmount) | ||
}) | ||
it('should resize automatically at 80% of 1MM', () => { | ||
const world = createWorld() | ||
const n = defaultSize * 0.8 | ||
@@ -24,0 +17,0 @@ for (let i = 0; i < n; i++) { |
import assert, { strictEqual } from 'assert' | ||
import { $componentMap } from '../../src/Component.js' | ||
import { $entityEnabled, $entityMasks, resetGlobals, resizeWorld, 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
255193
2133