kippy 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.
- package/README.md +21 -0
- package/dist/physics.d.ts +18 -1
- package/dist/physics.js +141 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,6 +229,27 @@ collider.layer; // Set with the matching parameter above, default is (1 << 0)
|
|
|
229
229
|
collider.mask; // Set with the matching parameter above, default is 0xFFFFFFFF
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
+
or a `BoxCollider`:
|
|
233
|
+
```js
|
|
234
|
+
const collider = new BoxCollider({
|
|
235
|
+
width, // Circle collider's width, type number
|
|
236
|
+
height, // Circle collider's height, type number
|
|
237
|
+
offset, // Offset from entity's position, type Vector2
|
|
238
|
+
isTrigger, // If true, trigger callbacks are called and collision physics like bouncing
|
|
239
|
+
// will not apply. Otherwise, collision callbacks are called and physics apply
|
|
240
|
+
layer, // A bit mask to determine what collision layer this collider is at
|
|
241
|
+
mask, // A bit mask to check what colliders to collide
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// You can mutate these props to configure the box collider
|
|
245
|
+
collider.width; // Set with the matching parameter above, required
|
|
246
|
+
collider.height; // Set with the matching parameter above, required
|
|
247
|
+
collider.offset; // Set with the matching parameter above, default is Vector2(0, 0)
|
|
248
|
+
collider.isTrigger; // Set with the matching parameter above, default is false
|
|
249
|
+
collider.layer; // Set with the matching parameter above, default is (1 << 0)
|
|
250
|
+
collider.mask; // Set with the matching parameter above, default is 0xFFFFFFFF
|
|
251
|
+
```
|
|
252
|
+
|
|
232
253
|
And you can handle when two objects collide:
|
|
233
254
|
```js
|
|
234
255
|
collider.onCollisionEnter = (other, info) => {};
|
package/dist/physics.d.ts
CHANGED
|
@@ -44,7 +44,24 @@ export declare class CircleCollider {
|
|
|
44
44
|
mask: number;
|
|
45
45
|
constructor(options: CircleColliderOptions);
|
|
46
46
|
}
|
|
47
|
-
export
|
|
47
|
+
export interface BoxColliderOptions {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
offset?: Vector2;
|
|
51
|
+
isTrigger?: boolean;
|
|
52
|
+
layer?: number;
|
|
53
|
+
mask?: number;
|
|
54
|
+
}
|
|
55
|
+
export declare class BoxCollider {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
offset: Vector2;
|
|
59
|
+
isTrigger: boolean;
|
|
60
|
+
layer: number;
|
|
61
|
+
mask: number;
|
|
62
|
+
constructor(options: BoxColliderOptions);
|
|
63
|
+
}
|
|
64
|
+
export type Collider = CircleCollider | BoxCollider;
|
|
48
65
|
export interface SpatialGridOptions {
|
|
49
66
|
cellSize?: number;
|
|
50
67
|
grid?: Map<string, Set<Entity>>;
|
package/dist/physics.js
CHANGED
|
@@ -43,6 +43,22 @@ export class CircleCollider {
|
|
|
43
43
|
this.mask = options.mask ?? 0xFFFFFFFF;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
export class BoxCollider {
|
|
47
|
+
width;
|
|
48
|
+
height;
|
|
49
|
+
offset;
|
|
50
|
+
isTrigger;
|
|
51
|
+
layer;
|
|
52
|
+
mask;
|
|
53
|
+
constructor(options) {
|
|
54
|
+
this.width = options.width;
|
|
55
|
+
this.height = options.height;
|
|
56
|
+
this.offset = options.offset ?? new Vector2(0, 0);
|
|
57
|
+
this.isTrigger = options.isTrigger ?? false;
|
|
58
|
+
this.layer = options.layer ?? (1 << 0);
|
|
59
|
+
this.mask = options.mask ?? 0xFFFFFFFF;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
export class SpatialGrid {
|
|
47
63
|
cellSize;
|
|
48
64
|
grid;
|
|
@@ -123,6 +139,16 @@ export class SpatialGrid {
|
|
|
123
139
|
maxY: entity.position.y + radius
|
|
124
140
|
};
|
|
125
141
|
}
|
|
142
|
+
else if (entity.collider instanceof BoxCollider) {
|
|
143
|
+
const halfWidth = entity.collider.width;
|
|
144
|
+
const halfHeight = entity.collider.height;
|
|
145
|
+
return {
|
|
146
|
+
minX: entity.position.x - halfWidth,
|
|
147
|
+
maxX: entity.position.x + halfWidth,
|
|
148
|
+
minY: entity.position.y - halfHeight,
|
|
149
|
+
maxY: entity.position.y + halfHeight
|
|
150
|
+
};
|
|
151
|
+
}
|
|
126
152
|
else {
|
|
127
153
|
throw new Error("Collider type not supported");
|
|
128
154
|
}
|
|
@@ -179,6 +205,11 @@ export class Physics {
|
|
|
179
205
|
// Check collision
|
|
180
206
|
const collisionResult = this.checkCollision(entity, other);
|
|
181
207
|
if (collisionResult.info) {
|
|
208
|
+
// Wake if contacting an awake body
|
|
209
|
+
if (!entity.body?.isSleeping || !other.body?.isSleeping) {
|
|
210
|
+
entity.body?.wake();
|
|
211
|
+
other.body?.wake();
|
|
212
|
+
}
|
|
182
213
|
// Track collision
|
|
183
214
|
if (!currentCollisions.has(entity)) {
|
|
184
215
|
currentCollisions.set(entity, new Map());
|
|
@@ -189,8 +220,6 @@ export class Physics {
|
|
|
189
220
|
this.collisionPairs.get(other)?.has(entity));
|
|
190
221
|
if (!wasColliding) {
|
|
191
222
|
// ENTER
|
|
192
|
-
entity.body?.wake();
|
|
193
|
-
other.body?.wake();
|
|
194
223
|
if (collisionResult.isTrigger) {
|
|
195
224
|
entity.onTriggerEnter?.(other);
|
|
196
225
|
other.onTriggerEnter?.(entity);
|
|
@@ -305,7 +334,7 @@ export class Physics {
|
|
|
305
334
|
// Check collision
|
|
306
335
|
const posA = entityA.position.add(entityA.collider.offset);
|
|
307
336
|
const posB = entityB.position.add(entityB.collider.offset);
|
|
308
|
-
// Check different types of colliders
|
|
337
|
+
// Check different types of colliders
|
|
309
338
|
if (entityA.collider instanceof CircleCollider && entityB.collider instanceof CircleCollider) {
|
|
310
339
|
const distance = posA.distance(posB);
|
|
311
340
|
const radiusSum = entityA.collider.radius + entityB.collider.radius;
|
|
@@ -334,6 +363,110 @@ export class Physics {
|
|
|
334
363
|
}
|
|
335
364
|
};
|
|
336
365
|
}
|
|
366
|
+
else if (entityA.collider instanceof BoxCollider && entityB.collider instanceof BoxCollider) {
|
|
367
|
+
const halfAx = entityA.collider.width / 2;
|
|
368
|
+
const halfAy = entityA.collider.height / 2;
|
|
369
|
+
const halfBx = entityB.collider.width / 2;
|
|
370
|
+
const halfBy = entityB.collider.height / 2;
|
|
371
|
+
const delta = posB.sub(posA);
|
|
372
|
+
const overlapX = halfAx + halfBx - Math.abs(delta.x);
|
|
373
|
+
const overlapY = halfAy + halfBy - Math.abs(delta.y);
|
|
374
|
+
if (overlapX <= 0 || overlapY <= 0) {
|
|
375
|
+
return { isTrigger: false };
|
|
376
|
+
}
|
|
377
|
+
let normal;
|
|
378
|
+
let penetration;
|
|
379
|
+
if (overlapX < overlapY) {
|
|
380
|
+
penetration = overlapX;
|
|
381
|
+
normal = new Vector2(delta.x < 0 ? -1 : 1, 0);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
penetration = overlapY;
|
|
385
|
+
normal = new Vector2(0, delta.y < 0 ? -1 : 1);
|
|
386
|
+
}
|
|
387
|
+
// Contact point: center of overlapping region
|
|
388
|
+
const contactX = posA.x + Math.sign(delta.x) * (halfAx - overlapX / 2);
|
|
389
|
+
const contactY = posA.y + Math.sign(delta.y) * (halfAy - overlapY / 2);
|
|
390
|
+
let accumulatedImpulse = 0;
|
|
391
|
+
const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
|
|
392
|
+
this.collisionPairs.get(entityB)?.get(entityA);
|
|
393
|
+
if (lastContactInfo) {
|
|
394
|
+
accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
isTrigger,
|
|
398
|
+
info: {
|
|
399
|
+
normal,
|
|
400
|
+
penetration,
|
|
401
|
+
contact: new Vector2(contactX, contactY),
|
|
402
|
+
accumulatedNormalImpulse: accumulatedImpulse
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
else if ((entityA.collider instanceof CircleCollider && entityB.collider instanceof BoxCollider) ||
|
|
407
|
+
(entityA.collider instanceof BoxCollider && entityB.collider instanceof CircleCollider)) {
|
|
408
|
+
const isCircleA = entityA.collider instanceof CircleCollider;
|
|
409
|
+
const circlePos = isCircleA ? posA : posB;
|
|
410
|
+
const boxPos = isCircleA ? posB : posA;
|
|
411
|
+
const circle = (isCircleA ? entityA : entityB).collider;
|
|
412
|
+
const box = (isCircleA ? entityB : entityA).collider;
|
|
413
|
+
const halfW = box.width / 2;
|
|
414
|
+
const halfH = box.height / 2;
|
|
415
|
+
// Box center -> circle center
|
|
416
|
+
const delta = circlePos.sub(boxPos);
|
|
417
|
+
const inside = Math.abs(delta.x) < halfW && Math.abs(delta.y) < halfH;
|
|
418
|
+
let normal;
|
|
419
|
+
let penetration;
|
|
420
|
+
let contact;
|
|
421
|
+
if (inside) {
|
|
422
|
+
const overlapX = halfW - Math.abs(delta.x);
|
|
423
|
+
const overlapY = halfH - Math.abs(delta.y);
|
|
424
|
+
if (overlapX < overlapY) {
|
|
425
|
+
const sign = delta.x < 0 ? -1 : 1;
|
|
426
|
+
const boxToCircle = new Vector2(sign, 0);
|
|
427
|
+
normal = isCircleA ? boxToCircle.neg() : boxToCircle; // A→B
|
|
428
|
+
penetration = circle.radius + overlapX;
|
|
429
|
+
contact = new Vector2(boxPos.x + sign * halfW, circlePos.y);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
const sign = delta.y < 0 ? -1 : 1;
|
|
433
|
+
const boxToCircle = new Vector2(0, sign);
|
|
434
|
+
normal = isCircleA ? boxToCircle.neg() : boxToCircle;
|
|
435
|
+
penetration = circle.radius + overlapY;
|
|
436
|
+
contact = new Vector2(circlePos.x, boxPos.y + sign * halfH);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const clampedX = Math.max(-halfW, Math.min(halfW, delta.x));
|
|
441
|
+
const clampedY = Math.max(-halfH, Math.min(halfH, delta.y));
|
|
442
|
+
const closest = boxPos.add(new Vector2(clampedX, clampedY));
|
|
443
|
+
const diff = circlePos.sub(closest);
|
|
444
|
+
const distSq = diff.magnitudeSquared();
|
|
445
|
+
if (distSq >= circle.radius * circle.radius) {
|
|
446
|
+
return { isTrigger: false };
|
|
447
|
+
}
|
|
448
|
+
const dist = Math.sqrt(distSq);
|
|
449
|
+
const boxToCircle = dist > 0 ? diff.scale(1 / dist) : new Vector2(1, 0);
|
|
450
|
+
normal = isCircleA ? boxToCircle.neg() : boxToCircle;
|
|
451
|
+
penetration = circle.radius - dist;
|
|
452
|
+
contact = closest;
|
|
453
|
+
}
|
|
454
|
+
let accumulatedImpulse = 0;
|
|
455
|
+
const lastContactInfo = this.collisionPairs.get(entityA)?.get(entityB) ||
|
|
456
|
+
this.collisionPairs.get(entityB)?.get(entityA);
|
|
457
|
+
if (lastContactInfo) {
|
|
458
|
+
accumulatedImpulse = lastContactInfo.accumulatedNormalImpulse;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
isTrigger,
|
|
462
|
+
info: {
|
|
463
|
+
normal,
|
|
464
|
+
penetration,
|
|
465
|
+
contact,
|
|
466
|
+
accumulatedNormalImpulse: accumulatedImpulse
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
337
470
|
return {
|
|
338
471
|
isTrigger: false
|
|
339
472
|
};
|
|
@@ -384,11 +517,14 @@ export class Physics {
|
|
|
384
517
|
bodyA.velocity = bodyA.velocity.sub(info.normal.scale(actualImpulse * invMassA));
|
|
385
518
|
bodyB.velocity = bodyB.velocity.add(info.normal.scale(actualImpulse * invMassB));
|
|
386
519
|
// Apply angular impulse (torque from contact point)
|
|
387
|
-
|
|
520
|
+
// Disabled for now because colliders must rotate too and there isn't polygon collider solver for now
|
|
521
|
+
/*const rA = info.contact.sub(entityA.position);
|
|
388
522
|
const rB = info.contact.sub(entityB.position);
|
|
523
|
+
|
|
389
524
|
const angularImpulseA = rA.cross(info.normal.scale(-actualImpulse));
|
|
390
525
|
const angularImpulseB = rB.cross(info.normal.scale(actualImpulse));
|
|
526
|
+
|
|
391
527
|
bodyA.rotationVelocity += angularImpulseA * (isFinite(bodyA.inertia) ? 1 / bodyA.inertia : 0);
|
|
392
|
-
bodyB.rotationVelocity += angularImpulseB * (isFinite(bodyB.inertia) ? 1 / bodyB.inertia : 0)
|
|
528
|
+
bodyB.rotationVelocity += angularImpulseB * (isFinite(bodyB.inertia) ? 1 / bodyB.inertia : 0);*/
|
|
393
529
|
}
|
|
394
530
|
}
|