Colyseus for Flutter

The Colyseus multiplayer client, as dart:ffi bindings over the native C SDK. Prebuilt native libraries ship with the package, so there is no toolchain to install.

Supports macOS, iOS, Android, Linux and Windows. Web is not supported: that target needs the SDK's Emscripten build and a different transport.

Install

flutter pub add colyseus

macOS and iOS apps need the outbound-network entitlement (com.apple.security.network.client) in both DebugProfile.entitlements and Release.entitlements. Without it every connection fails silently inside the sandbox, and flutter create does not add it.

Use

import 'package:colyseus/colyseus.dart';

final client = ColyseusClient('ws://localhost:2567');
final room = await client.joinOrCreate('my_room');

room.onStateChange.listen((_) {
  final players = room.state!.getMap('players')!;
  for (final entry in players.entries) { ... }
});

room.onMessage('chat').listen((data) => print(data['text']));
room.send('move', {'x': 10, 'y': 20});

Prediction

Waiting for the server to confirm your own movement costs a round trip. The predict layer applies each input immediately and reconciles when the server disagrees:

Colyseus.autoPoll = false;              // the app drives the frame

final predict = Predict.of(room);
final input = room.input()!;

// Other players: smoothed, since their inputs aren't yours to predict.
predict.attachAll('players',
    config: {'x': PredictMode.damped, 'y': PredictMode.damped},
    exceptKey: room.sessionId);

// Yours: predicted and reconciled.
final me = room.state!.getMap('players')![room.sessionId] as SchemaInstance;
final recon = predict.reconciler(me,
  input: input,
  fields: const ['x', 'y', 'vx', 'vy'],
  step: (ctx, state, cmd) => stepPlayer(state, cmd, ctx.dt),  // shared with the server
);

void onFrame() {
  Colyseus.pump();                      // decode inbound, deliver events
  final steps = predict.tick(clock.now);
  for (var i = 0; i < steps; i++) {
    input.data['moveX'] = keyboard.x;
    input.send();                       // predicted immediately
  }
  draw(recon.value('x'), recon.value('y'));
}

step has to compute exactly what the server computes. When it does, recon.drift.ema stays at the floating-point noise floor; when it drifts, that number tells you.

HTTP and auth

final res = await client.http.get('/test');
print(res.json['things']);
await client.http.post('/save', body: {'name': 'endel'});

final data = await client.auth.signInAnonymously();
print(data.user?['anonymousId']);

// The token is shared with client.http, so later requests are authenticated.
client.auth.onChange.listen((d) {
  if (d.token == null) showLoginScreen();
});

Both run on a worker thread and answer through a NativeCallable.listener, so neither call stalls the frame loop. A non-2xx reply throws ColyseusHttpException; a rejected auth call throws ColyseusAuthException.

The token is persisted through the platform's secure storage under one process-wide key, so it survives a restart and leaks between test runs. A suite that signs in should sign out again, or later clients will send a token the next server rejects.

Libraries

colyseus
Colyseus client SDK for Flutter, over the native C SDK via dart:ffi.