# Phaser 3 Engineering Standards Use this reference when creating, extending, or reviewing a Phaser 3 game. It defines a small, reliable default and the conditions under which that default should grow. ## Rule levels - **Required** rules apply to all new work. In an existing project, preserve a sound established convention when changing it would be unrelated or risky; record the exception. - **Conditional** guidance applies only when its stated condition is true. Do not add its machinery in anticipation of possible future needs. ## Contents - [Compatibility baseline](#compatibility-baseline) - [Grow-on-demand structure](#grow-on-demand-structure) - [Scenes and lifecycle](#scenes-and-lifecycle) - [Assets and loading](#assets-and-loading) - [Input actions](#input-actions) - [Physics choice and rules](#physics-choice-and-rules) - [Scale and responsive layout](#scale-and-responsive-layout) - [State ownership](#state-ownership) - [Time, delta, and determinism](#time-delta-and-determinism) - [Tuning and configuration](#tuning-and-configuration) - [Verification and completion](#verification-and-completion) - [Common failure patterns](#common-failure-patterns) - [Official references](#official-references) ## Compatibility baseline ### Required - For a new project, declare the exact dependency `"phaser": "3.90.0"` and commit the package-manager lockfile. Do not use `latest`, `^3.90.0`, `~3.90.0`, or an unversioned install command. `latest` now resolves to Phaser 4 and can silently change the API surface. - Start new projects with Vite, the `vanilla-ts` template, strict TypeScript, and no UI framework. Set `compilerOptions.strict` to `true`; fix type errors instead of broadly disabling checks or spreading `any`. - Use ES modules and keep one obvious browser entry point that creates exactly one `Phaser.Game` instance. - Use the Phaser 3.90 API documentation as the authority. Official examples may target an earlier Phaser 3 release, so verify copied APIs against 3.90 and reject Phaser 2 or 4 code. - Keep the dependency surface small. A library must solve a present, demonstrated need and must not duplicate a Phaser facility already sufficient for the game. ### Conditional - In an existing project, keep its working bundler, language, package manager, and framework unless migration is explicitly in scope. - Add React, Vue, Svelte, or another UI framework only when Phaser is embedded in an application whose non-game UI already needs it. Keep the Phaser game boundary explicit. - Upgrade Phaser only as a deliberate compatibility task with release-note review and a complete browser regression pass. ## Grow-on-demand structure ### Required Begin with the smallest structure that gives the current game clear ownership: ```text index.html src/ main.ts game/ config.ts scenes/ GameScene.ts public/ assets/ ``` - `main.ts` owns game startup only; `config.ts` owns `Phaser.Types.Core.GameConfig`; Scenes own presentation and orchestration. - Keep a one-scene prototype in one scene file until a real boundary appears. - Organize new files by cohesive gameplay system or responsibility, not by generic class type alone. - Delete abandoned experiments and unused assets once a direction is chosen. Do not leave two active implementations of the same rule. - Avoid speculative architecture: no service container, event bus, ECS, repository layer, global state library, or plugin framework without a present need. ### Conditional - Add `BootScene` when startup configuration or a tiny boot asset set must load before the main preload. - Add `PreloadScene` when load time is visible enough to need progress, error, or retry UI. - Add `MenuScene`, `ResultScene`, or overlay Scenes when they have distinct lifecycle, input, or rendering needs—not merely to shorten a file. - Extract `gameplay/`, `ui/`, `audio/`, `data/`, or `shared/` only after each has more than one cohesive consumer. - Introduce a pure domain module when rules such as scoring, progression, spawning, or combat can be expressed without Phaser objects; this is also the preferred unit-test boundary. ## Scenes and lifecycle Phaser starts a Scene through `init` → `preload` → `create`, then calls `update(time, delta)` while it is running. A stopped Scene instance can start again; its constructor is not a per-run reset hook. ### Required - Give every Scene one stable, unique key. Keep its constructor limited to `super(...)` and values that truly live for the entire Scene instance. - Reset every run-specific flag, counter, collection, and reference in `init` or `create`. A restart must behave like a fresh run. - Load required assets in `preload` before using them in `create`. Keep gameplay setup out of `preload`. - Create input handlers, colliders, timers, tweens, and subscriptions from one visible setup path. Prevent a second registration on restart. - Treat `shutdown` as the cleanup boundary. Remove anything not owned and cleaned by the Scene systems: `window` or `document` listeners, game/registry/external-emitter listeners, observers, intervals, network subscriptions, and retained callbacks. - Use `destroy` only for final Scene removal. Do not rely on it for ordinary stop, start, or restart cleanup. - Guard terminal transitions so collisions or consecutive update frames cannot start, stop, or restart a Scene twice. - Pass small typed data objects between Scenes. Never pass live Game Objects, physics bodies, cameras, or Scene plugins as shared state. - Make `this.scene.restart()` safe to run repeatedly. Smoke-test at least two complete restarts. - Guard global animation creation with `this.anims.exists(key)` or create global animations once in a dedicated startup path. ### Conditional - When using `sleep`/`wake`, handle their events explicitly: sleep does not run shutdown, and wake does not rerun `create`. - When using `pause`/`resume`, decide which timers, physics, input, audio, and overlay Scenes should pause; verify the decision rather than assuming all systems share one clock. - Run a separate UI Scene above gameplay only when UI must retain an independent lifecycle. Define which Scene owns shared data and transition authority. - Stream assets during play only when level size requires it; specify loading, cancellation, cache, and failure behavior first. ## Assets and loading ### Required - Give every texture, atlas, animation, audio clip, tilemap, and data file a stable semantic key. Centralize keys as typed constants; do not scatter raw string keys through gameplay code. - Use namespaced keys such as `player.body`, `player.run`, and `audio.hit` to avoid collisions and clarify asset type and ownership. - Keep filenames lowercase and portable; avoid spaces, case-only distinctions, and platform-specific path separators. - For Vite assets placed under `public/assets`, derive the loader base from `import.meta.env.BASE_URL`. Do not assume the game is always hosted at domain root `/`. - Surface load failures. A missing required asset must produce a clear error or retry state, not a silent blank canvas. - Load each shared asset once and reuse Phaser's global caches. Do not reload the same level-independent asset in every Scene. - Keep source art separate from runtime exports. Commit only optimized runtime formats and sizes the game actually uses. - Match spritesheet frame dimensions exactly. Verify atlas frame names, animation ranges, texture filtering, and origins in the browser. ### Conditional - Use a texture atlas when many small sprites are displayed together or a production art pipeline already emits one. - Enable `pixelArt` and disable smoothing only for intentional pixel art; render at integer-friendly scales and inspect movement for shimmer. - Preload alternate audio formats when the supported browser matrix requires them. Start or resume audio only after a user gesture and preserve the user's mute setting. - Add asset manifests, generated key types, compression, or lazy loading only when asset volume makes manual management error-prone or initial load materially slow. ## Input actions ### Required - Translate devices into semantic actions such as `moveLeft`, `jump`, `confirm`, `pause`, and `restart`. Gameplay rules consume actions, not scattered key codes or pointer events. - Distinguish held actions from edge-triggered actions. Poll `isDown` for continuous movement; use `Phaser.Input.Keyboard.JustDown`, `JustUp`, or explicit event edges for one-shot actions. - Keep input registration and teardown with the owning Scene. Clear latched actions on shutdown, pause, focus loss, and device disconnection. - Prevent browser defaults for gameplay keys such as arrows or Space when scrolling would interfere with play. - Convert pointer screen coordinates through the relevant camera before using them as world coordinates. - Ensure one physical gesture produces at most one gameplay command, even when keyboard, pointer, and UI handlers overlap. - Keep restart, pause, and debug controls separate from the core movement state so they cannot remain stuck. ### Conditional - When touch is supported, provide visible touch controls or direct gestures; test multi-touch, finger occlusion, cancellation, and both target orientations. - When gamepads are supported, handle connect/disconnect at runtime, dead zones, axis normalization, and a keyboard fallback. - Add rebinding and persisted mappings when the brief or accessibility requirements call for them. Store semantic action mappings, not engine objects. - Use input buffering or coyote time only when the design calls for forgiving timing; keep its duration in tuning configuration. ## Physics choice and rules Choose the least complex model that can express the game: | Need | Choice | | --- | --- | | No collision or only simple range checks | Manual movement and geometry checks | | Fast axis-aligned or circular collision, platformer, top-down movement | Arcade Physics | | Rotated/compound bodies, joints, torque, stacking, physical simulation | Matter Physics | ### Required - Default to Arcade Physics for ordinary arcade gameplay. Use Matter only when a documented mechanic needs its capabilities. - Use one physics system for interacting objects. Arcade and Matter bodies do not collide with each other; never attach the same Game Object to both. - Specify units for velocity, acceleration, gravity, dimensions, and cooldowns. Treat Arcade velocity as pixels per second; do not multiply a velocity passed to Arcade by frame delta. - Match physics bodies to the intended collision silhouette, not blindly to transparent texture bounds. Recheck offsets after changing scale or origin. - Separate collision (`collider`, with resolution) from trigger detection (`overlap`, without resolution) intentionally. - Make collision callbacks idempotent or guard them; physics can report contact across multiple steps. - Define world bounds and out-of-bounds behavior explicitly. Do not let important objects disappear forever without a recovery or terminal rule. - Keep physics debug rendering development-only and disable it in production builds. ### Conditional - After moving or scaling an Arcade static body, call `refreshBody()` so its body matches its visual transform. - Use Arcade collision categories, body masks, immovable/pushable settings, and custom process callbacks when layers have distinct interaction rules. - For Matter, define body shapes, friction, restitution, mass/density, collision categories, and sleeping deliberately; test unstable stacks and high-speed tunneling. - Use a fixed simulation strategy or seeded replay only when determinism is a product requirement. Isolate rendering interpolation from authoritative simulation state. ## Scale and responsive layout ### Required - Choose one logical game resolution and design gameplay in those world units. - For new fixed-composition games, default to `Phaser.Scale.FIT` with `Phaser.Scale.CENTER_BOTH`. Let CSS size the canvas container; do not derive simulation coordinates from CSS pixels. - Give the parent element an explicit usable size. Remove unintended page margins and prevent page scrolling around the game canvas. - Anchor HUD and menus to camera/viewport bounds, not world objects, unless the UI is intentionally diegetic. - Test the minimum and maximum supported aspect ratios. Critical play space, text, and controls must remain visible and usable. - Recompute responsive layout from current dimensions; do not incrementally nudge old positions on every resize event. - Keep camera bounds, physics bounds, and visible world bounds conceptually separate and configure each intentionally. ### Conditional - Use `RESIZE` instead of `FIT` only when the design should reveal more or less world as the viewport changes. Listen for resize and relayout cameras, UI, and hit areas. - For mobile targets, account for orientation changes, browser chrome, display cutouts, and CSS safe-area insets. Do not place essential controls at unsafe edges. - Raise render resolution for high-density displays only after profiling fill rate and texture memory; cap it on lower-powered mobile devices. - Letterbox intentionally when preserving composition matters more than filling every pixel. Give the surrounding page a designed background. ## State ownership Use the narrowest owner that matches the state's lifetime: | State lifetime | Preferred owner | | --- | --- | | One Game Object | That object or its focused controller | | One Scene run | The Scene or a Scene-owned system | | One play session across Scenes | A typed plain-data session store | | Across browser sessions | A versioned persistence adapter | | Static balance values | Read-only tuning/configuration modules | ### Required - Assign one authoritative writer for each piece of state. Other systems receive snapshots, queries, or explicit commands. - Keep rules state as plain serializable data where practical. Keep Phaser Game Objects, bodies, sounds, and timers out of saves and cross-Scene stores. - Separate definition data, current runtime state, and rendered view. Do not mutate imported configuration to represent a run. - Use the Phaser registry only behind named, typed accessors for genuinely game-global values. Do not turn it into an unstructured dumping ground. - Make transitions explicit: title → playing → paused → result → restart. Reject commands invalid for the current state. - Avoid mutable globals and values attached to `window`. Destroy the `Phaser.Game` instance if the host page unmounts or recreates it. - Version persistent save data, validate it on load, and provide a safe fallback for corrupt or old data. ### Conditional - Use events for discrete notifications with multiple legitimate listeners. Use direct calls or returned values for single-owner commands and queries. - Introduce a finite-state machine when booleans allow impossible combinations or transition guards are being duplicated. - Add immutable updates, snapshots, or replay logs only when undo, deterministic replay, networking, or difficult state debugging requires them. ## Time, delta, and determinism ### Required - Remember that `update(time, delta)` receives `delta` in milliseconds. Convert once with `const dt = delta / 1000` for manual velocities expressed per second. - Make manual movement, meters, cooldowns, spawning, and animation-independent rules frame-rate independent. Never express gameplay duration as a count of render frames. - Do not multiply Arcade Physics velocity by `delta`; the physics world already integrates it. - Prefer Scene-owned `this.time` events and tweens to raw `setTimeout` or `setInterval` so lifecycle and pausing remain coherent. - Decide what happens on tab blur or a long frame. Pause, or clamp the delta used by manual integration to prevent a large teleport or instant timer drain. - Remove or invalidate delayed callbacks when their owner shuts down. A callback from a previous run must never mutate the next run. - Store durations with unit-bearing names such as `spawnIntervalMs` or `invulnerabilitySeconds`. ### Conditional - Use wall-clock time only for mechanics that must advance while the game is closed; validate clock jumps and persist a reference timestamp. - Use `Phaser.Math.RandomDataGenerator` with an explicit seed when reproducing a run, testing procedural output, or synchronizing simulations matters. - Use a custom fixed-step loop only when the design or networking model requires it. Define accumulation, maximum catch-up steps, interpolation, and overload behavior. ## Tuning and configuration ### Required - Put player-facing balance values in small typed, read-only configuration objects grouped by system. Keep engine boot configuration separate from gameplay tuning. - Name values by meaning and unit; include a short rationale or valid range when a number is not self-evident. - Avoid repeated magic numbers in Scenes and callbacks. One mechanic must read from one source of truth. - Keep tunable values traceable to the relevant game-design rule and acceptance criteria. When tuning changes the designed behavior, update both. - Clamp user- or data-supplied values at system boundaries. Reject invalid level or content data with a useful message. - Keep secrets and privileged rules off the client. Everything shipped to a browser can be inspected or modified. ### Conditional - Load level/content JSON when non-programmers need to edit repeated content or when many levels share one schema. Validate it before constructing gameplay objects. - Add a development-only tuning panel when iteration speed justifies it. Ensure its state and debug commands cannot leak into production behavior. - Introduce schema generation, migrations, or remote configuration only when content volume or live operations creates a current need. ## Verification and completion ### Required automated checks - Install from the committed lockfile in a clean environment. - Run strict type checking and every existing lint or automated test command. - Run the production build. A development server compiling successfully is not a substitute. - Keep rules such as score calculation, spawn selection, damage, progression, and save migration pure where possible, and add focused tests when they are nontrivial or regression-prone. ### Required browser smoke check Run the built game in a real browser, preferably from the production preview, and verify: 1. The page loads into the intended first interactive state. 2. The primary input performs the core action and visible/audio feedback occurs. 3. The player can reach the success or failure result through the intended loop. 4. Restart returns to a clean initial run; complete and restart the loop at least twice. 5. There are no new console errors, unhandled promise rejections, failed required asset requests, or duplicate-event symptoms. 6. Pause/focus loss and return do not cause stuck input, a time jump, or resumed sounds that should remain paused. 7. The supported minimum and maximum viewport sizes remain playable. ### Conditional checks - Test each declared input family—keyboard, pointer, touch, and gamepad—on representative hardware. - Add browser automation for stable critical flows such as boot, start, result, restart, save/load, and resize when manual regression becomes costly. - Profile frame time, draw calls, memory, asset size, and mobile thermal behavior when measured performance misses its target. Optimize measured bottlenecks, not guesses. - Test offline, slow network, load failure, localization, accessibility, and persistence recovery when the product claims those capabilities. ## Common failure patterns | Symptom | Likely cause | Required response | | --- | --- | --- | | Phaser 4 types or APIs appear in a Phaser 3 game | Installed `latest` or a version range resolved unexpectedly | Pin `phaser` to exactly `3.90.0`, recreate the lockfile deliberately, and use the versioned API docs | | Blank canvas with no visible error | Wrong parent size, Scene not registered, or required asset failed | Inspect console/network, verify parent dimensions and Scene order, and surface loader errors | | Input fires twice after restart | Listener registered on every `create` without cleanup | Register once per run and unsubscribe on shutdown | | Result Scene starts repeatedly | Transition is triggered from repeated overlap/update callbacks | Add a terminal-state guard before changing Scenes | | Score, health, or flags survive restart | State initialized only in the constructor | Reset all run state in `init` or `create` | | Movement speed changes with frame rate | Per-frame movement or wrong delta units | Express speed per second and use `delta / 1000` for manual integration | | Object teleports after tab return | A large delta was integrated after suspension | Pause on blur or clamp manual-integration delta | | Pointer aim is offset when camera moves or canvas scales | Screen coordinates used as world coordinates | Convert through the active camera | | Sprite and collision do not line up | Body size/offset was not updated after origin or scale changes | Configure and visually debug the body after final transform | | Static collider remains at its old position | Arcade static body changed without refresh | Call `refreshBody()` after the transform | | Assets work locally but 404 under a subpath | Root-relative URL ignored Vite base path | Derive loader URLs from `import.meta.env.BASE_URL` | | Audio is silent on mobile | Playback started before a user gesture | Unlock/resume audio from an explicit interaction and expose mute state | | Pixel art is blurred or shimmers | Filtering or fractional scaling/positioning | Enable pixel-art settings and use integer-friendly scales and positions | | Game exists twice after navigation | Host recreated the game without destroying the old instance | Keep one owner and call `game.destroy(true)` during host teardown | ## Official references - [Phaser 3.90.0 release](https://github.com/phaserjs/phaser/releases/tag/v3.90.0) - [Phaser 3.90.0 API documentation](https://docs.phaser.io/api-documentation/3.90.0/api-documentation) - [Scenes and lifecycle](https://docs.phaser.io/phaser/concepts/scenes) - [Loader](https://docs.phaser.io/phaser/concepts/loader) - [Input](https://docs.phaser.io/phaser/concepts/input) - [Arcade Physics](https://docs.phaser.io/phaser/concepts/physics/arcade) - [Matter Physics](https://docs.phaser.io/phaser/concepts/physics/matter) - [Scale Manager](https://docs.phaser.io/phaser/concepts/scale-manager) - [Time and Clock](https://docs.phaser.io/phaser/concepts/time) - [Vite getting started](https://vite.dev/guide/) - [TypeScript strict mode](https://www.typescriptlang.org/tsconfig/strict.html)