@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
225 lines (169 loc) • 7.32 kB
JavaScript
import { assert } from "../../../core/assert.js";
import { current_time_in_seconds } from "../../../core/time/current_time_in_seconds.js";
import { logger } from "../../logging/GlobalLogger.js";
import { SerializationFlags, SerializationMetadata } from "../components/SerializationMetadata.js";
import { BinaryCollectionSerializer } from "./binary/collection/BinaryCollectionSerializer.js";
import { BinaryObjectSerializationAdapter } from "./binary/object/BinaryObjectSerializationAdapter.js";
import { COMPONENT_SERIALIZATION_TRANSIENT_FIELD } from "./COMPONENT_SERIALIZATION_TRANSIENT_FIELD.js";
/**
* @template T
* @param {Class<T>|constructor|function|{serializable?:boolean}} componentClass
* @returns {boolean}
*/
function isComponentClassSerializable(componentClass) {
return componentClass.serializable !== false;
}
/**
* Whether an entity should be serialized at all
* @param {number} entity
* @param {EntityComponentDataset} dataset
* @param {number} smComponentIndex
* @param {Object} componentInstance
* @returns {boolean}
*/
function isEntityTransient(entity, dataset, smComponentIndex, componentInstance) {
if (smComponentIndex === -1) {
return false;
}
//check component instance flag
if (componentInstance[COMPONENT_SERIALIZATION_TRANSIENT_FIELD] === true) {
return true;
}
/**
*
* @type {SerializationMetadata}
*/
const serializationMetadata = dataset.getComponentByIndex(entity, smComponentIndex);
if (serializationMetadata === undefined) {
return false;
}
const isTransient = serializationMetadata.getFlag(SerializationFlags.Transient);
return isTransient;
}
/**
* @example
* const serializer = new BinaryBufferSerializer();
*
* const registry = new BinarySerializationRegistry();
* registry.registerAdapter(new NameSerializationAdapter());
*
* serializer.registry = registry;
*
* const ecd = new EntityComponentDataset();
* new Entity()
* .add(new Name("John Doe"))
* .build(ecd);
*
* serializer.process(buffer, ecd);
*
* @template CTX
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
class BinaryBufferSerializer {
/**
*
* @type {BinarySerializationRegistry}
*/
registry = null;
/**
* Context to be supplied to individual serialization adapters
* @type {CTX|null}
*/
engine = null;
/**
*
* @param {EntityComponentDataset} dataset
* @param {BinaryBuffer} buffer
*/
process(buffer, dataset) {
assert.defined(buffer, 'buffer');
assert.notNull(buffer, 'buffer');
assert.equal(buffer.isBinaryBuffer, true, 'buffer.isBinaryBuffer !== true');
assert.defined(dataset, 'dataset');
assert.notNull(dataset, 'dataset');
assert.equal(dataset.isEntityComponentDataset, true, 'dataset.isEntityComponentDataset !== true');
console.groupCollapsed('Serialization');
console.time('serializing');
const smComponentIndex = dataset.computeComponentTypeIndex(SerializationMetadata);
// get a list of serializable component classes
const serializableComponentTypes = dataset.getComponentTypeMap().filter(isComponentClassSerializable);
const numSerializableTypes = serializableComponentTypes.length;
const FORMAT_VERSION = 0;
buffer.writeUint16(FORMAT_VERSION)
const typeCountAddress = buffer.position;
buffer.writeUint16(numSerializableTypes);
const collectionSerializer = new BinaryCollectionSerializer();
const registry = this.registry;
collectionSerializer.setRegistry(registry);
collectionSerializer.setBuffer(buffer);
const objectAdapter = new BinaryObjectSerializationAdapter();
objectAdapter.initialize(registry);
let numTypesWritten = 0;
let i;
const engine = this.engine;
for (i = 0; i < numSerializableTypes; i++) {
const componentType = serializableComponentTypes[i];
const positionComponentsStart = buffer.position;
const className = componentType.typeName;
if (className === undefined) {
throw new Error(`Component type has no .typeName property`);
}
collectionSerializer.setClass(className);
const problemLog = [];
const initialized = collectionSerializer.initialize({
/**
*
* @param {string} className
* @param {Class} klass
* @param {BinaryClassSerializationAdapter} adapter
*/
adapterOptionsSupplier(className, klass, adapter) {
return [engine, objectAdapter];
},
problemConsumer(message) {
problemLog.push(message);
}
});
if (!initialized) {
logger.error(`Failed to initialize serializer for class '${className}': ${problemLog.join('; ')}`)
continue;
}
const __start_time = current_time_in_seconds();
let lastEntity = 0;
dataset.traverseComponents(componentType, function (componentInstance, entity) {
if (isEntityTransient(entity, dataset, smComponentIndex, componentInstance)) {
// skip
return;
}
assert.ok(entity >= lastEntity, `entity[=${entity}] < lastEntity[=${lastEntity}]`);
// write only the entity step
const step = entity - lastEntity;
lastEntity = entity;
collectionSerializer.write(step, componentInstance);
});
const numComponentsWritten = collectionSerializer.getElementCount();
const __end_time = current_time_in_seconds();
collectionSerializer.finalize();
if (numComponentsWritten > 0) {
numTypesWritten++;
const currentPosition = buffer.position;
// print size of the component set
const componentsByteSize = currentPosition - positionComponentsStart;
console.log(`Component ${componentType.typeName} written. Count: ${numComponentsWritten}, Bytes: ${componentsByteSize}, Bytes/component: ${Math.ceil(componentsByteSize / numComponentsWritten)}. Time taken: ${((__end_time - __start_time) * 1000).toFixed(2)}ms`);
} else {
// no elements written, lets re-wind to skip the type
buffer.position = positionComponentsStart;
}
}
// go back and patch actual number of written classes
const p = buffer.position;
buffer.position = typeCountAddress;
buffer.writeUint16(numTypesWritten);
// restore position
buffer.position = p;
console.timeEnd('serializing');
console.groupEnd();
}
}
export default BinaryBufferSerializer;