Skip to content

Commit

Permalink
Fix the collision conditions for 3D physics characters (#7264)
Browse files Browse the repository at this point in the history
* Migrate to Jolt 0.31.0
  • Loading branch information
D8H authored Dec 31, 2024
1 parent cc6b4a2 commit 8e8f3a7
Show file tree
Hide file tree
Showing 7 changed files with 2,897 additions and 2,474 deletions.
84 changes: 53 additions & 31 deletions Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ namespace gdjs {

export class Physics3DRuntimeBehavior extends gdjs.RuntimeBehavior {
bodyUpdater: gdjs.Physics3DRuntimeBehavior.BodyUpdater;
collisionChecker: gdjs.Physics3DRuntimeBehavior.CollisionChecker;
owner3D: gdjs.RuntimeObject3D;

bodyType: string;
Expand Down Expand Up @@ -338,7 +339,7 @@ namespace gdjs {
* each time the methods onContactBegin and onContactEnd are called by the contact
* listener.
*/
private _currentContacts: Array<Physics3DRuntimeBehavior> = [];
_currentContacts: Array<Physics3DRuntimeBehavior> = [];

private _destroyedDuringFrameLogic: boolean;
_body: Jolt.Body | null = null;
Expand Down Expand Up @@ -381,6 +382,9 @@ namespace gdjs {
this.bodyUpdater = new gdjs.Physics3DRuntimeBehavior.DefaultBodyUpdater(
this
);
this.collisionChecker = new gdjs.Physics3DRuntimeBehavior.DefaultCollisionChecker(
this
);
this.owner3D = owner;
this.bodyType = behaviorData.bodyType;
this.bullet = behaviorData.bullet;
Expand Down Expand Up @@ -540,7 +544,7 @@ namespace gdjs {
if (this._body) {
this._sharedData.bodyInterface.SetPosition(
this._body.GetID(),
this.getVec3(
this.getRVec3(
behaviorSpecificProps.px,
behaviorSpecificProps.py,
behaviorSpecificProps.pz
Expand Down Expand Up @@ -1696,27 +1700,7 @@ namespace gdjs {
behaviorName
) as Physics3DRuntimeBehavior | null;
if (!behavior1) return false;

if (
behavior1._currentContacts.some(
(behavior) => behavior.owner === object2
)
) {
return true;
}
// If a contact has started at this frame and ended right away, it
// won't appear in current contacts but the condition should return
// true anyway.
if (
behavior1._contactsStartedThisFrame.some(
(behavior) => behavior.owner === object2
)
) {
return true;
}

// No contact found
return false;
return behavior1.collisionChecker.isColliding(object2);
}

static hasCollisionStartedBetween(
Expand All @@ -1728,10 +1712,7 @@ namespace gdjs {
behaviorName
) as Physics3DRuntimeBehavior | null;
if (!behavior1) return false;

return behavior1._contactsStartedThisFrame.some(
(behavior) => behavior.owner === object2
);
return behavior1.collisionChecker.hasCollisionStartedWith(object2);
}

static hasCollisionStoppedBetween(
Expand All @@ -1743,10 +1724,7 @@ namespace gdjs {
behaviorName
) as Physics3DRuntimeBehavior | null;
if (!behavior1) return false;

return behavior1._contactsEndedThisFrame.some(
(behavior) => behavior.owner === object2
);
return behavior1.collisionChecker.hasCollisionStoppedWith(object2);
}
}

Expand Down Expand Up @@ -1877,5 +1855,49 @@ namespace gdjs {
);
}
}

export interface CollisionChecker {
isColliding(object: gdjs.RuntimeObject): boolean;
hasCollisionStartedWith(object: gdjs.RuntimeObject): boolean;
hasCollisionStoppedWith(object: gdjs.RuntimeObject): boolean;
}

/**
* The default collision checker uses the contacts found while
* stepping the physics simulation. For characters, another one is used
* as characters are simulated before the rest of the physics simulation.
*/
export class DefaultCollisionChecker implements CollisionChecker {
behavior: gdjs.Physics3DRuntimeBehavior;

constructor(behavior: gdjs.Physics3DRuntimeBehavior) {
this.behavior = behavior;
}

isColliding(object: gdjs.RuntimeObject): boolean {
if (
this.behavior._currentContacts.some(
(behavior) => behavior.owner === object
)
) {
return true;
}
return this.behavior._contactsStartedThisFrame.some(
(behavior) => behavior.owner === object
);
}

hasCollisionStartedWith(object: gdjs.RuntimeObject): boolean {
return this.behavior._contactsStartedThisFrame.some(
(behavior) => behavior.owner === object
);
}

hasCollisionStoppedWith(object: gdjs.RuntimeObject): boolean {
return this.behavior._contactsEndedThisFrame.some(
(behavior) => behavior.owner === object
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ namespace gdjs {
* before stepping the world.
*/
_sharedData: gdjs.Physics3DSharedData;
collisionChecker: gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterCollisionChecker;

// TODO Should there be angle were the character can climb but will slip?
_slopeMaxAngle: float;
Expand Down Expand Up @@ -121,6 +122,9 @@ namespace gdjs {
instanceContainer.getScene(),
behaviorData.Physics3D
);
this.collisionChecker = new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterCollisionChecker(
this
);

this._slopeMaxAngle = 0;
this.setSlopeMaxAngle(behaviorData.slopeMaxAngle);
Expand Down Expand Up @@ -180,6 +184,7 @@ namespace gdjs {
behavior.bodyUpdater = new gdjs.PhysicsCharacter3DRuntimeBehavior.CharacterBodyUpdater(
this
);
behavior.collisionChecker = this.collisionChecker;
behavior.recreateBody();

// Always begin in the direction of the object.
Expand Down Expand Up @@ -328,7 +333,9 @@ namespace gdjs {
);
}

onDeActivate() {}
onDeActivate() {
this.collisionChecker.clearContacts();
}

onActivate() {}

Expand Down Expand Up @@ -498,6 +505,7 @@ namespace gdjs {
shapeFilter,
this._sharedData.jolt.GetTempAllocator()
);
this.collisionChecker.updateContacts();

if (this.isOnFloor()) {
this._canJump = true;
Expand Down Expand Up @@ -1426,5 +1434,87 @@ namespace gdjs {
this.updateCharacterPosition();
}
}

/**
* A character is simulated by Jolt before the rest of the physics simulation
* (see `doBeforePhysicsStep`).
* This means that contacts with the character would only rarely be recognized by
* the physics engine if using the default contact listeners.
* Instead, this class allows to properly track contacts of the character
* using Jolt `CharacterVirtual::GetActiveContacts`.
*/
export class CharacterCollisionChecker
implements gdjs.Physics3DRuntimeBehavior.CollisionChecker {
characterBehavior: gdjs.PhysicsCharacter3DRuntimeBehavior;

_currentContacts: Array<Physics3DRuntimeBehavior> = [];
_previousContacts: Array<Physics3DRuntimeBehavior> = [];

constructor(characterBehavior: gdjs.PhysicsCharacter3DRuntimeBehavior) {
this.characterBehavior = characterBehavior;
}

clearContacts(): void {
this._previousContacts.length = 0;
this._currentContacts.length = 0;
}

updateContacts(): void {
const swap = this._previousContacts;
this._previousContacts = this._currentContacts;
this._currentContacts = swap;
this._currentContacts.length = 0;

const { character, _sharedData } = this.characterBehavior;
if (!character) {
return;
}
const contacts = character.GetActiveContacts();
for (let index = 0; index < contacts.size(); index++) {
const contact = contacts.at(index);

const bodyLockInterface = _sharedData.physicsSystem.GetBodyLockInterface();
const body = bodyLockInterface.TryGetBody(contact.mBodyB);
const behavior = body.gdjsAssociatedBehavior;
if (behavior) {
this._currentContacts.push(behavior);
}
}
}

isColliding(object: gdjs.RuntimeObject): boolean {
const { character } = this.characterBehavior;
if (!character) {
return false;
}
return this._currentContacts.some(
(behavior) => behavior.owner === object
);
}

hasCollisionStartedWith(object: gdjs.RuntimeObject): boolean {
const { character } = this.characterBehavior;
if (!character) {
return false;
}
return (
this._currentContacts.some((behavior) => behavior.owner === object) &&
!this._previousContacts.some((behavior) => behavior.owner === object)
);
}

hasCollisionStoppedWith(object: gdjs.RuntimeObject): boolean {
const { character } = this.characterBehavior;
if (!character) {
return false;
}
return (
!this._currentContacts.some(
(behavior) => behavior.owner === object
) &&
this._previousContacts.some((behavior) => behavior.owner === object)
);
}
}
}
}
Loading

0 comments on commit 8e8f3a7

Please sign in to comment.