create-playcanvas 0.5.1 → 0.6.1
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 +20 -27
- package/dist/index.mjs +44 -42
- package/package.json +1 -1
- package/skills/calibrate-model/references/direct-engine.md +7 -4
- package/skills/calibrate-model/references/react.md +8 -3
- package/skills/calibrate-model/references/web-components.md +7 -4
- package/skills/configure-animation/references/web-components.md +18 -9
- package/skills/inspect-glb/SKILL.md +27 -18
- package/skills/inspect-glb/scripts/inspect.mjs +179 -50
- package/templates/_features/physics/assets/public/ammo/LICENSE +21 -0
- package/templates/_features/physics/assets/public/ammo/SOURCE.md +16 -0
- package/templates/_features/physics/assets/public/ammo/ammo.js +973 -0
- package/templates/_features/physics/assets/public/ammo/ammo.wasm.js +937 -0
- package/templates/_features/physics/assets/public/ammo/ammo.wasm.wasm +0 -0
- package/templates/_features/physics/react/package.json +5 -0
- package/templates/_features/physics/react/src/sync-ammo.d.ts +4 -0
- package/templates/_shared/ar-placement/README.md +7 -0
- package/templates/_shared/ar-placement/src/ar-placement.ts +123 -0
- package/templates/_shared/first-person-controller/README.md +7 -0
- package/templates/_shared/first-person-controller/src/controller.ts +18 -0
- package/templates/_shared/first-person-controller/src/scripts.d.ts +5 -0
- package/templates/_shared/interactive-sphere/README.md +7 -0
- package/templates/_shared/model-viewer/README.md +7 -0
- package/templates/_shared/physics-playground/README.md +7 -0
- package/templates/_shared/product-configurator/ASSET_LICENSES.md +3 -0
- package/templates/_shared/product-configurator/README.md +7 -0
- package/templates/_shared/product-configurator/src/product.css +14 -0
- package/templates/_shared/scene-editor/README.md +7 -0
- package/templates/_shared/scene-editor/src/scene-editor.ts +57 -0
- package/templates/_shared/spinning-cube/README.md +7 -0
- package/templates/_shared/splat-viewer/README.md +7 -0
- package/templates/_shared/sprite-game/README.md +7 -0
- package/templates/_shared/sprite-game/src/sprite-game.ts +197 -0
- package/templates/_shared/third-person-controller/README.md +7 -0
- package/templates/_shared/third-person-controller/src/controller.ts +53 -0
- package/templates/_shared/third-person-controller/src/scripts.d.ts +5 -0
- package/templates/_shared/vr-starter/README.md +7 -0
- package/templates/_shared/vr-starter/src/vr.ts +40 -0
- package/templates/_shared/vr-starter/src/xr.d.ts +11 -0
- package/templates/engine/ar-placement/src/main.ts +77 -0
- package/templates/engine/base/.prettierignore +1 -0
- package/templates/engine/base/README.md +1 -1
- package/templates/engine/base/_npmrc +1 -0
- package/templates/engine/base/eslint.config.mjs +1 -1
- package/templates/engine/base/index.html +13 -0
- package/templates/engine/base/src/starter.css +104 -0
- package/templates/engine/first-person-controller/src/main.ts +139 -0
- package/templates/engine/interactive-sphere/src/App.ts +9 -7
- package/templates/engine/interactive-sphere/src/main.ts +2 -29
- package/templates/engine/interactive-sphere/src/style.css +18 -97
- package/templates/engine/model-viewer/src/main.ts +3 -2
- package/templates/engine/physics-playground/src/main.ts +132 -0
- package/templates/engine/product-configurator/src/main.ts +156 -0
- package/templates/engine/scene-editor/src/main.ts +90 -0
- package/templates/engine/splat-viewer/src/main.ts +4 -3
- package/templates/engine/sprite-game/src/main.ts +72 -0
- package/templates/engine/third-person-controller/src/main.ts +216 -0
- package/templates/engine/vr-starter/src/main.ts +101 -0
- package/templates/index.ts +66 -5
- package/templates/react/ar-placement/src/App.tsx +58 -0
- package/templates/react/base/.prettierignore +1 -0
- package/templates/react/base/README.md +1 -1
- package/templates/react/base/_npmrc +1 -0
- package/templates/react/base/eslint.config.mjs +1 -1
- package/templates/react/base/index.html +13 -0
- package/templates/react/base/src/main.tsx +10 -0
- package/templates/react/base/src/starter.css +104 -0
- package/templates/react/first-person-controller/src/App.tsx +115 -0
- package/templates/react/interactive-sphere/src/App.css +18 -68
- package/templates/react/interactive-sphere/src/App.tsx +1 -22
- package/templates/react/interactive-sphere/src/Scene.tsx +7 -6
- package/templates/react/interactive-sphere/src/index.css +1 -48
- package/templates/react/model-viewer/src/App.tsx +2 -2
- package/templates/react/physics-playground/src/App.tsx +85 -0
- package/templates/react/product-configurator/src/App.tsx +157 -0
- package/templates/react/scene-editor/src/App.tsx +84 -0
- package/templates/react/splat-viewer/src/App.tsx +3 -3
- package/templates/react/sprite-game/src/App.tsx +38 -0
- package/templates/react/third-person-controller/src/App.tsx +193 -0
- package/templates/react/vr-starter/src/App.tsx +99 -0
- package/templates/web-components/ar-placement/index.html +44 -0
- package/templates/web-components/ar-placement/src/main.ts +13 -0
- package/templates/web-components/base/.prettierignore +1 -0
- package/templates/web-components/base/README.md +1 -1
- package/templates/web-components/base/_npmrc +1 -0
- package/templates/web-components/base/eslint.config.mjs +1 -1
- package/templates/web-components/base/package.json +1 -1
- package/templates/web-components/base/src/starter.css +104 -0
- package/templates/web-components/first-person-controller/index.html +168 -0
- package/templates/web-components/first-person-controller/src/main.ts +15 -0
- package/templates/web-components/interactive-sphere/index.html +5 -22
- package/templates/web-components/interactive-sphere/src/main.ts +5 -4
- package/templates/web-components/interactive-sphere/src/style.css +18 -76
- package/templates/web-components/model-viewer/index.html +1 -1
- package/templates/web-components/model-viewer/src/main.ts +2 -1
- package/templates/web-components/physics-playground/index.html +51 -0
- package/templates/web-components/physics-playground/src/main.ts +32 -0
- package/templates/web-components/product-configurator/index.html +51 -0
- package/templates/web-components/product-configurator/src/main.ts +93 -0
- package/templates/web-components/scene-editor/index.html +51 -0
- package/templates/web-components/scene-editor/src/main.ts +26 -0
- package/templates/web-components/spinning-cube/src/main.ts +1 -0
- package/templates/web-components/splat-viewer/index.html +2 -2
- package/templates/web-components/splat-viewer/src/main.ts +2 -1
- package/templates/web-components/sprite-game/index.html +34 -0
- package/templates/web-components/sprite-game/src/main.ts +9 -0
- package/templates/web-components/third-person-controller/index.html +183 -0
- package/templates/web-components/third-person-controller/src/main.ts +16 -0
- package/templates/web-components/vr-starter/index.html +80 -0
- package/templates/web-components/vr-starter/src/main.ts +12 -0
- package/templates/engine/interactive-sphere/src/assets/typescript.svg +0 -1
- package/templates/react/interactive-sphere/src/assets/react.svg +0 -1
- package/templates/web-components/interactive-sphere/src/assets/typescript.svg +0 -1
|
Binary file
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Entity, Quat, Vec3, XrHitTestSource } from 'playcanvas';
|
|
2
|
+
import {
|
|
3
|
+
Color,
|
|
4
|
+
Entity as PcEntity,
|
|
5
|
+
Script,
|
|
6
|
+
StandardMaterial,
|
|
7
|
+
XRSPACE_LOCALFLOOR,
|
|
8
|
+
XRTRACKABLE_PLANE,
|
|
9
|
+
XRTRACKABLE_POINT,
|
|
10
|
+
XRTYPE_AR
|
|
11
|
+
} from 'playcanvas';
|
|
12
|
+
|
|
13
|
+
class ArPlacement extends Script {
|
|
14
|
+
static scriptName = 'arPlacement';
|
|
15
|
+
|
|
16
|
+
camera!: Entity;
|
|
17
|
+
preview?: Entity;
|
|
18
|
+
reticle!: Entity;
|
|
19
|
+
material!: StandardMaterial;
|
|
20
|
+
position?: Vec3;
|
|
21
|
+
rotation?: Quat;
|
|
22
|
+
source?: XrHitTestSource;
|
|
23
|
+
|
|
24
|
+
initialize() {
|
|
25
|
+
const reticleMaterial = new StandardMaterial();
|
|
26
|
+
reticleMaterial.diffuse = new Color(1, 0.35, 0.15);
|
|
27
|
+
reticleMaterial.update();
|
|
28
|
+
this.material = new StandardMaterial();
|
|
29
|
+
this.material.diffuse = new Color(0.1, 0.7, 0.9);
|
|
30
|
+
this.material.update();
|
|
31
|
+
|
|
32
|
+
this.reticle = new PcEntity('reticle');
|
|
33
|
+
this.reticle.addComponent('render', { type: 'cylinder', material: reticleMaterial });
|
|
34
|
+
this.reticle.setLocalScale(0.15, 0.01, 0.15);
|
|
35
|
+
this.reticle.enabled = false;
|
|
36
|
+
this.app.root.addChild(this.reticle);
|
|
37
|
+
|
|
38
|
+
// xr UI and lifecycle events
|
|
39
|
+
const button = document.getElementById('xr-button') as HTMLButtonElement;
|
|
40
|
+
button.onclick = () => this.start();
|
|
41
|
+
this.app.xr?.hitTest.on('available', this.startHitTest, this);
|
|
42
|
+
this.app.xr?.input.on('select', this.place, this);
|
|
43
|
+
this.app.xr?.on('start', this.onStart, this);
|
|
44
|
+
this.app.xr?.on('end', this.onEnd, this);
|
|
45
|
+
this.app.xr?.on(`available:${XRTYPE_AR}`, this.updateStatus, this);
|
|
46
|
+
this.updateStatus(this.app.xr?.isAvailable(XRTYPE_AR) ?? false);
|
|
47
|
+
|
|
48
|
+
this.on('destroy', () => {
|
|
49
|
+
button.onclick = null;
|
|
50
|
+
this.reticle.destroy();
|
|
51
|
+
this.material.destroy();
|
|
52
|
+
reticleMaterial.destroy();
|
|
53
|
+
this.app.xr?.hitTest.off('available', this.startHitTest, this);
|
|
54
|
+
this.app.xr?.input.off('select', this.place, this);
|
|
55
|
+
this.app.xr?.off('start', this.onStart, this);
|
|
56
|
+
this.app.xr?.off('end', this.onEnd, this);
|
|
57
|
+
this.app.xr?.off(`available:${XRTYPE_AR}`, this.updateStatus, this);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
start() {
|
|
62
|
+
if (!this.app.xr?.isAvailable(XRTYPE_AR)) return;
|
|
63
|
+
this.camera.camera?.startXr(XRTYPE_AR, XRSPACE_LOCALFLOOR, {
|
|
64
|
+
callback: (error) => {
|
|
65
|
+
if (error) this.setStatus(error.message);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
startHitTest() {
|
|
71
|
+
this.app.xr?.hitTest.start({
|
|
72
|
+
entityTypes: [XRTRACKABLE_POINT, XRTRACKABLE_PLANE],
|
|
73
|
+
callback: (error, source) => {
|
|
74
|
+
if (error || !source) {
|
|
75
|
+
this.setStatus('Unable to start hit testing');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.source = source;
|
|
79
|
+
source.on('result', (position, rotation) => this.updateReticle(position, rotation));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// retain the latest hit so a select event can place the object
|
|
85
|
+
updateReticle(position: Vec3, rotation: Quat) {
|
|
86
|
+
this.position = position.clone();
|
|
87
|
+
this.rotation = rotation.clone();
|
|
88
|
+
this.reticle.enabled = true;
|
|
89
|
+
this.reticle.setPosition(position);
|
|
90
|
+
this.reticle.setRotation(rotation);
|
|
91
|
+
this.setStatus('Select to place a box');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
place() {
|
|
95
|
+
if (!this.position || !this.rotation) return;
|
|
96
|
+
const box = new PcEntity('placed-box');
|
|
97
|
+
box.addComponent('render', { type: 'box', material: this.material });
|
|
98
|
+
box.setLocalScale(0.2, 0.2, 0.2);
|
|
99
|
+
box.setPosition(this.position.x, this.position.y + 0.1, this.position.z);
|
|
100
|
+
box.setRotation(this.rotation);
|
|
101
|
+
this.app.root.addChild(box);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onStart() {
|
|
105
|
+
if (this.preview) this.preview.enabled = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onEnd() {
|
|
109
|
+
if (this.preview) this.preview.enabled = true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
updateStatus(available: boolean) {
|
|
113
|
+
const button = document.getElementById('xr-button') as HTMLButtonElement;
|
|
114
|
+
button.disabled = !available;
|
|
115
|
+
this.setStatus(available ? 'AR is available' : 'AR requires a compatible mobile device and secure context');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setStatus(message: string) {
|
|
119
|
+
document.getElementById('xr-status')!.textContent = message;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { ArPlacement };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Entity, RigidBodyComponentSystem } from 'playcanvas';
|
|
2
|
+
import { FirstPersonController } from 'playcanvas/scripts/esm/first-person-controller.mjs';
|
|
3
|
+
|
|
4
|
+
export const addFirstPersonController = (player: Entity, camera: Entity) => {
|
|
5
|
+
(player.rigidbody!.system as RigidBodyComponentSystem).gravity.set(0, -18, 0);
|
|
6
|
+
if (!player.script) player.addComponent('script');
|
|
7
|
+
player.script!.create(FirstPersonController, {
|
|
8
|
+
properties: {
|
|
9
|
+
camera,
|
|
10
|
+
jumpForce: 650,
|
|
11
|
+
speedGround: 55
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return () => {
|
|
16
|
+
player.script?.destroy('firstPersonController');
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Asset licences
|
|
2
|
+
|
|
3
|
+
"Lamborghini V12 Vision GT [REALISTIC FREE]" by [ALEX MILOS](https://sketchfab.com/TEDIORE), sourced from [Sketchfab](https://sketchfab.com/3d-models/lamborghini-v12-vision-gt-realistic-free-18b9f7e2826d437f8593c40b784cd915), is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Entity, Layer } from 'playcanvas';
|
|
2
|
+
import { Gizmo, RotateGizmo, ScaleGizmo, Script, TranslateGizmo } from 'playcanvas';
|
|
3
|
+
|
|
4
|
+
type Mode = 'translate' | 'rotate' | 'scale';
|
|
5
|
+
|
|
6
|
+
class SceneEditor extends Script {
|
|
7
|
+
static scriptName = 'sceneEditor';
|
|
8
|
+
|
|
9
|
+
camera!: Entity;
|
|
10
|
+
targets: Entity[] = [];
|
|
11
|
+
layer!: Layer;
|
|
12
|
+
selected = 0;
|
|
13
|
+
gizmo?: TranslateGizmo | RotateGizmo | ScaleGizmo;
|
|
14
|
+
|
|
15
|
+
initialize() {
|
|
16
|
+
this.layer = Gizmo.createLayer(this.app);
|
|
17
|
+
this.setMode('translate');
|
|
18
|
+
|
|
19
|
+
// wire the scene and gizmo controls
|
|
20
|
+
document.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => {
|
|
21
|
+
button.onclick = () => this.setMode(button.dataset.mode as Mode);
|
|
22
|
+
});
|
|
23
|
+
document.querySelectorAll<HTMLButtonElement>('[data-target]').forEach((button) => {
|
|
24
|
+
button.onclick = () => {
|
|
25
|
+
this.selected = Number(button.dataset.target);
|
|
26
|
+
this.gizmo?.attach([this.targets[this.selected]]);
|
|
27
|
+
this.updateButtons();
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
this.on('destroy', () => this.gizmo?.destroy());
|
|
31
|
+
this.updateButtons();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// replace the active gizmo for the selected entity
|
|
35
|
+
setMode(mode: Mode) {
|
|
36
|
+
this.gizmo?.destroy();
|
|
37
|
+
const camera = this.camera.camera!;
|
|
38
|
+
this.gizmo =
|
|
39
|
+
mode === 'rotate'
|
|
40
|
+
? new RotateGizmo(camera, this.layer)
|
|
41
|
+
: mode === 'scale'
|
|
42
|
+
? new ScaleGizmo(camera, this.layer)
|
|
43
|
+
: new TranslateGizmo(camera, this.layer);
|
|
44
|
+
this.gizmo.attach([this.targets[this.selected]]);
|
|
45
|
+
document.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => {
|
|
46
|
+
button.ariaPressed = String(button.dataset.mode === mode);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updateButtons() {
|
|
51
|
+
document.querySelectorAll<HTMLButtonElement>('[data-target]').forEach((button) => {
|
|
52
|
+
button.ariaPressed = String(Number(button.dataset.target) === this.selected);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { SceneEditor };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { SpriteComponent } from 'playcanvas';
|
|
2
|
+
import {
|
|
3
|
+
ADDRESS_CLAMP_TO_EDGE,
|
|
4
|
+
Asset,
|
|
5
|
+
Color,
|
|
6
|
+
Entity,
|
|
7
|
+
FILTER_NEAREST,
|
|
8
|
+
PIXELFORMAT_RGBA8,
|
|
9
|
+
Script,
|
|
10
|
+
SPRITE_RENDERMODE_SIMPLE,
|
|
11
|
+
SPRITETYPE_ANIMATED,
|
|
12
|
+
Sprite,
|
|
13
|
+
StandardMaterial,
|
|
14
|
+
Texture,
|
|
15
|
+
TextureAtlas,
|
|
16
|
+
Vec2,
|
|
17
|
+
Vec4
|
|
18
|
+
} from 'playcanvas';
|
|
19
|
+
|
|
20
|
+
const CELL = 24;
|
|
21
|
+
const FRAMES = 4;
|
|
22
|
+
const SCALE = 2.5;
|
|
23
|
+
const FEET = (2 / CELL) * SCALE;
|
|
24
|
+
const SPEED = 3;
|
|
25
|
+
const PLATFORMS = [
|
|
26
|
+
{ x: -3.2, y: 1, width: 2.6 },
|
|
27
|
+
{ x: 0, y: 1.9, width: 2.4 },
|
|
28
|
+
{ x: 3.6, y: 1.1, width: 2.8 }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
class SpriteGame extends Script {
|
|
32
|
+
static scriptName = 'spriteGame';
|
|
33
|
+
|
|
34
|
+
keys = new Set<string>();
|
|
35
|
+
player!: Entity;
|
|
36
|
+
velocity = 0;
|
|
37
|
+
grounded = true;
|
|
38
|
+
|
|
39
|
+
initialize() {
|
|
40
|
+
// build a small procedural runner sheet
|
|
41
|
+
const canvas = document.createElement('canvas');
|
|
42
|
+
canvas.width = CELL * FRAMES;
|
|
43
|
+
canvas.height = CELL;
|
|
44
|
+
const ctx = canvas.getContext('2d')!;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < FRAMES; i++) {
|
|
47
|
+
const x = i * CELL;
|
|
48
|
+
ctx.fillStyle = '#ff5f45';
|
|
49
|
+
ctx.fillRect(x + 7, 4 + (i % 2), 10, 11);
|
|
50
|
+
ctx.fillStyle = '#ffd166';
|
|
51
|
+
ctx.fillRect(x + 9, 1 + (i % 2), 6, 5);
|
|
52
|
+
ctx.fillStyle = '#222';
|
|
53
|
+
ctx.fillRect(x + 8 + (i % 2) * 5, 15, 3, 7);
|
|
54
|
+
ctx.fillRect(x + 13 - (i % 2) * 5, 15, 3, 7);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// turn the canvas into an in-memory animated sprite asset
|
|
58
|
+
const texture = new Texture(this.app.graphicsDevice, {
|
|
59
|
+
width: canvas.width,
|
|
60
|
+
height: canvas.height,
|
|
61
|
+
format: PIXELFORMAT_RGBA8,
|
|
62
|
+
mipmaps: false
|
|
63
|
+
});
|
|
64
|
+
texture.addressU = ADDRESS_CLAMP_TO_EDGE;
|
|
65
|
+
texture.addressV = ADDRESS_CLAMP_TO_EDGE;
|
|
66
|
+
texture.minFilter = FILTER_NEAREST;
|
|
67
|
+
texture.magFilter = FILTER_NEAREST;
|
|
68
|
+
texture.setSource(canvas);
|
|
69
|
+
|
|
70
|
+
const atlas = new TextureAtlas();
|
|
71
|
+
atlas.texture = texture;
|
|
72
|
+
atlas.frames = Object.fromEntries(
|
|
73
|
+
Array.from({ length: FRAMES }, (_, i) => [
|
|
74
|
+
String(i),
|
|
75
|
+
{
|
|
76
|
+
rect: new Vec4(i * CELL, 0, CELL, CELL),
|
|
77
|
+
pivot: new Vec2(0.5, 0),
|
|
78
|
+
border: new Vec4()
|
|
79
|
+
}
|
|
80
|
+
])
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const sprite = new Sprite(this.app.graphicsDevice, {
|
|
84
|
+
atlas,
|
|
85
|
+
frameKeys: Array.from({ length: FRAMES }, (_, i) => String(i)),
|
|
86
|
+
pixelsPerUnit: CELL,
|
|
87
|
+
renderMode: SPRITE_RENDERMODE_SIMPLE
|
|
88
|
+
});
|
|
89
|
+
const asset = new Asset('runner', 'sprite', { url: '' });
|
|
90
|
+
asset.resource = sprite;
|
|
91
|
+
asset.loaded = true;
|
|
92
|
+
this.app.assets.add(asset);
|
|
93
|
+
|
|
94
|
+
this.player = new Entity('runner');
|
|
95
|
+
this.player.addComponent('sprite', { type: SPRITETYPE_ANIMATED });
|
|
96
|
+
this.player.sprite!.addClip({ name: 'run', fps: 10, loop: true, spriteAsset: asset.id });
|
|
97
|
+
this.player.sprite!.autoPlayClip = 'run';
|
|
98
|
+
this.player.setLocalScale(SCALE, SCALE, SCALE);
|
|
99
|
+
this.player.setLocalPosition(-5.2, 0, 0);
|
|
100
|
+
this.entity.addChild(this.player);
|
|
101
|
+
|
|
102
|
+
// build the scene decoration from primitives
|
|
103
|
+
const sky = new StandardMaterial();
|
|
104
|
+
sky.emissive = new Color(0.86, 0.95, 1);
|
|
105
|
+
sky.update();
|
|
106
|
+
const dirt = new StandardMaterial();
|
|
107
|
+
dirt.emissive = new Color(0.45, 0.28, 0.16);
|
|
108
|
+
dirt.update();
|
|
109
|
+
const grass = new StandardMaterial();
|
|
110
|
+
grass.emissive = new Color(0.28, 0.62, 0.34);
|
|
111
|
+
grass.update();
|
|
112
|
+
const decoration = (
|
|
113
|
+
name: string,
|
|
114
|
+
type: 'box' | 'sphere',
|
|
115
|
+
position: [number, number, number],
|
|
116
|
+
scale: [number, number, number],
|
|
117
|
+
material: StandardMaterial
|
|
118
|
+
) => {
|
|
119
|
+
const entity = new Entity(name);
|
|
120
|
+
entity.addComponent('render', { type, material });
|
|
121
|
+
entity.setLocalPosition(...position);
|
|
122
|
+
entity.setLocalScale(...scale);
|
|
123
|
+
this.entity.addChild(entity);
|
|
124
|
+
};
|
|
125
|
+
decoration('cloud-left', 'sphere', [-3.8, 4, -1], [1.2, 0.5, 0.2], sky);
|
|
126
|
+
decoration('cloud-center', 'sphere', [-2.9, 4.1, -1], [1.4, 0.65, 0.2], sky);
|
|
127
|
+
decoration('cloud-right', 'sphere', [3.6, 3.6, -1], [1.7, 0.6, 0.2], sky);
|
|
128
|
+
for (const [i, { x, y, width }] of PLATFORMS.entries()) {
|
|
129
|
+
decoration(`platform-${i}`, 'box', [x, y - 0.24, -0.5], [width, 0.32, 0.4], dirt);
|
|
130
|
+
decoration(`platform-grass-${i}`, 'box', [x, y - 0.04, -0.49], [width, 0.08, 0.4], grass);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// retain input and release scene-owned resources on teardown
|
|
134
|
+
window.addEventListener('keydown', this.onKeyDown);
|
|
135
|
+
window.addEventListener('keyup', this.onKeyUp);
|
|
136
|
+
this.on('destroy', () => {
|
|
137
|
+
window.removeEventListener('keydown', this.onKeyDown);
|
|
138
|
+
window.removeEventListener('keyup', this.onKeyUp);
|
|
139
|
+
texture.destroy();
|
|
140
|
+
sky.destroy();
|
|
141
|
+
dirt.destroy();
|
|
142
|
+
grass.destroy();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onKeyDown = (event: KeyboardEvent) => {
|
|
147
|
+
this.keys.add(event.code);
|
|
148
|
+
if (event.code === 'Space' && this.grounded) {
|
|
149
|
+
this.velocity = 5;
|
|
150
|
+
this.grounded = false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
onKeyUp = (event: KeyboardEvent) => this.keys.delete(event.code);
|
|
155
|
+
|
|
156
|
+
update(dt: number) {
|
|
157
|
+
// apply movement, gravity and simple top-down platform collisions
|
|
158
|
+
const direction =
|
|
159
|
+
Number(this.keys.has('ArrowRight') || this.keys.has('KeyD')) -
|
|
160
|
+
Number(this.keys.has('ArrowLeft') || this.keys.has('KeyA'));
|
|
161
|
+
const position = this.player.getLocalPosition();
|
|
162
|
+
|
|
163
|
+
this.velocity -= 12 * dt;
|
|
164
|
+
const x = mathClamp(position.x + direction * SPEED * dt, -5.2, 5.5);
|
|
165
|
+
let y = position.y + this.velocity * dt;
|
|
166
|
+
let floor = 0;
|
|
167
|
+
if (this.velocity <= 0) {
|
|
168
|
+
for (const platform of PLATFORMS) {
|
|
169
|
+
const top = platform.y - FEET;
|
|
170
|
+
if (
|
|
171
|
+
position.y >= top &&
|
|
172
|
+
y <= top &&
|
|
173
|
+
x >= platform.x - platform.width / 2 &&
|
|
174
|
+
x <= platform.x + platform.width / 2
|
|
175
|
+
) {
|
|
176
|
+
floor = Math.max(floor, top);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (y <= floor) {
|
|
181
|
+
y = floor;
|
|
182
|
+
this.velocity = 0;
|
|
183
|
+
this.grounded = true;
|
|
184
|
+
} else {
|
|
185
|
+
this.grounded = false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.player.setLocalPosition(x, y, position.z);
|
|
189
|
+
this.player.setLocalScale(direction < 0 ? -SCALE : SCALE, SCALE, SCALE);
|
|
190
|
+
const component = this.player.sprite as SpriteComponent;
|
|
191
|
+
component.speed = direction ? 1 : 0;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const mathClamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
|
|
196
|
+
|
|
197
|
+
export { SpriteGame };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Entity, RigidBodyComponentSystem } from 'playcanvas';
|
|
2
|
+
import { Script } from 'playcanvas';
|
|
3
|
+
import { ThirdPersonController } from 'playcanvas/scripts/esm/third-person-controller.mjs';
|
|
4
|
+
|
|
5
|
+
class RobotAnimation extends Script {
|
|
6
|
+
static scriptName = 'robotAnimation';
|
|
7
|
+
|
|
8
|
+
time = 0;
|
|
9
|
+
y = 0;
|
|
10
|
+
|
|
11
|
+
initialize() {
|
|
12
|
+
this.y = this.entity.getLocalPosition().y;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
update(dt: number) {
|
|
16
|
+
const body = this.entity.parent as Entity | null;
|
|
17
|
+
const velocity = body?.rigidbody?.linearVelocity;
|
|
18
|
+
const speed = velocity ? Math.hypot(velocity.x, velocity.z) : 0;
|
|
19
|
+
this.time += dt * Math.min(speed * 2, 14);
|
|
20
|
+
|
|
21
|
+
const swing = Math.sin(this.time) * Math.min(speed * 5, 45);
|
|
22
|
+
this.entity.findByName('left-arm')?.setLocalEulerAngles(swing, 0, 0);
|
|
23
|
+
this.entity.findByName('right-arm')?.setLocalEulerAngles(-swing, 0, 0);
|
|
24
|
+
this.entity.findByName('left-leg')?.setLocalEulerAngles(-swing, 0, 0);
|
|
25
|
+
this.entity.findByName('right-leg')?.setLocalEulerAngles(swing, 0, 0);
|
|
26
|
+
this.entity.setLocalPosition(0, this.y + Math.abs(Math.sin(this.time)) * Math.min(speed * 0.01, 0.04), 0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const addThirdPersonController = (player: Entity, camera: Entity, model: Entity) => {
|
|
31
|
+
(player.rigidbody!.system as RigidBodyComponentSystem).gravity.set(0, -18, 0);
|
|
32
|
+
if (!player.script) player.addComponent('script');
|
|
33
|
+
player.script!.create(ThirdPersonController, {
|
|
34
|
+
properties: {
|
|
35
|
+
camera,
|
|
36
|
+
characterModel: model,
|
|
37
|
+
cameraDistance: 5,
|
|
38
|
+
cameraHeight: 0.5,
|
|
39
|
+
initialPitch: 15,
|
|
40
|
+
invertLookY: true,
|
|
41
|
+
jumpForce: 520,
|
|
42
|
+
speedGround: 60
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!model.script) model.addComponent('script');
|
|
47
|
+
model.script!.create(RobotAnimation);
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
player.script?.destroy('thirdPersonController');
|
|
51
|
+
model.script?.destroy('robotAnimation');
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AppBase, Entity } from 'playcanvas';
|
|
2
|
+
import { XRSPACE_LOCALFLOOR, XRTYPE_VR } from 'playcanvas';
|
|
3
|
+
import { XrControllers } from 'playcanvas/scripts/esm/xr/xr-controllers.mjs';
|
|
4
|
+
import { XrNavigation } from 'playcanvas/scripts/esm/xr/xr-navigation.mjs';
|
|
5
|
+
|
|
6
|
+
export const setupVr = (app: AppBase, rig: Entity, camera: Entity) => {
|
|
7
|
+
// attach the controller and locomotion scripts to the player rig
|
|
8
|
+
if (!rig.script) rig.addComponent('script');
|
|
9
|
+
rig.script!.create(XrControllers);
|
|
10
|
+
rig.script!.create(XrNavigation, {
|
|
11
|
+
properties: { enableMove: true, enableTeleport: true, turnMode: 'snap' }
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const button = document.getElementById('xr-button') as HTMLButtonElement;
|
|
15
|
+
const status = document.getElementById('xr-status')!;
|
|
16
|
+
const { xr } = app;
|
|
17
|
+
|
|
18
|
+
// availability can change after the page loads
|
|
19
|
+
const update = () => {
|
|
20
|
+
const available = xr?.isAvailable(XRTYPE_VR) ?? false;
|
|
21
|
+
button.disabled = !available;
|
|
22
|
+
status.textContent = available ? 'VR is available' : 'VR requires a compatible headset and secure context';
|
|
23
|
+
};
|
|
24
|
+
button.onclick = () => {
|
|
25
|
+
camera.camera?.startXr(XRTYPE_VR, XRSPACE_LOCALFLOOR, {
|
|
26
|
+
callback: (error) => {
|
|
27
|
+
if (error) status.textContent = error.message;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
xr?.on(`available:${XRTYPE_VR}`, update);
|
|
32
|
+
update();
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
button.onclick = null;
|
|
36
|
+
xr?.off(`available:${XRTYPE_VR}`, update);
|
|
37
|
+
rig.script?.destroy('xrControllers');
|
|
38
|
+
rig.script?.destroy('xrNavigation');
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare module 'playcanvas/scripts/esm/xr/xr-controllers.mjs' {
|
|
2
|
+
import { Script } from 'playcanvas';
|
|
3
|
+
|
|
4
|
+
export class XrControllers extends Script {}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module 'playcanvas/scripts/esm/xr/xr-navigation.mjs' {
|
|
8
|
+
import { Script } from 'playcanvas';
|
|
9
|
+
|
|
10
|
+
export class XrNavigation extends Script {}
|
|
11
|
+
}
|