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.
Files changed (113) hide show
  1. package/README.md +20 -27
  2. package/dist/index.mjs +44 -42
  3. package/package.json +1 -1
  4. package/skills/calibrate-model/references/direct-engine.md +7 -4
  5. package/skills/calibrate-model/references/react.md +8 -3
  6. package/skills/calibrate-model/references/web-components.md +7 -4
  7. package/skills/configure-animation/references/web-components.md +18 -9
  8. package/skills/inspect-glb/SKILL.md +27 -18
  9. package/skills/inspect-glb/scripts/inspect.mjs +179 -50
  10. package/templates/_features/physics/assets/public/ammo/LICENSE +21 -0
  11. package/templates/_features/physics/assets/public/ammo/SOURCE.md +16 -0
  12. package/templates/_features/physics/assets/public/ammo/ammo.js +973 -0
  13. package/templates/_features/physics/assets/public/ammo/ammo.wasm.js +937 -0
  14. package/templates/_features/physics/assets/public/ammo/ammo.wasm.wasm +0 -0
  15. package/templates/_features/physics/react/package.json +5 -0
  16. package/templates/_features/physics/react/src/sync-ammo.d.ts +4 -0
  17. package/templates/_shared/ar-placement/README.md +7 -0
  18. package/templates/_shared/ar-placement/src/ar-placement.ts +123 -0
  19. package/templates/_shared/first-person-controller/README.md +7 -0
  20. package/templates/_shared/first-person-controller/src/controller.ts +18 -0
  21. package/templates/_shared/first-person-controller/src/scripts.d.ts +5 -0
  22. package/templates/_shared/interactive-sphere/README.md +7 -0
  23. package/templates/_shared/model-viewer/README.md +7 -0
  24. package/templates/_shared/physics-playground/README.md +7 -0
  25. package/templates/_shared/product-configurator/ASSET_LICENSES.md +3 -0
  26. package/templates/_shared/product-configurator/README.md +7 -0
  27. package/templates/_shared/product-configurator/src/product.css +14 -0
  28. package/templates/_shared/scene-editor/README.md +7 -0
  29. package/templates/_shared/scene-editor/src/scene-editor.ts +57 -0
  30. package/templates/_shared/spinning-cube/README.md +7 -0
  31. package/templates/_shared/splat-viewer/README.md +7 -0
  32. package/templates/_shared/sprite-game/README.md +7 -0
  33. package/templates/_shared/sprite-game/src/sprite-game.ts +197 -0
  34. package/templates/_shared/third-person-controller/README.md +7 -0
  35. package/templates/_shared/third-person-controller/src/controller.ts +53 -0
  36. package/templates/_shared/third-person-controller/src/scripts.d.ts +5 -0
  37. package/templates/_shared/vr-starter/README.md +7 -0
  38. package/templates/_shared/vr-starter/src/vr.ts +40 -0
  39. package/templates/_shared/vr-starter/src/xr.d.ts +11 -0
  40. package/templates/engine/ar-placement/src/main.ts +77 -0
  41. package/templates/engine/base/.prettierignore +1 -0
  42. package/templates/engine/base/README.md +1 -1
  43. package/templates/engine/base/_npmrc +1 -0
  44. package/templates/engine/base/eslint.config.mjs +1 -1
  45. package/templates/engine/base/index.html +13 -0
  46. package/templates/engine/base/src/starter.css +104 -0
  47. package/templates/engine/first-person-controller/src/main.ts +139 -0
  48. package/templates/engine/interactive-sphere/src/App.ts +9 -7
  49. package/templates/engine/interactive-sphere/src/main.ts +2 -29
  50. package/templates/engine/interactive-sphere/src/style.css +18 -97
  51. package/templates/engine/model-viewer/src/main.ts +3 -2
  52. package/templates/engine/physics-playground/src/main.ts +132 -0
  53. package/templates/engine/product-configurator/src/main.ts +156 -0
  54. package/templates/engine/scene-editor/src/main.ts +90 -0
  55. package/templates/engine/splat-viewer/src/main.ts +4 -3
  56. package/templates/engine/sprite-game/src/main.ts +72 -0
  57. package/templates/engine/third-person-controller/src/main.ts +216 -0
  58. package/templates/engine/vr-starter/src/main.ts +101 -0
  59. package/templates/index.ts +66 -5
  60. package/templates/react/ar-placement/src/App.tsx +58 -0
  61. package/templates/react/base/.prettierignore +1 -0
  62. package/templates/react/base/README.md +1 -1
  63. package/templates/react/base/_npmrc +1 -0
  64. package/templates/react/base/eslint.config.mjs +1 -1
  65. package/templates/react/base/index.html +13 -0
  66. package/templates/react/base/src/main.tsx +10 -0
  67. package/templates/react/base/src/starter.css +104 -0
  68. package/templates/react/first-person-controller/src/App.tsx +115 -0
  69. package/templates/react/interactive-sphere/src/App.css +18 -68
  70. package/templates/react/interactive-sphere/src/App.tsx +1 -22
  71. package/templates/react/interactive-sphere/src/Scene.tsx +7 -6
  72. package/templates/react/interactive-sphere/src/index.css +1 -48
  73. package/templates/react/model-viewer/src/App.tsx +2 -2
  74. package/templates/react/physics-playground/src/App.tsx +85 -0
  75. package/templates/react/product-configurator/src/App.tsx +157 -0
  76. package/templates/react/scene-editor/src/App.tsx +84 -0
  77. package/templates/react/splat-viewer/src/App.tsx +3 -3
  78. package/templates/react/sprite-game/src/App.tsx +38 -0
  79. package/templates/react/third-person-controller/src/App.tsx +193 -0
  80. package/templates/react/vr-starter/src/App.tsx +99 -0
  81. package/templates/web-components/ar-placement/index.html +44 -0
  82. package/templates/web-components/ar-placement/src/main.ts +13 -0
  83. package/templates/web-components/base/.prettierignore +1 -0
  84. package/templates/web-components/base/README.md +1 -1
  85. package/templates/web-components/base/_npmrc +1 -0
  86. package/templates/web-components/base/eslint.config.mjs +1 -1
  87. package/templates/web-components/base/package.json +1 -1
  88. package/templates/web-components/base/src/starter.css +104 -0
  89. package/templates/web-components/first-person-controller/index.html +168 -0
  90. package/templates/web-components/first-person-controller/src/main.ts +15 -0
  91. package/templates/web-components/interactive-sphere/index.html +5 -22
  92. package/templates/web-components/interactive-sphere/src/main.ts +5 -4
  93. package/templates/web-components/interactive-sphere/src/style.css +18 -76
  94. package/templates/web-components/model-viewer/index.html +1 -1
  95. package/templates/web-components/model-viewer/src/main.ts +2 -1
  96. package/templates/web-components/physics-playground/index.html +51 -0
  97. package/templates/web-components/physics-playground/src/main.ts +32 -0
  98. package/templates/web-components/product-configurator/index.html +51 -0
  99. package/templates/web-components/product-configurator/src/main.ts +93 -0
  100. package/templates/web-components/scene-editor/index.html +51 -0
  101. package/templates/web-components/scene-editor/src/main.ts +26 -0
  102. package/templates/web-components/spinning-cube/src/main.ts +1 -0
  103. package/templates/web-components/splat-viewer/index.html +2 -2
  104. package/templates/web-components/splat-viewer/src/main.ts +2 -1
  105. package/templates/web-components/sprite-game/index.html +34 -0
  106. package/templates/web-components/sprite-game/src/main.ts +9 -0
  107. package/templates/web-components/third-person-controller/index.html +183 -0
  108. package/templates/web-components/third-person-controller/src/main.ts +16 -0
  109. package/templates/web-components/vr-starter/index.html +80 -0
  110. package/templates/web-components/vr-starter/src/main.ts +12 -0
  111. package/templates/engine/interactive-sphere/src/assets/typescript.svg +0 -1
  112. package/templates/react/interactive-sphere/src/assets/react.svg +0 -1
  113. package/templates/web-components/interactive-sphere/src/assets/typescript.svg +0 -1
@@ -12,7 +12,7 @@ function Scene({ onClick }: SceneProps) {
12
12
  const [hovering, setHovering] = useState(false);
13
13
 
14
14
  // Set a material color based on the hover state
15
- const diffuse = hovering ? 'orange' : 'lightgrey';
15
+ const diffuse = hovering ? '#40ccff' : '#e6edf9';
16
16
 
17
17
  // Create a material for the sphere
18
18
  const material = useMaterial({ diffuse });
@@ -38,19 +38,20 @@ function Scene({ onClick }: SceneProps) {
38
38
  <Environment envAtlas={envMap} showSkybox={false} />
39
39
 
40
40
  {/* Render a background grid */}
41
- <Entity scale={[1000, 1000, 1000]}>
41
+ <Entity scale={[12, 12, 12]}>
42
42
  <Script script={Grid} />
43
43
  </Entity>
44
44
 
45
45
  {/* Create a camera entity with camera controls */}
46
- <Entity name="camera" position={[4, 1, 4]}>
47
- <Camera clearColor="#171717" />
48
- <Script script={CameraControls} />
46
+ <Entity name="camera" position={[3.2, 1.5, 3.2]} rotation={[-10, 45, 0]}>
47
+ <Camera clearColor="#07101a" />
48
+ <Script script={CameraControls} sceneSize={3} />
49
49
  </Entity>
50
50
 
51
51
  {/* Create and position entity with pointer events */}
52
52
  <Entity
53
- position={[0, 0.5, 0]}
53
+ position={[0, 0.7, 0]}
54
+ scale={[1.25, 1.25, 1.25]}
54
55
  onClick={onClick}
55
56
  onPointerOver={() => setHovering(true)}
56
57
  onPointerOut={() => setHovering(false)}
@@ -1,51 +1,4 @@
1
- :root {
2
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
- line-height: 1.5;
4
- font-weight: 400;
5
-
6
- color-scheme: light dark;
7
- color: rgba(255, 255, 255, 0.87);
8
- background-color: #242424;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- }
15
-
16
- a {
17
- font-weight: 500;
18
- color: #646cff;
19
- text-decoration: inherit;
20
- }
21
- a:hover {
22
- color: #535bf2;
23
- }
24
-
25
- a > img {
26
- vertical-align: middle;
27
- }
28
-
29
1
  body {
30
2
  margin: 0;
31
- display: flex;
32
- place-items: center;
33
- min-width: 320px;
34
- min-height: 100vh;
35
- }
36
-
37
- h1 {
38
- font-size: 2.4em;
39
- line-height: 1.1;
40
- opacity: 0.85;
41
- }
42
-
43
- @media (prefers-color-scheme: light) {
44
- :root {
45
- color: #213547;
46
- background-color: #ffffff;
47
- }
48
- a:hover {
49
- color: #747bff;
50
- }
3
+ overflow: hidden;
51
4
  }
@@ -30,9 +30,9 @@ function App() {
30
30
  {/* Create a camera entity with orbit controls. The sky is HDR, so it needs tone
31
31
  mapping to bring it back into displayable range - without it the whole image blows
32
32
  out to white */}
33
- <Entity name="camera" position={[0, 0, 3]}>
33
+ <Entity name="camera" position={[2.6, 1.5, 3.2]} rotation={[-16, 39, 0]}>
34
34
  <Camera toneMapping={TONEMAP_ACES2} />
35
- <Script script={CameraControls} />
35
+ <Script script={CameraControls} sceneSize={2.5} />
36
36
  </Entity>
37
37
 
38
38
  <Model />
@@ -0,0 +1,85 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Collision, Light, Render, RigidBody, Script } from '@playcanvas/react/components';
3
+ import { useMaterial } from '@playcanvas/react/hooks';
4
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
5
+ import { useState } from 'react';
6
+
7
+ import './starter.css';
8
+
9
+ const COLORS = ['#29ade6', '#f25738', '#f9ad2e'];
10
+ const TYPES = ['box', 'sphere', 'capsule', 'cylinder'] as const;
11
+ const SCALES: [number, number, number][] = [
12
+ [0.9, 0.9, 0.9],
13
+ [0.9, 0.9, 0.9],
14
+ [0.7, 1.1, 0.7],
15
+ [0.8, 1.1, 0.8]
16
+ ];
17
+
18
+ function Body({ i }: { i: number }) {
19
+ const type = TYPES[i % TYPES.length];
20
+ const material = useMaterial({ diffuse: COLORS[i % COLORS.length], gloss: 0.45 });
21
+ return (
22
+ <Entity
23
+ position={[((i % 4) - 1.5) * 1.1, 0.7 + Math.floor(i / 4) * 1.1, (Math.floor(i / 4) - 1) * 1.4]}
24
+ rotation={[i * 13, i * 29, 0]}
25
+ scale={SCALES[i % SCALES.length]}
26
+ >
27
+ <Render type={type} material={material} />
28
+ <Collision type={type} />
29
+ <RigidBody type="dynamic" mass={1} restitution={0.35} />
30
+ </Entity>
31
+ );
32
+ }
33
+
34
+ function Floor() {
35
+ const material = useMaterial({ diffuse: '#2e333d', gloss: 0.2 });
36
+ return (
37
+ <Entity name="floor" position={[0, -0.1, 0]} scale={[10, 0.2, 10]}>
38
+ <Render type="box" material={material} />
39
+ <Collision type="box" halfExtents={[5, 0.1, 5]} />
40
+ <RigidBody type="static" />
41
+ </Entity>
42
+ );
43
+ }
44
+
45
+ function App() {
46
+ const [count, setCount] = useState(12);
47
+ const [reset, setReset] = useState(0);
48
+
49
+ return (
50
+ <>
51
+ <Application usePhysics>
52
+ <Entity name="camera" position={[10, 8, 10]} rotation={[-23, 45, 0]}>
53
+ <Camera clearColor="#0d121c" />
54
+ <Script script={CameraControls} sceneSize={10} />
55
+ </Entity>
56
+ <Entity name="light" rotation={[45, 35, 0]}>
57
+ <Light type="directional" intensity={2.5} castShadows shadowBias={0.2} normalOffsetBias={0.05} />
58
+ </Entity>
59
+ <Floor />
60
+ {Array.from({ length: count }, (_, i) => (
61
+ <Body key={`${reset}-${i}`} i={i} />
62
+ ))}
63
+ </Application>
64
+ <div className="hud">
65
+ <section className="panel">
66
+ <h1>Physics Playground</h1>
67
+ <p>Rigid bodies, collisions and runtime spawning.</p>
68
+ <div className="controls">
69
+ <button onClick={() => setCount((value) => value + 1)}>Spawn object</button>
70
+ <button
71
+ onClick={() => {
72
+ setCount(12);
73
+ setReset((value) => value + 1);
74
+ }}
75
+ >
76
+ Reset
77
+ </button>
78
+ </div>
79
+ </section>
80
+ </div>
81
+ </>
82
+ );
83
+ }
84
+
85
+ export default App;
@@ -0,0 +1,157 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Light, Script } from '@playcanvas/react/components';
3
+ import { useApp, useModel } from '@playcanvas/react/hooks';
4
+ import type { ContainerResource, Entity as PcEntity, RenderComponent } from 'playcanvas';
5
+ import { Color, CylinderGeometry, Mesh, MeshInstance, StandardMaterial, TONEMAP_ACES2, Vec2, Vec3 } from 'playcanvas';
6
+ import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
7
+ import { ProceduralSky } from 'playcanvas/scripts/esm/sky/procedural-sky.mjs';
8
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
9
+
10
+ import './product.css';
11
+ import './starter.css';
12
+
13
+ const PRODUCT_URL = 'https://developer.playcanvas.com/assets/lambo.glb';
14
+ const PAINTS = [
15
+ { name: 'Crimson', color: '#d6293e', shade: '#9b1731' },
16
+ { name: 'Arctic', color: '#d8e4e9', shade: '#8fa8b5' },
17
+ { name: 'Volt', color: '#9acb34', shade: '#3c742d' }
18
+ ];
19
+ const CAMERA_FOCUS = new Vec3(0, 0.8, 0);
20
+ const CAMERA_PITCH = new Vec2(-85, -4);
21
+
22
+ function Product({ color, shade }: { color: string; shade: string }) {
23
+ const { asset } = useModel(PRODUCT_URL);
24
+ const root = useRef<PcEntity>(null);
25
+ const paint = useRef<StandardMaterial>(null);
26
+ const tone = useRef<StandardMaterial>(null);
27
+ const resource = asset?.resource as ContainerResource | undefined;
28
+
29
+ useLayoutEffect(() => {
30
+ if (!resource || !root.current) return;
31
+ const entity = resource.instantiateRenderEntity();
32
+ entity.setLocalEulerAngles(-90, -25, 0);
33
+ root.current.addChild(entity);
34
+
35
+ const meshes = (entity.findComponents('render') as RenderComponent[]).flatMap((render) => render.meshInstances);
36
+ const bounds = meshes[0].aabb.clone();
37
+ meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
38
+ const scale = 5 / Math.max(bounds.halfExtents.x * 2, bounds.halfExtents.z * 2);
39
+ entity.setLocalScale(scale, scale, scale);
40
+ root.current.root.syncHierarchy();
41
+ bounds.copy(meshes[0].aabb);
42
+ meshes.slice(1).forEach((mesh) => bounds.add(mesh.aabb));
43
+ entity.setPosition(-bounds.center.x, 0.03 - bounds.getMin().y, -bounds.center.z);
44
+
45
+ const body = meshes.filter((mesh) => mesh.material.name === 'material');
46
+ const panels = meshes.filter((mesh) => mesh.material.name === 'material_1');
47
+ paint.current = body[0].material.clone() as StandardMaterial;
48
+ tone.current = panels[0].material.clone() as StandardMaterial;
49
+ body.forEach((mesh) => (mesh.material = paint.current!));
50
+ panels.forEach((mesh) => (mesh.material = tone.current!));
51
+ return () => {
52
+ paint.current?.destroy();
53
+ tone.current?.destroy();
54
+ paint.current = null;
55
+ tone.current = null;
56
+ entity.destroy();
57
+ };
58
+ }, [resource]);
59
+
60
+ useLayoutEffect(() => {
61
+ if (!paint.current) return;
62
+ paint.current.diffuse.fromString(color);
63
+ tone.current!.diffuse.fromString(shade);
64
+ paint.current.update();
65
+ tone.current!.update();
66
+ }, [color, shade, resource]);
67
+
68
+ return <Entity ref={root} name="car" />;
69
+ }
70
+
71
+ function Studio() {
72
+ const app = useApp();
73
+ const ground = useRef<PcEntity>(null);
74
+
75
+ useEffect(() => {
76
+ const entity = ground.current;
77
+ if (!entity) return;
78
+
79
+ const podium = new StandardMaterial();
80
+ podium.diffuse = new Color(0.3, 0.32, 0.34);
81
+ podium.metalness = 0.15;
82
+ podium.gloss = 0.65;
83
+ podium.update();
84
+ const cylinder = Mesh.fromGeometry(
85
+ app.graphicsDevice,
86
+ new CylinderGeometry({ radius: 4, height: 0.16, heightSegments: 1, capSegments: 128 })
87
+ );
88
+ entity.setPosition(0, -0.08, 0);
89
+ entity.addComponent('render', {
90
+ meshInstances: [new MeshInstance(cylinder, podium)],
91
+ castShadows: false,
92
+ receiveShadows: true
93
+ });
94
+
95
+ return () => {
96
+ if (entity.render) entity.removeComponent('render');
97
+ cylinder.destroy();
98
+ podium.destroy();
99
+ };
100
+ }, [app]);
101
+
102
+ return <Entity ref={ground} name="studio-ground" />;
103
+ }
104
+
105
+ function Scene({ color, shade }: { color: string; shade: string }) {
106
+ return (
107
+ <>
108
+ <Entity name="sky">
109
+ <Script script={ProceduralSky} luminance={0.18} />
110
+ </Entity>
111
+ <Entity name="camera" position={[5.5, 3.2, 6.5]} rotation={[-16, 40, 0]}>
112
+ <Camera fov={40} toneMapping={TONEMAP_ACES2} renderSceneColorMap />
113
+ <Script
114
+ script={CameraControls}
115
+ sceneSize={6.2}
116
+ focusPoint={CAMERA_FOCUS}
117
+ pitchRange={CAMERA_PITCH}
118
+ enableFly={false}
119
+ enablePan={false}
120
+ />
121
+ </Entity>
122
+ <Entity name="light" rotation={[45, -145, 0]}>
123
+ <Light type="directional" intensity={2.5} castShadows shadowBias={0.2} normalOffsetBias={0.05} />
124
+ </Entity>
125
+ <Studio />
126
+ <Product color={color} shade={shade} />
127
+ </>
128
+ );
129
+ }
130
+
131
+ function App() {
132
+ const [paint, setPaint] = useState(PAINTS[0]);
133
+
134
+ return (
135
+ <>
136
+ <Application>
137
+ <Scene color={paint.color} shade={paint.shade} />
138
+ </Application>
139
+ <div className="hud">
140
+ <section className="panel">
141
+ <h1>Product Configurator</h1>
142
+ <p>Choose a paint finish, then drag to orbit.</p>
143
+ <div className="controls variants">
144
+ {PAINTS.map((item) => (
145
+ <button key={item.name} aria-pressed={paint === item} onClick={() => setPaint(item)}>
146
+ <span className="finish" style={{ '--color': item.color } as React.CSSProperties} />
147
+ {item.name}
148
+ </button>
149
+ ))}
150
+ </div>
151
+ </section>
152
+ </div>
153
+ </>
154
+ );
155
+ }
156
+
157
+ export default App;
@@ -0,0 +1,84 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Light, Render, Script } from '@playcanvas/react/components';
3
+ import { useApp, useMaterial } from '@playcanvas/react/hooks';
4
+ import type { Entity as PcEntity } from 'playcanvas';
5
+ import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
6
+ import { useEffect, useRef } from 'react';
7
+
8
+ import { SceneEditor } from './scene-editor';
9
+ import './starter.css';
10
+
11
+ function Scene() {
12
+ const app = useApp();
13
+ const host = useRef<PcEntity>(null);
14
+ const camera = useRef<PcEntity>(null);
15
+ const cube = useRef<PcEntity>(null);
16
+ const sphere = useRef<PcEntity>(null);
17
+ const coral = useMaterial({ diffuse: '#ff5145' });
18
+ const cyan = useMaterial({ diffuse: '#24addf' });
19
+
20
+ useEffect(() => {
21
+ const node = host.current;
22
+ const layer = app.scene.layers.getLayerByName('World');
23
+ if (!node || !camera.current || !cube.current || !sphere.current || !layer) return;
24
+ if (!node.script) node.addComponent('script');
25
+ node.script!.create(SceneEditor, {
26
+ properties: { camera: camera.current, targets: [cube.current, sphere.current], layer }
27
+ });
28
+ return () => {
29
+ node.script?.destroy('sceneEditor');
30
+ };
31
+ }, [app]);
32
+
33
+ return (
34
+ <>
35
+ <Entity name="camera" ref={camera} position={[4, 3.2, 5]} rotation={[-18, 39, 0]}>
36
+ <Camera clearColor="#0a0f17" />
37
+ </Entity>
38
+ <Entity name="light" rotation={[45, 30, 0]}>
39
+ <Light type="directional" intensity={2.5} castShadows shadowBias={0.2} normalOffsetBias={0.05} />
40
+ </Entity>
41
+ <Entity name="grid" scale={[12, 12, 12]}>
42
+ <Script script={Grid} />
43
+ </Entity>
44
+ <Entity name="cube" ref={cube} position={[-1.2, 0.5, 0]}>
45
+ <Render type="box" material={coral} />
46
+ </Entity>
47
+ <Entity name="sphere" ref={sphere} position={[1.2, 0.5, 0]}>
48
+ <Render type="sphere" material={cyan} />
49
+ </Entity>
50
+ <Entity name="editor" ref={host} />
51
+ </>
52
+ );
53
+ }
54
+
55
+ function App() {
56
+ return (
57
+ <>
58
+ <Application>
59
+ <Scene />
60
+ </Application>
61
+ <div className="hud">
62
+ <section className="panel">
63
+ <h1>Scene Editor</h1>
64
+ <p>Select an object, then translate, rotate or scale it.</p>
65
+ <div className="controls">
66
+ <button data-target="0" aria-pressed="true">
67
+ Cube
68
+ </button>
69
+ <button data-target="1">Sphere</button>
70
+ </div>
71
+ <div className="controls">
72
+ <button data-mode="translate" aria-pressed="true">
73
+ Move
74
+ </button>
75
+ <button data-mode="rotate">Rotate</button>
76
+ <button data-mode="scale">Scale</button>
77
+ </div>
78
+ </section>
79
+ </div>
80
+ </>
81
+ );
82
+ }
83
+
84
+ export default App;
@@ -26,9 +26,9 @@ function App() {
26
26
  return (
27
27
  <Application graphicsDeviceOptions={{ antialias: false }}>
28
28
  {/* Create a camera entity with orbit controls */}
29
- <Entity name="camera" position={[0, 0, 2.5]}>
30
- <Camera />
31
- <Script script={CameraControls} />
29
+ <Entity name="camera" position={[0, 0, 2.8]}>
30
+ <Camera clearColor="#8f949e" />
31
+ <Script script={CameraControls} sceneSize={2} />
32
32
  </Entity>
33
33
 
34
34
  <Splat />
@@ -0,0 +1,38 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Render, Script } from '@playcanvas/react/components';
3
+ import { useMaterial } from '@playcanvas/react/hooks';
4
+
5
+ import { SpriteGame } from './sprite-game';
6
+ import './starter.css';
7
+
8
+ function App() {
9
+ const dirt = useMaterial({ emissive: '#734729' });
10
+ const grass = useMaterial({ emissive: '#479e57' });
11
+
12
+ return (
13
+ <>
14
+ <Application>
15
+ <Entity name="camera" position={[0, 0, 10]}>
16
+ <Camera projection={1} orthoHeight={4.4} clearColor="#5cb9e6" />
17
+ </Entity>
18
+ <Entity name="dirt" position={[0, -3.39, 0]} scale={[20, 2.6, 1]}>
19
+ <Render type="box" material={dirt} />
20
+ </Entity>
21
+ <Entity name="grass" position={[0, -2, 0]} scale={[20, 0.18, 1]}>
22
+ <Render type="box" material={grass} />
23
+ </Entity>
24
+ <Entity name="game" position={[0, -2.15, 0]}>
25
+ <Script script={SpriteGame} />
26
+ </Entity>
27
+ </Application>
28
+ <div className="hud">
29
+ <section className="panel">
30
+ <h1>Sprite Game</h1>
31
+ <p>Use A/D or the arrow keys to move and Space to jump.</p>
32
+ </section>
33
+ </div>
34
+ </>
35
+ );
36
+ }
37
+
38
+ export default App;
@@ -0,0 +1,193 @@
1
+ import { Application, Entity } from '@playcanvas/react';
2
+ import { Camera, Collision, Light, Render, RigidBody } from '@playcanvas/react/components';
3
+ import { useMaterial, usePhysics } from '@playcanvas/react/hooks';
4
+ import type { Entity as PcEntity, StandardMaterial } from 'playcanvas';
5
+ import { useEffect, useRef } from 'react';
6
+
7
+ import { addThirdPersonController } from './controller';
8
+ import './starter.css';
9
+
10
+ const BLOCK_SCALE: [number, number, number] = [2.4, 1, 2.4];
11
+ const BLOCK_HALF_EXTENTS: [number, number, number] = [1.2, 0.5, 1.2];
12
+ const BLOCKS: [number, number, number][] = [
13
+ [-4.5, 0.5, -3.5],
14
+ [4.2, 0.5, -2.8],
15
+ [-3.7, 0.5, 3.2],
16
+ [3.8, 0.5, 3.6]
17
+ ];
18
+ const TREES: [number, number, number][] = [
19
+ [-7, -6, 1.1],
20
+ [6.5, -6.5, 1.25],
21
+ [-7.5, 5, 0.9],
22
+ [7, 5.5, 1.15],
23
+ [-2.2, -8, 0.85]
24
+ ];
25
+ const PINE_LAYERS: [number, number, number][] = [
26
+ [1.9, 2.1, 1.8],
27
+ [2.65, 1.6, 1.55],
28
+ [3.3, 1.1, 1.4]
29
+ ];
30
+
31
+ function Block({
32
+ name,
33
+ position,
34
+ scale,
35
+ material
36
+ }: {
37
+ name: string;
38
+ position: [number, number, number];
39
+ scale: [number, number, number];
40
+ material: StandardMaterial;
41
+ }) {
42
+ return (
43
+ <Entity name={name} position={position} scale={scale}>
44
+ <Render type="box" material={material} />
45
+ <Collision type="box" halfExtents={[scale[0] / 2, scale[1] / 2, scale[2] / 2]} />
46
+ <RigidBody type="static" />
47
+ </Entity>
48
+ );
49
+ }
50
+
51
+ function Pine({
52
+ name,
53
+ position: [x, z],
54
+ size,
55
+ wood,
56
+ leaves,
57
+ tips
58
+ }: {
59
+ name: string;
60
+ position: [number, number];
61
+ size: number;
62
+ wood: StandardMaterial;
63
+ leaves: StandardMaterial;
64
+ tips: StandardMaterial;
65
+ }) {
66
+ return (
67
+ <>
68
+ <Block
69
+ name={`${name}-trunk`}
70
+ position={[x, size * 0.9, z]}
71
+ scale={[size * 0.38, size * 1.8, size * 0.38]}
72
+ material={wood}
73
+ />
74
+ {PINE_LAYERS.map(([y, width, height], i) => (
75
+ <Entity
76
+ key={i}
77
+ name={`${name}-crown-${i}`}
78
+ position={[x, size * y, z]}
79
+ scale={[size * width, size * height, size * width]}
80
+ >
81
+ <Render type="cone" material={i === 1 ? tips : leaves} />
82
+ </Entity>
83
+ ))}
84
+ </>
85
+ );
86
+ }
87
+
88
+ function Scene() {
89
+ const player = useRef<PcEntity>(null);
90
+ const camera = useRef<PcEntity>(null);
91
+ const model = useRef<PcEntity>(null);
92
+ const { isPhysicsLoaded } = usePhysics();
93
+ const shirt = useMaterial({ diffuse: '#268bd2' });
94
+ const skin = useMaterial({ diffuse: '#c78759' });
95
+ const pants = useMaterial({ diffuse: '#2e4a85' });
96
+ const ground = useMaterial({ diffuse: '#33662e' });
97
+ const block = useMaterial({ diffuse: '#57594d' });
98
+ const wood = useMaterial({ diffuse: '#572e14' });
99
+ const leaves = useMaterial({ diffuse: '#144d1f' });
100
+ const tips = useMaterial({ diffuse: '#1f6629' });
101
+
102
+ useEffect(() => {
103
+ if (isPhysicsLoaded && player.current && camera.current && model.current) {
104
+ return addThirdPersonController(player.current, camera.current, model.current);
105
+ }
106
+ }, [isPhysicsLoaded]);
107
+
108
+ return (
109
+ <>
110
+ <Entity name="camera" ref={camera} position={[0, 3, 6]}>
111
+ <Camera clearColor="#7ab8e6" />
112
+ </Entity>
113
+ <Entity name="light" rotation={[45, 30, 0]}>
114
+ <Light type="directional" intensity={2.5} castShadows shadowBias={0.2} normalOffsetBias={0.05} />
115
+ </Entity>
116
+ <Entity name="ground" position={[0, -0.1, 0]} scale={[20, 0.2, 20]}>
117
+ <Render type="box" material={ground} />
118
+ <Collision type="box" halfExtents={[10, 0.1, 10]} />
119
+ <RigidBody type="static" />
120
+ </Entity>
121
+ {BLOCKS.map((position, i) => (
122
+ <Entity key={i} name={`block-${i}`} position={position} rotation={[0, i * 25, 0]} scale={BLOCK_SCALE}>
123
+ <Render type="box" material={block} />
124
+ <Collision type="box" halfExtents={BLOCK_HALF_EXTENTS} />
125
+ <RigidBody type="static" />
126
+ </Entity>
127
+ ))}
128
+ {TREES.map(([x, z, size], i) => (
129
+ <Pine
130
+ key={i}
131
+ name={`tree-${i}`}
132
+ position={[x, z]}
133
+ size={size}
134
+ wood={wood}
135
+ leaves={leaves}
136
+ tips={tips}
137
+ />
138
+ ))}
139
+ <Block name="fallen-log" position={[0, 0.35, 6]} scale={[3.8, 0.7, 0.7]} material={wood} />
140
+ <Block name="stump" position={[-1.8, 0.4, -2.2]} scale={[0.9, 0.8, 0.9]} material={wood} />
141
+ <Entity name="player" ref={player} position={[0, 1.1, 0]}>
142
+ <Collision type="capsule" radius={0.5} height={2.4} linearOffset={[0, 0.1, 0]} />
143
+ <RigidBody type="dynamic" mass={70} angularFactor={[0, 0, 0]} />
144
+ <Entity name="character" ref={model} position={[0, 0.2, 0]}>
145
+ <Entity name="body" scale={[0.56, 0.88, 0.38]}>
146
+ <Render type="box" material={shirt} />
147
+ </Entity>
148
+ <Entity name="head" position={[0, 0.76, 0]} scale={[0.52, 0.6, 0.52]}>
149
+ <Render type="box" material={skin} />
150
+ </Entity>
151
+ <Entity name="left-arm" position={[-0.38, 0.44, 0]}>
152
+ <Entity position={[0, -0.44, 0]} scale={[0.18, 0.88, 0.32]}>
153
+ <Render type="box" material={shirt} />
154
+ </Entity>
155
+ </Entity>
156
+ <Entity name="right-arm" position={[0.38, 0.44, 0]}>
157
+ <Entity position={[0, -0.44, 0]} scale={[0.18, 0.88, 0.32]}>
158
+ <Render type="box" material={shirt} />
159
+ </Entity>
160
+ </Entity>
161
+ <Entity name="left-leg" position={[-0.15, -0.44, 0]}>
162
+ <Entity position={[0, -0.43, 0]} scale={[0.24, 0.86, 0.34]}>
163
+ <Render type="box" material={pants} />
164
+ </Entity>
165
+ </Entity>
166
+ <Entity name="right-leg" position={[0.15, -0.44, 0]}>
167
+ <Entity position={[0, -0.43, 0]} scale={[0.24, 0.86, 0.34]}>
168
+ <Render type="box" material={pants} />
169
+ </Entity>
170
+ </Entity>
171
+ </Entity>
172
+ </Entity>
173
+ </>
174
+ );
175
+ }
176
+
177
+ function App() {
178
+ return (
179
+ <>
180
+ <Application usePhysics>
181
+ <Scene />
182
+ </Application>
183
+ <div className="hud">
184
+ <section className="panel">
185
+ <h1>Third-Person Controller</h1>
186
+ <p>Click the scene, then use WASD to move, mouse to orbit and Space to jump.</p>
187
+ </section>
188
+ </div>
189
+ </>
190
+ );
191
+ }
192
+
193
+ export default App;