Installation
Option 1: create-yage (recommended)
Section titled “Option 1: create-yage (recommended)”The quickest way to start a new YAGE game:
npm create yage@latest my-gamecd my-gamenpm run devThe scaffolder asks you to pick a template:
- Recommended — a playable platformer seed with physics, input, animations, sound effects, enemies, and collectibles. Great for learning the engine. Its production build is an installable app that runs offline (see below).
- Minimal — just
@yagejs/core+@yagejs/rendererand an empty scene. Start from scratch and add packages as you go (src/main.tshas commented-out examples showing how to wire each plugin).
Both templates include a Vite config, TypeScript setup, and an AGENTS.md
for AI-assisted development. Their page is set up for phones: the game fills
the visible screen and stays clear of the notch and home indicator (see
Mobile readiness). The
Recommended template also has a fullscreen button.
Installable and offline (Recommended template)
Section titled “Installable and offline (Recommended template)”The Recommended template builds your game as a Progressive Web App. Players
can install it to their home screen or desktop, and it keeps working without
a network connection after the first visit. The setup is the
vite-plugin-pwa entry in
vite.config.ts.
You do not list your assets anywhere. Each npm run build caches every file
in dist/ for offline play: your code, the physics WebAssembly module, and
everything under public/. When you add or change an asset, the next build
picks it up.
Before you ship:
- Change
name,short_name, anddescriptionin themanifestblock ofvite.config.ts. - Replace the icons in
public/:icon.svg(browser tab),pwa-192x192.pngandpwa-512x512.png(installed app), andapple-touch-icon.png(180×180, iOS home screen). Android crops the 512 icon to a circle or rounded square, so keep the artwork inside the centre 80%. - Serve the site over HTTPS. Browsers only install service workers on
secure origins;
localhostis the exception.
Things to know:
- The dev server has no service worker.
npm run devalways serves fresh files. Installing, offline play, and updates only work in the production build, whichnpm run previewserves locally. - Files over 10 MB fail the build. The error names each file. Raise
maximumFileSizeToCacheInBytesinvite.config.tsif a large music track needs to be available offline. - Players download the whole game on their first visit. Every file in the build is fetched up front, not only the first scene’s assets.
- Files from other servers are not cached. Assets loaded from a CDN or
another origin need a
runtimeCachingrule in theworkboxblock. - To remove it, delete the
VitePWA(...)entry and thevite-plugin-pwadependency. Players who installed the game keep the old service worker until you deploy one that unregisters itself; seeselfDestroyingin the plugin’s documentation.
Releasing a new version
Section titled “Releasing a new version”To release, run npm run build and upload dist/ to your host in place of
the previous build. There is no version number to bump. The build writes a
content hash of every file into dist/sw.js, so any changed file produces a
different sw.js.
Each player’s browser checks for a new version when the game opens. It
downloads sw.js from your server and compares it with the copy it has
installed. When the file differs:
- The game starts from the cache, on the version the player already has.
- The browser downloads the changed files in the background.
- The new version waits while any tab or window of the game is open. Reloading the page does not switch versions.
- Once every tab or window is closed, the next launch runs the new version.
A player therefore sees a release on the second launch after you deploy it. The old version keeps all of its files until the switch, so a game never runs with a mix of old and new files. A player who is offline keeps the cached version until they next open the game online.
Configure your host or CDN so it does not cache sw.js and index.html for
long. Browsers always fetch sw.js fresh, but a CDN that serves a stale copy
hides the release from every player. The other files in dist/ can use long
cache lifetimes.
The installed app’s name and icon update separately from your code. The browser refreshes them from the manifest on its own schedule, which can take a day or more on Android.
Offer an update in-game
Section titled “Offer an update in-game”To let players switch to a new version without closing the game, register
the service worker from your own code and show a prompt when a new version
is waiting. Add src/pwa.ts:
/// <reference types="vite-plugin-pwa/vanillajs" />import { registerSW } from "virtual:pwa-register";
let updateReady = false;
const updateSW = registerSW({ // A new version finished downloading and is waiting to take over. onNeedRefresh() { updateReady = true; }, // Without this, an open game only checks for a new version at launch. onRegisteredSW(_swUrl, registration) { if (!registration) return; setInterval(() => { // Fails while the player is offline; the next check tries again. registration.update().catch(() => {}); }, 60 * 60 * 1000); },});
/** True once a new version is downloaded and waiting. */export function isUpdateReady(): boolean { return updateReady;}
/** Switch to the waiting version. The page reloads. */export function applyUpdate(): Promise<void> { return updateSW();}Import it once from src/main.ts, for example with
import { isUpdateReady, applyUpdate } from "./pwa";. Importing
virtual:pwa-register replaces the plugin’s own registration script, so
nothing else changes in vite.config.ts.
Then decide where the game asks. applyUpdate() reloads the page, which
discards everything in memory, so call it where losing the current state is
acceptable: on the title screen, in a pause menu, or right after a save. A
game that saves between levels can apply the update without asking:
import { isUpdateReady, applyUpdate } from "./pwa";
engine.events.on("scene:replaced", () => { if (isUpdateReady()) void applyUpdate();});Things to know:
- Only players who have opened the game before get the reload. On a
player’s first visit, the service worker does not control the page yet.
If you deploy during that session,
applyUpdate()switches versions but does not reload, and the new version runs on the next launch.
Option 2: Manual install
Section titled “Option 2: Manual install”If you already have a project or prefer to set things up yourself, install packages individually:
npm install @yagejs/core @yagejs/rendererAdd more as needed:
npm install @yagejs/physics @yagejs/lighting @yagejs/input @yagejs/audio @yagejs/debugThe full list of packages:
| Package | Purpose |
|---|---|
@yagejs/core | ECS, math, events, scheduling |
@yagejs/renderer | PixiJS v8 rendering pipeline |
@yagejs/lighting | Radial lights and light-level queries |
@yagejs/physics | Rapier 2D physics integration |
@yagejs/input | Keyboard, mouse, and gamepad |
@yagejs/audio | Sound playback and mixing |
@yagejs/particles | Particle emitters and effects |
@yagejs/tilemap | Tile-based level support |
@yagejs/ui | In-game UI primitives |
@yagejs/ui-react | React bindings for game UI |
@yagejs/debug | Dev tools and inspectors |
@yagejs/save | Save / load state management |
@yagejs/core sits at the base of the dependency graph — every other package
depends on it, so it is always installed automatically.
Option 3: A local engine checkout
Section titled “Option 3: A local engine checkout”To run your game against an unreleased engine build — a change you are making, or one that is merged but not released — link the packages from a clone of the engine repo instead of installing them. Local Engine Checkout has the link set, the Pixi rule and the install flags.
Upgrading
Section titled “Upgrading”The @yagejs/* packages are released as a set. Every release publishes all of
them at the same version, and each one requires the others at that same minor.
Upgrade them in one step, naming every @yagejs/* package your package.json
lists — the three below are an example, not the full set:
npm install @yagejs/core@latest @yagejs/renderer@latest @yagejs/input@latestLeaving one out is the same as not upgrading it.
Moving one package to a new minor on its own can leave two versions of a shared
package installed. A game that upgrades @yagejs/save while @yagejs/renderer
stays behind ends up with two copies of @yagejs/core. Each copy has its own
service container and its own class identities, so an entity or service made
through one is not usable through the other — the same kind of failure as the
duplicate renderer above.
npm reports a version conflict for some of these mismatches and resolves others
into a second nested copy without reporting anything, so an install that
completes is not evidence the versions line up. When npm does report a
conflict, upgrade the whole set rather than reaching for --force or
--legacy-peer-deps, which install the combination the conflict was about.
Addons under @yagejs-addons/* version independently. Each declares the engine
minor it supports, so upgrade the addon in the same step as the engine.
Prerequisites
Section titled “Prerequisites”- Node.js 18 or later
- npm 9+ (or any compatible package manager — pnpm and yarn work fine)
- A bundler that supports TypeScript and ESM (Vite is recommended)
Vite Setup
Section titled “Vite Setup”If you used
create-yage, this is already configured. This section is for manual installs.
YAGE targets Vite 8 or later. If you use @yagejs/physics, its Rapier 2D
engine requires WebAssembly, so Vite needs the vite-plugin-wasm plugin to
handle .wasm imports:
npm install -D vite vite-plugin-wasmimport { defineConfig } from "vite";import wasm from "vite-plugin-wasm";
export default defineConfig({ plugins: [wasm()], oxc: { decorator: { legacy: true, }, }, build: { rollupOptions: { output: { keepNames: true, }, }, },});The oxc option is needed only when your game uses YAGE decorators such as
@trait. keepNames preserves readable class and function names in production
diagnostics. If you do not need them, use the shorter config:
import { defineConfig } from "vite";import wasm from "vite-plugin-wasm";
export default defineConfig({ plugins: [wasm()],});Why oxc.decorator.legacy: true
Section titled “Why oxc.decorator.legacy: true”Vite 8 replaced esbuild with oxc as its TypeScript transformer. oxc only
supports TypeScript’s legacy (stage-2) decorator transform. If your game
uses @trait, enable legacy: true so oxc transforms the decorator for the
browser.
Why output.keepNames: true
Section titled “Why output.keepNames: true”Vite 8 ships with the oxc minifier, which can shorten class and function
names. Set keepNames: true when you want production error reports and
Inspector output to retain the original names.
Verifying the Install
Section titled “Verifying the Install”Create a minimal entry file and run your dev server to confirm everything is working:
import { Engine } from "@yagejs/core";import { RendererPlugin } from "@yagejs/renderer";
const engine = new Engine();engine.use(new RendererPlugin({ width: 800, height: 600, backgroundColor: 0x1a1a2e }));await engine.start();If a blank game window appears with the configured background colour, YAGE is installed correctly.
Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/quick-start.md.