State Handling ยป Schema¶
The SchemaSerializer
has been introduced since Colyseus 0.10, and it's the default serialization method.
Server-side¶
To use the SchemaSerializer
, you must:
- Have a state class extending the
Schema
class - Annotate all your synchonizable properties with the
@type()
decorator - Instantiate the state for your room (
this.setState(new MyState())
)
Are you not using TypeScript?
Decorators are not part of ECMAScript yet, so the type
syntax on plain JavaScript is still a bit odd to use, which you can see in the "JavaScript" tab for each snippet.
import { Schema, type } from "@colyseus/schema";
class MyState extends Schema {
@type("string")
currentTurn: string;
}
const schema = require('@colyseus/schema');
const Schema = schema.Schema;
const type = schema.type;
class MyState extends Schema {
}
type("string")(MyState.prototype, "currentTurn");
Custom child data type¶
You may define more custom data types inside your "root" state definition, as a direct reference, map, or array.
import { Schema, type } from "@colyseus/schema";
class Map {
@type("number")
width: number;
@type("number")
height: number;
@type("number")
items: number = 10;
}
class MyState extends Schema {
@type(Map)
map: Map = new Map();
}
const schema = require('@colyseus/schema');
const Schema = schema.Schema;
const type = schema.type;
class Map extends Schema {
}
type("number")(Map.prototype, "width");
type("number")(Map.prototype, "height");
type("number")(Map.prototype, "items");
class MyState extends Schema {
constructor () {
super();
this.map = new Map();
}
}
type(Map)(MyState.prototype, "map");
Array of custom data type¶
When using arrays, it's important to use the ArraySchema
type. Do not use plain arrays.
ArraySchema
is recommended for describing the world map, or any collection in your game.
import { Schema, ArraySchema, type } from "@colyseus/schema";
class Block {
@type("x")
width: number;
@type("y")
height: number;
}
class MyState extends Schema {
@type([ Block ])
blocks = new ArraySchema<Block>();
}
const schema = require('@colyseus/schema');
const Schema = schema.Schema;
const ArraySchema = schema.ArraySchema;
const type = schema.type;
class Block extends Schema {
}
type("number")(Map.prototype, "x");
type("number")(Map.prototype, "y");
class MyState extends Schema {
constructor () {
super();
this.blocks = new ArraySchema();
}
}
type([ Block ])(MyState.prototype, "blocks");
Map of custom data type¶
When using a map, it's important to use the MapSchema
type. Do not use a plain object or the native Map type.
MapSchema
is recommended to track your game entities by id, such as players, enemies, etc.
import { Schema, MapSchema, type } from "@colyseus/schema";
class Player {
@type("x")
width: number;
@type("y")
height: number;
}
class MyState extends Schema {
@type({ map: Player })
players = new MapSchema<Player>();
}
const schema = require('@colyseus/schema');
const Schema = schema.Schema;
const MapSchema = schema.MapSchema;
const type = schema.type;
class Player extends Schema {
}
type("number")(Map.prototype, "x");
type("number")(Map.prototype, "y");
class MyState extends Schema {
constructor () {
super();
this.players = new MapSchema();
}
}
type({ map: Player })(MyState.prototype, "players");
Primitive types¶
These are the types you can provide for the @type()
decorator, and their limitations.
Tip
If you know exactly the range of your number
properties, you can optimize the serialization by providing the right primitive type for it.
Otherwise, use "number"
, which will adds an extra byte to identify itself during serialization.
Type | Description | Limitation |
---|---|---|
"string" |
utf8 strings | maximum byte size of 4294967295 |
"number" |
auto-detects the int or float type to be used. (adds an extra byte on output) |
0 to 18446744073709551615 |
"boolean" |
true or false |
0 or 1 |
"int8" |
signed 8-bit integer | -128 to 127 |
"uint8" |
unsigned 8-bit integer | 0 to 255 |
"int16" |
signed 16-bit integer | -32768 to 32767 |
"uint16" |
unsigned 16-bit integer | 0 to 65535 |
"int32" |
signed 32-bit integer | -2147483648 to 2147483647 |
"uint32" |
unsigned 32-bit integer | 0 to 4294967295 |
"int64" |
signed 64-bit integer | -9223372036854775808 to 9223372036854775807 |
"uint64" |
unsigned 64-bit integer | 0 to 18446744073709551615 |
"float32" |
single-precision floating-point number | -3.40282347e+38 to 3.40282347e+38 |
"float64" |
double-precision floating-point number | -1.7976931348623157e+308 to 1.7976931348623157e+308 |
Client-side¶
There are three ways to handle changes in the client-side when using SchemaSerializer
:
onChange(changes: DataChange[])
¶
You can register the onChange
to track a single object's property changes. The onChange
callback is called with an array of changed properties, along with their previous value.
room.state.onChange = (changes) => {
changes.forEach(change => {
console.log(change.field);
console.log(change.value);
console.log(change.previousValue);
});
};
You cannot register a onChange
callback for objects that haven't been synchronized to the client-side yet.
onAdd (instance, key)
¶
The onAdd
callback can only be used in maps (MapSchema
) and arrays (ArraySchema
). The onAdd
callback is called with the added instance and its key on holder object as argument.
room.state.players.onAdd = (player, key) => {
console.log(player, "has been added at", key);
// add your player entity to the game world!
// If you want to track changes on a child object inside a map, this is a common pattern:
player.onChange = function(changes) {
}
};
onRemove (instance, key)
¶
The onRemove
callback can only be used in maps (MapSchema
) and arrays (ArraySchema
). The onRemove
callback is called with the added instance and its key on holder object as argument.
room.state.players.onRemove = (player, key) => {
console.log(player, "has been removed at", key);
// remove your player entity from the game world!
};