Comparing version 0.2.14 to 0.2.15
@@ -1,828 +0,2 @@ | ||
const TYPES_ENUM = { | ||
bool: 'bool', | ||
i8: 'i8', | ||
ui8: 'ui8', | ||
ui8c: 'ui8c', | ||
i16: 'i16', | ||
ui16: 'ui16', | ||
i32: 'i32', | ||
ui32: 'ui32', | ||
f32: 'f32', | ||
f64: 'f64' | ||
}; | ||
const TYPES = { | ||
bool: 'bool', | ||
i8: Int8Array, | ||
ui8: Uint8Array, | ||
ui8c: Uint8ClampedArray, | ||
i16: Int16Array, | ||
ui16: Uint16Array, | ||
i32: Int32Array, | ||
ui32: Uint32Array, | ||
f32: Float32Array, | ||
f64: Float64Array | ||
}; | ||
const UNSIGNED_MAX = { | ||
uint8: 255, | ||
uint16: 65535, | ||
uint32: 4294967295 | ||
}; | ||
const grow = (ta, amount) => { | ||
const newTa = new ta.constructor(new ArrayBuffer(ta.buffer.byteLength + amount * ta.BYTES_PER_ELEMENT)); | ||
newTa.set(ta.buffer); | ||
return newTa; | ||
}; | ||
const roundToMultiple4 = x => Math.ceil(x / 4) * 4; | ||
const managers = {}; | ||
const $managerRef = Symbol('managerRef'); | ||
const $managerSize = Symbol('managerSize'); | ||
const $managerMaps = Symbol('maps'); | ||
const $managerSubarrays = Symbol('subarrays'); | ||
const $managerCursor = Symbol('managerCursor'); | ||
const $managerRemoved = Symbol('managerRemoved'); | ||
const $queryShadow = Symbol('queryShadow'); | ||
const $serializeShadow = Symbol('$serializeShadow'); | ||
const alloc = (schema, size = 1000000) => { | ||
const $manager = Symbol('manager'); | ||
if (schema.constructor.name === 'Map') { | ||
schema[$managerSize] = size; | ||
return schema; | ||
} | ||
managers[$manager] = { | ||
[$managerSize]: size, | ||
[$managerMaps]: {}, | ||
[$managerSubarrays]: {}, | ||
[$managerRef]: $manager, | ||
[$managerCursor]: 0, | ||
[$managerRemoved]: [] | ||
}; | ||
const props = schema ? Object.keys(schema) : []; | ||
let arrays = props.filter(p => Array.isArray(schema[p]) && typeof schema[p][0] === 'object'); | ||
const cursors = Object.keys(TYPES).reduce((a, type) => ({ ...a, | ||
[type]: 0 | ||
}), {}); | ||
if (typeof schema === 'string') { | ||
const type = schema; | ||
const totalBytes = size * TYPES[type].BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager] = new TYPES[type](buffer); | ||
} else if (Array.isArray(schema)) { | ||
arrays = schema; | ||
const { | ||
type, | ||
length | ||
} = schema[0]; | ||
const indexType = length < UNSIGNED_MAX.uint8 ? 'ui8' : length < UNSIGNED_MAX.uint16 ? 'ui16' : 'ui32'; | ||
if (!length) throw new Error('❌ Must define a length for component array.'); | ||
if (!TYPES[type]) throw new Error(`❌ Invalid component array property type ${type}.`); // create buffer for type if it does not already exist | ||
if (!managers[$manager][$managerSubarrays][type]) { | ||
const relevantArrays = arrays; | ||
const summedBytesPerElement = relevantArrays.reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0); | ||
const summedLength = relevantArrays.reduce((a, p) => a + length, 0); | ||
const buffer = new ArrayBuffer(roundToMultiple4(summedBytesPerElement * summedLength * size)); | ||
const array = new TYPES[type](buffer); | ||
array._indexType = indexType; | ||
array._indexBytes = TYPES[indexType].BYTES_PER_ELEMENT; | ||
managers[$manager][$managerSubarrays][type] = array; | ||
} // pre-generate subarrays for each eid | ||
let end = 0; | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
managers[$manager][eid] = managers[$manager][$managerSubarrays][type].subarray(from, to); | ||
end = to; | ||
} | ||
cursors[type] = end; | ||
managers[$manager]._reset = eid => managers[$manager][eid].fill(0); | ||
managers[$manager]._set = (eid, values) => managers[$manager][eid].set(values, 0); | ||
} else props.forEach(prop => { | ||
// Boolean Type | ||
if (schema[prop] === 'bool') { | ||
const Type = TYPES.uint8; | ||
const totalBytes = size * TYPES.uint8.BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][$managerMaps][prop] = schema[prop]; | ||
managers[$manager][prop] = new Type(buffer); | ||
managers[$manager][prop]._boolType = true; // Enum Type | ||
} else if (Array.isArray(schema[prop]) && typeof schema[prop][0] === 'string') { | ||
const Type = TYPES.uint8; | ||
const totalBytes = size * TYPES.uint8.BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][$managerMaps][prop] = schema[prop]; | ||
managers[$manager][prop] = new Type(buffer); // Array Type | ||
} else if (Array.isArray(schema[prop]) && typeof schema[prop][0] === 'object') { | ||
const { | ||
type, | ||
length | ||
} = schema[0]; | ||
if (!length) throw new Error('❌ Must define a length for component array.'); | ||
if (!TYPES[type]) throw new Error(`❌ Invalid component array property type ${type}.`); // create buffer for type if it does not already exist | ||
if (!managers[$manager][$managerSubarrays][type]) { | ||
const relevantArrays = arrays.filter(p => schema[p][0].type === type); | ||
const summedBytesPerElement = relevantArrays.reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0); | ||
const summedLength = relevantArrays.reduce((a, p) => a + length, 0); | ||
const buffer = new ArrayBuffer(roundToMultiple4(summedBytesPerElement * summedLength * size)); | ||
const array = new TYPES[type](buffer); | ||
array._indexType = index; | ||
array._indexBytes = TYPES[index].BYTES_PER_ELEMENT; | ||
managers[$manager][$managerSubarrays][type] = array; | ||
} // pre-generate subarrays for each eid | ||
managers[$manager][prop] = {}; | ||
let end = 0; | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
managers[$manager][prop][eid] = managers[$manager][$managerSubarrays][type].subarray(from, to); | ||
end = to; | ||
} | ||
cursors[type] = end; | ||
managers[$manager][prop]._reset = eid => managers[$manager][prop][eid].fill(0); | ||
managers[$manager][prop]._set = (eid, values) => managers[$manager][prop][eid].set(values, 0); // Object Type | ||
} else if (typeof schema[prop] === 'object') { | ||
managers[$manager][prop] = Manager(size, schema[prop], false); // String Type | ||
} else if (typeof schema[prop] === 'string') { | ||
const type = schema[prop]; | ||
const totalBytes = size * TYPES[type].BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
const queryShadowBuffer = new ArrayBuffer(totalBytes); | ||
const serializeShadowBuffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][prop] = new TYPES[type](buffer); | ||
managers[$manager][prop][$queryShadow] = new TYPES[type](queryShadowBuffer); | ||
managers[$manager][prop][$serializeShadow] = new TYPES[type](serializeShadowBuffer); // TypedArray Type | ||
} else if (typeof schema[prop] === 'function') { | ||
const Type = schema[prop]; | ||
const totalBytes = size * Type.BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][prop] = new Type(buffer); | ||
} else { | ||
throw new Error(`ECS Error: invalid property type ${schema[prop]}`); | ||
} | ||
}); // methods | ||
Object.defineProperty(managers[$manager], '_schema', { | ||
value: schema | ||
}); | ||
Object.defineProperty(managers[$manager], '_mapping', { | ||
value: prop => managers[$manager][$managerMaps][prop] | ||
}); // Recursively set all values to 0 | ||
Object.defineProperty(managers[$manager], '_reset', { | ||
value: eid => { | ||
for (const prop of managers[$manager]._props) { | ||
if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
if (ArrayBuffer.isView(managers[$manager][prop][eid])) { | ||
managers[$manager][prop][eid].fill(0); | ||
} else { | ||
managers[$manager][prop][eid] = 0; | ||
} | ||
} else { | ||
managers[$manager][prop]._reset(eid); | ||
} | ||
} | ||
} | ||
}); // Recursively set all values from a supplied object | ||
Object.defineProperty(managers[$manager], '_set', { | ||
value: (eid, values) => { | ||
for (const prop in values) { | ||
const mapping = managers[$manager]._mapping(prop); | ||
if (mapping && typeof values[prop] === 'string') { | ||
managers[$manager].enum(prop, eid, values[prop]); | ||
} else if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
managers[$manager][prop][eid] = values[prop]; | ||
} else if (Array.isArray(values[prop]) && ArrayBuffer.isView(managers[$manager][prop][eid])) { | ||
managers[$manager][prop][eid].set(values[prop], 0); | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
managers[$manager][prop]._set(eid, values[prop]); | ||
} | ||
} | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], '_get', { | ||
value: eid => { | ||
const obj = {}; | ||
for (const prop of managers[$manager]._props) { | ||
const mapping = managers[$manager]._mapping(prop); | ||
if (mapping) { | ||
obj[prop] = managers[$manager].enum(prop, eid); | ||
} else if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
obj[prop] = managers[$manager][prop][eid]; | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
if (ArrayBuffer.isView(managers[$manager][prop][eid])) { | ||
obj[prop] = Array.from(managers[$manager][prop][eid]); | ||
} else { | ||
obj[prop] = managers[$manager][prop]._get(eid); | ||
} | ||
} | ||
} | ||
return obj; | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], '_props', { | ||
value: props | ||
}); // Aggregate all typedArrays into single kvp array (memoized) | ||
let flattened; | ||
Object.defineProperty(managers[$manager], '_flatten', { | ||
value: (flat = []) => { | ||
if (flattened) return flattened; | ||
for (const prop of managers[$manager]._props) { | ||
if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
flat.push(managers[$manager][prop]); | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
managers[$manager][prop]._flatten(flat); | ||
} | ||
} | ||
flattened = flat; | ||
return flat; | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], 'enum', { | ||
value: (prop, eid, value) => { | ||
const mapping = managers[$manager]._mapping(prop); | ||
if (!mapping) { | ||
console.warn('Property is not an enum.'); | ||
return undefined; | ||
} | ||
if (value) { | ||
const index = mapping.indexOf(value); | ||
if (index === -1) { | ||
console.warn(`Value '${value}' is not part of enum.`); | ||
return undefined; | ||
} | ||
managers[$manager][prop][eid] = index; | ||
} else { | ||
return mapping[managers[$manager][prop][eid]]; | ||
} | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], '_grow', { | ||
value: amount => { | ||
managers[$manager][$managerSize] += amount; | ||
for (const prop of managers[$manager]._props) { | ||
if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
managers[$manager][prop] = grow(managers[$manager][prop], amount); | ||
managers[$manager][prop][$queryShadow] = grow(managers[$manager][prop], amount); | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
if (ArrayBuffer.isView(managers[$manager][prop][eid])) ; else { | ||
managers[$manager][prop]._grow(); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
return managers[$manager]; | ||
}; | ||
const $entityMasks = Symbol('entityMasks'); | ||
const $entityEnabled = Symbol('entityEnabled'); | ||
const $deferredEntityRemovals = Symbol('deferredEntityRemovals'); | ||
const $removedEntities = Symbol('removedEntities'); // 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; | ||
const getEntityCursor = () => globalEntityCursor; | ||
const addEntity = world => { | ||
const removed = world[$removedEntities]; | ||
const size = world[$size]; | ||
const enabled = world[$entityEnabled]; | ||
if (globalEntityCursor >= size - size / 5) { | ||
// if 80% full | ||
const amount = Math.ceil(size / 2 / 4) * 4; // grow by half the original size rounded up to a multiple of 4 | ||
// grow data stores | ||
world[$componentMap].forEach(component => { | ||
component.manager._grow(amount); | ||
}); | ||
world[$size] += amount; // TODO: grow metadata on world mappings for world's internal queries/components | ||
} | ||
const eid = removed.length > 0 ? removed.pop() : globalEntityCursor; | ||
enabled[eid] = 1; | ||
globalEntityCursor++; | ||
return eid; | ||
}; | ||
const removeEntity = (world, eid) => { | ||
const queries = world[$queries]; | ||
const removed = world[$removedEntities]; | ||
const enabled = world[$entityEnabled]; // Check if entity is already removed | ||
if (enabled[eid] === 0) return; // Remove entity from all queries | ||
// TODO: archetype graph | ||
queries.forEach(query => { | ||
queryRemoveEntity(world, query, eid); | ||
}); // Free the entity | ||
removed.push(eid); | ||
enabled[eid] = 0; // Clear component bitmasks | ||
for (let i = 0; i < world[$entityMasks].length; i++) world[$entityMasks][i][eid] = 0; | ||
}; | ||
const diff = (world, query) => { | ||
const q = world[$queryMap].get(query); | ||
q.changed.length = 0; | ||
const flat = q.flatProps; | ||
for (let i = 0; i < q.entities.length; i++) { | ||
const eid = q.entities[i]; | ||
let dirty = false; | ||
for (let pid = 0; pid < flat.length; pid++) { | ||
const prop = flat[pid]; | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
if (prop[eid][i] !== prop[eid][$queryShadow][i]) { | ||
dirty = true; | ||
prop[eid][$queryShadow][i] = prop[eid][i]; | ||
} | ||
} | ||
} else { | ||
if (prop[eid] !== prop[$queryShadow][eid]) { | ||
dirty = true; | ||
prop[$queryShadow][eid] = prop[eid]; | ||
} | ||
} | ||
} | ||
if (dirty) q.changed.push(eid); | ||
} | ||
return q.changed; | ||
}; | ||
const canonicalize = target => { | ||
let componentProps; | ||
let changedProps = new Set(); | ||
if (Array.isArray(target)) { | ||
componentProps = target.map(p => { | ||
if (p._flatten) { | ||
return p._flatten(); | ||
} else if (typeof p === 'function' && p.name === 'QueryChanged') { | ||
p = p(); | ||
if (p._flatten) { | ||
let props = p._flatten(); | ||
props.forEach(x => changedProps.add(x)); | ||
return props; | ||
} | ||
changedProps.add(p); | ||
return [p]; | ||
} | ||
}).reduce((a, v) => a.concat(v), []); | ||
} else { | ||
target[$componentMap].forEach(c => { | ||
componentProps = componentProps.concat(c._flatten()); | ||
}); | ||
} | ||
return [componentProps, changedProps]; | ||
}; | ||
const defineSerializer = (target, maxBytes = 5000000) => { | ||
const buffer = new ArrayBuffer(maxBytes); | ||
const view = new DataView(buffer); | ||
const [componentProps, changedProps] = canonicalize(target); | ||
return ents => { | ||
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; | ||
} | ||
prop[$serializeShadow][eid] = prop[eid]; | ||
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 array values | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
const val = prop[eid][i]; // write array index | ||
view[`set${indexType}`](where, i); | ||
where += indexBytes; // write value at that index | ||
view[`set${type}`](where, val); | ||
where += prop[eid].BYTES_PER_ELEMENT; | ||
count2++; | ||
} | ||
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; | ||
} | ||
} | ||
view.setUint32(countWhere, count); | ||
} | ||
return buffer.slice(0, where); | ||
}; | ||
}; | ||
const defineDeserializer = target => { | ||
const [componentProps] = canonicalize(target); | ||
return packet => { | ||
const view = new DataView(packet); | ||
let where = 0; // 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++) { | ||
const eid = view.getUint32(where); | ||
where += 4; | ||
if (ArrayBuffer.isView(ta[eid])) { | ||
const array = ta[eid]; | ||
const count = view[`get${array._indexType}`]; | ||
where += array._indexBytes; // iterate over count | ||
for (let i = 0; i < count; i++) { | ||
const value = view[`get${array.constructor.name.replace('Array', '')}`](where); | ||
where += array.BYTES_PER_ELEMENT; | ||
ta[eid][i] = value; | ||
} | ||
} else { | ||
let value = view[`get${ta.constructor.name.replace('Array', '')}`](where); | ||
where += ta.BYTES_PER_ELEMENT; | ||
ta[eid] = value; | ||
} | ||
} | ||
}; | ||
}; | ||
function Not(c) { | ||
return function QueryNot() { | ||
return c; | ||
}; | ||
} | ||
function Changed(c) { | ||
return function QueryChanged() { | ||
return c; | ||
}; | ||
} | ||
const $queries = Symbol('queries'); | ||
const $queryMap = Symbol('queryMap'); | ||
const $dirtyQueries = Symbol('$dirtyQueries'); | ||
const $queryComponents = Symbol('queryComponents'); | ||
const NONE = 2 ** 32; | ||
const enterQuery = (world, query, fn) => { | ||
if (!world[$queryMap].has(query)) registerQuery(world, query); | ||
world[$queryMap].get(query).enter = fn; | ||
}; | ||
const exitQuery = (world, query, fn) => { | ||
if (!world[$queryMap].has(query)) registerQuery(world, query); | ||
world[$queryMap].get(query).exit = fn; | ||
}; | ||
const registerQuery = (world, query) => { | ||
if (!world[$queryMap].has(query)) world[$queryMap].set(query, {}); | ||
let components = []; | ||
let notComponents = []; | ||
let changedComponents = []; | ||
query[$queryComponents].forEach(c => { | ||
if (typeof c === 'function') { | ||
if (c.name === 'QueryNot') { | ||
notComponents.push(c()); | ||
} | ||
if (c.name === 'QueryChanged') { | ||
changedComponents.push(c()); | ||
components.push(c()); | ||
} | ||
} else { | ||
components.push(c); | ||
} | ||
}); | ||
const mapComponents = c => world[$componentMap].get(c); | ||
const size = components.reduce((a, c) => c[$managerSize] > a ? c[$managerSize] : a, 0); | ||
const entities = []; | ||
const changed = []; | ||
const indices = new Uint32Array(size).fill(NONE); | ||
const enabled = new Uint8Array(size); | ||
const generations = components.concat(notComponents).map(c => { | ||
if (!world[$componentMap].has(c)) registerComponent(world, c); | ||
return c; | ||
}).map(mapComponents).map(c => c.generationId).reduce((a, v) => { | ||
if (a.includes(v)) return a; | ||
a.push(v); | ||
return a; | ||
}, []); | ||
const reduceBitmasks = (a, c) => { | ||
if (!a[c.generationId]) a[c.generationId] = 0; | ||
a[c.generationId] |= c.bitflag; | ||
return a; | ||
}; | ||
const masks = components.map(mapComponents).reduce(reduceBitmasks, {}); | ||
const notMasks = notComponents.map(mapComponents).reduce((a, c) => { | ||
if (!a[c.generationId]) { | ||
a[c.generationId] = 0; | ||
a[c.generationId] |= c.bitflag; | ||
} | ||
return a; | ||
}, {}); | ||
const flatProps = components.map(c => c._flatten ? c._flatten() : [c]).reduce((a, v) => a.concat(v), []); | ||
const toRemove = []; | ||
Object.assign(world[$queryMap].get(query), { | ||
entities, | ||
changed, | ||
enabled, | ||
components, | ||
notComponents, | ||
changedComponents, | ||
masks, | ||
notMasks, | ||
generations, | ||
indices, | ||
flatProps, | ||
toRemove | ||
}); | ||
world[$queries].add(query); | ||
for (let eid = 0; eid < getEntityCursor(); eid++) { | ||
if (!world[$entityEnabled][eid]) continue; | ||
if (queryCheckEntity(world, query, eid)) { | ||
queryAddEntity(world, query, eid); | ||
} | ||
} | ||
}; | ||
const defineQuery = components => { | ||
const query = function (world) { | ||
if (!world[$queryMap].has(query)) registerQuery(world, query); | ||
queryCommitRemovals(world, query); | ||
const q = world[$queryMap].get(query); | ||
if (q.changedComponents.length) return diff(world, query); | ||
return q.entities; | ||
}; | ||
query[$queryComponents] = components; | ||
return query; | ||
}; // TODO: archetype graph | ||
const queryCheckEntity = (world, query, eid) => { | ||
const { | ||
masks, | ||
notMasks, | ||
generations | ||
} = world[$queryMap].get(query); | ||
for (let i = 0; i < generations.length; i++) { | ||
const generationId = generations[i]; | ||
const qMask = masks[generationId]; | ||
const qNotMask = notMasks[generationId]; | ||
const eMask = world[$entityMasks][generationId][eid]; | ||
if (qNotMask && (eMask & qNotMask) !== 0) { | ||
return false; | ||
} | ||
if (qMask && (eMask & qMask) !== qMask) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
const queryCheckComponent = (world, query, component) => { | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
const { | ||
masks | ||
} = world[$queryMap].get(query); | ||
const mask = masks[generationId]; | ||
return (mask & bitflag) === bitflag; | ||
}; | ||
const queryCheckComponents = (world, query, components) => { | ||
return components.every(c => queryCheckComponent(world, query, c)); | ||
}; | ||
const queryAddEntity = (world, query, eid) => { | ||
const q = world[$queryMap].get(query); | ||
if (q.enabled[eid]) return; | ||
q.enabled[eid] = true; | ||
q.entities.push(eid); | ||
q.indices[eid] = q.entities.length - 1; | ||
if (q.enter) q.enter(eid); | ||
}; | ||
const queryCommitRemovals = (world, query) => { | ||
const q = world[$queryMap].get(query); | ||
while (q.toRemove.length) { | ||
const eid = q.toRemove.pop(); | ||
const index = q.indices[eid]; | ||
if (index === NONE) continue; | ||
const swapped = q.entities.pop(); | ||
if (swapped !== eid) { | ||
q.entities[index] = swapped; | ||
q.indices[swapped] = index; | ||
} | ||
q.indices[eid] = NONE; | ||
} | ||
world[$dirtyQueries].delete(q); | ||
}; | ||
const commitRemovals = world => { | ||
world[$dirtyQueries].forEach(q => { | ||
queryCommitRemovals(q); | ||
}); | ||
}; | ||
const queryRemoveEntity = (world, query, eid) => { | ||
const q = world[$queryMap].get(query); | ||
if (!q.enabled[eid]) return; | ||
q.enabled[eid] = false; | ||
if (q.exit) q.exit(eid); | ||
q.toRemove.push(eid); | ||
world[$dirtyQueries].add(q); | ||
}; | ||
const $componentMap = Symbol('componentMap'); | ||
const $deferredComponentRemovals = Symbol('de$deferredComponentRemovals'); | ||
const defineComponent = schema => alloc(schema); | ||
const incrementBitflag = world => { | ||
world[$bitflag] *= 2; | ||
if (world[$bitflag] >= 2 ** 32) { | ||
world[$bitflag] = 1; | ||
world[$entityMasks].push(new Uint32Array(world[$size])); | ||
} | ||
}; | ||
const registerComponent = (world, component) => { | ||
world[$componentMap].set(component, { | ||
generationId: world[$entityMasks].length - 1, | ||
bitflag: world[$bitflag], | ||
manager: component | ||
}); | ||
incrementBitflag(world); | ||
}; | ||
const registerComponents = (world, components) => { | ||
components.forEach(c => registerComponent(world, c)); | ||
}; | ||
const hasComponent = (world, component, eid) => { | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
const mask = world[$entityMasks][generationId][eid]; | ||
return (mask & bitflag) === bitflag; | ||
}; | ||
const addComponent = (world, component, eid) => { | ||
if (!world[$componentMap].has(component)) registerComponent(world, component); | ||
if (hasComponent(world, component, eid)) return; // Add bitflag to entity bitmask | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
world[$entityMasks][generationId][eid] |= bitflag; // Zero out each property value | ||
// component._reset(eid) | ||
// todo: archetype graph | ||
const queries = world[$queries]; | ||
queries.forEach(query => { | ||
const components = query[$queryComponents]; | ||
if (!queryCheckComponents(world, query, components)) return; | ||
const match = queryCheckEntity(world, query, eid); | ||
if (match) queryAddEntity(world, query, eid); | ||
}); | ||
}; | ||
const removeComponent = (world, component, eid) => { | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
if (!(world[$entityMasks][generationId][eid] & bitflag)) return; // todo: archetype graph | ||
const queries = world[$queries]; | ||
queries.forEach(query => { | ||
const components = query[$queryComponents]; | ||
if (!queryCheckComponents(world, query, components)) return; | ||
const match = queryCheckEntity(world, query, eid); | ||
if (match) queryRemoveEntity(world, query, eid); | ||
}); // Remove flag from entity bitmask | ||
world[$entityMasks][generationId][eid] &= ~bitflag; | ||
}; | ||
const $size = Symbol('size'); | ||
const $bitflag = Symbol('bitflag'); | ||
const createWorld = (size = 1000000) => { | ||
const world = {}; | ||
world[$size] = size; | ||
world[$entityEnabled] = new Uint8Array(size); | ||
world[$entityMasks] = [new Uint32Array(size)]; | ||
world[$removedEntities] = []; | ||
world[$bitflag] = 1; | ||
world[$componentMap] = new Map(); | ||
world[$queryMap] = new Map(); | ||
world[$queries] = new Set(); | ||
world[$dirtyQueries] = new Set(); | ||
world[$deferredComponentRemovals] = []; | ||
world[$deferredEntityRemovals] = []; | ||
return world; | ||
}; | ||
const defineSystem = update => { | ||
const system = world => { | ||
update(world); | ||
commitRemovals(world); | ||
}; | ||
Object.defineProperty(system, 'name', { | ||
value: (update.name || "AnonymousSystem") + "_internal", | ||
configurable: true | ||
}); | ||
return system; | ||
}; | ||
const pipe = fns => world => { | ||
for (let i = 0; i < fns.length; i++) { | ||
const fn = fns[i]; | ||
fn(world); | ||
} | ||
}; | ||
const Types = TYPES_ENUM; | ||
export { Changed, Not, Types, addComponent, addEntity, commitRemovals, createWorld, defineComponent, defineDeserializer, defineQuery, defineSerializer, defineSystem, enterQuery, exitQuery, hasComponent, pipe, registerComponent, registerComponents, removeComponent, removeEntity }; | ||
const e={bool:"Uint8",i8:"Int8",ui8:"Uint8",ui8c:"Uint8Clamped",i16:"Int16",ui16:"Uint16",i32:"Int32",ui32:"Uint32",f32:"Float32",f64:"Float64"},t={bool:"bool",i8:Int8Array,ui8:Uint8Array,ui8c:Uint8ClampedArray,i16:Int16Array,ui16:Uint16Array,i32:Int32Array,ui32:Uint32Array,f32:Float32Array,f64:Float64Array},n=256,r=65536,o=e=>4*Math.ceil(e/4),s=Symbol("storeRef"),i=Symbol("storeSize"),a=Symbol("storeMaps"),c=Symbol("storeFlattened"),l=Symbol("storeArrayCount"),u=Symbol("storeSubarrays"),f=Symbol("storeCursor"),y=Symbol("subarrayCursors"),d=Symbol("subarray"),g=Symbol("queryShadow"),b=Symbol("serializeShadow"),h=Symbol("indexType"),p=Symbol("indexBytes"),m={},E=(e,t)=>{const n=new ArrayBuffer(t*e.BYTES_PER_ELEMENT),r=new e.constructor(n);return r.set(e,0),r},S=(e,t)=>{Object.keys(e).forEach((n=>{const r=e[n];r[d]||(ArrayBuffer.isView(r)?(e[n]=E(r,t),e[n][g]=E(r[g],t),e[n][b]=E(r[b],t)):"object"==typeof r&&S(e[n],t))}))},A=(e,n)=>{e[i]=n,S(e,n),(e=>{const n=e[i],r=e[y]={};Object.keys(e[u]).forEach((s=>{const i=e[l],a=e[0].length,c=Array(i).fill(0).reduce(((e,n)=>e+t[s].BYTES_PER_ELEMENT),0),f=Array(i).fill(0).reduce(((e,t)=>e+a),0),y=new ArrayBuffer(o(c*f*n)),g=new t[s](y);g.set(e[u][s].buffer,0),e[u][s]=g;for(let t=0;t<n;t++){const n=r[s]+t*a,o=n+a;e[t]=e[u][s].subarray(n,o),e[t][d]=!0}}))})(e)},w=(e,n)=>{const r=n*t[e].BYTES_PER_ELEMENT,o=new ArrayBuffer(r);return new t[e](o)},_=(s,a,c)=>{const f=s[i],m=s[y],E=c<n?"ui8":c<r?"ui16":"ui32";if(!c)throw new Error("❌ Must define a length for component array.");if(!t[a])throw new Error(`❌ Invalid component array property type ${a}.`);if(!s[u][a]){const n=s[l],r=Array(n).fill(0).reduce(((e,n)=>e+t[a].BYTES_PER_ELEMENT),0),i=Array(n).fill(0).reduce(((e,t)=>e+c),0),y=o(r*i*f),d=new ArrayBuffer(y),m=new t[a](d);s[u][a]=m,s[u][a][g]=m.slice(0),s[u][a][b]=m.slice(0),m[h]=e[E],m[p]=t[E].BYTES_PER_ELEMENT}let S=0;for(let e=0;e<f;e++){const t=m[a]+e*c,n=t+c;s[e]=s[u][a].subarray(t,n),s[e][d]=!0,S=n}return m[a]=S,s},B=e=>{e[g]=e.slice(0),e[b]=e.slice(0)},M=e=>Array.isArray(e)&&"object"==typeof e[0]&&e[0].hasOwnProperty("type")&&e[0].hasOwnProperty("length"),T=Symbol("entityMasks"),U=Symbol("entityEnabled"),I=Symbol("deferredEntityRemovals"),j=Symbol("removedEntities");let O=0;const R=e=>{const t=e[j],n=e[ae],r=e[U];if(O>=n-n/5){const t=4*Math.ceil(n/2/4);e[ae]+=t,e[Z].forEach((t=>{A(t.store,e[ae])})),e[Y].forEach((t=>{t.indices=E(t.indices,e[ae]),t.enabled=E(t.enabled,e[ae])}))}const o=t.length>0?t.pop():O;return r[o]=1,O++,o},k=(e,t)=>{const n=e[L],r=e[j],o=e[U];if(0!==o[t]){n.forEach((n=>{X(e,n,t)})),r.push(t),o[t]=0;for(let n=0;n<e[T].length;n++)e[T][n][t]=0}},P=e=>{let t,n=new Set;return Array.isArray(e)?t=e.map((e=>{if(e._flatten)return e._flatten();if("function"==typeof e&&"QueryChanged"===e.name){if((e=e())._flatten){let t=e._flatten();return t.forEach((e=>n.add(e))),t}return n.add(e),[e]}})).reduce(((e,t)=>e.concat(t)),[]):e[Z].forEach((e=>{t=t.concat(e._flatten())})),[t,n]},C=(e,t=5e6)=>{const n=new ArrayBuffer(t),r=new DataView(n),[o,s]=P(e);return e=>{if(!e.length)return;let t=0;for(let n=0;n<o.length;n++){const i=o[n],a=s.has(i);r.setUint8(t,n),t+=1;const c=t;t+=4;let l=0;for(let n=0;n<e.length;n++){const o=e[n];if(!a||i[o]!==i[b][o])if(l++,r.setUint32(t,o),t+=4,ArrayBuffer.isView(i[o])){const e=i[o].constructor.name.replace("Array",""),n=i[o]._indexType,s=i[o]._indexBytes,a=t;t+=1;let c=0;for(let a=0;a<i[o].length;a++){const l=i[o][a];r["set"+n](t,a),t+=s,r["set"+e](t,l),t+=i[o].BYTES_PER_ELEMENT,c++}r["set"+n](a,c)}else{const e=i.constructor.name.replace("Array","");r["set"+e](t,i[o]),t+=i.BYTES_PER_ELEMENT,i[b]||console.log(i),i[b][o]=i[o]}}r.setUint32(c,l)}return n.slice(0,t)}},v=e=>{const[t]=P(e);return e=>{const n=new DataView(e);let r=0;const o=n.getUint8(r);r+=1;const s=n.getUint32(r);r+=4;const i=t[o];for(let e=0;e<s;e++){const e=n.getUint32(r);if(r+=4,ArrayBuffer.isView(i[e])){const t=i[e],o=n["get"+t._indexType];r+=t._indexBytes;for(let s=0;s<o;s++){const o=n["get"+t.constructor.name.replace("Array","")](r);r+=t.BYTES_PER_ELEMENT,i[e][s]=o}}else{let t=n["get"+i.constructor.name.replace("Array","")](r);r+=i.BYTES_PER_ELEMENT,i[e]=t}}}};function x(e){return function(){return e}}function N(e){return function(){return e}}const L=Symbol("queries"),Y=Symbol("queryMap"),V=Symbol("$dirtyQueries"),F=Symbol("queryComponents"),q=2**32,Q=(e,t,n)=>{e[Y].has(t)||$(e,t),e[Y].get(t).enter=n},z=(e,t,n)=>{e[Y].has(t)||$(e,t),e[Y].get(t).exit=n},$=(e,t)=>{e[Y].has(t)||e[Y].set(t,{});let n=[],r=[],o=[];t[F].forEach((e=>{"function"==typeof e?("QueryNot"===e.name&&r.push(e()),"QueryChanged"===e.name&&(o.push(e()),n.push(e()))):n.push(e)}));const s=t=>e[Z].get(t),a=n.reduce(((e,t)=>t[i]>e?t[i]:e),0),c=new Uint32Array(a).fill(q),l=new Uint8Array(a),u=n.concat(r).map((t=>(e[Z].has(t)||ne(e,t),t))).map(s).map((e=>e.generationId)).reduce(((e,t)=>(e.includes(t)||e.push(t),e)),[]),f=n.map(s).reduce(((e,t)=>(e[t.generationId]||(e[t.generationId]=0),e[t.generationId]|=t.bitflag,e)),{}),y=r.map(s).reduce(((e,t)=>(e[t.generationId]||(e[t.generationId]=0,e[t.generationId]|=t.bitflag),e)),{}),d=n.map((e=>e._flatten?e._flatten():[e])).reduce(((e,t)=>e.concat(t)),[]);Object.assign(e[Y].get(t),{entities:[],changed:[],enabled:l,components:n,notComponents:r,changedComponents:o,masks:f,notMasks:y,generations:u,indices:c,flatProps:d,toRemove:[]}),e[L].add(t);for(let n=0;n<O;n++)e[U][n]&&G(e,t,n)&&J(e,t,n)},D=e=>{const t=function(e){e[Y].has(t)||$(e,t),K(e,t);const n=e[Y].get(t);return n.changedComponents.length?((e,t)=>{const n=e[Y].get(t);n.changed.length=0;const r=n.flatProps;for(let e=0;e<n.entities.length;e++){const t=n.entities[e];let o=!1;for(let e=0;e<r.length;e++){const n=r[e];if(ArrayBuffer.isView(n[t]))for(let e=0;e<n[t].length;e++)n[t][e]!==n[t][g][e]&&(o=!0,n[t][g][e]=n[t][e]);else n[t]!==n[g][t]&&(o=!0,n[g][t]=n[t])}o&&n.changed.push(t)}return n.changed})(e,t):n.entities};return t[F]=e,t},G=(e,t,n)=>{const{masks:r,notMasks:o,generations:s}=e[Y].get(t);for(let t=0;t<s.length;t++){const i=s[t],a=r[i],c=o[i],l=e[T][i][n];if(c&&0!=(l&c))return!1;if(a&&(l&a)!==a)return!1}return!0},H=(e,t,n)=>n.every((n=>((e,t,n)=>{const{generationId:r,bitflag:o}=e[Z].get(n),{masks:s}=e[Y].get(t);return(s[r]&o)===o})(e,t,n))),J=(e,t,n)=>{const r=e[Y].get(t);r.enabled[n]||(r.enabled[n]=!0,r.entities.push(n),r.indices[n]=r.entities.length-1,r.enter&&r.enter(n))},K=(e,t)=>{const n=e[Y].get(t);for(;n.toRemove.length;){const e=n.toRemove.pop(),t=n.indices[e];if(t===q)continue;const r=n.entities.pop();r!==e&&(n.entities[t]=r,n.indices[r]=t),n.indices[e]=q}e[V].delete(n)},W=e=>{e[V].forEach((e=>{K(e)}))},X=(e,t,n)=>{const r=e[Y].get(t);r.enabled[n]&&(r.enabled[n]=!1,r.exit&&r.exit(n),r.toRemove.push(n),e[V].add(r))},Z=Symbol("componentMap"),ee=Symbol("de$deferredComponentRemovals"),te=e=>((e,n=1e6)=>{const r=Symbol("store");if("Map"===e.constructor.name)return e[i]=n,e;const o=(t,n)=>(M(e[n])?t++:e[n]instanceof Object&&(t+=Object.keys(e[n]).reduce(o,0)),t),d=M(e)?1:Object.keys(e).reduce(o,0),g={[i]:n,[a]:{},[u]:{},[s]:r,[f]:0,[y]:Object.keys(t).reduce(((e,t)=>({...e,[t]:0})),{}),[l]:d,[c]:[]};if("string"==typeof e)return m[r]=Object.assign(w(e,n),g),g[c].push(m[r]),B(m[r]),m[r];if(M(e)){const{type:t,length:n}=e[0];return m[r]=Object.assign(_(g,t,n),g),g[c].push(m[r]),m[r]}if(e instanceof Object&&Object.keys(e).length){const t=(e,r)=>{if("string"==typeof e[r])e[r]=w(e[r],n),g[c].push(e[r]),B(e[r]);else if(M(e[r])){const{type:t,length:n}=e[r][0];e[r]=_(g,t,n)}else e[r]instanceof Object&&(e[r]=Object.keys(e[r]).reduce(t,e[r]));return e};return m[r]=Object.assign(Object.keys(e).reduce(t,e),g),m[r]}return{}})(e),ne=(e,t)=>{e[Z].set(t,{generationId:e[T].length-1,bitflag:e[ce],store:t}),(e=>{e[ce]*=2,e[ce]>=2**32&&(e[ce]=1,e[T].push(new Uint32Array(e[ae])))})(e)},re=(e,t)=>{t.forEach((t=>ne(e,t)))},oe=(e,t,n)=>{const{generationId:r,bitflag:o}=e[Z].get(t);return(e[T][r][n]&o)===o},se=(e,t,n)=>{if(e[Z].has(t)||ne(e,t),oe(e,t,n))return;const{generationId:r,bitflag:o}=e[Z].get(t);e[T][r][n]|=o;e[L].forEach((t=>{const r=t[F];if(!H(e,t,r))return;G(e,t,n)&&J(e,t,n)}))},ie=(e,t,n)=>{const{generationId:r,bitflag:o}=e[Z].get(t);if(!(e[T][r][n]&o))return;e[L].forEach((t=>{const r=t[F];if(!H(e,t,r))return;G(e,t,n)&&X(e,t,n)})),e[T][r][n]&=~o},ae=Symbol("size"),ce=Symbol("bitflag"),le=(e=1e6)=>{const t={};return t[ae]=e,t[U]=new Uint8Array(e),t[T]=[new Uint32Array(e)],t[j]=[],t[ce]=1,t[Z]=new Map,t[Y]=new Map,t[L]=new Set,t[V]=new Set,t[ee]=[],t[I]=[],t},ue=e=>{const t=t=>{e(t),W(t)};return Object.defineProperty(t,"name",{value:(e.name||"AnonymousSystem")+"_internal",configurable:!0}),t},fe=(...e)=>t=>{e=Array.isArray(e[0])?e[0]:e;for(let n=0;n<e.length;n++){(0,e[n])(t)}},ye={bool:"bool",i8:"i8",ui8:"ui8",ui8c:"ui8c",i16:"i16",ui16:"ui16",i32:"i32",ui32:"ui32",f32:"f32",f64:"f64"};export{N as Changed,x as Not,ye as Types,se as addComponent,R as addEntity,W as commitRemovals,le as createWorld,te as defineComponent,v as defineDeserializer,D as defineQuery,C as defineSerializer,ue as defineSystem,Q as enterQuery,z as exitQuery,oe as hasComponent,fe as pipe,ne as registerComponent,re as registerComponents,ie as removeComponent,k as removeEntity}; | ||
//# sourceMappingURL=index.es.js.map |
@@ -1,851 +0,2 @@ | ||
'use strict'; | ||
Object.defineProperty(exports, '__esModule', { value: true }); | ||
const TYPES_ENUM = { | ||
bool: 'bool', | ||
i8: 'i8', | ||
ui8: 'ui8', | ||
ui8c: 'ui8c', | ||
i16: 'i16', | ||
ui16: 'ui16', | ||
i32: 'i32', | ||
ui32: 'ui32', | ||
f32: 'f32', | ||
f64: 'f64' | ||
}; | ||
const TYPES = { | ||
bool: 'bool', | ||
i8: Int8Array, | ||
ui8: Uint8Array, | ||
ui8c: Uint8ClampedArray, | ||
i16: Int16Array, | ||
ui16: Uint16Array, | ||
i32: Int32Array, | ||
ui32: Uint32Array, | ||
f32: Float32Array, | ||
f64: Float64Array | ||
}; | ||
const UNSIGNED_MAX = { | ||
uint8: 255, | ||
uint16: 65535, | ||
uint32: 4294967295 | ||
}; | ||
const grow = (ta, amount) => { | ||
const newTa = new ta.constructor(new ArrayBuffer(ta.buffer.byteLength + amount * ta.BYTES_PER_ELEMENT)); | ||
newTa.set(ta.buffer); | ||
return newTa; | ||
}; | ||
const roundToMultiple4 = x => Math.ceil(x / 4) * 4; | ||
const managers = {}; | ||
const $managerRef = Symbol('managerRef'); | ||
const $managerSize = Symbol('managerSize'); | ||
const $managerMaps = Symbol('maps'); | ||
const $managerSubarrays = Symbol('subarrays'); | ||
const $managerCursor = Symbol('managerCursor'); | ||
const $managerRemoved = Symbol('managerRemoved'); | ||
const $queryShadow = Symbol('queryShadow'); | ||
const $serializeShadow = Symbol('$serializeShadow'); | ||
const alloc = (schema, size = 1000000) => { | ||
const $manager = Symbol('manager'); | ||
if (schema.constructor.name === 'Map') { | ||
schema[$managerSize] = size; | ||
return schema; | ||
} | ||
managers[$manager] = { | ||
[$managerSize]: size, | ||
[$managerMaps]: {}, | ||
[$managerSubarrays]: {}, | ||
[$managerRef]: $manager, | ||
[$managerCursor]: 0, | ||
[$managerRemoved]: [] | ||
}; | ||
const props = schema ? Object.keys(schema) : []; | ||
let arrays = props.filter(p => Array.isArray(schema[p]) && typeof schema[p][0] === 'object'); | ||
const cursors = Object.keys(TYPES).reduce((a, type) => ({ ...a, | ||
[type]: 0 | ||
}), {}); | ||
if (typeof schema === 'string') { | ||
const type = schema; | ||
const totalBytes = size * TYPES[type].BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager] = new TYPES[type](buffer); | ||
} else if (Array.isArray(schema)) { | ||
arrays = schema; | ||
const { | ||
type, | ||
length | ||
} = schema[0]; | ||
const indexType = length < UNSIGNED_MAX.uint8 ? 'ui8' : length < UNSIGNED_MAX.uint16 ? 'ui16' : 'ui32'; | ||
if (!length) throw new Error('❌ Must define a length for component array.'); | ||
if (!TYPES[type]) throw new Error(`❌ Invalid component array property type ${type}.`); // create buffer for type if it does not already exist | ||
if (!managers[$manager][$managerSubarrays][type]) { | ||
const relevantArrays = arrays; | ||
const summedBytesPerElement = relevantArrays.reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0); | ||
const summedLength = relevantArrays.reduce((a, p) => a + length, 0); | ||
const buffer = new ArrayBuffer(roundToMultiple4(summedBytesPerElement * summedLength * size)); | ||
const array = new TYPES[type](buffer); | ||
array._indexType = indexType; | ||
array._indexBytes = TYPES[indexType].BYTES_PER_ELEMENT; | ||
managers[$manager][$managerSubarrays][type] = array; | ||
} // pre-generate subarrays for each eid | ||
let end = 0; | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
managers[$manager][eid] = managers[$manager][$managerSubarrays][type].subarray(from, to); | ||
end = to; | ||
} | ||
cursors[type] = end; | ||
managers[$manager]._reset = eid => managers[$manager][eid].fill(0); | ||
managers[$manager]._set = (eid, values) => managers[$manager][eid].set(values, 0); | ||
} else props.forEach(prop => { | ||
// Boolean Type | ||
if (schema[prop] === 'bool') { | ||
const Type = TYPES.uint8; | ||
const totalBytes = size * TYPES.uint8.BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][$managerMaps][prop] = schema[prop]; | ||
managers[$manager][prop] = new Type(buffer); | ||
managers[$manager][prop]._boolType = true; // Enum Type | ||
} else if (Array.isArray(schema[prop]) && typeof schema[prop][0] === 'string') { | ||
const Type = TYPES.uint8; | ||
const totalBytes = size * TYPES.uint8.BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][$managerMaps][prop] = schema[prop]; | ||
managers[$manager][prop] = new Type(buffer); // Array Type | ||
} else if (Array.isArray(schema[prop]) && typeof schema[prop][0] === 'object') { | ||
const { | ||
type, | ||
length | ||
} = schema[0]; | ||
if (!length) throw new Error('❌ Must define a length for component array.'); | ||
if (!TYPES[type]) throw new Error(`❌ Invalid component array property type ${type}.`); // create buffer for type if it does not already exist | ||
if (!managers[$manager][$managerSubarrays][type]) { | ||
const relevantArrays = arrays.filter(p => schema[p][0].type === type); | ||
const summedBytesPerElement = relevantArrays.reduce((a, p) => a + TYPES[type].BYTES_PER_ELEMENT, 0); | ||
const summedLength = relevantArrays.reduce((a, p) => a + length, 0); | ||
const buffer = new ArrayBuffer(roundToMultiple4(summedBytesPerElement * summedLength * size)); | ||
const array = new TYPES[type](buffer); | ||
array._indexType = index; | ||
array._indexBytes = TYPES[index].BYTES_PER_ELEMENT; | ||
managers[$manager][$managerSubarrays][type] = array; | ||
} // pre-generate subarrays for each eid | ||
managers[$manager][prop] = {}; | ||
let end = 0; | ||
for (let eid = 0; eid < size; eid++) { | ||
const from = cursors[type] + eid * length; | ||
const to = from + length; | ||
managers[$manager][prop][eid] = managers[$manager][$managerSubarrays][type].subarray(from, to); | ||
end = to; | ||
} | ||
cursors[type] = end; | ||
managers[$manager][prop]._reset = eid => managers[$manager][prop][eid].fill(0); | ||
managers[$manager][prop]._set = (eid, values) => managers[$manager][prop][eid].set(values, 0); // Object Type | ||
} else if (typeof schema[prop] === 'object') { | ||
managers[$manager][prop] = Manager(size, schema[prop], false); // String Type | ||
} else if (typeof schema[prop] === 'string') { | ||
const type = schema[prop]; | ||
const totalBytes = size * TYPES[type].BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
const queryShadowBuffer = new ArrayBuffer(totalBytes); | ||
const serializeShadowBuffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][prop] = new TYPES[type](buffer); | ||
managers[$manager][prop][$queryShadow] = new TYPES[type](queryShadowBuffer); | ||
managers[$manager][prop][$serializeShadow] = new TYPES[type](serializeShadowBuffer); // TypedArray Type | ||
} else if (typeof schema[prop] === 'function') { | ||
const Type = schema[prop]; | ||
const totalBytes = size * Type.BYTES_PER_ELEMENT; | ||
const buffer = new ArrayBuffer(totalBytes); | ||
managers[$manager][prop] = new Type(buffer); | ||
} else { | ||
throw new Error(`ECS Error: invalid property type ${schema[prop]}`); | ||
} | ||
}); // methods | ||
Object.defineProperty(managers[$manager], '_schema', { | ||
value: schema | ||
}); | ||
Object.defineProperty(managers[$manager], '_mapping', { | ||
value: prop => managers[$manager][$managerMaps][prop] | ||
}); // Recursively set all values to 0 | ||
Object.defineProperty(managers[$manager], '_reset', { | ||
value: eid => { | ||
for (const prop of managers[$manager]._props) { | ||
if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
if (ArrayBuffer.isView(managers[$manager][prop][eid])) { | ||
managers[$manager][prop][eid].fill(0); | ||
} else { | ||
managers[$manager][prop][eid] = 0; | ||
} | ||
} else { | ||
managers[$manager][prop]._reset(eid); | ||
} | ||
} | ||
} | ||
}); // Recursively set all values from a supplied object | ||
Object.defineProperty(managers[$manager], '_set', { | ||
value: (eid, values) => { | ||
for (const prop in values) { | ||
const mapping = managers[$manager]._mapping(prop); | ||
if (mapping && typeof values[prop] === 'string') { | ||
managers[$manager].enum(prop, eid, values[prop]); | ||
} else if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
managers[$manager][prop][eid] = values[prop]; | ||
} else if (Array.isArray(values[prop]) && ArrayBuffer.isView(managers[$manager][prop][eid])) { | ||
managers[$manager][prop][eid].set(values[prop], 0); | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
managers[$manager][prop]._set(eid, values[prop]); | ||
} | ||
} | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], '_get', { | ||
value: eid => { | ||
const obj = {}; | ||
for (const prop of managers[$manager]._props) { | ||
const mapping = managers[$manager]._mapping(prop); | ||
if (mapping) { | ||
obj[prop] = managers[$manager].enum(prop, eid); | ||
} else if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
obj[prop] = managers[$manager][prop][eid]; | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
if (ArrayBuffer.isView(managers[$manager][prop][eid])) { | ||
obj[prop] = Array.from(managers[$manager][prop][eid]); | ||
} else { | ||
obj[prop] = managers[$manager][prop]._get(eid); | ||
} | ||
} | ||
} | ||
return obj; | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], '_props', { | ||
value: props | ||
}); // Aggregate all typedArrays into single kvp array (memoized) | ||
let flattened; | ||
Object.defineProperty(managers[$manager], '_flatten', { | ||
value: (flat = []) => { | ||
if (flattened) return flattened; | ||
for (const prop of managers[$manager]._props) { | ||
if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
flat.push(managers[$manager][prop]); | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
managers[$manager][prop]._flatten(flat); | ||
} | ||
} | ||
flattened = flat; | ||
return flat; | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], 'enum', { | ||
value: (prop, eid, value) => { | ||
const mapping = managers[$manager]._mapping(prop); | ||
if (!mapping) { | ||
console.warn('Property is not an enum.'); | ||
return undefined; | ||
} | ||
if (value) { | ||
const index = mapping.indexOf(value); | ||
if (index === -1) { | ||
console.warn(`Value '${value}' is not part of enum.`); | ||
return undefined; | ||
} | ||
managers[$manager][prop][eid] = index; | ||
} else { | ||
return mapping[managers[$manager][prop][eid]]; | ||
} | ||
} | ||
}); | ||
Object.defineProperty(managers[$manager], '_grow', { | ||
value: amount => { | ||
managers[$manager][$managerSize] += amount; | ||
for (const prop of managers[$manager]._props) { | ||
if (ArrayBuffer.isView(managers[$manager][prop])) { | ||
managers[$manager][prop] = grow(managers[$manager][prop], amount); | ||
managers[$manager][prop][$queryShadow] = grow(managers[$manager][prop], amount); | ||
} else if (typeof managers[$manager][prop] === 'object') { | ||
if (ArrayBuffer.isView(managers[$manager][prop][eid])) ; else { | ||
managers[$manager][prop]._grow(); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
return managers[$manager]; | ||
}; | ||
const $entityMasks = Symbol('entityMasks'); | ||
const $entityEnabled = Symbol('entityEnabled'); | ||
const $deferredEntityRemovals = Symbol('deferredEntityRemovals'); | ||
const $removedEntities = Symbol('removedEntities'); // 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; | ||
const getEntityCursor = () => globalEntityCursor; | ||
const addEntity = world => { | ||
const removed = world[$removedEntities]; | ||
const size = world[$size]; | ||
const enabled = world[$entityEnabled]; | ||
if (globalEntityCursor >= size - size / 5) { | ||
// if 80% full | ||
const amount = Math.ceil(size / 2 / 4) * 4; // grow by half the original size rounded up to a multiple of 4 | ||
// grow data stores | ||
world[$componentMap].forEach(component => { | ||
component.manager._grow(amount); | ||
}); | ||
world[$size] += amount; // TODO: grow metadata on world mappings for world's internal queries/components | ||
} | ||
const eid = removed.length > 0 ? removed.pop() : globalEntityCursor; | ||
enabled[eid] = 1; | ||
globalEntityCursor++; | ||
return eid; | ||
}; | ||
const removeEntity = (world, eid) => { | ||
const queries = world[$queries]; | ||
const removed = world[$removedEntities]; | ||
const enabled = world[$entityEnabled]; // Check if entity is already removed | ||
if (enabled[eid] === 0) return; // Remove entity from all queries | ||
// TODO: archetype graph | ||
queries.forEach(query => { | ||
queryRemoveEntity(world, query, eid); | ||
}); // Free the entity | ||
removed.push(eid); | ||
enabled[eid] = 0; // Clear component bitmasks | ||
for (let i = 0; i < world[$entityMasks].length; i++) world[$entityMasks][i][eid] = 0; | ||
}; | ||
const diff = (world, query) => { | ||
const q = world[$queryMap].get(query); | ||
q.changed.length = 0; | ||
const flat = q.flatProps; | ||
for (let i = 0; i < q.entities.length; i++) { | ||
const eid = q.entities[i]; | ||
let dirty = false; | ||
for (let pid = 0; pid < flat.length; pid++) { | ||
const prop = flat[pid]; | ||
if (ArrayBuffer.isView(prop[eid])) { | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
if (prop[eid][i] !== prop[eid][$queryShadow][i]) { | ||
dirty = true; | ||
prop[eid][$queryShadow][i] = prop[eid][i]; | ||
} | ||
} | ||
} else { | ||
if (prop[eid] !== prop[$queryShadow][eid]) { | ||
dirty = true; | ||
prop[$queryShadow][eid] = prop[eid]; | ||
} | ||
} | ||
} | ||
if (dirty) q.changed.push(eid); | ||
} | ||
return q.changed; | ||
}; | ||
const canonicalize = target => { | ||
let componentProps; | ||
let changedProps = new Set(); | ||
if (Array.isArray(target)) { | ||
componentProps = target.map(p => { | ||
if (p._flatten) { | ||
return p._flatten(); | ||
} else if (typeof p === 'function' && p.name === 'QueryChanged') { | ||
p = p(); | ||
if (p._flatten) { | ||
let props = p._flatten(); | ||
props.forEach(x => changedProps.add(x)); | ||
return props; | ||
} | ||
changedProps.add(p); | ||
return [p]; | ||
} | ||
}).reduce((a, v) => a.concat(v), []); | ||
} else { | ||
target[$componentMap].forEach(c => { | ||
componentProps = componentProps.concat(c._flatten()); | ||
}); | ||
} | ||
return [componentProps, changedProps]; | ||
}; | ||
const defineSerializer = (target, maxBytes = 5000000) => { | ||
const buffer = new ArrayBuffer(maxBytes); | ||
const view = new DataView(buffer); | ||
const [componentProps, changedProps] = canonicalize(target); | ||
return ents => { | ||
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; | ||
} | ||
prop[$serializeShadow][eid] = prop[eid]; | ||
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 array values | ||
for (let i = 0; i < prop[eid].length; i++) { | ||
const val = prop[eid][i]; // write array index | ||
view[`set${indexType}`](where, i); | ||
where += indexBytes; // write value at that index | ||
view[`set${type}`](where, val); | ||
where += prop[eid].BYTES_PER_ELEMENT; | ||
count2++; | ||
} | ||
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; | ||
} | ||
} | ||
view.setUint32(countWhere, count); | ||
} | ||
return buffer.slice(0, where); | ||
}; | ||
}; | ||
const defineDeserializer = target => { | ||
const [componentProps] = canonicalize(target); | ||
return packet => { | ||
const view = new DataView(packet); | ||
let where = 0; // 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++) { | ||
const eid = view.getUint32(where); | ||
where += 4; | ||
if (ArrayBuffer.isView(ta[eid])) { | ||
const array = ta[eid]; | ||
const count = view[`get${array._indexType}`]; | ||
where += array._indexBytes; // iterate over count | ||
for (let i = 0; i < count; i++) { | ||
const value = view[`get${array.constructor.name.replace('Array', '')}`](where); | ||
where += array.BYTES_PER_ELEMENT; | ||
ta[eid][i] = value; | ||
} | ||
} else { | ||
let value = view[`get${ta.constructor.name.replace('Array', '')}`](where); | ||
where += ta.BYTES_PER_ELEMENT; | ||
ta[eid] = value; | ||
} | ||
} | ||
}; | ||
}; | ||
function Not(c) { | ||
return function QueryNot() { | ||
return c; | ||
}; | ||
} | ||
function Changed(c) { | ||
return function QueryChanged() { | ||
return c; | ||
}; | ||
} | ||
const $queries = Symbol('queries'); | ||
const $queryMap = Symbol('queryMap'); | ||
const $dirtyQueries = Symbol('$dirtyQueries'); | ||
const $queryComponents = Symbol('queryComponents'); | ||
const NONE = 2 ** 32; | ||
const enterQuery = (world, query, fn) => { | ||
if (!world[$queryMap].has(query)) registerQuery(world, query); | ||
world[$queryMap].get(query).enter = fn; | ||
}; | ||
const exitQuery = (world, query, fn) => { | ||
if (!world[$queryMap].has(query)) registerQuery(world, query); | ||
world[$queryMap].get(query).exit = fn; | ||
}; | ||
const registerQuery = (world, query) => { | ||
if (!world[$queryMap].has(query)) world[$queryMap].set(query, {}); | ||
let components = []; | ||
let notComponents = []; | ||
let changedComponents = []; | ||
query[$queryComponents].forEach(c => { | ||
if (typeof c === 'function') { | ||
if (c.name === 'QueryNot') { | ||
notComponents.push(c()); | ||
} | ||
if (c.name === 'QueryChanged') { | ||
changedComponents.push(c()); | ||
components.push(c()); | ||
} | ||
} else { | ||
components.push(c); | ||
} | ||
}); | ||
const mapComponents = c => world[$componentMap].get(c); | ||
const size = components.reduce((a, c) => c[$managerSize] > a ? c[$managerSize] : a, 0); | ||
const entities = []; | ||
const changed = []; | ||
const indices = new Uint32Array(size).fill(NONE); | ||
const enabled = new Uint8Array(size); | ||
const generations = components.concat(notComponents).map(c => { | ||
if (!world[$componentMap].has(c)) registerComponent(world, c); | ||
return c; | ||
}).map(mapComponents).map(c => c.generationId).reduce((a, v) => { | ||
if (a.includes(v)) return a; | ||
a.push(v); | ||
return a; | ||
}, []); | ||
const reduceBitmasks = (a, c) => { | ||
if (!a[c.generationId]) a[c.generationId] = 0; | ||
a[c.generationId] |= c.bitflag; | ||
return a; | ||
}; | ||
const masks = components.map(mapComponents).reduce(reduceBitmasks, {}); | ||
const notMasks = notComponents.map(mapComponents).reduce((a, c) => { | ||
if (!a[c.generationId]) { | ||
a[c.generationId] = 0; | ||
a[c.generationId] |= c.bitflag; | ||
} | ||
return a; | ||
}, {}); | ||
const flatProps = components.map(c => c._flatten ? c._flatten() : [c]).reduce((a, v) => a.concat(v), []); | ||
const toRemove = []; | ||
Object.assign(world[$queryMap].get(query), { | ||
entities, | ||
changed, | ||
enabled, | ||
components, | ||
notComponents, | ||
changedComponents, | ||
masks, | ||
notMasks, | ||
generations, | ||
indices, | ||
flatProps, | ||
toRemove | ||
}); | ||
world[$queries].add(query); | ||
for (let eid = 0; eid < getEntityCursor(); eid++) { | ||
if (!world[$entityEnabled][eid]) continue; | ||
if (queryCheckEntity(world, query, eid)) { | ||
queryAddEntity(world, query, eid); | ||
} | ||
} | ||
}; | ||
const defineQuery = components => { | ||
const query = function (world) { | ||
if (!world[$queryMap].has(query)) registerQuery(world, query); | ||
queryCommitRemovals(world, query); | ||
const q = world[$queryMap].get(query); | ||
if (q.changedComponents.length) return diff(world, query); | ||
return q.entities; | ||
}; | ||
query[$queryComponents] = components; | ||
return query; | ||
}; // TODO: archetype graph | ||
const queryCheckEntity = (world, query, eid) => { | ||
const { | ||
masks, | ||
notMasks, | ||
generations | ||
} = world[$queryMap].get(query); | ||
for (let i = 0; i < generations.length; i++) { | ||
const generationId = generations[i]; | ||
const qMask = masks[generationId]; | ||
const qNotMask = notMasks[generationId]; | ||
const eMask = world[$entityMasks][generationId][eid]; | ||
if (qNotMask && (eMask & qNotMask) !== 0) { | ||
return false; | ||
} | ||
if (qMask && (eMask & qMask) !== qMask) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
}; | ||
const queryCheckComponent = (world, query, component) => { | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
const { | ||
masks | ||
} = world[$queryMap].get(query); | ||
const mask = masks[generationId]; | ||
return (mask & bitflag) === bitflag; | ||
}; | ||
const queryCheckComponents = (world, query, components) => { | ||
return components.every(c => queryCheckComponent(world, query, c)); | ||
}; | ||
const queryAddEntity = (world, query, eid) => { | ||
const q = world[$queryMap].get(query); | ||
if (q.enabled[eid]) return; | ||
q.enabled[eid] = true; | ||
q.entities.push(eid); | ||
q.indices[eid] = q.entities.length - 1; | ||
if (q.enter) q.enter(eid); | ||
}; | ||
const queryCommitRemovals = (world, query) => { | ||
const q = world[$queryMap].get(query); | ||
while (q.toRemove.length) { | ||
const eid = q.toRemove.pop(); | ||
const index = q.indices[eid]; | ||
if (index === NONE) continue; | ||
const swapped = q.entities.pop(); | ||
if (swapped !== eid) { | ||
q.entities[index] = swapped; | ||
q.indices[swapped] = index; | ||
} | ||
q.indices[eid] = NONE; | ||
} | ||
world[$dirtyQueries].delete(q); | ||
}; | ||
const commitRemovals = world => { | ||
world[$dirtyQueries].forEach(q => { | ||
queryCommitRemovals(q); | ||
}); | ||
}; | ||
const queryRemoveEntity = (world, query, eid) => { | ||
const q = world[$queryMap].get(query); | ||
if (!q.enabled[eid]) return; | ||
q.enabled[eid] = false; | ||
if (q.exit) q.exit(eid); | ||
q.toRemove.push(eid); | ||
world[$dirtyQueries].add(q); | ||
}; | ||
const $componentMap = Symbol('componentMap'); | ||
const $deferredComponentRemovals = Symbol('de$deferredComponentRemovals'); | ||
const defineComponent = schema => alloc(schema); | ||
const incrementBitflag = world => { | ||
world[$bitflag] *= 2; | ||
if (world[$bitflag] >= 2 ** 32) { | ||
world[$bitflag] = 1; | ||
world[$entityMasks].push(new Uint32Array(world[$size])); | ||
} | ||
}; | ||
const registerComponent = (world, component) => { | ||
world[$componentMap].set(component, { | ||
generationId: world[$entityMasks].length - 1, | ||
bitflag: world[$bitflag], | ||
manager: component | ||
}); | ||
incrementBitflag(world); | ||
}; | ||
const registerComponents = (world, components) => { | ||
components.forEach(c => registerComponent(world, c)); | ||
}; | ||
const hasComponent = (world, component, eid) => { | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
const mask = world[$entityMasks][generationId][eid]; | ||
return (mask & bitflag) === bitflag; | ||
}; | ||
const addComponent = (world, component, eid) => { | ||
if (!world[$componentMap].has(component)) registerComponent(world, component); | ||
if (hasComponent(world, component, eid)) return; // Add bitflag to entity bitmask | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
world[$entityMasks][generationId][eid] |= bitflag; // Zero out each property value | ||
// component._reset(eid) | ||
// todo: archetype graph | ||
const queries = world[$queries]; | ||
queries.forEach(query => { | ||
const components = query[$queryComponents]; | ||
if (!queryCheckComponents(world, query, components)) return; | ||
const match = queryCheckEntity(world, query, eid); | ||
if (match) queryAddEntity(world, query, eid); | ||
}); | ||
}; | ||
const removeComponent = (world, component, eid) => { | ||
const { | ||
generationId, | ||
bitflag | ||
} = world[$componentMap].get(component); | ||
if (!(world[$entityMasks][generationId][eid] & bitflag)) return; // todo: archetype graph | ||
const queries = world[$queries]; | ||
queries.forEach(query => { | ||
const components = query[$queryComponents]; | ||
if (!queryCheckComponents(world, query, components)) return; | ||
const match = queryCheckEntity(world, query, eid); | ||
if (match) queryRemoveEntity(world, query, eid); | ||
}); // Remove flag from entity bitmask | ||
world[$entityMasks][generationId][eid] &= ~bitflag; | ||
}; | ||
const $size = Symbol('size'); | ||
const $bitflag = Symbol('bitflag'); | ||
const createWorld = (size = 1000000) => { | ||
const world = {}; | ||
world[$size] = size; | ||
world[$entityEnabled] = new Uint8Array(size); | ||
world[$entityMasks] = [new Uint32Array(size)]; | ||
world[$removedEntities] = []; | ||
world[$bitflag] = 1; | ||
world[$componentMap] = new Map(); | ||
world[$queryMap] = new Map(); | ||
world[$queries] = new Set(); | ||
world[$dirtyQueries] = new Set(); | ||
world[$deferredComponentRemovals] = []; | ||
world[$deferredEntityRemovals] = []; | ||
return world; | ||
}; | ||
const defineSystem = update => { | ||
const system = world => { | ||
update(world); | ||
commitRemovals(world); | ||
}; | ||
Object.defineProperty(system, 'name', { | ||
value: (update.name || "AnonymousSystem") + "_internal", | ||
configurable: true | ||
}); | ||
return system; | ||
}; | ||
const pipe = fns => world => { | ||
for (let i = 0; i < fns.length; i++) { | ||
const fn = fns[i]; | ||
fn(world); | ||
} | ||
}; | ||
const Types = TYPES_ENUM; | ||
exports.Changed = Changed; | ||
exports.Not = Not; | ||
exports.Types = Types; | ||
exports.addComponent = addComponent; | ||
exports.addEntity = addEntity; | ||
exports.commitRemovals = commitRemovals; | ||
exports.createWorld = createWorld; | ||
exports.defineComponent = defineComponent; | ||
exports.defineDeserializer = defineDeserializer; | ||
exports.defineQuery = defineQuery; | ||
exports.defineSerializer = defineSerializer; | ||
exports.defineSystem = defineSystem; | ||
exports.enterQuery = enterQuery; | ||
exports.exitQuery = exitQuery; | ||
exports.hasComponent = hasComponent; | ||
exports.pipe = pipe; | ||
exports.registerComponent = registerComponent; | ||
exports.registerComponents = registerComponents; | ||
exports.removeComponent = removeComponent; | ||
exports.removeEntity = removeEntity; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e={bool:"Uint8",i8:"Int8",ui8:"Uint8",ui8c:"Uint8Clamped",i16:"Int16",ui16:"Uint16",i32:"Int32",ui32:"Uint32",f32:"Float32",f64:"Float64"},t={bool:"bool",i8:Int8Array,ui8:Uint8Array,ui8c:Uint8ClampedArray,i16:Int16Array,ui16:Uint16Array,i32:Int32Array,ui32:Uint32Array,f32:Float32Array,f64:Float64Array},n=256,r=65536,o=e=>4*Math.ceil(e/4),s=Symbol("storeRef"),i=Symbol("storeSize"),a=Symbol("storeMaps"),c=Symbol("storeFlattened"),l=Symbol("storeArrayCount"),u=Symbol("storeSubarrays"),f=Symbol("storeCursor"),y=Symbol("subarrayCursors"),p=Symbol("subarray"),d=Symbol("queryShadow"),g=Symbol("serializeShadow"),m=Symbol("indexType"),b=Symbol("indexBytes"),h={},E=(e,t)=>{const n=new ArrayBuffer(t*e.BYTES_PER_ELEMENT),r=new e.constructor(n);return r.set(e,0),r},S=(e,t)=>{Object.keys(e).forEach((n=>{const r=e[n];r[p]||(ArrayBuffer.isView(r)?(e[n]=E(r,t),e[n][d]=E(r[d],t),e[n][g]=E(r[g],t)):"object"==typeof r&&S(e[n],t))}))},A=(e,n)=>{e[i]=n,S(e,n),(e=>{const n=e[i],r=e[y]={};Object.keys(e[u]).forEach((s=>{const i=e[l],a=e[0].length,c=Array(i).fill(0).reduce(((e,n)=>e+t[s].BYTES_PER_ELEMENT),0),f=Array(i).fill(0).reduce(((e,t)=>e+a),0),y=new ArrayBuffer(o(c*f*n)),d=new t[s](y);d.set(e[u][s].buffer,0),e[u][s]=d;for(let t=0;t<n;t++){const n=r[s]+t*a,o=n+a;e[t]=e[u][s].subarray(n,o),e[t][p]=!0}}))})(e)},w=(e,n)=>{const r=n*t[e].BYTES_PER_ELEMENT,o=new ArrayBuffer(r);return new t[e](o)},_=(s,a,c)=>{const f=s[i],h=s[y],E=c<n?"ui8":c<r?"ui16":"ui32";if(!c)throw new Error("❌ Must define a length for component array.");if(!t[a])throw new Error(`❌ Invalid component array property type ${a}.`);if(!s[u][a]){const n=s[l],r=Array(n).fill(0).reduce(((e,n)=>e+t[a].BYTES_PER_ELEMENT),0),i=Array(n).fill(0).reduce(((e,t)=>e+c),0),y=o(r*i*f),p=new ArrayBuffer(y),h=new t[a](p);s[u][a]=h,s[u][a][d]=h.slice(0),s[u][a][g]=h.slice(0),h[m]=e[E],h[b]=t[E].BYTES_PER_ELEMENT}let S=0;for(let e=0;e<f;e++){const t=h[a]+e*c,n=t+c;s[e]=s[u][a].subarray(t,n),s[e][p]=!0,S=n}return h[a]=S,s},x=e=>{e[d]=e.slice(0),e[g]=e.slice(0)},M=e=>Array.isArray(e)&&"object"==typeof e[0]&&e[0].hasOwnProperty("type")&&e[0].hasOwnProperty("length"),T=Symbol("entityMasks"),B=Symbol("entityEnabled"),U=Symbol("deferredEntityRemovals"),j=Symbol("removedEntities");let C=0;const I=e=>{let t,n=new Set;return Array.isArray(e)?t=e.map((e=>{if(e._flatten)return e._flatten();if("function"==typeof e&&"QueryChanged"===e.name){if((e=e())._flatten){let t=e._flatten();return t.forEach((e=>n.add(e))),t}return n.add(e),[e]}})).reduce(((e,t)=>e.concat(t)),[]):e[q].forEach((e=>{t=t.concat(e._flatten())})),[t,n]};const O=Symbol("queries"),R=Symbol("queryMap"),P=Symbol("$dirtyQueries"),k=Symbol("queryComponents"),v=2**32,N=(e,t)=>{e[R].has(t)||e[R].set(t,{});let n=[],r=[],o=[];t[k].forEach((e=>{"function"==typeof e?("QueryNot"===e.name&&r.push(e()),"QueryChanged"===e.name&&(o.push(e()),n.push(e()))):n.push(e)}));const s=t=>e[q].get(t),a=n.reduce(((e,t)=>t[i]>e?t[i]:e),0),c=new Uint32Array(a).fill(v),l=new Uint8Array(a),u=n.concat(r).map((t=>(e[q].has(t)||$(e,t),t))).map(s).map((e=>e.generationId)).reduce(((e,t)=>(e.includes(t)||e.push(t),e)),[]),f=n.map(s).reduce(((e,t)=>(e[t.generationId]||(e[t.generationId]=0),e[t.generationId]|=t.bitflag,e)),{}),y=r.map(s).reduce(((e,t)=>(e[t.generationId]||(e[t.generationId]=0,e[t.generationId]|=t.bitflag),e)),{}),p=n.map((e=>e._flatten?e._flatten():[e])).reduce(((e,t)=>e.concat(t)),[]);Object.assign(e[R].get(t),{entities:[],changed:[],enabled:l,components:n,notComponents:r,changedComponents:o,masks:f,notMasks:y,generations:u,indices:c,flatProps:p,toRemove:[]}),e[O].add(t);for(let n=0;n<C;n++)e[B][n]&&L(e,t,n)&&Q(e,t,n)},L=(e,t,n)=>{const{masks:r,notMasks:o,generations:s}=e[R].get(t);for(let t=0;t<s.length;t++){const i=s[t],a=r[i],c=o[i],l=e[T][i][n];if(c&&0!=(l&c))return!1;if(a&&(l&a)!==a)return!1}return!0},Y=(e,t,n)=>n.every((n=>((e,t,n)=>{const{generationId:r,bitflag:o}=e[q].get(n),{masks:s}=e[R].get(t);return(s[r]&o)===o})(e,t,n))),Q=(e,t,n)=>{const r=e[R].get(t);r.enabled[n]||(r.enabled[n]=!0,r.entities.push(n),r.indices[n]=r.entities.length-1,r.enter&&r.enter(n))},V=(e,t)=>{const n=e[R].get(t);for(;n.toRemove.length;){const e=n.toRemove.pop(),t=n.indices[e];if(t===v)continue;const r=n.entities.pop();r!==e&&(n.entities[t]=r,n.indices[r]=t),n.indices[e]=v}e[P].delete(n)},z=e=>{e[P].forEach((e=>{V(e)}))},F=(e,t,n)=>{const r=e[R].get(t);r.enabled[n]&&(r.enabled[n]=!1,r.exit&&r.exit(n),r.toRemove.push(n),e[P].add(r))},q=Symbol("componentMap"),D=Symbol("de$deferredComponentRemovals"),$=(e,t)=>{e[q].set(t,{generationId:e[T].length-1,bitflag:e[H],store:t}),(e=>{e[H]*=2,e[H]>=2**32&&(e[H]=1,e[T].push(new Uint32Array(e[G])))})(e)},W=(e,t,n)=>{const{generationId:r,bitflag:o}=e[q].get(t);return(e[T][r][n]&o)===o},G=Symbol("size"),H=Symbol("bitflag"),J={bool:"bool",i8:"i8",ui8:"ui8",ui8c:"ui8c",i16:"i16",ui16:"ui16",i32:"i32",ui32:"ui32",f32:"f32",f64:"f64"};exports.Changed=function(e){return function(){return e}},exports.Not=function(e){return function(){return e}},exports.Types=J,exports.addComponent=(e,t,n)=>{if(e[q].has(t)||$(e,t),W(e,t,n))return;const{generationId:r,bitflag:o}=e[q].get(t);e[T][r][n]|=o;e[O].forEach((t=>{const r=t[k];if(!Y(e,t,r))return;L(e,t,n)&&Q(e,t,n)}))},exports.addEntity=e=>{const t=e[j],n=e[G],r=e[B];if(C>=n-n/5){const t=4*Math.ceil(n/2/4);e[G]+=t,e[q].forEach((t=>{A(t.store,e[G])})),e[R].forEach((t=>{t.indices=E(t.indices,e[G]),t.enabled=E(t.enabled,e[G])}))}const o=t.length>0?t.pop():C;return r[o]=1,C++,o},exports.commitRemovals=z,exports.createWorld=(e=1e6)=>{const t={};return t[G]=e,t[B]=new Uint8Array(e),t[T]=[new Uint32Array(e)],t[j]=[],t[H]=1,t[q]=new Map,t[R]=new Map,t[O]=new Set,t[P]=new Set,t[D]=[],t[U]=[],t},exports.defineComponent=e=>((e,n=1e6)=>{const r=Symbol("store");if("Map"===e.constructor.name)return e[i]=n,e;const o=(t,n)=>(M(e[n])?t++:e[n]instanceof Object&&(t+=Object.keys(e[n]).reduce(o,0)),t),p=M(e)?1:Object.keys(e).reduce(o,0),d={[i]:n,[a]:{},[u]:{},[s]:r,[f]:0,[y]:Object.keys(t).reduce(((e,t)=>({...e,[t]:0})),{}),[l]:p,[c]:[]};if("string"==typeof e)return h[r]=Object.assign(w(e,n),d),d[c].push(h[r]),x(h[r]),h[r];if(M(e)){const{type:t,length:n}=e[0];return h[r]=Object.assign(_(d,t,n),d),d[c].push(h[r]),h[r]}if(e instanceof Object&&Object.keys(e).length){const t=(e,r)=>{if("string"==typeof e[r])e[r]=w(e[r],n),d[c].push(e[r]),x(e[r]);else if(M(e[r])){const{type:t,length:n}=e[r][0];e[r]=_(d,t,n)}else e[r]instanceof Object&&(e[r]=Object.keys(e[r]).reduce(t,e[r]));return e};return h[r]=Object.assign(Object.keys(e).reduce(t,e),d),h[r]}return{}})(e),exports.defineDeserializer=e=>{const[t]=I(e);return e=>{const n=new DataView(e);let r=0;const o=n.getUint8(r);r+=1;const s=n.getUint32(r);r+=4;const i=t[o];for(let e=0;e<s;e++){const e=n.getUint32(r);if(r+=4,ArrayBuffer.isView(i[e])){const t=i[e],o=n["get"+t._indexType];r+=t._indexBytes;for(let s=0;s<o;s++){const o=n["get"+t.constructor.name.replace("Array","")](r);r+=t.BYTES_PER_ELEMENT,i[e][s]=o}}else{let t=n["get"+i.constructor.name.replace("Array","")](r);r+=i.BYTES_PER_ELEMENT,i[e]=t}}}},exports.defineQuery=e=>{const t=function(e){e[R].has(t)||N(e,t),V(e,t);const n=e[R].get(t);return n.changedComponents.length?((e,t)=>{const n=e[R].get(t);n.changed.length=0;const r=n.flatProps;for(let e=0;e<n.entities.length;e++){const t=n.entities[e];let o=!1;for(let e=0;e<r.length;e++){const n=r[e];if(ArrayBuffer.isView(n[t]))for(let e=0;e<n[t].length;e++)n[t][e]!==n[t][d][e]&&(o=!0,n[t][d][e]=n[t][e]);else n[t]!==n[d][t]&&(o=!0,n[d][t]=n[t])}o&&n.changed.push(t)}return n.changed})(e,t):n.entities};return t[k]=e,t},exports.defineSerializer=(e,t=5e6)=>{const n=new ArrayBuffer(t),r=new DataView(n),[o,s]=I(e);return e=>{if(!e.length)return;let t=0;for(let n=0;n<o.length;n++){const i=o[n],a=s.has(i);r.setUint8(t,n),t+=1;const c=t;t+=4;let l=0;for(let n=0;n<e.length;n++){const o=e[n];if(!a||i[o]!==i[g][o])if(l++,r.setUint32(t,o),t+=4,ArrayBuffer.isView(i[o])){const e=i[o].constructor.name.replace("Array",""),n=i[o]._indexType,s=i[o]._indexBytes,a=t;t+=1;let c=0;for(let a=0;a<i[o].length;a++){const l=i[o][a];r["set"+n](t,a),t+=s,r["set"+e](t,l),t+=i[o].BYTES_PER_ELEMENT,c++}r["set"+n](a,c)}else{const e=i.constructor.name.replace("Array","");r["set"+e](t,i[o]),t+=i.BYTES_PER_ELEMENT,i[g]||console.log(i),i[g][o]=i[o]}}r.setUint32(c,l)}return n.slice(0,t)}},exports.defineSystem=e=>{const t=t=>{e(t),z(t)};return Object.defineProperty(t,"name",{value:(e.name||"AnonymousSystem")+"_internal",configurable:!0}),t},exports.enterQuery=(e,t,n)=>{e[R].has(t)||N(e,t),e[R].get(t).enter=n},exports.exitQuery=(e,t,n)=>{e[R].has(t)||N(e,t),e[R].get(t).exit=n},exports.hasComponent=W,exports.pipe=(...e)=>t=>{e=Array.isArray(e[0])?e[0]:e;for(let n=0;n<e.length;n++){(0,e[n])(t)}},exports.registerComponent=$,exports.registerComponents=(e,t)=>{t.forEach((t=>$(e,t)))},exports.removeComponent=(e,t,n)=>{const{generationId:r,bitflag:o}=e[q].get(t);if(!(e[T][r][n]&o))return;e[O].forEach((t=>{const r=t[k];if(!Y(e,t,r))return;L(e,t,n)&&F(e,t,n)})),e[T][r][n]&=~o},exports.removeEntity=(e,t)=>{const n=e[O],r=e[j],o=e[B];if(0!==o[t]){n.forEach((n=>{F(e,n,t)})),r.push(t),o[t]=0;for(let n=0;n<e[T].length;n++)e[T][n][t]=0}}; | ||
//# sourceMappingURL=index.min.js.map |
{ | ||
"name": "bitecs", | ||
"version": "0.2.14", | ||
"version": "0.2.15", | ||
"description": "Tiny, data-driven, high performance ECS library written in Javascript", | ||
@@ -5,0 +5,0 @@ "license": "MPL-2.0", |
@@ -6,10 +6,19 @@ # 👾 bitECS 👾 | ||
## Features | ||
- Functional | ||
- `<3kb` gzipped | ||
- Zero dependencies | ||
- Serialization | ||
- Queries & Query Modifiers | ||
- Node or Browser | ||
- [_Killer performance_](https://github.com/noctjs/ecs-benchmark) | ||
🔮 Functional | ||
🤏 `<3kb` gzipped | ||
🙅♂️ Zero dependencies | ||
💾 Serialization | ||
✨ Queries with modifiers | ||
🌐 Node or browser | ||
🔥 Blazing fast | ||
- [ecs-benchmark](https://github.com/noctjs/ecs-benchmark) | ||
- [js-ecs-benchmark](https://github.com/ddmills/js-ecs-benchmark) | ||
## Install | ||
@@ -31,4 +40,2 @@ ``` | ||
registerComponent, | ||
registerComponents, | ||
defineComponent, | ||
@@ -82,3 +89,3 @@ addComponent, | ||
const Velocity = defineComponent(Vector2) | ||
const Health = defineComponent(ui16) | ||
const Health = defineComponent({ value: ui16 }) | ||
const Alive = defineComponent() // "tag" component | ||
@@ -119,4 +126,2 @@ const Mapping = defineComponent(new Map()) // can use a map to associate regular JS objects with entities | ||
* Use queries to access relevant entities for the system. | ||
* | ||
* Note: Entity and component removals are deferred until the system has finished running. | ||
**/ | ||
@@ -163,3 +168,3 @@ | ||
/** | ||
* Pipe | ||
* pipe | ||
* | ||
@@ -215,3 +220,3 @@ * Creates a sequence of systems which are executed in serial. | ||
// serializes the Position data of entities which match the movementQuery | ||
// whose values have changed since last call of the function | ||
// whose component values have changed since last call of the function | ||
packet = serializeOnlyChangedPositions(movementQuery(world)) | ||
@@ -218,0 +223,0 @@ |
56
test.js
@@ -30,7 +30,7 @@ import { performance} from'perf_hooks' | ||
const Position = defineComponent(Vector2) | ||
const Velocity = defineComponent(Vector2) | ||
const Velocity = defineComponent({x: [{type:f32,length:2}]}) | ||
// registerComponents(world, [Position, Velocity]) | ||
// const query = defineQuery([Position,Velocity]) | ||
const serialize = defineSerializer([Changed(Position)]) | ||
const query = defineQuery([Position,Velocity]) | ||
const serialize = defineSerializer([Changed(Position), Velocity]) | ||
const deserialize = defineDeserializer([Position,Velocity]) | ||
@@ -40,13 +40,13 @@ | ||
const n = 2 | ||
const n = 500_000 | ||
for (let i = 0; i < n; i++) { | ||
const eid = addEntity(world) | ||
// addComponent(world, Position, eid) | ||
// addComponent(world, Velocity, eid) | ||
addComponent(world, Position, eid) | ||
addComponent(world, Velocity, eid) | ||
Position.x[eid]++ | ||
Position.y[eid]++ | ||
Velocity.x[eid].fill(1) | ||
// Velocity.y[eid].fill(1) | ||
} | ||
// console.log(query(world)) | ||
// removeComponent(world, Position, 0) | ||
@@ -57,21 +57,35 @@ // console.log(query(world)) | ||
// console.log(query(world)) | ||
// console.log(query(world).length) | ||
console.log(notPosition(world)) | ||
// console.log(notPosition(world)) | ||
// let then | ||
let then | ||
// console.log(`n = ${n}`) | ||
console.log(`n = ${n}`) | ||
// then = now() | ||
// const ents = query(world) | ||
// console.log('query', (now()-then).toFixed(2)) | ||
then = now() | ||
const ents = query(world) | ||
console.log('query', (now()-then).toFixed(2)) | ||
// then = now() | ||
// const packet = serialize(ents) | ||
// console.log('serialize', (now()-then).toFixed(2)) | ||
then = now() | ||
const packet = serialize(ents) | ||
console.log('serialize', (now()-then).toFixed(2)) | ||
// then = now() | ||
// deserialize(packet) | ||
// console.log('deserialize', (now()-then).toFixed(2)) | ||
for (let i = 0; i < n; i++) { | ||
Position.x[i]++ | ||
Position.y[i]++ | ||
Velocity.x[i].fill(2) | ||
// Velocity.y[eid].fill(2) | ||
} | ||
// console.log('before',Velocity.x[0]) | ||
then = now() | ||
deserialize(packet) | ||
console.log('deserialize', (now()-then).toFixed(2)) | ||
// console.log('after',Velocity.x[0]) | ||
console.log(Velocity.x[0][0]) |
13
test2.js
@@ -48,3 +48,3 @@ import { performance} from'perf_hooks' | ||
const n = 1000 | ||
const n = 500_000 | ||
for (let i = 0; i < n; i++) { | ||
@@ -58,12 +58,13 @@ const eid = addEntity(world) | ||
move(world) | ||
let then | ||
let then = now() | ||
for (let i = 0; i < 10000; i++) { | ||
for (let i = 0; i < 50; i++) { | ||
then = now() | ||
move(world) | ||
console.log(now()-then) | ||
} | ||
console.log(now()-then) | ||
console.log(Position.x[0]) | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
221
148841
196
1