MonoGame SDK

Install package

Add the Colyseus.MonoGame NuGet package to your project:

Terminal
dotnet add package Colyseus.MonoGame

Quick Example

This example shows how to connect to a room, listen for state changes via Callbacks, and send messages.

Game1.cs
using Microsoft.Xna.Framework;
using Colyseus;
using Colyseus.Schema;
 
public class Game1 : Game
{
    Client client;
    Room<MyRoomState> room;
 
    protected override async void LoadContent()
    {
        client = new Client("ws://localhost:2567");
 
        room = await client.JoinOrCreate<MyRoomState>("my_room");
        System.Diagnostics.Debug.WriteLine("Joined room: " + room.Id);
 
        // Get state callbacks handler
        var callbacks = Callbacks.Get(room);
 
        // Listen for changes on a state property
        callbacks.Listen(state => state.currentTurn, (currentValue, previousValue) => {
            System.Diagnostics.Debug.WriteLine($"Turn changed: {previousValue} -> {currentValue}");
        });
 
        // Listen for players being added
        callbacks.OnAdd(state => state.players, (sessionId, player) => {
            System.Diagnostics.Debug.WriteLine($"Player joined: {sessionId}");
 
            callbacks.Listen(player, p => p.hp, (currentHp, previousHp) => {
                System.Diagnostics.Debug.WriteLine($"Player {sessionId} hp: {currentHp}");
            });
        });
 
        // Listen for players being removed
        callbacks.OnRemove(state => state.players, (sessionId, player) => {
            System.Diagnostics.Debug.WriteLine($"Player left: {sessionId}");
        });
 
        // Send a message to the server
        room.Send("move", new { x = 10f, y = 20f });
 
        // Listen for messages from the server
        room.OnMessage<string>("chat", (message) => {
            System.Diagnostics.Debug.WriteLine("Chat: " + message);
        });
    }
 
    protected override void OnExiting(object sender, EventArgs args)
    {
        if (room != null) room.Leave();
        base.OnExiting(sender, args);
    }
}

Optional: ColyseusGameComponent

The SDK automatically dispatches WebSocket messages internally via an async Task loop, so no extra setup is needed. However, you can optionally register the ColyseusGameComponent to synchronize message dispatching with MonoGame’s Update() cycle:

Game1.cs
using Colyseus.MonoGame;
 
protected override void Initialize()
{
    Components.Add(new ColyseusGameComponent(this));
    base.Initialize();
}

SDK API

Navigate to the Client SDK for API Reference, and select the C# (Unity) tab.

State Schema Codegen

For compiled languages like C#, you need to generate client-side schema classes that match your server’s state structure. Run the following command from your server directory:

Terminal
npx schema-codegen src/rooms/schema/* --csharp --output ../MyGame/States/

See the full State Schema Codegen documentation for more options and details.

DynamicSchema (Skip Code Generation)

If you want to skip the code generation step entirely, you can use DynamicSchema instead of generated schema classes. DynamicSchema builds its type metadata at runtime from the server handshake, so no generated files are needed.

Game1.cs
using Microsoft.Xna.Framework;
using Colyseus;
using Colyseus.Schema;
 
public class Game1 : Game
{
    Client client;
    Room<DynamicSchema> room;
 
    protected override async void LoadContent()
    {
        client = new Client("ws://localhost:2567");
 
        room = await client.JoinOrCreate<DynamicSchema>("my_room");
        System.Diagnostics.Debug.WriteLine("Joined room: " + room.Id);
 
        // Access state fields dynamically
        var players = room.State.Get<MapSchema<DynamicSchema>>("players");
        var self = players[room.SessionId];
        var hp = self.Get<int>("hp");
 
        // Callbacks use string-based property names instead of expressions
        var callbacks = Callbacks.Get(room);
 
        callbacks.Listen<string>("currentTurn", (currentValue, previousValue) => {
            System.Diagnostics.Debug.WriteLine($"Turn changed: {previousValue} -> {currentValue}");
        });
 
        callbacks.OnAdd<DynamicSchema>("players", (sessionId, player) => {
            System.Diagnostics.Debug.WriteLine($"Player joined: {sessionId}");
 
            // Access nested collections
            var items = player.Get<ArraySchema<DynamicSchema>>("items");
        });
 
        callbacks.OnRemove<DynamicSchema>("players", (sessionId, player) => {
            System.Diagnostics.Debug.WriteLine($"Player left: {sessionId}");
        });
    }
 
    protected override void OnExiting(object sender, EventArgs args)
    {
        if (room != null) room.Leave();
        base.OnExiting(sender, args);
    }
}
⚠️

DynamicSchema trades compile-time type safety for convenience. You won’t get IDE autocomplete on state fields, and type errors will only surface at runtime. For production projects, Schema Codegen is recommended.

Message Types Codegen

The same schema-codegen command also generates C# classes for any TypeScript interface containing "Message" in its name. This allows you to use strongly-typed message objects on the client side.

For example, given the following server-side code:

src/rooms/MyRoom.ts
interface MoveMessage {
  x: number;
  y: number;
}
 
// ...
 
messages = {
  move: (client: Client, message: MoveMessage) => {
    // handle move
  }
}

Running the codegen command will automatically generate a matching C# class:

Generated C# output
public class MoveMessage {
    public float x;
    public float y;
}

You can then use these classes when sending and receiving messages on the client:

Sending a message to the server
room.Send("move", new MoveMessage() { x = 1.0f, y = 2.0f });

The same applies to messages sent from the server to the client. For example, given a server-side interface:

src/rooms/MyRoom.ts
interface EventMessage {
  type: string;
  data: string;
}
 
// ...
 
onJoin(client: Client) {
  client.send("event", { type: "welcome", data: "Hello!" } as EventMessage);
}

You can listen for it on the client with the generated type:

Receiving a message from the server
room.OnMessage<EventMessage>("event", (message) => {
    System.Diagnostics.Debug.WriteLine(message.type);
    System.Diagnostics.Debug.WriteLine(message.data);
});

Debugging

Breakpoints

If you set a breakpoint in your application while the WebSocket connection is open, the connection will be closed automatically after 3 seconds due to inactivity. To prevent the WebSocket connection from dropping, use pingInterval: 0 during development:

app.config.ts
import { defineServer } from "colyseus";
import { WebSocketTransport } from "@colyseus/ws-transport";
 
const server = defineServer({
    // ...
    transport: new WebSocketTransport({
        pingInterval: 0, // <--- HERE
    }),
    // ...
});

Make sure to have a pingInterval higher than 0 on production. The default pingInterval value is 3000.