create-playcanvas 0.8.0 → 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.
Files changed (36) hide show
  1. package/dist/index.mjs +35 -33
  2. package/package.json +4 -4
  3. package/skills/add-effects/SKILL.md +21 -72
  4. package/skills/apply-conventions/SKILL.md +33 -59
  5. package/skills/assemble-scene/SKILL.md +18 -43
  6. package/skills/assemble-scene/references/direct-engine.md +2 -2
  7. package/skills/assemble-scene/references/react.md +3 -3
  8. package/skills/assemble-scene/references/web-components.md +2 -2
  9. package/skills/bake-lighting/SKILL.md +25 -50
  10. package/skills/build-app/SKILL.md +29 -28
  11. package/skills/build-app/references/web-components.md +3 -3
  12. package/skills/calibrate-model/SKILL.md +18 -48
  13. package/skills/calibrate-model/references/web-components.md +3 -3
  14. package/skills/configure-animation/SKILL.md +21 -37
  15. package/skills/configure-animation/references/direct-engine.md +2 -2
  16. package/skills/configure-animation/references/react.md +3 -3
  17. package/skills/find-examples/SKILL.md +24 -60
  18. package/skills/inspect-glb/SKILL.md +21 -62
  19. package/skills/light-scene/SKILL.md +29 -82
  20. package/skills/load-assets/SKILL.md +52 -0
  21. package/skills/override-shader-chunks/SKILL.md +21 -55
  22. package/skills/reduce-draw-calls/SKILL.md +24 -48
  23. package/skills/reuse-scripts/SKILL.md +23 -42
  24. package/skills/reuse-scripts/references/direct-engine.md +8 -28
  25. package/skills/reuse-scripts/references/react.md +2 -12
  26. package/skills/reuse-scripts/references/web-components.md +2 -15
  27. package/skills/verify-pixels/SKILL.md +30 -44
  28. package/templates/engine/base/package.json +3 -3
  29. package/templates/react/ar-placement/src/App.tsx +29 -21
  30. package/templates/react/base/package.json +8 -8
  31. package/templates/react/sprite-game/src/App.tsx +21 -13
  32. package/templates/web-components/base/package.json +4 -4
  33. package/templates/web-components/interactive-sphere/index.html +1 -1
  34. package/templates/web-components/splat-viewer/index.html +1 -1
  35. package/skills/build-hud/SKILL.md +0 -38
  36. package/skills/manage-game-state/SKILL.md +0 -48
@@ -1,20 +1,7 @@
1
1
  # Web Components projects
2
2
 
3
- Expose an Engine script as an ES module and register it as an asset:
4
-
5
- ```html
6
- <pc-app>
7
- <pc-asset src="/scripts/camera-controls.mjs"></pc-asset>
8
- <pc-scene>
9
- <pc-entity name="Camera">
10
- <pc-camera></pc-camera>
11
- <pc-script>
12
- <pc-script-instance name="cameraControls" move-speed="4"></pc-script-instance>
13
- </pc-script>
14
- </pc-entity>
15
- </pc-scene>
16
- </pc-app>
17
- ```
3
+ Expose the selected script as an ES module and register its URL with a `pc-asset` directly under
4
+ `pc-app`. Attach a `pc-script` to the owning `pc-entity`, with a `pc-script-instance` for the class.
18
5
 
19
6
  The `name` must equal the class's static `scriptName`. Extra kebab-case attributes map to camelCase
20
7
  script properties. Use the `attributes` JSON attribute for nested or reserved properties; it
@@ -1,50 +1,36 @@
1
1
  ---
2
2
  name: verify-pixels
3
- description: Use when changing PlayCanvas rendering code that must not change the rendered image — draw-call optimization, hardware instancing, shader or material refactors, or asset pipeline swaps — to prove the output unchanged 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
- A rendering change that must not alter the image is not proven by eyeballing a screenshot at one
9
- pose. Build a deterministic capture, compare byte-exact counts across a pose × phase matrix, and
10
- prove the capture harness itself is trustworthy before trusting what it reports about the change.
11
-
12
- ## Make the frame deterministic
13
-
14
- - Drive every animated shader or vertex effect from one app-owned time value, never `Date.now()` or
15
- `performance.now()` read inside the render path, so a captured phase is exactly reproducible.
16
- - Freeze the clock with `app.timeScale = 0` before capturing; nothing should advance between frames
17
- you did not explicitly step.
18
- - Step frames explicitly: set `app.autoRender = false` once, then set `app.renderNextFrame = true`
19
- before each frame you want rendered. The engine renders exactly that frame and clears the flag —
20
- do not rely on the free-running render loop plus a timed screenshot.
21
- - Read the rendered pixels with `await device.readPixelsAsync(x, y, w, h, pixels)` against the exact
22
- backbuffer, not a re-encoded screenshot (e.g. `canvas.toDataURL`) that can introduce compression or
23
- colour-management differences the eye won't catch. In the installed engine this method lives on
24
- `WebglGraphicsDevice`, not the base `GraphicsDevice` type, so narrow to it (or branch on
25
- `device.isWebGL2`) before calling; a WebGPU project needs its own equivalent readback. If the
26
- project already has a screenshot path, hold it to the same rule: fixed size, fixed pose, no lossy
27
- step before the byte comparison.
28
-
29
- ## Build a pose × phase matrix
30
-
31
- Single-pose, single-frame proof cannot see everything a rendering refactor can break. Choose at
32
- least two representative camera poses — angles that would expose a dropped instance, a wrong batch
33
- bound, or a seam differently — and at least two animation phases, including one mid-animation, not
34
- only frame zero. Capture every pose × phase pair for the build before the change and the build after
35
- it, with the same canvas size, camera, and lighting each time.
36
-
37
- ## Run a same-build control first
38
-
39
- Before trusting any diff between the old and new build, capture the same pose × phase matrix twice
40
- from the *unmodified* build. Two captures of identical, frozen state must be bit-identical. If they
41
- are not, the capture path itself is the source of noise — an unseeded animation, an asset still
42
- loading, a GPU timing race — and must be fixed before it can say anything about the real change.
43
-
44
- ## Gate and report
45
-
46
- Byte-compare each pose × phase pair between the two builds; do not diff by looking. Zero differing
47
- pixels passes outright. Any nonzero diff must be reviewed on-screen, and its cause and extent stated
48
- in the change description — never merged silently. Report the actual count every time, for example
49
- "0 of 65536 pixels differ" or "312 of 65536 pixels differ, confined to the object's silhouette edge".
50
- "Looks the same" or "no visible difference" is not a result.
8
+ ## Choose the gate
9
+
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.
15
+
16
+ ## Make frames deterministic
17
+
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.
24
+
25
+ ## Capture and compare
26
+
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.
35
+
36
+ Keep harness code out of the shipped bundle.
@@ -15,14 +15,14 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "playcanvas": "^2.22.0"
18
+ "playcanvas": "^2.22.6"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@playcanvas/eslint-config": "3.0.0-beta.8",
22
22
  "eslint": "^9.39.5",
23
- "prettier": "^3.9.6",
23
+ "prettier": "^3.9.9",
24
24
  "typescript": "~6.0.3",
25
- "vite": "^8.2.2"
25
+ "vite": "^8.3.1"
26
26
  },
27
27
  "engines": {
28
28
  "node": ">=22.23.2"
@@ -7,7 +7,7 @@ import { useState } from 'react';
7
7
  import { ArPlacement } from './ar-placement';
8
8
  import './starter.css';
9
9
 
10
- function App() {
10
+ function Scene() {
11
11
  const [camera, setCamera] = useState<PcEntity | null>(null);
12
12
  const [preview, setPreview] = useState<PcEntity | null>(null);
13
13
  const surface = useMaterial({ diffuse: '#474d57' });
@@ -16,29 +16,37 @@ function App() {
16
16
 
17
17
  return (
18
18
  <>
19
- <Application graphicsDeviceOptions={{ alpha: true, xrCompatible: true }}>
20
- <Entity name="camera" ref={setCamera} position={[3, 2.4, 4]} rotation={[-20, 37, 0]}>
21
- <Camera clearColor="#0d141f" />
19
+ <Entity name="camera" ref={setCamera} position={[3, 2.4, 4]} rotation={[-20, 37, 0]}>
20
+ <Camera clearColor="#0d141f" />
21
+ </Entity>
22
+ <Entity name="light" rotation={[45, 30, 0]}>
23
+ <Light type="directional" intensity={2} />
24
+ </Entity>
25
+ <Entity name="preview" ref={setPreview}>
26
+ <Entity name="surface" scale={[4, 0.15, 4]}>
27
+ <Render type="cylinder" material={surface} />
28
+ </Entity>
29
+ <Entity name="box" position={[0, 0.65, 0]}>
30
+ <Render type="box" material={cyan} />
22
31
  </Entity>
23
- <Entity name="light" rotation={[45, 30, 0]}>
24
- <Light type="directional" intensity={2} />
32
+ <Entity name="reticle" position={[0, 0.12, 0]} scale={[1.5, 0.03, 1.5]}>
33
+ <Render type="cylinder" material={coral} />
25
34
  </Entity>
26
- <Entity name="preview" ref={setPreview}>
27
- <Entity name="surface" scale={[4, 0.15, 4]}>
28
- <Render type="cylinder" material={surface} />
29
- </Entity>
30
- <Entity name="box" position={[0, 0.65, 0]}>
31
- <Render type="box" material={cyan} />
32
- </Entity>
33
- <Entity name="reticle" position={[0, 0.12, 0]} scale={[1.5, 0.03, 1.5]}>
34
- <Render type="cylinder" material={coral} />
35
- </Entity>
35
+ </Entity>
36
+ {camera && preview && (
37
+ <Entity name="placement">
38
+ <Script script={ArPlacement} camera={camera} preview={preview} />
36
39
  </Entity>
37
- {camera && preview && (
38
- <Entity name="placement">
39
- <Script script={ArPlacement} camera={camera} preview={preview} />
40
- </Entity>
41
- )}
40
+ )}
41
+ </>
42
+ );
43
+ }
44
+
45
+ function App() {
46
+ return (
47
+ <>
48
+ <Application graphicsDeviceOptions={{ alpha: true, xrCompatible: true }}>
49
+ <Scene />
42
50
  </Application>
43
51
  <div className="hud">
44
52
  <section className="panel">
@@ -15,20 +15,20 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@playcanvas/react": "^0.11.5",
19
- "playcanvas": "^2.22.0",
20
- "react": "^19.2.8",
21
- "react-dom": "^19.2.8"
18
+ "@playcanvas/react": "^0.11.7",
19
+ "playcanvas": "^2.22.6",
20
+ "react": "^19.3.0",
21
+ "react-dom": "^19.3.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playcanvas/eslint-config": "3.0.0-beta.8",
25
- "@types/react": "^19.2.18",
26
- "@types/react-dom": "^19.2.7",
25
+ "@types/react": "^19.3.0",
26
+ "@types/react-dom": "^19.3.0",
27
27
  "@vitejs/plugin-react": "^6.1.1",
28
28
  "eslint": "^9.39.5",
29
- "prettier": "^3.9.6",
29
+ "prettier": "^3.9.9",
30
30
  "typescript": "~6.0.3",
31
- "vite": "^8.2.2"
31
+ "vite": "^8.3.1"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=22.23.2"
@@ -5,25 +5,33 @@ import { useMaterial } from '@playcanvas/react/hooks';
5
5
  import { SpriteGame } from './sprite-game';
6
6
  import './starter.css';
7
7
 
8
- function App() {
8
+ function Scene() {
9
9
  const dirt = useMaterial({ emissive: '#734729' });
10
10
  const grass = useMaterial({ emissive: '#479e57' });
11
11
 
12
+ return (
13
+ <>
14
+ <Entity name="camera" position={[0, 0, 10]}>
15
+ <Camera projection={1} orthoHeight={4.4} clearColor="#5cb9e6" />
16
+ </Entity>
17
+ <Entity name="dirt" position={[0, -3.39, 0]} scale={[20, 2.6, 1]}>
18
+ <Render type="box" material={dirt} />
19
+ </Entity>
20
+ <Entity name="grass" position={[0, -2, 0]} scale={[20, 0.18, 1]}>
21
+ <Render type="box" material={grass} />
22
+ </Entity>
23
+ <Entity name="game" position={[0, -2.15, 0]}>
24
+ <Script script={SpriteGame} />
25
+ </Entity>
26
+ </>
27
+ );
28
+ }
29
+
30
+ function App() {
12
31
  return (
13
32
  <>
14
33
  <Application>
15
- <Entity name="camera" position={[0, 0, 10]}>
16
- <Camera projection={1} orthoHeight={4.4} clearColor="#5cb9e6" />
17
- </Entity>
18
- <Entity name="dirt" position={[0, -3.39, 0]} scale={[20, 2.6, 1]}>
19
- <Render type="box" material={dirt} />
20
- </Entity>
21
- <Entity name="grass" position={[0, -2, 0]} scale={[20, 0.18, 1]}>
22
- <Render type="box" material={grass} />
23
- </Entity>
24
- <Entity name="game" position={[0, -2.15, 0]}>
25
- <Script script={SpriteGame} />
26
- </Entity>
34
+ <Scene />
27
35
  </Application>
28
36
  <div className="hud">
29
37
  <section className="panel">
@@ -15,15 +15,15 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@playcanvas/web-components": "^0.21.0",
19
- "playcanvas": "^2.22.0"
18
+ "@playcanvas/web-components": "^0.26.0",
19
+ "playcanvas": "^2.22.6"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@playcanvas/eslint-config": "3.0.0-beta.8",
23
23
  "eslint": "^9.39.5",
24
- "prettier": "^3.9.6",
24
+ "prettier": "^3.9.9",
25
25
  "typescript": "~6.0.3",
26
- "vite": "^8.2.2"
26
+ "vite": "^8.3.1"
27
27
  },
28
28
  "engines": {
29
29
  "node": ">=22.23.2"
@@ -9,7 +9,7 @@
9
9
  <body>
10
10
  <pc-app>
11
11
  <!-- Materials are declared once and referenced by id -->
12
- <pc-material id="sphere-material" diffuse="lightgrey"></pc-material>
12
+ <pc-material id="sphere-material" diffuse="#e6edf9"></pc-material>
13
13
 
14
14
  <pc-scene>
15
15
  <!-- Camera. Orbit controls are attached in src/main.ts -->
@@ -8,7 +8,7 @@
8
8
  </head>
9
9
  <body>
10
10
  <!-- Antialiasing is disabled - splat rendering is fragment bound, so MSAA costs a lot and adds little -->
11
- <pc-app antialias="false" high-resolution="false">
11
+ <pc-app antialias="false" max-pixel-ratio="1">
12
12
  <!-- Hosted so the template stays small. Capture or edit your own splat at
13
13
  https://superspl.at, then drop the .sog or .ply in public/ and point this at it instead -->
14
14
  <pc-asset id="splat" src="https://developer.playcanvas.com/assets/toy-cat.sog"></pc-asset>
@@ -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.