Skip to main content

Serialization

ExcaliburJS Serialization System

The Serialization System provides a centralized, extensible architecture for saving and loading game Entities, Actors, and Components in ExcaliburJS.

Overview

The Serializer class serves as the central manager for all serialization operations, providing:

  • Component type registration and management
  • Entity and Actor serialization/deserialization
  • Custom Actor class support
  • Graphics registry for reference-based serialization
  • Custom type serializers for complex objects
  • JSON conversion utilities
  • Data validation

Getting Started

Basic Setup

ts
// Initialize with auto-registration of common components (default)
ex.Serializer.init();
 
// OR initialize without auto-registering components
ex.Serializer.init(false);
ts
// Initialize with auto-registration of common components (default)
ex.Serializer.init();
 
// OR initialize without auto-registering components
ex.Serializer.init(false);

Serializing an Entity

ts
// Serialize to object
const entityData = ex.Serializer.serializeEntity(myEntity);
 
// Serialize to JSON string
const json = ex.Serializer.entityToJSON(myEntity, true); // pretty print
ts
// Serialize to object
const entityData = ex.Serializer.serializeEntity(myEntity);
 
// Serialize to JSON string
const json = ex.Serializer.entityToJSON(myEntity, true); // pretty print

Deserializing an Entity

ts
// Deserialize from object
const entity = ex.Serializer.deserializeEntity(entityData);
 
// Deserialize from JSON string
let newEntity = ex.Serializer.entityFromJSON(json);
ts
// Deserialize from object
const entity = ex.Serializer.deserializeEntity(entityData);
 
// Deserialize from JSON string
let newEntity = ex.Serializer.entityFromJSON(json);

Component Registry

Components must be registered before they can be serialized/deserialized. Common Excalibur Actor components are pre-registered on init.

Registering Components

ts
// Register a single component
 
class HealthComponent extends ex.Component {
currentHealth: number;
maxHealth: number;
 
constructor(maxHealth: number) {
super();
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
}
}
 
ex.Serializer.registerComponent(HealthComponent);
 
// Register multiple components
ex.Serializer.registerComponents([myComponent1, myComponent2]);
ts
// Register a single component
 
class HealthComponent extends ex.Component {
currentHealth: number;
maxHealth: number;
 
constructor(maxHealth: number) {
super();
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
}
}
 
ex.Serializer.registerComponent(HealthComponent);
 
// Register multiple components
ex.Serializer.registerComponents([myComponent1, myComponent2]);

Checking Registration

ts
// Check if a component is registered
if (ex.Serializer.isComponentRegistered('TransformComponent')) {
// Component is registered
}
 
// Get all registered components
const registeredTypes = ex.Serializer.getRegisteredComponents();
console.log(registeredTypes); // ['TransformComponent', 'MotionComponent', ...]
ts
// Check if a component is registered
if (ex.Serializer.isComponentRegistered('TransformComponent')) {
// Component is registered
}
 
// Get all registered components
const registeredTypes = ex.Serializer.getRegisteredComponents();
console.log(registeredTypes); // ['TransformComponent', 'MotionComponent', ...]

Unregistering Components

ts
// Unregister a specific component
ex.Serializer.unregisterComponent('TransformComponent');
 
// Clear all components
ex.Serializer.clearComponents();
ts
// Unregister a specific component
ex.Serializer.unregisterComponent('TransformComponent');
 
// Clear all components
ex.Serializer.clearComponents();

Custom Actor Classes

The serialization system supports custom Actor subclasses, preserving their specific implementations.

Registering Custom Actors

Custom Actor classes are registered by the 'name' property on construction, which leaves them as defaults if you do not specify.

ts
class Player extends ex.Actor {
constructor() {
super({
name: 'Player',
});
// Player-specific initialization
}
}
 
class Enemy extends ex.Actor {
constructor() {
super({
name: 'Enemy',
});
// Enemy-specific initialization
}
}
 
// Register custom actors
ex.Serializer.registerCustomActor(Player);
ex.Serializer.registerCustomActors([Enemy, Boss, NPC]);
ts
class Player extends ex.Actor {
constructor() {
super({
name: 'Player',
});
// Player-specific initialization
}
}
 
class Enemy extends ex.Actor {
constructor() {
super({
name: 'Enemy',
});
// Enemy-specific initialization
}
}
 
// Register custom actors
ex.Serializer.registerCustomActor(Player);
ex.Serializer.registerCustomActors([Enemy, Boss, NPC]);

Serializing Custom Actors

After you register a custom Actor class, then when you serialize a custom Actor, the system automatically detects its type and includes a customInstance field:

ts
class Player extends ex.Actor {
constructor() {
super({
name: 'myTestActor',
});
// Player-specific initialization
}
}
 
ex.Serializer.registerCustomActor(Player);
const player = new Player();
const actorData = ex.Serializer.serializeActor(player);
// actorData.customInstance === 'myTestActor'
ts
class Player extends ex.Actor {
constructor() {
super({
name: 'myTestActor',
});
// Player-specific initialization
}
}
 
ex.Serializer.registerCustomActor(Player);
const player = new Player();
const actorData = ex.Serializer.serializeActor(player);
// actorData.customInstance === 'myTestActor'

Deserializing Custom Actors

The system automatically instantiates the correct custom Actor class:

ts
let actorData: ex.EntityData = {
type: 'Actor',
name: "player",
tags: [],
components: [],
children: [],
customInstance: 'Player'
};
const player = ex.Serializer.deserializeActor(actorData);
// player is an instance of the class with name: 'Player', not just Actor
ts
let actorData: ex.EntityData = {
type: 'Actor',
name: "player",
tags: [],
components: [],
children: [],
customInstance: 'Player'
};
const player = ex.Serializer.deserializeActor(actorData);
// player is an instance of the class with name: 'Player', not just Actor

Managing Custom Actors

ts
// Check if registered
if (ex.Serializer.isCustomActorRegistered('Player')) {
// Player is registered
}
 
// Get registered custom actors
const customActors = ex.Serializer.getRegisteredCustomActors();
 
// Get a specific actor constructor
const PlayerCtor = ex.Serializer.getCustomActor('Player');
 
// Unregister
ex.Serializer.unregisterCustomActor('Player');
 
// Clear all
ex.Serializer.clearCustomActors();
ts
// Check if registered
if (ex.Serializer.isCustomActorRegistered('Player')) {
// Player is registered
}
 
// Get registered custom actors
const customActors = ex.Serializer.getRegisteredCustomActors();
 
// Get a specific actor constructor
const PlayerCtor = ex.Serializer.getCustomActor('Player');
 
// Unregister
ex.Serializer.unregisterCustomActor('Player');
 
// Clear all
ex.Serializer.clearCustomActors();

Graphics Registry

The Graphics Registry enables reference-based serialization for graphics. Instead of serializing entire graphic objects, you serialize references (IDs) and resolve them during deserialization.

Why Use Graphics Registry?

Graphics objects (Sprites, Animations, etc.) can be large and complex. The registry pattern:

  • Reduces serialized data size
  • Avoids duplicate graphic data
  • Enables sharing graphics between entities
  • Simplifies graphic management

The purpose of registering Graphics to the Serializer is to assist the 'deserialization' as we are not serializing and storing the graphics themselves, just the reference to the graphics. Thus, this is assisted by using graphics in this pattern:

ts
ex.Serializer.registerGraphic('mygraphic', playerSprite);
 
class Player extends ex.Actor{
constructor(){
super();
// add grahphic by key
this.graphics.add('mygraphic', playerSprite) //<-- this sets the key identifier for serialization
this.graphics.use('mygraphic')
}
}
 
ts
ex.Serializer.registerGraphic('mygraphic', playerSprite);
 
class Player extends ex.Actor{
constructor(){
super();
// add grahphic by key
this.graphics.add('mygraphic', playerSprite) //<-- this sets the key identifier for serialization
this.graphics.use('mygraphic')
}
}
 

When the Graphics Component is deserialized it will grab the 'mygraphic' and attempt to reattach the Sprite to the new Actor.

Registering Graphics

ts
// Load your resources
const playerImage = new ex.ImageSource('./player.png');
playerImage.load();
 
// Create graphics
const playerIdleSprite = playerImage.toSprite();
const playerWalkAnimation =new ex.Animation(testAnimationOptions);
 
// Register graphics with unique IDs
// When an actor deserializes, it will use the id set to look up the Graphic
ex.Serializer.registerGraphic('player-idle', playerIdleSprite);
ex.Serializer.registerGraphic('player-walk', playerWalkAnimation);
 
// Register multiple at once
ex.Serializer.registerGraphics({
'enemy-idle': enemyIdleSprite,
'enemy-attack': enemyAttackSprite,
'coin-spin': coinAnimation
});
ts
// Load your resources
const playerImage = new ex.ImageSource('./player.png');
playerImage.load();
 
// Create graphics
const playerIdleSprite = playerImage.toSprite();
const playerWalkAnimation =new ex.Animation(testAnimationOptions);
 
// Register graphics with unique IDs
// When an actor deserializes, it will use the id set to look up the Graphic
ex.Serializer.registerGraphic('player-idle', playerIdleSprite);
ex.Serializer.registerGraphic('player-walk', playerWalkAnimation);
 
// Register multiple at once
ex.Serializer.registerGraphics({
'enemy-idle': enemyIdleSprite,
'enemy-attack': enemyAttackSprite,
'coin-spin': coinAnimation
});

Managing Graphics

ts
// Get a graphic
const graphic = ex.Serializer.getGraphic('player-idle');
 
// Check if registered
if (ex.Serializer.isGraphicRegistered('player-idle')) {
// Graphic is available
}
 
// Get all registered graphic IDs
const graphicIds = ex.Serializer.getRegisteredGraphics();
 
// Unregister a graphic
ex.Serializer.unregisterGraphic('player-idle');
 
// Clear all graphics
ex.Serializer.clearGraphics();
ts
// Get a graphic
const graphic = ex.Serializer.getGraphic('player-idle');
 
// Check if registered
if (ex.Serializer.isGraphicRegistered('player-idle')) {
// Graphic is available
}
 
// Get all registered graphic IDs
const graphicIds = ex.Serializer.getRegisteredGraphics();
 
// Unregister a graphic
ex.Serializer.unregisterGraphic('player-idle');
 
// Clear all graphics
ex.Serializer.clearGraphics();

Actor Serialization

Actors have special handling to preserve their convenience properties and references.

Serializing Actors

ts
const actor = new ex.Actor({ x: 100, y: 200 });
// ... configure actor ...
 
const actorData = ex.Serializer.serializeActor(actor);
ts
const actor = new ex.Actor({ x: 100, y: 200 });
// ... configure actor ...
 
const actorData = ex.Serializer.serializeActor(actor);

Deserializing Actors

The system automatically:

  • Removes existing default components
  • Adds deserialized components
  • Restores actor property references (actor.transform, actor.graphics, etc.)
ts
const actor = ex.Serializer.deserializeActor(actorData);
// actor.transform, actor.graphics, etc. are properly set
ts
const actor = ex.Serializer.deserializeActor(actorData);
// actor.transform, actor.graphics, etc. are properly set

Custom Type Serializers

For complex types that need special serialization logic, you can register custom serializers.

Registering Custom Serializers

ts
ex.Serializer.registerCustomSerializer(
'MyCustomType',
// Serialize function
(obj) => ({
data: obj.someData,
value: obj.someValue
}),
// Deserialize function
(data) => new MyCustomType(data.data, data.value)
);
ts
ex.Serializer.registerCustomSerializer(
'MyCustomType',
// Serialize function
(obj) => ({
data: obj.someData,
value: obj.someValue
}),
// Deserialize function
(data) => new MyCustomType(data.data, data.value)
);

Example

ts
// Color serializer
ex.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 })
);
 
ts
// Color serializer
ex.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 })
);
 

Built-in Serializers

The system provides serializers for common ExcaliburJS types:

Vectors

Registered as {x: number, y: number}

Colors

Registered as {r: number, g: number, b: number, a: number}

BoundingBox

Registered as {left: number, top: number, right: number, bottom: number}

Data Formats

EntityData Format

ts
interface EntityData {
type: 'Entity' | 'Actor';
name: string;
tags: string[];
components: ComponentData[];
children: EntityData[];
customInstance?: string; // For custom Actor classes
}
ts
interface EntityData {
type: 'Entity' | 'Actor';
name: string;
tags: string[];
components: ComponentData[];
children: EntityData[];
customInstance?: string; // For custom Actor classes
}

ComponentData Format

ts
interface ComponentData {
type: string; // Component class name
// ... component-specific fields
}
ts
interface ComponentData {
type: string; // Component class name
// ... component-specific fields
}

Complete Example

ts
// Initialize
ex.Serializer.init();
 
// Register custom actors
class Player extends ex.Actor { /* ... */ }
ex.Serializer.registerCustomActor(Player);
 
// Load and register graphics
const spriteSheet = new ex.ImageSource('./player.png');
spriteSheet.load();
const playerSprite = spriteSheet.toSprite();
ex.Serializer.registerGraphic('player-sprite', playerSprite);
 
// Create and serialize an actor
const player = new Player();
player.pos.x = 100;
player.pos.y = 200;
player.graphics.use(playerSprite);
 
const savedActorData = ex.Serializer.serializeActor(player);
console.log('Saved:', savedActorData);
 
// Later: deserialize
const loadedPlayer:ex.Entity = ex.Serializer.deserializeActor(savedActorData) as ex.Entity;
game.add(loadedPlayer);
ts
// Initialize
ex.Serializer.init();
 
// Register custom actors
class Player extends ex.Actor { /* ... */ }
ex.Serializer.registerCustomActor(Player);
 
// Load and register graphics
const spriteSheet = new ex.ImageSource('./player.png');
spriteSheet.load();
const playerSprite = spriteSheet.toSprite();
ex.Serializer.registerGraphic('player-sprite', playerSprite);
 
// Create and serialize an actor
const player = new Player();
player.pos.x = 100;
player.pos.y = 200;
player.graphics.use(playerSprite);
 
const savedActorData = ex.Serializer.serializeActor(player);
console.log('Saved:', savedActorData);
 
// Later: deserialize
const loadedPlayer:ex.Entity = ex.Serializer.deserializeActor(savedActorData) as ex.Entity;
game.add(loadedPlayer);