├── .gitignore ├── wallaby.cjs ├── package.json ├── src ├── Utils.js ├── DataReader.js ├── DataWriter.js └── ViewCursor.js ├── index.js ├── README.md └── test ├── ViewCursor.test.js ├── DataWriter.test.js └── DataReader.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /wallaby.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | files: [ 4 | 'package.json', 5 | 'src/**/*.js', 6 | ], 7 | 8 | tests: [ 9 | 'test/**/*.test.js' 10 | ], 11 | env: { 12 | type: 'node' 13 | }, 14 | workers: { restart: true } 15 | }; 16 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soa-serializer", 3 | "version": "0.0.1", 4 | "description": "SoA serialization library", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "mocha ./**/*.test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Web-ECS/SoA-serializer.git" 13 | }, 14 | "keywords": [ 15 | "SoA", 16 | "serialization", 17 | "ECS" 18 | ], 19 | "author": "Nathaniel Martin", 20 | "license": "MPL-2.0", 21 | "bugs": { 22 | "url": "https://github.com/Web-ECS/SoA-serializer/issues" 23 | }, 24 | "homepage": "https://github.com/Web-ECS/SoA-serializer#readme", 25 | "devDependencies": { 26 | "mocha": "^9.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an internal cache of inputs to outputs for idemponent functions 3 | * 4 | * @param fn function to memoize inputs for 5 | * @returns {function} 6 | */ 7 | const memoize = (fn) => { 8 | const cache = new Map() 9 | return (input) => { 10 | if (cache.has(input)) return cache.get(input) 11 | else { 12 | const output = fn(input) 13 | cache.set(input, output) 14 | return output 15 | } 16 | } 17 | } 18 | 19 | /** 20 | * Recursively flattens all of a component's SoA leaf properties into a linear array 21 | * Function is idemponent, thus safely memoized 22 | * 23 | * @param {object} component 24 | */ 25 | export const flatten = memoize((component) => 26 | // get all props on component 27 | Object.keys(component) 28 | .sort() 29 | // flatMap props to 30 | .flatMap((p) => { 31 | if (!ArrayBuffer.isView(component[p])) { 32 | return flatten(component[p]) 33 | } 34 | return component[p] 35 | }) 36 | .flat() 37 | ) 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { createDataWriter } from './src/DataWriter' 2 | import { createDataReader } from './src/DataReader' 3 | import { 4 | createViewCursor, 5 | sliceViewCursor, 6 | scrollViewCursor, 7 | moveViewCursor, 8 | 9 | writeProp, 10 | writePropIfChanged, 11 | writeFloat64, 12 | writeFloat32, 13 | writeUint64, 14 | writeInt64, 15 | writeUint32, 16 | writeInt32, 17 | writeUint16, 18 | writeInt16, 19 | writeUint8, 20 | writeInt8, 21 | 22 | spaceFloat64, 23 | spaceUint64, 24 | spaceInt64, 25 | spaceUint32, 26 | spaceInt32, 27 | spaceUint16, 28 | spaceInt16, 29 | spaceUint8, 30 | spaceInt8, 31 | 32 | readProp, 33 | readFloat64, 34 | readFloat32, 35 | readUint64, 36 | readInt64, 37 | readUint32, 38 | readInt32, 39 | readUint16, 40 | readInt16, 41 | readUint8, 42 | readInt8, 43 | } from './src/ViewCursor' 44 | 45 | export default { 46 | createDataWriter, 47 | createDataReader, 48 | 49 | createViewCursor, 50 | sliceViewCursor, 51 | scrollViewCursor, 52 | moveViewCursor, 53 | 54 | writeProp, 55 | writePropIfChanged, 56 | writeFloat64, 57 | writeFloat32, 58 | writeUint64, 59 | writeInt64, 60 | writeUint32, 61 | writeInt32, 62 | writeUint16, 63 | writeInt16, 64 | writeUint8, 65 | writeInt8, 66 | 67 | spaceFloat64, 68 | spaceUint64, 69 | spaceInt64, 70 | spaceUint32, 71 | spaceInt32, 72 | spaceUint16, 73 | spaceInt16, 74 | spaceUint8, 75 | spaceInt8, 76 | 77 | readProp, 78 | readFloat64, 79 | readFloat32, 80 | readUint64, 81 | readInt64, 82 | readUint32, 83 | readInt32, 84 | readUint16, 85 | readInt16, 86 | readUint8, 87 | readInt8, 88 | } -------------------------------------------------------------------------------- /src/DataReader.js: -------------------------------------------------------------------------------- 1 | import { flatten } from './Utils.js' 2 | import { createViewCursor, readProp, readUint8, readUint16, readUint32, readUint64 } from './ViewCursor.js' 3 | 4 | export const checkBitflag = (changeMask, flag) => (changeMask & flag) === flag 5 | 6 | export const readComponentProp = (v, prop, entity) => { 7 | prop[entity] = readProp(v, prop) 8 | } 9 | 10 | /** 11 | * Reads a component dynamically 12 | * (less efficient than statically due to inner loop) 13 | * 14 | * @param {any} component 15 | */ 16 | export const readComponent = (component, diff) => { 17 | // todo: test performance of using flatten in this scope vs return function scope 18 | const props = flatten(component) 19 | const readChanged = props.length <= 8 20 | ? readUint8 21 | : props.length <= 16 22 | ? readUint16 23 | : props.length <= 32 24 | ? readUint32 25 | : readUint64 26 | 27 | return (v, entity) => { 28 | const changeMask = diff ? readChanged(v) : Number.MAX_SAFE_INTEGER 29 | 30 | for (let i = 0; i < props.length; i++) { 31 | // skip reading property if not in the change mask 32 | if (diff && !checkBitflag(changeMask, 1 << i)) { 33 | continue 34 | } 35 | readComponentProp(v, props[i], entity) 36 | } 37 | } 38 | } 39 | export const readEntity = (componentReaders, diff) => { 40 | const readChanged = componentReaders.length <= 8 41 | ? readUint8 42 | : componentReaders.length <= 16 43 | ? readUint16 44 | : componentReaders.length <= 32 45 | ? readUint32 46 | : readUint64 47 | 48 | return (v, idMap) => { 49 | const id = readUint32(v) 50 | const entity = idMap ? idMap.get(id) : id 51 | if (entity === undefined) throw new Error('entity not found in idMap') 52 | 53 | const changeMask = diff ? readChanged(v) : Number.MAX_SAFE_INTEGER 54 | 55 | for (let i = 0; i < componentReaders.length; i++) { 56 | // skip reading component if not in the changeMask 57 | if (diff && !checkBitflag(changeMask, 1 << i)) { 58 | continue 59 | } 60 | const read = componentReaders[i] 61 | read(v, entity) 62 | } 63 | } 64 | } 65 | 66 | export const createEntityReader = (components, diff) => readEntity(components.map(c => readComponent(c, diff)), diff) 67 | 68 | export const readEntities = (entityReader, v, idMap, packet) => { 69 | while (v.cursor < packet.byteLength) { 70 | const count = readUint32(v) 71 | for (let i = 0; i < count; i++) { 72 | entityReader(v, idMap) 73 | } 74 | } 75 | } 76 | 77 | export const createDataReader = (components, diff = false) => { 78 | 79 | const entityReader = createEntityReader(components, diff) 80 | 81 | return (packet, idMap) => { 82 | const view = createViewCursor(packet) 83 | return readEntities(entityReader, view, idMap, packet) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/DataWriter.js: -------------------------------------------------------------------------------- 1 | import { flatten } from './Utils.js' 2 | import { 3 | createViewCursor, 4 | spaceUint64, 5 | spaceUint32, 6 | spaceUint16, 7 | spaceUint8, 8 | writeProp, 9 | sliceViewCursor, 10 | writePropIfChanged, 11 | moveViewCursor, 12 | writeUint32 13 | } from './ViewCursor.js' 14 | 15 | /** 16 | * Writes a component dynamically 17 | * (less efficient than statically due to inner loop) 18 | * 19 | * @param {any} component 20 | */ 21 | export const writeComponent = (component, diff) => { 22 | // todo: test performance of using flatten in the return scope vs this scope 23 | const props = flatten(component) 24 | const changeMaskSpacer = props.length <= 8 25 | ? spaceUint8 26 | : props.length <= 16 27 | ? spaceUint16 28 | : props.length <= 32 29 | ? spaceUint32 30 | : spaceUint64 31 | 32 | // todo: support more than 64 props (use a function which generates multiple spacers) 33 | 34 | const properWriter = diff ? writePropIfChanged : writeProp 35 | 36 | return (v, entity) => { 37 | const writeChangeMask = diff ? changeMaskSpacer(v) : () => {} 38 | let changeMask = 0 39 | 40 | for (let i = 0; i < props.length; i++) { 41 | changeMask |= properWriter(v, props[i], entity) ? 1 << i : 0b0 42 | } 43 | 44 | writeChangeMask(changeMask) 45 | 46 | return changeMask > 0 ? 1 : 0 47 | } 48 | } 49 | 50 | export const writeEntity = (componentWriters, diff) => { 51 | 52 | const changeMaskSpacer = componentWriters.length <= 8 53 | ? spaceUint8 54 | : componentWriters.length <= 16 55 | ? spaceUint16 56 | : componentWriters.length <= 32 57 | ? spaceUint32 58 | : spaceUint64 59 | 60 | // todo: support more than 64 components (use a function which generates multiple spacers) 61 | 62 | return (v, entity) => { 63 | const rewind = v.cursor 64 | 65 | writeUint32(v, entity) 66 | 67 | const writeChangeMask = diff ? changeMaskSpacer(v) : () => {} 68 | 69 | let changeMask = 0 70 | 71 | for (let i = 0, l = componentWriters.length; i < l; i++) { 72 | const write = componentWriters[i] 73 | changeMask |= write(v, entity) ? 1 << i : 0 74 | } 75 | 76 | if (changeMask > 0) { 77 | writeChangeMask(changeMask) 78 | return 1 79 | } else { 80 | moveViewCursor(v, rewind) 81 | return 0 82 | } 83 | } 84 | } 85 | 86 | export const createEntityWriter = (components, diff) => writeEntity(components.map(c => writeComponent(c, diff)), diff) 87 | 88 | export const writeEntities = (entityWriter, v, entities, idMap) => { 89 | const writeCount = spaceUint32(v) 90 | 91 | let count = 0 92 | for (let i = 0, l = entities.length; i < l; i++) { 93 | const eid = idMap ? idMap.get(eid) : entities[i] 94 | count += entityWriter(v, eid) 95 | } 96 | 97 | writeCount(count) 98 | 99 | return sliceViewCursor(v) 100 | } 101 | 102 | export const createDataWriter = (components, diff = false, size = 100000) => { 103 | const view = createViewCursor(new ArrayBuffer(size)) 104 | 105 | const entityWriter = createEntityWriter(components, diff) 106 | 107 | return (entities, idMap) => { 108 | return writeEntities(entityWriter, view, entities, idMap) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data-oriented Serialization for SoA/AoA 2 | 3 | A zero-dependency serialization library for data-oriented design structures like SoA (Structure of Arrays) and AoA (Array of Arrays). 4 | 5 | ## Features 6 | 7 | - DataWriter: Serializes data from SoA object(s) to binary data provided array of indices to extract data from 8 | - DataReader: Deserializes from binary data to SoA object(s) to the appropriate indices 9 | - Binary data packed in an AoS-like format for optimal space efficiency 10 | - ID mapping included for reading/writing data from/to different sets of indices 11 | - Includes snapshot and delta modes 12 | - Snapshot mode serializes the entire state (default) 13 | - Delta mode only serializes state that has changed since the last serialization call 14 | 15 | ## Planned 16 | 17 | - Support for AoA serialization 18 | - More flexible API 19 | - Schemas 20 | - Ability to pass in ArrayBuffers to write to 21 | 22 | ## Example 23 | 24 | ```js 25 | import assert from 'assert' 26 | import { createDataWriter, createDataReader } from '@webecs/do-serialization' 27 | 28 | /* SoA Object */ 29 | 30 | const n = 100 31 | 32 | const Transform = { 33 | position: { 34 | x: new Float32Array(n), 35 | y: new Float32Array(n), 36 | z: new Float32Array(n), 37 | }, 38 | rotation: { 39 | x: new Float32Array(n), 40 | y: new Float32Array(n), 41 | z: new Float32Array(n), 42 | }, 43 | } 44 | 45 | 46 | /* Config */ 47 | 48 | // a simple array of SoA objects acts as the configuration 49 | const config = [Transform] 50 | 51 | 52 | /* Snapshot Mode */ 53 | 54 | // DataWriter and DataReader must have the same config in order to function correctly 55 | const write = createDataWriter(config) 56 | const read = createDataReader(config) 57 | 58 | // initialize SoA state 59 | const e = 0 60 | Transform.position.x[e] = 1 61 | Transform.position.y[e] = 2 62 | Transform.position.z[e] = 3 63 | 64 | // serialize 65 | let data = write([e]) 66 | 67 | // reset SoA state 68 | Transform.position.x[e] = 0 69 | Transform.position.y[e] = 0 70 | Transform.position.z[e] = 0 71 | 72 | // deserialize 73 | read(data) 74 | 75 | // assert data was deserialized onto SoA state 76 | assert(Transform.position.x[e] === 1) // true 77 | assert(Transform.position.y[e] === 2) // true 78 | assert(Transform.position.z[e] === 3) // true 79 | 80 | 81 | /* Delta Mode */ 82 | 83 | // true value for second parameter enables delta mode (needed for both writer & reader) 84 | const writeDeltas = createDataWriter(config, true) 85 | const readDeltas = createDataReader(config, true) 86 | 87 | Transform.position.x[e] = 0 88 | Transform.position.y[e] = 0 89 | Transform.position.z[e] = 0 90 | 91 | // serialize 92 | data = writeDeltas([e]) 93 | 94 | assert(data.byteLength === 0) // true, no changes made to the data 95 | 96 | // mutate SoA state 97 | Transform.position.x[e] = 1 98 | Transform.position.y[e] = 2 99 | Transform.position.z[e] = 3 100 | 101 | // serialize 102 | data = writeDeltas([e]) 103 | 104 | assert(data.byteLength > 0) // true, changes have been made to the data since the last call 105 | 106 | // reset SoA state 107 | Transform.position.x[e] = 0 108 | Transform.position.y[e] = 0 109 | Transform.position.z[e] = 0 110 | 111 | // deserialize 112 | readDeltas(data) 113 | 114 | // assert changed data was deserialized onto SoA state 115 | assert(Transform.position.x[e] === 1) // true 116 | assert(Transform.position.y[e] === 2) // true 117 | assert(Transform.position.z[e] === 3) // true 118 | 119 | 120 | /* ID Mapping */ 121 | 122 | const idMap = new Map([[0,12]]) 123 | 124 | // this will write index 0 from the SoA object as index 12 in the binary data 125 | data = write([e], idMap) 126 | 127 | 128 | // this will read index 0 from the binary data to index 12 on the SoA object 129 | read(data, idMap) 130 | 131 | ``` 132 | -------------------------------------------------------------------------------- /test/ViewCursor.test.js: -------------------------------------------------------------------------------- 1 | import assert, { strictEqual } from 'assert' 2 | import { 3 | createViewCursor, 4 | readFloat32, 5 | readProp, 6 | readUint16, 7 | readUint32, 8 | readUint8, 9 | sliceViewCursor, 10 | spaceUint16, 11 | spaceUint32, 12 | spaceUint8, 13 | writeFloat32, 14 | writeProp, 15 | writePropIfChanged, 16 | writeUint16, 17 | writeUint32, 18 | writeUint8 19 | } from "../src/ViewCursor.js" 20 | 21 | describe('ViewCursor read/write', () => { 22 | 23 | describe('ViewCursor', () => { 24 | 25 | it('should createViewCursor', () => { 26 | const view = createViewCursor() 27 | assert(view.hasOwnProperty('cursor')) 28 | strictEqual(view.cursor, 0) 29 | assert(view.hasOwnProperty('shadowMap')) 30 | assert(view.shadowMap instanceof Map) 31 | }) 32 | 33 | it('should sliceViewCursor', () => { 34 | const view = createViewCursor() 35 | writeUint32(view, 32) 36 | strictEqual(sliceViewCursor(view).byteLength, 4) 37 | strictEqual(view.cursor, 0) 38 | }) 39 | 40 | }) 41 | 42 | describe('writers', () => { 43 | 44 | it('should writeProp', () => { 45 | const view = createViewCursor() 46 | const prop = new Float32Array(1) 47 | const entity = 0 48 | const val = 1.5 49 | prop[entity] = val 50 | writeProp(view, prop, 0) 51 | strictEqual(view.getFloat32(0), val) 52 | }) 53 | 54 | it('should writePropIfChanged', () => { 55 | const view = createViewCursor() 56 | const prop = new Float32Array(1) 57 | const entity = 0 58 | const val = 1.5 59 | 60 | prop[entity] = val 61 | 62 | writePropIfChanged(view, prop, 0) 63 | strictEqual(view.getFloat32(0), val) 64 | 65 | writePropIfChanged(view, prop, 0) 66 | strictEqual(view.getFloat32(4), 0) 67 | 68 | prop[entity]++ 69 | 70 | writePropIfChanged(view, prop, 0) 71 | strictEqual(view.getFloat32(4), val+1) 72 | }) 73 | 74 | it('should writeFloat32', () => { 75 | const view = createViewCursor() 76 | const val = 1.5 77 | writeFloat32(view, val) 78 | strictEqual(view.cursor, Float32Array.BYTES_PER_ELEMENT) 79 | strictEqual(view.getFloat32(0), val) 80 | }) 81 | 82 | it('should writeUint32', () => { 83 | const view = createViewCursor() 84 | const val = 12345678 85 | writeUint32(view, val) 86 | strictEqual(view.cursor, Uint32Array.BYTES_PER_ELEMENT) 87 | strictEqual(view.getUint32(0), val) 88 | }) 89 | 90 | it('should writeUint16', () => { 91 | const view = createViewCursor() 92 | const val = 12345 93 | writeUint16(view, val) 94 | strictEqual(view.cursor, Uint16Array.BYTES_PER_ELEMENT) 95 | strictEqual(view.getUint16(0), val) 96 | }) 97 | 98 | it('should writeUint8', () => { 99 | const view = createViewCursor() 100 | const val = 123 101 | writeUint8(view, val) 102 | strictEqual(view.cursor, Uint8Array.BYTES_PER_ELEMENT) 103 | strictEqual(view.getUint8(0), val) 104 | }) 105 | 106 | it('should spaceUint32', () => { 107 | const view = createViewCursor() 108 | const val = 12345678 109 | const writeUint32 = spaceUint32(view) 110 | writeUint32(val) 111 | strictEqual(view.cursor, Uint32Array.BYTES_PER_ELEMENT) 112 | strictEqual(view.getUint32(0), val) 113 | }) 114 | 115 | it('should spaceUint16', () => { 116 | const view = createViewCursor() 117 | const val = 12345 118 | const writeUint16 = spaceUint16(view) 119 | writeUint16(val) 120 | strictEqual(view.cursor, Uint16Array.BYTES_PER_ELEMENT) 121 | strictEqual(view.getUint16(0), val) 122 | }) 123 | 124 | it('should spaceUint8', () => { 125 | const view = createViewCursor() 126 | const val = 123 127 | const writeUint8 = spaceUint8(view) 128 | writeUint8(val) 129 | strictEqual(view.cursor, Uint8Array.BYTES_PER_ELEMENT) 130 | strictEqual(view.getUint8(0), val) 131 | }) 132 | 133 | }) 134 | 135 | describe('readers', () => { 136 | 137 | it('should readProp', () => { 138 | const view = createViewCursor() 139 | const prop = new Float32Array(1) 140 | const val = 1.5 141 | view.setFloat32(0, val) 142 | strictEqual(readProp(view, prop), val) 143 | }) 144 | 145 | it('should readFloat32', () => { 146 | const view = createViewCursor() 147 | const val = 1.5 148 | view.setFloat32(0, val) 149 | strictEqual(readFloat32(view), val) 150 | }) 151 | 152 | it('should readUint32', () => { 153 | const view = createViewCursor() 154 | const val = 12345678 155 | view.setUint32(0, val) 156 | strictEqual(readUint32(view), val) 157 | }) 158 | 159 | it('should readUint16', () => { 160 | const view = createViewCursor() 161 | const val = 12345 162 | view.setUint16(0, val) 163 | strictEqual(readUint16(view), val) 164 | }) 165 | 166 | it('should readUint8', () => { 167 | const view = createViewCursor() 168 | const val = 123 169 | view.setUint8(0, val) 170 | strictEqual(readUint8(view), val) 171 | }) 172 | 173 | }) 174 | 175 | }) -------------------------------------------------------------------------------- /src/ViewCursor.js: -------------------------------------------------------------------------------- 1 | export const createViewCursor = (buffer = new ArrayBuffer(100000)) => { 2 | const view = new DataView(buffer) 3 | view.cursor = 0 4 | view.shadowMap = new Map() 5 | return view 6 | } 7 | 8 | export const sliceViewCursor = (v) => { 9 | const packet = v.buffer.slice(0, v.cursor) 10 | v.cursor = 0 11 | return packet 12 | } 13 | 14 | export const scrollViewCursor = (v, amount) => { 15 | v.cursor += amount 16 | return v 17 | } 18 | 19 | export const moveViewCursor = (v, where) => { 20 | v.cursor = where 21 | return v 22 | } 23 | 24 | /* Writers */ 25 | 26 | // dynamically obtains primitive type of passed in TypedArray object 27 | // todo: memoize prop type 28 | export const writeProp = (v, prop, entity) => { 29 | v[`set${prop.constructor.name.replace('Array', '')}`](v.cursor, prop[entity]) 30 | v.cursor += prop.BYTES_PER_ELEMENT 31 | return v 32 | } 33 | 34 | export const writePropIfChanged = (v, prop, entity) => { 35 | const { shadowMap } = v 36 | 37 | // todo: decide if initialization counts as a change (probably shouldn't) 38 | // const shadowInit = !shadowMap.has(prop) 39 | 40 | const shadow = shadowMap.get(prop) || (shadowMap.set(prop, prop.slice().fill(0)) && shadowMap.get(prop)) 41 | 42 | const changed = shadow[entity] !== prop[entity] // || shadowInit 43 | 44 | shadow[entity] = prop[entity] 45 | 46 | if (!changed) { 47 | return false 48 | } 49 | 50 | writeProp(v, prop, entity) 51 | 52 | return true 53 | } 54 | 55 | export const writeFloat64 = (v, value) => { 56 | v.setFloat64(v.cursor, value) 57 | v.cursor += Float64Array.BYTES_PER_ELEMENT 58 | return v 59 | } 60 | 61 | export const writeFloat32 = (v, value) => { 62 | v.setFloat32(v.cursor, value) 63 | v.cursor += Float32Array.BYTES_PER_ELEMENT 64 | return v 65 | } 66 | 67 | export const writeUint64 = (v, value) => { 68 | v.setUint64(v.cursor, value) 69 | v.cursor += BigUint64Array.BYTES_PER_ELEMENT 70 | return v 71 | } 72 | 73 | export const writeInt64 = (v, value) => { 74 | v.setInt64(v.cursor, value) 75 | v.cursor += BigInt64Array.BYTES_PER_ELEMENT 76 | return v 77 | } 78 | 79 | export const writeUint32 = (v, value) => { 80 | v.setUint32(v.cursor, value) 81 | v.cursor += Uint32Array.BYTES_PER_ELEMENT 82 | return v 83 | } 84 | 85 | export const writeInt32 = (v, value) => { 86 | v.setInt32(v.cursor, value) 87 | v.cursor += Int32Array.BYTES_PER_ELEMENT 88 | return v 89 | } 90 | 91 | export const writeUint16 = (v, value) => { 92 | v.setUint16(v.cursor, value) 93 | v.cursor += Uint16Array.BYTES_PER_ELEMENT 94 | return v 95 | } 96 | 97 | export const writeInt16 = (v, value) => { 98 | v.setInt16(v.cursor, value) 99 | v.cursor += Int16Array.BYTES_PER_ELEMENT 100 | return v 101 | } 102 | 103 | export const writeUint8 = (v, value) => { 104 | v.setUint8(v.cursor, value) 105 | v.cursor += Uint8Array.BYTES_PER_ELEMENT 106 | return v 107 | } 108 | 109 | export const writeInt8 = (v, value) => { 110 | v.setInt8(v.cursor, value) 111 | v.cursor += Int8Array.BYTES_PER_ELEMENT 112 | return v 113 | } 114 | 115 | /* Spacers */ 116 | 117 | export const spaceFloat64 = (v) => { 118 | const savePoint = v.cursor 119 | v.cursor += Float64Array.BYTES_PER_ELEMENT 120 | return (value) => { 121 | v.setFloat64(savePoint, value) 122 | return v 123 | } 124 | } 125 | 126 | export const spaceFloat32 = (v) => { 127 | const savePoint = v.cursor 128 | v.cursor += Float32Array.BYTES_PER_ELEMENT 129 | return (value) => { 130 | v.setFloat32(savePoint, value) 131 | return v 132 | } 133 | } 134 | 135 | export const spaceUint64 = (v) => { 136 | const savePoint = v.cursor 137 | v.cursor += BigUint64Array.BYTES_PER_ELEMENT 138 | return (value) => { 139 | v.setUint64(savePoint, value) 140 | return v 141 | } 142 | } 143 | 144 | export const spaceInt64 = (v) => { 145 | const savePoint = v.cursor 146 | v.cursor += BigInt64Array.BYTES_PER_ELEMENT 147 | return (value) => { 148 | v.setInt64(savePoint, value) 149 | return v 150 | } 151 | } 152 | 153 | export const spaceUint32 = (v) => { 154 | const savePoint = v.cursor 155 | v.cursor += Uint32Array.BYTES_PER_ELEMENT 156 | return (value) => { 157 | v.setUint32(savePoint, value) 158 | return v 159 | } 160 | } 161 | 162 | export const spaceInt32 = (v) => { 163 | const savePoint = v.cursor 164 | v.cursor += Int32Array.BYTES_PER_ELEMENT 165 | return (value) => { 166 | v.setInt32(savePoint, value) 167 | return v 168 | } 169 | } 170 | 171 | export const spaceUint16 = (v) => { 172 | const savePoint = v.cursor 173 | v.cursor += Uint16Array.BYTES_PER_ELEMENT 174 | return (value) => { 175 | v.setUint16(savePoint, value) 176 | return v 177 | } 178 | } 179 | 180 | export const spaceInt16 = (v) => { 181 | const savePoint = v.cursor 182 | v.cursor += Int16Array.BYTES_PER_ELEMENT 183 | return (value) => { 184 | v.setInt16(savePoint, value) 185 | return v 186 | } 187 | } 188 | 189 | export const spaceUint8 = (v) => { 190 | const savePoint = v.cursor 191 | v.cursor += Uint8Array.BYTES_PER_ELEMENT 192 | return (value) => { 193 | v.setUint8(savePoint, value) 194 | return v 195 | } 196 | } 197 | 198 | export const spaceInt8 = (v) => { 199 | const savePoint = v.cursor 200 | v.cursor += Int8Array.BYTES_PER_ELEMENT 201 | return (value) => { 202 | v.setInt8(savePoint, value) 203 | return v 204 | } 205 | } 206 | 207 | /* Readers */ 208 | 209 | // dynamically obtains primitive type of passed in TypedArray object 210 | // todo: memoize prop type 211 | export const readProp = (v, prop) => { 212 | const val = v[`get${prop.constructor.name.replace('Array', '')}`](v.cursor) 213 | v.cursor += prop.BYTES_PER_ELEMENT 214 | return val 215 | } 216 | 217 | export const readFloat64 = (v) => { 218 | const val = v.getFloat64(v.cursor) 219 | v.cursor += Float64Array.BYTES_PER_ELEMENT 220 | return val 221 | } 222 | 223 | export const readFloat32 = (v) => { 224 | const val = v.getFloat32(v.cursor) 225 | v.cursor += Float32Array.BYTES_PER_ELEMENT 226 | return val 227 | } 228 | 229 | export const readUint64 = (v) => { 230 | const val = v.getBigUint64(v.cursor) 231 | v.cursor += BigUint64Array.BYTES_PER_ELEMENT 232 | return val 233 | } 234 | 235 | export const readInt64 = (v) => { 236 | const val = v.getBigUint64(v.cursor) 237 | v.cursor += BigInt64Array.BYTES_PER_ELEMENT 238 | return val 239 | } 240 | 241 | export const readUint32 = (v) => { 242 | const val = v.getUint32(v.cursor) 243 | v.cursor += Uint32Array.BYTES_PER_ELEMENT 244 | return val 245 | } 246 | 247 | export const readInt32 = (v) => { 248 | const val = v.getInt32(v.cursor) 249 | v.cursor += Int32Array.BYTES_PER_ELEMENT 250 | return val 251 | } 252 | 253 | export const readUint16 = (v) => { 254 | const val = v.getUint16(v.cursor) 255 | v.cursor += Uint16Array.BYTES_PER_ELEMENT 256 | return val 257 | } 258 | 259 | export const readInt16 = (v) => { 260 | const val = v.getInt16(v.cursor) 261 | v.cursor += Int16Array.BYTES_PER_ELEMENT 262 | return val 263 | } 264 | 265 | export const readUint8 = (v) => { 266 | const val = v.getUint8(v.cursor) 267 | v.cursor += Uint8Array.BYTES_PER_ELEMENT 268 | return val 269 | } 270 | 271 | export const readInt8 = (v) => { 272 | const val = v.getInt8(v.cursor) 273 | v.cursor += Int8Array.BYTES_PER_ELEMENT 274 | return val 275 | } 276 | -------------------------------------------------------------------------------- /test/DataWriter.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'assert' 2 | import { createDataWriter, createEntityWriter, writeComponent, writeEntities, writeEntity } from '../src/DataWriter.js' 3 | import { createViewCursor, readFloat32, readUint32, readUint8, sliceViewCursor } from '../src/ViewCursor.js' 4 | 5 | const n = 100 6 | 7 | const Transform = { 8 | position: { 9 | x: new Float32Array(n), 10 | y: new Float32Array(n), 11 | z: new Float32Array(n), 12 | }, 13 | rotation: { 14 | x: new Float32Array(n), 15 | y: new Float32Array(n), 16 | z: new Float32Array(n), 17 | }, 18 | } 19 | 20 | describe('AoS DataWriter', () => { 21 | 22 | describe('snapshot mode', () => { 23 | 24 | it('should writeComponent', () => { 25 | const writeView = createViewCursor() 26 | const entity = 1 27 | 28 | const [x, y, z] = [1.5, 2.5, 3.5] 29 | Transform.position.x[entity] = x 30 | Transform.position.y[entity] = y 31 | Transform.position.z[entity] = z 32 | 33 | const writePosition = writeComponent(Transform.position) 34 | 35 | writePosition(writeView, entity) 36 | 37 | const testView = createViewCursor(writeView.buffer) 38 | 39 | strictEqual(writeView.cursor, 40 | (3 * Float32Array.BYTES_PER_ELEMENT)) 41 | 42 | strictEqual(readFloat32(testView), x) 43 | strictEqual(readFloat32(testView), y) 44 | strictEqual(readFloat32(testView), z) 45 | }) 46 | 47 | it('should writeEntity', () => { 48 | const writeView = createViewCursor() 49 | const entity = 1 50 | 51 | const [x, y, z] = [1.5, 2.5, 3.5] 52 | Transform.position.x[entity] = x 53 | Transform.position.y[entity] = y 54 | Transform.position.z[entity] = z 55 | Transform.rotation.x[entity] = x 56 | Transform.rotation.y[entity] = y 57 | Transform.rotation.z[entity] = z 58 | 59 | writeEntity([writeComponent(Transform)])(writeView, entity) 60 | 61 | const readView = createViewCursor(writeView.buffer) 62 | 63 | strictEqual(writeView.cursor, 64 | (1 * Uint32Array.BYTES_PER_ELEMENT) + 65 | (6 * Float32Array.BYTES_PER_ELEMENT)) 66 | 67 | strictEqual(readUint32(readView), 1) 68 | 69 | strictEqual(readFloat32(readView), x) 70 | strictEqual(readFloat32(readView), y) 71 | strictEqual(readFloat32(readView), z) 72 | strictEqual(readFloat32(readView), x) 73 | strictEqual(readFloat32(readView), y) 74 | strictEqual(readFloat32(readView), z) 75 | }) 76 | 77 | it('should writeEntities', () => { 78 | const writeView = createViewCursor() 79 | 80 | const n = 5 81 | const entities = Array(n).fill(0).map((_,i)=>i) 82 | 83 | const [x, y, z] = [1.5, 2.5, 3.5] 84 | 85 | entities.forEach(entity => { 86 | Transform.position.x[entity] = x 87 | Transform.position.y[entity] = y 88 | Transform.position.z[entity] = z 89 | Transform.rotation.x[entity] = x 90 | Transform.rotation.y[entity] = y 91 | Transform.rotation.z[entity] = z 92 | }) 93 | 94 | const entityWriter = createEntityWriter([Transform]) 95 | 96 | const packet = writeEntities(entityWriter, writeView, entities) 97 | 98 | const expectedBytes = (1 * Uint32Array.BYTES_PER_ELEMENT) + 99 | n * ( 100 | (1 * Uint32Array.BYTES_PER_ELEMENT) + 101 | (6 * Float32Array.BYTES_PER_ELEMENT) 102 | ) 103 | 104 | strictEqual(writeView.cursor, 0) 105 | strictEqual(packet.byteLength, expectedBytes) 106 | 107 | const readView = createViewCursor(writeView.buffer) 108 | 109 | const count = readUint32(readView) 110 | strictEqual(count, entities.length) 111 | 112 | for (let i = 0; i < count; i++) { 113 | 114 | strictEqual(readUint32(readView), i) 115 | 116 | strictEqual(readFloat32(readView), x) 117 | strictEqual(readFloat32(readView), y) 118 | strictEqual(readFloat32(readView), z) 119 | strictEqual(readFloat32(readView), x) 120 | strictEqual(readFloat32(readView), y) 121 | strictEqual(readFloat32(readView), z) 122 | 123 | } 124 | 125 | }) 126 | 127 | it('should createDataWriter', () => { 128 | const write = createDataWriter([Transform]) 129 | 130 | const n = 50 131 | const entities = Array(n).fill(0).map((_,i)=>i) 132 | 133 | const [x, y, z] = [1.5, 2.5, 3.5] 134 | 135 | entities.forEach(entity => { 136 | Transform.position.x[entity] = x 137 | Transform.position.y[entity] = y 138 | Transform.position.z[entity] = z 139 | Transform.rotation.x[entity] = x 140 | Transform.rotation.y[entity] = y 141 | Transform.rotation.z[entity] = z 142 | }) 143 | 144 | const packet = write(entities) 145 | 146 | const expectedBytes = (1 * Uint32Array.BYTES_PER_ELEMENT) + 147 | n * ( 148 | (1 * Uint32Array.BYTES_PER_ELEMENT) + 149 | (6 * Float32Array.BYTES_PER_ELEMENT) 150 | ) 151 | 152 | strictEqual(packet.byteLength, expectedBytes) 153 | 154 | const readView = createViewCursor(packet) 155 | 156 | const count = readUint32(readView) 157 | strictEqual(count, entities.length) 158 | 159 | for (let i = 0; i < count; i++) { 160 | 161 | strictEqual(readUint32(readView), i) 162 | 163 | strictEqual(readFloat32(readView), x) 164 | strictEqual(readFloat32(readView), y) 165 | strictEqual(readFloat32(readView), z) 166 | strictEqual(readFloat32(readView), x) 167 | strictEqual(readFloat32(readView), y) 168 | strictEqual(readFloat32(readView), z) 169 | 170 | } 171 | 172 | }) 173 | 174 | }) 175 | 176 | describe('delta mode', () => { 177 | 178 | it('should writeComponent', () => { 179 | const writeView = createViewCursor() 180 | const entity = 1 181 | 182 | const [x, y, z] = [1.5, 2.5, 3.5] 183 | Transform.position.x[entity] = x 184 | Transform.position.y[entity] = y 185 | Transform.position.z[entity] = z 186 | 187 | const writePosition = writeComponent(Transform.position, true) 188 | 189 | writePosition(writeView, entity) 190 | 191 | const testView = createViewCursor(writeView.buffer) 192 | 193 | strictEqual(writeView.cursor, 194 | (1 * Uint8Array.BYTES_PER_ELEMENT) + 195 | (3 * Float32Array.BYTES_PER_ELEMENT)) 196 | 197 | strictEqual(readUint8(testView), 0b111) 198 | strictEqual(readFloat32(testView), x) 199 | strictEqual(readFloat32(testView), y) 200 | strictEqual(readFloat32(testView), z) 201 | 202 | sliceViewCursor(writeView) 203 | 204 | Transform.position.x[entity]++ 205 | Transform.position.z[entity]++ 206 | 207 | writePosition(writeView, entity) 208 | 209 | const readView = createViewCursor(writeView.buffer) 210 | 211 | strictEqual(writeView.cursor, 212 | (1 * Uint8Array.BYTES_PER_ELEMENT) + 213 | (2 * Float32Array.BYTES_PER_ELEMENT)) 214 | 215 | strictEqual(readUint8(readView), 0b101) 216 | strictEqual(readFloat32(readView), x+1) 217 | strictEqual(readFloat32(readView), z+1) 218 | }) 219 | 220 | it('should writeEntity', () => { 221 | const writeView = createViewCursor() 222 | const entity = 1 223 | 224 | const [x, y, z] = [1.5, 2.5, 3.5] 225 | Transform.position.x[entity] = x 226 | Transform.position.y[entity] = y 227 | Transform.position.z[entity] = z 228 | Transform.rotation.x[entity] = x 229 | Transform.rotation.y[entity] = y 230 | Transform.rotation.z[entity] = z 231 | 232 | writeEntity([writeComponent(Transform, true)], true)(writeView, entity) 233 | 234 | const readView = createViewCursor(writeView.buffer) 235 | 236 | strictEqual(writeView.cursor, 237 | (1 * Uint32Array.BYTES_PER_ELEMENT) + 238 | (2 * Uint8Array.BYTES_PER_ELEMENT) + 239 | (6 * Float32Array.BYTES_PER_ELEMENT)) 240 | 241 | strictEqual(readUint32(readView), 1) 242 | 243 | strictEqual(readUint8(readView), 0b1) 244 | 245 | strictEqual(readUint8(readView), 0b111111) 246 | strictEqual(readFloat32(readView), x) 247 | strictEqual(readFloat32(readView), y) 248 | strictEqual(readFloat32(readView), z) 249 | strictEqual(readFloat32(readView), x) 250 | strictEqual(readFloat32(readView), y) 251 | strictEqual(readFloat32(readView), z) 252 | }) 253 | 254 | it('should writeEntities', () => { 255 | const writeView = createViewCursor() 256 | 257 | const n = 5 258 | const entities = Array(n).fill(0).map((_,i)=>i) 259 | 260 | const [x, y, z] = [1.5, 2.5, 3.5] 261 | 262 | entities.forEach(entity => { 263 | Transform.position.x[entity] = x 264 | Transform.position.y[entity] = y 265 | Transform.position.z[entity] = z 266 | Transform.rotation.x[entity] = x 267 | Transform.rotation.y[entity] = y 268 | Transform.rotation.z[entity] = z 269 | }) 270 | 271 | const entityWriter = createEntityWriter([Transform], true) 272 | 273 | const packet = writeEntities(entityWriter, writeView, entities) 274 | 275 | const expectedBytes = (1 * Uint32Array.BYTES_PER_ELEMENT) + 276 | n * ( 277 | (1 * Uint32Array.BYTES_PER_ELEMENT) + 278 | (2 * Uint8Array.BYTES_PER_ELEMENT) + 279 | (6 * Float32Array.BYTES_PER_ELEMENT) 280 | ) 281 | 282 | strictEqual(writeView.cursor, 0) 283 | strictEqual(packet.byteLength, expectedBytes) 284 | 285 | const readView = createViewCursor(writeView.buffer) 286 | 287 | const count = readUint32(readView) 288 | strictEqual(count, entities.length) 289 | 290 | for (let i = 0; i < count; i++) { 291 | 292 | strictEqual(readUint32(readView), i) 293 | 294 | strictEqual(readUint8(readView), 0b1) 295 | 296 | strictEqual(readUint8(readView), 0b111111) 297 | strictEqual(readFloat32(readView), x) 298 | strictEqual(readFloat32(readView), y) 299 | strictEqual(readFloat32(readView), z) 300 | strictEqual(readFloat32(readView), x) 301 | strictEqual(readFloat32(readView), y) 302 | strictEqual(readFloat32(readView), z) 303 | 304 | } 305 | 306 | }) 307 | 308 | it('should createDataWriter', () => { 309 | const write = createDataWriter([Transform], true) 310 | 311 | const n = 50 312 | const entities = Array(n).fill(0).map((_,i)=>i) 313 | 314 | const [x, y, z] = [1.5, 2.5, 3.5] 315 | 316 | entities.forEach(entity => { 317 | Transform.position.x[entity] = x 318 | Transform.position.y[entity] = y 319 | Transform.position.z[entity] = z 320 | Transform.rotation.x[entity] = x 321 | Transform.rotation.y[entity] = y 322 | Transform.rotation.z[entity] = z 323 | }) 324 | 325 | const packet = write(entities) 326 | 327 | const expectedBytes = (1 * Uint32Array.BYTES_PER_ELEMENT) + 328 | n * ( 329 | (1 * Uint32Array.BYTES_PER_ELEMENT) + 330 | (2 * Uint8Array.BYTES_PER_ELEMENT) + 331 | (6 * Float32Array.BYTES_PER_ELEMENT) 332 | ) 333 | 334 | strictEqual(packet.byteLength, expectedBytes) 335 | 336 | const readView = createViewCursor(packet) 337 | 338 | const count = readUint32(readView) 339 | strictEqual(count, entities.length) 340 | 341 | for (let i = 0; i < count; i++) { 342 | 343 | strictEqual(readUint32(readView), i) 344 | 345 | strictEqual(readUint8(readView), 0b1) 346 | 347 | strictEqual(readUint8(readView), 0b111111) 348 | strictEqual(readFloat32(readView), x) 349 | strictEqual(readFloat32(readView), y) 350 | strictEqual(readFloat32(readView), z) 351 | strictEqual(readFloat32(readView), x) 352 | strictEqual(readFloat32(readView), y) 353 | strictEqual(readFloat32(readView), z) 354 | 355 | } 356 | 357 | }) 358 | 359 | }) 360 | 361 | }) -------------------------------------------------------------------------------- /test/DataReader.test.js: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'assert' 2 | import { createViewCursor, readProp, writeProp } from '../src/ViewCursor.js' 3 | import { checkBitflag, createDataReader, readComponent, readComponentProp, readEntity } from '../src/DataReader.js' 4 | import { createDataWriter, createEntityWriter, writeComponent, writeEntities, writeEntity } from '../src/DataWriter.js' 5 | 6 | const n = 100 7 | 8 | const Transform = { 9 | position: { 10 | x: new Float32Array(n), 11 | y: new Float32Array(n), 12 | z: new Float32Array(n), 13 | }, 14 | rotation: { 15 | x: new Float32Array(n), 16 | y: new Float32Array(n), 17 | z: new Float32Array(n), 18 | }, 19 | } 20 | 21 | 22 | describe('AoS DataReader', () => { 23 | 24 | it('should checkBitflag', () => { 25 | const A = 2**0 26 | const B = 2**1 27 | const C = 2**2 28 | const mask = A | C 29 | strictEqual(checkBitflag(mask, A), true) 30 | strictEqual(checkBitflag(mask, B), false) 31 | strictEqual(checkBitflag(mask, C), true) 32 | }) 33 | 34 | it('should readComponentProp', () => { 35 | const view = createViewCursor() 36 | const entity = 1 37 | 38 | const prop = Transform.position.x 39 | 40 | prop[entity] = 1.5 41 | 42 | writeProp(view, prop, entity) 43 | 44 | prop[entity] = 0 45 | 46 | view.cursor = 0 47 | 48 | readComponentProp(view, prop, entity) 49 | 50 | strictEqual(prop[entity], 1.5) 51 | }) 52 | 53 | describe('snapshot mode', () => { 54 | 55 | it('should readComponent Transform.position', () => { 56 | const writePosition = writeComponent(Transform.position) 57 | 58 | const view = createViewCursor() 59 | const entity = 1 60 | 61 | const [x, y, z] = [1.5, 2.5, 3.5] 62 | Transform.position.x[entity] = x 63 | Transform.position.y[entity] = y 64 | Transform.position.z[entity] = z 65 | 66 | writePosition(view, entity) 67 | 68 | Transform.position.x[entity] = 0 69 | Transform.position.y[entity] = 0 70 | Transform.position.z[entity] = 0 71 | 72 | view.cursor = 0 73 | 74 | const readPosition = readComponent(Transform.position) 75 | 76 | readPosition(view, entity) 77 | 78 | strictEqual(Transform.position.x[entity], x) 79 | strictEqual(Transform.position.y[entity], y) 80 | strictEqual(Transform.position.z[entity], z) 81 | 82 | Transform.position.x[entity] = 10.5 83 | Transform.position.z[entity] = 11.5 84 | 85 | const rewind = view.cursor 86 | 87 | writePosition(view, entity) 88 | 89 | Transform.position.x[entity] = 5.5 90 | Transform.position.z[entity] = 6.5 91 | 92 | view.cursor = rewind 93 | 94 | readPosition(view, entity) 95 | 96 | strictEqual(Transform.position.x[entity], 10.5) 97 | strictEqual(Transform.position.y[entity], y) 98 | strictEqual(Transform.position.z[entity], 11.5) 99 | }) 100 | 101 | it('should readEntity', () => { 102 | const componentReaders = [readComponent(Transform)] 103 | const componentWriters = [writeComponent(Transform)] 104 | 105 | const view = createViewCursor() 106 | const entity = 1 107 | 108 | const idMap = new Map([[entity,entity]]) 109 | 110 | const [x, y, z] = [1.5, 2.5, 3.5] 111 | Transform.position.x[entity] = x 112 | Transform.position.y[entity] = y 113 | Transform.position.z[entity] = z 114 | Transform.rotation.x[entity] = x 115 | Transform.rotation.y[entity] = y 116 | Transform.rotation.z[entity] = z 117 | 118 | writeEntity(componentWriters)(view, entity) 119 | 120 | Transform.position.x[entity] = 0 121 | Transform.position.y[entity] = 0 122 | Transform.position.z[entity] = 0 123 | Transform.rotation.x[entity] = 0 124 | Transform.rotation.y[entity] = 0 125 | Transform.rotation.z[entity] = 0 126 | 127 | view.cursor = 0 128 | 129 | readEntity(componentReaders)(view, idMap) 130 | 131 | strictEqual(Transform.position.x[entity], x) 132 | strictEqual(Transform.position.y[entity], y) 133 | strictEqual(Transform.position.z[entity], z) 134 | strictEqual(Transform.rotation.x[entity], x) 135 | strictEqual(Transform.rotation.y[entity], y) 136 | strictEqual(Transform.rotation.z[entity], z) 137 | 138 | Transform.position.x[entity] = 0 139 | Transform.rotation.z[entity] = 0 140 | 141 | view.cursor = 0 142 | 143 | writeEntity(componentWriters)(view, entity) 144 | 145 | Transform.position.x[entity] = x 146 | Transform.rotation.z[entity] = z 147 | 148 | view.cursor = 0 149 | 150 | readEntity(componentReaders)(view, idMap) 151 | 152 | strictEqual(Transform.position.x[entity], 0) 153 | strictEqual(Transform.position.y[entity], y) 154 | strictEqual(Transform.position.z[entity], z) 155 | strictEqual(Transform.rotation.x[entity], x) 156 | strictEqual(Transform.rotation.y[entity], y) 157 | strictEqual(Transform.rotation.z[entity], 0) 158 | 159 | }) 160 | 161 | it('should readEntities', () => { 162 | const view = createViewCursor() 163 | 164 | const idMap = new Map() 165 | 166 | const n = 5 167 | const entities = Array(n).fill(0).map((_,i)=>i) 168 | 169 | const [x, y, z] = [1.5, 2.5, 3.5] 170 | 171 | entities.forEach(entity => { 172 | Transform.position.x[entity] = x 173 | Transform.position.y[entity] = y 174 | Transform.position.z[entity] = z 175 | Transform.rotation.x[entity] = x 176 | Transform.rotation.y[entity] = y 177 | Transform.rotation.z[entity] = z 178 | idMap.set(entity, entity) 179 | }) 180 | 181 | const entityWriter = createEntityWriter([Transform]) 182 | writeEntities(entityWriter, view, entities) 183 | 184 | for (let i = 0; i < entities.length; i++) { 185 | const entity = entities[i] 186 | 187 | strictEqual(Transform.position.x[entity], x) 188 | strictEqual(Transform.position.y[entity], y) 189 | strictEqual(Transform.position.z[entity], z) 190 | strictEqual(Transform.rotation.x[entity], x) 191 | strictEqual(Transform.rotation.y[entity], y) 192 | strictEqual(Transform.rotation.z[entity], z) 193 | 194 | } 195 | 196 | }) 197 | 198 | it('should createDataReader', () => { 199 | const write = createDataWriter([Transform]) 200 | 201 | const idMap = new Map() 202 | 203 | const n = 5 204 | const entities = Array(n).fill(0).map((_,i)=>i) 205 | entities 206 | 207 | const [x, y, z] = [1.5, 2.5, 3.5] 208 | 209 | entities.forEach(entity => { 210 | Transform.position.x[entity] = x 211 | Transform.position.y[entity] = y 212 | Transform.position.z[entity] = z 213 | Transform.rotation.x[entity] = x 214 | Transform.rotation.y[entity] = y 215 | Transform.rotation.z[entity] = z 216 | idMap.set(entity, entity) 217 | }) 218 | 219 | const packet = write(entities) 220 | 221 | for (let i = 0; i < entities.length; i++) { 222 | const entity = entities[i] 223 | 224 | Transform.position.x[entity] = 0 225 | Transform.position.y[entity] = 0 226 | Transform.position.z[entity] = 0 227 | Transform.rotation.x[entity] = 0 228 | Transform.rotation.y[entity] = 0 229 | Transform.rotation.z[entity] = 0 230 | } 231 | 232 | const read = createDataReader([Transform]) 233 | 234 | read(packet, idMap) 235 | 236 | for (let i = 0; i < entities.length; i++) { 237 | const entity = entities[i] 238 | strictEqual(Transform.position.x[entity], x) 239 | strictEqual(Transform.position.y[entity], y) 240 | strictEqual(Transform.position.z[entity], z) 241 | strictEqual(Transform.rotation.x[entity], x) 242 | strictEqual(Transform.rotation.y[entity], y) 243 | strictEqual(Transform.rotation.z[entity], z) 244 | } 245 | 246 | }) 247 | 248 | }) 249 | 250 | describe('delta mode', () => { 251 | 252 | it('should readComponent Transform.position', () => { 253 | const writePosition = writeComponent(Transform.position, true) 254 | const view = createViewCursor() 255 | const entity = 1 256 | 257 | const [x, y, z] = [1.5, 2.5, 3.5] 258 | Transform.position.x[entity] = x 259 | Transform.position.y[entity] = y 260 | Transform.position.z[entity] = z 261 | 262 | writePosition(view, entity) 263 | 264 | Transform.position.x[entity] = 0 265 | Transform.position.y[entity] = 0 266 | Transform.position.z[entity] = 0 267 | 268 | view.cursor = 0 269 | 270 | const readPosition = readComponent(Transform.position, true) 271 | 272 | readPosition(view, entity) 273 | 274 | strictEqual(Transform.position.x[entity], x) 275 | strictEqual(Transform.position.y[entity], y) 276 | strictEqual(Transform.position.z[entity], z) 277 | 278 | Transform.position.x[entity] = 10.5 279 | Transform.position.z[entity] = 11.5 280 | 281 | const rewind = view.cursor 282 | 283 | writePosition(view, entity) 284 | 285 | Transform.position.x[entity] = 5.5 286 | Transform.position.z[entity] = 6.5 287 | 288 | view.cursor = rewind 289 | 290 | readPosition(view, entity) 291 | 292 | strictEqual(Transform.position.x[entity], 10.5) 293 | strictEqual(Transform.position.y[entity], y) 294 | strictEqual(Transform.position.z[entity], 11.5) 295 | }) 296 | 297 | it('should readEntity', () => { 298 | const componentReaders = [readComponent(Transform, true)] 299 | const componentWriters = [writeComponent(Transform, true)] 300 | const view = createViewCursor() 301 | const entity = 1 302 | 303 | const idMap = new Map([[entity,entity]]) 304 | 305 | const [x, y, z] = [1.5, 2.5, 3.5] 306 | Transform.position.x[entity] = x 307 | Transform.position.y[entity] = y 308 | Transform.position.z[entity] = z 309 | Transform.rotation.x[entity] = x 310 | Transform.rotation.y[entity] = y 311 | Transform.rotation.z[entity] = z 312 | 313 | writeEntity(componentWriters, true)(view, entity) 314 | 315 | Transform.position.x[entity] = 0 316 | Transform.position.y[entity] = 0 317 | Transform.position.z[entity] = 0 318 | Transform.rotation.x[entity] = 0 319 | Transform.rotation.y[entity] = 0 320 | Transform.rotation.z[entity] = 0 321 | 322 | view.cursor = 0 323 | 324 | readEntity(componentReaders, true)(view, idMap) 325 | 326 | strictEqual(Transform.position.x[entity], x) 327 | strictEqual(Transform.position.y[entity], y) 328 | strictEqual(Transform.position.z[entity], z) 329 | strictEqual(Transform.rotation.x[entity], x) 330 | strictEqual(Transform.rotation.y[entity], y) 331 | strictEqual(Transform.rotation.z[entity], z) 332 | 333 | Transform.position.x[entity] = 0 334 | Transform.rotation.z[entity] = 0 335 | 336 | view.cursor = 0 337 | 338 | writeEntity(componentWriters, true)(view, entity) 339 | 340 | Transform.position.x[entity] = x 341 | Transform.rotation.z[entity] = z 342 | 343 | view.cursor = 0 344 | 345 | readEntity(componentReaders, true)(view, idMap) 346 | 347 | strictEqual(Transform.position.x[entity], 0) 348 | strictEqual(Transform.position.y[entity], y) 349 | strictEqual(Transform.position.z[entity], z) 350 | strictEqual(Transform.rotation.x[entity], x) 351 | strictEqual(Transform.rotation.y[entity], y) 352 | strictEqual(Transform.rotation.z[entity], 0) 353 | 354 | }) 355 | 356 | it('should readEntities', () => { 357 | const view = createViewCursor() 358 | 359 | const idMap = new Map() 360 | 361 | const n = 5 362 | const entities = Array(n).fill(0).map((_,i)=>i) 363 | 364 | const [x, y, z] = [1.5, 2.5, 3.5] 365 | 366 | entities.forEach(entity => { 367 | Transform.position.x[entity] = x 368 | Transform.position.y[entity] = y 369 | Transform.position.z[entity] = z 370 | Transform.rotation.x[entity] = x 371 | Transform.rotation.y[entity] = y 372 | Transform.rotation.z[entity] = z 373 | idMap.set(entity, entity) 374 | }) 375 | 376 | const entityWriter = createEntityWriter([Transform], true) 377 | writeEntities(entityWriter, view, entities) 378 | 379 | for (let i = 0; i < entities.length; i++) { 380 | const entity = entities[i] 381 | 382 | strictEqual(Transform.position.x[entity], x) 383 | strictEqual(Transform.position.y[entity], y) 384 | strictEqual(Transform.position.z[entity], z) 385 | strictEqual(Transform.rotation.x[entity], x) 386 | strictEqual(Transform.rotation.y[entity], y) 387 | strictEqual(Transform.rotation.z[entity], z) 388 | 389 | } 390 | 391 | }) 392 | 393 | it('should createDataReader', () => { 394 | const write = createDataWriter([Transform], true) 395 | 396 | const idMap = new Map() 397 | 398 | const n = 50 399 | const entities = Array(n).fill(0).map((_,i)=>i) 400 | 401 | const [x, y, z] = [1.5, 2.5, 3.5] 402 | 403 | entities.forEach(entity => { 404 | Transform.position.x[entity] = x 405 | Transform.position.y[entity] = y 406 | Transform.position.z[entity] = z 407 | Transform.rotation.x[entity] = x 408 | Transform.rotation.y[entity] = y 409 | Transform.rotation.z[entity] = z 410 | idMap.set(entity, entity) 411 | }) 412 | 413 | const packet = write(entities) 414 | 415 | for (let i = 0; i < entities.length; i++) { 416 | const entity = entities[i] 417 | 418 | Transform.position.x[entity] = 0 419 | Transform.position.y[entity] = 0 420 | Transform.position.z[entity] = 0 421 | Transform.rotation.x[entity] = 0 422 | Transform.rotation.y[entity] = 0 423 | Transform.rotation.z[entity] = 0 424 | } 425 | 426 | const read = createDataReader([Transform], true) 427 | 428 | read(packet, idMap) 429 | 430 | for (let i = 0; i < entities.length; i++) { 431 | const entity = entities[i] 432 | strictEqual(Transform.position.x[entity], x) 433 | strictEqual(Transform.position.y[entity], y) 434 | strictEqual(Transform.position.z[entity], z) 435 | strictEqual(Transform.rotation.x[entity], x) 436 | strictEqual(Transform.rotation.y[entity], y) 437 | strictEqual(Transform.rotation.z[entity], z) 438 | } 439 | 440 | }) 441 | 442 | }) 443 | 444 | }) --------------------------------------------------------------------------------