Transports Overview

Transports are the networking layer that enables multiplayer functionality in martini-kit. They handle peer-to-peer or client-server communication, allowing the host to broadcast state updates and clients to send actions.

What is a Transport?

A transport is any implementation of the Transport interface that can:

  • Send messages to specific peers or broadcast to all
  • Receive messages from other peers
  • Notify when peers join or leave
  • Track connection state and metrics

martini-kit is transport-agnostic - the core multiplayer engine works with any transport implementation, whether it’s in-memory, P2P WebRTC, WebSocket, or a custom solution.

Available Transports

TransportLatencyUse CaseSetupProduction Ready
LocalTransport0msSame-page testing, unit testsEasy❌ Dev only
IframeBridgeTransport~1msIDE sandboxes, iframe isolationMedium❌ Dev only
TrysteroTransport20-100msP2P games, no server neededMedium✅ Yes
Custom (WebSocket)10-50msServer-based games, scalabilityHard✅ Yes

Choosing a Transport

For Development & Testing

Use LocalTransport when:

  • Building demo pages with side-by-side instances
  • Writing unit or integration tests
  • Rapid local development without network overhead
  • Testing game logic in isolation

Use IframeBridgeTransport when:

  • Building sandboxed IDEs (like martini-kit IDE)
  • Testing across multiple iframe contexts
  • Isolating peer instances for security

For Production Games

Use TrysteroTransport (P2P) when:

  • Building small multiplayer games (2-8 players)
  • No server infrastructure budget
  • Players can tolerate 50-100ms peer-to-peer latency
  • NAT traversal is acceptable (works for ~80% of connections)

Build a Custom Transport (WebSocket/WebRTC server) when:

  • Building larger multiplayer games (8+ players)
  • Need reliable, low-latency connections
  • Have server infrastructure
  • Want full control over networking topology

The Transport Interface

All transports implement this interface:

export interface Transport {
  // Message handling
  send(message: WireMessage, targetId?: string): void;
  onMessage(handler: (message: WireMessage, senderId: string) => void): () => void;

  // Peer management
  onPeerJoin(handler: (peerId: string) => void): () => void;
  onPeerLeave(handler: (peerId: string) => void): () => void;
  getPeerIds(): string[];

  // Identity
  getPlayerId(): string;
  isHost(): boolean;

  // Optional metrics for debugging/monitoring
  metrics?: TransportMetrics;
}

Key Methods

send(message, targetId?)

Send a message to peer(s):

  • Broadcast: Omit targetId to send to all peers
  • Unicast: Provide targetId to send to specific peer
// Broadcast to all peers
transport.send({ type: 'state_sync', payload: stateDiff });

// Send to specific peer
transport.send({ type: 'action', payload: action }, 'player-2');

onMessage(handler)

Listen for incoming messages from other peers:

const unsubscribe = transport.onMessage((message, senderId) => {
  console.log('Received', message.type, 'from', senderId);
});

// Clean up when done
unsubscribe();

onPeerJoin(handler) / onPeerLeave(handler)

Listen for peers connecting or disconnecting:

transport.onPeerJoin((peerId) => {
  console.log('Player joined:', peerId);
  runtime.addPlayer(peerId);
});

transport.onPeerLeave((peerId) => {
  console.log('Player left:', peerId);
  runtime.removePlayer(peerId);
});

getPeerIds() / getPlayerId() / isHost()

Get identity information:

const myId = transport.getPlayerId();        // "player-abc123"
const peers = transport.getPeerIds();        // ["player-def456", "player-ghi789"]
const amHost = transport.isHost();           // true or false

Transport Metrics

Transports can optionally implement TransportMetrics for debugging and monitoring:

export interface TransportMetrics {
  getConnectionState(): 'disconnected' | 'connecting' | 'connected';
  onConnectionChange(callback: (state: ConnectionState) => void): () => void;
  getPeerCount(): number;
  getMessageStats(): { sent: number; received: number; errors: number };
  getLatencyMs?(): number | undefined;  // Optional
  resetStats?(): void;                    // Optional
}

Using Metrics

const transport = new LocalTransport({ roomId: 'my-game', isHost: true });

if (transport.metrics) {
  // Check connection state
  console.log('State:', transport.metrics.getConnectionState()); // "connected"

  // Listen for state changes
  transport.metrics.onConnectionChange((state) => {
    console.log('Connection state changed:', state);
  });

  // Get message statistics
  const stats = transport.metrics.getMessageStats();
  console.log(`Sent: ${stats.sent}, Received: ${stats.received}, Errors: ${stats.errors}`);

  // Get peer count
  console.log('Connected peers:', transport.metrics.getPeerCount());
}

Message Flow Architecture

Host-Authoritative Pattern

┌─────────────────────────────────────────────────┐
│                    Host                          │
│  ┌─────────────────────────────────────────┐   │
│  │  1. Receives actions from clients        │   │
│  │  2. Applies actions to authoritative state│  │
│  │  3. Generates state diffs (patches)      │   │
│  │  4. Broadcasts patches to all clients    │   │
│  └─────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘
          ▲                        │
          │ actions                │ state patches
          │                        ▼
┌─────────────────┐      ┌─────────────────┐
│    Client 1     │      │    Client 2     │
│  1. Send action │      │  1. Send action │
│  2. Recv patches│      │  2. Recv patches│
│  3. Apply patches│     │  3. Apply patches│
│  4. Re-render   │      │  4. Re-render   │
└─────────────────┘      └─────────────────┘

Message Types

The WireMessage type supports these message types:

export interface WireMessage {
  type: 'state_sync' | 'action' | 'player_join' | 'player_leave' | 'event' | 'heartbeat';
  payload?: any;
  senderId?: string;
  timestamp?: number;
  [key: string]: any;
}
  • state_sync: Host broadcasts state diffs to clients
  • action: Any peer sends an action to the host
  • player_join: Notify peers when a player joins
  • player_leave: Notify peers when a player leaves
  • event: Custom events emitted via context.emit()
  • heartbeat: Keep-alive messages for connection monitoring

Comparison with Other Frameworks

vs Colyseus (Server-based only)

  • Colyseus: Requires server infrastructure, client-server only
  • martini-kit: Transport-agnostic - use P2P or server-based

vs Photon (Hosted service)

  • Photon: Proprietary hosted service, costs money at scale
  • martini-kit: Open-source, self-hosted, free

vs Netcode for GameObjects (Unity)

  • Netcode: Unity-specific, C# only
  • martini-kit: Engine-agnostic, works with any renderer (Phaser, Three.js, etc.)

Performance Considerations

Bandwidth Usage

martini-kit uses a diff/patch algorithm to minimize bandwidth:

// Instead of sending full state every frame (wasteful):
{ players: { p1: { x: 150, y: 200, health: 80 } } }  // ~50 bytes

// martini-kit only sends what changed:
[{ op: 'replace', path: ['players', 'p1', 'x'], value: 150 }]  // ~20 bytes

Typical bandwidth usage:

  • Small games (2 players): 1-3 KB/s per client
  • Medium games (4-8 players): 5-15 KB/s per client
  • Large games (16+ players): 20-50 KB/s per client

Sync Rate

Default sync rate is 50ms (20 FPS). You can adjust this:

const runtime = new GameRuntime(game, transport, {
  isHost: true,
  playerIds: ['p1', 'p2'],
  syncInterval: 100  // Slower sync (10 FPS) - lower bandwidth
});

Recommendations:

  • Fast-paced games (shooters, racing): 16-33ms (30-60 FPS)
  • Medium-paced games (platformers): 33-50ms (20-30 FPS)
  • Slow-paced games (turn-based, strategy): 100-200ms (5-10 FPS)

Debugging Transports

Log Messages

Use metrics to track message flow:

const stats = transport.metrics?.getMessageStats();
console.log('Messages sent:', stats?.sent);
console.log('Messages received:', stats?.received);
console.log('Errors:', stats?.errors);

Connection State

Monitor connection changes:

transport.metrics?.onConnectionChange((state) => {
  if (state === 'disconnected') {
    console.error('Lost connection!');
    // Show reconnection UI
  }
});

Peer Discovery

Track peer join/leave events:

transport.onPeerJoin((peerId) => {
  console.log('Peer joined:', peerId);
  console.log('Total peers:', transport.getPeerIds().length);
});

transport.onPeerLeave((peerId) => {
  console.log('Peer left:', peerId);
  console.log('Remaining peers:', transport.getPeerIds().length);
});

Next Steps

See Also