dacha 0.18.0-alpha.4 → 0.18.0-alpha.5
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/build/contrib/components/character-body/index.d.ts +66 -0
- package/build/contrib/components/character-body/index.js +81 -0
- package/build/contrib/components/collider/index.d.ts +9 -9
- package/build/contrib/components/collider/index.js +6 -6
- package/build/contrib/components/index.d.ts +1 -0
- package/build/contrib/components/index.js +1 -0
- package/build/contrib/events/index.d.ts +29 -0
- package/build/contrib/events/index.js +9 -0
- package/build/contrib/systems/character-controller/index.d.ts +32 -0
- package/build/contrib/systems/character-controller/index.js +262 -0
- package/build/contrib/systems/character-controller/utils.d.ts +2 -0
- package/build/contrib/systems/character-controller/utils.js +9 -0
- package/build/contrib/systems/index.d.ts +1 -0
- package/build/contrib/systems/index.js +1 -0
- package/build/contrib/systems/physics-system/api.d.ts +50 -8
- package/build/contrib/systems/physics-system/api.js +51 -10
- package/build/contrib/systems/physics-system/physics-system.d.ts +1 -0
- package/build/contrib/systems/physics-system/physics-system.js +22 -3
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-box-cast-geometry.d.ts +4 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-box-cast-geometry.js +7 -4
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-box-geometry.d.ts +2 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-box-geometry.js +15 -15
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-capsule-cast-geometry.d.ts +4 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-capsule-cast-geometry.js +14 -10
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-capsule-geometry.d.ts +2 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-capsule-geometry.js +21 -9
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-circle-cast-geometry.d.ts +4 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-circle-cast-geometry.js +10 -6
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-circle-geometry.d.ts +2 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-circle-geometry.js +19 -13
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-ray-geometry.js +1 -1
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-segment-geometry.d.ts +2 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/build-segment-geometry.js +9 -5
- package/build/contrib/systems/physics-system/subsystems/collision-detection/geometry-builders/index.d.ts +2 -2
- package/build/contrib/systems/physics-system/subsystems/collision-detection/index.d.ts +5 -3
- package/build/contrib/systems/physics-system/subsystems/collision-detection/index.js +31 -1
- package/build/contrib/systems/physics-system/subsystems/collision-detection/query-utils.d.ts +8 -5
- package/build/contrib/systems/physics-system/subsystems/collision-detection/query-utils.js +50 -5
- package/build/contrib/systems/physics-system/subsystems/collision-detection/types.d.ts +4 -0
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/index.d.ts +2 -5
- package/build/contrib/systems/physics-system/subsystems/constraint-solver/index.js +11 -45
- package/build/contrib/systems/physics-system/subsystems/physics/index.d.ts +9 -3
- package/build/contrib/systems/physics-system/subsystems/physics/index.js +8 -5
- package/build/contrib/systems/physics-system/tests/helpers.d.ts +1 -1
- package/build/contrib/systems/physics-system/tests/helpers.js +3 -2
- package/build/contrib/systems/physics-system/types.d.ts +58 -2
- package/build/contrib/utils/one-way-validator.d.ts +12 -0
- package/build/contrib/utils/one-way-validator.js +53 -0
- package/build/events/index.d.ts +2 -2
- package/build/events/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Component } from '../../../engine/component';
|
|
2
|
+
import { Vector2 } from '../../../engine/math-lib';
|
|
3
|
+
import type { Actor } from '../../../engine/actor';
|
|
4
|
+
export type CharacterMotionMode = 'surface' | 'free';
|
|
5
|
+
export interface CharacterBodyConfig {
|
|
6
|
+
motionMode?: CharacterMotionMode;
|
|
7
|
+
upDirectionX?: number;
|
|
8
|
+
upDirectionY?: number;
|
|
9
|
+
skinWidth?: number;
|
|
10
|
+
maxSlopeAngle?: number;
|
|
11
|
+
maxSlides?: number;
|
|
12
|
+
maxRecoveries?: number;
|
|
13
|
+
groundSnapDistance?: number;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Kinematic character controller state and movement settings.
|
|
18
|
+
*
|
|
19
|
+
* The controller system consumes `velocity` during fixed updates, performs
|
|
20
|
+
* sweep/slide collision handling, and writes the resulting target through the
|
|
21
|
+
* actor's kinematic rigid body.
|
|
22
|
+
*
|
|
23
|
+
* @category Components
|
|
24
|
+
*/
|
|
25
|
+
export declare class CharacterBody extends Component {
|
|
26
|
+
private _up;
|
|
27
|
+
/** @internal Pending one-step displacement consumed by CharacterController */
|
|
28
|
+
_displacement: Vector2;
|
|
29
|
+
/** Controls contact classification and whether ground snapping is enabled. */
|
|
30
|
+
motionMode: CharacterMotionMode;
|
|
31
|
+
/** Desired character velocity in world units per second */
|
|
32
|
+
velocity: Vector2;
|
|
33
|
+
/** Small distance kept between the character shape and blocking colliders */
|
|
34
|
+
skinWidth: number;
|
|
35
|
+
/** Maximum walkable ground angle in radians, measured from upDirection */
|
|
36
|
+
maxSlopeAngle: number;
|
|
37
|
+
/** Maximum sweep/slide collision iterations used during one fixed update */
|
|
38
|
+
maxSlides: number;
|
|
39
|
+
/** Maximum overlap depenetration iterations used before sweep/slide movement */
|
|
40
|
+
maxRecoveries: number;
|
|
41
|
+
/** Distance used to probe opposite upDirection and keep ground contact over small gaps */
|
|
42
|
+
groundSnapDistance: number;
|
|
43
|
+
/** Whether the controller should be ignored by CharacterController */
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
/** Whether the controller is standing on walkable ground after the last fixed update */
|
|
46
|
+
onGround: boolean;
|
|
47
|
+
/** Whether movement hit a side wall or non-walkable surface during the last fixed update */
|
|
48
|
+
onWall: boolean;
|
|
49
|
+
/** Whether movement hit a ceiling during the last fixed update */
|
|
50
|
+
onCeiling: boolean;
|
|
51
|
+
/** Normal of the current walkable ground surface, updated by CharacterController */
|
|
52
|
+
groundNormal: Vector2;
|
|
53
|
+
/** Actor providing the current walkable ground, or null when not grounded */
|
|
54
|
+
groundActor: Actor | null;
|
|
55
|
+
constructor(config?: CharacterBodyConfig);
|
|
56
|
+
/** Direction treated as up for ground, ceiling, slopes, ground probes, and jumps */
|
|
57
|
+
get upDirection(): Vector2;
|
|
58
|
+
set upDirection(value: Vector2);
|
|
59
|
+
/**
|
|
60
|
+
* Adds a one-step world-space displacement request.
|
|
61
|
+
*
|
|
62
|
+
* The displacement is consumed by CharacterController on the next fixed
|
|
63
|
+
* update and is already expected to be scaled by delta time.
|
|
64
|
+
*/
|
|
65
|
+
move(displacement: Vector2): void;
|
|
66
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Component } from '../../../engine/component';
|
|
2
|
+
import { MathOps, Vector2 } from '../../../engine/math-lib';
|
|
3
|
+
/**
|
|
4
|
+
* Kinematic character controller state and movement settings.
|
|
5
|
+
*
|
|
6
|
+
* The controller system consumes `velocity` during fixed updates, performs
|
|
7
|
+
* sweep/slide collision handling, and writes the resulting target through the
|
|
8
|
+
* actor's kinematic rigid body.
|
|
9
|
+
*
|
|
10
|
+
* @category Components
|
|
11
|
+
*/
|
|
12
|
+
export class CharacterBody extends Component {
|
|
13
|
+
_up;
|
|
14
|
+
/** @internal Pending one-step displacement consumed by CharacterController */
|
|
15
|
+
_displacement;
|
|
16
|
+
/** Controls contact classification and whether ground snapping is enabled. */
|
|
17
|
+
motionMode;
|
|
18
|
+
/** Desired character velocity in world units per second */
|
|
19
|
+
velocity;
|
|
20
|
+
/** Small distance kept between the character shape and blocking colliders */
|
|
21
|
+
skinWidth;
|
|
22
|
+
/** Maximum walkable ground angle in radians, measured from upDirection */
|
|
23
|
+
maxSlopeAngle;
|
|
24
|
+
/** Maximum sweep/slide collision iterations used during one fixed update */
|
|
25
|
+
maxSlides;
|
|
26
|
+
/** Maximum overlap depenetration iterations used before sweep/slide movement */
|
|
27
|
+
maxRecoveries;
|
|
28
|
+
/** Distance used to probe opposite upDirection and keep ground contact over small gaps */
|
|
29
|
+
groundSnapDistance;
|
|
30
|
+
/** Whether the controller should be ignored by CharacterController */
|
|
31
|
+
disabled;
|
|
32
|
+
/** Whether the controller is standing on walkable ground after the last fixed update */
|
|
33
|
+
onGround;
|
|
34
|
+
/** Whether movement hit a side wall or non-walkable surface during the last fixed update */
|
|
35
|
+
onWall;
|
|
36
|
+
/** Whether movement hit a ceiling during the last fixed update */
|
|
37
|
+
onCeiling;
|
|
38
|
+
/** Normal of the current walkable ground surface, updated by CharacterController */
|
|
39
|
+
groundNormal;
|
|
40
|
+
/** Actor providing the current walkable ground, or null when not grounded */
|
|
41
|
+
groundActor;
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
super();
|
|
44
|
+
this._up = new Vector2(0, -1);
|
|
45
|
+
this._displacement = new Vector2(0, 0);
|
|
46
|
+
this.upDirection = new Vector2(config.upDirectionX ?? 0, config.upDirectionY ?? -1);
|
|
47
|
+
this.motionMode = config.motionMode ?? 'surface';
|
|
48
|
+
this.disabled = config.disabled ?? false;
|
|
49
|
+
this.velocity = new Vector2(0, 0);
|
|
50
|
+
this.skinWidth = config.skinWidth ?? 0.1;
|
|
51
|
+
this.maxSlopeAngle = MathOps.degToRad(config.maxSlopeAngle ?? 45);
|
|
52
|
+
this.maxSlides = config.maxSlides ?? 4;
|
|
53
|
+
this.maxRecoveries = config.maxRecoveries ?? 3;
|
|
54
|
+
this.groundSnapDistance = config.groundSnapDistance ?? 1;
|
|
55
|
+
this.onGround = false;
|
|
56
|
+
this.onWall = false;
|
|
57
|
+
this.onCeiling = false;
|
|
58
|
+
this.groundNormal = this.upDirection.clone();
|
|
59
|
+
this.groundActor = null;
|
|
60
|
+
}
|
|
61
|
+
/** Direction treated as up for ground, ceiling, slopes, ground probes, and jumps */
|
|
62
|
+
get upDirection() {
|
|
63
|
+
return this._up;
|
|
64
|
+
}
|
|
65
|
+
set upDirection(value) {
|
|
66
|
+
this._up = value.clone().normalize();
|
|
67
|
+
if (this._up.magnitude === 0) {
|
|
68
|
+
throw new Error('Character controller upDirection must be non-zero');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Adds a one-step world-space displacement request.
|
|
73
|
+
*
|
|
74
|
+
* The displacement is consumed by CharacterController on the next fixed
|
|
75
|
+
* update and is already expected to be scaled by delta time.
|
|
76
|
+
*/
|
|
77
|
+
move(displacement) {
|
|
78
|
+
this._displacement.add(displacement);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
CharacterBody.componentName = 'CharacterBody';
|
|
@@ -11,24 +11,24 @@ export interface BaseColliderConfig {
|
|
|
11
11
|
}
|
|
12
12
|
export interface BoxColliderConfig extends BaseColliderConfig {
|
|
13
13
|
type: 'box';
|
|
14
|
-
sizeX
|
|
15
|
-
sizeY
|
|
14
|
+
sizeX?: number;
|
|
15
|
+
sizeY?: number;
|
|
16
16
|
}
|
|
17
17
|
export interface CircleColliderConfig extends BaseColliderConfig {
|
|
18
18
|
type: 'circle';
|
|
19
|
-
radius
|
|
19
|
+
radius?: number;
|
|
20
20
|
}
|
|
21
21
|
export interface SegmentColliderConfig extends BaseColliderConfig {
|
|
22
22
|
type: 'segment';
|
|
23
|
-
point1X
|
|
24
|
-
point1Y
|
|
25
|
-
point2X
|
|
26
|
-
point2Y
|
|
23
|
+
point1X?: number;
|
|
24
|
+
point1Y?: number;
|
|
25
|
+
point2X?: number;
|
|
26
|
+
point2Y?: number;
|
|
27
27
|
}
|
|
28
28
|
export interface CapsuleColliderConfig extends BaseColliderConfig {
|
|
29
29
|
type: 'capsule';
|
|
30
|
-
height
|
|
31
|
-
radius
|
|
30
|
+
height?: number;
|
|
31
|
+
radius?: number;
|
|
32
32
|
}
|
|
33
33
|
export type ColliderConfig = BoxColliderConfig | CircleColliderConfig | SegmentColliderConfig | CapsuleColliderConfig;
|
|
34
34
|
export interface BoxColliderShape {
|
|
@@ -50,27 +50,27 @@ export class Collider extends Component {
|
|
|
50
50
|
case 'box':
|
|
51
51
|
this.shape = {
|
|
52
52
|
type: config.type,
|
|
53
|
-
size: { x: config.sizeX, y: config.sizeY },
|
|
53
|
+
size: { x: config.sizeX ?? 0, y: config.sizeY ?? 0 },
|
|
54
54
|
};
|
|
55
55
|
break;
|
|
56
56
|
case 'circle':
|
|
57
57
|
this.shape = {
|
|
58
58
|
type: config.type,
|
|
59
|
-
radius: config.radius,
|
|
59
|
+
radius: config.radius ?? 0,
|
|
60
60
|
};
|
|
61
61
|
break;
|
|
62
62
|
case 'segment':
|
|
63
63
|
this.shape = {
|
|
64
64
|
type: config.type,
|
|
65
|
-
point1: { x: config.point1X, y: config.point1Y },
|
|
66
|
-
point2: { x: config.point2X, y: config.point2Y },
|
|
65
|
+
point1: { x: config.point1X ?? 0, y: config.point1Y ?? 0 },
|
|
66
|
+
point2: { x: config.point2X ?? 0, y: config.point2Y ?? 0 },
|
|
67
67
|
};
|
|
68
68
|
break;
|
|
69
69
|
case 'capsule':
|
|
70
70
|
this.shape = {
|
|
71
71
|
type: config.type,
|
|
72
|
-
height: config.height,
|
|
73
|
-
radius: config.radius,
|
|
72
|
+
height: config.height ?? 0,
|
|
73
|
+
radius: config.radius ?? 0,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -2,6 +2,7 @@ export { Camera, type CameraConfig } from './camera';
|
|
|
2
2
|
export { KeyboardControl, type KeyboardControlConfig, } from './keyboard-control';
|
|
3
3
|
export { Collider, type ColliderConfig, type ColliderShape, type ColliderType, type BoxColliderShape, type CircleColliderShape, type SegmentColliderShape, type CapsuleColliderShape, } from './collider';
|
|
4
4
|
export { RigidBody, type RigidBodyConfig } from './rigid-body';
|
|
5
|
+
export { CharacterBody, type CharacterBodyConfig } from './character-body';
|
|
5
6
|
export { Animatable, type AnimatableConfig } from './animatable';
|
|
6
7
|
export { Sprite, type SpriteConfig } from './sprite';
|
|
7
8
|
export { Transform, type TransformConfig } from './transform';
|
|
@@ -2,6 +2,7 @@ export { Camera } from './camera';
|
|
|
2
2
|
export { KeyboardControl, } from './keyboard-control';
|
|
3
3
|
export { Collider, } from './collider';
|
|
4
4
|
export { RigidBody } from './rigid-body';
|
|
5
|
+
export { CharacterBody } from './character-body';
|
|
5
6
|
export { Animatable } from './animatable';
|
|
6
7
|
export { Sprite } from './sprite';
|
|
7
8
|
export { Transform } from './transform';
|
|
@@ -56,6 +56,15 @@ export declare const CollisionStay = "CollisionStay";
|
|
|
56
56
|
* @category Events
|
|
57
57
|
*/
|
|
58
58
|
export declare const CollisionLeave = "CollisionLeave";
|
|
59
|
+
/**
|
|
60
|
+
* Dispatched when a character body hits a blocking actor
|
|
61
|
+
*
|
|
62
|
+
* @event
|
|
63
|
+
* @type {CharacterHitEvent}
|
|
64
|
+
*
|
|
65
|
+
* @category Events
|
|
66
|
+
*/
|
|
67
|
+
export declare const CharacterHit = "CharacterHit";
|
|
59
68
|
/**
|
|
60
69
|
* Dispatched to play audio on an actor
|
|
61
70
|
*
|
|
@@ -155,6 +164,25 @@ export type CollisionStayEvent = CollisionStateEvent;
|
|
|
155
164
|
* @category Events
|
|
156
165
|
*/
|
|
157
166
|
export type CollisionLeaveEvent = CollisionStateEvent;
|
|
167
|
+
/** Event signature for the {@link CharacterHit} event
|
|
168
|
+
*
|
|
169
|
+
* @category Events
|
|
170
|
+
*/
|
|
171
|
+
export type CharacterHitEvent = ActorEvent<{
|
|
172
|
+
/** Actor hit by the character body */
|
|
173
|
+
actor: Actor;
|
|
174
|
+
/** Hit point in world space */
|
|
175
|
+
point: {
|
|
176
|
+
x: number;
|
|
177
|
+
y: number;
|
|
178
|
+
};
|
|
179
|
+
/** Hit normal pointing from the hit actor to the character */
|
|
180
|
+
normal: Vector2;
|
|
181
|
+
/** Distance from the cast origin to the hit */
|
|
182
|
+
distance: number;
|
|
183
|
+
/** Surface classification used by the character controller */
|
|
184
|
+
kind: 'ground' | 'wall' | 'ceiling';
|
|
185
|
+
}>;
|
|
158
186
|
/** Event signature for the {@link SetAudioVolume} event when used on actor level
|
|
159
187
|
*
|
|
160
188
|
* @category Events
|
|
@@ -174,6 +202,7 @@ declare module '../../types/events' {
|
|
|
174
202
|
[CollisionEnter]: CollisionEnterEvent;
|
|
175
203
|
[CollisionStay]: CollisionStayEvent;
|
|
176
204
|
[CollisionLeave]: CollisionLeaveEvent;
|
|
205
|
+
[CharacterHit]: CharacterHitEvent;
|
|
177
206
|
[PlayAudio]: ActorEvent;
|
|
178
207
|
[StopAudio]: ActorEvent;
|
|
179
208
|
[SetAudioVolume]: SetAudioSourceVolumeEvent;
|
|
@@ -52,6 +52,15 @@ export const CollisionStay = 'CollisionStay';
|
|
|
52
52
|
* @category Events
|
|
53
53
|
*/
|
|
54
54
|
export const CollisionLeave = 'CollisionLeave';
|
|
55
|
+
/**
|
|
56
|
+
* Dispatched when a character body hits a blocking actor
|
|
57
|
+
*
|
|
58
|
+
* @event
|
|
59
|
+
* @type {CharacterHitEvent}
|
|
60
|
+
*
|
|
61
|
+
* @category Events
|
|
62
|
+
*/
|
|
63
|
+
export const CharacterHit = 'CharacterHit';
|
|
55
64
|
/**
|
|
56
65
|
* Dispatched to play audio on an actor
|
|
57
66
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SceneSystem } from '../../../engine/system';
|
|
2
|
+
import type { SceneSystemOptions, UpdateOptions } from '../../../engine/system';
|
|
3
|
+
/**
|
|
4
|
+
* Kinematic character controller system with sweep/slide collision movement.
|
|
5
|
+
*
|
|
6
|
+
* The system expects actors to have `CharacterBody`, `Transform`,
|
|
7
|
+
* `Collider`, and a kinematic `RigidBody`. Put this system before
|
|
8
|
+
* `PhysicsSystem` in system configuration so `movePosition()` targets are
|
|
9
|
+
* consumed by physics in the same fixed step.
|
|
10
|
+
*
|
|
11
|
+
* @category Systems
|
|
12
|
+
*/
|
|
13
|
+
export declare class CharacterController extends SceneSystem {
|
|
14
|
+
private actorQuery;
|
|
15
|
+
private world;
|
|
16
|
+
private oneWayValidator;
|
|
17
|
+
constructor(options: SceneSystemOptions);
|
|
18
|
+
onSceneDestroy(): void;
|
|
19
|
+
private isBlockingHit;
|
|
20
|
+
private isWalkable;
|
|
21
|
+
private isRecoverableOverlap;
|
|
22
|
+
private resetGroundState;
|
|
23
|
+
private resetState;
|
|
24
|
+
private castAll;
|
|
25
|
+
private cast;
|
|
26
|
+
private castGround;
|
|
27
|
+
private recoverOverlaps;
|
|
28
|
+
private handleHit;
|
|
29
|
+
private move;
|
|
30
|
+
private updateGroundState;
|
|
31
|
+
fixedUpdate(options: UpdateOptions): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { ActorQuery } from '../../../engine/actor';
|
|
2
|
+
import { SceneSystem } from '../../../engine/system';
|
|
3
|
+
import { Vector2, VectorOps } from '../../../engine/math-lib';
|
|
4
|
+
import { Collider, CharacterBody, RigidBody, Transform, } from '../../components';
|
|
5
|
+
import { PhysicsAPI } from '../physics-system';
|
|
6
|
+
import { OneWayValidator } from '../../utils/one-way-validator';
|
|
7
|
+
import { CharacterHit } from '../../events';
|
|
8
|
+
import { clipAgainstNormal } from './utils';
|
|
9
|
+
const DISTANCE_EPSILON = 0.000001;
|
|
10
|
+
const SNAP_EPSILON = 0.000001;
|
|
11
|
+
/**
|
|
12
|
+
* Kinematic character controller system with sweep/slide collision movement.
|
|
13
|
+
*
|
|
14
|
+
* The system expects actors to have `CharacterBody`, `Transform`,
|
|
15
|
+
* `Collider`, and a kinematic `RigidBody`. Put this system before
|
|
16
|
+
* `PhysicsSystem` in system configuration so `movePosition()` targets are
|
|
17
|
+
* consumed by physics in the same fixed step.
|
|
18
|
+
*
|
|
19
|
+
* @category Systems
|
|
20
|
+
*/
|
|
21
|
+
export class CharacterController extends SceneSystem {
|
|
22
|
+
actorQuery;
|
|
23
|
+
world;
|
|
24
|
+
oneWayValidator;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
super();
|
|
27
|
+
this.world = options.world;
|
|
28
|
+
this.actorQuery = new ActorQuery({
|
|
29
|
+
scene: options.scene,
|
|
30
|
+
filter: [CharacterBody, Transform, Collider, RigidBody],
|
|
31
|
+
});
|
|
32
|
+
this.oneWayValidator = new OneWayValidator();
|
|
33
|
+
}
|
|
34
|
+
onSceneDestroy() {
|
|
35
|
+
this.actorQuery.destroy();
|
|
36
|
+
}
|
|
37
|
+
isBlockingHit(actor, hit) {
|
|
38
|
+
const rigidBody = hit.actor.getComponent(RigidBody);
|
|
39
|
+
if (!rigidBody || rigidBody.disabled) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!rigidBody?.oneWay || !rigidBody.oneWayNormal) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return this.oneWayValidator.validate(hit.actor, actor, hit.normal);
|
|
46
|
+
}
|
|
47
|
+
isWalkable(actor, normal) {
|
|
48
|
+
const character = actor.getComponent(CharacterBody);
|
|
49
|
+
return (VectorOps.dotProduct(normal, character.upDirection) >=
|
|
50
|
+
Math.cos(character.maxSlopeAngle));
|
|
51
|
+
}
|
|
52
|
+
isRecoverableOverlap(actor, hit) {
|
|
53
|
+
const rigidBody = hit.actor.getComponent(RigidBody);
|
|
54
|
+
if (!rigidBody || rigidBody.disabled || rigidBody.type === 'dynamic') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!rigidBody.oneWay || !rigidBody.oneWayNormal) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return this.oneWayValidator.validate(hit.actor, actor, hit.normal);
|
|
61
|
+
}
|
|
62
|
+
resetGroundState(character) {
|
|
63
|
+
if (character.onGround) {
|
|
64
|
+
character.groundActor = null;
|
|
65
|
+
character.groundNormal = character.upDirection.clone();
|
|
66
|
+
character.onGround = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
resetState(character) {
|
|
70
|
+
this.resetGroundState(character);
|
|
71
|
+
character.onWall = false;
|
|
72
|
+
character.onCeiling = false;
|
|
73
|
+
}
|
|
74
|
+
castAll(actor, position, displacement) {
|
|
75
|
+
const physicsApi = this.world.systemApi.get(PhysicsAPI);
|
|
76
|
+
const character = actor.getComponent(CharacterBody);
|
|
77
|
+
const transform = actor.getComponent(Transform);
|
|
78
|
+
const distance = displacement.magnitude;
|
|
79
|
+
if (distance <= DISTANCE_EPSILON) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return physicsApi.castActorAll({
|
|
83
|
+
actor,
|
|
84
|
+
offset: {
|
|
85
|
+
x: position.x - transform.world.position.x,
|
|
86
|
+
y: position.y - transform.world.position.y,
|
|
87
|
+
},
|
|
88
|
+
direction: displacement,
|
|
89
|
+
maxDistance: distance + character.skinWidth,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
cast(actor, position, displacement) {
|
|
93
|
+
const hits = this.castAll(actor, position, displacement);
|
|
94
|
+
for (const hit of hits) {
|
|
95
|
+
if (hit.distance <= DISTANCE_EPSILON &&
|
|
96
|
+
VectorOps.dotProduct(displacement, hit.normal) > DISTANCE_EPSILON) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (this.isBlockingHit(actor, hit)) {
|
|
100
|
+
return hit;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
castGround(actor, position, displacement) {
|
|
106
|
+
const hits = this.castAll(actor, position, displacement);
|
|
107
|
+
for (const hit of hits) {
|
|
108
|
+
if (this.isBlockingHit(actor, hit) &&
|
|
109
|
+
this.isWalkable(actor, hit.normal)) {
|
|
110
|
+
return hit;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
recoverOverlaps(actor, position) {
|
|
116
|
+
const physicsApi = this.world.systemApi.get(PhysicsAPI);
|
|
117
|
+
const character = actor.getComponent(CharacterBody);
|
|
118
|
+
const transform = actor.getComponent(Transform);
|
|
119
|
+
for (let i = 0; i < character.maxRecoveries; i += 1) {
|
|
120
|
+
const hits = physicsApi.overlapActor({
|
|
121
|
+
actor,
|
|
122
|
+
offset: {
|
|
123
|
+
x: position.x - transform.world.position.x,
|
|
124
|
+
y: position.y - transform.world.position.y,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
let recovered = false;
|
|
128
|
+
for (const hit of hits) {
|
|
129
|
+
if (!this.isRecoverableOverlap(actor, hit)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const correction = hit.penetration + character.skinWidth;
|
|
133
|
+
if (correction <= DISTANCE_EPSILON) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
position.x += hit.normal.x * correction;
|
|
137
|
+
position.y += hit.normal.y * correction;
|
|
138
|
+
recovered = true;
|
|
139
|
+
}
|
|
140
|
+
if (!recovered) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
handleHit(actor, hit) {
|
|
146
|
+
const character = actor.getComponent(CharacterBody);
|
|
147
|
+
let kind;
|
|
148
|
+
if (character.motionMode === 'surface') {
|
|
149
|
+
const upDot = VectorOps.dotProduct(hit.normal, character.upDirection);
|
|
150
|
+
const threshold = Math.cos(character.maxSlopeAngle);
|
|
151
|
+
if (upDot >= threshold) {
|
|
152
|
+
kind = 'ground';
|
|
153
|
+
character.onGround = true;
|
|
154
|
+
character.groundNormal = hit.normal;
|
|
155
|
+
character.groundActor = hit.actor;
|
|
156
|
+
}
|
|
157
|
+
else if (upDot <= -threshold) {
|
|
158
|
+
kind = 'ceiling';
|
|
159
|
+
character.onCeiling = true;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
kind = 'wall';
|
|
163
|
+
character.onWall = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
kind = 'wall';
|
|
168
|
+
character.onWall = true;
|
|
169
|
+
}
|
|
170
|
+
actor.dispatchEvent(CharacterHit, {
|
|
171
|
+
actor: hit.actor,
|
|
172
|
+
point: hit.point,
|
|
173
|
+
normal: hit.normal,
|
|
174
|
+
distance: hit.distance,
|
|
175
|
+
kind,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
move(actor, position, displacement) {
|
|
179
|
+
const character = actor.getComponent(CharacterBody);
|
|
180
|
+
for (let i = 0; i < character.maxSlides; i += 1) {
|
|
181
|
+
const distance = displacement.magnitude;
|
|
182
|
+
if (distance <= DISTANCE_EPSILON) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
const hit = this.cast(actor, position, displacement);
|
|
186
|
+
if (!hit) {
|
|
187
|
+
position.add(displacement);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
const safeDistance = Math.max(hit.distance - character.skinWidth, 0);
|
|
191
|
+
const direction = displacement.clone().normalize();
|
|
192
|
+
position.x += direction.x * safeDistance;
|
|
193
|
+
position.y += direction.y * safeDistance;
|
|
194
|
+
this.handleHit(actor, hit);
|
|
195
|
+
clipAgainstNormal(character.velocity, hit.normal);
|
|
196
|
+
const remainingDistance = Math.max(distance - safeDistance, 0);
|
|
197
|
+
displacement.x = direction.x * remainingDistance;
|
|
198
|
+
displacement.y = direction.y * remainingDistance;
|
|
199
|
+
clipAgainstNormal(displacement, hit.normal);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
updateGroundState(actor, target, skipSnapping) {
|
|
203
|
+
const character = actor.getComponent(CharacterBody);
|
|
204
|
+
if (skipSnapping) {
|
|
205
|
+
this.resetGroundState(character);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const probeDistance = Math.max(character.groundSnapDistance, character.skinWidth);
|
|
209
|
+
const probeDirection = character.upDirection
|
|
210
|
+
.clone()
|
|
211
|
+
.multiplyNumber(-probeDistance);
|
|
212
|
+
const hit = this.castGround(actor, target, probeDirection);
|
|
213
|
+
if (!hit) {
|
|
214
|
+
this.resetGroundState(character);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
character.onGround = true;
|
|
218
|
+
character.groundNormal = hit.normal;
|
|
219
|
+
character.groundActor = hit.actor;
|
|
220
|
+
const snapDistance = Math.max(hit.distance - character.skinWidth, 0);
|
|
221
|
+
const snapDirection = character.upDirection
|
|
222
|
+
.clone()
|
|
223
|
+
.multiplyNumber(-snapDistance);
|
|
224
|
+
target.x += snapDirection.x;
|
|
225
|
+
target.y += snapDirection.y;
|
|
226
|
+
}
|
|
227
|
+
fixedUpdate(options) {
|
|
228
|
+
if (!this.world.systemApi.has(PhysicsAPI)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const deltaTimeInSeconds = options.deltaTime / 1000;
|
|
232
|
+
this.oneWayValidator.update();
|
|
233
|
+
this.actorQuery.getActors().forEach((actor) => {
|
|
234
|
+
const transform = actor.getComponent(Transform);
|
|
235
|
+
const character = actor.getComponent(CharacterBody);
|
|
236
|
+
const rigidBody = actor.getComponent(RigidBody);
|
|
237
|
+
const collider = actor.getComponent(Collider);
|
|
238
|
+
this.resetState(character);
|
|
239
|
+
if (character.disabled ||
|
|
240
|
+
rigidBody.disabled ||
|
|
241
|
+
collider.disabled ||
|
|
242
|
+
rigidBody.type !== 'kinematic') {
|
|
243
|
+
character._displacement.multiplyNumber(0);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const displacement = character.velocity
|
|
247
|
+
.clone()
|
|
248
|
+
.multiplyNumber(deltaTimeInSeconds)
|
|
249
|
+
.add(character._displacement);
|
|
250
|
+
const movingUp = VectorOps.dotProduct(displacement, character.upDirection) >
|
|
251
|
+
SNAP_EPSILON;
|
|
252
|
+
const position = new Vector2(transform.world.position.x, transform.world.position.y);
|
|
253
|
+
this.recoverOverlaps(actor, position);
|
|
254
|
+
this.move(actor, position, displacement);
|
|
255
|
+
this.updateGroundState(actor, position, movingUp || character.motionMode === 'free');
|
|
256
|
+
rigidBody.movePosition(position);
|
|
257
|
+
character._displacement.multiplyNumber(0);
|
|
258
|
+
});
|
|
259
|
+
this.oneWayValidator.lateUpdate();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
CharacterController.systemName = 'CharacterController';
|
|
@@ -3,6 +3,7 @@ export { CameraSystem, CameraAPI } from './camera-system';
|
|
|
3
3
|
export { GameStatsMeter } from './game-stats-meter';
|
|
4
4
|
export { KeyboardInputSystem } from './keyboard-input-system';
|
|
5
5
|
export { KeyboardControlSystem } from './keyboard-control-system';
|
|
6
|
+
export { CharacterController } from './character-controller';
|
|
6
7
|
export { MouseControlSystem } from './mouse-control-system';
|
|
7
8
|
export { MouseInputSystem } from './mouse-input-system';
|
|
8
9
|
export { PhysicsSystem, PhysicsAPI } from './physics-system';
|
|
@@ -3,6 +3,7 @@ export { CameraSystem, CameraAPI } from './camera-system';
|
|
|
3
3
|
export { GameStatsMeter } from './game-stats-meter';
|
|
4
4
|
export { KeyboardInputSystem } from './keyboard-input-system';
|
|
5
5
|
export { KeyboardControlSystem } from './keyboard-control-system';
|
|
6
|
+
export { CharacterController } from './character-controller';
|
|
6
7
|
export { MouseControlSystem } from './mouse-control-system';
|
|
7
8
|
export { MouseInputSystem } from './mouse-input-system';
|
|
8
9
|
export { PhysicsSystem, PhysicsAPI } from './physics-system';
|