@colyseus/schema

中文文档
5.0.45发布于 2026/11/05MITTypeScriptESM + CJS零依赖近 30 天下载 79.2万

Automatic state replication for multiplayer games. Mutate plain objects, and every client gets a live, typed mirror through delta encoding.

原始使用文档
文档目录21 节
  1. Schema definition
  2. Decorators (still supported)
  3. TypeScript support
  4. Supported types
  5. Primitive Types
  6. Declaration:
  7. Reflection
  8. StateView / .view()
  9. Encoder
  10. Encoding with views
  11. Decoder
  12. Backwards/forwards compatibility
  13. Limitations and best practices
  14. Generating client-side schema files (for strictly typed lan…
  15. Code Generation Options
  16. Bundle Mode
  17. Benchmarks:
  18. Decoder implementation in other languages
  19. Why
  20. Inspiration:
  21. License

Automatic state replication for multiplayer games.
Made for Colyseus, yet can be used standalone.

Features

  • Plain Objects, Synced: Assign a property, push to an array, or set on a map. Change tracking, encoding and decoding happen for you.
  • Delta Encoding: Only the properties that changed are sent, in a compact binary format.
  • Trigger Callbacks at Decoding: Bring your own callback system at decoding, or use the built-in one.
  • Instance Reference Tracking: Share references of the same instance across the state.
  • State Views: Per-client visibility. Decide which properties and instances each client receives.
  • Reflection: Encode/Decode schema definitions.
  • Schema Generation: Generate client-side schema files for strictly typed languages.
  • Type Safety: Strictly typed schema definitions.
  • Multiple Language Support: Decoders available for multiple languages (C#, Lua, Haxe).

Schema definition

Define synchronizable structures with schema() and t.* field builders:

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,
  arrayOfPlayers: t.array(Player),
  mapOfPlayers: t.map(Player),
}, "MyState");
export type MyState = SchemaType<typeof MyState>;

schema() returns a real class (instanceof works) and runs in plain JavaScript and TypeScript alike, with no compiler configuration. The type aliases are TypeScript-only sugar — omit them in plain JS.

Decorators (still supported)

The classic @type() decorator style remains fully supported, and both styles produce the identical wire format:

import { Schema, type, ArraySchema, MapSchema } from '@colyseus/schema';

export class Player extends Schema {
  @type("string") name: string;
  @type("number") x: number;
  @type("number") y: number;
}

We are moving away from decorators due to ecosystem compatibility issues: they depend on the legacy experimentalDecorators implementation and useDefineForClassFields: false, which conflict with modern toolchain defaults (esbuild, SWC, Vite), diverge from the TC39 decorators specification, and aren't available in plain JavaScript. See the Decorators reference for setup and the full decorator documentation.

TypeScript support

Compatible with TypeScript 5.x, 6.x and 7.x.

The @type() decorator uses legacy decorators — enable them in your tsconfig.json:

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

Note: schema-codegen requires TypeScript 5.x or 6.x installed in your project — TypeScript 7's native compiler no longer ships the JS compiler API that codegen uses to parse your schema files. The runtime and your build are not affected by this.

Supported types

Primitive Types

TypeDescriptionLimitation
stringutf8 stringsmaximum byte size of 4294967295
numberauto-detects int or float type. (extra byte on output)0 to 18446744073709551615
booleantrue or false0 or 1
int8signed 8-bit integer-128 to 127
uint8unsigned 8-bit integer0 to 255
int16signed 16-bit integer-32768 to 32767
uint16unsigned 16-bit integer0 to 65535
int32signed 32-bit integer-2147483648 to 2147483647
uint32unsigned 32-bit integer0 to 4294967295
int64signed 64-bit integer-9223372036854775808 to 9223372036854775807
uint64unsigned 64-bit integer0 to 18446744073709551615
float32single-precision floating-point number-3.40282347e+38 to 3.40282347e+38
float64double-precision floating-point number-1.7976931348623157e+308 to 1.7976931348623157e+308

Declaration:

Each primitive type is declared through its t.* factory (t.string(), t.uint8(), …). Collection elements take the type name, not a builder: t.array("string"), never t.array(t.string()).

Primitive types (string, number, boolean, etc)

name: t.string(),
health: t.int32(),

Child Schema structures

player: Player,   // shorthand for t.ref(Player)

Array of Schema structure

arrayOfPlayers: t.array(Player),

Array of a primitive type

You can't mix types inside arrays.

arrayOfNumbers: t.array("number"),
arrayOfStrings: t.array("string"),

Map of Schema structure

mapOfPlayers: t.map(Player),

Map of a primitive type

You can't mix primitive types inside maps.

mapOfNumbers: t.map("number"),
mapOfStrings: t.map("string"),

Reflection

The Schema definitions can encode itself through Reflection. You can have the definition implementation in the server-side, and just send the encoded reflection to the client-side, for example:

import { schema, t, Encoder, Reflection } from "@colyseus/schema";

const MyState = schema({
  currentTurn: t.string(),
  // ... more definitions
}, "MyState");

// server-side: encode the schema definition itself
const encoder = new Encoder(new MyState());
const encodedStateSchema = Reflection.encode(encoder);
// ... send `encodedStateSchema` across the network

// client-side: rebuild the state without having its definition
const decoder = Reflection.decode(encodedStateSchema);
const myState = decoder.state;

StateView / .view()

You can use the .view() field modifier to filter properties that should be sent only to StateView's that have access to it.

import { schema, t } from "@colyseus/schema";

const Player = schema({
  secret: t.string().view(),
  notSecret: t.string(),
}, "Player");

const MyState = schema({
  players: t.map(Player),
}, "MyState");

Using the StateView

const view = new StateView();
view.add(player);

Encoder

There are 3 major features of the Encoder class:

  • Encoding the full state
  • Encoding the state changes
  • Encoding state with filters (properties tagged with .view())
import { Encoder } from "@colyseus/schema";

const state = new MyState();
const encoder = new Encoder(state);

New clients must receive the full state on their first connection:

const fullEncode = encoder.encodeAll();
// ... send "fullEncode" to client and decode it

Further state changes must be sent in order:

const changesBuffer = encoder.encode();
// ... send "changesBuffer" to client and decode it

Encoding with views

When using .view() and StateView's, a single "full encode" must be used for multiple views. Each view also must add its own changes.

// shared buffer iterator
const it = { offset: 0 };

// shared full encode
encoder.encodeAll(it);
const sharedOffset = it.offset;

// view 1
const fullEncode1 = encoder.encodeAllView(view1, sharedOffset, it);
// ... send "fullEncode1" to client1 and decode it

// view 2
const fullEncode2 = encoder.encodeAllView(view2, sharedOffset, it);
// ... send "fullEncode" to client2 and decode it

Encoding changes per views:

// shared buffer iterator
const it = { offset: 0 };

// shared changes encode
encoder.encode(it);
const sharedOffset = it.offset;

// view 1
const view1Encoded = this.encoder.encodeView(view1, sharedOffset, it);
// ... send "view1Encoded" to client1 and decode it

// view 2
const view2Encoded = this.encoder.encodeView(view2, sharedOffset, it);
// ... send "view2Encoded" to client2 and decode it

// discard all changes after encoding is done.
encoder.discardChanges();

Decoder

The Decoder class is used to decode the binary data received from the server.

import { Decoder } from "@colyseus/schema";

const state = new MyState();
const decoder = new Decoder(state);
decoder.decode(encodedBytes);

Backwards/forwards compatibility

Backwards/forwards compatibility is possible by declaring new fields at the end of existing structures, and earlier declarations to not be removed, but be marked .deprecated() when needed.

This is particularly useful for native-compiled targets, such as C#, C++, Haxe, etc - where the client-side can potentially not have the most up-to-date version of the schema definitions.

Limitations and best practices

  • Each Schema structure can hold up to 64 fields. If you need more fields, use nested structures.
  • Fields tagged with .view(), .unreliable(), or .fullStateOnly() at field indexes ≥ 32 use a slower per-mutation classification path (linear scan over the tagged-field list instead of a single bitwise op). For schemas with more than 32 fields, declare frequently-mutated tagged fields earlier so they fall in the bitmask fast path.
  • Schemas with ≤ 8 fields store per-field operation bytes inline in two numbers (no allocation per instance). Schemas with > 8 fields allocate a small Uint8Array per instance for op storage. The difference is only material when allocating thousands of instances per tick — prefer narrower nested structures in that regime.
  • NaN or null numbers are encoded as 0
  • null strings are encoded as ""
  • Infinity numbers are encoded as Number.MAX_SAFE_INTEGER
  • Multi-dimensional arrays are not supported.
  • Items inside Arrays and Maps must be all instance of the same type.
  • @colyseus/schema encodes only field values in the specified order.
    • Both encoder (server) and decoder (client) must have same schema definition.
    • The order of the fields must be the same.

Generating client-side schema files (for strictly typed languages)

If you're using JavaScript or LUA, there's no need to bother about this. Interpreted programming languages are able to re-build the Schema locally through the use of Reflection.

You can generate the client-side schema files based on your server-side schema definitions automatically — both schema() and decorator styles are supported.

schema-codegen requires TypeScript 5.x or 6.x installed in your project (TypeScript 7+ no longer ships the JS compiler API it uses for parsing).

# 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

Code Generation Options

OptionDescription
--outputThe output directory for generated client-side schema files (required)
--bundleBundle all generated files into a single file
--namespaceGenerate namespace/package on output code
--decoratorCustom name for @type decorator to scan for
--tsconfigtsconfig.json to resolve import path aliases with (default: the nearest tsconfig.json/jsconfig.json above each source file)

Imports are followed to discover related schemas, including bare specifiers mapped by compilerOptions.paths/baseUrl and barrel files that re-export them. Imports of installed packages are not followed.

Bundle Mode

By default, the code generator creates one file per schema class. Use the --bundle option to combine all generated classes into a single file:

# 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

Bundle mode output filenames:

  • 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)

Benchmarks:

Scenario@colyseus/schemamsgpack + fossil-delta
Initial state size (100 entities)26713283
Updating x/y of 1 entity after initial state926
Updating x/y of 50 entities after initial state342684
Updating x/y of 100 entities after initial state6681529

Decoder implementation in other languages

Each Colyseus SDK has its own decoder implementation of the @colyseus/schema protocol:

Why

Initial thoughts/assumptions, for Colyseus:

  • little to no bottleneck for detecting state changes.
  • have a schema definition on both the server and the client
  • better experience on statically-typed languages (C#, C++)
  • mutations should be cheap.

Practical Colyseus issues this should solve:

  • Avoid decoding large objects that haven't been patched
  • Allow to send different patches for each client
  • Better developer experience on statically-typed languages

Inspiration:

License

MIT

版本与发布记录

@colyseus/schema 共发布 473 个版本,首次发布于 2019/01/29;过去 12 个月发布了 82 个版本。

版本发布日期距今
5.0.45latest2026/11/055 天前
5.0.442026/11/055 天前
5.0.432026/10/2911 天前
5.0.422026/10/2020 天前
5.0.412026/10/1823 天前
5.0.402026/10/1526 天前

其他发布通道:alpha → 3.0.0-alpha.50,legacy → 2.0.37,legacy-4 → 4.0.31,next → 5.0.19。

关于 @colyseus/schema 的常见问题

@colyseus/schema 是什么?

Automatic state replication for multiplayer games. Mutate plain objects, and every client gets a live, typed mirror through delta encoding.

如何安装 @colyseus/schema?

在项目目录运行 npm install @colyseus/schema,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 schema-debug、schema-codegen,可以用 npx schema-debug 直接运行。

@colyseus/schema 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@colyseus/schema 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@colyseus/schema 可以商用吗?

@colyseus/schema 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@colyseus/schema 的下载量有多少?

根据 npm 官方下载统计,@colyseus/schema 在 2026/10/11 – 2026/11/09 这 30 天内被下载 791,666 次,日均约 26,389 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@colyseus/schema 还在维护吗?

最新版本 5.0.45 发布于 2026/11/05(5 天前);至今共发布 473 个版本,其中过去 12 个月发布了 82 个;首次发布于 2019/01/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。