Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/core/src/Script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ export class Script extends Component {
}

this._entity._addScript(this);
if (this._hasCollisionEventCallbacks()) {
this.scene.physics._markCollisionEventConsumersDirty();
}
}

/**
Expand All @@ -252,6 +255,18 @@ export class Script extends Component {
}

this._entity._removeScript(this);
if (this._hasCollisionEventCallbacks()) {
this.scene.physics._markCollisionEventConsumersDirty();
}
}

private _hasCollisionEventCallbacks(): boolean {
const { prototype } = Script;
return (
this.onCollisionEnter !== prototype.onCollisionEnter ||
this.onCollisionExit !== prototype.onCollisionExit ||
this.onCollisionStay !== prototype.onCollisionStay
);
}

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/physics/Collision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ export class Collision {
*/
getContacts(outContacts: ContactPoint[]): number {
const nativeCollision = this._nativeCollision;
const smallerShapeId = Math.min(nativeCollision.shape0Id, nativeCollision.shape1Id);
const factor = this.shape.id === smallerShapeId ? 1 : -1;
const factor = this.shape.id === nativeCollision.shape1Id ? 1 : -1;
const nativeContactPoints = nativeCollision.getContacts();
const length = nativeContactPoints.size();
for (let i = 0; i < length; i++) {
Expand Down
53 changes: 53 additions & 0 deletions packages/core/src/physics/PhysicsScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export class PhysicsScene {

private _gravity: Vector3 = new Vector3(0, -9.81, 0);
private _nativePhysicsScene: IPhysicsScene;
private _collisionEventConsumersDirty = true;
private _hasCollisionEventConsumersCache = false;
private _contactEventEnabled: boolean | undefined;

/**
* The gravity of physics scene.
Expand Down Expand Up @@ -646,6 +649,7 @@ export class PhysicsScene {
for (let i = 0; i < step; i++) {
componentsManager.callScriptOnPhysicsUpdate();
this._callColliderOnUpdate();
this._syncContactEventDemand();
nativePhysicsManager.update(fixedTimeStep);
this._callColliderOnLateUpdate();
this._dispatchEvents(nativePhysicsManager.updateEvents());
Expand All @@ -661,6 +665,7 @@ export class PhysicsScene {
if (collider._index === -1) {
collider._index = this._colliders.length;
this._colliders.add(collider);
this._markCollisionEventConsumersDirty();
}
this._nativePhysicsScene.addCollider(<ICollider>collider._nativeCollider);
}
Expand All @@ -674,6 +679,7 @@ export class PhysicsScene {
if (controller._index === -1) {
controller._index = this._colliders.length;
this._colliders.add(controller);
this._markCollisionEventConsumersDirty();
}
this._nativePhysicsScene.addCharacterController(<ICharacterController>controller._nativeCollider);
}
Expand All @@ -687,6 +693,7 @@ export class PhysicsScene {
const replaced = this._colliders.deleteByIndex(collider._index);
replaced && (replaced._index = collider._index);
collider._index = -1;
this._markCollisionEventConsumersDirty();
this._nativePhysicsScene.removeCollider(<ICollider>collider._nativeCollider);
}

Expand All @@ -699,9 +706,17 @@ export class PhysicsScene {
const replaced = this._colliders.deleteByIndex(controller._index);
replaced && (replaced._index = controller._index);
controller._index = -1;
this._markCollisionEventConsumersDirty();
this._nativePhysicsScene.removeCharacterController(<ICharacterController>controller._nativeCollider);
}

/**
* @internal
*/
_markCollisionEventConsumersDirty(): void {
this._collisionEventConsumersDirty = true;
}

/**
* @internal
*/
Expand Down Expand Up @@ -825,6 +840,44 @@ export class PhysicsScene {
}
}

private _hasCollisionEventConsumers(): boolean {
if (!this._collisionEventConsumersDirty) {
return this._hasCollisionEventConsumersCache;
}

const { _elements: colliders } = this._colliders;
const { onCollisionEnter, onCollisionExit, onCollisionStay } = Script.prototype;

for (let i = this._colliders.length - 1; i >= 0; --i) {
const scripts = colliders[i].entity._scripts;
const scriptElements = scripts._elements;
for (let j = scripts.length - 1; j >= 0; --j) {
const script = scriptElements[j];
if (
script.onCollisionEnter !== onCollisionEnter ||
script.onCollisionExit !== onCollisionExit ||
script.onCollisionStay !== onCollisionStay
) {
this._collisionEventConsumersDirty = false;
this._hasCollisionEventConsumersCache = true;
return true;
}
}
}

this._collisionEventConsumersDirty = false;
this._hasCollisionEventConsumersCache = false;
return this._hasCollisionEventConsumersCache;
}

private _syncContactEventDemand(): void {
const enabled = this._hasCollisionEventConsumers();
if (this._contactEventEnabled !== enabled) {
this._nativePhysicsScene.setContactEventEnabled?.(enabled);
this._contactEventEnabled = enabled;
}
}

private _setGravity(): void {
this._nativePhysicsScene.setGravity(this._gravity);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/design/src/physics/IPhysicsScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export interface IPhysicsScene {
*/
update(elapsedTime: number): void;

/**
* Enable contact event buffering.
* @param enabled - Whether collision contact events should be buffered for dispatch.
*/
setContactEventEnabled?(enabled: boolean): void;

/**
* Collect buffered collision and trigger events.
* Must be called after update() and after syncing transforms back from physics.
Expand Down
7 changes: 7 additions & 0 deletions packages/physics-lite/src/LitePhysicsScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ export class LitePhysicsScene implements IPhysicsScene {
}
}

/**
* {@inheritDoc IPhysicsScene.setContactEventEnabled }
*/
setContactEventEnabled(_enabled: boolean): void {
// Physics-lite only produces trigger events, so there is no contact buffer to toggle.
}

/**
* {@inheritDoc IPhysicsScene.updateEvents }
*/
Expand Down
17 changes: 17 additions & 0 deletions packages/physics-physx/src/PhysXPhysicsScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class PhysXPhysicsScene implements IPhysicsScene {
private _activeTriggers: DisorderedArray<TriggerEvent> = new DisorderedArray<TriggerEvent>();
private _contactEvents: ContactEvent[] = [];
private _contactEventCount = 0;
private _contactEventEnabled = true;
private _triggerEvents: TriggerEvent[] = [];
private _physicsEvents: IPhysicsEvents = { contactEvents: [], contactEventCount: 0, triggerEvents: [] };

Expand Down Expand Up @@ -191,6 +192,20 @@ export class PhysXPhysicsScene implements IPhysicsScene {
this._fetchResults();
}

/**
* {@inheritDoc IPhysicsScene.setContactEventEnabled }
*/
setContactEventEnabled(enabled: boolean): void {
if (this._contactEventEnabled === enabled) {
return;
}

this._contactEventEnabled = enabled;
if (!enabled) {
this._contactEventCount = 0;
}
}

/**
* {@inheritDoc IPhysicsScene.updateEvents }
*/
Expand Down Expand Up @@ -547,6 +562,8 @@ export class PhysXPhysicsScene implements IPhysicsScene {
}

private _bufferContactEvent(collision: ICollision, state: number): void {
if (!this._contactEventEnabled) return;

const index = this._contactEventCount++;
const event = (this._contactEvents[index] ||= new ContactEvent());
event.shape0Id = collision.shape0Id;
Expand Down
28 changes: 27 additions & 1 deletion tests/src/core/physics/Collision.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BoxColliderShape, DynamicCollider, Entity, Engine, Script, StaticCollider } from "@galacean/engine-core";
import { Vector3 } from "@galacean/engine-math";
import { PhysXPhysics } from "@galacean/engine-physics-physx";
import { WebGLEngine } from "@galacean/engine";
import { WebGLEngine } from "@galacean/engine-rhi-webgl";
import { Collision } from "packages/core/types/physics/Collision";
import { describe, beforeAll, beforeEach, expect, it } from "vitest";

Expand Down Expand Up @@ -164,4 +164,30 @@ describe("Collision", function () {
engine.sceneManager.activeScene.physics._update(1);
});
});

it("reports contact normal from static other shape to dynamic self shape", function () {
engine.sceneManager.activeScene.physics.gravity = new Vector3(0, 0, 0);
const dynamicBox = addBox(new Vector3(1, 1, 1), DynamicCollider, new Vector3(-3, 0, 0));
const staticBox = addBox(new Vector3(1, 1, 1), StaticCollider, new Vector3(0, 0, 0));

return new Promise<void>((done) => {
dynamicBox.addComponent(
class extends Script {
onCollisionEnter(other: Collision): void {
expect(other.shape).toBe(staticBox.getComponent(StaticCollider).shapes[0]);
const contacts = [];
other.getContacts(contacts);
expect(contacts.length).toBeGreaterThan(0);
expect(formatValue(contacts[0].normal.x)).toBe(-1);

done();
}
}
);

dynamicBox.getComponent(DynamicCollider).applyForce(new Vector3(1000, 0, 0));
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
});
});
});
116 changes: 115 additions & 1 deletion tests/src/core/physics/PhysicsScene.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { Ray, Vector3, Quaternion } from "@galacean/engine-math";
import { LitePhysics } from "@galacean/engine-physics-lite";
import { PhysXPhysics } from "@galacean/engine-physics-physx";
import { WebGLEngine } from "@galacean/engine";
import { WebGLEngine } from "@galacean/engine-rhi-webgl";
import { vi, describe, beforeAll, expect, it, afterEach } from "vitest";

class CollisionTestScript extends Script {
Expand Down Expand Up @@ -74,12 +74,44 @@ class CollisionTestScript extends Script {
}
}

class CollisionDemandScript extends Script {
onCollisionEnter(): void {}
}

class TriggerDemandScript extends Script {
onTriggerEnter(): void {}
}

function updatePhysics(physics) {
for (let i = 0; i < 5; ++i) {
physics._update(8);
}
}

function watchNativeContactEventDemand(physicsScene: PhysicsScene) {
const nativeScene = (physicsScene as any)._nativePhysicsScene;
const original = nativeScene.setContactEventEnabled;
const calls: boolean[] = [];
nativeScene.setContactEventEnabled = (enabled: boolean) => {
calls.push(enabled);
original?.call(nativeScene, enabled);
};
return {
calls,
restore() {
if (original) {
nativeScene.setContactEventEnabled = original;
} else {
delete nativeScene.setContactEventEnabled;
}
}
};
}

function getLastContactEventDemandCall(calls: boolean[]): boolean {
return calls[calls.length - 1] ?? false;
}

function resetSpy() {
// reset spy on collision test script.
CollisionTestScript.prototype.onCollisionEnter = vi.fn(CollisionTestScript.prototype.onCollisionEnter);
Expand Down Expand Up @@ -422,6 +454,88 @@ describe("Physics Test", () => {
expect(enginePhysX.sceneManager.scenes[0].physics.fixedTimeStep).to.eq(fixedTimeStep);
});

it("auto-disables native contact events when no active collision callback exists", () => {
const scene = enginePhysX.sceneManager.activeScene;
const physicsScene = scene.physics;
const root = scene.createRootEntity("contact-demand-disabled");
const entity = root.createChild("body");
const collider = entity.addComponent(StaticCollider);
collider.addShape(new BoxColliderShape());
const contactEventDemand = watchNativeContactEventDemand(physicsScene);

try {
physicsScene._update(physicsScene.fixedTimeStep);
expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false);
} finally {
contactEventDemand.restore();
root.destroy();
}
});

it("auto-enables native contact events only while an active collision callback exists", () => {
const scene = enginePhysX.sceneManager.activeScene;
const physicsScene = scene.physics;
const root = scene.createRootEntity("contact-demand-enabled");
const entity = root.createChild("body");
const collider = entity.addComponent(StaticCollider);
collider.addShape(new BoxColliderShape());
const script = entity.addComponent(CollisionDemandScript);
const contactEventDemand = watchNativeContactEventDemand(physicsScene);

try {
physicsScene._update(physicsScene.fixedTimeStep);
expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(true);

script.enabled = false;
physicsScene._update(physicsScene.fixedTimeStep);
expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false);
} finally {
contactEventDemand.restore();
root.destroy();
}
});

it("does not rescan contact event demand on every fixed substep", () => {
const scene = enginePhysX.sceneManager.activeScene;
const physicsScene = scene.physics;
const fixedTimeStep = physicsScene.fixedTimeStep;
const root = scene.createRootEntity("contact-demand-substeps");
const entity = root.createChild("body");
const collider = entity.addComponent(StaticCollider);
collider.addShape(new BoxColliderShape());
entity.addComponent(CollisionDemandScript);
const contactEventDemand = watchNativeContactEventDemand(physicsScene);

try {
physicsScene.fixedTimeStep = 1 / 480;
physicsScene._update(1 / 60);
expect(contactEventDemand.calls).to.deep.eq([true]);
} finally {
physicsScene.fixedTimeStep = fixedTimeStep;
contactEventDemand.restore();
root.destroy();
}
});

it("keeps native contact events disabled when only trigger callbacks exist", () => {
const scene = enginePhysX.sceneManager.activeScene;
const physicsScene = scene.physics;
const root = scene.createRootEntity("contact-demand-trigger-only");
const entity = root.createChild("body");
const collider = entity.addComponent(StaticCollider);
collider.addShape(new BoxColliderShape());
entity.addComponent(TriggerDemandScript);
const contactEventDemand = watchNativeContactEventDemand(physicsScene);

try {
physicsScene._update(physicsScene.fixedTimeStep);
expect(getLastContactEventDemandCall(contactEventDemand.calls)).to.eq(false);
} finally {
contactEventDemand.restore();
root.destroy();
}
});

it("raycast", () => {
const scene = enginePhysX.sceneManager.activeScene;
const physicsScene = scene.physics;
Expand Down
Loading