create-playcanvas 0.16.0 → 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 (62) 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/index.html +20 -0
  16. package/templates/engine/{ar-placement → ar}/src/main.ts +22 -12
  17. package/templates/engine/base/package.json +2 -2
  18. package/templates/engine/first-person-game/index.html +26 -0
  19. package/templates/engine/first-person-game/src/main.ts +167 -0
  20. package/templates/engine/platformer/index.html +19 -0
  21. package/templates/engine/{sprite-game → platformer}/src/main.ts +21 -13
  22. package/templates/engine/product-configurator/index.html +30 -0
  23. package/templates/engine/product-configurator/src/main.ts +33 -32
  24. package/templates/engine/third-person-game/index.html +19 -0
  25. package/templates/engine/{third-person-controller → third-person-game}/src/main.ts +16 -11
  26. package/templates/engine/vr/index.html +20 -0
  27. package/templates/engine/{vr-starter → vr}/src/main.ts +22 -12
  28. package/templates/index.ts +14 -14
  29. package/templates/react/{ar-placement → ar}/src/App.tsx +1 -1
  30. package/templates/react/base/package.json +2 -2
  31. package/templates/react/first-person-game/src/App.tsx +140 -0
  32. package/templates/react/{sprite-game → platformer}/src/App.tsx +3 -3
  33. package/templates/react/{third-person-controller → third-person-game}/src/App.tsx +1 -1
  34. package/templates/react/{vr-starter → vr}/src/App.tsx +1 -1
  35. package/templates/web-components/{ar-placement → ar}/index.html +2 -2
  36. package/templates/web-components/base/package.json +2 -2
  37. package/templates/web-components/first-person-game/index.html +80 -0
  38. package/templates/web-components/first-person-game/src/main.ts +74 -0
  39. package/templates/web-components/{sprite-game → platformer}/index.html +2 -2
  40. package/templates/web-components/{sprite-game → platformer}/src/main.ts +2 -2
  41. package/templates/web-components/{third-person-controller → third-person-game}/index.html +2 -2
  42. package/templates/web-components/{vr-starter → vr}/index.html +2 -2
  43. package/templates/_shared/first-person-controller/README.md +0 -7
  44. package/templates/_shared/first-person-controller/src/controller.ts +0 -18
  45. package/templates/_shared/spinning-cube/README.md +0 -7
  46. package/templates/engine/first-person-controller/src/main.ts +0 -138
  47. package/templates/react/first-person-controller/src/App.tsx +0 -115
  48. package/templates/web-components/first-person-controller/index.html +0 -163
  49. package/templates/web-components/first-person-controller/src/main.ts +0 -15
  50. /package/templates/_shared/{ar-placement → ar}/src/ar-placement.ts +0 -0
  51. /package/templates/_shared/{first-person-controller → first-person-game}/src/scripts.d.ts +0 -0
  52. /package/templates/_shared/{third-person-controller → third-person-game}/src/controller.ts +0 -0
  53. /package/templates/_shared/{third-person-controller → third-person-game}/src/scripts.d.ts +0 -0
  54. /package/templates/_shared/{vr-starter → vr}/src/vr.ts +0 -0
  55. /package/templates/_shared/{vr-starter → vr}/src/xr.d.ts +0 -0
  56. /package/templates/engine/{spinning-cube → blank}/src/main.ts +0 -0
  57. /package/templates/react/{spinning-cube → blank}/src/App.tsx +0 -0
  58. /package/templates/web-components/{ar-placement → ar}/src/main.ts +0 -0
  59. /package/templates/web-components/{spinning-cube → blank}/index.html +0 -0
  60. /package/templates/web-components/{spinning-cube → blank}/src/main.ts +0 -0
  61. /package/templates/web-components/{third-person-controller → third-person-game}/src/main.ts +0 -0
  62. /package/templates/web-components/{vr-starter → vr}/src/main.ts +0 -0
@@ -29,33 +29,38 @@ import './product.css';
29
29
  import './starter.css';
30
30
 
31
31
  const PRODUCT_URL = 'https://developer.playcanvas.com/assets/lambo.glb';
32
- const PAINTS = [
33
- { name: 'Crimson', color: '#d6293e', shade: '#9b1731' },
34
- { name: 'Arctic', color: '#d8e4e9', shade: '#8fa8b5' },
35
- { name: 'Volt', color: '#9acb34', shade: '#3c742d' }
36
- ];
37
32
  const CAMERA_FOCUS = new Vec3(0, 0.8, 0);
38
33
  const CAMERA_PITCH = new Vec2(-85, -4);
39
34
 
40
- document.body.insertAdjacentHTML(
41
- 'beforeend',
42
- `<div class="hud"><section class="panel"><h1>Product Configurator</h1><p>Choose a paint finish, then drag to orbit.</p><div class="controls variants" id="paints">${PAINTS.map(({ name, color }, i) => `<button aria-pressed="${i === 0}" data-color="${color}"><span class="finish" style="--color:${color}"></span>${name}</button>`).join('')}</div></section></div>`
43
- );
44
-
45
35
  const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
36
+
46
37
  const device = await createGraphicsDevice(canvas);
47
- device.maxPixelRatio = Math.min(device.maxPixelRatio, 2);
38
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
48
39
 
49
- const options = new AppOptions();
50
- options.graphicsDevice = device;
51
- options.componentSystems = [RenderComponentSystem, CameraComponentSystem, LightComponentSystem, ScriptComponentSystem];
52
- options.resourceHandlers = [TextureHandler, ContainerHandler];
40
+ const createOptions = new AppOptions();
41
+ createOptions.graphicsDevice = device;
42
+ createOptions.componentSystems = [
43
+ RenderComponentSystem,
44
+ CameraComponentSystem,
45
+ LightComponentSystem,
46
+ ScriptComponentSystem
47
+ ];
48
+ createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
53
49
 
54
50
  const app = new AppBase(canvas);
55
- app.init(options);
51
+ app.init(createOptions);
52
+
53
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
56
54
  app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
57
55
  app.setCanvasResolution(RESOLUTION_AUTO);
58
56
 
57
+ // Ensure canvas is resized when window changes size
58
+ const resize = () => app.resizeCanvas();
59
+ window.addEventListener('resize', resize);
60
+ app.on('destroy', () => {
61
+ window.removeEventListener('resize', resize);
62
+ });
63
+
59
64
  const product = new Asset('car', 'container', { url: PRODUCT_URL });
60
65
  await new Promise<void>((resolve) => new AssetListLoader([product], app.assets).load(() => resolve()));
61
66
 
@@ -82,10 +87,18 @@ const paint = body[0].material!.clone() as StandardMaterial;
82
87
  const shade = panels[0].material!.clone() as StandardMaterial;
83
88
  body.forEach((mesh) => (mesh.material = paint));
84
89
  panels.forEach((mesh) => (mesh.material = shade));
85
- paint.diffuse.fromString(PAINTS[0].color);
86
- shade.diffuse.fromString(PAINTS[0].shade);
87
- paint.update();
88
- shade.update();
90
+
91
+ // Each paint button in index.html carries its finish's two colors
92
+ const buttons = document.querySelectorAll<HTMLButtonElement>('#paints button');
93
+ const choose = (button: HTMLButtonElement) => {
94
+ paint.diffuse.fromString(button.dataset.color!);
95
+ shade.diffuse.fromString(button.dataset.shade!);
96
+ paint.update();
97
+ shade.update();
98
+ buttons.forEach((item) => (item.ariaPressed = String(item === button)));
99
+ };
100
+ buttons.forEach((button) => (button.onclick = () => choose(button)));
101
+ choose(buttons[0]);
89
102
 
90
103
  const sky = new Entity('sky');
91
104
  sky.addComponent('script');
@@ -141,15 +154,3 @@ light.addComponent('light', {
141
154
  });
142
155
  light.setEulerAngles(45, -145, 0);
143
156
  app.root.addChild(light);
144
-
145
- document.querySelectorAll<HTMLButtonElement>('#paints button').forEach((button, i, buttons) => {
146
- button.onclick = () => {
147
- paint.diffuse.fromString(PAINTS[i].color);
148
- shade.diffuse.fromString(PAINTS[i].shade);
149
- paint.update();
150
- shade.update();
151
- buttons.forEach((item, j) => (item.ariaPressed = String(i === j)));
152
- };
153
- });
154
-
155
- window.addEventListener('resize', () => app.resizeCanvas());
@@ -0,0 +1,19 @@
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>Third-Person Game</title>
8
+ </head>
9
+ <body>
10
+ <canvas id="application-canvas"></canvas>
11
+ <div class="hud">
12
+ <section class="panel">
13
+ <h1>Third-Person Game</h1>
14
+ <p>Click the scene, then use WASD to move, mouse to orbit and Space to jump.</p>
15
+ </section>
16
+ </div>
17
+ <script type="module" src="/src/main.ts"></script>
18
+ </body>
19
+ </html>
@@ -49,16 +49,14 @@ await new Promise<void>((resolve) => {
49
49
  WasmModule.getInstance('Ammo', () => resolve());
50
50
  });
51
51
 
52
- document.body.insertAdjacentHTML(
53
- 'beforeend',
54
- '<div class="hud"><section class="panel"><h1>Third-Person Controller</h1><p>Click the scene, then use WASD to move, mouse to orbit and Space to jump.</p></section></div>'
55
- );
56
-
57
52
  const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
53
+
58
54
  const device = await createGraphicsDevice(canvas);
59
- const options = new AppOptions();
60
- options.graphicsDevice = device;
61
- options.componentSystems = [
55
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
56
+
57
+ const createOptions = new AppOptions();
58
+ createOptions.graphicsDevice = device;
59
+ createOptions.componentSystems = [
62
60
  RenderComponentSystem,
63
61
  CameraComponentSystem,
64
62
  LightComponentSystem,
@@ -68,10 +66,19 @@ options.componentSystems = [
68
66
  ];
69
67
 
70
68
  const app = new AppBase(canvas);
71
- app.init(options);
69
+ app.init(createOptions);
72
70
  app.start();
71
+
72
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
73
73
  app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
74
74
  app.setCanvasResolution(RESOLUTION_AUTO);
75
+
76
+ // Ensure canvas is resized when window changes size
77
+ const resize = () => app.resizeCanvas();
78
+ window.addEventListener('resize', resize);
79
+ app.on('destroy', () => {
80
+ window.removeEventListener('resize', resize);
81
+ });
75
82
  app.scene.ambientLight = new Color(0.28, 0.32, 0.38);
76
83
 
77
84
  const groundMaterial = new StandardMaterial();
@@ -216,5 +223,3 @@ light.addComponent('light', {
216
223
  });
217
224
  light.setEulerAngles(45, 30, 0);
218
225
  app.root.addChild(light);
219
-
220
- window.addEventListener('resize', () => app.resizeCanvas());
@@ -0,0 +1,20 @@
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>Virtual Reality</title>
8
+ </head>
9
+ <body>
10
+ <canvas id="application-canvas"></canvas>
11
+ <div class="hud">
12
+ <section class="panel">
13
+ <h1>Virtual Reality</h1>
14
+ <p id="xr-status">Checking VR support…</p>
15
+ <div class="controls"><button id="xr-button" disabled>Enter VR</button></div>
16
+ </section>
17
+ </div>
18
+ <script type="module" src="/src/main.ts"></script>
19
+ </body>
20
+ </html>
@@ -17,23 +17,35 @@ import {
17
17
  import './starter.css';
18
18
  import { setupVr } from './vr';
19
19
 
20
- document.body.insertAdjacentHTML(
21
- 'beforeend',
22
- '<div class="hud"><section class="panel"><h1>VR Starter</h1><p id="xr-status">Checking VR support…</p><div class="controls"><button id="xr-button" disabled>Enter VR</button></div></section></div>'
23
- );
24
-
25
20
  const canvas = document.getElementById('application-canvas') as HTMLCanvasElement;
21
+
26
22
  const device = await createGraphicsDevice(canvas);
27
- const options = new AppOptions();
28
- options.graphicsDevice = device;
29
- options.componentSystems = [RenderComponentSystem, CameraComponentSystem, LightComponentSystem, ScriptComponentSystem];
30
- options.xr = XrManager;
23
+ device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
24
+
25
+ const createOptions = new AppOptions();
26
+ createOptions.graphicsDevice = device;
27
+ createOptions.componentSystems = [
28
+ RenderComponentSystem,
29
+ CameraComponentSystem,
30
+ LightComponentSystem,
31
+ ScriptComponentSystem
32
+ ];
33
+ createOptions.xr = XrManager;
31
34
 
32
35
  const app = new AppBase(canvas);
33
- app.init(options);
36
+ app.init(createOptions);
34
37
  app.start();
38
+
39
+ // Set the canvas to fill the window and automatically change resolution to be the same as the canvas size
35
40
  app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
36
41
  app.setCanvasResolution(RESOLUTION_AUTO);
42
+
43
+ // Ensure canvas is resized when window changes size
44
+ const resize = () => app.resizeCanvas();
45
+ window.addEventListener('resize', resize);
46
+ app.on('destroy', () => {
47
+ window.removeEventListener('resize', resize);
48
+ });
37
49
  app.scene.ambientLight = new Color(0.32, 0.34, 0.38);
38
50
 
39
51
  const material = (color: Color) => {
@@ -97,5 +109,3 @@ light.addComponent('light', {
97
109
  });
98
110
  light.setEulerAngles(45, 30, 0);
99
111
  app.root.addChild(light);
100
-
101
- window.addEventListener('resize', () => app.resizeCanvas());
@@ -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
  },
@@ -0,0 +1,140 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Collision, Light, RigidBody, Script } from '@playcanvas/react/components';
3
+ import { useAppEvent, useModel, usePhysics } from '@playcanvas/react/hooks';
4
+ import type { Entity as PcEntity } from 'playcanvas';
5
+ import { SHADOW_PCF5_32F, TONEMAP_ACES2 } from 'playcanvas';
6
+ import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
7
+ import { useCallback, useRef, useState } from 'react';
8
+
9
+ import { CARDS, Game } from './game';
10
+ import type { GameEvents, GameState } from './game';
11
+ import './game.css';
12
+ import './starter.css';
13
+
14
+ // Swap this for your own level - files in public/ are served next to index.html, so use a ./ path.
15
+ // The game finds its nodes by name: a mesh called `level` to look at, one called `collision` to
16
+ // stand on, any number called `orb-…` to collect, and one called `spawn` where the player starts
17
+ const LEVEL_URL = './level.glb';
18
+
19
+ function Scene({
20
+ onState,
21
+ onJoystick
22
+ }: {
23
+ onState: GameEvents['game:state'];
24
+ onJoystick: GameEvents['joystick:left'];
25
+ }) {
26
+ const [sun, setSun] = useState<PcEntity | null>(null);
27
+ const [player, setPlayer] = useState<PcEntity | null>(null);
28
+ const [camera, setCamera] = useState<PcEntity | null>(null);
29
+ const { asset: level } = useModel(LEVEL_URL);
30
+ const { isPhysicsLoaded } = usePhysics();
31
+
32
+ useAppEvent<GameEvents>('game:state', onState);
33
+ useAppEvent<GameEvents>('joystick:left', onJoystick);
34
+
35
+ return (
36
+ <>
37
+ {/* The sun is a directional light that the sky keeps pointing the way its own sun does.
38
+ Its shadows reach 45 m, far enough to cross the yard from any corner, split into
39
+ four cascades because one shadow map over that distance is too coarse up close */}
40
+ <Entity name="sun" ref={setSun}>
41
+ <Light
42
+ type="directional"
43
+ intensity={2}
44
+ castShadows
45
+ shadowDistance={45}
46
+ numCascades={4}
47
+ shadowResolution={2048}
48
+ shadowType={SHADOW_PCF5_32F}
49
+ shadowBias={0.2}
50
+ normalOffsetBias={0.05}
51
+ />
52
+ </Entity>
53
+
54
+ {/* The procedural sky is both the background and the ambient light. Its default
55
+ luminance is scene-scale bright, so turn it down to keep the yard out of clipping */}
56
+ <Entity name="sky">
57
+ {sun && <Script script={ProceduralSky} luminance={0.3} azimuth={150} elevation={45} sunLight={sun} />}
58
+ </Entity>
59
+
60
+ {/* The sky lights the shade blue. A warm light from the other side, with no shadows of
61
+ its own, stands in for the sunlight the ground bounces back, so the shade stays soft
62
+ and the sand warm */}
63
+ <Entity name="fill" rotation={[60, -30, 0]}>
64
+ <Light type="directional" intensity={0.7} color="#ffdbb3" />
65
+ </Entity>
66
+
67
+ {/* The player is a capsule the physics moves, with the camera at eye height. The game
68
+ places it at the level's spawn point and gives it a first-person controller */}
69
+ <Entity name="player" ref={setPlayer}>
70
+ <Collision type="capsule" radius={0.45} height={1.8} />
71
+ <RigidBody type="dynamic" mass={80} angularFactor={[0, 0, 0]} />
72
+ {/* The sky is HDR, so the camera needs tone mapping to bring it back into
73
+ displayable range */}
74
+ <Entity name="camera" ref={setCamera} position={[0, 0.65, 0]}>
75
+ <Camera fov={80} toneMapping={TONEMAP_ACES2} />
76
+ </Entity>
77
+ </Entity>
78
+
79
+ {/* The game needs the level loaded, physics running and the player in the scene */}
80
+ {level && isPhysicsLoaded && player && camera && (
81
+ <Entity name="game">
82
+ <Script script={Game} level={level} player={player} camera={camera} />
83
+ </Entity>
84
+ )}
85
+ </>
86
+ );
87
+ }
88
+
89
+ function App() {
90
+ const [hud, setHud] = useState<{ state: GameState; found: number; total: number; seconds: number }>();
91
+ const joystick = useRef<HTMLDivElement>(null);
92
+ const knob = useRef<HTMLDivElement>(null);
93
+
94
+ // The HUD follows the game's state
95
+ const onState = useCallback<GameEvents['game:state']>((state, found, total, seconds) => {
96
+ setHud({ state, found, total, seconds });
97
+ }, []);
98
+
99
+ // A thumb on the left of the screen steers, and the circle and knob show where it is. They move
100
+ // with every touch event, so they are placed directly rather than through React state
101
+ const onJoystick = useCallback<GameEvents['joystick:left']>((baseX, baseY, knobX, knobY) => {
102
+ if (!joystick.current || !knob.current) return;
103
+ joystick.current.hidden = knob.current.hidden = baseX < 0;
104
+ joystick.current.style.left = `${baseX}px`;
105
+ joystick.current.style.top = `${baseY}px`;
106
+ knob.current.style.left = `${knobX}px`;
107
+ knob.current.style.top = `${knobY}px`;
108
+ }, []);
109
+
110
+ const card = hud && CARDS[hud.state];
111
+
112
+ return (
113
+ <>
114
+ <Application usePhysics>
115
+ <Scene onState={onState} onJoystick={onJoystick} />
116
+ </Application>
117
+ <div className="hud">
118
+ {hud && (
119
+ <header className="status">
120
+ <span>
121
+ {hud.found} / {hud.total}
122
+ </span>
123
+ <span>{hud.seconds}s</span>
124
+ </header>
125
+ )}
126
+ {card && (
127
+ <section className="panel card">
128
+ <h1>{card.title}</h1>
129
+ <p>{card.hint}</p>
130
+ </section>
131
+ )}
132
+ <div className="crosshair" />
133
+ <div className="joystick" ref={joystick} hidden />
134
+ <div className="joystick-knob" ref={knob} hidden />
135
+ </div>
136
+ </>
137
+ );
138
+ }
139
+
140
+ export default App;
@@ -2,7 +2,7 @@ import { Application, Entity } from '@playcanvas/react';
2
2
  import { Camera, Render, Script } from '@playcanvas/react/components';
3
3
  import { useMaterial } from '@playcanvas/react/hooks';
4
4
 
5
- import { SpriteGame } from './sprite-game';
5
+ import { Platformer } from './platformer';
6
6
  import './starter.css';
7
7
 
8
8
  function Scene() {
@@ -21,7 +21,7 @@ function Scene() {
21
21
  <Render type="box" material={grass} />
22
22
  </Entity>
23
23
  <Entity name="game" position={[0, -2.15, 0]}>
24
- <Script script={SpriteGame} />
24
+ <Script script={Platformer} />
25
25
  </Entity>
26
26
  </>
27
27
  );
@@ -35,7 +35,7 @@ function App() {
35
35
  </Application>
36
36
  <div className="hud">
37
37
  <section className="panel">
38
- <h1>Sprite Game</h1>
38
+ <h1>Platformer</h1>
39
39
  <p>Use A/D or the arrow keys to move and Space to jump.</p>
40
40
  </section>
41
41
  </div>
@@ -185,7 +185,7 @@ function App() {
185
185
  </Application>
186
186
  <div className="hud">
187
187
  <section className="panel">
188
- <h1>Third-Person Controller</h1>
188
+ <h1>Third-Person Game</h1>
189
189
  <p>Click the scene, then use WASD to move, mouse to orbit and Space to jump.</p>
190
190
  </section>
191
191
  </div>
@@ -83,7 +83,7 @@ function App() {
83
83
  </Application>
84
84
  <div className="hud">
85
85
  <section className="panel">
86
- <h1>VR Starter</h1>
86
+ <h1>Virtual Reality</h1>
87
87
  <p id="xr-status">Checking VR support…</p>
88
88
  <div className="controls">
89
89
  <button id="xr-button" disabled>
@@ -4,7 +4,7 @@
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
  <pc-app alpha>
@@ -34,7 +34,7 @@
34
34
  </pc-app>
35
35
  <div class="hud">
36
36
  <section class="panel">
37
- <h1>AR Placement</h1>
37
+ <h1>Augmented Reality</h1>
38
38
  <p id="xr-status">Checking AR support…</p>
39
39
  <div class="controls"><button id="xr-button" disabled>Start AR</button></div>
40
40
  </section>
@@ -16,13 +16,13 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@playcanvas/web-components": "^0.30.0",
19
- "playcanvas": "^2.24.0"
19
+ "playcanvas": "^2.24.1"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@playcanvas/eslint-config": "3.0.0",
23
23
  "@playcanvas/inspector": "^0.5.0",
24
24
  "eslint": "^9.39.5",
25
- "prettier": "^3.10.0",
25
+ "prettier": "^3.10.1",
26
26
  "typescript": "~6.0.3",
27
27
  "vite": "^8.3.8"
28
28
  },
@@ -0,0 +1,80 @@
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
+ <pc-app>
11
+ <!-- Swap this for your own level - files in public/ are served next to index.html, so use a ./ path.
12
+ The game finds its nodes by name: a mesh called `level` to look at, one called
13
+ `collision` to stand on, any number called `orb-…` to collect, and one called `spawn`
14
+ where the player starts -->
15
+ <pc-asset id="level" src="./level.glb"></pc-asset>
16
+
17
+ <!-- Physics runs in Ammo, a WebAssembly module -->
18
+ <pc-wasm name="Ammo" glue="./ammo/ammo.wasm.js" wasm="./ammo/ammo.wasm.wasm"></pc-wasm>
19
+
20
+ <pc-scene>
21
+ <!-- The sun is a directional light that the sky keeps pointing the way its own sun does.
22
+ Its shadows reach 45 m, far enough to cross the yard from any corner, split into
23
+ four cascades because one shadow map over that distance is too coarse up close -->
24
+ <pc-entity name="sun">
25
+ <pc-light
26
+ type="directional"
27
+ intensity="2"
28
+ cast-shadows
29
+ shadow-distance="45"
30
+ num-cascades="4"
31
+ shadow-resolution="2048"
32
+ shadow-type="pcf5-32f"
33
+ shadow-bias="0.2"
34
+ normal-offset-bias="0.05"
35
+ ></pc-light>
36
+ </pc-entity>
37
+
38
+ <!-- The procedural sky is both the background and the ambient light. The script is
39
+ attached in src/main.ts -->
40
+ <pc-entity name="sky"></pc-entity>
41
+
42
+ <!-- The sky lights the shade blue. A warm light from the other side, with no shadows of
43
+ its own, stands in for the sunlight the ground bounces back, so the shade stays soft
44
+ and the sand warm -->
45
+ <pc-entity name="fill" rotation="60 -30 0">
46
+ <pc-light type="directional" intensity="0.7" color="#ffdbb3"></pc-light>
47
+ </pc-entity>
48
+
49
+ <!-- The player is a capsule the physics moves, with the camera at eye height. The game
50
+ places it at the level's spawn point and gives it a first-person controller -->
51
+ <pc-entity name="player">
52
+ <pc-collision type="capsule" radius="0.45" height="1.8"></pc-collision>
53
+ <pc-rigid-body type="dynamic" mass="80" angular-factor="0 0 0"></pc-rigid-body>
54
+ <!-- The sky is HDR, so the camera needs tone mapping to bring it back into
55
+ displayable range -->
56
+ <pc-entity name="camera" position="0 0.65 0">
57
+ <pc-camera fov="80" tonemap="aces2"></pc-camera>
58
+ </pc-entity>
59
+ </pc-entity>
60
+
61
+ <!-- The game is attached in src/main.ts -->
62
+ <pc-entity name="game"></pc-entity>
63
+ </pc-scene>
64
+ </pc-app>
65
+ <div class="hud">
66
+ <header class="status">
67
+ <span id="orbs"></span>
68
+ <span id="time"></span>
69
+ </header>
70
+ <section class="panel card" id="card">
71
+ <h1 id="card-title"></h1>
72
+ <p id="card-hint"></p>
73
+ </section>
74
+ <div class="crosshair"></div>
75
+ <div class="joystick" id="joystick" hidden></div>
76
+ <div class="joystick-knob" id="joystick-knob" hidden></div>
77
+ </div>
78
+ <script type="module" src="/src/main.ts"></script>
79
+ </body>
80
+ </html>