create-playcanvas 0.16.1 → 0.17.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 (61) hide show
  1. package/README.md +16 -16
  2. package/dist/index.mjs +20 -20
  3. package/package.json +2 -2
  4. package/templates/_shared/{ar-placement → ar}/README.md +1 -1
  5. package/templates/_shared/blank/README.md +7 -0
  6. package/templates/_shared/first-person-game/AGENTS.md +11 -0
  7. package/templates/_shared/first-person-game/README.md +21 -0
  8. package/templates/_shared/first-person-game/public/level.glb +0 -0
  9. package/templates/_shared/first-person-game/src/game.css +47 -0
  10. package/templates/_shared/first-person-game/src/game.ts +232 -0
  11. package/templates/_shared/{sprite-game → platformer}/README.md +1 -1
  12. package/templates/_shared/{sprite-game/src/sprite-game.ts → platformer/src/platformer.ts} +3 -3
  13. package/templates/_shared/{third-person-controller → third-person-game}/README.md +1 -1
  14. package/templates/_shared/{vr-starter → vr}/README.md +1 -1
  15. package/templates/engine/{ar-placement → ar}/index.html +2 -2
  16. package/templates/engine/base/package.json +2 -2
  17. package/templates/engine/first-person-game/index.html +26 -0
  18. package/templates/engine/first-person-game/src/main.ts +167 -0
  19. package/templates/engine/{sprite-game → platformer}/index.html +2 -2
  20. package/templates/engine/{sprite-game → platformer}/src/main.ts +2 -2
  21. package/templates/engine/{third-person-controller → third-person-game}/index.html +2 -2
  22. package/templates/engine/{vr-starter → vr}/index.html +2 -2
  23. package/templates/index.ts +14 -14
  24. package/templates/react/{ar-placement → ar}/src/App.tsx +1 -1
  25. package/templates/react/base/package.json +2 -2
  26. package/templates/react/first-person-game/src/App.tsx +140 -0
  27. package/templates/react/{sprite-game → platformer}/src/App.tsx +3 -3
  28. package/templates/react/{third-person-controller → third-person-game}/src/App.tsx +1 -1
  29. package/templates/react/{vr-starter → vr}/src/App.tsx +1 -1
  30. package/templates/web-components/{ar-placement → ar}/index.html +2 -2
  31. package/templates/web-components/base/package.json +2 -2
  32. package/templates/web-components/first-person-game/index.html +80 -0
  33. package/templates/web-components/first-person-game/src/main.ts +74 -0
  34. package/templates/web-components/{sprite-game → platformer}/index.html +2 -2
  35. package/templates/web-components/{sprite-game → platformer}/src/main.ts +2 -2
  36. package/templates/web-components/{third-person-controller → third-person-game}/index.html +2 -2
  37. package/templates/web-components/{vr-starter → vr}/index.html +2 -2
  38. package/templates/_shared/first-person-controller/README.md +0 -7
  39. package/templates/_shared/first-person-controller/src/controller.ts +0 -18
  40. package/templates/_shared/spinning-cube/README.md +0 -7
  41. package/templates/engine/first-person-controller/index.html +0 -19
  42. package/templates/engine/first-person-controller/src/main.ts +0 -143
  43. package/templates/react/first-person-controller/src/App.tsx +0 -115
  44. package/templates/web-components/first-person-controller/index.html +0 -163
  45. package/templates/web-components/first-person-controller/src/main.ts +0 -15
  46. /package/templates/_shared/{ar-placement → ar}/src/ar-placement.ts +0 -0
  47. /package/templates/_shared/{first-person-controller → first-person-game}/src/scripts.d.ts +0 -0
  48. /package/templates/_shared/{third-person-controller → third-person-game}/src/controller.ts +0 -0
  49. /package/templates/_shared/{third-person-controller → third-person-game}/src/scripts.d.ts +0 -0
  50. /package/templates/_shared/{vr-starter → vr}/src/vr.ts +0 -0
  51. /package/templates/_shared/{vr-starter → vr}/src/xr.d.ts +0 -0
  52. /package/templates/engine/{ar-placement → ar}/src/main.ts +0 -0
  53. /package/templates/engine/{spinning-cube → blank}/src/main.ts +0 -0
  54. /package/templates/engine/{third-person-controller → third-person-game}/src/main.ts +0 -0
  55. /package/templates/engine/{vr-starter → vr}/src/main.ts +0 -0
  56. /package/templates/react/{spinning-cube → blank}/src/App.tsx +0 -0
  57. /package/templates/web-components/{ar-placement → ar}/src/main.ts +0 -0
  58. /package/templates/web-components/{spinning-cube → blank}/index.html +0 -0
  59. /package/templates/web-components/{spinning-cube → blank}/src/main.ts +0 -0
  60. /package/templates/web-components/{third-person-controller → third-person-game}/src/main.ts +0 -0
  61. /package/templates/web-components/{vr-starter → vr}/src/main.ts +0 -0
@@ -0,0 +1,232 @@
1
+ // The game, and the file to replace to make this starter your own (see "Make it your own" in
2
+ // README.md). Only the entry file uses it, to attach `Game` and drive the HUD from the `game:state`
3
+ // app event and `CARDS`
4
+ import type { Asset, Entity, RigidBodyComponentSystem } from 'playcanvas';
5
+ import { Script, Vec3 } from 'playcanvas';
6
+ import { FirstPersonController } from 'playcanvas/scripts/esm/first-person-controller.mjs';
7
+
8
+ /**
9
+ * Where a run is: waiting for the first click, in play, held, held because the browser wouldn't
10
+ * lock the mouse, or finished either way.
11
+ */
12
+ export type GameState = 'ready' | 'playing' | 'paused' | 'blocked' | 'won' | 'lost';
13
+
14
+ /** The app events the HUD follows. */
15
+ export type GameEvents = {
16
+ /** Fired by the game whenever its state, the orbs found or the whole seconds left change. */
17
+ 'game:state': (state: GameState, found: number, total: number, seconds: number) => void;
18
+
19
+ /**
20
+ * Fired by the controller as a thumb on the left of the screen steers: where it went down and
21
+ * where it is now, in page pixels, and -1 for all four once it lifts.
22
+ */
23
+ 'joystick:left': (baseX: number, baseY: number, knobX: number, knobY: number) => void;
24
+ };
25
+
26
+ /** What the HUD card says in each state. `null` hides the card. */
27
+ export const CARDS: Record<GameState, { title: string; hint: string } | null> = {
28
+ ready: { title: 'Orb Run', hint: 'Collect every orb before the clock runs out. Click or tap to play' },
29
+ playing: null,
30
+ paused: { title: 'Paused', hint: 'Click or tap to resume' },
31
+ blocked: {
32
+ title: 'Mouse not locked',
33
+ hint: "The browser didn't lock the mouse to the game, and the keyboard and mouse can't steer without it. Click to try again, or open the game in its own browser tab if this keeps happening"
34
+ },
35
+ won: { title: 'You got them all', hint: 'Click or tap to play again' },
36
+ lost: { title: 'Out of time', hint: 'Click or tap to play again' }
37
+ };
38
+
39
+ // seconds a run lasts
40
+ const TIME_LIMIT = 90;
41
+
42
+ // how close the middle of the player must come to an orb to collect it, in metres
43
+ const REACH = 1.2;
44
+
45
+ // the height of the player's middle above the ground it stands on: half the capsule's height
46
+ const PLAYER_HEIGHT = 0.9;
47
+
48
+ // milliseconds after a run ends before a click starts another, so a touch that was steering when
49
+ // the clock ran out doesn't restart it
50
+ const RESTART_DELAY = 800;
51
+
52
+ // stronger than Earth's 9.8, so a jump is quick and lands firmly
53
+ const GRAVITY = -18;
54
+
55
+ // degrees an orb turns each second
56
+ const ORB_SPIN = 90;
57
+
58
+ /**
59
+ * A first-person game: collect every orb in the level before the clock runs out. It builds the level
60
+ * from the `level` asset, gives `player` a first-person controller and talks to the page only
61
+ * through the `game:state` app event.
62
+ *
63
+ * The level is a GLB whose nodes are found by name: a mesh called `level` to look at, one called
64
+ * `collision` to stand on, any number called `orb-…` to collect, and one called `spawn` where the
65
+ * player starts.
66
+ */
67
+ class Game extends Script {
68
+ static scriptName = 'game';
69
+
70
+ /** The level's GLB, as a loaded container asset. */
71
+ level!: Asset<'container'>;
72
+
73
+ /** The player: an entity with a capsule collision and a dynamic rigid body. */
74
+ player!: Entity;
75
+
76
+ /** The camera, a child of the player at eye height. */
77
+ camera!: Entity;
78
+
79
+ state: GameState = 'ready';
80
+ found = 0;
81
+ timeLeft = TIME_LIMIT;
82
+ endedAt = 0;
83
+
84
+ // whether the canvas holds the mouse
85
+ locked = false;
86
+
87
+ orbs: Entity[] = [];
88
+ spawn = new Vec3();
89
+
90
+ initialize() {
91
+ const canvas = this.app.graphicsDevice.canvas;
92
+
93
+ const root = this.level.resource!.instantiateRenderEntity();
94
+ this.entity.addChild(root);
95
+
96
+ // the player stands on the level's collision mesh, which is simpler than the one they see: a
97
+ // flight of stairs is a ramp in it, so the capsule glides up. It is never drawn. A level
98
+ // without one is stood on as it is seen
99
+ const solid = (root.findByName('collision') ?? root.findByName('level')) as Entity;
100
+ solid.addComponent('collision', { type: 'mesh', renderAsset: solid.render!.asset });
101
+ solid.addComponent('rigidbody', { type: 'static' });
102
+ if (solid.name === 'collision') solid.render!.enabled = false;
103
+
104
+ this.orbs = root.find((node) => node.name.startsWith('orb-')) as Entity[];
105
+ this.spawn.copy(root.findByName('spawn')!.getPosition()).add(new Vec3(0, PLAYER_HEIGHT, 0));
106
+
107
+ (this.app.systems.rigidbody as RigidBodyComponentSystem).gravity.set(0, GRAVITY, 0);
108
+ if (!this.player.script) this.player.addComponent('script');
109
+
110
+ canvas.addEventListener('pointerdown', this.onPointerDown);
111
+ document.addEventListener('pointerlockchange', this.onPointerLockChange);
112
+ document.addEventListener('pointerlockerror', this.onPointerLockError);
113
+ document.addEventListener('visibilitychange', this.onVisibilityChange);
114
+ this.on('destroy', () => {
115
+ canvas.removeEventListener('pointerdown', this.onPointerDown);
116
+ document.removeEventListener('pointerlockchange', this.onPointerLockChange);
117
+ document.removeEventListener('pointerlockerror', this.onPointerLockError);
118
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
119
+ this.player.script?.destroy('firstPersonController');
120
+ root.destroy();
121
+ this.app.timeScale = 1;
122
+ });
123
+
124
+ this.reset();
125
+ }
126
+
127
+ /** Puts everything back where a run starts. The first run goes through here too. */
128
+ reset() {
129
+ this.found = 0;
130
+ this.timeLeft = TIME_LIMIT;
131
+ for (const orb of this.orbs) orb.enabled = true;
132
+
133
+ this.player.rigidbody!.teleport(this.spawn);
134
+ this.player.rigidbody!.linearVelocity = Vec3.ZERO;
135
+
136
+ // a new controller each run, so the view starts level and facing into the yard again
137
+ this.player.script!.destroy('firstPersonController');
138
+ this.camera.setLocalEulerAngles(0, 0, 0);
139
+ this.player.script!.create(FirstPersonController, {
140
+ properties: {
141
+ camera: this.camera,
142
+ // tuned for the player's 80 kg under this gravity: a jump rises about 1.8 m, enough
143
+ // for every step and gap in the yard, and the ground speed is a brisk run
144
+ jumpForce: 650,
145
+ speedGround: 55
146
+ }
147
+ });
148
+
149
+ this.setState('ready');
150
+ }
151
+
152
+ setState(state: GameState) {
153
+ this.state = state;
154
+
155
+ // stop the clock outside play: physics, the controller and this script's own update all
156
+ // run on scaled time, so nothing moves and no time is lost
157
+ this.app.timeScale = state === 'playing' ? 1 : 0;
158
+ this.report();
159
+ }
160
+
161
+ report() {
162
+ this.app.fire('game:state', this.state, this.found, this.orbs.length, Math.ceil(this.timeLeft));
163
+ }
164
+
165
+ end(state: 'won' | 'lost') {
166
+ this.endedAt = performance.now();
167
+ this.setState(state);
168
+
169
+ // hand the mouse back, after the state has changed, so losing the lock isn't read as a pause
170
+ if (document.pointerLockElement) document.exitPointerLock();
171
+ }
172
+
173
+ onPointerDown = (event: PointerEvent) => {
174
+ if (this.state === 'playing') return;
175
+
176
+ if (this.state === 'won' || this.state === 'lost') {
177
+ if (performance.now() - this.endedAt < RESTART_DELAY) return;
178
+ this.reset();
179
+
180
+ // the controller asks for the mouse on every click, but the one this reset made joined
181
+ // too late to hear this click, so ask for it here. Only here: asking twice for one
182
+ // click makes the browser report a lock that was lost
183
+ if (event.pointerType === 'mouse') this.app.graphicsDevice.canvas.requestPointerLock();
184
+ }
185
+
186
+ this.setState('playing');
187
+ };
188
+
189
+ // Escape gives the mouse back, which holds the game. Only a lock that was held can be lost: a
190
+ // browser that refuses the lock also reports a change, and touch never has one
191
+ onPointerLockChange = () => {
192
+ const locked = document.pointerLockElement === this.app.graphicsDevice.canvas;
193
+ if (this.locked && !locked && this.state === 'playing') this.setState('paused');
194
+ this.locked = locked;
195
+ };
196
+
197
+ // The controller ignores the keyboard and the mouse until the canvas holds the lock. A browser
198
+ // can refuse it, as a preview pane embedded in an editor does, and Chrome does for a second after
199
+ // Escape. Say so, since otherwise the run starts and nothing steers it
200
+ onPointerLockError = () => {
201
+ if (this.state === 'playing') this.setState('blocked');
202
+ };
203
+
204
+ onVisibilityChange = () => {
205
+ if (document.hidden && this.state === 'playing') this.setState('paused');
206
+ };
207
+
208
+ update(dt: number) {
209
+ if (this.state !== 'playing') return;
210
+
211
+ const position = this.player.getPosition();
212
+
213
+ const seconds = Math.ceil(this.timeLeft);
214
+ this.timeLeft = Math.max(this.timeLeft - dt, 0);
215
+
216
+ const found = this.found;
217
+ for (const orb of this.orbs) {
218
+ if (!orb.enabled) continue;
219
+ orb.rotate(0, ORB_SPIN * dt, 0);
220
+ if (orb.getPosition().distance(position) < REACH) {
221
+ orb.enabled = false;
222
+ this.found++;
223
+ }
224
+ }
225
+
226
+ if (this.found === this.orbs.length) this.end('won');
227
+ else if (this.timeLeft === 0) this.end('lost');
228
+ else if (this.found !== found || Math.ceil(this.timeLeft) !== seconds) this.report();
229
+ }
230
+ }
231
+
232
+ export { Game };
@@ -1,4 +1,4 @@
1
- ## Sprite Game
1
+ ## Platformer
2
2
 
3
3
  A small procedural sprite animation, platforms and a simple side-scrolling controller.
4
4
 
@@ -28,8 +28,8 @@ const PLATFORMS = [
28
28
  { x: 3.6, y: 1.1, width: 2.8 }
29
29
  ];
30
30
 
31
- class SpriteGame extends Script {
32
- static scriptName = 'spriteGame';
31
+ class Platformer extends Script {
32
+ static scriptName = 'platformer';
33
33
 
34
34
  keys = new Set<string>();
35
35
  player!: Entity;
@@ -194,4 +194,4 @@ class SpriteGame extends Script {
194
194
 
195
195
  const mathClamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
196
196
 
197
- export { SpriteGame };
197
+ export { Platformer };
@@ -1,4 +1,4 @@
1
- ## Third-Person Controller
1
+ ## Third-Person Game
2
2
 
3
3
  A physics-backed third-person controller, follow camera and animated robot.
4
4
 
@@ -1,4 +1,4 @@
1
- ## VR Starter
1
+ ## Virtual Reality
2
2
 
3
3
  An immersive VR room with controller models and standard snap-turn and teleport locomotion.
4
4
 
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>AR Placement</title>
7
+ <title>Augmented Reality</title>
8
8
  </head>
9
9
  <body>
10
10
  <canvas id="application-canvas"></canvas>
11
11
  <div class="hud">
12
12
  <section class="panel">
13
- <h1>AR Placement</h1>
13
+ <h1>Augmented Reality</h1>
14
14
  <p id="xr-status">Checking AR support…</p>
15
15
  <div class="controls"><button id="xr-button" disabled>Start AR</button></div>
16
16
  </section>
@@ -15,13 +15,13 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "playcanvas": "^2.24.0"
18
+ "playcanvas": "^2.24.1"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@playcanvas/eslint-config": "3.0.0",
22
22
  "@playcanvas/inspector": "^0.5.0",
23
23
  "eslint": "^9.39.5",
24
- "prettier": "^3.10.0",
24
+ "prettier": "^3.10.1",
25
25
  "typescript": "~6.0.3",
26
26
  "vite": "^8.3.8"
27
27
  },
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>First-Person Game</title>
8
+ </head>
9
+ <body>
10
+ <canvas id="application-canvas"></canvas>
11
+ <div class="hud">
12
+ <header class="status">
13
+ <span id="orbs"></span>
14
+ <span id="time"></span>
15
+ </header>
16
+ <section class="panel card" id="card">
17
+ <h1 id="card-title"></h1>
18
+ <p id="card-hint"></p>
19
+ </section>
20
+ <div class="crosshair"></div>
21
+ <div class="joystick" id="joystick" hidden></div>
22
+ <div class="joystick-knob" id="joystick-knob" hidden></div>
23
+ </div>
24
+ <script type="module" src="/src/main.ts"></script>
25
+ </body>
26
+ </html>
@@ -0,0 +1,167 @@
1
+ import {
2
+ AppBase,
3
+ AppOptions,
4
+ Asset,
5
+ AssetListLoader,
6
+ CameraComponentSystem,
7
+ CollisionComponentSystem,
8
+ Color,
9
+ ContainerHandler,
10
+ Entity,
11
+ FILLMODE_FILL_WINDOW,
12
+ LightComponentSystem,
13
+ RenderComponentSystem,
14
+ RESOLUTION_AUTO,
15
+ RigidBodyComponentSystem,
16
+ ScriptComponentSystem,
17
+ SHADOW_PCF5_32F,
18
+ TextureHandler,
19
+ TONEMAP_ACES2,
20
+ Vec3,
21
+ WasmModule,
22
+ createGraphicsDevice
23
+ } from 'playcanvas';
24
+ import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
25
+
26
+ import { CARDS, Game } from './game';
27
+ import type { GameEvents } from './game';
28
+ import './game.css';
29
+ import './starter.css';
30
+
31
+ // Swap this for your own level - files in public/ are served next to index.html, so use a ./ path.
32
+ // The game finds its nodes by name: a mesh called `level` to look at, one called `collision` to
33
+ // stand on, any number called `orb-…` to collect, and one called `spawn` where the player starts
34
+ const LEVEL_URL = './level.glb';
35
+
36
+ // Physics runs in Ammo, a WebAssembly module, which has to be ready before the app is created
37
+ WasmModule.setConfig('Ammo', {
38
+ glueUrl: './ammo/ammo.wasm.js',
39
+ wasmUrl: './ammo/ammo.wasm.wasm'
40
+ });
41
+ await new Promise<void>((resolve) => {
42
+ WasmModule.getInstance('Ammo', () => resolve());
43
+ });
44
+
45
+ const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
46
+
47
+ const device = await createGraphicsDevice(canvas);
48
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
49
+
50
+ const createOptions = new AppOptions();
51
+ createOptions.graphicsDevice = device;
52
+ createOptions.componentSystems = [
53
+ RenderComponentSystem,
54
+ CameraComponentSystem,
55
+ LightComponentSystem,
56
+ ScriptComponentSystem,
57
+ CollisionComponentSystem,
58
+ RigidBodyComponentSystem
59
+ ];
60
+ createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
61
+
62
+ const app = new AppBase(canvas);
63
+ app.init(createOptions);
64
+
65
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
66
+ app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
67
+ app.setCanvasResolution(RESOLUTION_AUTO);
68
+
69
+ // Ensure canvas is resized when window changes size
70
+ const resize = () => app.resizeCanvas();
71
+ window.addEventListener('resize', resize);
72
+ app.on('destroy', () => {
73
+ window.removeEventListener('resize', resize);
74
+ });
75
+
76
+ // Load the level before starting, so the first frame already has something in it
77
+ const level = new Asset('level', 'container', { url: LEVEL_URL });
78
+
79
+ await new Promise<void>((resolve) => {
80
+ new AssetListLoader([level], app.assets).load(() => resolve());
81
+ });
82
+
83
+ app.start();
84
+
85
+ // The HUD follows the game's state
86
+ const hud = {
87
+ orbs: document.getElementById('orbs')!,
88
+ time: document.getElementById('time')!,
89
+ card: document.getElementById('card')!,
90
+ title: document.getElementById('card-title')!,
91
+ hint: document.getElementById('card-hint')!,
92
+ joystick: document.getElementById('joystick')!,
93
+ knob: document.getElementById('joystick-knob')!
94
+ };
95
+
96
+ const onState: GameEvents['game:state'] = (state, found, total, seconds) => {
97
+ const card = CARDS[state];
98
+ hud.orbs.textContent = `${found} / ${total}`;
99
+ hud.time.textContent = `${seconds}s`;
100
+ hud.card.hidden = !card;
101
+ if (card) {
102
+ hud.title.textContent = card.title;
103
+ hud.hint.textContent = card.hint;
104
+ }
105
+ };
106
+ app.on('game:state', onState);
107
+
108
+ // A thumb on the left of the screen steers, and the circle and knob show where it is
109
+ const onJoystick: GameEvents['joystick:left'] = (baseX, baseY, knobX, knobY) => {
110
+ hud.joystick.hidden = hud.knob.hidden = baseX < 0;
111
+ hud.joystick.style.left = `${baseX}px`;
112
+ hud.joystick.style.top = `${baseY}px`;
113
+ hud.knob.style.left = `${knobX}px`;
114
+ hud.knob.style.top = `${knobY}px`;
115
+ };
116
+ app.on('joystick:left', onJoystick);
117
+
118
+ // The sun is a directional light that the sky keeps pointing the way its own sun does
119
+ const sun = new Entity('sun');
120
+ sun.addComponent('light', {
121
+ type: 'directional',
122
+ intensity: 2,
123
+ castShadows: true,
124
+ // far enough to reach across the yard from any corner
125
+ shadowDistance: 45,
126
+ // one shadow map over that distance is too coarse up close, so split it into cascades
127
+ numCascades: 4,
128
+ shadowResolution: 2048,
129
+ shadowType: SHADOW_PCF5_32F,
130
+ shadowBias: 0.2,
131
+ normalOffsetBias: 0.05
132
+ });
133
+ app.root.addChild(sun);
134
+
135
+ // The procedural sky is both the background and the ambient light. Its default luminance is
136
+ // scene-scale bright, so turn it down to keep the yard out of clipping
137
+ const sky = new Entity('sky');
138
+ sky.addComponent('script');
139
+ sky.script!.create(ProceduralSky, {
140
+ properties: { luminance: 0.3, azimuth: 150, elevation: 45, sunLight: sun }
141
+ });
142
+ app.root.addChild(sky);
143
+
144
+ // The sky lights the shade blue. A warm light from the other side, with no shadows of its own,
145
+ // stands in for the sunlight the ground bounces back, so the shade stays soft and the sand warm
146
+ const fill = new Entity('fill');
147
+ fill.addComponent('light', { type: 'directional', intensity: 0.7, color: new Color().fromString('#ffdbb3') });
148
+ fill.setEulerAngles(60, -30, 0);
149
+ app.root.addChild(fill);
150
+
151
+ // The player is a capsule the physics moves, with the camera at eye height. The game places it
152
+ // at the level's spawn point and gives it a first-person controller
153
+ const player = new Entity('player');
154
+ player.addComponent('collision', { type: 'capsule', radius: 0.45, height: 1.8 });
155
+ player.addComponent('rigidbody', { type: 'dynamic', mass: 80, angularFactor: Vec3.ZERO });
156
+ app.root.addChild(player);
157
+
158
+ // The sky is HDR, so the camera needs tone mapping to bring it back into displayable range
159
+ const camera = new Entity('camera');
160
+ camera.addComponent('camera', { fov: 80, toneMapping: TONEMAP_ACES2 });
161
+ camera.setLocalPosition(0, 0.65, 0);
162
+ player.addChild(camera);
163
+
164
+ const game = new Entity('game');
165
+ game.addComponent('script');
166
+ game.script!.create(Game, { properties: { level, player, camera } });
167
+ app.root.addChild(game);
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Sprite Game</title>
7
+ <title>Platformer</title>
8
8
  </head>
9
9
  <body>
10
10
  <canvas id="application-canvas"></canvas>
11
11
  <div class="hud">
12
12
  <section class="panel">
13
- <h1>Sprite Game</h1>
13
+ <h1>Platformer</h1>
14
14
  <p>Use A/D or the arrow keys to move and Space to jump.</p>
15
15
  </section>
16
16
  </div>
@@ -13,7 +13,7 @@ import {
13
13
  createGraphicsDevice
14
14
  } from 'playcanvas';
15
15
 
16
- import { SpriteGame } from './sprite-game';
16
+ import { Platformer } from './platformer';
17
17
  import './starter.css';
18
18
 
19
19
  const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
@@ -76,5 +76,5 @@ app.root.addChild(grass);
76
76
  const game = new Entity('game');
77
77
  game.setPosition(0, -2.15, 0);
78
78
  game.addComponent('script');
79
- game.script!.create(SpriteGame);
79
+ game.script!.create(Platformer);
80
80
  app.root.addChild(game);
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Third-Person Controller</title>
7
+ <title>Third-Person Game</title>
8
8
  </head>
9
9
  <body>
10
10
  <canvas id="application-canvas"></canvas>
11
11
  <div class="hud">
12
12
  <section class="panel">
13
- <h1>Third-Person Controller</h1>
13
+ <h1>Third-Person Game</h1>
14
14
  <p>Click the scene, then use WASD to move, mouse to orbit and Space to jump.</p>
15
15
  </section>
16
16
  </div>
@@ -4,13 +4,13 @@
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>VR Starter</title>
7
+ <title>Virtual Reality</title>
8
8
  </head>
9
9
  <body>
10
10
  <canvas id="application-canvas"></canvas>
11
11
  <div class="hud">
12
12
  <section class="panel">
13
- <h1>VR Starter</h1>
13
+ <h1>Virtual Reality</h1>
14
14
  <p id="xr-status">Checking VR support…</p>
15
15
  <div class="controls"><button id="xr-button" disabled>Enter VR</button></div>
16
16
  </section>
@@ -36,10 +36,10 @@ export const FEATURES: Record<string, Record<string, string[]>> = {
36
36
  // `outro` follows the commands in the closing message
37
37
  export const STARTERS = [
38
38
  {
39
- name: 'spinning-cube',
40
- display: 'Spinning Cube',
39
+ name: 'blank',
40
+ display: 'Blank',
41
41
  category: 'Basics',
42
- description: 'a rotating cube and a light'
42
+ description: 'just the skeleton, with a rotating cube and a light'
43
43
  },
44
44
  {
45
45
  name: 'model-viewer',
@@ -60,22 +60,22 @@ export const STARTERS = [
60
60
  description: 'product variants, materials and orbit controls'
61
61
  },
62
62
  {
63
- name: 'first-person-controller',
64
- display: 'First-Person Controller',
63
+ name: 'first-person-game',
64
+ display: 'First-Person Game',
65
65
  category: 'Games',
66
- description: 'first-person movement, mouse look and jumping',
66
+ description: 'a timed orb hunt across ramps, stairs and jumps',
67
67
  features: ['physics']
68
68
  },
69
69
  {
70
- name: 'third-person-controller',
71
- display: 'Third-Person Controller',
70
+ name: 'third-person-game',
71
+ display: 'Third-Person Game',
72
72
  category: 'Games',
73
73
  description: 'an animated character, follow camera and locomotion',
74
74
  features: ['physics']
75
75
  },
76
76
  {
77
- name: 'sprite-game',
78
- display: 'Sprite Game',
77
+ name: 'platformer',
78
+ display: 'Platformer',
79
79
  category: 'Games',
80
80
  description: 'an orthographic camera, sprite animation and input'
81
81
  },
@@ -95,14 +95,14 @@ export const STARTERS = [
95
95
  description: 'click to select, gizmos and a scene saved in the browser'
96
96
  },
97
97
  {
98
- name: 'vr-starter',
99
- display: 'VR Starter',
98
+ name: 'vr',
99
+ display: 'Virtual Reality',
100
100
  category: 'XR',
101
101
  description: 'immersive VR, controllers and locomotion'
102
102
  },
103
103
  {
104
- name: 'ar-placement',
105
- display: 'AR Placement',
104
+ name: 'ar',
105
+ display: 'Augmented Reality',
106
106
  category: 'XR',
107
107
  description: 'mobile AR, hit testing and object placement'
108
108
  }
@@ -50,7 +50,7 @@ function App() {
50
50
  </Application>
51
51
  <div className="hud">
52
52
  <section className="panel">
53
- <h1>AR Placement</h1>
53
+ <h1>Augmented Reality</h1>
54
54
  <p id="xr-status">Checking AR support…</p>
55
55
  <div className="controls">
56
56
  <button id="xr-button" disabled>
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/react": "^0.11.7",
19
- "playcanvas": "^2.24.0",
19
+ "playcanvas": "^2.24.1",
20
20
  "react": "^19.3.1",
21
21
  "react-dom": "^19.3.1"
22
22
  },
@@ -27,7 +27,7 @@
27
27
  "@types/react-dom": "^19.3.0",
28
28
  "@vitejs/plugin-react": "^6.3.0",
29
29
  "eslint": "^9.39.5",
30
- "prettier": "^3.10.0",
30
+ "prettier": "^3.10.1",
31
31
  "typescript": "~6.0.3",
32
32
  "vite": "^8.3.8"
33
33
  },