@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.
- package/dist/components/rapier-3d-character-controller.component.d.ts +15 -0
- package/dist/components/rapier-3d-character-controller.component.js +32 -1
- package/dist/components/rapier-3d-rigid-body.component.js +6 -4
- package/package.json +4 -4
- package/test/components/rapier-3d-character-controller.component.spec.ts +40 -0
- package/test/components/rapier-3d-world.component.spec.ts +28 -0
|
@@ -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),
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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', () => {
|