create-playcanvas 0.4.0 → 0.5.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.
- package/README.md +5 -0
- package/dist/index.mjs +42 -41
- package/package.json +2 -1
- package/skills/add-effects/SKILL.md +81 -0
- package/skills/apply-conventions/SKILL.md +73 -0
- package/skills/assemble-scene/SKILL.md +53 -0
- package/skills/assemble-scene/references/direct-engine.md +16 -0
- package/skills/assemble-scene/references/react.md +24 -0
- package/skills/assemble-scene/references/web-components.md +19 -0
- package/skills/build-app/SKILL.md +41 -0
- package/skills/build-app/references/direct-engine.md +12 -0
- package/skills/build-app/references/react.md +20 -0
- package/skills/build-app/references/web-components.md +55 -0
- package/skills/build-hud/SKILL.md +36 -0
- package/skills/calibrate-model/SKILL.md +65 -0
- package/skills/calibrate-model/references/direct-engine.md +22 -0
- package/skills/calibrate-model/references/react.md +27 -0
- package/skills/calibrate-model/references/web-components.md +19 -0
- package/skills/configure-animation/SKILL.md +50 -0
- package/skills/configure-animation/references/direct-engine.md +32 -0
- package/skills/configure-animation/references/react.md +32 -0
- package/skills/configure-animation/references/web-components.md +13 -0
- package/skills/find-examples/SKILL.md +79 -0
- package/skills/find-examples/references/direct-engine.md +15 -0
- package/skills/find-examples/references/react.md +17 -0
- package/skills/find-examples/references/web-components.md +17 -0
- package/skills/inspect-glb/SKILL.md +72 -0
- package/skills/inspect-glb/scripts/inspect.mjs +239 -0
- package/skills/light-scene/SKILL.md +84 -0
- package/skills/manage-game-state/SKILL.md +48 -0
- package/skills/reuse-scripts/SKILL.md +59 -0
- package/skills/reuse-scripts/references/direct-engine.md +41 -0
- package/skills/reuse-scripts/references/react.md +28 -0
- package/skills/reuse-scripts/references/web-components.md +27 -0
- package/templates/engine/base/.prettierignore +2 -0
- package/templates/engine/base/README.md +4 -0
- package/templates/engine/base/eslint.config.mjs +1 -1
- package/templates/react/base/.prettierignore +2 -0
- package/templates/react/base/README.md +4 -0
- package/templates/react/base/eslint.config.mjs +1 -0
- package/templates/web-components/base/.prettierignore +2 -0
- package/templates/web-components/base/README.md +4 -0
- package/templates/web-components/base/eslint.config.mjs +1 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// zero-dep glb metadata inspector. bounds come from decoded vertex positions, not accessor min/max,
|
|
3
|
+
// so a rotated node reports its true extent and models can be placed touching.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
|
|
6
|
+
const GLB_MAGIC = 0x46546c67;
|
|
7
|
+
const JSON_CHUNK = 0x4e4f534a;
|
|
8
|
+
const BIN_CHUNK = 0x004e4942;
|
|
9
|
+
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
10
|
+
|
|
11
|
+
// componentType -> byte size, signedness and normalized divisor (glTF 2.0 table 3.1 / 5.1)
|
|
12
|
+
const COMPONENTS = {
|
|
13
|
+
5120: { size: 1, signed: true, norm: 127, read: (b, o) => b.readInt8(o) },
|
|
14
|
+
5121: { size: 1, signed: false, norm: 255, read: (b, o) => b.readUInt8(o) },
|
|
15
|
+
5122: { size: 2, signed: true, norm: 32767, read: (b, o) => b.readInt16LE(o) },
|
|
16
|
+
5123: { size: 2, signed: false, norm: 65535, read: (b, o) => b.readUInt16LE(o) },
|
|
17
|
+
5125: { size: 4, signed: false, norm: 4294967295, read: (b, o) => b.readUInt32LE(o) },
|
|
18
|
+
5126: { size: 4, signed: true, norm: 1, read: (b, o) => b.readFloatLE(o) }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mul = (a, b) => {
|
|
22
|
+
const o = new Array(16);
|
|
23
|
+
for (let c = 0; c < 4; c++) {
|
|
24
|
+
for (let r = 0; r < 4; r++) {
|
|
25
|
+
o[c * 4 + r] =
|
|
26
|
+
a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return o;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const compose = ([tx, ty, tz], [x, y, z, w], [sx, sy, sz]) => {
|
|
33
|
+
const x2 = x + x,
|
|
34
|
+
y2 = y + y,
|
|
35
|
+
z2 = z + z;
|
|
36
|
+
const xx = x * x2,
|
|
37
|
+
xy = x * y2,
|
|
38
|
+
xz = x * z2;
|
|
39
|
+
const yy = y * y2,
|
|
40
|
+
yz = y * z2,
|
|
41
|
+
zz = z * z2;
|
|
42
|
+
const wx = w * x2,
|
|
43
|
+
wy = w * y2,
|
|
44
|
+
wz = w * z2;
|
|
45
|
+
return [
|
|
46
|
+
(1 - (yy + zz)) * sx,
|
|
47
|
+
(xy + wz) * sx,
|
|
48
|
+
(xz - wy) * sx,
|
|
49
|
+
0,
|
|
50
|
+
(xy - wz) * sy,
|
|
51
|
+
(1 - (xx + zz)) * sy,
|
|
52
|
+
(yz + wx) * sy,
|
|
53
|
+
0,
|
|
54
|
+
(xz + wy) * sz,
|
|
55
|
+
(yz - wx) * sz,
|
|
56
|
+
(1 - (xx + yy)) * sz,
|
|
57
|
+
0,
|
|
58
|
+
tx,
|
|
59
|
+
ty,
|
|
60
|
+
tz,
|
|
61
|
+
1
|
|
62
|
+
];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const xform = (m, [x, y, z]) => [
|
|
66
|
+
m[0] * x + m[4] * y + m[8] * z + m[12],
|
|
67
|
+
m[1] * x + m[5] * y + m[9] * z + m[13],
|
|
68
|
+
m[2] * x + m[6] * y + m[10] * z + m[14]
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// + 0 normalizes -0 so a floored model reports 0, not -0
|
|
72
|
+
const round = (v) => Math.round(v * 1e4) / 1e4 + 0;
|
|
73
|
+
|
|
74
|
+
// buffer 0 of a GLB is the BIN chunk; other buffers are external files or data URIs
|
|
75
|
+
const bufferBytes = (g, bin, index) => {
|
|
76
|
+
const uri = g.buffers?.[index]?.uri;
|
|
77
|
+
if (uri === undefined) return bin;
|
|
78
|
+
const base64 = /^data:[^,]*;base64,(.*)$/s.exec(uri);
|
|
79
|
+
return base64 ? Buffer.from(base64[1], 'base64') : null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// widen min/max with every decoded POSITION transformed by matrix. returns null on success,
|
|
83
|
+
// otherwise a reason the vertices could not be read so the caller can fall back and say so.
|
|
84
|
+
const accumulate = (g, bin, accessor, matrix, min, max) => {
|
|
85
|
+
const acc = g.accessors?.[accessor];
|
|
86
|
+
if (!acc) return 'no-position-accessor';
|
|
87
|
+
if (acc.sparse) return 'sparse-accessor';
|
|
88
|
+
if (acc.bufferView === undefined) return 'no-vertex-data';
|
|
89
|
+
const comp = COMPONENTS[acc.componentType];
|
|
90
|
+
if (!comp || acc.type !== 'VEC3') return 'unsupported-accessor';
|
|
91
|
+
const view = g.bufferViews?.[acc.bufferView];
|
|
92
|
+
if (!view) return 'unsupported-accessor';
|
|
93
|
+
if (view.extensions?.EXT_meshopt_compression) return 'meshopt-compressed';
|
|
94
|
+
const bytes = bufferBytes(g, bin, view.buffer ?? 0);
|
|
95
|
+
if (!bytes) return 'external-buffer';
|
|
96
|
+
const stride = view.byteStride ?? comp.size * 3;
|
|
97
|
+
const base = (view.byteOffset ?? 0) + (acc.byteOffset ?? 0);
|
|
98
|
+
if (!acc.count || base + (acc.count - 1) * stride + comp.size * 3 > bytes.length) return 'truncated-buffer';
|
|
99
|
+
const scale = acc.normalized ? comp.norm : 1;
|
|
100
|
+
for (let v = 0; v < acc.count; v++) {
|
|
101
|
+
const o = base + v * stride;
|
|
102
|
+
let x = comp.read(bytes, o) / scale;
|
|
103
|
+
let y = comp.read(bytes, o + comp.size) / scale;
|
|
104
|
+
let z = comp.read(bytes, o + comp.size * 2) / scale;
|
|
105
|
+
if (acc.normalized && comp.signed) {
|
|
106
|
+
if (x < -1) x = -1;
|
|
107
|
+
if (y < -1) y = -1;
|
|
108
|
+
if (z < -1) z = -1;
|
|
109
|
+
}
|
|
110
|
+
// inlined to avoid allocating a vector per vertex on million-vertex meshes
|
|
111
|
+
const px = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
|
|
112
|
+
const py = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
|
|
113
|
+
const pz = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
|
|
114
|
+
if (px < min[0]) min[0] = px;
|
|
115
|
+
if (py < min[1]) min[1] = py;
|
|
116
|
+
if (pz < min[2]) min[2] = pz;
|
|
117
|
+
if (px > max[0]) max[0] = px;
|
|
118
|
+
if (py > max[1]) max[1] = py;
|
|
119
|
+
if (pz > max[2]) max[2] = pz;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// loose fallback: bound the 8 corners of the accessor's local box. over-estimates a mesh that does
|
|
125
|
+
// not fill its box when the node rotation is not a multiple of 90 degrees.
|
|
126
|
+
const accumulateCorners = (acc, matrix, min, max) => {
|
|
127
|
+
if (!acc?.min || !acc?.max) return;
|
|
128
|
+
for (let c = 0; c < 8; c++) {
|
|
129
|
+
const point = xform(matrix, [
|
|
130
|
+
c & 1 ? acc.max[0] : acc.min[0],
|
|
131
|
+
c & 2 ? acc.max[1] : acc.min[1],
|
|
132
|
+
c & 4 ? acc.max[2] : acc.min[2]
|
|
133
|
+
]);
|
|
134
|
+
for (let k = 0; k < 3; k++) {
|
|
135
|
+
if (point[k] < min[k]) min[k] = point[k];
|
|
136
|
+
if (point[k] > max[k]) max[k] = point[k];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const inspectGlb = (buf) => {
|
|
142
|
+
if (buf.length < 20 || buf.readUInt32LE(0) !== GLB_MAGIC || buf.readUInt32LE(16) !== JSON_CHUNK) {
|
|
143
|
+
throw new Error('not a GLB (or JSON chunk missing)');
|
|
144
|
+
}
|
|
145
|
+
const jsonLength = buf.readUInt32LE(12);
|
|
146
|
+
const g = JSON.parse(buf.toString('utf8', 20, 20 + jsonLength));
|
|
147
|
+
const binAt = 20 + jsonLength;
|
|
148
|
+
const bin =
|
|
149
|
+
binAt + 8 <= buf.length && buf.readUInt32LE(binAt + 4) === BIN_CHUNK
|
|
150
|
+
? buf.subarray(binAt + 8, binAt + 8 + buf.readUInt32LE(binAt))
|
|
151
|
+
: null;
|
|
152
|
+
const nodes = g.nodes ?? [];
|
|
153
|
+
const nodeName = (i) => nodes[i]?.name ?? `node_${i}`;
|
|
154
|
+
const parents = new Array(nodes.length).fill(-1);
|
|
155
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
156
|
+
for (const child of nodes[i].children ?? []) parents[child] = i;
|
|
157
|
+
}
|
|
158
|
+
const nodePath = (i) => {
|
|
159
|
+
const names = [];
|
|
160
|
+
for (; i >= 0; i = parents[i]) names.unshift(nodeName(i));
|
|
161
|
+
return names.join('/');
|
|
162
|
+
};
|
|
163
|
+
const children = new Set(nodes.flatMap((n) => n.children ?? []));
|
|
164
|
+
const roots = g.scenes?.[g.scene ?? 0]?.nodes ?? nodes.map((_, i) => i).filter((i) => !children.has(i));
|
|
165
|
+
|
|
166
|
+
const world = new Array(nodes.length);
|
|
167
|
+
const walk = (i, parent) => {
|
|
168
|
+
const n = nodes[i];
|
|
169
|
+
const local = n.matrix ?? compose(n.translation ?? [0, 0, 0], n.rotation ?? [0, 0, 0, 1], n.scale ?? [1, 1, 1]);
|
|
170
|
+
world[i] = mul(parent, local);
|
|
171
|
+
for (const child of n.children ?? []) walk(child, world[i]);
|
|
172
|
+
};
|
|
173
|
+
for (const root of roots) walk(root, IDENTITY);
|
|
174
|
+
|
|
175
|
+
const min = [Infinity, Infinity, Infinity];
|
|
176
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
177
|
+
const notes = new Set();
|
|
178
|
+
let meshNodes = 0;
|
|
179
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
180
|
+
if (nodes[i].mesh === undefined || !world[i]) continue;
|
|
181
|
+
meshNodes++;
|
|
182
|
+
// glTF 2.0: "the transform of the skinned mesh node MUST be ignored" - joints drive the pose
|
|
183
|
+
const matrix = nodes[i].skin === undefined ? world[i] : IDENTITY;
|
|
184
|
+
for (const prim of g.meshes?.[nodes[i].mesh]?.primitives ?? []) {
|
|
185
|
+
const note = prim.extensions?.KHR_draco_mesh_compression
|
|
186
|
+
? 'draco-compressed'
|
|
187
|
+
: accumulate(g, bin, prim.attributes?.POSITION, matrix, min, max);
|
|
188
|
+
if (!note) continue;
|
|
189
|
+
notes.add(note);
|
|
190
|
+
accumulateCorners(g.accessors?.[prim.attributes?.POSITION], matrix, min, max);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const ok = min[0] !== Infinity;
|
|
195
|
+
return {
|
|
196
|
+
aabb: ok ? { min: min.map(round), max: max.map(round) } : null,
|
|
197
|
+
dims: ok ? max.map((v, i) => round(v - min[i])) : null,
|
|
198
|
+
center: ok ? max.map((v, i) => round((v + min[i]) / 2)) : null,
|
|
199
|
+
groundOffset: ok ? round(-min[1]) : null,
|
|
200
|
+
boundsSource: ok ? (notes.size ? 'accessor-minmax' : 'vertices') : null,
|
|
201
|
+
...(notes.size ? { boundsNotes: [...notes].sort() } : {}),
|
|
202
|
+
nodes: nodes.length,
|
|
203
|
+
nodePaths: nodes.map((_, i) => nodePath(i)).sort(),
|
|
204
|
+
meshNodes,
|
|
205
|
+
materials: (g.materials ?? []).map((m, i) => m.name ?? `material_${i}`),
|
|
206
|
+
clips: (g.animations ?? []).map((a, i) => ({
|
|
207
|
+
name: a.name ?? `clip_${i}`,
|
|
208
|
+
duration: round(Math.max(0, ...a.samplers.map((s) => g.accessors?.[s.input]?.max?.[0] ?? 0)))
|
|
209
|
+
})),
|
|
210
|
+
joints: [...new Set((g.skins ?? []).flatMap((s) => s.joints ?? []).map(nodeName))].sort(),
|
|
211
|
+
animationTargets: [
|
|
212
|
+
...new Set(
|
|
213
|
+
(g.animations ?? [])
|
|
214
|
+
.flatMap((a) => a.channels ?? [])
|
|
215
|
+
.flatMap((c) =>
|
|
216
|
+
c.target?.node === undefined ? [] : [`${nodePath(c.target.node)}.${c.target.path}`]
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
].sort(),
|
|
220
|
+
skinned: !!g.skins?.length
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (import.meta.main ?? import.meta.filename === process.argv[1]) {
|
|
225
|
+
const files = process.argv.slice(2);
|
|
226
|
+
if (!files.length) {
|
|
227
|
+
console.error('usage: node inspect.mjs <file.glb> [more.glb ...]');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
// report per-file failures so one unreadable file cannot discard a whole glob
|
|
231
|
+
const out = files.map((file) => {
|
|
232
|
+
try {
|
|
233
|
+
return { file, ...inspectGlb(fs.readFileSync(file)) };
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return { file, error: e.message };
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
console.log(JSON.stringify(out.length === 1 ? out[0] : out, null, 2));
|
|
239
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: light-scene
|
|
3
|
+
description: Use when a PlayCanvas scene must read as a deliberate cinematic image rather than a flat asset preview, to set key and ambient lighting, sky and image-based lighting, shadows, tone mapping and exposure, and a restrained post-process grade of bloom, fog, and colour.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cinematic rendering
|
|
7
|
+
|
|
8
|
+
A polished frame is lit and graded, not just populated. Treat lighting, sky, shadows, tone mapping,
|
|
9
|
+
and post as one pass over the assembled scene, and tune every value against a real screenshot rather
|
|
10
|
+
than from memory.
|
|
11
|
+
|
|
12
|
+
Prefer shipped building blocks over hand-written shaders and passes. Discover them with the
|
|
13
|
+
`reuse-scripts` skill and adapt the closest official example with `find-examples`:
|
|
14
|
+
|
|
15
|
+
- the core `CameraFrame` helper for tone mapping, ambient occlusion, bloom, vignette, and grade;
|
|
16
|
+
- a procedural sky and image-based light for ambient colour and reflections;
|
|
17
|
+
- a shadow catcher for grounding subjects that have no lit floor.
|
|
18
|
+
|
|
19
|
+
For an outdoor scene with water, stop before authoring the scene and read both `reuse-scripts` and
|
|
20
|
+
`find-examples`. Use `graphics/water.example.mjs` as the initial composition recipe: it already
|
|
21
|
+
joins the production `Water` script, sky, sun, `CameraFrame`, water layer, scene depth, and
|
|
22
|
+
reflection pipeline. Get that complete frame rendering before replacing its assets or tuning its
|
|
23
|
+
look. A custom normal-mapped material, tessellated plane, or reflection-coloured mesh is not the
|
|
24
|
+
baseline and does not substitute for `Water` when the brief asks for reflective water.
|
|
25
|
+
|
|
26
|
+
Keep the two named production integrations visible in the implementation:
|
|
27
|
+
|
|
28
|
+
- import and create `Water` from `playcanvas/scripts/esm/water.mjs` on the water entity, preserving
|
|
29
|
+
the example's required render component, camera reference, water layer, depth map, and textures;
|
|
30
|
+
- construct `CameraFrame` from `playcanvas` for the gameplay camera, set a deliberate tone map and
|
|
31
|
+
restrained grade, call `update()`, and destroy it with the application.
|
|
32
|
+
|
|
33
|
+
Do not continue to cosmetic tuning while either integration is missing or diagnostics report a
|
|
34
|
+
shader, texture, or framebuffer error.
|
|
35
|
+
|
|
36
|
+
## Light and expose
|
|
37
|
+
|
|
38
|
+
- Establish one dominant key light with a clear direction and warm or cool intent, then add soft
|
|
39
|
+
fill or ambient so shadows are not crushed to black. A single flat ambient with no key reads as an
|
|
40
|
+
asset gallery.
|
|
41
|
+
- Choose a physically based tone mapping and a deliberate exposure, then balance light intensities to
|
|
42
|
+
that exposure, not the reverse.
|
|
43
|
+
- Drive ambient and reflections from the sky or an environment map, not a constant colour, so
|
|
44
|
+
materials pick up their surroundings.
|
|
45
|
+
- Tune the HDR stack in isolation before styling it. Begin with neutral tone mapping, scene exposure
|
|
46
|
+
`1`, procedural-sky luminance near its official-example value, bloom off, grading neutral, fog off,
|
|
47
|
+
and reflection/specular strengths at their defaults. Capture a screenshot, then change one class of
|
|
48
|
+
value at a time. Do not simultaneously raise key intensity, sky luminance, exposure, reflection,
|
|
49
|
+
saturation, fog brightness, and bloom.
|
|
50
|
+
- Treat linked procedural skies as the owner of the sun light's direction, colour, and intensity.
|
|
51
|
+
Set the light's base intensity before assigning it to the sky and tune the sky elevation/azimuth;
|
|
52
|
+
do not also hand-author the light transform or colour as though they remained independent.
|
|
53
|
+
- Preserve the source assets' albedo colours. If diffuse texture colours collapse toward white or a
|
|
54
|
+
single warm tint, reduce the HDR stack before compensating with darker material colours.
|
|
55
|
+
|
|
56
|
+
## Shadow and ground
|
|
57
|
+
|
|
58
|
+
- Enable shadows on the key light and size the shadow area and resolution to the framed subject, not
|
|
59
|
+
the whole world. Soften the penumbra rather than only raising resolution.
|
|
60
|
+
- Ground subjects with a contact or catcher shadow when the surface beneath them is not itself lit.
|
|
61
|
+
|
|
62
|
+
## Grade with restraint
|
|
63
|
+
|
|
64
|
+
- Bloom, fog, vignette, and colour grade unify a frame; overdone bloom and heavy fog destroy
|
|
65
|
+
silhouette and depth. Start subtle and increase only against the screenshot. `CameraFrame` bloom
|
|
66
|
+
intensity is documented for the `0` to `0.1` range; stay inside it.
|
|
67
|
+
- Match fog colour and density to the sky so the horizon stays coherent.
|
|
68
|
+
- Preserve surface detail through the brightest reflection path. If glare clips a large part of the
|
|
69
|
+
frame to white or a reflection becomes an opaque black blob, rebalance exposure, light intensity,
|
|
70
|
+
reflection strength, and shadow bias before adding more post-processing.
|
|
71
|
+
- Compare both the brightest surface and the shadow-facing side of the hero subject in the same
|
|
72
|
+
screenshot. Reject the grade if either broad highlight clipping or crushed-black silhouette hides
|
|
73
|
+
texture detail. A mechanically valid frame is not visually complete while either failure remains.
|
|
74
|
+
|
|
75
|
+
## Prove the look
|
|
76
|
+
|
|
77
|
+
Tune against pixels, not numbers. Capture the intended framing with an image-returning browser
|
|
78
|
+
screenshot tool and inspect the returned image, as `apply-conventions` and the local agent guide
|
|
79
|
+
require; a saved filepath is not evidence. Confirm the subject is exposed and readable, shadows are
|
|
80
|
+
present but not crushed, and the grade is restrained, from the real gameplay camera at the target
|
|
81
|
+
resolution. Confirm the post-process camera keeps rendering as it orbits and pitches.
|
|
82
|
+
|
|
83
|
+
Choose the authoring surface with the `build-app` skill and set lights, camera, and scene settings
|
|
84
|
+
through its own primitives before reaching into the Engine.
|
|
@@ -0,0 +1,48 @@
|
|
|
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.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reuse-scripts
|
|
3
|
+
description: Use before implementing non-core PlayCanvas behavior to discover and reuse production scripts shipped with the installed Engine, reproduce their official example integration, and verify the result against the installed version.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Engine scripts
|
|
7
|
+
|
|
8
|
+
The installed `playcanvas` package ships production scripts under `scripts/esm/**`. Discover the
|
|
9
|
+
current set rather than maintaining a stale list:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
rg 'static scriptName =' node_modules/playcanvas/scripts/esm \
|
|
13
|
+
| sed "s|.*/scripts/esm/||; s|:.*static scriptName = ['\"]| -> |; s|['\"].*||" \
|
|
14
|
+
| sort
|
|
15
|
+
```
|
|
16
|
+
|
|
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.
|
|
23
|
+
|
|
24
|
+
Use a relevant shipped script as the default implementation. If it cannot own the whole feature,
|
|
25
|
+
preserve its input signs, angular damping, bounds, and lifecycle invariants in the custom portion
|
|
26
|
+
instead of replacing them from memory.
|
|
27
|
+
|
|
28
|
+
## Reproduce the reference integration
|
|
29
|
+
|
|
30
|
+
Treat the selected script source and its closest official example as complementary references:
|
|
31
|
+
|
|
32
|
+
1. Read the source for exports, properties, defaults, fallbacks, required components, and lifecycle.
|
|
33
|
+
2. Read the example for assets, entity references, mesh requirements, layer ordering, scene settings,
|
|
34
|
+
and render-pipeline setup.
|
|
35
|
+
3. Reproduce the smallest working baseline before customizing it. Preserve defaults that are not
|
|
36
|
+
deliberately changed, and tune one property group at a time.
|
|
37
|
+
4. After a rendered frame, fail on console, shader, or missing-asset diagnostics. Exercise the
|
|
38
|
+
behavior with real input where applicable and inspect returned screenshots from representative
|
|
39
|
+
views at the final backbuffer density.
|
|
40
|
+
5. Compare the result with the official example when visual quality matters. Keep iterating or
|
|
41
|
+
report the gap; a running fallback is not proof of correct integration.
|
|
42
|
+
|
|
43
|
+
If no matching example exists, state that and derive the integration from installed source instead
|
|
44
|
+
of inventing it from memory.
|
|
45
|
+
|
|
46
|
+
For an outdoor water scene, start from `graphics/water.example.mjs` as one integration recipe. It
|
|
47
|
+
combines `Water`, `ProceduralSky`, `CameraControls`, `CameraFrame`, a dedicated water layer, scene
|
|
48
|
+
depth, normal and caustics textures, and conservative rendering defaults. Establish that complete
|
|
49
|
+
baseline before adapting its camera, assets, time of day, or water tuning.
|
|
50
|
+
|
|
51
|
+
## Preserve grouped defaults
|
|
52
|
+
|
|
53
|
+
Grouped property updates differ by authoring surface. Read the selected reference and preserve
|
|
54
|
+
defaults that are not being changed.
|
|
55
|
+
|
|
56
|
+
Read exactly one reference matching the code being edited:
|
|
57
|
+
[direct Engine](references/direct-engine.md), [React](references/react.md), or
|
|
58
|
+
[Web Components](references/web-components.md). Choose from imports and markup, not installed
|
|
59
|
+
dependencies alone.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Direct Engine projects
|
|
2
|
+
|
|
3
|
+
Import the named script class and attach it to the owning entity:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
7
|
+
|
|
8
|
+
camera.addComponent('script');
|
|
9
|
+
const controls = camera.script?.create(CameraControls);
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Some production helpers are core Engine exports rather than scripts. Follow the matching example's
|
|
13
|
+
construction style instead of forcing them through `script.create`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { CameraFrame } from 'playcanvas';
|
|
17
|
+
|
|
18
|
+
const frame = new CameraFrame(app, camera.camera!);
|
|
19
|
+
frame.rendering.sceneDepthMap = true;
|
|
20
|
+
frame.update();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Keep script constructors at module scope. `create` takes two distinct option keys that are not
|
|
24
|
+
interchangeable: `properties` is assigned straight onto the instance, while `attributes` supplies
|
|
25
|
+
declared `@attribute` fields. Passing the wrong one silently does nothing.
|
|
26
|
+
|
|
27
|
+
Prefer mutating an existing grouped property when only a few fields change:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
|
|
31
|
+
|
|
32
|
+
const sky = skyEntity.script?.create(ProceduralSky);
|
|
33
|
+
if (sky) sky.sunLight = sun;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If the installed script subpaths have no declarations, declare only the patterns the project uses:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
declare module 'playcanvas/scripts/esm/*.mjs';
|
|
40
|
+
declare module 'playcanvas/scripts/esm/*/*.mjs';
|
|
41
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# React projects
|
|
2
|
+
|
|
3
|
+
Import the named script class and attach it through React:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { Entity } from '@playcanvas/react';
|
|
7
|
+
import { Camera, Script } from '@playcanvas/react/components';
|
|
8
|
+
import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
|
|
9
|
+
|
|
10
|
+
<Entity name="Camera" position={[4, 1, 4]}>
|
|
11
|
+
<Camera />
|
|
12
|
+
<Script script={CameraControls} />
|
|
13
|
+
</Entity>;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Keep imported or custom script constructors at module scope. Declaring a class inside a component
|
|
17
|
+
creates a new identity on render and can recreate the script.
|
|
18
|
+
|
|
19
|
+
Props on `<Script>` become script properties. Pass complete grouped properties or use a ref to
|
|
20
|
+
mutate the existing group after mount. Verify the current component props in the installed
|
|
21
|
+
`@playcanvas/react` declarations.
|
|
22
|
+
|
|
23
|
+
If the installed Engine script subpaths have no declarations, add:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
declare module 'playcanvas/scripts/esm/*.mjs';
|
|
27
|
+
declare module 'playcanvas/scripts/esm/*/*.mjs';
|
|
28
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Web Components projects
|
|
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-scripts>
|
|
12
|
+
<pc-script name="cameraControls" move-speed="4"></pc-script>
|
|
13
|
+
</pc-scripts>
|
|
14
|
+
</pc-entity>
|
|
15
|
+
</pc-scene>
|
|
16
|
+
</pc-app>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `name` must equal the class's static `scriptName`. Extra kebab-case attributes map to camelCase
|
|
20
|
+
script properties. Use the `attributes` JSON attribute for nested or reserved properties; it
|
|
21
|
+
recursively merges partial grouped objects into the script's defaults. Arrays are not merged element
|
|
22
|
+
by element, so an array in the JSON replaces the script's default array whole, and a kebab-case
|
|
23
|
+
attribute always wins over the same key inside the JSON.
|
|
24
|
+
|
|
25
|
+
Use `asset:`, `entity:`, `vec2:`, `vec3:`, `vec4:`, or `color:` prefixes where a property needs a
|
|
26
|
+
typed reference. Verify the selected script's export, property names, and defaults against the
|
|
27
|
+
installed Engine file.
|
|
@@ -30,6 +30,10 @@ Open <http://localhost:5173>. Edit the files under `src/` and save to see the sc
|
|
|
30
30
|
|
|
31
31
|
Run `npm run build` to generate a deployable static site in `dist/`.
|
|
32
32
|
|
|
33
|
+
## Agent skills
|
|
34
|
+
|
|
35
|
+
This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
|
|
36
|
+
|
|
33
37
|
## Further reading
|
|
34
38
|
|
|
35
39
|
- [PlayCanvas Engine manual](https://developer.playcanvas.com/user-manual/engine/)
|
|
@@ -30,6 +30,10 @@ Open <http://localhost:5173>. Edit the files under `src/` and save to see the sc
|
|
|
30
30
|
|
|
31
31
|
Run `npm run build` to generate a deployable static site in `dist/`.
|
|
32
32
|
|
|
33
|
+
## Agent skills
|
|
34
|
+
|
|
35
|
+
This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
|
|
36
|
+
|
|
33
37
|
## Further reading
|
|
34
38
|
|
|
35
39
|
- [PlayCanvas React manual](https://developer.playcanvas.com/user-manual/react/)
|
|
@@ -58,6 +58,10 @@ JetBrains IDEs pick it up automatically. For VS Code, add to `.vscode/settings.j
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
## Agent skills
|
|
62
|
+
|
|
63
|
+
This project includes [`@playcanvas/skills`](https://github.com/playcanvas/skills) under `.claude/skills/` and `.agents/skills/`, so Claude Code, Codex and Cursor pick up PlayCanvas-specific workflows automatically.
|
|
64
|
+
|
|
61
65
|
## Further reading
|
|
62
66
|
|
|
63
67
|
- [PlayCanvas Web Components manual](https://developer.playcanvas.com/user-manual/web-components/)
|