create-playcanvas 0.10.0 → 0.11.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 +12 -2
- package/dist/index.mjs +23 -23
- package/package.json +2 -2
- package/templates/_shared/third-person-controller/src/controller.ts +25 -3
- package/templates/_shared/web-lens/README.md +42 -0
- package/templates/_shared/web-lens/package.json +9 -0
- package/templates/_shared/web-lens/scripts/package-lens.mjs +231 -0
- package/templates/_shared/web-lens/src/dodge-game.ts +271 -0
- package/templates/_shared/web-lens/src/lens.css +155 -0
- package/templates/_shared/web-lens/src/lens.ts +91 -0
- package/templates/engine/base/package.json +4 -4
- package/templates/engine/third-person-controller/src/main.ts +10 -5
- package/templates/engine/web-lens/index.html +26 -0
- package/templates/engine/web-lens/src/main.ts +89 -0
- package/templates/engine/web-lens/vite.config.ts +44 -0
- package/templates/index.ts +6 -0
- package/templates/react/base/package.json +7 -7
- package/templates/react/third-person-controller/src/App.tsx +5 -2
- package/templates/react/web-lens/index.html +12 -0
- package/templates/react/web-lens/src/App.tsx +85 -0
- package/templates/react/web-lens/vite.config.ts +46 -0
- package/templates/web-components/base/package.json +4 -4
- package/templates/web-components/third-person-controller/index.html +17 -2
- package/templates/web-components/web-lens/index.html +36 -0
- package/templates/web-components/web-lens/src/main.ts +47 -0
- package/templates/web-components/web-lens/vite.config.ts +44 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
|
|
3
|
+
// Approximates the Web Lens sandbox, which allows resources from the Lens itself and nothing else
|
|
4
|
+
const LENS_SANDBOX = [
|
|
5
|
+
"default-src 'self' data: blob:",
|
|
6
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
|
|
7
|
+
"style-src 'self' 'unsafe-inline'",
|
|
8
|
+
"frame-src 'none'",
|
|
9
|
+
"object-src 'none'",
|
|
10
|
+
"base-uri 'none'",
|
|
11
|
+
"form-action 'none'"
|
|
12
|
+
].join('; ');
|
|
13
|
+
|
|
14
|
+
// https://vite.dev/config/
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
// Snapchat serves a Lens from its own origin, so every URL in the build must be relative
|
|
17
|
+
base: './',
|
|
18
|
+
build: {
|
|
19
|
+
// npm run package zips this folder and writes the zip beside it, inside the ignored dist/
|
|
20
|
+
outDir: 'dist/lens',
|
|
21
|
+
// source maps only add to the 8 MB upload
|
|
22
|
+
sourcemap: false
|
|
23
|
+
},
|
|
24
|
+
preview: {
|
|
25
|
+
// npm run start serves the build under the Lens rules, so a request for anything outside
|
|
26
|
+
// the zip fails in the browser console as it would in Snapchat
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Security-Policy': LENS_SANDBOX,
|
|
29
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
30
|
+
'Cross-Origin-Embedder-Policy': 'require-corp'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
plugins: [
|
|
34
|
+
{
|
|
35
|
+
// Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
|
|
36
|
+
name: 'playcanvas-inspector',
|
|
37
|
+
apply: 'serve',
|
|
38
|
+
config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
|
|
39
|
+
transformIndexHtml: () => [
|
|
40
|
+
{ tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
});
|
package/templates/index.ts
CHANGED
|
@@ -90,6 +90,12 @@ export const STARTERS = [
|
|
|
90
90
|
category: 'Games',
|
|
91
91
|
description: 'an orthographic camera, sprite animation and input'
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'web-lens',
|
|
95
|
+
display: 'Snapchat Web Lens',
|
|
96
|
+
category: 'Games',
|
|
97
|
+
description: 'a portrait game with Snapchat leaderboards, packaged for upload'
|
|
98
|
+
},
|
|
93
99
|
{
|
|
94
100
|
name: 'scene-editor',
|
|
95
101
|
display: 'Scene Editor',
|
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@playcanvas/react": "^0.11.7",
|
|
19
|
-
"playcanvas": "^2.23.
|
|
20
|
-
"react": "^19.3.
|
|
21
|
-
"react-dom": "^19.3.
|
|
19
|
+
"playcanvas": "^2.23.3",
|
|
20
|
+
"react": "^19.3.1",
|
|
21
|
+
"react-dom": "^19.3.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@playcanvas/eslint-config": "3.0.0-beta.8",
|
|
25
|
-
"@playcanvas/inspector": "^0.
|
|
25
|
+
"@playcanvas/inspector": "^0.3.0",
|
|
26
26
|
"@types/react": "^19.3.0",
|
|
27
27
|
"@types/react-dom": "^19.3.0",
|
|
28
|
-
"@vitejs/plugin-react": "^6.1.
|
|
28
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
29
29
|
"eslint": "^9.39.5",
|
|
30
|
-
"prettier": "^3.9.
|
|
30
|
+
"prettier": "^3.9.10",
|
|
31
31
|
"typescript": "~6.0.3",
|
|
32
|
-
"vite": "^8.3.
|
|
32
|
+
"vite": "^8.3.5"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=22.23.2"
|
|
@@ -79,6 +79,8 @@ function Pine({
|
|
|
79
79
|
scale={[size * width, size * height, size * width]}
|
|
80
80
|
>
|
|
81
81
|
<Render type="cone" material={i === 1 ? tips : leaves} />
|
|
82
|
+
{/* A trigger: it stops the camera's line of sight but the player walks through it */}
|
|
83
|
+
<Collision type="cone" radius={(size * width) / 2} height={size * height} />
|
|
82
84
|
</Entity>
|
|
83
85
|
))}
|
|
84
86
|
</>
|
|
@@ -139,9 +141,10 @@ function Scene() {
|
|
|
139
141
|
<Block name="fallen-log" position={[0, 0.35, 6]} scale={[3.8, 0.7, 0.7]} material={wood} />
|
|
140
142
|
<Block name="stump" position={[-1.8, 0.4, -2.2]} scale={[0.9, 0.8, 0.9]} material={wood} />
|
|
141
143
|
<Entity name="player" ref={player} position={[0, 1.1, 0]}>
|
|
142
|
-
|
|
144
|
+
{/* A box rather than a capsule, so the player stands on ledges instead of sliding off them */}
|
|
145
|
+
<Collision type="box" halfExtents={[0.4, 1.2, 0.4]} linearOffset={[0, 0.2, 0]} />
|
|
143
146
|
<RigidBody type="dynamic" mass={70} angularFactor={[0, 0, 0]} />
|
|
144
|
-
<Entity name="character" ref={model} position={[0, 0.
|
|
147
|
+
<Entity name="character" ref={model} position={[0, 0.3, 0]}>
|
|
145
148
|
<Entity name="body" scale={[0.56, 0.88, 0.38]}>
|
|
146
149
|
<Render type="box" material={shirt} />
|
|
147
150
|
</Entity>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<title>Dodge</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Application, Entity } from '@playcanvas/react';
|
|
2
|
+
import { Camera, Light, Script } from '@playcanvas/react/components';
|
|
3
|
+
import { useApp, useAppEvent } from '@playcanvas/react/hooks';
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { CARDS, DodgeGame } from './dodge-game';
|
|
7
|
+
import type { GameEvents, GameState } from './dodge-game';
|
|
8
|
+
import { hasLeaderboard, loadAvatar, loadBest, recordScore, showLeaderboard } from './lens';
|
|
9
|
+
import './lens.css';
|
|
10
|
+
|
|
11
|
+
function Scene({ onState }: { onState: GameEvents['game:state'] }) {
|
|
12
|
+
const app = useApp();
|
|
13
|
+
useAppEvent<GameEvents>('game:state', onState);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// render at up to twice the CSS resolution: sharp on a phone, without the cost of 3x
|
|
17
|
+
// eslint-disable-next-line react-hooks/immutability -- the device belongs to the engine, not React
|
|
18
|
+
app.graphicsDevice.maxPixelRatio = Math.min(window.devicePixelRatio, 2);
|
|
19
|
+
}, [app]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{/* fix the field's width, so a taller phone shows more of the falling blocks */}
|
|
24
|
+
<Entity name="camera" position={[0, 0, 7]}>
|
|
25
|
+
<Camera clearColor="#0b1020" fov={45} horizontalFov />
|
|
26
|
+
</Entity>
|
|
27
|
+
<Entity name="light" rotation={[40, 30, 0]}>
|
|
28
|
+
<Light type="directional" intensity={1.5} />
|
|
29
|
+
</Entity>
|
|
30
|
+
<Entity name="game">
|
|
31
|
+
<Script script={DodgeGame} />
|
|
32
|
+
</Entity>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function App() {
|
|
38
|
+
const [state, setState] = useState<GameState>('ready');
|
|
39
|
+
const [score, setScore] = useState(0);
|
|
40
|
+
const [best, setBest] = useState(0);
|
|
41
|
+
const [avatar, setAvatar] = useState<string>();
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
loadBest().then(setBest);
|
|
45
|
+
loadAvatar().then(setAvatar);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
// the HUD follows the game's state, and the Lens records each finished run
|
|
49
|
+
const onState = useCallback((next: GameState, value: number) => {
|
|
50
|
+
setState(next);
|
|
51
|
+
setScore(value);
|
|
52
|
+
if (next === 'over') recordScore(value).then(setBest);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const card = CARDS[state];
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<main className="lens">
|
|
59
|
+
{/* lens.css sizes the canvas, filling a phone screen and centering a 9:16 column anywhere wider */}
|
|
60
|
+
<Application>
|
|
61
|
+
<Scene onState={onState} />
|
|
62
|
+
</Application>
|
|
63
|
+
<div className="lens-hud">
|
|
64
|
+
<header className="lens-bar">
|
|
65
|
+
{avatar && <img className="lens-avatar" src={avatar} alt="Your Bitmoji" />}
|
|
66
|
+
<span className="lens-score">{score}</span>
|
|
67
|
+
<span className="lens-best">Best {best}</span>
|
|
68
|
+
</header>
|
|
69
|
+
{card && (
|
|
70
|
+
<section className="lens-card">
|
|
71
|
+
<h1 className="lens-title">{card.title}</h1>
|
|
72
|
+
{state === 'over' && hasLeaderboard() && (
|
|
73
|
+
<button className="lens-leaderboard" onClick={showLeaderboard}>
|
|
74
|
+
Leaderboard
|
|
75
|
+
</button>
|
|
76
|
+
)}
|
|
77
|
+
<p className="lens-hint">{card.hint}</p>
|
|
78
|
+
</section>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</main>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default App;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
// Approximates the Web Lens sandbox, which allows resources from the Lens itself and nothing else
|
|
5
|
+
const LENS_SANDBOX = [
|
|
6
|
+
"default-src 'self' data: blob:",
|
|
7
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
|
|
8
|
+
"style-src 'self' 'unsafe-inline'",
|
|
9
|
+
"frame-src 'none'",
|
|
10
|
+
"object-src 'none'",
|
|
11
|
+
"base-uri 'none'",
|
|
12
|
+
"form-action 'none'"
|
|
13
|
+
].join('; ');
|
|
14
|
+
|
|
15
|
+
// https://vite.dev/config/
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
// Snapchat serves a Lens from its own origin, so every URL in the build must be relative
|
|
18
|
+
base: './',
|
|
19
|
+
build: {
|
|
20
|
+
// npm run package zips this folder and writes the zip beside it, inside the ignored dist/
|
|
21
|
+
outDir: 'dist/lens',
|
|
22
|
+
// source maps only add to the 8 MB upload
|
|
23
|
+
sourcemap: false
|
|
24
|
+
},
|
|
25
|
+
preview: {
|
|
26
|
+
// npm run start serves the build under the Lens rules, so a request for anything outside
|
|
27
|
+
// the zip fails in the browser console as it would in Snapchat
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Security-Policy': LENS_SANDBOX,
|
|
30
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
31
|
+
'Cross-Origin-Embedder-Policy': 'require-corp'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
plugins: [
|
|
35
|
+
react(),
|
|
36
|
+
{
|
|
37
|
+
// Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
|
|
38
|
+
name: 'playcanvas-inspector',
|
|
39
|
+
apply: 'serve',
|
|
40
|
+
config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
|
|
41
|
+
transformIndexHtml: () => [
|
|
42
|
+
{ tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
});
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@playcanvas/web-components": "^0.28.0",
|
|
19
|
-
"playcanvas": "^2.23.
|
|
19
|
+
"playcanvas": "^2.23.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@playcanvas/eslint-config": "3.0.0-beta.8",
|
|
23
|
-
"@playcanvas/inspector": "^0.
|
|
23
|
+
"@playcanvas/inspector": "^0.3.0",
|
|
24
24
|
"eslint": "^9.39.5",
|
|
25
|
-
"prettier": "^3.9.
|
|
25
|
+
"prettier": "^3.9.10",
|
|
26
26
|
"typescript": "~6.0.3",
|
|
27
|
-
"vite": "^8.3.
|
|
27
|
+
"vite": "^8.3.5"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=22.23.2"
|
|
@@ -65,12 +65,15 @@
|
|
|
65
65
|
></pc-entity>
|
|
66
66
|
<pc-entity name="tree-0-crown-0" position="-7 2.09 -6" scale="2.31 1.98 2.31"
|
|
67
67
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
68
|
+
><pc-collision type="cone" radius="1.155" height="1.98"></pc-collision
|
|
68
69
|
></pc-entity>
|
|
69
70
|
<pc-entity name="tree-0-crown-1" position="-7 2.915 -6" scale="1.76 1.705 1.76"
|
|
70
71
|
><pc-render type="cone" material="tips-material"></pc-render
|
|
72
|
+
><pc-collision type="cone" radius="0.88" height="1.705"></pc-collision
|
|
71
73
|
></pc-entity>
|
|
72
74
|
<pc-entity name="tree-0-crown-2" position="-7 3.63 -6" scale="1.21 1.54 1.21"
|
|
73
75
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
76
|
+
><pc-collision type="cone" radius="0.605" height="1.54"></pc-collision
|
|
74
77
|
></pc-entity>
|
|
75
78
|
<pc-entity name="tree-1-trunk" position="6.5 1.125 -6.5" scale="0.475 2.25 0.475"
|
|
76
79
|
><pc-render type="box" material="wood-material"></pc-render
|
|
@@ -79,12 +82,15 @@
|
|
|
79
82
|
></pc-entity>
|
|
80
83
|
<pc-entity name="tree-1-crown-0" position="6.5 2.375 -6.5" scale="2.625 2.25 2.625"
|
|
81
84
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
85
|
+
><pc-collision type="cone" radius="1.3125" height="2.25"></pc-collision
|
|
82
86
|
></pc-entity>
|
|
83
87
|
<pc-entity name="tree-1-crown-1" position="6.5 3.3125 -6.5" scale="2 1.9375 2"
|
|
84
88
|
><pc-render type="cone" material="tips-material"></pc-render
|
|
89
|
+
><pc-collision type="cone" radius="1" height="1.9375"></pc-collision
|
|
85
90
|
></pc-entity>
|
|
86
91
|
<pc-entity name="tree-1-crown-2" position="6.5 4.125 -6.5" scale="1.375 1.75 1.375"
|
|
87
92
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
93
|
+
><pc-collision type="cone" radius="0.6875" height="1.75"></pc-collision
|
|
88
94
|
></pc-entity>
|
|
89
95
|
<pc-entity name="tree-2-trunk" position="-7.5 0.81 5" scale="0.342 1.62 0.342"
|
|
90
96
|
><pc-render type="box" material="wood-material"></pc-render
|
|
@@ -93,12 +99,15 @@
|
|
|
93
99
|
></pc-entity>
|
|
94
100
|
<pc-entity name="tree-2-crown-0" position="-7.5 1.71 5" scale="1.89 1.62 1.89"
|
|
95
101
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
102
|
+
><pc-collision type="cone" radius="0.945" height="1.62"></pc-collision
|
|
96
103
|
></pc-entity>
|
|
97
104
|
<pc-entity name="tree-2-crown-1" position="-7.5 2.385 5" scale="1.44 1.395 1.44"
|
|
98
105
|
><pc-render type="cone" material="tips-material"></pc-render
|
|
106
|
+
><pc-collision type="cone" radius="0.72" height="1.395"></pc-collision
|
|
99
107
|
></pc-entity>
|
|
100
108
|
<pc-entity name="tree-2-crown-2" position="-7.5 2.97 5" scale="0.99 1.26 0.99"
|
|
101
109
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
110
|
+
><pc-collision type="cone" radius="0.495" height="1.26"></pc-collision
|
|
102
111
|
></pc-entity>
|
|
103
112
|
<pc-entity name="tree-3-trunk" position="7 1.035 5.5" scale="0.437 2.07 0.437"
|
|
104
113
|
><pc-render type="box" material="wood-material"></pc-render
|
|
@@ -107,12 +116,15 @@
|
|
|
107
116
|
></pc-entity>
|
|
108
117
|
<pc-entity name="tree-3-crown-0" position="7 2.185 5.5" scale="2.415 2.07 2.415"
|
|
109
118
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
119
|
+
><pc-collision type="cone" radius="1.2075" height="2.07"></pc-collision
|
|
110
120
|
></pc-entity>
|
|
111
121
|
<pc-entity name="tree-3-crown-1" position="7 3.0475 5.5" scale="1.84 1.7825 1.84"
|
|
112
122
|
><pc-render type="cone" material="tips-material"></pc-render
|
|
123
|
+
><pc-collision type="cone" radius="0.92" height="1.7825"></pc-collision
|
|
113
124
|
></pc-entity>
|
|
114
125
|
<pc-entity name="tree-3-crown-2" position="7 3.795 5.5" scale="1.265 1.61 1.265"
|
|
115
126
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
127
|
+
><pc-collision type="cone" radius="0.6325" height="1.61"></pc-collision
|
|
116
128
|
></pc-entity>
|
|
117
129
|
<pc-entity name="tree-4-trunk" position="-2.2 0.765 -8" scale="0.323 1.53 0.323"
|
|
118
130
|
><pc-render type="box" material="wood-material"></pc-render
|
|
@@ -121,12 +133,15 @@
|
|
|
121
133
|
></pc-entity>
|
|
122
134
|
<pc-entity name="tree-4-crown-0" position="-2.2 1.615 -8" scale="1.785 1.53 1.785"
|
|
123
135
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
136
|
+
><pc-collision type="cone" radius="0.8925" height="1.53"></pc-collision
|
|
124
137
|
></pc-entity>
|
|
125
138
|
<pc-entity name="tree-4-crown-1" position="-2.2 2.2525 -8" scale="1.36 1.3175 1.36"
|
|
126
139
|
><pc-render type="cone" material="tips-material"></pc-render
|
|
140
|
+
><pc-collision type="cone" radius="0.68" height="1.3175"></pc-collision
|
|
127
141
|
></pc-entity>
|
|
128
142
|
<pc-entity name="tree-4-crown-2" position="-2.2 2.805 -8" scale="0.935 1.19 0.935"
|
|
129
143
|
><pc-render type="cone" material="leaves-material"></pc-render
|
|
144
|
+
><pc-collision type="cone" radius="0.4675" height="1.19"></pc-collision
|
|
130
145
|
></pc-entity>
|
|
131
146
|
<pc-entity name="fallen-log" position="0 0.35 6" scale="3.8 0.7 0.7"
|
|
132
147
|
><pc-render type="box" material="wood-material"></pc-render
|
|
@@ -139,9 +154,9 @@
|
|
|
139
154
|
><pc-rigid-body type="static"></pc-rigid-body
|
|
140
155
|
></pc-entity>
|
|
141
156
|
<pc-entity name="player" position="0 1.1 0">
|
|
142
|
-
<pc-collision type="
|
|
157
|
+
<pc-collision type="box" half-extents="0.4 1.2 0.4" linear-offset="0 0.2 0"></pc-collision>
|
|
143
158
|
<pc-rigid-body type="dynamic" mass="70" angular-factor="0 0 0"></pc-rigid-body>
|
|
144
|
-
<pc-entity name="character" position="0 0.
|
|
159
|
+
<pc-entity name="character" position="0 0.3 0">
|
|
145
160
|
<pc-entity name="body" scale="0.56 0.88 0.38"
|
|
146
161
|
><pc-render type="box" material="shirt-material"></pc-render
|
|
147
162
|
></pc-entity>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<title>Dodge</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main class="lens">
|
|
10
|
+
<pc-app max-pixel-ratio="2">
|
|
11
|
+
<pc-scene>
|
|
12
|
+
<pc-entity name="camera" position="0 0 7">
|
|
13
|
+
<pc-camera clear-color="#0b1020" fov="45" horizontal-fov></pc-camera>
|
|
14
|
+
</pc-entity>
|
|
15
|
+
<pc-entity name="light" rotation="40 30 0">
|
|
16
|
+
<pc-light type="directional" intensity="1.5"></pc-light>
|
|
17
|
+
</pc-entity>
|
|
18
|
+
<pc-entity name="game"></pc-entity>
|
|
19
|
+
</pc-scene>
|
|
20
|
+
</pc-app>
|
|
21
|
+
<div class="lens-hud">
|
|
22
|
+
<header class="lens-bar">
|
|
23
|
+
<img class="lens-avatar" alt="Your Bitmoji" hidden />
|
|
24
|
+
<span class="lens-score">0</span>
|
|
25
|
+
<span class="lens-best">Best 0</span>
|
|
26
|
+
</header>
|
|
27
|
+
<section class="lens-card">
|
|
28
|
+
<h1 class="lens-title">Dodge</h1>
|
|
29
|
+
<button class="lens-leaderboard" hidden>Leaderboard</button>
|
|
30
|
+
<p class="lens-hint">Drag to steer. Tap to play</p>
|
|
31
|
+
</section>
|
|
32
|
+
</div>
|
|
33
|
+
</main>
|
|
34
|
+
<script type="module" src="/src/main.ts"></script>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EntityElement } from '@playcanvas/web-components';
|
|
2
|
+
import { whenReady } from '@playcanvas/web-components';
|
|
3
|
+
|
|
4
|
+
import { CARDS, DodgeGame } from './dodge-game';
|
|
5
|
+
import type { GameState } from './dodge-game';
|
|
6
|
+
import { hasLeaderboard, loadAvatar, loadBest, recordScore, showLeaderboard } from './lens';
|
|
7
|
+
import './lens.css';
|
|
8
|
+
|
|
9
|
+
// the HUD follows the game's state, and the Lens records each finished run
|
|
10
|
+
const hud = {
|
|
11
|
+
avatar: document.querySelector<HTMLImageElement>('.lens-avatar')!,
|
|
12
|
+
score: document.querySelector('.lens-score')!,
|
|
13
|
+
best: document.querySelector('.lens-best')!,
|
|
14
|
+
card: document.querySelector<HTMLElement>('.lens-card')!,
|
|
15
|
+
title: document.querySelector('.lens-title')!,
|
|
16
|
+
hint: document.querySelector('.lens-hint')!,
|
|
17
|
+
leaderboard: document.querySelector<HTMLButtonElement>('.lens-leaderboard')!
|
|
18
|
+
};
|
|
19
|
+
const showBest = (best: number) => {
|
|
20
|
+
hud.best.textContent = `Best ${best}`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const { app } = await whenReady('pc-app');
|
|
24
|
+
app!.on('game:state', (state: GameState, score: number) => {
|
|
25
|
+
const card = CARDS[state];
|
|
26
|
+
hud.score.textContent = String(score);
|
|
27
|
+
hud.card.hidden = !card;
|
|
28
|
+
if (card) {
|
|
29
|
+
hud.title.textContent = card.title;
|
|
30
|
+
hud.hint.textContent = card.hint;
|
|
31
|
+
}
|
|
32
|
+
hud.leaderboard.hidden = state !== 'over' || !hasLeaderboard();
|
|
33
|
+
if (state === 'over') recordScore(score).then(showBest);
|
|
34
|
+
});
|
|
35
|
+
hud.leaderboard.addEventListener('click', showLeaderboard);
|
|
36
|
+
|
|
37
|
+
loadBest().then(showBest);
|
|
38
|
+
loadAvatar().then((url) => {
|
|
39
|
+
if (!url) return;
|
|
40
|
+
hud.avatar.src = url;
|
|
41
|
+
hud.avatar.hidden = false;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// the game is an engine script imported through the bundler, so it attaches through the entity
|
|
45
|
+
const game = await whenReady<EntityElement>('pc-entity[name="game"]');
|
|
46
|
+
game.entity!.addComponent('script');
|
|
47
|
+
game.entity!.script!.create(DodgeGame);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
|
|
3
|
+
// Approximates the Web Lens sandbox, which allows resources from the Lens itself and nothing else
|
|
4
|
+
const LENS_SANDBOX = [
|
|
5
|
+
"default-src 'self' data: blob:",
|
|
6
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob:",
|
|
7
|
+
"style-src 'self' 'unsafe-inline'",
|
|
8
|
+
"frame-src 'none'",
|
|
9
|
+
"object-src 'none'",
|
|
10
|
+
"base-uri 'none'",
|
|
11
|
+
"form-action 'none'"
|
|
12
|
+
].join('; ');
|
|
13
|
+
|
|
14
|
+
// https://vite.dev/config/
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
// Snapchat serves a Lens from its own origin, so every URL in the build must be relative
|
|
17
|
+
base: './',
|
|
18
|
+
build: {
|
|
19
|
+
// npm run package zips this folder and writes the zip beside it, inside the ignored dist/
|
|
20
|
+
outDir: 'dist/lens',
|
|
21
|
+
// source maps only add to the 8 MB upload
|
|
22
|
+
sourcemap: false
|
|
23
|
+
},
|
|
24
|
+
preview: {
|
|
25
|
+
// npm run start serves the build under the Lens rules, so a request for anything outside
|
|
26
|
+
// the zip fails in the browser console as it would in Snapchat
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Security-Policy': LENS_SANDBOX,
|
|
29
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
30
|
+
'Cross-Origin-Embedder-Policy': 'require-corp'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
plugins: [
|
|
34
|
+
{
|
|
35
|
+
// Dev only: install the PlayCanvas Inspector before the app starts (press ` to toggle)
|
|
36
|
+
name: 'playcanvas-inspector',
|
|
37
|
+
apply: 'serve',
|
|
38
|
+
config: () => ({ optimizeDeps: { include: ['@playcanvas/inspector'] } }),
|
|
39
|
+
transformIndexHtml: () => [
|
|
40
|
+
{ tag: 'script', attrs: { type: 'module', src: '/src/inspector.ts' }, injectTo: 'head-prepend' }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
});
|