UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

225 lines (169 loc) • 7.32 kB
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;