@gg-web-engine/rapier3d 0.0.62 → 0.0.64
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 +131 -0
- package/dist/components/rapier-3d-character-controller.component.js +398 -0
- package/dist/components/rapier-3d-raycast-vehicle.component.d.ts +1 -1
- package/dist/components/rapier-3d-raycast-vehicle.component.js +5 -2
- package/dist/components/rapier-3d-rigid-body.component.d.ts +1 -1
- package/dist/components/rapier-3d-rigid-body.component.js +2 -2
- package/dist/components/rapier-3d-trigger.component.d.ts +1 -1
- package/dist/components/rapier-3d-trigger.component.js +2 -2
- package/dist/components/rapier-3d-world.component.d.ts +6 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rapier-3d-factory.d.ts +6 -1
- package/dist/rapier-3d-factory.js +23 -0
- package/dist/types.d.ts +2 -0
- package/package.json +3 -3
- package/test/components/rapier-3d-character-controller-freefall.spec.ts +43 -0
- package/test/components/rapier-3d-character-controller.component.spec.ts +250 -0
- package/test/components/rapier-3d-player-character-controller-integration.spec.ts +181 -0
- package/test/rapier-3d-factory.spec.ts +68 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { CharacterController3dOptions, CollisionGroup, DebugBody3DSettings, Entity3d, ICharacterController3dComponent, Point3, Point4 } from '@gg-web-engine/core';
|
|
2
|
+
import { Collider, InteractionGroups, KinematicCharacterController, RigidBody, RigidBodyDesc } from '@dimforge/rapier3d-compat';
|
|
3
|
+
import { Rapier3dWorldComponent } from './rapier-3d-world.component';
|
|
4
|
+
import { Rapier3dGgWorld, Rapier3dPhysicsTypeDocRepo } from '../types';
|
|
5
|
+
/**
|
|
6
|
+
* A capsule-shaped kinematic character controller backed by Rapier's own `KinematicCharacterController`
|
|
7
|
+
* (`world.createCharacterController`). `move()` is made fully synchronous (see
|
|
8
|
+
* `ICharacterController3dComponent`'s doc for why this matters) by never relying on
|
|
9
|
+
* `setNextKinematicTranslation` + a later `world.step()` to actually reposition the body - the plain
|
|
10
|
+
* (non-"next") `RigidBody.setTranslation`/`setRotation` is used instead, immediately followed by
|
|
11
|
+
* `World.propagateModifiedBodyPositionsToColliders` so the capsule's new position is visible to
|
|
12
|
+
* Rapier's collider state (and thus to the *next* `move()` call, or to any raycast) without needing a
|
|
13
|
+
* simulation step in between. `setNextKinematicTranslation`/`setNextKinematicRotation` are
|
|
14
|
+
* additionally set to the same target so that dynamic bodies pushed by the character still get a
|
|
15
|
+
* reasonable velocity estimate on whatever `world.step()` happens to run afterwards - this is a
|
|
16
|
+
* nice-to-have, not load-bearing for the synchronous contract.
|
|
17
|
+
*
|
|
18
|
+
* Note: a collider only enters Rapier's broad-phase as part of a `World.step()` - a level's static
|
|
19
|
+
* geometry (or this character itself) created and never stepped even once is invisible to `move()`'s
|
|
20
|
+
* sweep test, exactly as it would be to `world.raycast()`. This is a pre-existing engine property, not
|
|
21
|
+
* specific to this component; a normal per-frame game loop that calls `physicsWorld.simulate()`
|
|
22
|
+
* every tick already satisfies it after the first tick.
|
|
23
|
+
*
|
|
24
|
+
* Note: unlike `Rapier3dRigidBodyComponent`/`Rapier3dTriggerComponent`, this component's native body
|
|
25
|
+
* handle is *not* registered in `Rapier3dWorldComponent.handleIdEntityMap` - `world.raycast()` cannot
|
|
26
|
+
* currently resolve a hit against a character controller back to this component (it will simply be
|
|
27
|
+
* absent from `RaycastResult.hitBody`). Wiring that up would require widening the reverse-map's and
|
|
28
|
+
* `raycast()`'s return-type generics repo-wide for a corner case outside this interface's contract;
|
|
29
|
+
* left as a documented limitation rather than done speculatively.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Rapier3dCharacterControllerComponent implements ICharacterController3dComponent<Rapier3dPhysicsTypeDocRepo> {
|
|
32
|
+
protected readonly world: Rapier3dWorldComponent;
|
|
33
|
+
protected readonly options: Required<CharacterController3dOptions>;
|
|
34
|
+
protected _bodyDescr: RigidBodyDesc;
|
|
35
|
+
entity: Entity3d | null;
|
|
36
|
+
name: string;
|
|
37
|
+
readonly radius: number;
|
|
38
|
+
readonly centersDistance: number;
|
|
39
|
+
private _up;
|
|
40
|
+
get up(): Point3;
|
|
41
|
+
set up(value: Point3);
|
|
42
|
+
private _isGrounded;
|
|
43
|
+
get isGrounded(): boolean;
|
|
44
|
+
private _groundNormal;
|
|
45
|
+
get groundNormal(): Point3 | null;
|
|
46
|
+
private static warnedMissingDtForPush;
|
|
47
|
+
protected _nativeBody: RigidBody | null;
|
|
48
|
+
protected _nativeCollider: Collider | null;
|
|
49
|
+
protected _nativeController: KinematicCharacterController | null;
|
|
50
|
+
get nativeBody(): RigidBody | null;
|
|
51
|
+
get nativeCollider(): Collider | null;
|
|
52
|
+
get nativeController(): KinematicCharacterController | null;
|
|
53
|
+
readonly debugBodySettings: DebugBody3DSettings;
|
|
54
|
+
get position(): Point3;
|
|
55
|
+
set position(value: Point3);
|
|
56
|
+
get rotation(): Point4;
|
|
57
|
+
set rotation(value: Point4);
|
|
58
|
+
protected collisionGroups: InteractionGroups;
|
|
59
|
+
get interactWithCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
60
|
+
set interactWithCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
61
|
+
get ownCollisionGroups(): ReadonlyArray<CollisionGroup>;
|
|
62
|
+
set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
63
|
+
constructor(world: Rapier3dWorldComponent, options: Required<CharacterController3dOptions>, _bodyDescr: RigidBodyDesc);
|
|
64
|
+
/**
|
|
65
|
+
* Makes any position/rotation change applied directly to `_nativeBody` (outside of `move()`, e.g.
|
|
66
|
+
* via the `position`/`rotation` setters) immediately visible to Rapier's collider state, without
|
|
67
|
+
* requiring a `world.step()` - see the class doc for why this matters. The pinned
|
|
68
|
+
* `@dimforge/rapier3d-compat` build only exposes `propagateModifiedBodyPositionsToColliders()` for
|
|
69
|
+
* this (no separate `QueryPipeline`/`updateSceneQueries` object to rebuild - the character
|
|
70
|
+
* controller queries `World`'s live `broadPhase`/`narrowPhase` directly), so that's the only call
|
|
71
|
+
* needed here.
|
|
72
|
+
*/
|
|
73
|
+
private syncColliderTransform;
|
|
74
|
+
move(desiredTranslation: Point3, dt?: number): void;
|
|
75
|
+
/**
|
|
76
|
+
* Shoves any dynamic body this tick's sweep bumped into - see `addToWorld`'s doc for why this is
|
|
77
|
+
* hand-rolled rather than Rapier's own `setApplyImpulsesToDynamicBodies`. Mirrors
|
|
78
|
+
* `AmmoCharacterControllerComponent.pushDynamicBody` exactly: models the contact as a simple
|
|
79
|
+
* inelastic collision against a virtual body of mass `options.pushMass` moving at `characterSpeed`
|
|
80
|
+
* (this tick's *horizontal* displacement - vertical/jump motion never pushes anything sideways -
|
|
81
|
+
* converted to a real m/s via `dt`, not a raw per-tick distance), driving the hit body's velocity
|
|
82
|
+
* along the push direction towards `characterSpeed * pushMass / (pushMass + bodyMass)` and only
|
|
83
|
+
* ever adding forward velocity, never removing any (so a body already outrunning the character in
|
|
84
|
+
* that direction is left alone). `computedCollision()` already has everything needed - populated
|
|
85
|
+
* by the `computeColliderMovement` call above regardless of this method's own logic, so no extra
|
|
86
|
+
* sweep/query is needed to reach it.
|
|
87
|
+
*/
|
|
88
|
+
private pushDynamicBodies;
|
|
89
|
+
/**
|
|
90
|
+
* Rapier's character controller doesn't expose a single "ground normal" directly - only a list of
|
|
91
|
+
* per-obstacle collisions (`computedCollision`) from the last `computeColliderMovement` call, each
|
|
92
|
+
* with its own contact normal. Best-effort approach: scan those collisions and return whichever
|
|
93
|
+
* normal points *most* nearly along `up` (i.e. the most floor-like of the bunch, however steep it
|
|
94
|
+
* actually is) - this deliberately does **not** discard a candidate merely for being steep (e.g.
|
|
95
|
+
* balanced on the flank of a sphere/cylinder, far past `maxSlopeClimbAngleRad`): that judgment
|
|
96
|
+
* belongs entirely to `CharacterController3dEntity.isWalkableGround` at the core level, which
|
|
97
|
+
* needs the *real* contact normal to make it, not a value already pre-filtered down here. An
|
|
98
|
+
* earlier version discarded any candidate with `dot(normal, up) <= 0.1` and fell back to the plain
|
|
99
|
+
* `up` vector when nothing cleared that bar - which silently reported perfectly-flat ground for a
|
|
100
|
+
* character resting against a normal steep enough to fail that same threshold, defeating
|
|
101
|
+
* `isWalkableGround` entirely (confirmed empirically: a character run-and-jumped onto the flank of
|
|
102
|
+
* a static sphere, landing on a contact whose true outward normal was ~70° off `up` - well past the
|
|
103
|
+
* default ~50° `maxSlopeClimbAngleRad` - permanently reported `groundNormal: {0,0,1}` instead, so
|
|
104
|
+
* the core entity kept treating it as resting on flat ground and it never slid off, visibly stuck
|
|
105
|
+
* balanced on a sliver of the sphere even with every input released).
|
|
106
|
+
*
|
|
107
|
+
* `numComputedCollisions()` itself is frequently `0` on a call that is still genuinely grounded -
|
|
108
|
+
* `computeColliderMovement` doesn't record an entry for a character caught by snap-to-ground alone
|
|
109
|
+
* (no obstacle actually blocked the *desired* movement that call), which in practice is most idle
|
|
110
|
+
* ticks: a character standing still (`desiredTranslation` exactly `{0,0,0}`, e.g. player released
|
|
111
|
+
* every key) has nothing for the sweep to hit, so it settles into being grounded via snap alone,
|
|
112
|
+
* over and over, tick after tick, without ever producing a fresh collision entry again. Guessing
|
|
113
|
+
* flat `up` on every such tick is exactly the bug above, just via a different, far more common
|
|
114
|
+
* path than "no collision was ever recorded" suggests - it's not a rare edge case, it's what happens
|
|
115
|
+
* the very first idle tick after any landing (including this one, right after the collision that
|
|
116
|
+
* *did* populate the true steep normal above). Fix: on a `0`-collision grounded call, reuse
|
|
117
|
+
* whichever normal this same field already held **before** this call (the character's own contact
|
|
118
|
+
* geometry hasn't changed just because this particular call didn't happen to re-sweep it) rather
|
|
119
|
+
* than guessing - `move()` only overwrites `this._groundNormal` with this method's return value
|
|
120
|
+
* *after* calling it, so reading the field here still sees the previous call's result. Only when
|
|
121
|
+
* there is no prior normal to reuse either (the very first grounded call ever, landing exactly via
|
|
122
|
+
* snap with nothing recorded yet) does this fall back to the plain `up` vector. Returns `null` if
|
|
123
|
+
* not grounded at all - `_groundNormal` naturally clears itself the moment the character goes
|
|
124
|
+
* airborne, so a later landing never reuses a stale value from a previous, unrelated surface.
|
|
125
|
+
*/
|
|
126
|
+
private computeGroundNormal;
|
|
127
|
+
clone(): Rapier3dCharacterControllerComponent;
|
|
128
|
+
addToWorld(world: Rapier3dGgWorld): void;
|
|
129
|
+
removeFromWorld(world: Rapier3dGgWorld, dispose?: boolean): void;
|
|
130
|
+
dispose(): void;
|
|
131
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { BitMask, DebugBody3DSettings, Pnt3, Qtrn, } from '@gg-web-engine/core';
|
|
2
|
+
import { Quaternion, RigidBodyDesc, Vector3, } from '@dimforge/rapier3d-compat';
|
|
3
|
+
/**
|
|
4
|
+
* A capsule-shaped kinematic character controller backed by Rapier's own `KinematicCharacterController`
|
|
5
|
+
* (`world.createCharacterController`). `move()` is made fully synchronous (see
|
|
6
|
+
* `ICharacterController3dComponent`'s doc for why this matters) by never relying on
|
|
7
|
+
* `setNextKinematicTranslation` + a later `world.step()` to actually reposition the body - the plain
|
|
8
|
+
* (non-"next") `RigidBody.setTranslation`/`setRotation` is used instead, immediately followed by
|
|
9
|
+
* `World.propagateModifiedBodyPositionsToColliders` so the capsule's new position is visible to
|
|
10
|
+
* Rapier's collider state (and thus to the *next* `move()` call, or to any raycast) without needing a
|
|
11
|
+
* simulation step in between. `setNextKinematicTranslation`/`setNextKinematicRotation` are
|
|
12
|
+
* additionally set to the same target so that dynamic bodies pushed by the character still get a
|
|
13
|
+
* reasonable velocity estimate on whatever `world.step()` happens to run afterwards - this is a
|
|
14
|
+
* nice-to-have, not load-bearing for the synchronous contract.
|
|
15
|
+
*
|
|
16
|
+
* Note: a collider only enters Rapier's broad-phase as part of a `World.step()` - a level's static
|
|
17
|
+
* geometry (or this character itself) created and never stepped even once is invisible to `move()`'s
|
|
18
|
+
* sweep test, exactly as it would be to `world.raycast()`. This is a pre-existing engine property, not
|
|
19
|
+
* specific to this component; a normal per-frame game loop that calls `physicsWorld.simulate()`
|
|
20
|
+
* every tick already satisfies it after the first tick.
|
|
21
|
+
*
|
|
22
|
+
* Note: unlike `Rapier3dRigidBodyComponent`/`Rapier3dTriggerComponent`, this component's native body
|
|
23
|
+
* handle is *not* registered in `Rapier3dWorldComponent.handleIdEntityMap` - `world.raycast()` cannot
|
|
24
|
+
* currently resolve a hit against a character controller back to this component (it will simply be
|
|
25
|
+
* absent from `RaycastResult.hitBody`). Wiring that up would require widening the reverse-map's and
|
|
26
|
+
* `raycast()`'s return-type generics repo-wide for a corner case outside this interface's contract;
|
|
27
|
+
* left as a documented limitation rather than done speculatively.
|
|
28
|
+
*/
|
|
29
|
+
export class Rapier3dCharacterControllerComponent {
|
|
30
|
+
get up() {
|
|
31
|
+
return this._up;
|
|
32
|
+
}
|
|
33
|
+
set up(value) {
|
|
34
|
+
var _a;
|
|
35
|
+
this._up = Pnt3.norm(value);
|
|
36
|
+
(_a = this._nativeController) === null || _a === void 0 ? void 0 : _a.setUp(new Vector3(this._up.x, this._up.y, this._up.z));
|
|
37
|
+
}
|
|
38
|
+
get isGrounded() {
|
|
39
|
+
return this._isGrounded;
|
|
40
|
+
}
|
|
41
|
+
get groundNormal() {
|
|
42
|
+
return this._groundNormal;
|
|
43
|
+
}
|
|
44
|
+
get nativeBody() {
|
|
45
|
+
return this._nativeBody;
|
|
46
|
+
}
|
|
47
|
+
get nativeCollider() {
|
|
48
|
+
return this._nativeCollider;
|
|
49
|
+
}
|
|
50
|
+
get nativeController() {
|
|
51
|
+
return this._nativeController;
|
|
52
|
+
}
|
|
53
|
+
get position() {
|
|
54
|
+
return Pnt3.clone(this._nativeBody ? this._nativeBody.translation() : this._bodyDescr.translation);
|
|
55
|
+
}
|
|
56
|
+
set position(value) {
|
|
57
|
+
if (this._nativeBody) {
|
|
58
|
+
const v = new Vector3(value.x, value.y, value.z);
|
|
59
|
+
this._nativeBody.setTranslation(v, true);
|
|
60
|
+
this._nativeBody.setNextKinematicTranslation(v);
|
|
61
|
+
this.syncColliderTransform();
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this._bodyDescr.setTranslation(value.x, value.y, value.z);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
get rotation() {
|
|
68
|
+
return Qtrn.clone(this._nativeBody ? this._nativeBody.rotation() : this._bodyDescr.rotation);
|
|
69
|
+
}
|
|
70
|
+
set rotation(value) {
|
|
71
|
+
if (this._nativeBody) {
|
|
72
|
+
const q = new Quaternion(value.x, value.y, value.z, value.w);
|
|
73
|
+
this._nativeBody.setRotation(q, true);
|
|
74
|
+
this._nativeBody.setNextKinematicRotation(q);
|
|
75
|
+
this.syncColliderTransform();
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this._bodyDescr.setRotation(new Quaternion(value.x, value.y, value.z, value.w));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
get interactWithCollisionGroups() {
|
|
82
|
+
return BitMask.unpack(this.collisionGroups, 16);
|
|
83
|
+
}
|
|
84
|
+
set interactWithCollisionGroups(value) {
|
|
85
|
+
var _a;
|
|
86
|
+
let mask = value === 'all' ? BitMask.full(16) : BitMask.pack(value, 16);
|
|
87
|
+
mask = mask | (this.collisionGroups & (BitMask.full(16) << 16));
|
|
88
|
+
if (mask === this.collisionGroups) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.collisionGroups = mask;
|
|
92
|
+
(_a = this._nativeCollider) === null || _a === void 0 ? void 0 : _a.setCollisionGroups(this.collisionGroups);
|
|
93
|
+
}
|
|
94
|
+
get ownCollisionGroups() {
|
|
95
|
+
return BitMask.unpack(this.collisionGroups >> 16, 16);
|
|
96
|
+
}
|
|
97
|
+
set ownCollisionGroups(value) {
|
|
98
|
+
var _a;
|
|
99
|
+
let mask = value === 'all' ? BitMask.full(16) : BitMask.pack(value, 16);
|
|
100
|
+
mask = (mask << 16) | (this.collisionGroups & BitMask.full(16));
|
|
101
|
+
if (mask === this.collisionGroups) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.collisionGroups = mask;
|
|
105
|
+
(_a = this._nativeCollider) === null || _a === void 0 ? void 0 : _a.setCollisionGroups(this.collisionGroups);
|
|
106
|
+
}
|
|
107
|
+
constructor(world, options, _bodyDescr) {
|
|
108
|
+
this.world = world;
|
|
109
|
+
this.options = options;
|
|
110
|
+
this._bodyDescr = _bodyDescr;
|
|
111
|
+
this.entity = null;
|
|
112
|
+
this.name = '';
|
|
113
|
+
this._isGrounded = false;
|
|
114
|
+
this._groundNormal = null;
|
|
115
|
+
this._nativeBody = null;
|
|
116
|
+
this._nativeCollider = null;
|
|
117
|
+
this._nativeController = null;
|
|
118
|
+
this.collisionGroups = BitMask.full(32);
|
|
119
|
+
this.radius = options.radius;
|
|
120
|
+
this.centersDistance = options.centersDistance;
|
|
121
|
+
this._up = Pnt3.norm(options.up);
|
|
122
|
+
this.debugBodySettings = new DebugBody3DSettings({ type: 'RIGID_DYNAMIC', sleeping: () => false }, { shape: 'CAPSULE', radius: this.radius, centersDistance: this.centersDistance });
|
|
123
|
+
this.ownCollisionGroups = options.ownCollisionGroups;
|
|
124
|
+
this.interactWithCollisionGroups = options.interactWithCollisionGroups;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Makes any position/rotation change applied directly to `_nativeBody` (outside of `move()`, e.g.
|
|
128
|
+
* via the `position`/`rotation` setters) immediately visible to Rapier's collider state, without
|
|
129
|
+
* requiring a `world.step()` - see the class doc for why this matters. The pinned
|
|
130
|
+
* `@dimforge/rapier3d-compat` build only exposes `propagateModifiedBodyPositionsToColliders()` for
|
|
131
|
+
* this (no separate `QueryPipeline`/`updateSceneQueries` object to rebuild - the character
|
|
132
|
+
* controller queries `World`'s live `broadPhase`/`narrowPhase` directly), so that's the only call
|
|
133
|
+
* needed here.
|
|
134
|
+
*/
|
|
135
|
+
syncColliderTransform() {
|
|
136
|
+
this.world.nativeWorld.propagateModifiedBodyPositionsToColliders();
|
|
137
|
+
}
|
|
138
|
+
move(desiredTranslation, dt) {
|
|
139
|
+
if (!this._nativeBody || !this._nativeCollider || !this._nativeController) {
|
|
140
|
+
// not yet added to the world - nothing to sweep against (matches
|
|
141
|
+
// `AmmoCharacterControllerComponent.move`'s no-op contract, see `ICharacterController3dComponent`)
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// make sure collider positions reflect any obstacle moved (by anything) since the last
|
|
145
|
+
// world.step()/propagate call, so the upcoming sweep test is accurate
|
|
146
|
+
this.syncColliderTransform();
|
|
147
|
+
// Rapier's own snap-to-ground, left enabled unconditionally, would otherwise undo a jump
|
|
148
|
+
// takeoff the very next tick: `computeColliderMovement` treats a character within
|
|
149
|
+
// `snapToGroundDistance` of the floor it just left as still grounded and pulls it right back
|
|
150
|
+
// down onto it, and a jump's own per-tick rise (`jumpSpeed * dt`) starts out far smaller than
|
|
151
|
+
// the default 0.3 snap distance - so every jump was silently cancelled before it ever left the
|
|
152
|
+
// ground. This is the exact same failure mode `AmmoCharacterControllerComponent`'s own
|
|
153
|
+
// hand-rolled mover hit and fixed (see that class's `move()` doc's `movingUp` guard); toggling
|
|
154
|
+
// snap-to-ground off for ticks that are actively rising, back on otherwise, mirrors it here.
|
|
155
|
+
//
|
|
156
|
+
// Autostep needs the identical guard, for a related but distinct reason: it only misbehaves
|
|
157
|
+
// while jumping *and* simultaneously blocked horizontally by something taller than
|
|
158
|
+
// `maxStepHeight` (e.g. running at a barrier and jumping right as you reach it, rather than
|
|
159
|
+
// jumping in open space) - confirmed empirically, a jump that looked perfect in the open turned
|
|
160
|
+
// into a small up-then-snap-back-down "flick" the instant the same jump was attempted pressed
|
|
161
|
+
// up against such an obstacle, the rise stopping right around `maxStepHeight` itself before
|
|
162
|
+
// reverting to standing height. Autostep's own "raise up to maxStepHeight, retry the blocked
|
|
163
|
+
// horizontal move, keep the raise only if that retry actually clears" evaluation runs as part of
|
|
164
|
+
// the very same `computeColliderMovement` call handling the jump's vertical component - when the
|
|
165
|
+
// retry still doesn't clear (barrier taller than the raise), whatever it does to "give back" the
|
|
166
|
+
// failed step attempt isn't scoped to just the horizontal axis, so it cancels the deliberate
|
|
167
|
+
// vertical rise sharing that same call too. There's no real reason to want auto-step-climbing
|
|
168
|
+
// active while already deliberately jumping, so disable it under the same condition.
|
|
169
|
+
const movingUp = Pnt3.dot(desiredTranslation, this._up) > 1e-9;
|
|
170
|
+
if (movingUp) {
|
|
171
|
+
this._nativeController.disableSnapToGround();
|
|
172
|
+
this._nativeController.disableAutostep();
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
if (this.options.snapToGroundDistance > 0) {
|
|
176
|
+
this._nativeController.enableSnapToGround(this.options.snapToGroundDistance);
|
|
177
|
+
}
|
|
178
|
+
if (this.options.maxStepHeight > 0) {
|
|
179
|
+
this._nativeController.enableAutostep(this.options.maxStepHeight, this.options.minStepWidth, true);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const desired = new Vector3(desiredTranslation.x, desiredTranslation.y, desiredTranslation.z);
|
|
183
|
+
this._nativeController.computeColliderMovement(this._nativeCollider, desired);
|
|
184
|
+
const computed = this._nativeController.computedMovement();
|
|
185
|
+
const current = this._nativeBody.translation();
|
|
186
|
+
const next = new Vector3(current.x + computed.x, current.y + computed.y, current.z + computed.z);
|
|
187
|
+
this._nativeBody.setTranslation(next, true);
|
|
188
|
+
this._nativeBody.setNextKinematicTranslation(next);
|
|
189
|
+
this.syncColliderTransform();
|
|
190
|
+
this._isGrounded = this._nativeController.computedGrounded();
|
|
191
|
+
this._groundNormal = this.computeGroundNormal();
|
|
192
|
+
this.pushDynamicBodies(desiredTranslation, dt);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Shoves any dynamic body this tick's sweep bumped into - see `addToWorld`'s doc for why this is
|
|
196
|
+
* hand-rolled rather than Rapier's own `setApplyImpulsesToDynamicBodies`. Mirrors
|
|
197
|
+
* `AmmoCharacterControllerComponent.pushDynamicBody` exactly: models the contact as a simple
|
|
198
|
+
* inelastic collision against a virtual body of mass `options.pushMass` moving at `characterSpeed`
|
|
199
|
+
* (this tick's *horizontal* displacement - vertical/jump motion never pushes anything sideways -
|
|
200
|
+
* converted to a real m/s via `dt`, not a raw per-tick distance), driving the hit body's velocity
|
|
201
|
+
* along the push direction towards `characterSpeed * pushMass / (pushMass + bodyMass)` and only
|
|
202
|
+
* ever adding forward velocity, never removing any (so a body already outrunning the character in
|
|
203
|
+
* that direction is left alone). `computedCollision()` already has everything needed - populated
|
|
204
|
+
* by the `computeColliderMovement` call above regardless of this method's own logic, so no extra
|
|
205
|
+
* sweep/query is needed to reach it.
|
|
206
|
+
*/
|
|
207
|
+
pushDynamicBodies(desiredTranslation, dt) {
|
|
208
|
+
var _a;
|
|
209
|
+
const pushMass = this.options.pushMass;
|
|
210
|
+
if (pushMass <= 0 || !this._nativeController) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const vertical = Pnt3.scalarMult(this._up, Pnt3.dot(desiredTranslation, this._up));
|
|
214
|
+
const horizontal = Pnt3.sub(desiredTranslation, vertical);
|
|
215
|
+
const horizLen = Pnt3.len(horizontal);
|
|
216
|
+
if (horizLen <= 1e-9) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// `dt` is required to recover a real m/s speed from `horizLen` (see
|
|
220
|
+
// `ICharacterController3dComponent.move()`'s doc). Falling back to the raw per-tick
|
|
221
|
+
// displacement as if it were already a speed would understate push force by roughly a factor of
|
|
222
|
+
// `dt` - silently wrong, not just imprecise - so skip the push for this tick instead when `dt`
|
|
223
|
+
// isn't available, same as `AmmoCharacterControllerComponent.pushDynamicBody`.
|
|
224
|
+
if (!dt || dt <= 1e-9) {
|
|
225
|
+
if (!Rapier3dCharacterControllerComponent.warnedMissingDtForPush) {
|
|
226
|
+
Rapier3dCharacterControllerComponent.warnedMissingDtForPush = true;
|
|
227
|
+
console.warn('[Rapier3dCharacterControllerComponent] move() was called without `dt` while `pushMass` > ' +
|
|
228
|
+
'0 - skipping this dynamic-body push rather than approximating character speed from raw ' +
|
|
229
|
+
'per-tick displacement (which would understate push force by roughly 1/dt). Pass the ' +
|
|
230
|
+
'real tick delta (seconds) as the third argument to move() to enable pushing dynamic bodies.');
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const direction = Pnt3.scalarMult(horizontal, 1 / horizLen);
|
|
235
|
+
const characterSpeed = horizLen / dt;
|
|
236
|
+
const count = this._nativeController.numComputedCollisions();
|
|
237
|
+
for (let i = 0; i < count; i++) {
|
|
238
|
+
const collision = this._nativeController.computedCollision(i);
|
|
239
|
+
const body = (_a = collision === null || collision === void 0 ? void 0 : collision.collider) === null || _a === void 0 ? void 0 : _a.parent();
|
|
240
|
+
if (!body || !body.isDynamic()) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const bodyMass = body.mass();
|
|
244
|
+
if (bodyMass <= 0) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const pushSpeed = characterSpeed * (pushMass / (pushMass + bodyMass));
|
|
248
|
+
const v = body.linvel();
|
|
249
|
+
const currentAlong = v.x * direction.x + v.y * direction.y + v.z * direction.z;
|
|
250
|
+
if (pushSpeed <= currentAlong) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const delta = pushSpeed - currentAlong;
|
|
254
|
+
body.setLinvel({ x: v.x + direction.x * delta, y: v.y + direction.y * delta, z: v.z + direction.z * delta }, true);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Rapier's character controller doesn't expose a single "ground normal" directly - only a list of
|
|
259
|
+
* per-obstacle collisions (`computedCollision`) from the last `computeColliderMovement` call, each
|
|
260
|
+
* with its own contact normal. Best-effort approach: scan those collisions and return whichever
|
|
261
|
+
* normal points *most* nearly along `up` (i.e. the most floor-like of the bunch, however steep it
|
|
262
|
+
* actually is) - this deliberately does **not** discard a candidate merely for being steep (e.g.
|
|
263
|
+
* balanced on the flank of a sphere/cylinder, far past `maxSlopeClimbAngleRad`): that judgment
|
|
264
|
+
* belongs entirely to `CharacterController3dEntity.isWalkableGround` at the core level, which
|
|
265
|
+
* needs the *real* contact normal to make it, not a value already pre-filtered down here. An
|
|
266
|
+
* earlier version discarded any candidate with `dot(normal, up) <= 0.1` and fell back to the plain
|
|
267
|
+
* `up` vector when nothing cleared that bar - which silently reported perfectly-flat ground for a
|
|
268
|
+
* character resting against a normal steep enough to fail that same threshold, defeating
|
|
269
|
+
* `isWalkableGround` entirely (confirmed empirically: a character run-and-jumped onto the flank of
|
|
270
|
+
* a static sphere, landing on a contact whose true outward normal was ~70° off `up` - well past the
|
|
271
|
+
* default ~50° `maxSlopeClimbAngleRad` - permanently reported `groundNormal: {0,0,1}` instead, so
|
|
272
|
+
* the core entity kept treating it as resting on flat ground and it never slid off, visibly stuck
|
|
273
|
+
* balanced on a sliver of the sphere even with every input released).
|
|
274
|
+
*
|
|
275
|
+
* `numComputedCollisions()` itself is frequently `0` on a call that is still genuinely grounded -
|
|
276
|
+
* `computeColliderMovement` doesn't record an entry for a character caught by snap-to-ground alone
|
|
277
|
+
* (no obstacle actually blocked the *desired* movement that call), which in practice is most idle
|
|
278
|
+
* ticks: a character standing still (`desiredTranslation` exactly `{0,0,0}`, e.g. player released
|
|
279
|
+
* every key) has nothing for the sweep to hit, so it settles into being grounded via snap alone,
|
|
280
|
+
* over and over, tick after tick, without ever producing a fresh collision entry again. Guessing
|
|
281
|
+
* flat `up` on every such tick is exactly the bug above, just via a different, far more common
|
|
282
|
+
* path than "no collision was ever recorded" suggests - it's not a rare edge case, it's what happens
|
|
283
|
+
* the very first idle tick after any landing (including this one, right after the collision that
|
|
284
|
+
* *did* populate the true steep normal above). Fix: on a `0`-collision grounded call, reuse
|
|
285
|
+
* whichever normal this same field already held **before** this call (the character's own contact
|
|
286
|
+
* geometry hasn't changed just because this particular call didn't happen to re-sweep it) rather
|
|
287
|
+
* than guessing - `move()` only overwrites `this._groundNormal` with this method's return value
|
|
288
|
+
* *after* calling it, so reading the field here still sees the previous call's result. Only when
|
|
289
|
+
* there is no prior normal to reuse either (the very first grounded call ever, landing exactly via
|
|
290
|
+
* snap with nothing recorded yet) does this fall back to the plain `up` vector. Returns `null` if
|
|
291
|
+
* not grounded at all - `_groundNormal` naturally clears itself the moment the character goes
|
|
292
|
+
* airborne, so a later landing never reuses a stale value from a previous, unrelated surface.
|
|
293
|
+
*/
|
|
294
|
+
computeGroundNormal() {
|
|
295
|
+
var _a;
|
|
296
|
+
if (!this._isGrounded || !this._nativeController) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const count = this._nativeController.numComputedCollisions();
|
|
300
|
+
let best = null;
|
|
301
|
+
let bestDot = -Infinity;
|
|
302
|
+
for (let i = 0; i < count; i++) {
|
|
303
|
+
const collision = this._nativeController.computedCollision(i);
|
|
304
|
+
if (!(collision === null || collision === void 0 ? void 0 : collision.normal1)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const normal = Pnt3.clone(collision.normal1);
|
|
308
|
+
const dot = Pnt3.dot(normal, this._up);
|
|
309
|
+
if (dot > bestDot) {
|
|
310
|
+
bestDot = dot;
|
|
311
|
+
best = normal;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return (_a = best !== null && best !== void 0 ? best : this._groundNormal) !== null && _a !== void 0 ? _a : Pnt3.clone(this._up);
|
|
315
|
+
}
|
|
316
|
+
clone() {
|
|
317
|
+
// read the CURRENT position/rotation, not `_bodyDescr`'s construction-time values - once
|
|
318
|
+
// `_nativeBody` exists, the `position`/`rotation` setters write straight to it and never touch
|
|
319
|
+
// `_bodyDescr` again (see those setters above), so `_bodyDescr` alone would be stale for any
|
|
320
|
+
// controller that has moved since being added to the world.
|
|
321
|
+
const pos = this.position;
|
|
322
|
+
const rot = this.rotation;
|
|
323
|
+
const bd = RigidBodyDesc.kinematicPositionBased();
|
|
324
|
+
bd.setTranslation(pos.x, pos.y, pos.z);
|
|
325
|
+
bd.setRotation(new Quaternion(rot.x, rot.y, rot.z, rot.w));
|
|
326
|
+
const comp = new Rapier3dCharacterControllerComponent(this.world, this.options, bd);
|
|
327
|
+
comp.collisionGroups = this.collisionGroups;
|
|
328
|
+
return comp;
|
|
329
|
+
}
|
|
330
|
+
addToWorld(world) {
|
|
331
|
+
if (world.physicsWorld != this.world) {
|
|
332
|
+
throw new Error('Rapier3D bodies cannot be shared between different worlds');
|
|
333
|
+
}
|
|
334
|
+
const nativeWorld = this.world.nativeWorld;
|
|
335
|
+
this._nativeBody = nativeWorld.createRigidBody(this._bodyDescr);
|
|
336
|
+
const colliderDescr = this.world.factory.createColliderDescr({
|
|
337
|
+
shape: 'CAPSULE',
|
|
338
|
+
radius: this.radius,
|
|
339
|
+
centersDistance: this.centersDistance,
|
|
340
|
+
})[0];
|
|
341
|
+
this._nativeCollider = nativeWorld.createCollider(colliderDescr, this._nativeBody);
|
|
342
|
+
this._nativeCollider.setCollisionGroups(this.collisionGroups);
|
|
343
|
+
this._nativeController = nativeWorld.createCharacterController(this.options.offset);
|
|
344
|
+
this._nativeController.setUp(new Vector3(this._up.x, this._up.y, this._up.z));
|
|
345
|
+
this._nativeController.setMaxSlopeClimbAngle(this.options.maxSlopeClimbAngleRad);
|
|
346
|
+
if (this.options.maxStepHeight > 0) {
|
|
347
|
+
this._nativeController.enableAutostep(this.options.maxStepHeight, this.options.minStepWidth, true);
|
|
348
|
+
}
|
|
349
|
+
if (this.options.snapToGroundDistance > 0) {
|
|
350
|
+
this._nativeController.enableSnapToGround(this.options.snapToGroundDistance);
|
|
351
|
+
}
|
|
352
|
+
// Rapier's own `KinematicCharacterController` has a built-in equivalent of `pushMass`
|
|
353
|
+
// (`setApplyImpulsesToDynamicBodies(true)` + `setCharacterMass(...)`) that looked like the
|
|
354
|
+
// obvious way to implement pushing here - no hand-rolled logic needed, unlike
|
|
355
|
+
// `AmmoCharacterControllerComponent` (whose ghost-based mover has no native equivalent at all).
|
|
356
|
+
// It was tried first, but is deliberately **not** used: confirmed empirically, enabling it on
|
|
357
|
+
// this kinematic-position-based character body doesn't just get the push physics wrong (mass
|
|
358
|
+
// ordering inverted - a *heavier* box ended up moving further than a lighter one at otherwise
|
|
359
|
+
// identical settings) but genuinely explodes - a pushed box's position jumped by 5+ meters in a
|
|
360
|
+
// single 16ms tick and kept climbing indefinitely tick after tick, not settling. Root cause not
|
|
361
|
+
// fully identified (plausibly `characterMass`'s override interacting badly with this body's own
|
|
362
|
+
// `mass()`, which a kinematic body reports as `0`, somewhere in Rapier's impulse resolution -
|
|
363
|
+
// not something this package's pinned `@dimforge/rapier3d-compat` build exposes enough to debug
|
|
364
|
+
// further from JS). `pushDynamicBodies` below is a hand-rolled equivalent instead, mirroring
|
|
365
|
+
// `AmmoCharacterControllerComponent.pushDynamicBody`'s own formula and contract exactly (down to
|
|
366
|
+
// the same `pushMass <= 0` "disable pushing" convention) - built on `computedCollision()`, which
|
|
367
|
+
// `computeColliderMovement` already populates every `move()` call regardless of this native
|
|
368
|
+
// feature being enabled, so no extra query is needed to reach it.
|
|
369
|
+
this.world.added$.next(this);
|
|
370
|
+
}
|
|
371
|
+
removeFromWorld(world, dispose) {
|
|
372
|
+
if (world.physicsWorld != this.world) {
|
|
373
|
+
throw new Error('Rapier3D bodies cannot be shared between different worlds');
|
|
374
|
+
}
|
|
375
|
+
if (this._nativeController) {
|
|
376
|
+
this.world.nativeWorld.removeCharacterController(this._nativeController);
|
|
377
|
+
this._nativeController = null;
|
|
378
|
+
}
|
|
379
|
+
if (this._nativeBody) {
|
|
380
|
+
if (this._nativeCollider) {
|
|
381
|
+
this.world.nativeWorld.removeCollider(this._nativeCollider, false);
|
|
382
|
+
this._nativeCollider = null;
|
|
383
|
+
}
|
|
384
|
+
this.world.nativeWorld.removeRigidBody(this._nativeBody);
|
|
385
|
+
this._nativeBody = null;
|
|
386
|
+
}
|
|
387
|
+
this.world.removed$.next(this);
|
|
388
|
+
}
|
|
389
|
+
dispose() {
|
|
390
|
+
if (this._nativeBody) {
|
|
391
|
+
this.removeFromWorld({ physicsWorld: this.world }, true);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Module-wide (not per-instance) so a scene with several characters all being driven without
|
|
396
|
+
// `dt` still only logs once, not once per character per tick - see `pushDynamicBodies`'s
|
|
397
|
+
// missing-`dt` handling below.
|
|
398
|
+
Rapier3dCharacterControllerComponent.warnedMissingDtForPush = false;
|
|
@@ -12,7 +12,7 @@ export declare class Rapier3dRaycastVehicleComponent extends Rapier3dRigidBodyCo
|
|
|
12
12
|
constructor(world: Rapier3dWorldComponent, chassisBody: Rapier3dRigidBodyComponent);
|
|
13
13
|
get wheelSpeed(): number;
|
|
14
14
|
addToWorld(world: Rapier3dGgWorld): void;
|
|
15
|
-
removeFromWorld(world: Rapier3dGgWorld): void;
|
|
15
|
+
removeFromWorld(world: Rapier3dGgWorld, dispose?: boolean): void;
|
|
16
16
|
addWheel(options: WheelOptions, suspensionOptions: SuspensionOptions): void;
|
|
17
17
|
setSteering(wheelIndex: number, steering: number): void;
|
|
18
18
|
applyEngineForce(wheelIndex: number, force: number): void;
|
|
@@ -26,14 +26,17 @@ export class Rapier3dRaycastVehicleComponent extends Rapier3dRigidBodyComponent
|
|
|
26
26
|
}
|
|
27
27
|
setInterval(() => this._nativeVehicle.updateVehicle(0.01), 10);
|
|
28
28
|
}
|
|
29
|
-
removeFromWorld(world) {
|
|
29
|
+
removeFromWorld(world, dispose) {
|
|
30
30
|
if (world.physicsWorld != this.world) {
|
|
31
31
|
throw new Error('Rapier3D bodies cannot be shared between different worlds');
|
|
32
32
|
}
|
|
33
33
|
if (this.nativeVehicle) {
|
|
34
34
|
world.physicsWorld.nativeWorld.removeVehicleController(this.nativeVehicle);
|
|
35
35
|
}
|
|
36
|
-
super.removeFromWorld(world);
|
|
36
|
+
super.removeFromWorld(world, dispose);
|
|
37
|
+
if (dispose) {
|
|
38
|
+
this.dispose();
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
addWheel(options, suspensionOptions) {
|
|
39
42
|
const descr = [
|
|
@@ -37,7 +37,7 @@ export declare class Rapier3dRigidBodyComponent implements IRigidBody3dComponent
|
|
|
37
37
|
set ownCollisionGroups(value: ReadonlyArray<CollisionGroup> | 'all');
|
|
38
38
|
clone(): Rapier3dRigidBodyComponent;
|
|
39
39
|
addToWorld(world: Rapier3dGgWorld): void;
|
|
40
|
-
removeFromWorld(world: Rapier3dGgWorld): void;
|
|
40
|
+
removeFromWorld(world: Rapier3dGgWorld, dispose?: boolean): void;
|
|
41
41
|
resetMotion(): void;
|
|
42
42
|
dispose(): void;
|
|
43
43
|
}
|
|
@@ -150,7 +150,7 @@ export class Rapier3dRigidBodyComponent {
|
|
|
150
150
|
this.world.handleIdEntityMap.set(this._nativeBody.handle, this);
|
|
151
151
|
this.world.added$.next(this);
|
|
152
152
|
}
|
|
153
|
-
removeFromWorld(world) {
|
|
153
|
+
removeFromWorld(world, dispose) {
|
|
154
154
|
if (world.physicsWorld != this.world) {
|
|
155
155
|
throw new Error('Rapier3D bodies cannot be shared between different worlds');
|
|
156
156
|
}
|
|
@@ -171,7 +171,7 @@ export class Rapier3dRigidBodyComponent {
|
|
|
171
171
|
}
|
|
172
172
|
dispose() {
|
|
173
173
|
if (this.nativeBody) {
|
|
174
|
-
this.removeFromWorld({ physicsWorld: this.world });
|
|
174
|
+
this.removeFromWorld({ physicsWorld: this.world }, true);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
}
|
|
@@ -17,7 +17,7 @@ export declare class Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent
|
|
|
17
17
|
protected readonly onLeft$: Subject<Rapier3dRigidBodyComponent>;
|
|
18
18
|
constructor(world: Rapier3dWorldComponent, _colliderDescr: ColliderDesc[], shape: Shape3DDescriptor, _bodyDescr: RigidBodyDesc);
|
|
19
19
|
addToWorld(world: Rapier3dGgWorld): void;
|
|
20
|
-
removeFromWorld(world: Rapier3dGgWorld): void;
|
|
20
|
+
removeFromWorld(world: Rapier3dGgWorld, dispose?: boolean): void;
|
|
21
21
|
checkOverlaps(): void;
|
|
22
22
|
clone(): Rapier3dTriggerComponent;
|
|
23
23
|
dispose(): void;
|
|
@@ -29,12 +29,12 @@ export class Rapier3dTriggerComponent extends Rapier3dRigidBodyComponent {
|
|
|
29
29
|
this.world.handleIdEntityMap.set(this._nativeBody.handle, this);
|
|
30
30
|
this.world.added$.next(this);
|
|
31
31
|
}
|
|
32
|
-
removeFromWorld(world) {
|
|
32
|
+
removeFromWorld(world, dispose) {
|
|
33
33
|
for (const body of this.overlaps) {
|
|
34
34
|
this.onLeft$.next(body);
|
|
35
35
|
}
|
|
36
36
|
this.overlaps.clear();
|
|
37
|
-
super.removeFromWorld(world);
|
|
37
|
+
super.removeFromWorld(world, dispose);
|
|
38
38
|
}
|
|
39
39
|
checkOverlaps() {
|
|
40
40
|
this.world.eventQueue.drainCollisionEvents((h1, h2, started) => {
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { CollisionGroup, IPhysicsWorld3dComponent, Point3, RaycastOptions, RaycastResult } from '@gg-web-engine/core';
|
|
2
2
|
import { EventQueue, World } from '@dimforge/rapier3d-compat';
|
|
3
3
|
import { Rapier3dRigidBodyComponent } from './rapier-3d-rigid-body.component';
|
|
4
|
+
import { Rapier3dCharacterControllerComponent } from './rapier-3d-character-controller.component';
|
|
4
5
|
import { Rapier3dFactory } from '../rapier-3d-factory';
|
|
5
6
|
import { Rapier3dLoader } from '../rapier-3d-loader';
|
|
6
7
|
import { Rapier3dPhysicsTypeDocRepo } from '../types';
|
|
7
8
|
import { Subject } from 'rxjs';
|
|
9
|
+
type Rapier3dWorldChild = Rapier3dRigidBodyComponent | Rapier3dCharacterControllerComponent;
|
|
8
10
|
export declare class Rapier3dWorldComponent implements IPhysicsWorld3dComponent<Rapier3dPhysicsTypeDocRepo> {
|
|
9
11
|
private _factory;
|
|
10
12
|
get factory(): Rapier3dFactory;
|
|
11
13
|
private _loader;
|
|
12
14
|
get loader(): Rapier3dLoader;
|
|
13
|
-
readonly added$: Subject<
|
|
14
|
-
readonly removed$: Subject<
|
|
15
|
-
readonly children:
|
|
15
|
+
readonly added$: Subject<Rapier3dWorldChild>;
|
|
16
|
+
readonly removed$: Subject<Rapier3dWorldChild>;
|
|
17
|
+
readonly children: Rapier3dWorldChild[];
|
|
16
18
|
private _gravity;
|
|
17
19
|
get gravity(): Point3;
|
|
18
20
|
set gravity(value: Point3);
|
|
@@ -31,3 +33,4 @@ export declare class Rapier3dWorldComponent implements IPhysicsWorld3dComponent<
|
|
|
31
33
|
raycast(options: RaycastOptions<Point3>): RaycastResult<Point3, Rapier3dRigidBodyComponent>;
|
|
32
34
|
dispose(): void;
|
|
33
35
|
}
|
|
36
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './components/rapier-3d-rigid-body.component';
|
|
2
2
|
export * from './components/rapier-3d-trigger.component';
|
|
3
|
+
export * from './components/rapier-3d-character-controller.component';
|
|
3
4
|
export * from './components/rapier-3d-world.component';
|
|
4
5
|
export * from './rapier-3d-factory';
|
|
5
6
|
export * from './rapier-3d-loader';
|