create-playcanvas 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,65 +1,25 @@
1
1
  ---
2
2
  name: override-shader-chunks
3
- description: Use when a PlayCanvas StandardMaterial needs custom shading its properties cannot express — procedural or stylized looks, custom vertex displacement, per-material screen effects — before writing a standalone shader from scratch.
3
+ description: Use when a PlayCanvas StandardMaterial needs custom shading its properties cannot express, such as stylized looks or vertex displacement, before writing a ShaderMaterial.
4
4
  ---
5
5
 
6
- # Override shading through chunks
7
-
8
- A `StandardMaterial`'s shader is assembled from named chunks (lighting, combine, transform, and
9
- more). Overriding one keeps the rest of the pipeline — fog, tonemapping, shadows, skinning,
10
- instancing — intact. Reach for a fully custom `ShaderMaterial` only when the look needs a stage the
11
- chunk system does not expose at all (its own vertex/fragment pair and attribute layout); a chunk
12
- override is the default for everything else, since it is cheaper and stays integrated.
13
-
14
- ## Pin the version, read the source
15
-
16
- Set `material.shaderChunksVersion` to the installed engine's major.minor (for example `'2.22'`)
17
- before shipping any override. Chunk names and their contracts — what a chunk defines, what globals
18
- it reads or writes — are version-keyed and change between releases. Read the installed `playcanvas`
19
- source for the exact chunk being touched; do not recall its contract from memory or an older
20
- version's documentation.
21
-
22
- ## Clone before overriding a shared material
23
-
24
- Clone a material that other mesh instances share — including default and asset-imported materials —
25
- before changing its chunks, properties, or material-level parameters for only some meshes, and assign
26
- the clone. For per-mesh uniforms, use `meshInstance.setParameter()` without cloning the material.
27
-
28
- ## Set chunks through the documented accessor
29
-
30
- Use `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)`, and the WGSL equivalent —
31
- the public, documented method — not the underlying `shaderChunks` maps, which are internal. Call
32
- `material.update()` after any change so cached shader variants are cleared.
33
-
34
- ## Emissive-only overrides
35
-
36
- An `emissivePS` override implements `void getEmission()`, writing the result into `dEmission`. When
37
- the override is meant to fully drive the visible color, zero the material's `diffuse`/`specular` and
38
- disable `useSkybox` so no other lighting term still contributes underneath it.
39
-
40
- ## Uniforms are raw
41
-
42
- `setParameter` uploads exactly what it is given, with no color-space conversion. Convert any
43
- authored sRGB color with `Color#linear()` before calling `setParameter`; an sRGB color passed
44
- straight through renders visibly darker and less saturated than it was authored.
45
-
46
- ## Keep vertex chunks instancing-aware
47
-
48
- A custom `transformCoreVS` (or any vertex chunk reading the model matrix) must keep the engine's
49
- `INSTANCING` define path — pulling the per-instance matrix under that define — with a
50
- non-instanced fallback that uses `matrix_model` directly, matching the stock chunk's structure.
51
- Dropping this path silently breaks instancing for any mesh instance using the material.
52
-
53
- ## Provide GLSL and WGSL, or pin to WebGL
54
-
55
- Override both `SHADERLANGUAGE_GLSL` and `SHADERLANGUAGE_WGSL` chunks for anything touched. On a
56
- WebGPU device, a chunk supplied only in GLSL forces the material's entire shader — not just that
57
- chunk — onto a GLSL-compiled-then-transpiled path; without the optional transpiler modules loaded
58
- (the common case), that shader fails to compile. Supply both languages, or explicitly pin the
59
- graphics device to WebGL if WGSL support is not required.
60
-
61
- ## Expand bounds for vertex displacement
62
-
63
- If the override displaces vertices, expand the mesh's bounds by the maximum displacement (see
64
- `apply-conventions`) — CPU-computed bounds never observe shader-side deformation, so culling breaks
65
- silently otherwise.
6
+ # Override shader chunks
7
+
8
+ Overriding a `StandardMaterial` chunk keeps fog, tone mapping, shadows, skinning, and instancing
9
+ intact. Use a `ShaderMaterial` only when the look needs its own vertex and fragment pair.
10
+
11
+ - Set `material.shaderChunksVersion` to the installed major.minor, such as `'2.22'`. Chunk names
12
+ and contracts change between versions, so read the installed chunk source, not memory.
13
+ - Set chunks with `material.getShaderChunks(SHADERLANGUAGE_GLSL).set(name, source)` and the WGSL
14
+ equivalent, then call `material.update()`. For every material on a device, use
15
+ `ShaderChunks.get(device, language)`; the global `shaderChunks` object is deprecated.
16
+ - Supply both GLSL and WGSL. On WebGPU, one GLSL-only chunk sends the whole shader through a
17
+ transpiler that is usually not loaded, so it fails to compile. Otherwise pin the device to WebGL.
18
+ - Clone shared materials, including imported ones, before overriding them for only some meshes.
19
+ Per-mesh uniforms use `meshInstance.setParameter()`.
20
+ - Uniforms are raw; convert sRGB colours with `Color#linear()`.
21
+ - Vertex chunks that read the model matrix, such as `transformCoreVS`, must keep the `INSTANCING`
22
+ path with a `matrix_model` fallback, like the stock chunk.
23
+ - Expand mesh bounds by the maximum displacement; CPU bounds do not see shader deformation.
24
+ - For an emissive-only look, implement `getEmission()` writing `dEmission`, zero `diffuse` and
25
+ `specular`, and disable `useSkybox`.
@@ -1,67 +1,37 @@
1
1
  ---
2
2
  name: reduce-draw-calls
3
- description: Use when a PlayCanvas application submits too many draw calls or frame time is CPU-bound — repeated meshes, dense grids, always-on interface elements — before hand-writing mesh merging or custom renderers.
3
+ description: Use when a PlayCanvas app has too many draw calls or is CPU-bound, before hand-merging meshes or writing custom renderers.
4
4
  ---
5
5
 
6
- # Cut draw calls, ranked by cost
6
+ # Cut draw calls
7
7
 
8
- Work the ladder in order and stop at the first rung that clears the budget. Each rung trades away
9
- less flexibility than the next; skipping straight to a custom renderer or hand-written mesh merge
10
- costs more engineering time than the draw calls it saves.
8
+ Measure first with `app.stats.drawCalls.total` or `MiniStats`; the per-pass breakdown fills only in
9
+ profiler builds. Also halve the pixel ratio and call `app.resizeCanvas()`: if frame time drops, the
10
+ cost is fill rate, not draw calls. Stop at the first rung that meets the budget and report
11
+ before-and-after counts.
11
12
 
12
- ## Measure first
13
+ ## 1. Stop drawing hidden things
13
14
 
14
- Read `app.stats.drawCalls.total` (or the `forward`/`depth`/`shadow` breakdown) before changing
15
- anything, or drop in `MiniStats` for a live overlay. Every rung below is proved against this number,
16
- not against intuition about what "looks expensive."
15
+ An element at opacity 0 still draws. Set `enabled = false`.
17
16
 
18
- Draw count alone does not identify the bottleneck. Test sensitivity to pixel cost by halving the
19
- effective pixel ratio (`Math.min(graphicsDevice.maxPixelRatio, window.devicePixelRatio)`) and calling
20
- `app.resizeCanvas()`. Verify the backbuffer shrank and compare frame times at the same scene state.
21
- If frame time improves materially, prioritize resolution, multisampling and per-pixel shader cost.
22
- Otherwise profile CPU and GPU work before choosing a rung; restore the original ratio after the test.
17
+ ## 2. BatchManager
23
18
 
24
- ## Rung 1: stop drawing invisible things
19
+ Call `app.batcher.addGroup(name, dynamic, maxAabbSize)`, then set `batchGroupId` on render, sprite,
20
+ or element components. Use `dynamic: false` for geometry that never moves. Members join when
21
+ enabled in the live tree, in any order, and groups regenerate automatically when membership
22
+ changes. Call `markGroupDirty(id)` only after moving members of a static group. Manual `AppOptions`
23
+ bootstraps must set `batchManager`.
25
24
 
26
- An element at opacity 0 still submits a draw call — its mesh instance exists and is still in a
27
- layer, so the GPU processes it every frame for no visible result. Toggle `enabled` on the entity to
28
- actually skip it. Check every element that is ever fully transparent, not only the ones on screen
29
- when you're profiling — this rung is easy to skip precisely because nothing looks wrong.
25
+ ## 3. Hardware instancing
30
26
 
31
- ## Rung 2: `BatchManager` — merge without touching shaders
27
+ Use this for many copies of one mesh and material with per-frame transforms. Build a buffer with
28
+ `VertexFormat.getDefaultInstancingFormat(device)`, one mat4 per instance, and call
29
+ `meshInstance.setInstancing(vb)`; `instancingCount` follows the buffer.
32
30
 
33
- `app.batcher.addGroup(name, dynamic, maxAabbSize)`, then set `batchGroupId` on each member's
34
- component (render, sprite, or UI element). Use `dynamic: false` for geometry that never moves.
31
+ There is no per-instance culling. By default the group always draws; `setInstancing(vb, true)`
32
+ culls it as one unit against a `RenderComponent#customAabb` covering every instance. Split large
33
+ instance sets into spatial chunks. Custom vertex chunks need the `INSTANCING` path
34
+ (`override-shader-chunks`). Start from the `graphics/instancing-basic` and
35
+ `graphics/instancing-custom` examples.
35
36
 
36
- Contract: the `batchGroupId` setter only inserts into the batcher while `entity.enabled` is true,
37
- and that flag requires both the local enable state and hierarchy attachment — set `batchGroupId`
38
- after the entity is parented into the live tree, not before, or the member silently drops out of the
39
- group. `BatchManager.generate()` runs once automatically on the app's first rendered frame; if group
40
- membership changes afterward, call `app.batcher.markGroupDirty(id)` (or `generate([id])`) yourself.
41
- An Engine-only app without the full `Application` bootstrap must register the class via
42
- `AppOptions.batchManager` before it can batch anything.
43
-
44
- ## Rung 3: hardware instancing — merge without touching layout
45
-
46
- Reach for this once distinct materials or per-frame transform updates would defeat `BatchManager`.
47
- Build a per-instance vertex buffer with `VertexFormat.getDefaultInstancingFormat(device)` (one mat4
48
- per instance), call `meshInstance.setInstancing(vb)`, and set `instancingCount`. A custom vertex
49
- chunk needs its own `INSTANCING` code path with an identity-matrix fallback for the non-instanced
50
- case (cross-reference `override-shader-chunks`).
51
-
52
- Culling trade-off: instanced meshes cull as one unit against a single bounding volume — an
53
- off-screen instance inside an otherwise-visible group still draws unless you opt in. Pass
54
- `setInstancing(vb, true)` and set a `RenderComponent#customAabb` spanning every instance's world
55
- extent so the renderer has something correct to cull against instead of culling nothing or
56
- culling the whole group by one instance's bounds.
57
-
58
- Adapt the official recipes rather than deriving the buffer layout or vertex-shader wiring from
59
- memory: `graphics/instancing-basic` for the format/buffer contract, `graphics/instancing-custom` for
60
- the vertex-shader side. Locate both at the installed engine version via `find-examples`.
61
-
62
- ## Prove and report
63
-
64
- Verify the rendered image is unchanged with `verify-pixels` before calling the change done —
65
- merging or instancing must not move a single pixel. Re-measure `app.stats.drawCalls` afterward and
66
- state the before/after counts in the change description; "fewer draw calls" without numbers is not
67
- a result.
37
+ Confirm the image is unchanged with `verify-pixels`.
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  name: reuse-scripts
3
- description: Use before implementing non-core PlayCanvas behavior to discover the installed Engine's curated scripts and reuse or adapt those that fit the required behavior and approved art direction.
3
+ description: Use before writing PlayCanvas behaviour from scratch, to reuse the scripts and helpers the installed Engine already ships.
4
4
  ---
5
5
 
6
- # Engine scripts
6
+ # Reuse shipped Engine code
7
7
 
8
- Before writing behavior from scratch, inspect the curated scripts shipped with the installed
9
- `playcanvas` package under `scripts/esm/**`. Discover the current set:
8
+ Before writing behaviour, check what the installed `playcanvas` package already provides.
9
+
10
+ Scripts live under `scripts/esm/**`; import only from there. List them:
10
11
 
11
12
  ```sh
12
13
  rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
@@ -14,50 +15,26 @@ rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
14
15
  | sort
15
16
  ```
16
17
 
17
- Read the selected file for its named export, `@attribute` properties, and defaults. Not every module
18
- is a `Script`; the parsers below `scripts/esm/parsers` are plain classes registered with a resource
19
- handler instead.
20
-
21
- Only import from `scripts/esm/**`. Legacy sibling directories depend on the global Engine namespace.
22
- After selecting a script, use `find-examples` to locate its matching versioned Engine example. When
23
- `node_modules/playcanvas` is a linked or source checkout, its `examples/src/examples/**` are already
24
- on disk; read them there and skip the fetch.
25
-
26
- ## Check fit before integration
27
-
28
- Compare candidates with the required behavior, approved art direction, and runtime constraints.
29
- Reuse a suitable script directly or adapt its configuration and extension points. If none fits,
30
- state the limitation and implement only the missing behavior, preserving reusable parts and their
31
- lifecycle, bounds, and input invariants.
32
-
33
- For visual behavior, resolve an unclear art direction with the user before substantial implementation.
34
- Present references, mockups, or inexpensive variants; follow existing approval and confirm significant
35
- departures. A script's default appearance is only a starting point for assessing its fit.
36
-
37
- ## Adapt the reference integration
38
-
39
- Treat the selected script source and its closest official example as complementary references:
18
+ Common ones are `CameraControls`, `FirstPersonController`, `ThirdPersonController`,
19
+ `ShadowCatcher`, `ProceduralSky`, `Water`, and `Grid`, plus XR and splat scripts. Modules under
20
+ `scripts/esm/parsers` are resource-handler classes, not scripts.
40
21
 
41
- 1. Read the source for exports, properties, defaults, fallbacks, required components, and lifecycle.
42
- 2. Read the example for assets, entity references, mesh requirements, layer ordering, scene settings,
43
- and render-pipeline setup.
44
- 3. Integrate the selected behavior with its required components, assets, and passes. Adapt the look
45
- to the approved direction and preserve defaults and dependencies that still apply.
46
- 4. After a rendered frame, fail on console, shader, or missing-asset diagnostics. Exercise the
47
- behavior with real input where applicable and inspect returned screenshots from representative
48
- views at the final backbuffer density.
49
- 5. Use the example to check integration correctness and the user's approved direction to judge the
50
- final look. Report any remaining gap.
22
+ Core exports that are not scripts include `CameraFrame` for post-processing, `Picker`
23
+ (`getSelectionAsync`), `TransformGizmo`, `OutlineRenderer`, `MiniStats`, `GltfExporter`, and
24
+ `UsdzExporter`.
51
25
 
52
- If no matching example exists, state that and derive the integration from installed source instead
53
- of inventing it from memory.
26
+ ## Integrate
54
27
 
55
- ## Preserve grouped defaults
28
+ 1. Read the source for exports, `@attribute` properties, defaults, and required components.
29
+ 2. Find its official example with `find-examples`, and copy the assets, layers, scene settings, and
30
+ render setup it depends on.
31
+ 3. Adapt configuration and extension points. If nothing fits, implement only the missing part and
32
+ say what was missing.
33
+ 4. After a rendered frame, fail on console, shader, or missing-asset errors.
56
34
 
57
- Grouped property updates differ by authoring surface. Read the selected reference and preserve
58
- defaults that are not being changed.
35
+ Keep a shipped script's input mapping and damping unless asked otherwise, and verify input
36
+ directions with real input rather than deriving signs from memory.
59
37
 
60
- Read exactly one reference matching the code being edited:
61
- [direct Engine](references/direct-engine.md), [React](references/react.md), or
62
- [Web Components](references/web-components.md). Choose from imports and markup, not installed
63
- dependencies alone.
38
+ Grouped property updates differ by surface; preserve defaults you do not change. Read only the
39
+ reference matching the code: [direct Engine](references/direct-engine.md),
40
+ [React](references/react.md), or [Web Components](references/web-components.md).
@@ -6,9 +6,9 @@ and attach it with `entity.script.create()`. Preserve an existing script compone
6
6
  Some production helpers are core Engine exports rather than scripts. Follow the matching example's
7
7
  construction and teardown contract instead of forcing them through `script.create`.
8
8
 
9
- Keep script constructors at module scope. `create` takes two distinct option keys that are not
10
- interchangeable: `properties` is assigned straight onto the instance, while `attributes` supplies
11
- declared `@attribute` fields. Passing the wrong one silently does nothing.
9
+ Keep script constructors at module scope. `create` takes two option keys: `attributes` is passed to
10
+ the constructor for declared `@attribute` fields, while `properties` is `Object.assign`ed onto the
11
+ instance after construction, overwriting any field it names.
12
12
 
13
13
  Prefer mutating an existing grouped property when only a few fields change; preserve other defaults
14
14
  and follow any update or rebuild contract in the selected source.
@@ -1,70 +1,36 @@
1
1
  ---
2
2
  name: verify-pixels
3
- description: Use when changing PlayCanvas rendering code that must not visibly change the rendered image — draw-call optimization, hardware instancing, shader or material refactors, noise or lightmap bakes, or asset pipeline swaps — before shipping.
3
+ description: Use when a PlayCanvas rendering change is meant to leave the image unchanged, to prove it with deterministic captures and pixel comparison.
4
4
  ---
5
5
 
6
6
  # Prove pixels unchanged
7
7
 
8
- Choose the gate before building the capture harness. Both classes require controlled captures
9
- that cover the affected surfaces.
8
+ ## Choose the gate
10
9
 
11
- ## Classify the change
10
+ - Byte-exact: the change keeps the same decoded data and the same shading math, so every pixel
11
+ must match.
12
+ - Side-by-side review: the change alters sampled values, random order, precision, or where math
13
+ runs. Report it as "not byte-gateable" and show old and new at each pose; do not invent
14
+ thresholds.
12
15
 
13
- | Change | Gate |
14
- | --- | --- |
15
- | Batching, instancing, mesh indexing, chunking, pooling that preserves RNG order, an asset format swap with identical decoded data | Byte-exact matrix |
16
- | Re-rolled or re-sampled procedural noise, a changed RNG consumption order, math moved between CPU and GPU, changed precision, fewer octaves or samples | Not byte-gateable: side-by-side review |
16
+ ## Make frames deterministic
17
17
 
18
- For the second class, sampled values or precision can change. Report it as "not byte-gateable" and
19
- use the side-by-side gate after the shared capture setup below; do not invent uncalibrated perceptual
20
- thresholds to approve it.
18
+ - Load every asset and match camera, canvas size, lighting, and animation time between builds. Seed
19
+ incidental randomness.
20
+ - Drive animated shaders from one app-owned time value, not `Date.now()` or `performance.now()`.
21
+ - `app.timeScale = 0` makes `dt` zero, but `update` still runs; anything counting frames or reading
22
+ wall-clock time still advances.
23
+ - Set `app.autoRender = false`, then `app.renderNextFrame = true` before each frame to capture.
21
24
 
22
- ## Cover every touched surface
25
+ ## Capture and compare
23
26
 
24
- List every touched material or surface and a capture pose it dominates. Use at least two
25
- representative poses, adding as many as coverage requires; a horizon strip does not cover a water
26
- shader change. For animated surfaces, include a second phase mid-animation.
27
+ - WebGL: `await (device as WebglGraphicsDevice).readPixelsAsync(x, y, w, h, pixels)`. WebGPU:
28
+ render into a `RenderTarget` and read its `colorBuffer` with `Texture#read()`. Avoid colour
29
+ conversion and lossy encoding.
30
+ - Capture the unmodified build twice first. The captures must match exactly, or the harness itself
31
+ is noisy.
32
+ - Cover every touched surface in at least two poses, plus a mid-animation phase where relevant.
33
+ - Report the count, for example "0 of 65536 pixels differ", or the count and where the differences
34
+ are. "Looks the same" is not a result.
27
35
 
28
- ## Make the frame deterministic
29
-
30
- For both gates, load assets fully and match the scene state, camera, canvas size, lighting, and
31
- animation time between builds. Seed incidental randomness so only the intended change varies.
32
-
33
- - Drive every animated shader or vertex effect from one app-owned time value, never `Date.now()` or
34
- `performance.now()` read inside the render path, so a captured phase is exactly reproducible.
35
- - Freeze the clock with `app.timeScale = 0` before capturing; nothing should advance between frames
36
- you did not explicitly step.
37
- - Step frames explicitly: set `app.autoRender = false` once, then set `app.renderNextFrame = true`
38
- before each frame you want rendered. The engine renders exactly that frame and clears the flag —
39
- do not rely on the free-running render loop plus a timed screenshot.
40
-
41
- ## Gate and report the byte-exact class
42
-
43
- Read the exact backbuffer with `await device.readPixelsAsync(x, y, w, h, pixels)`. In the installed
44
- engine this method lives on `WebglGraphicsDevice`, so narrow to it; WebGPU needs an equivalent
45
- readback. An existing capture path must preserve raw pixels without colour conversion or lossy
46
- encoding before comparison.
47
-
48
- Before trusting any diff between the old and new build, capture the same pose × phase matrix twice
49
- from the *unmodified* build. Two captures of identical, frozen state must be bit-identical. If they
50
- are not, the capture path itself is the source of noise — an unseeded animation, an asset still
51
- loading, a GPU timing race — and must be fixed before it can say anything about the real change.
52
-
53
- Byte-compare each pose × phase pair between the two builds; do not diff by looking. Zero differing
54
- pixels passes outright. Any nonzero diff must be reviewed on-screen, and its cause and extent stated
55
- in the change description — never merged silently. Report the actual count every time, for example
56
- "0 of 65536 pixels differ" or "312 of 65536 pixels differ, confined to the object's silhouette edge".
57
- "Looks the same" or "no visible difference" is not a result.
58
-
59
- ## Review the other class side by side
60
-
61
- For each planned pose and phase, show the controlled old and new captures together in one image
62
- for the user's accept or reject. State what differs and why, for example "noise tile replaces
63
- runtime fbm: pattern period changed, tint and amplitude match". If no difference is visible, report
64
- that. Skip the cross-build byte comparison for this class.
65
-
66
- ## Keep the harness small
67
-
68
- Reuse the project's capture harness when available. Keep harness code out of the shipped bundle,
69
- behind a dev-only import or in a tools directory, and list its files in the change description.
70
- Capture with a headless browser and return images only at accept-or-reject points, not after every edit.
36
+ Keep harness code out of the shipped bundle.
@@ -1,38 +0,0 @@
1
- ---
2
- name: build-hud
3
- description: Use when building a PlayCanvas game's on-screen interface such as ready, pause, and victory overlays, menus, and live indicators like gauges, timers, or reload meters, so the interface is driven by game state, readable over the scene, and separate from the 3D hierarchy.
4
- ---
5
-
6
- # Game interface
7
-
8
- A HUD is a view of game state, not a source of it. Render it from the current state each frame or on
9
- each transition; never store gameplay truth in the DOM or a UI element.
10
-
11
- ## Choose the UI layer
12
-
13
- - Build the interface in the natural UI layer for the surface — a DOM or React overlay, or the
14
- Engine `screen` and `element` UI — decided with the `build-app` skill. Keep it outside gameplay
15
- model hierarchies, as `assemble-scene` requires.
16
- - For a screen-space Engine UI, adapt a user-interface example with `find-examples` rather than
17
- guessing element anchoring, scaling, and font handling.
18
-
19
- ## Drive from state
20
-
21
- - Map each overlay to a game state and each indicator to a snapshot field. A transition shows or
22
- hides overlays; it does not duplicate the state the game already owns (see `manage-game-state`).
23
- - An indicator that is idle most of the time toggles `enabled` with its visibility; an element left
24
- drawing at opacity 0 still costs a draw call every frame (see `reduce-draw-calls`).
25
- - Anchor and scale elements to the viewport so the layout holds at the target resolution and on
26
- resize. Verify at the acceptance resolution, not the default window size.
27
- - Keep copy minimal and legible over the scene: back text with a plate or shadow where the frame is
28
- bright, and unify interface colour with the scene's grade from `light-scene`.
29
-
30
- ## Prove it reflects state
31
-
32
- Drive real input through every transition and confirm with screenshots that the correct overlay
33
- shows and each indicator tracks the snapshot — a reload meter empties and refills, a timer advances,
34
- a menu appears only in its state. A saved filepath or a visibility flag is not visual evidence;
35
- inspect the returned pixels as the local agent guide requires.
36
-
37
- Choose the authoring surface with the `build-app` skill and build the interface through its own UI
38
- primitives before reaching into the Engine.
@@ -1,48 +0,0 @@
1
- ---
2
- name: manage-game-state
3
- description: Use when structuring a PlayCanvas game's control flow — a small state machine such as ready, playing, paused, and over, pointer-lock capture, pausing on focus or pointer-lock loss, a full reset, and a stable clock and timestep — so the loop stays deterministic and recoverable.
4
- ---
5
-
6
- # Game state and loop
7
-
8
- Model the game as a small explicit state machine with one owner of the clock. Every transition names
9
- its states; behaviour, input, and the interface read the current state rather than a scatter of
10
- booleans.
11
-
12
- ## States and transitions
13
-
14
- - Enumerate the states — for example ready, playing, paused, over — and make transitions explicit
15
- and total. Do not infer state from side effects.
16
- - Gate the simulation on the active state: advance gameplay, timers, and physics only while playing.
17
-
18
- ## Capture input and pause
19
-
20
- - Request pointer lock from a user gesture such as a click, never on load. Treat loss of pointer lock
21
- and tab blur as a pause: freeze the simulation and the clock, and resume on the next gesture.
22
- - Make `pointerlockchange` idempotent: when `document.pointerLockElement` is the canvas, transition
23
- ready or paused to playing and leave playing unchanged; only transition playing to paused when the
24
- element is no longer the canvas. Never implement it as a toggle. Browsers and automation can both
25
- report an already-acquired lock, and a duplicate enter event must not pause the game.
26
- - Clamp the per-frame delta before integrating anything, as `build-app` requires, so a backgrounded
27
- tab or a slow frame cannot inject one large step into movement, cooldowns, or the clock.
28
-
29
- ## Reset
30
-
31
- - A reset restores every owned system to its ready values — entities, camera, clock, timers,
32
- cooldowns, effects, and overlays — not only the player. Route reset through the same setup the
33
- initial state uses, so ready and reset cannot drift apart.
34
- - Transition to `ready` explicitly before releasing pointer lock. Never derive the reset state from
35
- `document.pointerLockElement`; pointer lock is an input side effect, not the state machine.
36
- - Define timer semantics once and keep them literal in state and snapshots. A field named `reload`
37
- is remaining cooldown time: `0` means ready, firing assigns a positive duration, and the active
38
- loop counts it down to exactly `0`. Do not expose a normalized readiness value under that name.
39
-
40
- ## Prove the loop
41
-
42
- Drive the full cycle with real input: enter play, lose pointer lock and confirm the clock and
43
- simulation freeze, resume, reset and confirm every field returns to its ready value, then reach the
44
- end state and restart. Read these from the app's own state snapshot, never from console mutation —
45
- manufactured state is not evidence.
46
-
47
- Choose the authoring surface with the `build-app` skill and own the loop and lifecycle in the surface
48
- that creates the application.