-
Notifications
You must be signed in to change notification settings - Fork 0
Development
This guide covers the full developer workflow: environment setup, project architecture, Tauri v2 internals, the Phaser 4 scene system, and step-by-step instructions for adding a new game.
| Tool | Version | Purpose |
|---|---|---|
| Node.js | 20 LTS | JavaScript runtime & npm |
| Rust | 1.77.2+ | Tauri native shell compilation |
| Cargo | Bundled with Rust | Rust package manager |
| Vite | 8.0.14 (devDep) | Frontend bundler / HMR dev server |
| TypeScript | ~6.0.3 | Type-safe game logic |
| Tauri CLI | 2.11.2 (devDep) | Native app packaging |
Install Rust via rustup: https://rustup.rs
For Android cross-compilation:
rustup target add aarch64-linux-android
# Also requires Android NDK via Android Studioretro-game-replicas/
├── index.html # Vite entry point (mounts #app div)
├── package.json # npm scripts, deps (Phaser 4, Tauri API)
├── tsconfig.json # TypeScript config (strict mode)
├── vite.config.ts # Vite build config (if present)
│
├── src/
│ ├── main.ts # Phaser game config + scene registry
│ ├── style.css # Global styles (body, canvas centering)
│ ├── counter.ts # (Legacy utility, not used in games)
│ └── scenes/
│ ├── LobbyScene.ts # Main menu, game selector, CRT toggle
│ ├── SnakeScene.ts
│ ├── PongScene.ts
│ ├── AsteroidsScene.ts
│ ├── BreakoutScene.ts
│ ├── FroggerScene.ts
│ ├── InvadersScene.ts
│ ├── TetrisScene.ts
│ ├── MinesweeperScene.ts
│ ├── RunnerScene.ts
│ ├── BirdScene.ts
│ └── CyberScene.ts
│
├── src-tauri/
│ ├── tauri.conf.json # App metadata, window size, bundle targets
│ ├── Cargo.toml # Rust dependencies (tauri 2.11.1)
│ ├── build.rs # Tauri build script
│ ├── src/
│ │ └── lib.rs # Rust app entry (tauri::Builder)
│ ├── capabilities/ # Tauri v2 permission system
│ └── icons/ # App icons (32x32, 128x128, .icns, .ico)
│
├── public/ # Static assets (served as-is by Vite)
├── dist/ # Vite build output (consumed by Tauri)
└── docs/
└── images/ # Screenshots used in README
Tauri v2 replaces the traditional Electron model (bundled Chromium) with the system WebView:
- Windows: Microsoft Edge WebView2 (Chromium-based, always updated via Windows Update)
- macOS: WKWebView (WebKit, part of the OS)
- Android: Android WebView (Chromium-based)
This makes the binary dramatically smaller (~15 MB vs. Electron's ~100+ MB) at the cost of minor cross-platform rendering differences (mitigated because Phaser uses a WebGL canvas rather than DOM rendering).
The Rust backend (src-tauri/src/lib.rs) in this project is minimal — it simply boots the Tauri window and loads the Vite-built frontend. No custom Tauri commands are registered.
Key config (src-tauri/tauri.conf.json):
{
"productName": "Retro Arcade Launcher",
"version": "1.0.2",
"identifier": "com.biossystem.retroarcade",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [{
"title": "Retro Arcade Launcher",
"width": 800,
"height": 600,
"resizable": true
}]
}
}Phaser 4 is the game engine. It provides:
-
WebGL renderer (with Canvas fallback via
Phaser.AUTO) - Scene system for game-state management
- Arcade Physics (AABB + circle) for collision detection
- Input plugins for keyboard, mouse, touch, and gamepad
- GameObjects (Graphics, Text, Container, Particles, Rectangle)
- PostFX camera pipeline for the CRT shader
The game is configured in src/main.ts as a single Phaser.Game instance at 640×480 logical pixels, scaled to fit the window with Phaser.Scale.FIT:
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO, // WebGL preferred, Canvas fallback
width: 640,
height: 480,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
backgroundColor: '#0a0a0a',
pixelArt: true, // Disables anti-aliasing for crisp pixels
physics: {
default: 'arcade',
arcade: { debug: false }
},
scene: [LobbyScene, SnakeScene, PongScene, ...]
};Each game is a Phaser Scene subclass with three key lifecycle methods:
class MyGameScene extends Phaser.Scene {
constructor() { super('MyGameScene'); }
// Called once when scene starts. Receives data from scene.start(key, data).
create(data: { difficulty: string }) { ... }
// Called every frame (~60fps). delta = ms since last frame.
update(time: number, delta: number) { ... }
}Scenes transition via:
-
this.scene.start('TargetScene', { difficulty: 'NORMAL' })— stops current, starts target -
this.scene.restart({ difficulty: this.difficulty })— restarts current scene with data
All games receive a difficulty string ('EASY' | 'NORMAL' | 'HARD' | 'EXPERT') via the scene data object. Each scene is responsible for tuning its own parameters:
create(data: any) {
this.difficulty = data?.difficulty || 'NORMAL';
switch (this.difficulty) {
case 'EASY': this.speed = 80; break;
case 'NORMAL': this.speed = 120; break;
case 'HARD': this.speed = 160; break;
case 'EXPERT': this.speed = 210; break;
}
}Scores are stored in localStorage with the pattern:
arcade_score_<SceneName>_<DIFFICULTY>
Example keys:
-
arcade_score_SnakeScene_NORMAL→"240" -
arcade_score_TetrisScene_EXPERT→"5600"
To save a high score:
const key = `arcade_score_${this.scene.key}_${this.difficulty}`;
const prev = parseInt(localStorage.getItem(key) || '0');
if (this.score > prev) {
localStorage.setItem(key, String(this.score));
}The lobby displays the EASY-difficulty high score for quick reference beside each game entry.
# 1. Install dependencies
npm install
# 2. Start the Tauri dev environment
# - Starts Vite HMR server on localhost:5173
# - Opens native Tauri window pointing at it
npm run tauri dev
# 3. TypeScript type checking (without building)
npx tsc --noEmit
# 4. Production build
npm run tauri buildVite's HMR (Hot Module Replacement) is active during tauri dev — editing any .ts or .css file in src/ will hot-reload the frontend without restarting the Rust process.
Follow these steps to add a new game called "Neon Chess" (scene key: ChessScene) as an example.
# Create the file
touch src/scenes/ChessScene.ts// src/scenes/ChessScene.ts
import Phaser from 'phaser';
export default class ChessScene extends Phaser.Scene {
private difficulty = 'NORMAL';
constructor() {
super('ChessScene'); // Must be unique across all scenes
}
create(data: any) {
this.difficulty = data?.difficulty || 'NORMAL';
// Add your game UI, objects, physics, input...
this.add.text(320, 240, 'NEON CHESS', {
fontFamily: 'Courier',
fontSize: '32px',
color: '#00ffcc'
}).setOrigin(0.5);
// Always provide an ESC back to lobby
this.input.keyboard?.on('keydown-ESC', () => {
this.scene.start('LobbyScene');
});
}
update(_time: number, delta: number) {
// Game loop...
}
}// src/main.ts
import ChessScene from './scenes/ChessScene'; // Add import
const config: Phaser.Types.Core.GameConfig = {
// ...
scene: [
LobbyScene,
SnakeScene,
// ... existing scenes ...
ChessScene // ← Add here
]
};In src/scenes/LobbyScene.ts, add an entry to the games array:
private games = [
// ... existing games ...
{ name: 'NEON CHESS', scene: 'ChessScene', icon: '♟️' },
];The lobby will automatically display it in the list, read its high score from localStorage, and pass the selected difficulty to ChessScene.create().
npm run tauri devNavigate to your new game in the lobby, select a difficulty, and verify it launches correctly. Press Esc to confirm the lobby return works.
-
TypeScript strict mode is enabled — avoid
anywhere possible. - Use
privateaccess modifiers for all scene properties. - Initialize all Phaser GameObjects in
create(), not the constructor. - Always clean up keyboard listeners in
create()before re-adding:this.input.keyboard?.removeAllListeners();
- Use
Phaser.Math.Wrap()for wrapping indices (e.g., menu navigation). - Prefer
this.scene.start()overthis.scene.switch()for clean state resets. - Store difficulty in
this.difficultyand pass it toscene.restart()on game over.
The repository includes a .github/ directory with workflow files for automated builds. On every push to main, the CI:
- Installs Node.js dependencies (
npm ci) - Builds the Vite frontend (
npm run build) - Runs
tauri buildfor Windows (x64), macOS (arm64, x64), and Android - Uploads artifacts to the GitHub Release
Back to Home