Workshop monorepo for building a last-player-standing multiplayer game. The game runs entirely on Flutter and serverless primitives: the Flame engine drives the 2D canvas, and Supabase Realtime powers the netcode with Broadcast events and Presence. There is no game server.
The database layer runs on the typesafe Supabase v3 groundwork, merged into supabase-flutter and published to pub.dev:
- #1634 typed table
access with
PostgrestTableandPostgrestColumn, insupabase_flutter3.0.0-dev.9 - #1635 the
supabase_typegencode generator, released assupabase_typegen0.5.1
Everything resolves from pub.dev. There are no git dependencies and no
dependency_overrides anywhere in the workspace.
| Package | Description |
|---|---|
packages/skeleton |
The starting point for the exercises: dependencies wired up, no game |
packages/game |
The finished multiplayer game (Flutter web and macOS) |
packages/slides |
The workshop slide deck, built with flutter_deck |
- Flutter 3.47 or newer
- Supabase CLI 2.x and Docker
- Melos 8.x:
dart pub global activate melos
dart pub get
supabase startsupabase start boots the local stack (API on port 54621) and applies the
scores table migration. The game defaults to the local URL and the standard
local publishable key, so no configuration is needed for local play.
packages/skeleton is where attendees write their code. It carries the
dependencies, the dart-define configuration, the tuning constants, and the web
scaffolding, and nothing else. The game itself is what the exercises build:
melos run skeletonSee packages/skeleton/README.md for the details.
melos run gameOr manually, with explicit configuration:
cd packages/game
flutter run -d chrome \
--dart-define=SUPABASE_URL=http://127.0.0.1:54621 \
--dart-define=SUPABASE_KEY=sb_publishable_... \
--dart-define=ROOM=mainOpen two instances (run the command twice) to play against yourself. Players
sharing the same ROOM value meet in the same arena; split large groups into
separate rooms to stay under the Realtime message limits.
melos run slidesNavigate with the arrow keys. Press the period key for the navigation drawer. The presenter view with speaker notes opens from the deck controls.
The integration smoke test exercises Broadcast, Presence, and the typed
scores table against the local stack:
supabase start
cd packages/game
flutter testpackages/game/lib/src/db/supabase_schema.g.dart is generated by
supabase_typegen from supabase/schema.json, a snapshot of the postgres-meta
introspection document. After changing a migration, update the snapshot to
match and regenerate:
cd packages/game
dart run supabase_typegen \
--output lib/src/db/supabase_schema.g.dart \
--import package:supabase_flutter/supabase_flutter.dart \
< ../../supabase/schema.jsonThe types can also be generated straight from a database, without the
snapshot. A recent Supabase CLI (2.120 has it) supports Dart directly. Use
--local for the running local stack, or --project-id your-project-ref for a
hosted project after supabase login:
cd packages/game
supabase gen types --lang dart --local > lib/src/db/supabase_schema.g.dartThe CLI runs the supabase_typegen dev dependency of the package it is called
from. On an older CLI, call the generator yourself:
cd packages/game
dart run supabase_typegen --local --output lib/src/db/supabase_schema.g.dartsupabase link --project-ref your-project-ref
supabase db push
cd packages/game
flutter build web \
--dart-define=SUPABASE_URL=https://your-project-ref.supabase.co \
--dart-define=SUPABASE_KEY=sb_publishable_...Enable anonymous sign-ins for the hosted project under Authentication
settings, then serve build/web from any static host.
- One Realtime channel per room carries five broadcast events:
state,shoot,hit,death, androundStart. - The netcode is peer-authoritative: every client simulates its own player and bullets, and the victim of a hit applies its own damage before broadcasting the result. Each player has exactly one authority, so there are no conflicts.
- A round is defined by
{seed, startedAt}: every client generates an identical world from the seed and derives the round clock from the start time. The world costs zero bandwidth. - Player state is throttled to 20 packets per second and remote players smooth over the gaps with dead reckoning.
- Presence powers the lobby roster, disconnect handling, and match discovery: players in a match advertise the seed so late joiners can spectate.
- The winner records the round through the typed
scorestable, and the lobby leaderboard is a typed Postgres Changes stream.