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

307 lines
22 KiB
Markdown

# 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)