create-playcanvas 0.5.0 → 0.6.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 (109) hide show
  1. package/README.md +20 -27
  2. package/dist/index.mjs +44 -42
  3. package/package.json +1 -1
  4. package/templates/_features/physics/assets/public/ammo/LICENSE +21 -0
  5. package/templates/_features/physics/assets/public/ammo/SOURCE.md +16 -0
  6. package/templates/_features/physics/assets/public/ammo/ammo.js +973 -0
  7. package/templates/_features/physics/assets/public/ammo/ammo.wasm.js +937 -0
  8. package/templates/_features/physics/assets/public/ammo/ammo.wasm.wasm +0 -0
  9. package/templates/_features/physics/react/package.json +5 -0
  10. package/templates/_features/physics/react/src/sync-ammo.d.ts +4 -0
  11. package/templates/_shared/ar-placement/README.md +7 -0
  12. package/templates/_shared/ar-placement/src/ar-placement.ts +123 -0
  13. package/templates/_shared/first-person-controller/README.md +7 -0
  14. package/templates/_shared/first-person-controller/src/controller.ts +18 -0
  15. package/templates/_shared/first-person-controller/src/scripts.d.ts +5 -0
  16. package/templates/_shared/interactive-sphere/README.md +7 -0
  17. package/templates/_shared/model-viewer/README.md +7 -0
  18. package/templates/_shared/physics-playground/README.md +7 -0
  19. package/templates/_shared/product-configurator/ASSET_LICENSES.md +3 -0
  20. package/templates/_shared/product-configurator/README.md +7 -0
  21. package/templates/_shared/product-configurator/src/product.css +14 -0
  22. package/templates/_shared/scene-editor/README.md +7 -0
  23. package/templates/_shared/scene-editor/src/scene-editor.ts +57 -0
  24. package/templates/_shared/spinning-cube/README.md +7 -0
  25. package/templates/_shared/splat-viewer/README.md +7 -0
  26. package/templates/_shared/sprite-game/README.md +7 -0
  27. package/templates/_shared/sprite-game/src/sprite-game.ts +197 -0
  28. package/templates/_shared/third-person-controller/README.md +7 -0
  29. package/templates/_shared/third-person-controller/src/controller.ts +53 -0
  30. package/templates/_shared/third-person-controller/src/scripts.d.ts +5 -0
  31. package/templates/_shared/vr-starter/README.md +7 -0
  32. package/templates/_shared/vr-starter/src/vr.ts +40 -0
  33. package/templates/_shared/vr-starter/src/xr.d.ts +11 -0
  34. package/templates/engine/ar-placement/src/main.ts +77 -0
  35. package/templates/engine/base/.prettierignore +1 -0
  36. package/templates/engine/base/README.md +1 -1
  37. package/templates/engine/base/_npmrc +1 -0
  38. package/templates/engine/base/eslint.config.mjs +1 -1
  39. package/templates/engine/base/index.html +13 -0
  40. package/templates/engine/base/package.json +2 -2
  41. package/templates/engine/base/src/starter.css +104 -0
  42. package/templates/engine/first-person-controller/src/main.ts +139 -0
  43. package/templates/engine/interactive-sphere/src/App.ts +9 -7
  44. package/templates/engine/interactive-sphere/src/main.ts +2 -29
  45. package/templates/engine/interactive-sphere/src/style.css +18 -97
  46. package/templates/engine/model-viewer/src/main.ts +3 -2
  47. package/templates/engine/physics-playground/src/main.ts +132 -0
  48. package/templates/engine/product-configurator/src/main.ts +156 -0
  49. package/templates/engine/scene-editor/src/main.ts +90 -0
  50. package/templates/engine/splat-viewer/src/main.ts +4 -3
  51. package/templates/engine/sprite-game/src/main.ts +72 -0
  52. package/templates/engine/third-person-controller/src/main.ts +216 -0
  53. package/templates/engine/vr-starter/src/main.ts +101 -0
  54. package/templates/index.ts +66 -5
  55. package/templates/react/ar-placement/src/App.tsx +58 -0
  56. package/templates/react/base/.prettierignore +1 -0
  57. package/templates/react/base/README.md +1 -1
  58. package/templates/react/base/_npmrc +1 -0
  59. package/templates/react/base/eslint.config.mjs +1 -1
  60. package/templates/react/base/index.html +13 -0
  61. package/templates/react/base/package.json +2 -2
  62. package/templates/react/base/src/main.tsx +10 -0
  63. package/templates/react/base/src/starter.css +104 -0
  64. package/templates/react/first-person-controller/src/App.tsx +115 -0
  65. package/templates/react/interactive-sphere/src/App.css +18 -68
  66. package/templates/react/interactive-sphere/src/App.tsx +1 -22
  67. package/templates/react/interactive-sphere/src/Scene.tsx +7 -6
  68. package/templates/react/interactive-sphere/src/index.css +1 -48
  69. package/templates/react/model-viewer/src/App.tsx +2 -2
  70. package/templates/react/physics-playground/src/App.tsx +85 -0
  71. package/templates/react/product-configurator/src/App.tsx +157 -0
  72. package/templates/react/scene-editor/src/App.tsx +84 -0
  73. package/templates/react/splat-viewer/src/App.tsx +3 -3
  74. package/templates/react/sprite-game/src/App.tsx +38 -0
  75. package/templates/react/third-person-controller/src/App.tsx +193 -0
  76. package/templates/react/vr-starter/src/App.tsx +99 -0
  77. package/templates/web-components/ar-placement/index.html +44 -0
  78. package/templates/web-components/ar-placement/src/main.ts +13 -0
  79. package/templates/web-components/base/.prettierignore +1 -0
  80. package/templates/web-components/base/README.md +1 -1
  81. package/templates/web-components/base/_npmrc +1 -0
  82. package/templates/web-components/base/eslint.config.mjs +1 -1
  83. package/templates/web-components/base/package.json +3 -3
  84. package/templates/web-components/base/src/starter.css +104 -0
  85. package/templates/web-components/first-person-controller/index.html +168 -0
  86. package/templates/web-components/first-person-controller/src/main.ts +15 -0
  87. package/templates/web-components/interactive-sphere/index.html +5 -22
  88. package/templates/web-components/interactive-sphere/src/main.ts +5 -4
  89. package/templates/web-components/interactive-sphere/src/style.css +18 -76
  90. package/templates/web-components/model-viewer/index.html +1 -1
  91. package/templates/web-components/model-viewer/src/main.ts +2 -1
  92. package/templates/web-components/physics-playground/index.html +51 -0
  93. package/templates/web-components/physics-playground/src/main.ts +32 -0
  94. package/templates/web-components/product-configurator/index.html +51 -0
  95. package/templates/web-components/product-configurator/src/main.ts +93 -0
  96. package/templates/web-components/scene-editor/index.html +51 -0
  97. package/templates/web-components/scene-editor/src/main.ts +26 -0
  98. package/templates/web-components/spinning-cube/src/main.ts +1 -0
  99. package/templates/web-components/splat-viewer/index.html +2 -2
  100. package/templates/web-components/splat-viewer/src/main.ts +2 -1
  101. package/templates/web-components/sprite-game/index.html +34 -0
  102. package/templates/web-components/sprite-game/src/main.ts +9 -0
  103. package/templates/web-components/third-person-controller/index.html +183 -0
  104. package/templates/web-components/third-person-controller/src/main.ts +16 -0
  105. package/templates/web-components/vr-starter/index.html +80 -0
  106. package/templates/web-components/vr-starter/src/main.ts +12 -0
  107. package/templates/engine/interactive-sphere/src/assets/typescript.svg +0 -1
  108. package/templates/react/interactive-sphere/src/assets/react.svg +0 -1
  109. package/templates/web-components/interactive-sphere/src/assets/typescript.svg +0 -1
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "sync-ammo": "^0.1.2"
4
+ }
5
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'sync-ammo' {
2
+ const Ammo: unknown;
3
+ export default Ammo;
4
+ }
@@ -0,0 +1,7 @@
1
+ ## AR Placement
2
+
3
+ An AR scene that detects tracked planes and points, then shows a placement reticle.
4
+
5
+ ### Controls
6
+
7
+ Use **Start AR** on a compatible mobile device in a secure context. Select a tracked surface to place a box.
@@ -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,7 @@
1
+ ## First-Person Controller
2
+
3
+ A physics-backed player controller, camera and simple obstacle course.
4
+
5
+ ### Controls
6
+
7
+ Click the scene, use WASD to move, move the mouse to look and press Space to jump.
@@ -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,5 @@
1
+ declare module 'playcanvas/scripts/esm/first-person-controller.mjs' {
2
+ import { Script } from 'playcanvas';
3
+
4
+ export class FirstPersonController extends Script {}
5
+ }
@@ -0,0 +1,7 @@
1
+ ## Interactive Sphere
2
+
3
+ A lit sphere, an environment map, grid and orbit camera controls.
4
+
5
+ ### Controls
6
+
7
+ Drag to orbit the camera. Click the sphere to increment the counter.
@@ -0,0 +1,7 @@
1
+ ## Model Viewer
2
+
3
+ A glTF model with a procedural sky, image-based lighting and orbit camera controls.
4
+
5
+ ### Controls
6
+
7
+ Drag to orbit the camera. Replace the model asset to view your own model.
@@ -0,0 +1,7 @@
1
+ ## Physics Playground
2
+
3
+ A rigid-body scene with a static floor, orbit camera and dynamic primitive shapes.
4
+
5
+ ### Controls
6
+
7
+ Use **Spawn object** to add a body and **Reset** to recreate the starting set. Drag to orbit the camera.
@@ -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,7 @@
1
+ ## Product Configurator
2
+
3
+ A glTF product model, procedural studio lighting and selectable paint finishes.
4
+
5
+ ### Controls
6
+
7
+ Choose a paint finish, then drag to orbit the camera.
@@ -0,0 +1,14 @@
1
+ .variants button {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 8px;
5
+ }
6
+
7
+ .finish {
8
+ width: 14px;
9
+ height: 14px;
10
+ border: 1px solid #ffffff66;
11
+ border-radius: 50%;
12
+ background: var(--color);
13
+ box-shadow: 0 1px 4px #0008;
14
+ }
@@ -0,0 +1,7 @@
1
+ ## Scene Editor
2
+
3
+ A small editable scene with selectable entities and translate, rotate and scale gizmos.
4
+
5
+ ### Controls
6
+
7
+ Select the cube or sphere, then choose Move, Rotate or Scale to change the active gizmo.
@@ -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,7 @@
1
+ ## Spinning Cube
2
+
3
+ A rotating cube, a camera and a directional light. It is the smallest PlayCanvas scene in the catalog.
4
+
5
+ ### Controls
6
+
7
+ The scene runs automatically; no input is required.
@@ -0,0 +1,7 @@
1
+ ## Splat Viewer
2
+
3
+ A Gaussian splat with an orbit camera and a hosted sample asset.
4
+
5
+ ### Controls
6
+
7
+ Drag to orbit the camera. Replace the splat URL with your own capture.
@@ -0,0 +1,7 @@
1
+ ## Sprite Game
2
+
3
+ A small procedural sprite animation, platforms and a simple side-scrolling controller.
4
+
5
+ ### Controls
6
+
7
+ Use A/D or the arrow keys to move, and press Space to jump.
@@ -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,7 @@
1
+ ## Third-Person Controller
2
+
3
+ A physics-backed third-person controller, follow camera and animated robot.
4
+
5
+ ### Controls
6
+
7
+ Click the scene, use WASD to move, move the mouse to orbit and press Space to jump.
@@ -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,5 @@
1
+ declare module 'playcanvas/scripts/esm/third-person-controller.mjs' {
2
+ import { Script } from 'playcanvas';
3
+
4
+ export class ThirdPersonController extends Script {}
5
+ }
@@ -0,0 +1,7 @@
1
+ ## VR Starter
2
+
3
+ An immersive VR room with controller models and standard snap-turn and teleport locomotion.
4
+
5
+ ### Controls
6
+
7
+ Use **Enter VR** on a compatible headset in a secure context. Movement and teleport controls are provided by the controller scripts.
@@ -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
+ }