easy-game-maker
v0.2.4
Published
TypeScript-first cross-platform 2D and 3D game engine
Maintainers
Readme
Easy Game Maker (EGM)
TypeScript-first 2D and 3D game engine. Write once, publish everywhere.
npm install -g easy-game-maker
egm new my-game
cd my-game && npm install && egm simulatePlatforms supported
Only the desktop target can be built today. Every other target is planned: running its command prints a "not available yet" message and exits with code 1.
| Command | Output | Status |
|---|---|---|
| egm build desktop macos | .app + .dmg | Available |
| egm build desktop windows | .msi + .exe (Tauri) | Available |
| egm build desktop linux | .AppImage + .deb (Tauri) | Available |
| egm build web | Static bundle for Netlify, Vercel or any CDN | Coming soon |
| egm build ios | Xcode project for the App Store | Coming soon |
| egm build android | Gradle project for Google Play | Coming soon |
| egm build tizen | Tizen project, package as .wgt with Tizen Studio | Coming soon |
| egm build webos | .ipk for LG Smart TV | Coming soon |
| egm build androidtv | Gradle + Leanback project for Android TV | Coming soon |
| egm build tvos | Xcode project for Apple TV | Coming soon |
| egm build xbox | PWA project; .msix requires the Windows SDK | Coming soon |
| egm build playstation | PS5 project plus manual PlayStation SDK packaging | Coming soon |
Table of Contents
- Architecture
- Installation
- Project structure
- Configuration —
egm.config.ts - Engine API
- CLI reference
- E2E Testing
- Simulator
- Icons
- Build requirements
- Examples
Architecture
Your game (TypeScript)
└── App → SceneManager → Scene → DisplayObjects
└── Physics / Tweens / Audio / Network
│
─────────┴──────────────────────────────────────────
WebGLRenderer InputManager AudioManager
PhysicsWorld TransitionMgr TimerManager
NetworkManager AssetManager ShaderSystem
─────────────────────────────────────────────────────
│
┌─────────┴─────────────────────────────┐
│ │
egm simulate egm build <platform>
Simulator (localhost:5173) dist/<platform>/
X-Ray · Record E2E Web · iOS · Android
Device previews Desktop · TV · ConsoleThe engine is a pure TypeScript/WebGL2 library. Games run in any browser or WebView — the CLI wraps them in native shells per platform (Swift for iOS/macOS, Kotlin for Android/AndroidTV, Tauri for Windows/Linux).
Installation
Requirements: Node.js 18+
npm install -g easy-game-maker
# verify
egm --versionProject structure
my-game/
├── src/
│ ├── main.ts ← createApp() entry point
│ ├── scenes/
│ │ ├── MenuScene.ts
│ │ └── GameScene.ts
│ ├── game/ ← entities, logic
│ └── helpers/
│ └── loadTexture.ts
├── public/
│ └── assets/ ← images, audio, fonts
├── egm.config.ts ← project config
├── vite.config.ts
├── tsconfig.json
└── package.jsonConfiguration
egm.config.ts
import { defineConfig } from 'easy-game-maker';
export default defineConfig({
app: {
name: 'My Game',
version: '1.0.0',
bundleId: 'com.studio.mygame',
icon: 'public/assets/icon.png', // 512×512+ source image
},
display: {
width: 568,
height: 320,
orientation: 'landscape', // 'portrait' | 'landscape'
backgroundColor: '#1a1a2e',
scaling: 'fit', // see below
},
build: {
ios: { deploymentTarget: '16.0' },
android: { minSdkVersion: 26, targetSdkVersion: 35 },
desktop: { width: 1136, height: 640, resizable: false },
},
});scaling modes
| Value | Behaviour |
|---|---|
| fit | Scales to fit screen, preserves aspect ratio, letterbox bars (default) |
| fill | Scales to fill screen, preserves aspect ratio, crops edges |
| stretch | Stretches to fill screen (may distort) |
| none | No scaling |
Scaling is injected automatically into every build — no game code change needed.
Engine API
App
import { App } from 'easy-game-maker';
const app = new App({
width: 568,
height: 320,
backgroundColor: '#000',
physics: false, // enable engine-level physics world
});
app.init(); // creates canvas, initialises systems
app.scenes.add('menu', MenuScene);
void app.scenes.go('menu', { params: { app } });
app.run();Properties:
| Property | Type | Description |
|---|---|---|
| app.renderer | WebGLRenderer | WebGL2 renderer, sprite batcher |
| app.input | InputManager | Pointer & keyboard events |
| app.audio | AudioManager | Sound & music (Web Audio API) |
| app.physics | PhysicsWorld | planck.js physics |
| app.scenes | SceneManager | Scene stack / navigation |
| app.timers | TimerManager | Delayed / repeating callbacks |
| app.transitions | TransitionManager | Tween animations |
| app.assets | AssetManager | Batch asset preloading |
| app.network | NetworkManager | Multiplayer via native WebSocket |
Scenes
import { Scene, type SceneParams, type App } from 'easy-game-maker';
export class GameScene extends Scene {
private _app!: App;
override async onCreate(params?: SceneParams): Promise<void> {
this._app = params?.['app'] as App;
// load textures, build scene graph — await texture loads here
}
override onUpdate(dt: number): void {
// called every frame; dt = seconds since last frame (capped at 0.1)
}
override onResume(): void { /* scene becomes active */ }
override onPause(): void { /* scene goes to background */ }
override destroy(): void { /* cleanup when destroyed */ }
}
// Navigate between scenes
await app.scenes.go('game', {
transition: 'fade',
duration: 300,
params: { app, level: 2 },
});Display Objects
import {
Sprite, AnimatedSprite, RectShape, CircleShape,
LineShape, Text, Group,
} from 'easy-game-maker';
// Sprite
const s = new Sprite({ texture, x: 100, y: 200, width: 64, height: 64 });
s.anchorX = 0.5; // 0 = left edge, 0.5 = center, 1 = right edge
s.anchorY = 0.5;
s.rotation = Math.PI / 4; // radians
s.alpha = 0.8;
s.scaleX = 2;
scene.add(s);
// Animated sprite (frame array)
const anim = new AnimatedSprite({ frames: [tex0, tex1, tex2], fps: 12, x: 0, y: 0 });
anim.play();
// Shapes
const rect = new RectShape({ x: 0, y: 0, width: 100, height: 50, fill: '#f00' });
const circle = new CircleShape({ x: 200, y: 200, radius: 30, fill: '#0f0', stroke: '#fff', strokeWidth: 2 });
const line = new LineShape({ x1: 0, y1: 0, x2: 100, y2: 100, stroke: '#fff', strokeWidth: 3 });
// Text
const label = new Text({ text: 'Score: 0', x: 10, y: 10, fontSize: 24, color: '#fff' });
// Group (container with its own transform)
const group = new Group();
group.add(s);
group.add(label);
scene.add(group);Input
import type { PointerEvent2D, KeyEvent2D } from 'easy-game-maker';
// Pointer — fires in game coordinates (accounts for CSS scaling)
app.input.on<PointerEvent2D>('pointerdown', (e) => { console.log(e.x, e.y); });
app.input.on<PointerEvent2D>('pointermove', (e) => { ... });
app.input.on<PointerEvent2D>('pointerup', (e) => { ... });
// Keyboard
app.input.on<KeyEvent2D>('keydown', (e) => {
if (e.code === 'Space') jump();
});
app.input.on<KeyEvent2D>('keyup', (e) => { ... });
// Polling
if (app.input.isKeyDown('ArrowLeft')) moveLeft();
const { x, y, isDown } = app.input.pointer;Physics
Uses planck.js (Box2D port). Coordinates are in pixels; the engine converts to metres internally.
import { PhysicsWorld } from 'easy-game-maker';
// Per-scene physics world (recommended — gives full control)
const physics = new PhysicsWorld({ gravity: { x: 0, y: 9.8 } });
const body = physics.addBody(sprite, {
type: 'dynamic', // 'dynamic' | 'static' | 'kinematic'
shape: 'circle', // 'circle' | 'rect'
radius: 20, // for circle
// width, height // for rect
});
// Events: 'beginContact' and 'endContact' (payload: bodyA, bodyB, displayA, displayB)
physics.on('beginContact', ({ bodyA, bodyB }) => {
console.log('hit!', bodyA, bodyB);
});
// IMPORTANT — use fixed-step for stability across platforms:
private _acc = 0;
override onUpdate(dt: number): void {
this._acc += dt;
while (this._acc >= 1/60) {
physics.step(1/60);
this._acc -= 1/60;
}
}Animation
import { TransitionManager, Easing } from 'easy-game-maker';
const tween = new TransitionManager();
// Animate any numeric properties on any object
tween.to(sprite as unknown as Record<string, number>, {
x: 400,
alpha: 0,
duration: 1000, // milliseconds
easing: Easing.outQuad,
onComplete: () => scene.remove(sprite),
});
// Must be updated every frame
override onUpdate(dt: number): void {
tween.update(dt);
}Audio
// Load
await app.audio.loadBuffer('shoot', await fetch('assets/shoot.ogg').then(r => r.arrayBuffer()));
// Play
app.audio.play('shoot', { volume: 0.8 });
app.audio.play('music', { loop: true, volume: 0.5 });
app.audio.stop('music');Network - Multiplayer
Uses the browser's native WebSocket API. The engine supplies the client; you provide a server that implements the room protocol.
// Configure the server once, for example in MenuScene.
app.network.setServer('ws://localhost:2567');
// Join a room. The server must respond with room:joined or room:error.
const room = await app.network.joinRoom<RaceState>('race', {
playerName: 'Ada',
map: 'circuit-1',
});
// Receive JSON messages sent by the server.
room.onMessage<{ id: string; x: number; y: number }>('kart:update', (kart) => {
updateKartPosition(kart.id, kart.x, kart.y);
});
// Subscribe to typed server messages
room.onMessage<{ startTime: number }>('race:start', ({ startTime }) => {
beginRace(startTime);
});
// Send messages to server (20 Hz typical)
room.send('kart:update', { x, y, angle, speed });
// Leave
room.leave();NetworkManager methods:
| Method | Description |
|---|---|
| setServer(url) | Set the WebSocket server URL |
| joinRoom<T>(type, opts?) | Connect and join a server room |
| leaveAll() | Leave all joined rooms |
CLI Reference
egm new <name> # scaffold a new project
egm simulate # start Simulator (localhost:5173)
egm build <platform> [os] # build for target platform
egm test # run test suite (Vitest)
egm e2e # run E2E tests (visual)
egm e2e --headless # only generates __e2e.html; does NOT run tests, exits 1
egm e2e --headless --json # same, plus .egm-e2e-report.json with status "not-run"Build targets
Only desktop is available for now. The other targets are refused with a
"not available yet" message (exit code 1).
egm build desktop # builds for the current OS
egm build desktop --yes # answer yes to "build now?" / "open the app?" (--no-prompt: never ask)
egm build desktop macos # .app + .dmg
egm build desktop windows # .msi + .exe (requires Rust + Tauri)
egm build desktop linux # .AppImage + .debComing soon: web, ios, android, tizen, webos, androidtv, tvos, xbox and playstation.
E2E Testing
The current E2E runner generates a visual runner from files in src/e2e/*.e2e.ts. Its helpers are bundled with generated projects; they are not yet published as an importable package subpath.
// `easy-game-maker/e2e` is not an installable subpath: the runner replaces this import
// with its own `test` function. Relative imports between your .e2e.ts files are bundled.
import { test } from 'easy-game-maker/e2e';
test('Menu navigates to level', async ({ game }) => {
await game.wait(3000);
await game.screenshot('menu-ready');
await game.tap(284, 240); // tap Play button
await game.expect.scene('level'); // assert scene changed (retries 6s)
await game.screenshot('level-loaded');
});
test('Drag cuts the rope', async ({ game }) => {
await game.wait(1000);
await game.expect.scene('game');
await game.drag(120, 150, 230, 130, { duration: 300 });
await game.expect.state(
(win) => win.__EGM_APP__?.scenes?.current?._state !== 'playing',
'rope should be cut',
);
});egm e2e # opens visual runner in browsergame fixture API:
| Method | Description |
|---|---|
| game.wait(ms) | Wait N milliseconds |
| game.tap(x, y) | Pointer tap at game coordinates |
| game.drag(x1,y1, x2,y2, opts?) | Drag gesture |
| game.key(key, opts?) | Press and release a key |
| game.screenshot(name?) | Capture canvas screenshot |
| game.expect.scene(name) | Assert active scene (with retry) |
| game.expect.state(fn, msg?) | Assert custom game state (with retry) |
Simulator
egm simulateOpens at http://localhost:5173/__simulator.html with:
- Live reload on file change
- Device presets (iPhone, Android, tablet, desktop HD/FHD)
- X-Ray overlay — visualises display objects and physics bodies
- Record E2E — records interactions and generates
.e2e.tstest files - Keyboard shortcuts
1–9to switch device
Icons
Set app.icon in egm.config.ts pointing to a 512×512+ PNG. EGM auto-generates all required sizes per platform at build time.
| Platform | Sizes generated |
|---|---|
| Android | 48, 72, 96, 144, 192 px (all mipmap densities) |
| Android TV | 320×180 launcher banner |
| macOS | 32, 128, 256 px + .icns via iconutil |
| Tizen | 512×512 |
| webOS | 80×80, 130×130 |
| Xbox | 44, 50, 150, 310 (all UWP required sizes) |
Uses sips (macOS built-in), sharp, or convert (ImageMagick) — whichever is installed first.
Build Requirements
Web
No extra tools needed.
iOS
xcodes install 15.4 # or newer
sudo xcode-select --switch /Applications/Xcode-15.4.app/Contents/Developer
sudo xcodebuild -license acceptAndroid
brew install --cask android-studio
# Add to ~/.zshrc:
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$PATH:$ANDROID_HOME/platform-tools"macOS Desktop
xcode-select --installWindows / Linux Desktop (Tauri)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install tauri-cli --version "^2"
# Linux only:
sudo apt install libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-devSamsung Tizen
# Install Tizen Studio + TV Extension:
# https://developer.samsung.com/smarttv/develop/getting-started/setting-up-sdk/installing-tv-sdk.html
# Or use zip (built-in on macOS/Linux) — no signing, for testing onlyLG webOS
npm install -g @webos-tools/cliXbox
Register at partner.microsoft.com ($19 one-time). Upload the generated .msix.
PlayStation 5
Apply (free) at partners.playstation.com. Use the generated project.gp4 with Sony's orbis-pub-cmd.
Examples
Located in easy-game-maker-examples/:
| Project | Description | Key features |
|---|---|---|
| pong | Classic Pong | Physics, score |
| tetris | Tetris clone | Grid logic, level progression |
| fhz | Fruits Hate Zombies | Catapult physics, 5 levels, audio |
| cut-the-rope | Rope cutting puzzle | planck.js constraints, 3 levels |
| chess | Chess | Turn-based logic, validation |
| small-mission | Multiplayer arena | Native WebSocket rooms and real-time state |
Run any example
cd easy-game-maker-examples/fhz
npm install
egm simulateDevelopment
git clone <repo>
cd easy-game-maker
npm install
npm run build:engine # Vite → dist/engine/engine.js
npm run build:cli # tsup → dist/cli/index.cjs
npm run build # both
npm test # Vitest unit testsLicense
MIT
