@ue-too/dynamics 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +538 -4
- package/collision-filter.d.ts +115 -0
- package/constraint.d.ts +70 -0
- package/dynamic-tree.d.ts +16 -0
- package/index.d.ts +189 -0
- package/index.js +2 -1947
- package/index.js.map +9 -9
- package/package.json +3 -3
- package/pair-manager.d.ts +36 -0
- package/quadtree.d.ts +4 -0
- package/rigidbody.d.ts +21 -0
- package/world.d.ts +51 -0
package/README.md
CHANGED
|
@@ -1,7 +1,541 @@
|
|
|
1
|
-
# dynamics
|
|
1
|
+
# @ue-too/dynamics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
2D physics engine with rigid body dynamics and collision detection.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@ue-too/dynamics)
|
|
6
|
+
[](https://github.com/ue-too/ue-too/blob/main/LICENSE.txt)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
> **Experimental**: This package is an experimental implementation. Please **DO NOT** use this in production.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`@ue-too/dynamics` provides a complete 2D physics simulation engine featuring rigid body dynamics, collision detection, constraint solving, and performance optimizations like spatial indexing and sleeping bodies.
|
|
13
|
+
|
|
14
|
+
### Key Features
|
|
15
|
+
|
|
16
|
+
- **Rigid Body Physics**: Linear and angular velocity, mass, moment of inertia
|
|
17
|
+
- **Collision Detection**: Broad phase (spatial indexing) + narrow phase (SAT)
|
|
18
|
+
- **Collision Response**: Impulse-based resolution with friction and restitution
|
|
19
|
+
- **Constraints**: Pin joints (fixed and between bodies) with Baumgarte stabilization
|
|
20
|
+
- **Spatial Indexing**: QuadTree, Dynamic Tree, and Sweep-and-Prune algorithms
|
|
21
|
+
- **Sleeping System**: Automatically disable resting bodies for performance
|
|
22
|
+
- **Collision Filtering**: Category-based filtering with masks and groups
|
|
23
|
+
- **Shape Types**: Circles and convex polygons
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Using Bun:
|
|
28
|
+
```bash
|
|
29
|
+
bun add @ue-too/dynamics
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Using npm:
|
|
33
|
+
```bash
|
|
34
|
+
npm install @ue-too/dynamics
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
Here's a simple example creating a physics world with a falling ball:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { World, Circle, Polygon } from '@ue-too/dynamics';
|
|
43
|
+
|
|
44
|
+
// Create a physics world (2000x2000 world size)
|
|
45
|
+
const world = new World(2000, 2000, 'dynamictree');
|
|
46
|
+
|
|
47
|
+
// Create static ground
|
|
48
|
+
const ground = new Polygon(
|
|
49
|
+
{ x: 0, y: -100 }, // Position
|
|
50
|
+
[ // Vertices (local space)
|
|
51
|
+
{ x: -1000, y: 0 },
|
|
52
|
+
{ x: 1000, y: 0 },
|
|
53
|
+
{ x: 1000, y: 50 },
|
|
54
|
+
{ x: -1000, y: 50 }
|
|
55
|
+
],
|
|
56
|
+
0, // Rotation
|
|
57
|
+
100, // Mass (ignored for static bodies)
|
|
58
|
+
true // isStatic
|
|
59
|
+
);
|
|
60
|
+
world.addRigidBody('ground', ground);
|
|
61
|
+
|
|
62
|
+
// Create dynamic ball
|
|
63
|
+
const ball = new Circle(
|
|
64
|
+
{ x: 0, y: 200 }, // Position
|
|
65
|
+
20, // Radius
|
|
66
|
+
0, // Rotation
|
|
67
|
+
10, // Mass
|
|
68
|
+
false // isStatic
|
|
69
|
+
);
|
|
70
|
+
world.addRigidBody('ball', ball);
|
|
71
|
+
|
|
72
|
+
// Simulation loop (60 FPS)
|
|
73
|
+
setInterval(() => {
|
|
74
|
+
world.step(1/60); // deltaTime in seconds
|
|
75
|
+
|
|
76
|
+
console.log('Ball position:', ball.position);
|
|
77
|
+
}, 16);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Core Concepts
|
|
81
|
+
|
|
82
|
+
### Rigid Bodies
|
|
83
|
+
|
|
84
|
+
Rigid bodies are objects that don't deform. They have:
|
|
85
|
+
|
|
86
|
+
- **Position**: World coordinates
|
|
87
|
+
- **Rotation**: Angle in radians
|
|
88
|
+
- **Velocity**: Linear velocity vector
|
|
89
|
+
- **Angular Velocity**: Rotation speed in radians/second
|
|
90
|
+
- **Mass**: Affects how much force is needed to move the body
|
|
91
|
+
- **Moment of Inertia**: Resistance to rotational acceleration
|
|
92
|
+
|
|
93
|
+
### Static vs Dynamic Bodies
|
|
94
|
+
|
|
95
|
+
- **Static**: Don't move, infinite mass (walls, floors, platforms)
|
|
96
|
+
- **Dynamic**: Move and respond to forces (players, projectiles, debris)
|
|
97
|
+
- **Kinematic**: Move but don't respond to collisions (moving platforms)
|
|
98
|
+
|
|
99
|
+
### Collision Detection Phases
|
|
100
|
+
|
|
101
|
+
1. **Broad Phase**: Uses spatial indexing to quickly find potentially colliding pairs
|
|
102
|
+
- QuadTree: Good for static worlds
|
|
103
|
+
- Dynamic Tree: Good for mixed static/dynamic
|
|
104
|
+
- Sweep-and-Prune: Best for many dynamic bodies
|
|
105
|
+
|
|
106
|
+
2. **Narrow Phase**: Precise collision detection using Separating Axis Theorem (SAT)
|
|
107
|
+
|
|
108
|
+
### Collision Filtering
|
|
109
|
+
|
|
110
|
+
Bodies can be filtered by:
|
|
111
|
+
- **Category**: What category this body belongs to (bit flags)
|
|
112
|
+
- **Mask**: Which categories this body collides with (bit flags)
|
|
113
|
+
- **Group**: Positive groups collide only with same group, negative never collide
|
|
114
|
+
|
|
115
|
+
## Core APIs
|
|
116
|
+
|
|
117
|
+
### World Class
|
|
118
|
+
|
|
119
|
+
Main physics world container.
|
|
120
|
+
|
|
121
|
+
**Constructor:**
|
|
122
|
+
```typescript
|
|
123
|
+
const world = new World(
|
|
124
|
+
worldWidth: number,
|
|
125
|
+
worldHeight: number,
|
|
126
|
+
spatialIndexType?: 'quadtree' | 'dynamictree' | 'sap'
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Methods:**
|
|
131
|
+
- `step(deltaTime: number)`: Advance simulation by deltaTime seconds
|
|
132
|
+
- `addRigidBody(id: string, body: RigidBody)`: Add a rigid body
|
|
133
|
+
- `removeRigidBody(id: string)`: Remove a rigid body
|
|
134
|
+
- `addConstraint(constraint: Constraint)`: Add a constraint
|
|
135
|
+
- `removeConstraint(constraint: Constraint)`: Remove a constraint
|
|
136
|
+
- `setSpatialIndexType(type)`: Change spatial indexing algorithm
|
|
137
|
+
- `queryAABB(aabb): RigidBody[]`: Find all bodies in an AABB region
|
|
138
|
+
- `queryPoint(point): RigidBody[]`: Find all bodies containing a point
|
|
139
|
+
- `rayCast(from, to): RayCastResult[]`: Ray casting
|
|
140
|
+
|
|
141
|
+
**Properties:**
|
|
142
|
+
- `gravity: Point`: World gravity vector (default: `{x: 0, y: -9.8}`)
|
|
143
|
+
- `sleepingEnabled: boolean`: Enable/disable sleeping system
|
|
144
|
+
- `bodies: Map<string, RigidBody>`: All bodies in the world
|
|
145
|
+
- `constraints: Constraint[]`: All constraints
|
|
146
|
+
|
|
147
|
+
### Circle Class
|
|
148
|
+
|
|
149
|
+
Circular rigid body.
|
|
150
|
+
|
|
151
|
+
**Constructor:**
|
|
152
|
+
```typescript
|
|
153
|
+
const circle = new Circle(
|
|
154
|
+
position: Point,
|
|
155
|
+
radius: number,
|
|
156
|
+
rotation: number,
|
|
157
|
+
mass: number,
|
|
158
|
+
isStatic: boolean
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Properties:**
|
|
163
|
+
- `position: Point`: World position
|
|
164
|
+
- `velocity: Point`: Linear velocity
|
|
165
|
+
- `rotation: number`: Rotation in radians
|
|
166
|
+
- `angularVelocity: number`: Rotation speed
|
|
167
|
+
- `mass: number`: Mass (0 for static bodies)
|
|
168
|
+
- `radius: number`: Circle radius
|
|
169
|
+
- `restitution: number`: Bounciness (0-1)
|
|
170
|
+
- `friction: number`: Friction coefficient
|
|
171
|
+
- `collisionFilter: CollisionFilter`: Filtering
|
|
172
|
+
|
|
173
|
+
### Polygon Class
|
|
174
|
+
|
|
175
|
+
Convex polygon rigid body.
|
|
176
|
+
|
|
177
|
+
**Constructor:**
|
|
178
|
+
```typescript
|
|
179
|
+
const polygon = new Polygon(
|
|
180
|
+
position: Point,
|
|
181
|
+
vertices: Point[], // Local space vertices
|
|
182
|
+
rotation: number,
|
|
183
|
+
mass: number,
|
|
184
|
+
isStatic: boolean
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Properties:**
|
|
189
|
+
- Same as Circle, plus:
|
|
190
|
+
- `vertices: Point[]`: Vertices in local space
|
|
191
|
+
- `worldVertices: Point[]`: Vertices in world space (computed)
|
|
192
|
+
|
|
193
|
+
**Methods:**
|
|
194
|
+
- `updateWorldVertices()`: Recompute world vertices after transform changes
|
|
195
|
+
|
|
196
|
+
### Constraints
|
|
197
|
+
|
|
198
|
+
#### PinJoint
|
|
199
|
+
|
|
200
|
+
Pin joint between two bodies.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const joint = new PinJoint(
|
|
204
|
+
bodyA: RigidBody,
|
|
205
|
+
bodyB: RigidBody,
|
|
206
|
+
anchorA: Point, // Local anchor on bodyA
|
|
207
|
+
anchorB: Point // Local anchor on bodyB
|
|
208
|
+
);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### FixedPinJoint
|
|
212
|
+
|
|
213
|
+
Pin joint from body to world point.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const fixedJoint = new FixedPinJoint(
|
|
217
|
+
body: RigidBody,
|
|
218
|
+
localAnchor: Point, // Local anchor on body
|
|
219
|
+
worldAnchor: Point // Fixed world position
|
|
220
|
+
);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Collision Filter
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
type CollisionFilter = {
|
|
227
|
+
category: number; // What category this body is (bit flags)
|
|
228
|
+
mask: number; // What categories to collide with (bit flags)
|
|
229
|
+
group: number; // Collision group
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Predefined Categories:**
|
|
234
|
+
```typescript
|
|
235
|
+
enum CollisionCategory {
|
|
236
|
+
STATIC = 0x0001,
|
|
237
|
+
PLAYER = 0x0002,
|
|
238
|
+
ENEMY = 0x0004,
|
|
239
|
+
PROJECTILE = 0x0008,
|
|
240
|
+
SENSOR = 0x0010,
|
|
241
|
+
PLATFORM = 0x0020
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Common Use Cases
|
|
246
|
+
|
|
247
|
+
### Basic Platformer Physics
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { World, Circle, Polygon, CollisionCategory } from '@ue-too/dynamics';
|
|
251
|
+
|
|
252
|
+
const world = new World(2000, 2000, 'dynamictree');
|
|
253
|
+
world.gravity = { x: 0, y: -20 }; // Downward gravity
|
|
254
|
+
|
|
255
|
+
// Ground
|
|
256
|
+
const ground = new Polygon(
|
|
257
|
+
{ x: 0, y: -150 },
|
|
258
|
+
[{ x: -500, y: 0 }, { x: 500, y: 0 }, { x: 500, y: 50 }, { x: -500, y: 50 }],
|
|
259
|
+
0, 0, true
|
|
260
|
+
);
|
|
261
|
+
ground.collisionFilter = {
|
|
262
|
+
category: CollisionCategory.STATIC,
|
|
263
|
+
mask: 0xFFFF, // Collides with everything
|
|
264
|
+
group: 0
|
|
265
|
+
};
|
|
266
|
+
world.addRigidBody('ground', ground);
|
|
267
|
+
|
|
268
|
+
// Player
|
|
269
|
+
const player = new Circle({ x: 0, y: 0 }, 20, 0, 10, false);
|
|
270
|
+
player.collisionFilter = {
|
|
271
|
+
category: CollisionCategory.PLAYER,
|
|
272
|
+
mask: CollisionCategory.STATIC | CollisionCategory.PLATFORM | CollisionCategory.ENEMY,
|
|
273
|
+
group: 0
|
|
274
|
+
};
|
|
275
|
+
player.restitution = 0; // No bounce
|
|
276
|
+
player.friction = 0.5;
|
|
277
|
+
world.addRigidBody('player', player);
|
|
278
|
+
|
|
279
|
+
// Apply jump force
|
|
280
|
+
function jump() {
|
|
281
|
+
player.velocity.y = 15; // Upward velocity
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Game loop
|
|
285
|
+
function update(deltaTime: number) {
|
|
286
|
+
world.step(deltaTime);
|
|
287
|
+
// Render player at player.position
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Pendulum with Constraints
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { World, Circle, FixedPinJoint } from '@ue-too/dynamics';
|
|
295
|
+
|
|
296
|
+
const world = new World(2000, 2000);
|
|
297
|
+
world.gravity = { x: 0, y: -9.8 };
|
|
298
|
+
|
|
299
|
+
// Pendulum bob
|
|
300
|
+
const bob = new Circle({ x: 0, y: 100 }, 20, 0, 10, false);
|
|
301
|
+
bob.restitution = 0.8; // Bouncy
|
|
302
|
+
world.addRigidBody('bob', bob);
|
|
303
|
+
|
|
304
|
+
// Fix to world origin
|
|
305
|
+
const joint = new FixedPinJoint(
|
|
306
|
+
bob,
|
|
307
|
+
{ x: 0, y: 0 }, // Bob's center
|
|
308
|
+
{ x: 0, y: 0 } // World origin
|
|
309
|
+
);
|
|
310
|
+
world.addConstraint(joint);
|
|
311
|
+
|
|
312
|
+
// Simulation
|
|
313
|
+
function update(deltaTime: number) {
|
|
314
|
+
world.step(deltaTime);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Chain of Bodies
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { World, Circle, PinJoint } from '@ue-too/dynamics';
|
|
322
|
+
|
|
323
|
+
const world = new World(2000, 2000);
|
|
324
|
+
world.gravity = { x: 0, y: -9.8 };
|
|
325
|
+
|
|
326
|
+
const links: Circle[] = [];
|
|
327
|
+
const numLinks = 5;
|
|
328
|
+
|
|
329
|
+
// Create chain links
|
|
330
|
+
for (let i = 0; i < numLinks; i++) {
|
|
331
|
+
const link = new Circle({ x: i * 30, y: 0 }, 10, 0, 5, false);
|
|
332
|
+
world.addRigidBody(`link${i}`, link);
|
|
333
|
+
links.push(link);
|
|
334
|
+
|
|
335
|
+
if (i > 0) {
|
|
336
|
+
// Connect to previous link
|
|
337
|
+
const joint = new PinJoint(
|
|
338
|
+
links[i - 1],
|
|
339
|
+
links[i],
|
|
340
|
+
{ x: 10, y: 0 }, // Right edge of previous
|
|
341
|
+
{ x: -10, y: 0 } // Left edge of current
|
|
342
|
+
);
|
|
343
|
+
world.addConstraint(joint);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Fix first link to world
|
|
348
|
+
const fixedJoint = new FixedPinJoint(
|
|
349
|
+
links[0],
|
|
350
|
+
{ x: -10, y: 0 },
|
|
351
|
+
{ x: 0, y: 0 }
|
|
352
|
+
);
|
|
353
|
+
world.addConstraint(fixedJoint);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Collision Sensors
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { Circle, CollisionCategory } from '@ue-too/dynamics';
|
|
360
|
+
|
|
361
|
+
// Create a trigger zone that doesn't physically collide
|
|
362
|
+
const trigger = new Circle({ x: 100, y: 100 }, 50, 0, 0, true);
|
|
363
|
+
trigger.collisionFilter = {
|
|
364
|
+
category: CollisionCategory.SENSOR,
|
|
365
|
+
mask: CollisionCategory.PLAYER,
|
|
366
|
+
group: -1 // Negative group = never physically collide
|
|
367
|
+
};
|
|
368
|
+
world.addRigidBody('trigger', trigger);
|
|
369
|
+
|
|
370
|
+
// Listen for collisions
|
|
371
|
+
world.onCollision((bodyA, bodyB, contacts) => {
|
|
372
|
+
if (bodyA === trigger || bodyB === trigger) {
|
|
373
|
+
console.log('Player entered trigger zone!');
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Spatial Queries
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Find all bodies in a region
|
|
382
|
+
const aabb = {
|
|
383
|
+
min: { x: -50, y: -50 },
|
|
384
|
+
max: { x: 50, y: 50 }
|
|
385
|
+
};
|
|
386
|
+
const bodiesInRegion = world.queryAABB(aabb);
|
|
387
|
+
|
|
388
|
+
// Find bodies at a point
|
|
389
|
+
const bodiesAtPoint = world.queryPoint({ x: 100, y: 100 });
|
|
390
|
+
|
|
391
|
+
// Ray cast
|
|
392
|
+
const rayResults = world.rayCast(
|
|
393
|
+
{ x: 0, y: 0 }, // From
|
|
394
|
+
{ x: 100, y: 100 } // To
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
rayResults.forEach(result => {
|
|
398
|
+
console.log('Hit:', result.body, 'at distance:', result.distance);
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Performance Tuning
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { World } from '@ue-too/dynamics';
|
|
406
|
+
|
|
407
|
+
const world = new World(2000, 2000, 'sap'); // Sweep-and-prune for many dynamic bodies
|
|
408
|
+
|
|
409
|
+
// Enable sleeping
|
|
410
|
+
world.sleepingEnabled = true;
|
|
411
|
+
|
|
412
|
+
// Customize sleeping thresholds per body
|
|
413
|
+
body.sleepThreshold = 0.01; // Velocity threshold
|
|
414
|
+
body.sleepTime = 0.5; // Seconds at rest before sleeping
|
|
415
|
+
|
|
416
|
+
// Get performance stats
|
|
417
|
+
const stats = world.getCollisionStats();
|
|
418
|
+
console.log('Broad phase pairs:', stats.broadPhasePairs);
|
|
419
|
+
console.log('Narrow phase tests:', stats.narrowPhaseTests);
|
|
420
|
+
console.log('Active collisions:', stats.activeCollisions);
|
|
421
|
+
console.log('Sleeping bodies:', stats.sleepingBodies);
|
|
422
|
+
|
|
423
|
+
// Switch spatial index at runtime
|
|
424
|
+
world.setSpatialIndexType('dynamictree');
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## API Reference
|
|
428
|
+
|
|
429
|
+
For complete API documentation with detailed type information, see the [TypeDoc-generated documentation](../../docs/dynamics).
|
|
430
|
+
|
|
431
|
+
## TypeScript Support
|
|
432
|
+
|
|
433
|
+
This package is written in TypeScript with complete type definitions:
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import {
|
|
437
|
+
World,
|
|
438
|
+
Circle,
|
|
439
|
+
Polygon,
|
|
440
|
+
RigidBody,
|
|
441
|
+
Constraint,
|
|
442
|
+
CollisionCategory,
|
|
443
|
+
type Point
|
|
444
|
+
} from '@ue-too/dynamics';
|
|
445
|
+
|
|
446
|
+
// Bodies are fully typed
|
|
447
|
+
const circle: Circle = new Circle({ x: 0, y: 0 }, 20, 0, 10, false);
|
|
448
|
+
const polygon: Polygon = new Polygon(/* ... */);
|
|
449
|
+
|
|
450
|
+
// Constraints are typed
|
|
451
|
+
const joint: Constraint = new PinJoint(circle, polygon, { x: 0, y: 0 }, { x: 0, y: 0 });
|
|
452
|
+
|
|
453
|
+
// Filters are typed
|
|
454
|
+
circle.collisionFilter = {
|
|
455
|
+
category: CollisionCategory.PLAYER,
|
|
456
|
+
mask: CollisionCategory.STATIC | CollisionCategory.ENEMY,
|
|
457
|
+
group: 0
|
|
458
|
+
};
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Design Philosophy
|
|
462
|
+
|
|
463
|
+
This physics engine follows these principles:
|
|
464
|
+
|
|
465
|
+
- **Simplicity**: Focus on common 2D game physics use cases
|
|
466
|
+
- **Performance**: Spatial indexing and sleeping for scalability
|
|
467
|
+
- **Modularity**: Pluggable spatial index algorithms
|
|
468
|
+
- **Practicality**: Designed for games, not scientific simulation
|
|
469
|
+
- **Type Safety**: Full TypeScript support
|
|
470
|
+
|
|
471
|
+
## Performance Considerations
|
|
472
|
+
|
|
473
|
+
- **Spatial Indexing**: Choose based on your use case:
|
|
474
|
+
- QuadTree: Static worlds with few dynamic objects
|
|
475
|
+
- Dynamic Tree: Mixed static/dynamic (recommended default)
|
|
476
|
+
- Sweep-and-Prune: Many dynamic objects moving continuously
|
|
477
|
+
|
|
478
|
+
- **Sleeping System**: Automatically disables physics for resting bodies
|
|
479
|
+
- **Collision Filtering**: Reduces narrow phase tests significantly
|
|
480
|
+
- **Fixed Time Step**: Use fixed time steps (1/60) for stability
|
|
481
|
+
|
|
482
|
+
**Performance Tips:**
|
|
483
|
+
- Enable sleeping for worlds with many resting bodies
|
|
484
|
+
- Use collision filtering to avoid unnecessary collision tests
|
|
485
|
+
- Choose appropriate spatial index for your scenario
|
|
486
|
+
- Avoid very large mass ratios between bodies (causes instability)
|
|
487
|
+
- Use static bodies for immovable objects
|
|
488
|
+
- Limit polygon vertex counts (4-8 vertices is optimal)
|
|
489
|
+
|
|
490
|
+
## Limitations
|
|
491
|
+
|
|
492
|
+
- **2D Only**: No 3D support
|
|
493
|
+
- **Convex Polygons**: Concave shapes must be decomposed
|
|
494
|
+
- **No Continuous Collision**: Fast-moving objects may tunnel
|
|
495
|
+
- **Simple Friction Model**: Basic static and dynamic friction
|
|
496
|
+
- **Experimental**: Not production-ready, API may change
|
|
497
|
+
|
|
498
|
+
## Debugging Tips
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// Enable debug rendering
|
|
502
|
+
world.debugDraw = (ctx: CanvasRenderingContext2D) => {
|
|
503
|
+
// Draw all bodies
|
|
504
|
+
world.bodies.forEach(body => {
|
|
505
|
+
ctx.strokeStyle = body.isStatic ? 'gray' : 'blue';
|
|
506
|
+
if (body instanceof Circle) {
|
|
507
|
+
ctx.beginPath();
|
|
508
|
+
ctx.arc(body.position.x, body.position.y, body.radius, 0, Math.PI * 2);
|
|
509
|
+
ctx.stroke();
|
|
510
|
+
} else if (body instanceof Polygon) {
|
|
511
|
+
ctx.beginPath();
|
|
512
|
+
ctx.moveTo(body.worldVertices[0].x, body.worldVertices[0].y);
|
|
513
|
+
for (let i = 1; i < body.worldVertices.length; i++) {
|
|
514
|
+
ctx.lineTo(body.worldVertices[i].x, body.worldVertices[i].y);
|
|
515
|
+
}
|
|
516
|
+
ctx.closePath();
|
|
517
|
+
ctx.stroke();
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Related Packages
|
|
524
|
+
|
|
525
|
+
- **[@ue-too/math](../math)**: Vector operations used throughout the physics engine
|
|
526
|
+
- **[@ue-too/ecs](../ecs)**: Entity Component System that can integrate with physics
|
|
527
|
+
- **[@ue-too/board](../board)**: Canvas board for rendering physics simulations
|
|
528
|
+
|
|
529
|
+
## Further Reading
|
|
530
|
+
|
|
531
|
+
- [2D Game Physics](https://gamedevelopment.tutsplus.com/series/how-to-create-a-custom-physics-engine--gamedev-12715) - Tutorial series on 2D physics engines
|
|
532
|
+
- [Impulse-Based Dynamics](https://www.myphysicslab.com/engine2D/collision-en.html) - Physics engine theory
|
|
533
|
+
- [Separating Axis Theorem](https://en.wikipedia.org/wiki/Hyperplane_separation_theorem) - Collision detection algorithm
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT
|
|
538
|
+
|
|
539
|
+
## Repository
|
|
540
|
+
|
|
541
|
+
[https://github.com/ue-too/ue-too](https://github.com/ue-too/ue-too)
|
package/collision-filter.d.ts
CHANGED
|
@@ -1,10 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collision filtering configuration for rigid bodies.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Collision filters use a bitmask system to control which bodies can collide
|
|
6
|
+
* with each other. This is useful for creating layers, groups, and special
|
|
7
|
+
* collision rules in your physics simulation.
|
|
8
|
+
*
|
|
9
|
+
* ### How Filtering Works
|
|
10
|
+
*
|
|
11
|
+
* Two bodies A and B can collide if ALL of these conditions are met:
|
|
12
|
+
* 1. `(A.category & B.mask) !== 0` - A's category matches B's mask
|
|
13
|
+
* 2. `(B.category & A.mask) !== 0` - B's category matches A's mask
|
|
14
|
+
* 3. Group rules are satisfied (see group field)
|
|
15
|
+
*
|
|
16
|
+
* @category Types
|
|
17
|
+
*/
|
|
1
18
|
export interface CollisionFilter {
|
|
19
|
+
/**
|
|
20
|
+
* What category this body belongs to (bitmask).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* category: CollisionCategory.PLAYER // 0x0004
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
2
27
|
category: number;
|
|
28
|
+
/**
|
|
29
|
+
* What categories this body can collide with (bitmask).
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // Collide with everything except other players
|
|
34
|
+
* mask: ~CollisionCategory.PLAYER & 0xFFFF
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
3
37
|
mask: number;
|
|
38
|
+
/**
|
|
39
|
+
* Collision group for special rules.
|
|
40
|
+
* - 0: No group (use category/mask rules)
|
|
41
|
+
* - Positive: Bodies in same group ALWAYS collide
|
|
42
|
+
* - Negative: Bodies in same group NEVER collide
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // Ragdoll parts shouldn't collide with each other
|
|
47
|
+
* group: -1
|
|
48
|
+
*
|
|
49
|
+
* // Team members always collide (for physics interactions)
|
|
50
|
+
* group: 1
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
4
53
|
group: number;
|
|
5
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Default collision filter that collides with everything.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Uses category 0x0001 and mask 0xFFFF, meaning it belongs to the first
|
|
60
|
+
* category and can collide with all 16 categories.
|
|
61
|
+
*
|
|
62
|
+
* @category Collision
|
|
63
|
+
*/
|
|
6
64
|
export declare const DEFAULT_COLLISION_FILTER: CollisionFilter;
|
|
65
|
+
/**
|
|
66
|
+
* Determines if two bodies can collide based on their collision filters.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* Checks group rules first, then falls back to category/mask matching.
|
|
70
|
+
* This is used internally by the physics engine during broad phase collision detection.
|
|
71
|
+
*
|
|
72
|
+
* @param filterA - Collision filter of first body
|
|
73
|
+
* @param filterB - Collision filter of second body
|
|
74
|
+
* @returns True if the bodies should collide
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const player: CollisionFilter = {
|
|
79
|
+
* category: CollisionCategory.PLAYER,
|
|
80
|
+
* mask: 0xFFFF,
|
|
81
|
+
* group: 0
|
|
82
|
+
* };
|
|
83
|
+
*
|
|
84
|
+
* const enemy: CollisionFilter = {
|
|
85
|
+
* category: CollisionCategory.ENEMY,
|
|
86
|
+
* mask: CollisionCategory.PLAYER | CollisionCategory.STATIC,
|
|
87
|
+
* group: 0
|
|
88
|
+
* };
|
|
89
|
+
*
|
|
90
|
+
* console.log(canCollide(player, enemy)); // true
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @category Collision
|
|
94
|
+
*/
|
|
7
95
|
export declare function canCollide(filterA: CollisionFilter, filterB: CollisionFilter): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Predefined collision categories for common game entities.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* These are bitmask constants (powers of 2) that can be combined using bitwise OR.
|
|
101
|
+
* You can define up to 16 categories using values from 0x0001 to 0x8000.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* Using predefined categories
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // Player collides with everything except other players
|
|
107
|
+
* player.collisionFilter = {
|
|
108
|
+
* category: CollisionCategory.PLAYER,
|
|
109
|
+
* mask: ~CollisionCategory.PLAYER & 0xFFFF,
|
|
110
|
+
* group: 0
|
|
111
|
+
* };
|
|
112
|
+
*
|
|
113
|
+
* // Projectile only collides with enemies and static objects
|
|
114
|
+
* projectile.collisionFilter = {
|
|
115
|
+
* category: CollisionCategory.PROJECTILE,
|
|
116
|
+
* mask: CollisionCategory.ENEMY | CollisionCategory.STATIC,
|
|
117
|
+
* group: 0
|
|
118
|
+
* };
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @category Collision
|
|
122
|
+
*/
|
|
8
123
|
export declare const CollisionCategory: {
|
|
9
124
|
readonly STATIC: 1;
|
|
10
125
|
readonly DYNAMIC: 2;
|