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.
- package/README.md +16 -16
- package/dist/index.mjs +20 -20
- package/package.json +2 -2
- package/templates/_shared/{ar-placement → ar}/README.md +1 -1
- package/templates/_shared/blank/README.md +7 -0
- package/templates/_shared/first-person-game/AGENTS.md +11 -0
- package/templates/_shared/first-person-game/README.md +21 -0
- package/templates/_shared/first-person-game/public/level.glb +0 -0
- package/templates/_shared/first-person-game/src/game.css +47 -0
- package/templates/_shared/first-person-game/src/game.ts +232 -0
- package/templates/_shared/{sprite-game → platformer}/README.md +1 -1
- package/templates/_shared/{sprite-game/src/sprite-game.ts → platformer/src/platformer.ts} +3 -3
- package/templates/_shared/{third-person-controller → third-person-game}/README.md +1 -1
- package/templates/_shared/{vr-starter → vr}/README.md +1 -1
- package/templates/engine/ar/index.html +20 -0
- package/templates/engine/{ar-placement → ar}/src/main.ts +22 -12
- package/templates/engine/base/package.json +2 -2
- package/templates/engine/first-person-game/index.html +26 -0
- package/templates/engine/first-person-game/src/main.ts +167 -0
- package/templates/engine/platformer/index.html +19 -0
- package/templates/engine/{sprite-game → platformer}/src/main.ts +21 -13
- package/templates/engine/product-configurator/index.html +30 -0
- package/templates/engine/product-configurator/src/main.ts +33 -32
- package/templates/engine/third-person-game/index.html +19 -0
- package/templates/engine/{third-person-controller → third-person-game}/src/main.ts +16 -11
- package/templates/engine/vr/index.html +20 -0
- package/templates/engine/{vr-starter → vr}/src/main.ts +22 -12
- package/templates/index.ts +14 -14
- package/templates/react/{ar-placement → ar}/src/App.tsx +1 -1
- package/templates/react/base/package.json +2 -2
- package/templates/react/first-person-game/src/App.tsx +140 -0
- package/templates/react/{sprite-game → platformer}/src/App.tsx +3 -3
- package/templates/react/{third-person-controller → third-person-game}/src/App.tsx +1 -1
- package/templates/react/{vr-starter → vr}/src/App.tsx +1 -1
- package/templates/web-components/{ar-placement → ar}/index.html +2 -2
- package/templates/web-components/base/package.json +2 -2
- package/templates/web-components/first-person-game/index.html +80 -0
- package/templates/web-components/first-person-game/src/main.ts +74 -0
- package/templates/web-components/{sprite-game → platformer}/index.html +2 -2
- package/templates/web-components/{sprite-game → platformer}/src/main.ts +2 -2
- package/templates/web-components/{third-person-controller → third-person-game}/index.html +2 -2
- package/templates/web-components/{vr-starter → vr}/index.html +2 -2
- package/templates/_shared/first-person-controller/README.md +0 -7
- package/templates/_shared/first-person-controller/src/controller.ts +0 -18
- package/templates/_shared/spinning-cube/README.md +0 -7
- package/templates/engine/first-person-controller/src/main.ts +0 -138
- package/templates/react/first-person-controller/src/App.tsx +0 -115
- package/templates/web-components/first-person-controller/index.html +0 -163
- package/templates/web-components/first-person-controller/src/main.ts +0 -15
- /package/templates/_shared/{ar-placement → ar}/src/ar-placement.ts +0 -0
- /package/templates/_shared/{first-person-controller → first-person-game}/src/scripts.d.ts +0 -0
- /package/templates/_shared/{third-person-controller → third-person-game}/src/controller.ts +0 -0
- /package/templates/_shared/{third-person-controller → third-person-game}/src/scripts.d.ts +0 -0
- /package/templates/_shared/{vr-starter → vr}/src/vr.ts +0 -0
- /package/templates/_shared/{vr-starter → vr}/src/xr.d.ts +0 -0
- /package/templates/engine/{spinning-cube → blank}/src/main.ts +0 -0
- /package/templates/react/{spinning-cube → blank}/src/App.tsx +0 -0
- /package/templates/web-components/{ar-placement → ar}/src/main.ts +0 -0
- /package/templates/web-components/{spinning-cube → blank}/index.html +0 -0
- /package/templates/web-components/{spinning-cube → blank}/src/main.ts +0 -0
- /package/templates/web-components/{third-person-controller → third-person-game}/src/main.ts +0 -0
- /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(
|
|
38
|
+
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
|
48
39
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
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());
|
package/templates/index.ts
CHANGED
|
@@ -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: '
|
|
40
|
-
display: '
|
|
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-
|
|
64
|
-
display: 'First-Person
|
|
63
|
+
name: 'first-person-game',
|
|
64
|
+
display: 'First-Person Game',
|
|
65
65
|
category: 'Games',
|
|
66
|
-
description: '
|
|
66
|
+
description: 'a timed orb hunt across ramps, stairs and jumps',
|
|
67
67
|
features: ['physics']
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
|
-
name: 'third-person-
|
|
71
|
-
display: 'Third-Person
|
|
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: '
|
|
78
|
-
display: '
|
|
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
|
|
99
|
-
display: '
|
|
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
|
|
105
|
-
display: '
|
|
104
|
+
name: 'ar',
|
|
105
|
+
display: 'Augmented Reality',
|
|
106
106
|
category: 'XR',
|
|
107
107
|
description: 'mobile AR, hit testing and object placement'
|
|
108
108
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@playcanvas/react": "^0.11.7",
|
|
19
|
-
"playcanvas": "^2.24.
|
|
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.
|
|
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 {
|
|
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={
|
|
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>
|
|
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
|
|
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>
|
|
@@ -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>
|
|
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>
|
|
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.
|
|
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.
|
|
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>
|