react-three-game 0.0.60 → 0.0.61
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/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -204
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- package/src/tools/prefabeditor/utils.ts +0 -312
|
@@ -1,472 +0,0 @@
|
|
|
1
|
-
# Advanced Physics & Patterns
|
|
2
|
-
|
|
3
|
-
## Physics Type Decision Tree
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
Need physics?
|
|
7
|
-
├─ No → Don't add Physics component
|
|
8
|
-
└─ Yes → Does it move?
|
|
9
|
-
├─ Never moves (walls, floor, static props)
|
|
10
|
-
│ └─ type: "fixed"
|
|
11
|
-
│
|
|
12
|
-
├─ Moves via forces/gravity (balls, boxes, ragdolls)
|
|
13
|
-
│ └─ type: "dynamic"
|
|
14
|
-
│ ├─ Fast moving? → ccd: true
|
|
15
|
-
│ └─ Heavy? → mass: 10+
|
|
16
|
-
│
|
|
17
|
-
├─ Scripted animation (moving platforms, doors)
|
|
18
|
-
│ └─ type: "kinematicPosition"
|
|
19
|
-
│ └─ Update transform via updateNodeById
|
|
20
|
-
│
|
|
21
|
-
└─ Velocity-driven (conveyor belts, wind zones)
|
|
22
|
-
└─ type: "kinematicVelocity"
|
|
23
|
-
└─ Set velocity via RigidBody ref
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Type descriptions**:
|
|
27
|
-
- **fixed**: Immovable, infinite mass (ground, walls, buildings)
|
|
28
|
-
- **dynamic**: Affected by forces and gravity (player, projectiles, props)
|
|
29
|
-
- **kinematicPosition**: Move via code, push dynamic bodies (elevators, doors)
|
|
30
|
-
- **kinematicVelocity**: Set constant velocity, push dynamic bodies (conveyors)
|
|
31
|
-
|
|
32
|
-
**Performance tip**: Use `fixed` for anything that never moves - it's cheapest.
|
|
33
|
-
|
|
34
|
-
## Physics Material Properties
|
|
35
|
-
|
|
36
|
-
Complete reference for `Physics` component properties:
|
|
37
|
-
|
|
38
|
-
| Property | Type | Default | Description |
|
|
39
|
-
|----------|------|---------|-------------|
|
|
40
|
-
| `type` | `'dynamic'` \| `'fixed'` \| `'kinematicPosition'` \| `'kinematicVelocity'` | `'dynamic'` | Body type (see decision tree above) |
|
|
41
|
-
| `mass` | `number` | `1` | Body mass (dynamic only) |
|
|
42
|
-
| `restitution` | `number` | `0` | Bounciness (0 = no bounce, 1 = perfect bounce) |
|
|
43
|
-
| `friction` | `number` | `0.5` | Surface friction (0 = ice, 1+ = sticky) |
|
|
44
|
-
| `linearDamping` | `number` | `0` | Velocity decay (0 = none, 1 = full stop) |
|
|
45
|
-
| `angularDamping` | `number` | `0` | Rotation decay |
|
|
46
|
-
| `gravityScale` | `number` | `1` | Gravity multiplier (0 = floating, 2 = heavy) |
|
|
47
|
-
| `lockTranslations` | `boolean` | `false` | Freeze position |
|
|
48
|
-
| `lockRotations` | `boolean` | `false` | Freeze rotation |
|
|
49
|
-
| `enabledTranslations` | `[bool, bool, bool]` | `[true, true, true]` | Lock per axis (X, Y, Z) |
|
|
50
|
-
| `enabledRotations` | `[bool, bool, bool]` | `[true, true, true]` | Lock rotation per axis |
|
|
51
|
-
| `colliders` | `'hull'` \| `'trimesh'` \| `'cuboid'` \| `'ball'` | auto | Collider shape override (`fixed` defaults to `trimesh`, others to `hull`) |
|
|
52
|
-
| `ccd` | `boolean` | `false` | Continuous collision detection (fast objects) |
|
|
53
|
-
| `sensor` | `boolean` | `false` | Trigger only, no collision response |
|
|
54
|
-
| `activeCollisionTypes` | `'all'` | - | Enable kinematic/fixed collision detection (default: dynamic only) |
|
|
55
|
-
| `collisionGroups` | `number` | - | Rapier collision groups bitfield |
|
|
56
|
-
| `solverGroups` | `number` | - | Rapier solver groups bitfield |
|
|
57
|
-
|
|
58
|
-
**Example - Bouncy Ball**:
|
|
59
|
-
```json
|
|
60
|
-
{
|
|
61
|
-
"physics": {
|
|
62
|
-
"type": "Physics",
|
|
63
|
-
"properties": {
|
|
64
|
-
"type": "dynamic",
|
|
65
|
-
"mass": 0.5,
|
|
66
|
-
"restitution": 0.9,
|
|
67
|
-
"friction": 0.1,
|
|
68
|
-
"linearDamping": 0.05
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Example - Ice Surface**:
|
|
75
|
-
```json
|
|
76
|
-
{
|
|
77
|
-
"physics": {
|
|
78
|
-
"type": "Physics",
|
|
79
|
-
"properties": {
|
|
80
|
-
"type": "fixed",
|
|
81
|
-
"friction": 0,
|
|
82
|
-
"restitution": 0.1
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Force & Impulse Application
|
|
89
|
-
|
|
90
|
-
Access RigidBody refs via `PrefabRootRef.rigidBodyRefs` to apply physics forces to prefab objects:
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
import { useRef, useEffect } from 'react';
|
|
94
|
-
import { PrefabEditor } from 'react-three-game';
|
|
95
|
-
import type { PrefabEditorRef } from 'react-three-game';
|
|
96
|
-
import type { RapierRigidBody } from '@react-three/rapier';
|
|
97
|
-
|
|
98
|
-
function ForceApplier({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef> }) {
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
const interval = setInterval(() => {
|
|
101
|
-
const rootRef = editorRef.current?.rootRef.current;
|
|
102
|
-
if (!rootRef) return;
|
|
103
|
-
|
|
104
|
-
// Access RigidBody ref by node ID
|
|
105
|
-
const rigidBody = rootRef.rigidBodyRefs.get('ball') as RapierRigidBody;
|
|
106
|
-
|
|
107
|
-
if (rigidBody) {
|
|
108
|
-
// Apply upward impulse
|
|
109
|
-
rigidBody.applyImpulse({ x: 0, y: 5, z: 0 }, true);
|
|
110
|
-
|
|
111
|
-
// Or apply continuous force
|
|
112
|
-
rigidBody.addForce({ x: 0, y: 10, z: 0 }, true);
|
|
113
|
-
|
|
114
|
-
// Apply torque
|
|
115
|
-
rigidBody.applyTorqueImpulse({ x: 0, y: 1, z: 0 }, true);
|
|
116
|
-
}
|
|
117
|
-
}, 2000);
|
|
118
|
-
|
|
119
|
-
return () => clearInterval(interval);
|
|
120
|
-
}, [editorRef]);
|
|
121
|
-
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function Scene() {
|
|
126
|
-
const editorRef = useRef<PrefabEditorRef>(null);
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<PrefabEditor ref={editorRef} initialPrefab={prefab}>
|
|
130
|
-
<ForceApplier editorRef={editorRef} />
|
|
131
|
-
</PrefabEditor>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Alternative: Custom R3F components**
|
|
137
|
-
|
|
138
|
-
For fully custom physics objects, create R3F components with their own RigidBody refs:
|
|
139
|
-
|
|
140
|
-
```tsx
|
|
141
|
-
import { useRef } from 'react';
|
|
142
|
-
import { RigidBody } from '@react-three/rapier';
|
|
143
|
-
import type { RapierRigidBody } from '@react-three/rapier';
|
|
144
|
-
import { useFrame } from '@react-three/fiber';
|
|
145
|
-
import { PrefabEditor } from 'react-three-game';
|
|
146
|
-
|
|
147
|
-
function PhysicsBall() {
|
|
148
|
-
const rigidBodyRef = useRef<RapierRigidBody>(null);
|
|
149
|
-
|
|
150
|
-
useFrame(() => {
|
|
151
|
-
if (rigidBodyRef.current) {
|
|
152
|
-
// Apply jump force on interval
|
|
153
|
-
rigidBodyRef.current.applyImpulse({ x: 0, y: 5, z: 0 }, true);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
return (
|
|
158
|
-
<RigidBody ref={rigidBodyRef} position={[0, 5, 0]} type="dynamic">
|
|
159
|
-
<mesh castShadow>
|
|
160
|
-
<sphereGeometry args={[0.5, 32, 32]} />
|
|
161
|
-
<meshStandardMaterial color="orange" />
|
|
162
|
-
</mesh>
|
|
163
|
-
</RigidBody>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
<PrefabEditor initialPrefab={prefab}>
|
|
168
|
-
<PhysicsBall />
|
|
169
|
-
</PrefabEditor>
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Alternative: Kinematic position updates**
|
|
173
|
-
|
|
174
|
-
For smooth animated movement without forces, use `kinematicPosition` and update via `updateNodeById`:
|
|
175
|
-
|
|
176
|
-
```tsx
|
|
177
|
-
import { useRef } from 'react';
|
|
178
|
-
import { useFrame } from '@react-three/fiber';
|
|
179
|
-
import { PrefabEditor, updateNodeById } from 'react-three-game';
|
|
180
|
-
import type { PrefabEditorRef } from 'react-three-game';
|
|
181
|
-
|
|
182
|
-
function KinematicMover({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef> }) {
|
|
183
|
-
useFrame(({ clock }) => {
|
|
184
|
-
const prefab = editorRef.current?.prefab;
|
|
185
|
-
if (!prefab) return;
|
|
186
|
-
|
|
187
|
-
const y = 2 + Math.sin(clock.elapsedTime * 2) * 3;
|
|
188
|
-
|
|
189
|
-
const newRoot = updateNodeById(prefab.root, "platform", node => ({
|
|
190
|
-
...node,
|
|
191
|
-
components: {
|
|
192
|
-
...node.components,
|
|
193
|
-
transform: {
|
|
194
|
-
...node.components!.transform!,
|
|
195
|
-
properties: {
|
|
196
|
-
...node.components!.transform!.properties,
|
|
197
|
-
position: [0, y, 0]
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}));
|
|
202
|
-
|
|
203
|
-
editorRef.current!.setPrefab({ ...prefab, root: newRoot });
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**Rapier RigidBody methods**:
|
|
211
|
-
- `applyImpulse(vector, wakeUp)` - Instantaneous velocity change
|
|
212
|
-
- `addForce(vector, wakeUp)` - Continuous force application
|
|
213
|
-
- `applyTorqueImpulse(vector, wakeUp)` - Rotational impulse
|
|
214
|
-
- `addTorque(vector, wakeUp)` - Continuous torque
|
|
215
|
-
- `setLinvel(vector, wakeUp)` - Set linear velocity directly
|
|
216
|
-
- `setAngvel(vector, wakeUp)` - Set angular velocity directly
|
|
217
|
-
|
|
218
|
-
## Tilted Surfaces & Containment
|
|
219
|
-
|
|
220
|
-
**⚠️ Tilted walls don't contain objects** - physics objects slide off angled surfaces.
|
|
221
|
-
|
|
222
|
-
### ❌ Wrong Approach
|
|
223
|
-
```json
|
|
224
|
-
{
|
|
225
|
-
"id": "tilted-wall",
|
|
226
|
-
"components": {
|
|
227
|
-
"transform": { "type": "Transform", "properties": { "rotation": [0, 0, 0.3] } },
|
|
228
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [10, 5, 1] } },
|
|
229
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
Objects will **slide off** the tilted surface.
|
|
234
|
-
|
|
235
|
-
### ✅ Correct Pattern - Perpendicular Walls
|
|
236
|
-
```json
|
|
237
|
-
{
|
|
238
|
-
"id": "container",
|
|
239
|
-
"children": [
|
|
240
|
-
{
|
|
241
|
-
"id": "floor",
|
|
242
|
-
"components": {
|
|
243
|
-
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
|
|
244
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 1, 20] } },
|
|
245
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
"id": "wall-north",
|
|
250
|
-
"components": {
|
|
251
|
-
"transform": { "type": "Transform", "properties": { "position": [0, 2.5, -10] } },
|
|
252
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 5, 1] } },
|
|
253
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
"id": "wall-south",
|
|
258
|
-
"components": {
|
|
259
|
-
"transform": { "type": "Transform", "properties": { "position": [0, 2.5, 10] } },
|
|
260
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [20, 5, 1] } },
|
|
261
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
"id": "wall-east",
|
|
266
|
-
"components": {
|
|
267
|
-
"transform": { "type": "Transform", "properties": { "position": [10, 2.5, 0] } },
|
|
268
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [1, 5, 20] } },
|
|
269
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
270
|
-
}
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
"id": "wall-west",
|
|
274
|
-
"components": {
|
|
275
|
-
"transform": { "type": "Transform", "properties": { "position": [-10, 2.5, 0] } },
|
|
276
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [1, 5, 20] } },
|
|
277
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
**Key principle**: Walls must be **perpendicular to gravity** to contain dynamic objects.
|
|
285
|
-
|
|
286
|
-
## Instanced Physics
|
|
287
|
-
|
|
288
|
-
When using `"instanced": true` on models, physics behaves differently than standard objects. Physics instancing is designed for batched `fixed` and `dynamic` bodies, where instances of the same model share an `InstancedRigidBodies` path for better performance.
|
|
289
|
-
|
|
290
|
-
### Standard vs Instanced Physics
|
|
291
|
-
|
|
292
|
-
| Aspect | Standard Physics | Instanced Physics |
|
|
293
|
-
|--------|------------------|-------------------|
|
|
294
|
-
| RigidBody Component | Individual `<RigidBody>` per object | Single `<InstancedRigidBodies>` group per model + supported physics type |
|
|
295
|
-
| Ref Access | `rigidBodyRefs.get(nodeId)` returns single RigidBody | Not accessible via `rigidBodyRefs` |
|
|
296
|
-
| Force Application | Direct per-object | Must access via InstancedRigidBodies ref |
|
|
297
|
-
| Collider Type | `hull` (dynamic) or `trimesh` (fixed) | Auto-selected by instanced physics path |
|
|
298
|
-
| Performance | One draw call per object | One draw call for all instances |
|
|
299
|
-
|
|
300
|
-
### Defining Instanced Objects
|
|
301
|
-
|
|
302
|
-
Set `"instanced": true` in the model component. **Instances with the same model path and supported physics type are automatically batched**:
|
|
303
|
-
|
|
304
|
-
```json
|
|
305
|
-
{
|
|
306
|
-
"id": "tree1",
|
|
307
|
-
"components": {
|
|
308
|
-
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
|
|
309
|
-
"model": { "type": "Model", "properties": { "filename": "models/tree.glb", "instanced": true } },
|
|
310
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Add multiple instances - they'll be automatically batched:
|
|
316
|
-
|
|
317
|
-
```json
|
|
318
|
-
{
|
|
319
|
-
"id": "tree2",
|
|
320
|
-
"components": {
|
|
321
|
-
"transform": { "type": "Transform", "properties": { "position": [5, 0, 3] } },
|
|
322
|
-
"model": { "type": "Model", "properties": { "filename": "models/tree.glb", "instanced": true } },
|
|
323
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### Force Application on Instanced Objects
|
|
329
|
-
|
|
330
|
-
**Instanced physics bodies are not individually accessible.** For objects requiring force/impulse control, kinematic motion, or per-body refs, use non-instanced physics (`"instanced": false` or omit the property).
|
|
331
|
-
|
|
332
|
-
### When to Use Instanced Physics
|
|
333
|
-
|
|
334
|
-
✅ **Good for:**
|
|
335
|
-
- Many copies of the same static object (trees, rocks, buildings)
|
|
336
|
-
- Large scenes with 100+ similar objects
|
|
337
|
-
- Fixed physics bodies that never move
|
|
338
|
-
- Background props and decorations
|
|
339
|
-
|
|
340
|
-
❌ **Avoid for:**
|
|
341
|
-
- Objects requiring individual force/impulse control
|
|
342
|
-
- Dynamic objects with unique behaviors
|
|
343
|
-
- Objects that need to be individually removed/spawned
|
|
344
|
-
- Fewer than ~20 instances (overhead not worth it)
|
|
345
|
-
|
|
346
|
-
### Performance Notes
|
|
347
|
-
|
|
348
|
-
- **Batching**: All instances with the same `filename` and `physics.type` are rendered in a single draw call
|
|
349
|
-
- **Supported body types**: The instanced physics path is intended for `fixed` and `dynamic` bodies; use standard non-instanced physics for kinematic bodies
|
|
350
|
-
- **Scale handling**: Visual scale is applied per-instance, but collider scale may differ
|
|
351
|
-
- **Transform updates**: Use `updateNodeById` to move instances (triggers re-sync)
|
|
352
|
-
- **Memory**: One set of GPU buffers shared across all instances
|
|
353
|
-
|
|
354
|
-
## Sensors & Collision Events
|
|
355
|
-
|
|
356
|
-
Sensors are colliders that detect intersections without generating physical contact forces. Use them for trigger zones, pickup areas, damage zones, and gameplay triggers.
|
|
357
|
-
|
|
358
|
-
### Creating a Sensor
|
|
359
|
-
|
|
360
|
-
Set `sensor: true` in the Physics component:
|
|
361
|
-
|
|
362
|
-
```json
|
|
363
|
-
{
|
|
364
|
-
"id": "trigger-zone",
|
|
365
|
-
"components": {
|
|
366
|
-
"transform": { "type": "Transform", "properties": { "position": [0, 1, 0] } },
|
|
367
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [4, 2, 4] } },
|
|
368
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed", "sensor": true } }
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**Kinematic/Fixed Collision Detection**: By default, sensors only detect `dynamic` bodies. For kinematic sensors (like bullets) or to detect kinematic players, add `"activeCollisionTypes": "all"`:
|
|
374
|
-
|
|
375
|
-
```json
|
|
376
|
-
{
|
|
377
|
-
"physics": {
|
|
378
|
-
"type": "Physics",
|
|
379
|
-
"properties": {
|
|
380
|
-
"type": "kinematicPosition",
|
|
381
|
-
"sensor": true,
|
|
382
|
-
"activeCollisionTypes": "all" // Detects walls, floors, kinematic bodies
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### Physics Event Payload
|
|
389
|
-
|
|
390
|
-
All physics events include:
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
{
|
|
394
|
-
sourceEntityId: string; // The prefab entity that owns the collider
|
|
395
|
-
targetEntityId: string | null; // The other entity (if it's a prefab entity)
|
|
396
|
-
targetRigidBody: RapierRigidBody; // Direct access to the other RigidBody
|
|
397
|
-
}
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
`targetEntityId` is `null` when colliding with non-prefab physics bodies (custom R3F components). Use `targetRigidBody` to inspect those.
|
|
401
|
-
|
|
402
|
-
### Common Sensor Patterns
|
|
403
|
-
|
|
404
|
-
**Pickup Item:**
|
|
405
|
-
```json
|
|
406
|
-
{
|
|
407
|
-
"id": "coin",
|
|
408
|
-
"components": {
|
|
409
|
-
"transform": { "type": "Transform", "properties": { "position": [5, 0.5, 0] } },
|
|
410
|
-
"model": { "type": "Model", "properties": { "filename": "models/coin.glb" } },
|
|
411
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed", "sensor": true } }
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
```tsx
|
|
417
|
-
useGameEvent('sensor:enter', (payload) => {
|
|
418
|
-
if (payload.sourceEntityId === 'coin' && payload.targetEntityId === 'player') {
|
|
419
|
-
removeCoin();
|
|
420
|
-
gameEvents.emit('score:change', { delta: 100, total: score + 100 });
|
|
421
|
-
}
|
|
422
|
-
}, [score]);
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
**Damage Zone:**
|
|
426
|
-
```json
|
|
427
|
-
{
|
|
428
|
-
"id": "lava",
|
|
429
|
-
"components": {
|
|
430
|
-
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
|
|
431
|
-
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [10, 0.5, 10] } },
|
|
432
|
-
"material": { "type": "Material", "properties": { "color": "#ff4400" } },
|
|
433
|
-
"physics": { "type": "Physics", "properties": { "type": "fixed", "sensor": true } }
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
```tsx
|
|
439
|
-
useGameEvent('sensor:enter', ({ sourceEntityId, targetEntityId }) => {
|
|
440
|
-
if (sourceEntityId === 'lava') {
|
|
441
|
-
gameEvents.emit('player:damage', { entityId: targetEntityId, amount: 50 });
|
|
442
|
-
}
|
|
443
|
-
}, []);
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
**Level Transition:**
|
|
447
|
-
```tsx
|
|
448
|
-
useGameEvent('sensor:enter', ({ sourceEntityId, targetEntityId }) => {
|
|
449
|
-
if (sourceEntityId === 'exit-door' && targetEntityId === 'player') {
|
|
450
|
-
loadNextLevel();
|
|
451
|
-
}
|
|
452
|
-
}, []);
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Interop with Custom R3F Physics
|
|
456
|
-
|
|
457
|
-
For custom RigidBody components to participate in the event system, set `userData.entityId`:
|
|
458
|
-
|
|
459
|
-
```tsx
|
|
460
|
-
<RigidBody userData={{ entityId: 'player' }}>
|
|
461
|
-
<PlayerMesh />
|
|
462
|
-
</RigidBody>
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
Now when prefab sensors detect this body, `targetEntityId` will be `'player'`.
|
|
466
|
-
|
|
467
|
-
### Tips
|
|
468
|
-
|
|
469
|
-
- Sensors fire events for **all** intersecting bodies - filter by ID
|
|
470
|
-
- `sensor:exit` fires when something leaves a sensor zone
|
|
471
|
-
- `collision:enter/exit` fires for non-sensor physics bodies
|
|
472
|
-
- Entity IDs stored in `RigidBody.userData.entityId`
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
class SoundManager {
|
|
3
|
-
private static _instance: SoundManager | null = null
|
|
4
|
-
|
|
5
|
-
public context: AudioContext
|
|
6
|
-
private buffers = new Map<string, AudioBuffer>()
|
|
7
|
-
|
|
8
|
-
private masterGain: GainNode
|
|
9
|
-
private sfxGain: GainNode
|
|
10
|
-
private musicGain: GainNode
|
|
11
|
-
|
|
12
|
-
private constructor() {
|
|
13
|
-
const AudioCtx =
|
|
14
|
-
window.AudioContext || (window as any).webkitAudioContext
|
|
15
|
-
|
|
16
|
-
this.context = new AudioCtx()
|
|
17
|
-
|
|
18
|
-
this.masterGain = this.context.createGain()
|
|
19
|
-
this.sfxGain = this.context.createGain()
|
|
20
|
-
this.musicGain = this.context.createGain()
|
|
21
|
-
|
|
22
|
-
this.sfxGain.connect(this.masterGain)
|
|
23
|
-
this.musicGain.connect(this.masterGain)
|
|
24
|
-
this.masterGain.connect(this.context.destination)
|
|
25
|
-
|
|
26
|
-
this.masterGain.gain.value = 1
|
|
27
|
-
this.sfxGain.gain.value = 1
|
|
28
|
-
this.musicGain.gain.value = 1
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Singleton accessor */
|
|
32
|
-
static get instance(): SoundManager {
|
|
33
|
-
if (typeof window === 'undefined') {
|
|
34
|
-
// Return a dummy instance for SSR
|
|
35
|
-
return new Proxy({} as SoundManager, {
|
|
36
|
-
get: () => () => {}
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
if (!SoundManager._instance) {
|
|
40
|
-
SoundManager._instance = new SoundManager()
|
|
41
|
-
}
|
|
42
|
-
return SoundManager._instance
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Required once after user gesture (browser) */
|
|
46
|
-
resume() {
|
|
47
|
-
if (this.context.state !== "running") {
|
|
48
|
-
this.context.resume()
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Preload a sound from URL */
|
|
53
|
-
async load(path: string, url: string) {
|
|
54
|
-
if (this.buffers.has(path)) return
|
|
55
|
-
|
|
56
|
-
const res = await fetch(url)
|
|
57
|
-
const arrayBuffer = await res.arrayBuffer()
|
|
58
|
-
const buffer = await this.context.decodeAudioData(arrayBuffer)
|
|
59
|
-
|
|
60
|
-
this.buffers.set(path, buffer)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Play from already-loaded buffer (fails silently if not loaded) */
|
|
64
|
-
playSync(
|
|
65
|
-
path: string,
|
|
66
|
-
{
|
|
67
|
-
volume = 1,
|
|
68
|
-
playbackRate = 1,
|
|
69
|
-
detune = 0,
|
|
70
|
-
pitch = 1,
|
|
71
|
-
}: {
|
|
72
|
-
volume?: number
|
|
73
|
-
playbackRate?: number
|
|
74
|
-
detune?: number
|
|
75
|
-
pitch?: number
|
|
76
|
-
} = {}
|
|
77
|
-
) {
|
|
78
|
-
this.resume()
|
|
79
|
-
|
|
80
|
-
const buffer = this.buffers.get(path)
|
|
81
|
-
if (!buffer) return
|
|
82
|
-
|
|
83
|
-
const src = this.context.createBufferSource()
|
|
84
|
-
const gain = this.context.createGain()
|
|
85
|
-
|
|
86
|
-
src.buffer = buffer
|
|
87
|
-
src.playbackRate.value = playbackRate * pitch
|
|
88
|
-
src.detune.value = detune
|
|
89
|
-
|
|
90
|
-
gain.gain.value = volume
|
|
91
|
-
|
|
92
|
-
src.connect(gain)
|
|
93
|
-
gain.connect(this.sfxGain)
|
|
94
|
-
|
|
95
|
-
src.start()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Load and play SFX - accepts file path directly */
|
|
99
|
-
async play(
|
|
100
|
-
path: string,
|
|
101
|
-
options?: {
|
|
102
|
-
volume?: number
|
|
103
|
-
playbackRate?: number
|
|
104
|
-
detune?: number
|
|
105
|
-
pitch?: number
|
|
106
|
-
}
|
|
107
|
-
) {
|
|
108
|
-
// Auto-load from path if not already loaded
|
|
109
|
-
if (!this.buffers.has(path)) {
|
|
110
|
-
await this.load(path, path)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
this.playSync(path, options)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Volume controls */
|
|
117
|
-
setMasterVolume(v: number) {
|
|
118
|
-
this.masterGain.gain.value = v
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
setSfxVolume(v: number) {
|
|
122
|
-
this.sfxGain.gain.value = v
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
setMusicVolume(v: number) {
|
|
126
|
-
this.musicGain.gain.value = v
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export const sound = SoundManager.instance
|
package/src/helpers/index.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { GameObject } from "../tools/prefabeditor/types";
|
|
2
|
-
|
|
3
|
-
export type Vec3 = [number, number, number];
|
|
4
|
-
|
|
5
|
-
export interface GroundOptions {
|
|
6
|
-
/** GameObject id. Defaults to "ground". */
|
|
7
|
-
id?: string;
|
|
8
|
-
|
|
9
|
-
/** Plane size. Defaults to 50. */
|
|
10
|
-
size?: number;
|
|
11
|
-
|
|
12
|
-
/** Transform overrides. */
|
|
13
|
-
position?: Vec3;
|
|
14
|
-
rotation?: Vec3;
|
|
15
|
-
scale?: Vec3;
|
|
16
|
-
|
|
17
|
-
/** Material overrides. */
|
|
18
|
-
color?: string;
|
|
19
|
-
texture?: string;
|
|
20
|
-
/** When true, set repeat wrapping. Defaults to true if texture is provided. */
|
|
21
|
-
repeat?: boolean;
|
|
22
|
-
/** Texture repeat counts when repeat=true. Defaults to [25,25]. */
|
|
23
|
-
repeatCount?: [number, number];
|
|
24
|
-
|
|
25
|
-
/** Physics body type. Defaults to "fixed". */
|
|
26
|
-
physicsType?: "fixed" | "dynamic" | "kinematic";
|
|
27
|
-
|
|
28
|
-
/** Set true to disable the node. */
|
|
29
|
-
disabled?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Create a ready-to-use plane ground GameObject.
|
|
34
|
-
*
|
|
35
|
-
* Designed to reduce prefab boilerplate:
|
|
36
|
-
* - Transform (rotated to lie flat)
|
|
37
|
-
* - Geometry (plane)
|
|
38
|
-
* - Material (optional texture + repeat)
|
|
39
|
-
* - Physics (fixed by default)
|
|
40
|
-
*/
|
|
41
|
-
export function ground(options: GroundOptions = {}): GameObject {
|
|
42
|
-
const {
|
|
43
|
-
id = "ground",
|
|
44
|
-
size = 50,
|
|
45
|
-
position = [0, 0, 0],
|
|
46
|
-
rotation = [-Math.PI / 2, 0, 0],
|
|
47
|
-
scale = [1, 1, 1],
|
|
48
|
-
color = "#eeeeee",
|
|
49
|
-
texture,
|
|
50
|
-
repeat = texture ? true : false,
|
|
51
|
-
repeatCount = [25, 25],
|
|
52
|
-
physicsType = "fixed",
|
|
53
|
-
disabled = false,
|
|
54
|
-
} = options;
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
id,
|
|
58
|
-
disabled,
|
|
59
|
-
components: {
|
|
60
|
-
transform: {
|
|
61
|
-
type: "Transform",
|
|
62
|
-
properties: {
|
|
63
|
-
position,
|
|
64
|
-
rotation,
|
|
65
|
-
scale,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
geometry: {
|
|
69
|
-
type: "Geometry",
|
|
70
|
-
properties: {
|
|
71
|
-
geometryType: "plane",
|
|
72
|
-
args: [size, size],
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
material: {
|
|
76
|
-
type: "Material",
|
|
77
|
-
properties: {
|
|
78
|
-
color,
|
|
79
|
-
...(texture ? { texture } : {}),
|
|
80
|
-
...(repeat ? { repeat: true, repeatCount } : {}),
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
physics: {
|
|
84
|
-
type: "Physics",
|
|
85
|
-
properties: {
|
|
86
|
-
type: physicsType,
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|