Skip to content
mdr
All work
prototype2024·personal

Aether Drift

Side-scrolling dream platformer with Firebase-backed community levels.

Screenshot of Aether Drift

TL;DR

  • Next.js shell built to host Phaser 3 gameplay.
  • Firebase for accounts, leaderboards, UGC levels.
  • Typed event bus between game scenes and React UI.
  • Cross-platform (web + mobile) via responsive canvas.
  • Open-source under GPL v3.

Architecture

Flow
CLIENTNEXT.JS SHELLGAME RUNTIMEREMOTEPlayerweb · mobileNext.js shellauth · routingEvent bustypedPhaser scenesgameplayPhaser 3Firebaseauth · realtimeFirestoreCommunity levelsUGCLeaderboardrealtime

Game logic stays in Phaser. Account / UGC state lives in Firebase. The shell stitches them.

sync · HTTP / RPCasync · eventauth · permissiondata · read / write

Screens

  • Main menu

    Concept render. The actual game is in scaffolding phase — Phaser hosts the gameplay, Next.js wraps it with menus, settings, and a Firestore-backed community-levels browser.

  • Gameplay (concept)

    Side-scrolling platformer with a dream/nightmare metaphor. Collectible "dream fragments" trigger power-ups. The HUD lives in DOM, the world lives in Phaser.

  • Leaderboard

    Real-time leaderboard backed by Firestore in the full build. The static-export embed uses a mocked Firebase stub returning canned data — so you can see the shape without the backend.

Problem

A side-scrolling platformer with a dream/nightmare metaphor needs the visual layer of a game engine and the data layer of a web app — leaderboards, accounts, user-generated levels. Bolt those together badly and you've shipped two half-products.

Approach

Phaser 3 for gameplay (proper sprite batching, physics, scenes). Next.js as the shell — auth, leaderboards, level browser, prerendered SSR previews. Firebase for realtime state and user-generated content. The bridge between scenes and the outside world is a small typed event bus so React never reaches into Phaser internals.

Deep dive

The Phaser ↔ React bridge

Direct DOM access from a game scene is a fast way to a tangled codebase. The bridge here is a typed event bus: scenes emit, React subscribes, the reverse goes through a slim 'scene controller' interface. Nothing reaches into Phaser internals from the UI.

tslib/game/eventBus.ts — typed scene-to-UI communication
type GameEvents = {
  "score:changed": { value: number };
  "level:completed": { id: string; time: number };
  "dream:fragment": { x: number; y: number };
};

class EventBus<E extends Record<string, unknown>> {
  private listeners = new Map<keyof E, Set<(payload: any) => void>>();

  on<K extends keyof E>(event: K, fn: (payload: E[K]) => void) {
    const set = this.listeners.get(event) ?? new Set();
    set.add(fn);
    this.listeners.set(event, set);
    return () => set.delete(fn);
  }

  emit<K extends keyof E>(event: K, payload: E[K]) {
    this.listeners.get(event)?.forEach((fn) => fn(payload));
  }
}

export const gameBus = new EventBus<GameEvents>();

GPL v3, on purpose

Game source under GPL means anyone forking it has to keep it open. For a community-content game where players can submit levels, that felt aligned: the work and the contributions live in the same license.

Outcome

Shipped as far as the shell: the Next.js app, the typed event bus and the Firebase schema for accounts, leaderboards and community levels all exist. The Phaser scenes behind them were never filled in, so there is no gameplay to play yet — what's below are concept renders. Released as GPL v3.

Related

More in games.