Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696// ============================================================================// ExcaliburJS Serialization System - Centralized Architecture// ============================================================================
import { Entity } from '../entity-component-system/entity';import { Actor } from '../actor';import type { Component, ComponentCtor } from '../entity-component-system/component';import { MotionComponent, TransformComponent } from '../entity-component-system';import { PointerComponent } from '../input/pointer-component';import { GraphicsComponent } from '../graphics/graphics-component';import { ActionsComponent } from '../actions/actions-component';import { BodyComponent } from '../collision/body-component';import { ColliderComponent } from '../collision/collider-component';import type { Graphic } from '../graphics';import { Logger } from './log';
// ============================================================================// Core Serialization Interfaces// ============================================================================
/** * Base interface for all serialized data */export interface SerializedData { type: string; version?: string;}
/** * Entity serialization format */export interface EntityData extends SerializedData { type: 'Entity' | 'Actor'; name: string; tags: string[]; components: ComponentData[]; children: EntityData[]; customInstance?: string | null;}
/** * Component serialization format */export interface ComponentData extends SerializedData { type: string; [key: string]: any;}
// ============================================================================// Serialization System - Central Manager// ============================================================================
/** * Central serialization system for ExcaliburJS * Handles all serialization/deserialization with extensible registry */export class Serializer { // ============================================================================ // Private Members // ============================================================================ private static _LOGGER = Logger.getInstance(); // Component type registry private static _COMPONENTREGISTRY = new Map<string, ComponentCtor>(); // Graphics registry for reference-based serialization private static _GRAPHICSREGISTRY = new Map<string, typeof Graphic>(); // Actor class registry for custom actor types private static _ACTORREGISTRY = new Map<string, typeof Actor>(); // Custom serializers for complex types private static _CUSTOMSERIALIZERS = new Map< string, { serialize: (obj: any) => any; deserialize: (data: any) => any; } >(); private static _INITIALIZED = false;
// ============================================================================ // Initialization // ============================================================================
static init(autoRegisterComponents: boolean = true): void { if (Serializer._INITIALIZED) { Serializer._LOGGER.warn('Serializer already initialized. Call reset() before re-initializing.'); return; }
Serializer._REGISTERBUILTINSERIALIZERS();
if (autoRegisterComponents) { Serializer._REGISTERCOMMONCOMPONENTS(); } Serializer._INITIALIZED = true; }
static isInitialized(): boolean { return Serializer._INITIALIZED; }
// ============================================================================ // Component Registry // ============================================================================
// #region compRegistry static registerComponent<T extends Component>(ctor: ComponentCtor<T>): void { const typeName = ctor.name; if (Serializer._COMPONENTREGISTRY.has(typeName)) { Serializer._LOGGER.warn(`Component ${typeName} is already registered`); return; } Serializer._COMPONENTREGISTRY.set(typeName, ctor); }
private static _REGISTERCOMMONCOMPONENTS(): void { const commonComponents: ComponentCtor[] = [ TransformComponent, MotionComponent, GraphicsComponent, PointerComponent, ActionsComponent, BodyComponent, ColliderComponent ];
Serializer.registerComponents(commonComponents); }
static registerComponents(ctors: ComponentCtor[]): void { for (const ctor of ctors) { Serializer.registerComponent(ctor); } }
/** * Check if a component type is registered */ static isComponentRegistered(typeName: string): boolean { return Serializer._COMPONENTREGISTRY.has(typeName); }
/** * Get all registered component types */ static getRegisteredComponents(): string[] { return Array.from(Serializer._COMPONENTREGISTRY.keys()); }
/** * Unregister a component type */ static unregisterComponent(typeName: string): boolean { if (!Serializer._INITIALIZED) { Serializer._LOGGER.warn('Serializer not initialized. Call init() before registering components.'); return false; } return Serializer._COMPONENTREGISTRY.delete(typeName); }
/** * Clear all registered components */ static clearComponents(): void { Serializer._COMPONENTREGISTRY.clear(); }
// #endregion compRegistry
// ============================================================================ // Custom Actor Class Registry // ============================================================================
// #region customActorRegistry
static registerCustomActor(ctor: typeof Actor): void { const typeName = ctor.name; if (Serializer._ACTORREGISTRY.has(typeName)) { Serializer._LOGGER.warn(`Custom Actor ${typeName} is already registered`); return; } Serializer._ACTORREGISTRY.set(typeName, ctor); }
static registerCustomActors(ctors: Array<typeof Actor>): void { for (const ctor of ctors) { Serializer.registerCustomActor(ctor); } }
static isCustomActorRegistered(typeName: string): boolean { if (Serializer._ACTORREGISTRY.has(typeName)) { return true; } return false; }
static getRegisteredCustomActors(): string[] { if (Serializer._ACTORREGISTRY.size > 0) { return Array.from(Serializer._ACTORREGISTRY.keys()); } return []; }
static getCustomActor(typeName: string): typeof Actor | null { if (Serializer._ACTORREGISTRY.has(typeName)) { return Serializer._ACTORREGISTRY.get(typeName)!; } return null; }
static unregisterCustomActor(typeName: string): boolean { if (Serializer._ACTORREGISTRY.has(typeName)) { return Serializer._ACTORREGISTRY.delete(typeName); } return false; }
static clearCustomActors(): void { Serializer._ACTORREGISTRY.clear(); }
// #endregion customActorRegistry
// ============================================================================ // Graphics Registry // ============================================================================
// #region graphicsRegistry
static registerGraphic(id: string, graphic: any): void { if (Serializer._GRAPHICSREGISTRY.has(id)) { Serializer._LOGGER.warn(`Graphic ${id} is already registered`); return; } Serializer._GRAPHICSREGISTRY.set(id, graphic); }
static registerGraphics(graphics: Record<string, any>): void { for (const [id, graphic] of Object.entries(graphics)) { Serializer.registerGraphic(id, graphic); } } static getGraphic(id: string): any | undefined { return Serializer._GRAPHICSREGISTRY.get(id); }
/** * Check if a graphic is registered */ static isGraphicRegistered(id: string): boolean { return Serializer._GRAPHICSREGISTRY.has(id); }
/** * Get all registered graphic IDs */ static getRegisteredGraphics(): string[] { return Array.from(Serializer._GRAPHICSREGISTRY.keys()); }
/** * Unregister a graphic */ static unregisterGraphic(id: string): boolean { return Serializer._GRAPHICSREGISTRY.delete(id); }
/** * Clear all registered graphics */ static clearGraphics(): void { Serializer._GRAPHICSREGISTRY.clear(); }
// #endregion graphicsRegistry
// ============================================================================ // Component Serialization // ============================================================================
// #region componentSerialization
/** * Serialize a component */ static serializeComponent(component: Component): ComponentData | null { if (!Serializer._INITIALIZED) { Serializer._LOGGER.warn('Serializer not initialized. Call init() before registering components.'); return null; } if (!component.serialize) { Serializer._LOGGER.warn(`Component ${component.constructor.name} does not have a serialize method`); return null; }
try { const data = component.serialize(); // Ensure type field is set return { type: component.constructor.name, ...data }; } catch (error) { Serializer._LOGGER.error(`Error serializing component ${component.constructor.name}:`, error); return null; } }
/** * Deserialize a component */ static deserializeComponent(data: ComponentData): Component | null { if (!Serializer._INITIALIZED) { Serializer._LOGGER.warn('Serializer not initialized. Call init() before registering components.'); return null; } if (!data.type) { Serializer._LOGGER.error('Component data missing type field'); return null; }
const attachGraphic = data.type === 'GraphicsComponent';
const Ctor = Serializer._COMPONENTREGISTRY.get(data.type); if (!Ctor) { Serializer._LOGGER.warn(`Component type ${data.type} is not registered`); return null; }
try { const component = new Ctor(); if (component.deserialize) { component.deserialize(data);
if (attachGraphic) { const grph = Serializer.getGraphic((data as any).current).clone(); if (grph) { (component as GraphicsComponent).use(grph); }
if (data.tint) { (component as GraphicsComponent).current!.tint = Serializer._CUSTOMSERIALIZERS.get('Color')!.deserialize(data.tint); } } } return component; } catch (error) { Serializer._LOGGER.error(`Error deserializing component ${data.type}:`, error); return null; } }
// #endregion componentSerialization
// ============================================================================ // Entity Serialization // ============================================================================
// #region entitySerialization
static serializeEntity(entity: Entity): EntityData { const components: ComponentData[] = [];
// Serialize tags const tags: string[] = []; for (const tag of entity.tags) { tags.push(tag); }
for (const component of entity.getComponents()) { const componentData = Serializer.serializeComponent(component); if (componentData) { components.push(componentData); } }
const children: EntityData[] = []; for (const child of entity.children) { children.push(Serializer.serializeEntity(child)); }
return { type: 'Entity', name: entity.name, tags, components, children }; }
static deserializeEntity(data: EntityData): Entity | null { try { const entity = new Entity(); entity.name = data.name;
// Deserialize components for (const componentData of data.components) { const component = Serializer.deserializeComponent(componentData); if (component) { entity.addComponent(component); } }
// Recursively deserialize children for (const childData of data.children) { const child = Serializer.deserializeEntity(childData); if (child) { entity.addChild(child); } }
return entity; } catch (error) { Serializer._LOGGER.error('Error deserializing entity:', error); return null; } }
// #endregion entitySerialization
// ============================================================================ // Actor Serialization // ============================================================================
// #region actorSerialization
static serializeActor(actor: Actor): EntityData { const components: ComponentData[] = [];
// is actor custom actor
let customInstance: string | null = null;
for (const [key, ctor] of Serializer._ACTORREGISTRY.entries()) { if (actor instanceof ctor) { customInstance = key; break; } }
// Serialize tags const tags: string[] = []; for (const tag of actor.tags) { tags.push(tag); }
for (const component of actor.getComponents()) { const componentData = Serializer.serializeComponent(component); if (componentData) { components.push(componentData); } }
const children: EntityData[] = []; for (const child of actor.children) { children.push(Serializer.serializeEntity(child)); }
return { type: 'Actor', name: actor.name, tags, components, children, customInstance }; } static deserializeActor(data: EntityData): Actor | null { try { let entity: Actor;
if (data.customInstance) { const ctor = Serializer._ACTORREGISTRY.get(data.customInstance); if (ctor) { entity = new ctor() as Actor; } } else { entity = new Actor(); } entity!.name = data.name;
//remove all existing components // debugger; for (const component of entity!.getComponents()) { entity!.removeComponent(component, true); }
// debugger;
// Deserialize components for (const componentData of data.components) { const component = Serializer.deserializeComponent(componentData); if (component) { entity!.addComponent(component); } }
// Setup actor references and getters/setters if (entity!.get(TransformComponent)) { entity!.transform = entity!.get(TransformComponent)!; } if (entity!.get(PointerComponent)) { entity!.pointer = entity!.get(PointerComponent)!; } if (entity!.get(GraphicsComponent)) { entity!.graphics = entity!.get(GraphicsComponent)!; } if (entity!.get(MotionComponent)) { entity!.motion = entity!.get(MotionComponent)!; } if (entity!.get(ActionsComponent)) { entity!.actions = entity!.get(ActionsComponent)!; } if (entity!.get(BodyComponent)) { entity!.body = entity!.get(BodyComponent)!; } if (entity!.get(ColliderComponent)) { entity!.collider = entity!.get(ColliderComponent); }
// Recursively deserialize children for (const childData of data.children) { const child = Serializer.deserializeEntity(childData); if (child) { entity!.addChild(child); } }
return entity!; } catch (error) { Serializer._LOGGER.error('Error deserializing entity:', error); return null; } }
// #endregion actorSerialization
// ============================================================================ // JSON Conversion // ============================================================================
// #region JSONConversion
/** * Serialize entity to JSON string */ static entityToJSON(entity: Entity, pretty: boolean = false): string { const data = Serializer.serializeEntity(entity); return JSON.stringify(data, null, pretty ? 2 : 0); }
/** * Deserialize entity from JSON string */ static entityFromJSON(json: string): Entity | null { try { const data = JSON.parse(json); return Serializer.deserializeEntity(data); } catch (error) { Serializer._LOGGER.error('Error parsing entity JSON:', error); return null; } }
static actorToJSON(actor: Actor, pretty: boolean = false): string { const data = Serializer.serializeActor(actor); return JSON.stringify(data, null, pretty ? 2 : 0); }
static actorFromJSON(json: string): Actor | null { try { const data = JSON.parse(json); return Serializer.deserializeActor(data); } catch (error) { Serializer._LOGGER.error('Error parsing actor JSON:', error); return null; } }
// #endregion JSONConversion
// ============================================================================ // Validation // ============================================================================
// #region validation
/** * Validate entity data structure */ static validateEntityData(data: any): data is EntityData { if (!data || typeof data !== 'object') { return false; } if (data.type !== 'Entity') { return false; } if (typeof data.name !== 'string') { return false; } if (!Array.isArray(data.components)) { return false; } if (!Array.isArray(data.children)) { return false; }
// Validate components for (const comp of data.components) { if (!comp.type || typeof comp.type !== 'string') { return false; } }
// Recursively validate children for (const child of data.children) { if (!Serializer.validateEntityData(child)) { return false; } }
return true; }
// #endregion validation
// ============================================================================ // Utilities // ============================================================================
// #region Utilities
/** * Reset all registrations (useful for testing) */
static reset(): void { Serializer._COMPONENTREGISTRY.clear(); Serializer._CUSTOMSERIALIZERS.clear(); Serializer._GRAPHICSREGISTRY.clear(); Serializer._ACTORREGISTRY.clear(); Serializer._INITIALIZED = false; }
// ============================================================================ // Custom Type Serializers // ============================================================================
private static _REGISTERBUILTINSERIALIZERS(): void { // Vector serializer Serializer.registerCustomSerializer( 'Vector', (vec) => ({ x: vec.x, y: vec.y }), (data) => ({ x: data.x, y: data.y }) );
// Color serializer Serializer.registerCustomSerializer( 'Color', (color) => ({ r: color.r, g: color.g, b: color.b, a: color.a }), (data) => ({ r: data.r, g: data.g, b: data.b, a: data.a }) );
// BoundingBox serializer Serializer.registerCustomSerializer( 'BoundingBox', (bb) => ({ left: bb.left, top: bb.top, right: bb.right, bottom: bb.bottom }), (data) => ({ left: data.left, top: data.top, right: data.right, bottom: data.bottom }) ); }
/** * Register a custom serializer for a specific type * Useful for types like Vector, Color, BoundingBox, etc. */ static registerCustomSerializer(typeName: string, serialize: (obj: any) => any, deserialize: (data: any) => any): void { Serializer._CUSTOMSERIALIZERS.set(typeName, { serialize, deserialize }); }
static getCustomSerializer(typeName: string): { serialize: (obj: any) => any; deserialize: (data: any) => any } | undefined { return Serializer._CUSTOMSERIALIZERS.get(typeName); }
static getRegistry(registry: 'graphics' | 'actors' | 'components'): Map<string, any> { switch (registry) { case 'graphics': return Serializer._GRAPHICSREGISTRY; case 'actors': return Serializer._ACTORREGISTRY; case 'components': return Serializer._COMPONENTREGISTRY; } }
// #endregion Utilities}