@playertwo/transport-colyseus
v0.2.0
Published
Colyseus transport adapter for playertwo multiplayer SDK
Downloads
7
Maintainers
Readme
@playertwo/transport-colyseus
Colyseus room adapter for playertwo multiplayer SDK. Use Colyseus for matchmaking, rooms, and server infrastructure while using playertwo's declarative API for game logic.
Why Use This?
Best of Both Worlds:
- ✅ Colyseus handles rooms, matchmaking, and server management
- ✅ playertwo handles game logic with a clean, declarative API
Perfect For:
- Web games that need matchmaking/lobbies
- Server-authoritative multiplayer games
- Production deployments with managed infrastructure
- Teams familiar with Colyseus who want cleaner game logic code
Installation
pnpm add @playertwo/core @playertwo/transport-colyseus colyseus.jsQuick Start
Client-Side
import { Client } from 'colyseus.js';
import { defineGame, GameRuntime } from '@playertwo/core';
import { ColyseusTransport } from '@playertwo/transport-colyseus';
// Define your game logic with playertwo
const game = defineGame({
minPlayers: 2,
maxPlayers: 4,
setup: () => ({
players: {},
score: {}
}),
actions: {
move: (state, playerId, input) => {
state.players[playerId] = input.position;
},
updateScore: (state, playerId, points) => {
state.score[playerId] = (state.score[playerId] || 0) + points;
}
}
});
// Connect to Colyseus
const client = new Client('ws://localhost:2567');
const room = await client.joinOrCreate('my_game_room');
// Create playertwo transport from Colyseus room
const transport = new ColyseusTransport(room);
// Create game runtime
const runtime = new GameRuntime(game, transport, {
isHost: room.sessionId === '...' // Determine host via Colyseus
});
// Use playertwo's clean API
runtime.submitAction('move', { position: { x: 10, y: 20 } });Server-Side (Colyseus)
Create a Colyseus room that relays playertwo messages:
import { Room, Client } from '@colyseus/core';
export class playertwoGameRoom extends Room {
private hostId: string | null = null;
onCreate(options: any) {
console.log('playertwoGameRoom created');
// Handle playertwo messages
this.onMessage('playertwo', (client, message) => {
// Relay to all clients or targeted client
if (message.targetId) {
const targetClient = Array.from(this.clients).find(
c => c.sessionId === message.targetId
);
targetClient?.send('playertwo', message);
} else {
// Broadcast to all except sender
this.broadcast('playertwo', message, { except: client });
}
});
}
onJoin(client: Client, options: any) {
console.log(client.sessionId, 'joined');
// Elect first player as host
if (!this.hostId) {
this.hostId = client.sessionId;
this.broadcast('playertwo', {
type: 'host_announce',
hostId: this.hostId,
senderId: 'server'
});
}
// Notify all clients of new player
this.broadcast('playertwo', {
type: 'player_join',
payload: { playerId: client.sessionId },
senderId: 'server'
});
// Send current peers list to new player
const peers = Array.from(this.clients).map(c => c.sessionId);
client.send('playertwo', {
type: 'peers_list',
payload: { peers },
senderId: 'server'
});
}
onLeave(client: Client, consented: boolean) {
console.log(client.sessionId, 'left');
// Notify remaining clients
this.broadcast('playertwo', {
type: 'player_leave',
payload: { playerId: client.sessionId },
senderId: 'server'
});
// Elect new host if needed
if (client.sessionId === this.hostId && this.clients.length > 0) {
this.hostId = Array.from(this.clients)[0].sessionId;
this.broadcast('playertwo', {
type: 'host_announce',
hostId: this.hostId,
senderId: 'server'
});
}
}
}API Reference
new ColyseusTransport(room: Room)
Create a playertwo transport from a Colyseus room.
Parameters:
room: A connected Colyseus room instance
Example:
const room = await client.joinOrCreate('game_room');
const transport = new ColyseusTransport(room);Transport Methods
All standard playertwo Transport methods are supported:
send(message: WireMessage, targetId?: string): void
Send a message through the Colyseus room.
transport.send({
type: 'action',
payload: { action: 'move', x: 10, y: 20 }
});
// Send to specific player
transport.send({
type: 'state_sync',
payload: { state: {...} }
}, 'player-123');onMessage(handler: (message, senderId) => void): () => void
Listen for messages from other players.
const unsubscribe = transport.onMessage((message, senderId) => {
console.log('Received message from:', senderId, message);
});
// Stop listening
unsubscribe();onPeerJoin(handler: (peerId: string) => void): () => void
Listen for players joining.
transport.onPeerJoin((peerId) => {
console.log('Player joined:', peerId);
});onPeerLeave(handler: (peerId: string) => void): () => void
Listen for players leaving.
transport.onPeerLeave((peerId) => {
console.log('Player left:', peerId);
});getPlayerId(): string
Get your player ID (same as room.sessionId).
const myId = transport.getPlayerId();getPeerIds(): string[]
Get list of all connected players (excluding yourself).
const peers = transport.getPeerIds();
console.log('Connected players:', peers);isHost(): boolean
Check if you are the current host.
if (transport.isHost()) {
console.log('I am the host');
}Additional Methods
onError(handler: (error: Error) => void): () => void
Listen for connection errors.
transport.onError((error) => {
console.error('Transport error:', error);
});disconnect(): void
Leave the Colyseus room and clean up.
transport.disconnect();getRoom(): Room
Get the underlying Colyseus room (for advanced use cases).
const room = transport.getRoom();
console.log('Room ID:', room.id);
console.log('Room state:', room.state);Message Protocol
The transport uses a 'playertwo' message type on the Colyseus room. All playertwo messages are wrapped in this format:
{
type: 'action' | 'state_sync' | 'player_join' | 'player_leave' | 'host_announce' | ...,
payload?: any,
senderId: string,
targetId?: string // For targeted messages
}Control Messages
The server should send these control messages:
player_join
{
type: 'player_join',
payload: { playerId: string },
senderId: 'server'
}player_leave
{
type: 'player_leave',
payload: { playerId: string },
senderId: 'server'
}host_announce
{
type: 'host_announce',
hostId: string,
senderId: 'server'
}peers_list
{
type: 'peers_list',
payload: { peers: string[] },
senderId: 'server'
}Complete Example
Use the client/server snippets above as a starting point. For end-to-end demos, see the examples overview and adapt the patterns to your Colyseus rooms.
Comparison to Other Approaches
Pure Colyseus
// ❌ Imperative, verbose
room.onMessage('move', (message) => {
const player = players.get(message.playerId);
player.x = message.x;
player.y = message.y;
// Sync to other clients...
// Handle edge cases...
// Validate input...
});playertwo + Colyseus
// ✅ Declarative, concise
const game = defineGame({
actions: {
move: (state, playerId, input) => {
state.players[playerId] = input.position;
}
}
});Best Practices
Use Colyseus for Infrastructure
- Room management
- Matchmaking
- Persistence (via Colyseus state)
- Authentication
Use playertwo for Game Logic
- Player actions
- Game state updates
- Collision detection
- Game rules
Host Election
- Let Colyseus determine the host
- Send
host_announcemessages - playertwo will handle host-authoritative logic
Error Handling
- Listen to both Colyseus and playertwo errors
- Handle reconnection via Colyseus
- playertwo will auto-resume game state
Troubleshooting
"Messages not being received"
- Ensure your Colyseus server relays
'playertwo'messages - Check that
onMessage('playertwo', ...)is set up on the server - Verify the message format includes
senderId
"Host is always false"
- Server must send
host_announcemessages - Check that
hostIdmatches a client'ssessionId
"Peers list is empty"
- Server should send
player_join/player_leavemessages - Or send a
peers_listmessage on join
"TypeScript errors"
- Ensure
colyseus.jsis installed - Check that types are imported correctly:
import type { Room } from 'colyseus.js'
See Also
License
MIT
