Colyseus Schema

repository·master·Indexed 41 days ago

https://github.com/colyseus/schema

A high-performance binary serialization library for automatic state replication in multiplayer games. It provides delta encoding, state views for per-client visibility, and support for multiple languages. Version 5.0.33 features the schema() factory for defining synchronizable structures in JavaScript and TypeScript, as well as the schema-codegen CLI for generating client-side schemas in C#, C++, Haxe, and others.

Tokens
14.1K
Snippets
45
Records
61
Agent score
50%

What's inside Colyseus Schema

  1. How change tracking works in Colyseus Schema

    master

    Every Schema instance maintains an internal ChangeTree instance that tracks modifications made to its properties. Changes are categorized into three distinct types to support different synchronization needs:

    • all: Tracks all changes.
    • changes: Tracks specific changes.
    • filtered: Used specifically when encoding with StateView to provide a subset of the state.

    When a property within a Schema instance is modified, the change is cached within this ChangeTree for later encoding.

  2. How Reflection works for schema synchronization

    master

    Reflection allows you to encode the schema definition itself. This is useful for sending the schema from the server to a client that doesn't have the definition pre-compiled (e.g., a dynamic client). The server encodes the schema using Reflection.encode(encoder), and the client rebuilds the state structure using Reflection.decode(encodedSchema).

    import { schema, t, Encoder, Reflection } from "@colyseus/schema";
    
    const MyState = schema({
      currentTurn: t.string(),
    }, "MyState");
    
    // server-side: encode the schema definition itself
    const encoder = new Encoder(new MyState());
    const encodedStateSchema = Reflection.encode(encoder);
    
    // client-side: rebuild the state without having its definition
    const decoder = Reflection.decode(encodedStateSchema);
    const myState = decoder.state;
  3. Understand the two encoding modes: Encode All vs. Encode Changes

    master

    Colyseus Schema uses two primary encoding strategies to synchronize state between server and client:

    1. Encode All: Used when a client first joins a room. The server sends the complete, full state of the room to ensure the client is fully synchronized.
    2. Encode Changes: Used during active gameplay/sessions. The server encodes only the pending changes tracked in the ChangeTree to minimize bandwidth usage.
  4. How StateViews work for per-client visibility

    master

    You can restrict which properties a client sees by using the .view() modifier on field definitions. A StateView instance is then used during the encoding process to filter the data. Only properties not tagged with .view() (or those explicitly added to the view) will be sent to the client.

    import { schema, t, StateView } from "@colyseus/schema";
    
    const Player = schema({
      secret: t.string().view(), // Only visible to specific views
      notSecret: t.string(),    // Visible to everyone
    }, "Player");
    
    const MyState = schema({
      players: t.map(Player),
    }, "MyState");
    
    // Usage
    const view = new StateView();
    view.add(playerInstance);
  5. How Schema instances are encoded

    master

    The encoding format follows specific rules based on the type of Schema being used:

    • Root Structure: The root has a refId of 0.
    • General Schema: Instances are encoded as a sequence of index => value.
    • Child Schemas: When encoding a child Schema structure, only its refId is encoded to save space.
    • MapSchema, CollectionSchema, and SetSchema: These are encoded using a dynamicIndex (encoded sequentially) followed by the value.
    • ArraySchema: Encoded as a sequence of index + value.

    ArraySchema with StateView

    When using StateView to filter arrays:

    • Arrays of primitive types: You cannot filter individual items. Either the entire array is encoded, or none of it is.
    • Arrays of Schema instances: Instead of the array index, the refId of the schema instance is encoded.
  6. Choose between 'schema' and 'json' serializers

    master

    When a client joins a room, the format of the encoded state is determined by the SerializerId provided in the handshake.

    • schema (Default): A highly efficient binary format. It is the most performant option for real-time synchronization but is not human-readable.
    • json: A human-readable format that is less efficient in terms of bandwidth and CPU usage.
  7. Schema limitations and best practices

    master

    To ensure optimal performance and compatibility, follow these guidelines:

    • Field Limit: A single Schema can hold a maximum of 64 fields. Use nested structures if you need more.
    • Fast Path Optimization: Fields tagged with .view(), .unreliable(), or .fullStateOnly() at index 32 or higher use a slower linear scan. Place frequently mutated tagged fields at lower indexes (below 32) to stay on the bitwise fast path.
    • Memory/Allocation: Schemas with ≤ 8 fields are highly optimized (no per-field allocation). Schemas with > 8 fields allocate a Uint8Array for operation storage.
    • Data Types:
      • NaN or null numbers are encoded as 0.
      • null strings are encoded as "".
      • Infinity is encoded as Number.MAX_SAFE_INTEGER.
    • Constraints: Multi-dimensional arrays are not supported. All items in an Array or Map must be of the same type.
  8. Generate client-side schema files with schema-codegen

    master

    For strictly typed languages (C#, C++, Haxe, etc.), you can automatically generate client-side schema files from your server-side TypeScript definitions. This supports both schema() and decorator-based styles.

    Requirements:

    • schema-codegen requires TypeScript 5.x or 6.x installed in your project.
    • TypeScript 7+ is not supported because it no longer ships the required JS compiler API.

    Note for JavaScript/Lua: If you are using interpreted languages like JavaScript or Lua, you do not need to generate files manually; these languages can rebuild the Schema locally using Reflection.

    # C#/Unity
    schema-codegen ./schemas/State.ts --output ./unity-project/ --csharp
    
    # C/C++
    schema-codegen ./schemas/State.ts --output ./cpp-project/ --cpp
    
    # Haxe
    schema-codegen ./schemas/State.ts --output ./haxe-project/ --haxe
  9. Use Bundle Mode in schema-codegen

    master

    By default, schema-codegen creates one file per schema class. If you prefer a single file containing all generated classes, use the --bundle option. You can also use --namespace to wrap the bundled code in a specific namespace.

    Bundled Filenames by Language:

    • TypeScript: schema.ts (or {namespace}.ts)
    • JavaScript: schema.js (or {namespace}.js)
    • C#: Schema.cs (or {namespace}.cs)
    • C++: schema.hpp (or {namespace}.hpp)
    • Haxe: Schema.hx (or {namespace}.hx)
    • Java: Schema.java
    • Lua: schema.lua (or {namespace}.lua)
    • C: schema.h (or {namespace}.h)
    # Generate a single bundled file
    schema-codegen ./schemas/State.ts --output ./unity-project/ --csharp --bundle
    
    # With namespace
    schema-codegen ./schemas/State.ts --output ./unity-project/ --csharp --bundle --namespace MyGame.Schema
  10. Define schemas using Decorators

    master

    The classic @type() decorator style is still supported and produces the identical wire format as the schema() factory. However, this style requires legacy TypeScript configuration and is being phased out in favor of the schema() factory due to compatibility issues with modern build tools like esbuild, SWC, and Vite.

    import { Schema, type } from '@colyseus/schema';
    
    export class Player extends Schema {
      @type("string") name: string;
      @type("number") x: number;
      @type("number") y: number;
    }
  11. Define schemas using the `schema()` factory

    master

    The recommended way to define synchronizable structures is using the schema() function and t.* field builders. This approach works in both plain JavaScript and TypeScript without special compiler configuration. For TypeScript users, you can use SchemaType<typeof MySchema> to get strictly typed instances.

    When defining collections like arrays or maps, pass the type name (as a string) for primitive types, but pass the class reference for nested Schema structures.

    import { schema, t, type SchemaType } from '@colyseus/schema';
    
    export const Player = schema({
      name: t.string(),
      x: t.number(),
      y: t.number(),
    }, "Player");
    export type Player = SchemaType<typeof Player>;
    
    export const MyState = schema({
      fieldString: t.string(),
      fieldNumber: t.number(),
      player: Player, // Nested Schema
      arrayOfPlayers: t.array(Player), // Array of Schema
      mapOfPlayers: t.map(Player), // Map of Schema
      arrayOfNumbers: t.array("number"), // Array of primitive
      mapOfStrings: t.map("string"), // Map of primitive
    }, "MyState");
    export type MyState = SchemaType<typeof MyState>;
  12. Configure TypeScript for Decorators

    master

    If you choose to use the @type() decorator style, you must enable legacy decorators in your tsconfig.json to ensure compatibility with the @colyseus/schema implementation.

    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "useDefineForClassFields": false
      }
    }