Files
makelore/.opencode/skills/nianxxgame-skill/references/phaser3-standards.md
2026-07-29 17:22:35 +08:00

22 KiB

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

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:

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 initpreloadcreate, 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