@gg-web-engine/rapier3d 0.0.64 → 0.0.66

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.
@@ -1,6 +1,7 @@
1
1
  import { CharacterController3dOptions, CollisionGroup, DebugBody3DSettings, Entity3d, ICharacterController3dComponent, Point3, Point4 } from '@gg-web-engine/core';
2
2
  import { Collider, InteractionGroups, KinematicCharacterController, RigidBody, RigidBodyDesc } from '@dimforge/rapier3d-compat';
3
3
  import { Rapier3dWorldComponent } from './rapier-3d-world.component';
4
+ import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
4
5
  import { Rapier3dGgWorld, Rapier3dPhysicsTypeDocRepo } from '../types';
5
6
  /**
6
7
  * A capsule-shaped kinematic character controller backed by Rapier's own `KinematicCharacterController`
@@ -43,6 +44,12 @@ export declare class Rapier3dCharacterControllerComponent implements ICharacterC
43
44
  get isGrounded(): boolean;
44
45
  private _groundNormal;
45
46
  get groundNormal(): Point3 | null;
47
+ /** See `ICharacterController3dComponent.ignoredBodies`'s doc. Consulted fresh every `move()` call
48
+ * via `computeColliderMovement`'s own `filterPredicate` - unlike `AmmoCharacterControllerComponent`
49
+ * (which has to fake this by temporarily pulling ignored bodies out of the collision world),
50
+ * Rapier's character controller supports excluding specific colliders from a single query
51
+ * natively, so no such trick is needed here. */
52
+ readonly ignoredBodies: Set<Rapier3dRigidBodyComponent>;
46
53
  private static warnedMissingDtForPush;
47
54
  protected _nativeBody: RigidBody | null;
48
55
  protected _nativeCollider: Collider | null;
@@ -71,6 +78,14 @@ export declare class Rapier3dCharacterControllerComponent implements ICharacterC
71
78
  * needed here.
72
79
  */
73
80
  private syncColliderTransform;
81
+ /**
82
+ * Builds `computeColliderMovement`'s `filterPredicate` from `ignoredBodies` - `undefined` when
83
+ * empty (the common case) rather than an always-true closure, so an empty `ignoredBodies` set costs
84
+ * nothing extra per query. Compares by `RigidBody.handle` (a plain numeric id), not object
85
+ * identity - `Collider.parent()` isn't guaranteed to return the same wrapper instance across calls
86
+ * for the pinned `@dimforge/rapier3d-compat` build, only the same underlying native body.
87
+ */
88
+ private ignoredBodiesFilterPredicate;
74
89
  move(desiredTranslation: Point3, dt?: number): void;
75
90
  /**
76
91
  * Shoves any dynamic body this tick's sweep bumped into - see `addToWorld`'s doc for why this is
@@ -112,6 +112,12 @@ export class Rapier3dCharacterControllerComponent {
112
112
  this.name = '';
113
113
  this._isGrounded = false;
114
114
  this._groundNormal = null;
115
+ /** See `ICharacterController3dComponent.ignoredBodies`'s doc. Consulted fresh every `move()` call
116
+ * via `computeColliderMovement`'s own `filterPredicate` - unlike `AmmoCharacterControllerComponent`
117
+ * (which has to fake this by temporarily pulling ignored bodies out of the collision world),
118
+ * Rapier's character controller supports excluding specific colliders from a single query
119
+ * natively, so no such trick is needed here. */
120
+ this.ignoredBodies = new Set();
115
121
  this._nativeBody = null;
116
122
  this._nativeCollider = null;
117
123
  this._nativeController = null;
@@ -135,6 +141,31 @@ export class Rapier3dCharacterControllerComponent {
135
141
  syncColliderTransform() {
136
142
  this.world.nativeWorld.propagateModifiedBodyPositionsToColliders();
137
143
  }
144
+ /**
145
+ * Builds `computeColliderMovement`'s `filterPredicate` from `ignoredBodies` - `undefined` when
146
+ * empty (the common case) rather than an always-true closure, so an empty `ignoredBodies` set costs
147
+ * nothing extra per query. Compares by `RigidBody.handle` (a plain numeric id), not object
148
+ * identity - `Collider.parent()` isn't guaranteed to return the same wrapper instance across calls
149
+ * for the pinned `@dimforge/rapier3d-compat` build, only the same underlying native body.
150
+ */
151
+ ignoredBodiesFilterPredicate() {
152
+ if (this.ignoredBodies.size === 0) {
153
+ return undefined;
154
+ }
155
+ const ignoredHandles = new Set();
156
+ for (const body of this.ignoredBodies) {
157
+ if (body.nativeBody) {
158
+ ignoredHandles.add(body.nativeBody.handle);
159
+ }
160
+ }
161
+ if (ignoredHandles.size === 0) {
162
+ return undefined;
163
+ }
164
+ return (collider) => {
165
+ const parent = collider.parent();
166
+ return !parent || !ignoredHandles.has(parent.handle);
167
+ };
168
+ }
138
169
  move(desiredTranslation, dt) {
139
170
  if (!this._nativeBody || !this._nativeCollider || !this._nativeController) {
140
171
  // not yet added to the world - nothing to sweep against (matches
@@ -180,7 +211,7 @@ export class Rapier3dCharacterControllerComponent {
180
211
  }
181
212
  }
182
213
  const desired = new Vector3(desiredTranslation.x, desiredTranslation.y, desiredTranslation.z);
183
- this._nativeController.computeColliderMovement(this._nativeCollider, desired);
214
+ this._nativeController.computeColliderMovement(this._nativeCollider, desired, undefined, undefined, this.ignoredBodiesFilterPredicate());
184
215
  const computed = this._nativeController.computedMovement();
185
216
  const current = this._nativeBody.translation();
186
217
  const next = new Vector3(current.x + computed.x, current.y + computed.y, current.z + computed.z);
@@ -6,7 +6,7 @@ export class Rapier3dRigidBodyComponent {
6
6
  }
7
7
  set position(value) {
8
8
  if (this.nativeBody) {
9
- this.nativeBody.setTranslation(new Vector3(value.x, value.y, value.z), false);
9
+ this.nativeBody.setTranslation(new Vector3(value.x, value.y, value.z), true);
10
10
  }
11
11
  else {
12
12
  this._bodyDescr.setTranslation(value.x, value.y, value.z);
@@ -17,7 +17,7 @@ export class Rapier3dRigidBodyComponent {
17
17
  }
18
18
  set rotation(value) {
19
19
  if (this.nativeBody) {
20
- this.nativeBody.setRotation(new Quaternion(value.x, value.y, value.z, value.w), false);
20
+ this.nativeBody.setRotation(new Quaternion(value.x, value.y, value.z, value.w), true);
21
21
  }
22
22
  else {
23
23
  this._bodyDescr.setRotation(new Quaternion(value.x, value.y, value.z, value.w));
@@ -29,7 +29,8 @@ export class Rapier3dRigidBodyComponent {
29
29
  }
30
30
  set linearVelocity(value) {
31
31
  if (this.nativeBody) {
32
- this.nativeBody.setLinvel(new Vector3(value.x, value.y, value.z), false);
32
+ // see `position`'s setter above for why `true` (wake up) is required
33
+ this.nativeBody.setLinvel(new Vector3(value.x, value.y, value.z), true);
33
34
  }
34
35
  }
35
36
  get angularVelocity() {
@@ -38,7 +39,8 @@ export class Rapier3dRigidBodyComponent {
38
39
  }
39
40
  set angularVelocity(value) {
40
41
  if (this.nativeBody) {
41
- this.nativeBody.setAngvel(new Vector3(value.x, value.y, value.z), false);
42
+ // see `position`'s setter above for why `true` (wake up) is required
43
+ this.nativeBody.setAngvel(new Vector3(value.x, value.y, value.z), true);
42
44
  }
43
45
  }
44
46
  get nativeBody() {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@gg-web-engine/rapier3d",
3
- "version": "0.0.64",
3
+ "version": "0.0.66",
4
4
  "description": "An attempt to create open source game engine for browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "test": "jest",
9
- "prettier-format": "prettier --config ../core/.prettierrc 'src/**/*.ts' --write",
9
+ "prettier-format": "prettier --config ../../.prettierrc 'src/**/*.ts' --write",
10
10
  "prepublish": "rm -rf ./dist/ && tsc",
11
11
  "build": "tsc"
12
12
  },
@@ -28,7 +28,7 @@
28
28
  "homepage": "https://github.com/AndyGura/gg-web-engine#readme",
29
29
  "devDependencies": {
30
30
  "@dimforge/rapier3d-compat": "0.20.0",
31
- "@gg-web-engine/core": "0.0.64",
31
+ "@gg-web-engine/core": "0.0.66",
32
32
  "@types/jest": "^30.0.0",
33
33
  "@types/node": "^26.4.0",
34
34
  "jest": "^30.5.0",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@dimforge/rapier3d-compat": "0.20.0",
43
- "@gg-web-engine/core": "0.0.64",
43
+ "@gg-web-engine/core": "0.0.66",
44
44
  "rxjs": "7.8.2"
45
45
  },
46
46
  "jest": {
@@ -144,6 +144,46 @@ describe('Rapier3dCharacterControllerComponent', () => {
144
144
  expect(character.isGrounded).toBe(true);
145
145
  });
146
146
 
147
+ it(
148
+ 'walks straight through a body added to ignoredBodies instead of sliding to a stop against it ' +
149
+ "(regression: collision groups alone can't express excluding just one specific body while both " +
150
+ 'it and the character still need to collide with the rest of the world - see ' +
151
+ '`ICharacterController3dComponent.ignoredBodies`\'s doc)',
152
+ () => {
153
+ addFloor(0, 20, 0);
154
+ const wall = addWall(2);
155
+ const character = factory.createCharacterController(CHAR_OPTIONS, {
156
+ position: { x: 0, y: 0, z: HALF_HEIGHT + 0.5 },
157
+ });
158
+ character.addToWorld({ physicsWorld: world } as any);
159
+ character.ignoredBodies.add(wall);
160
+ settleWorld();
161
+ character.move({ x: 0, y: 0, z: -1 });
162
+
163
+ character.move({ x: 5, y: 0, z: 0 });
164
+
165
+ // unlike the baseline test above, nothing stopped it short of the wall's x=2 near face
166
+ expect(character.position.x).toBeCloseTo(5);
167
+ },
168
+ );
169
+
170
+ it('removing a body from ignoredBodies makes it block the character again', () => {
171
+ addFloor(0, 20, 0);
172
+ const wall = addWall(2);
173
+ const character = factory.createCharacterController(CHAR_OPTIONS, {
174
+ position: { x: 0, y: 0, z: HALF_HEIGHT + 0.5 },
175
+ });
176
+ character.addToWorld({ physicsWorld: world } as any);
177
+ character.ignoredBodies.add(wall);
178
+ character.ignoredBodies.delete(wall);
179
+ settleWorld();
180
+ character.move({ x: 0, y: 0, z: -1 });
181
+
182
+ character.move({ x: 5, y: 0, z: 0 });
183
+
184
+ expect(character.position.x).toBeLessThan(1.6);
185
+ });
186
+
147
187
  it('should step up a ledge shorter than maxStepHeight without getting stuck', () => {
148
188
  const maxStepHeight = 0.3;
149
189
  const stepZ = 0.2; // below maxStepHeight
@@ -87,6 +87,34 @@ describe('Rapier3dWorldComponent', () => {
87
87
  });
88
88
  });
89
89
 
90
+ describe('Sleeping bodies', () => {
91
+ it(
92
+ 'wakes a sleeping body when position/rotation/velocity is set programmatically ' +
93
+ '(regression: Rapier skips a sleeping body\'s island entirely during step(), so a ' +
94
+ 'transform/velocity write passing wakeUp=false was silently never simulated)',
95
+ () => {
96
+ const ball = world.factory.createRigidBody(
97
+ { shape: { shape: 'SPHERE', radius: 1 }, body: { dynamic: true, mass: 5 } },
98
+ { position: { x: 0, y: 0, z: 0 } },
99
+ );
100
+ ball.addToWorld({ physicsWorld: world } as any);
101
+
102
+ // let it fall asleep: zero gravity (see beforeEach) and zero velocity means nothing ever
103
+ // disturbs it, so Rapier's own sleep threshold/timer runs out on its own.
104
+ for (let i = 0; i < 200; i++) {
105
+ world.simulate(60);
106
+ }
107
+ expect((ball as any).nativeBody.isSleeping()).toBe(true);
108
+
109
+ ball.linearVelocity = { x: 1, y: 0, z: 0 };
110
+ for (let i = 0; i < 60; i++) {
111
+ world.simulate(60);
112
+ }
113
+ expect(ball.position.x).toBeGreaterThan(0.5);
114
+ },
115
+ );
116
+ });
117
+
90
118
  describe('Raycast', () => {
91
119
 
92
120
  it('should return no hit when ray does not intersect any object', () => {