Support collision group and fix the position of characterController synchronization late issue (#2614)

* feat: support collision group
---------

Co-authored-by: GuoLei1990 <gl3336563@163.com>
This commit is contained in:
luzhuang
2025-05-14 20:09:40 +08:00
committed by GitHub
parent d069479671
commit ea51cf33e0
26 changed files with 935 additions and 46 deletions

View File

@@ -0,0 +1,134 @@
/**
* @title LitePhysics Collision Group
* @category Physics
*/
import {
WebGLEngine,
SphereColliderShape,
DynamicCollider,
BoxColliderShape,
Vector3,
MeshRenderer,
PointLight,
PrimitiveMesh,
Camera,
Script,
StaticCollider,
ColliderShape,
PBRMaterial,
Entity,
Layer
} from "@galacean/engine";
import { LitePhysics } from "@galacean/engine-physics-lite";
import { initScreenshot, updateForE2E } from "./.mockForE2E";
class MoveScript extends Script {
onUpdate() {
this.entity.transform.position.y -= 0.1;
}
onTriggerEnter(other: ColliderShape) {
// Change color to green when collision occurs
(this.entity.getComponent(MeshRenderer).getMaterial() as PBRMaterial).baseColor.set(0, 1, 0, 1);
}
}
// Create a sphere with physics
function createPhysicsSphere(
rootEntity: Entity,
name: string,
position: Vector3,
radius: number,
color: Vector3,
collisionLayer: Layer
) {
const sphereEntity = rootEntity.createChild(name);
sphereEntity.transform.setPosition(position.x, position.y, position.z);
sphereEntity.addComponent(MoveScript);
// Add visual representation
const sphereMtl = new PBRMaterial(rootEntity.engine);
const sphereRenderer = sphereEntity.addComponent(MeshRenderer);
sphereMtl.baseColor.set(color.x, color.y, color.z, 1.0);
sphereMtl.metallic = 0.0;
sphereMtl.roughness = 0.5;
sphereRenderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius);
sphereRenderer.setMaterial(sphereMtl);
// Add physics
const physicsSphere = new SphereColliderShape();
physicsSphere.radius = radius;
const sphereCollider = sphereEntity.addComponent(DynamicCollider);
sphereCollider.collisionLayer = collisionLayer;
sphereCollider.addShape(physicsSphere);
return sphereEntity;
}
WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity("root");
// Set up ambient lighting
scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1);
scene.ambientLight.diffuseIntensity = 1.2;
// Set up camera
const cameraEntity = rootEntity.createChild("camera");
const camera = cameraEntity.addComponent(Camera);
cameraEntity.transform.setPosition(0, 3, 15);
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));
// Add point light
const light = rootEntity.createChild("light");
light.transform.setPosition(0, 10, 0);
const pointLight = light.addComponent(PointLight);
pointLight.intensity = 1.5;
const groundEntity = rootEntity.createChild("ground");
// 设置立方体的位置和大小
groundEntity.transform.setPosition(0, 1, 0);
// groundEntity.isActive = false;
// Visual representation of the ground cube
const groundMtl = new PBRMaterial(engine);
groundMtl.baseColor.set(0.5, 0.5, 0.5, 1.0);
groundMtl.roughness = 0.7;
const cubeSize = new Vector3(10, 0.2, 10);
const groundRenderer = groundEntity.addComponent(MeshRenderer);
groundRenderer.mesh = PrimitiveMesh.createCuboid(engine, cubeSize.x, cubeSize.y, cubeSize.z);
groundRenderer.setMaterial(groundMtl);
// Physics for the ground cube
const groundCollider = groundEntity.addComponent(StaticCollider);
const groundShape = new BoxColliderShape();
groundShape.size = cubeSize;
groundCollider.addShape(groundShape);
groundCollider.collisionLayer = Layer.Layer3;
const sphere1 = createPhysicsSphere(
rootEntity,
"RedSphere",
new Vector3(-2, 5, 0),
0.5,
new Vector3(1, 0, 0),
Layer.Layer1
);
const sphere2 = createPhysicsSphere(
rootEntity,
"BlueSphere",
new Vector3(2, 5, 0),
0.5,
new Vector3(0, 0, 1),
Layer.Layer2
);
scene.physics.setColliderLayerCollision(Layer.Layer2, Layer.Layer3, false);
updateForE2E(engine, 1000, 38);
initScreenshot(engine, camera);
});

View File

@@ -0,0 +1,146 @@
/**
* @title Physx Collision Group
* @category Physics
*/
import {
WebGLEngine,
SphereColliderShape,
DynamicCollider,
BoxColliderShape,
Vector3,
MeshRenderer,
PointLight,
PrimitiveMesh,
Camera,
Script,
StaticCollider,
ColliderShape,
PBRMaterial,
Entity,
Layer
} from "@galacean/engine";
import { PhysXPhysics } from "@galacean/engine-physics-physx";
import { initScreenshot, updateForE2E } from "./.mockForE2E";
class CheckScript extends Script {
onTriggerEnter(other: ColliderShape) {
console.log("onTriggerEnter", other);
// Change color to green when collision occurs
(this.entity.getComponent(MeshRenderer).getMaterial() as PBRMaterial).baseColor.set(0, 1, 0, 1);
}
onContactEnter(other: ColliderShape) {
console.log("onContactEnter", other);
// Change color to green when collision occurs
(this.entity.getComponent(MeshRenderer).getMaterial() as PBRMaterial).baseColor.set(0, 1, 0, 1);
}
}
// Create a sphere with physics
function createPhysicsSphere(
rootEntity: Entity,
name: string,
position: Vector3,
radius: number,
color: Vector3,
collisionLayer: number
) {
const sphereEntity = rootEntity.createChild(name);
sphereEntity.transform.setPosition(position.x, position.y, position.z);
// Add visual representation
const sphereMtl = new PBRMaterial(rootEntity.engine);
const sphereRenderer = sphereEntity.addComponent(MeshRenderer);
sphereMtl.baseColor.set(color.x, color.y, color.z, 1.0);
sphereMtl.metallic = 0.0;
sphereMtl.roughness = 0.5;
sphereRenderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius);
sphereRenderer.setMaterial(sphereMtl);
// Add physics
const physicsSphere = new SphereColliderShape();
physicsSphere.radius = radius;
physicsSphere.material.bounciness = 0.8;
const sphereCollider = sphereEntity.addComponent(DynamicCollider);
sphereCollider.collisionLayer = collisionLayer;
sphereEntity.addComponent(CheckScript);
sphereCollider.addShape(physicsSphere);
return sphereEntity;
}
WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity("root");
// Set up ambient lighting
scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1);
scene.ambientLight.diffuseIntensity = 1.2;
// Set up camera
const cameraEntity = rootEntity.createChild("camera");
const camera = cameraEntity.addComponent(Camera);
// 调整相机位置以便更好地观察穿透效果
cameraEntity.transform.setPosition(0, 3, 15);
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));
// Add point light
const light = rootEntity.createChild("light");
light.transform.setPosition(0, 10, 0);
const pointLight = light.addComponent(PointLight);
pointLight.intensity = 1.5;
// 创建立方体作为地面
const groundEntity = rootEntity.createChild("ground");
// 设置立方体的位置和大小
groundEntity.transform.setPosition(0, 1, 0);
// Visual representation of the ground cube
const groundMtl = new PBRMaterial(engine);
groundMtl.baseColor.set(0.5, 0.5, 0.5, 1.0);
groundMtl.roughness = 0.7;
// 设置半透明以便能看到穿透的球体
groundMtl.baseColor.a = 0.5;
const cubeSize = new Vector3(10, 0.2, 10);
const groundRenderer = groundEntity.addComponent(MeshRenderer);
groundRenderer.mesh = PrimitiveMesh.createCuboid(engine, cubeSize.x, cubeSize.y, cubeSize.z);
groundRenderer.setMaterial(groundMtl);
// Physics for the ground cube
const groundCollider = groundEntity.addComponent(StaticCollider);
const groundShape = new BoxColliderShape();
groundShape.size = cubeSize;
groundCollider.addShape(groundShape);
groundCollider.collisionLayer = Layer.Layer3;
// 创建可以碰撞的红色球体
const sphere1 = createPhysicsSphere(
rootEntity,
"RedSphere",
new Vector3(-2, 5, 0),
0.5,
new Vector3(1, 0, 0),
Layer.Layer1
);
// 创建可以穿透的蓝色球体
const sphere2 = createPhysicsSphere(
rootEntity,
"BlueSphere",
new Vector3(2, 5, 0),
0.5,
new Vector3(0, 0, 1),
Layer.Layer2
);
scene.physics.setColliderLayerCollision(Layer.Layer2, Layer.Layer3, false);
updateForE2E(engine, 110);
initScreenshot(engine, camera);
});

View File

@@ -214,6 +214,16 @@ export const E2E_CONFIG = {
category: "Physics",
caseFileName: "physx-collision",
threshold: 0.1
},
"LitePhysics Collision Group": {
category: "Physics",
caseFileName: "litePhysics-collision-group",
threshold: 0.1
},
"PhysXPhysics Collision Group": {
category: "Physics",
caseFileName: "physx-collision-group",
threshold: 0.1
}
},
Particle: {

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98e836afe6c51a5b510e7d7d462a79de8a7e7c720b81cdac10ffae4bf3e0bdca
size 37799

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fbd1f39578c6f1f92eaf6910ac91a05f76015e5c22742bf7690a885cd82bfc7e
size 40667

View File

@@ -81,13 +81,12 @@ export class CharacterController extends Collider {
constructor(entity: Entity) {
super(entity);
(<ICharacterController>this._nativeCollider) = PhysicsScene._nativePhysics.createCharacterController();
this._setUpDirection = this._setUpDirection.bind(this);
//@ts-ignore
this._upDirection._onValueChanged = this._setUpDirection;
// sync world position to physical space
this._onUpdate();
// Sync world position to physical space
(<ICharacterController>this._nativeCollider).setWorldPosition(this.entity.transform.worldPosition);
}
/**

View File

@@ -1,12 +1,14 @@
import { ICollider, IStaticCollider } from "@galacean/engine-design";
import { BoolUpdateFlag } from "../BoolUpdateFlag";
import { deepClone, ignoreClone } from "../clone/CloneManager";
import { ICustomClone } from "../clone/ComponentCloner";
import { Component } from "../Component";
import { DependentMode, dependentComponents } from "../ComponentsDependencies";
import { Entity } from "../Entity";
import { Layer } from "../Layer";
import { Transform } from "../Transform";
import { deepClone, ignoreClone } from "../clone/CloneManager";
import { ColliderShape } from "./shape/ColliderShape";
import { ICustomClone } from "../clone/ComponentCloner";
import { ColliderShapeChangeFlag } from "./enums/ColliderShapeChangeFlag";
/**
* Base class for all colliders.
@@ -24,6 +26,7 @@ export class Collider extends Component implements ICustomClone {
protected _updateFlag: BoolUpdateFlag;
@deepClone
protected _shapes: ColliderShape[] = [];
protected _collisionLayerIndex: number = 0;
/**
* The shapes of this collider.
@@ -32,6 +35,26 @@ export class Collider extends Component implements ICustomClone {
return this._shapes;
}
/**
* The collision layer of this collider, only support single layer.
*
* @defaultValue `Layer.Layer0`
*/
get collisionLayer(): Layer {
return (1 << this._collisionLayerIndex) as Layer;
}
set collisionLayer(value: Layer) {
// Check if value is a single layer (power of 2)
const index = Math.log2(value);
if (!Number.isInteger(index)) {
throw new Error("Collision layer must be a single layer (Layer.Layer0 to Layer.Layer31)");
}
this._collisionLayerIndex = index;
this._nativeCollider.setCollisionLayer(index);
}
/**
* @internal
*/
@@ -52,7 +75,7 @@ export class Collider extends Component implements ICustomClone {
}
this._shapes.push(shape);
this._addNativeShape(shape);
this._handleShapesChanged();
this._handleShapesChanged(ColliderShapeChangeFlag.Count);
}
}
@@ -65,7 +88,7 @@ export class Collider extends Component implements ICustomClone {
if (index !== -1) {
this._shapes.splice(index, 1);
this._removeNativeShape(shape);
this._handleShapesChanged();
this._handleShapesChanged(ColliderShapeChangeFlag.Count);
}
}
@@ -78,7 +101,7 @@ export class Collider extends Component implements ICustomClone {
this._removeNativeShape(shapes[i]);
}
shapes.length = 0;
this._handleShapesChanged();
this._handleShapesChanged(ColliderShapeChangeFlag.Count);
}
/**
@@ -129,12 +152,17 @@ export class Collider extends Component implements ICustomClone {
/**
* @internal
*/
_handleShapesChanged(): void {}
_handleShapesChanged(changeType: ColliderShapeChangeFlag): void {
if (changeType & ColliderShapeChangeFlag.Count) {
this._setCollisionLayer();
}
}
protected _syncNative(): void {
for (let i = 0, n = this.shapes.length; i < n; i++) {
this._addNativeShape(this.shapes[i]);
}
this._setCollisionLayer();
}
/**
@@ -161,4 +189,8 @@ export class Collider extends Component implements ICustomClone {
shape._collider = null;
this._nativeCollider.removeShape(shape._nativeShape);
}
private _setCollisionLayer(): void {
this._nativeCollider.setCollisionLayer(this._collisionLayerIndex);
}
}

View File

@@ -3,6 +3,7 @@ import { Quaternion, Vector3 } from "@galacean/engine-math";
import { ignoreClone } from "../clone/CloneManager";
import { Entity } from "../Entity";
import { Collider } from "./Collider";
import { ColliderShapeChangeFlag } from "./enums/ColliderShapeChangeFlag";
import { PhysicsScene } from "./PhysicsScene";
/**
@@ -420,7 +421,9 @@ export class DynamicCollider extends Collider {
/**
* @internal
*/
override _handleShapesChanged(): void {
override _handleShapesChanged(changeType: ColliderShapeChangeFlag): void {
super._handleShapesChanged(changeType);
if (this._automaticCenterOfMass || this._automaticInertiaTensor) {
this._setMassAndUpdateInertia();
}

View File

@@ -231,6 +231,38 @@ export class PhysicsScene {
}
}
/**
* Get whether two colliders can collide with each other.
* @param layer1 - The first collision layer
* @param layer2 - The second collision layer
* @returns Whether the colliders should collide
*/
getColliderLayerCollision(layer1: Layer, layer2: Layer): boolean {
const index1 = Math.log2(layer1);
const index2 = Math.log2(layer2);
if (!Number.isInteger(index1) || !Number.isInteger(index1)) {
throw new Error("Collision layer must be a single layer (Layer.Layer0 to Layer.Layer31)");
}
return PhysicsScene._nativePhysics.getColliderLayerCollision(index1, index2);
}
/**
* Set whether two colliders can collide with each other.
* @param layer1 - The first collision layer
* @param layer2 - The second collision layer
* @param isCollide - Whether the colliders should collide
*/
setColliderLayerCollision(layer1: Layer, layer2: Layer, isCollide: boolean): void {
const index1 = Math.log2(layer1);
const index2 = Math.log2(layer2);
if (!Number.isInteger(index1) || !Number.isInteger(index1)) {
throw new Error("Collision layer must be a single layer (Layer.Layer0 to Layer.Layer31)");
}
PhysicsScene._nativePhysics.setColliderLayerCollision(index1, index2, isCollide);
}
/**
* Casts a ray through the Scene and returns the first hit.
* @param ray - The ray

View File

@@ -0,0 +1,8 @@
/**
* @internal
*/
export enum ColliderShapeChangeFlag {
Property = 0x1,
Count = 0x2,
Both = Property | Count
}

View File

@@ -5,6 +5,7 @@ import { Collider } from "../Collider";
import { deepClone, ignoreClone } from "../../clone/CloneManager";
import { ICustomClone } from "../../clone/ComponentCloner";
import { Engine } from "../../Engine";
import { ColliderShapeChangeFlag } from "../enums/ColliderShapeChangeFlag";
/**
* Abstract class for collider shapes.
@@ -52,6 +53,7 @@ export abstract class ColliderShape implements ICustomClone {
/**
* Contact offset for this shape, the value must be greater than or equal to 0.
* @defaultValue 0.02
*/
get contactOffset(): number {
return this._contactOffset;
@@ -183,18 +185,18 @@ export abstract class ColliderShape implements ICustomClone {
this._nativeShape.setIsTrigger(this._isTrigger);
this._nativeShape.setMaterial(this._material._nativeMaterial);
this._collider?._handleShapesChanged();
this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property);
}
@ignoreClone
private _setPosition(): void {
this._nativeShape.setPosition(this._position);
this._collider?._handleShapesChanged();
this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property);
}
@ignoreClone
private _setRotation(): void {
this._nativeShape.setRotation(this._rotation);
this._collider?._handleShapesChanged();
this._collider?._handleShapesChanged(ColliderShapeChangeFlag.Property);
}
}

View File

@@ -16,6 +16,12 @@ export interface ICollider {
*/
removeShape(shape: IColliderShape): void;
/**
* Set the collision group of the collider.
* @param layer - The layer of the collider which the collider belongs to
*/
setCollisionLayer(layer: number): void;
/**
* Deletes the collider.
*/

View File

@@ -119,19 +119,35 @@ export interface IPhysics {
/**
* Create fixed joint.
* @param collider - Affector of joint
* @param collider - collider of joint
*/
createFixedJoint(collider: ICollider): IFixedJoint;
/**
* Create hinge joint.
* @param collider - Affector of joint
* @param collider - collider of joint
*/
createHingeJoint(collider: ICollider): IHingeJoint;
/**
* Create spring joint
* @param collider - Affector of joint
* @param collider - collider of joint
*/
createSpringJoint(collider: ICollider): ISpringJoint;
/**
* Get whether two collision layers can collide with each other.
* @param layer1 - The first collision layer
* @param layer2 - The second collision layer
* @returns Whether the layers should collide
*/
getColliderLayerCollision(layer1: number, layer2: number): boolean;
/**
* Set whether two collision layers can collide with each other.
* @param layer1 - The first collision layer
* @param layer2 - The second collision layer
* @param isCollide - Whether the layers should collide
*/
setColliderLayerCollision(layer1: number, layer2: number, isCollide: boolean): void;
}

View File

@@ -1,9 +1,10 @@
import { ICollider } from "@galacean/engine-design";
import { Quaternion, Ray, Vector3 } from "@galacean/engine";
import { Layer, Quaternion, Ray, Vector3 } from "@galacean/engine";
import { LiteHitResult } from "./LiteHitResult";
import { LiteColliderShape } from "./shape/LiteColliderShape";
import { LiteTransform } from "./LiteTransform";
import { LitePhysicsScene } from "./LitePhysicsScene";
import { LitePhysics } from "./LitePhysics";
/**
* Abstract class of physical collider.
@@ -11,6 +12,7 @@ import { LitePhysicsScene } from "./LitePhysicsScene";
export abstract class LiteCollider implements ICollider {
/** @internal */
abstract readonly _isStaticCollider: boolean;
private _litePhysics: LitePhysics;
/** @internal */
_scene: LitePhysicsScene;
@@ -18,9 +20,12 @@ export abstract class LiteCollider implements ICollider {
_shapes: LiteColliderShape[] = [];
/** @internal */
_transform: LiteTransform = new LiteTransform();
/** @internal */
_collisionLayer: number;
protected constructor() {
protected constructor(litePhysics: LitePhysics) {
this._transform.owner = this;
this._litePhysics = litePhysics;
}
/**
@@ -67,6 +72,13 @@ export abstract class LiteCollider implements ICollider {
outRotation.set(rotationQuaternion.x, rotationQuaternion.y, rotationQuaternion.z, rotationQuaternion.w);
}
/**
* {@inheritDoc ICollider.setCollisionLayer }
*/
setCollisionLayer(collisionLayer: Layer): void {
this._litePhysics.setColliderLayer(this, collisionLayer);
}
/**
* {@inheritDoc ICollider.destroy }
*/

View File

@@ -1,6 +1,7 @@
import { LiteCollider } from "./LiteCollider";
import { IDynamicCollider } from "@galacean/engine-design";
import { Logger, Quaternion, Vector3 } from "@galacean/engine";
import { LitePhysics } from "./LitePhysics";
/**
* A dynamic collider can act with self-defined movement or physical force
@@ -13,8 +14,8 @@ export class LiteDynamicCollider extends LiteCollider implements IDynamicCollide
* @param position - The global position
* @param rotation - The global rotation
*/
constructor(position: Vector3, rotation: Quaternion) {
super();
constructor(litePhysics: LitePhysics, position: Vector3, rotation: Quaternion) {
super(litePhysics);
this._transform.setPosition(position.x, position.y, position.z);
this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w);
}

View File

@@ -1,8 +1,9 @@
import { Quaternion, Vector3 } from "@galacean/engine";
import { Quaternion, Vector3, Layer } from "@galacean/engine";
import {
IBoxColliderShape,
ICapsuleColliderShape,
ICharacterController,
ICollider,
ICollision,
IDynamicCollider,
IFixedJoint,
@@ -25,6 +26,8 @@ import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape";
import { LitePhysicsManager } from "./LitePhysicsManager";
export class LitePhysics implements IPhysics {
private _layerCollisionMatrix: boolean[] = [];
/**
* {@inheritDoc IPhysics.initialize }
*/
@@ -52,6 +55,7 @@ export class LitePhysics implements IPhysics {
onTriggerPersist?: (obj1: number, obj2: number) => void
): LitePhysicsScene {
return new LitePhysicsScene(
this,
onContactBegin,
onContactEnd,
onContactPersist,
@@ -65,14 +69,14 @@ export class LitePhysics implements IPhysics {
* {@inheritDoc IPhysics.createStaticCollider }
*/
createStaticCollider(position: Vector3, rotation: Quaternion): IStaticCollider {
return new LiteStaticCollider(position, rotation);
return new LiteStaticCollider(this, position, rotation);
}
/**
* {@inheritDoc IPhysics.createDynamicCollider }
*/
createDynamicCollider(position: Vector3, rotation: Quaternion): IDynamicCollider {
return new LiteDynamicCollider(position, rotation);
return new LiteDynamicCollider(this, position, rotation);
}
/**
@@ -148,4 +152,46 @@ export class LitePhysics implements IPhysics {
createSpringJoint(collider: LiteCollider): ISpringJoint {
throw "Physics-lite don't support CapsuleColliderShape. Use Physics-PhysX instead!";
}
/**
* {@inheritDoc IPhysics.setColliderLayer }
*/
setColliderLayer(collider: LiteCollider, layer: Layer): void {
collider._collisionLayer = layer;
}
/**
* {@inheritDoc IPhysics.getColliderLayerCollision }
*/
getColliderLayerCollision(layer1: number, layer2: number): boolean {
const index = this._getColliderLayerIndex(layer1, layer2);
if (index > -1) {
return this._layerCollisionMatrix[index] ?? true;
}
// If either layer is Layer.Nothing, they cant collide
return false;
}
/**
* {@inheritDoc IPhysics.setColliderLayerCollision }
*/
setColliderLayerCollision(layer1: number, layer2: number, collide: boolean): void {
const index = this._getColliderLayerIndex(layer1, layer2);
if (index > -1) {
this._layerCollisionMatrix[index] = collide;
}
}
private _getColliderLayerIndex(layer1: number, layer2: number): number {
if (layer1 === 32 || layer2 === 32) {
return -1;
}
const min = Math.min(layer1, layer2);
const max = Math.max(layer1, layer2);
// Calculate a unique index for the layer pair using the triangular number formula
// This ensures that each layer combination maps to a unique index in the collision matrix
return (max * (max + 1)) / 2 + min;
}
}

View File

@@ -7,6 +7,7 @@ import { LiteStaticCollider } from "./LiteStaticCollider";
import { LiteBoxColliderShape } from "./shape/LiteBoxColliderShape";
import { LiteColliderShape } from "./shape/LiteColliderShape";
import { LiteSphereColliderShape } from "./shape/LiteSphereColliderShape";
import { LitePhysics } from "./LitePhysics";
/**
* A manager is a collection of colliders and constraints which can interact.
@@ -32,8 +33,10 @@ export class LitePhysicsScene implements IPhysicsScene {
private _currentEvents: DisorderedArray<TriggerEvent> = new DisorderedArray<TriggerEvent>();
private _eventMap: Record<number, Record<number, TriggerEvent>> = {};
private _eventPool: TriggerEvent[] = [];
private _physics: LitePhysics;
constructor(
physics: LitePhysics,
onContactEnter?: (collision: ICollision) => void,
onContactExit?: (collision: ICollision) => void,
onContactStay?: (collision: ICollision) => void,
@@ -41,6 +44,7 @@ export class LitePhysicsScene implements IPhysicsScene {
onTriggerExit?: (obj1: number, obj2: number) => void,
onTriggerStay?: (obj1: number, obj2: number) => void
) {
this._physics = physics;
this._onContactEnter = onContactEnter;
this._onContactExit = onContactExit;
this._onContactStay = onContactStay;
@@ -216,7 +220,14 @@ export class LitePhysicsScene implements IPhysicsScene {
if (myShape instanceof LiteBoxColliderShape) {
LitePhysicsScene._updateWorldBox(myShape, this._box);
for (let j = 0, len = colliders.length; j < len; j++) {
const colliderShape = colliders[j]._shapes;
const collider = colliders[j];
const colliderShape = collider._shapes;
// Skip collision check if layers can't collide
if (!this._checkColliderCollide(collider, myCollider)) {
continue;
}
for (let k = 0, len = colliderShape.length; k < len; k++) {
const shape = colliderShape[k];
const index1 = shape._id;
@@ -243,7 +254,14 @@ export class LitePhysicsScene implements IPhysicsScene {
} else if (myShape instanceof LiteSphereColliderShape) {
LitePhysicsScene._upWorldSphere(myShape, this._sphere);
for (let j = 0, len = colliders.length; j < len; j++) {
const colliderShape = colliders[j]._shapes;
const collider = colliders[j];
const colliderShape = collider._shapes;
// Skip collision check if layers can't collide
if (!this._checkColliderCollide(collider, myCollider)) {
continue;
}
for (let k = 0, len = colliderShape.length; k < len; k++) {
const shape = colliderShape[k];
const index1 = shape._id;
@@ -345,6 +363,17 @@ export class LitePhysicsScene implements IPhysicsScene {
return isHit;
}
private _checkColliderCollide(collider1: LiteCollider, collider2: LiteCollider): boolean {
const group1 = collider1._collisionLayer;
const group2 = collider2._collisionLayer;
if (group1 === group2) {
return true;
}
return this._physics.getColliderLayerCollision(group1, group2);
}
}
/**

View File

@@ -1,6 +1,7 @@
import { IStaticCollider } from "@galacean/engine-design";
import { LiteCollider } from "./LiteCollider";
import { Quaternion, Vector3 } from "@galacean/engine";
import { LitePhysics } from "./LitePhysics";
/**
* A static collider component that will not move.
@@ -14,8 +15,8 @@ export class LiteStaticCollider extends LiteCollider implements IStaticCollider
* @param position - The global position
* @param rotation - The global rotation
*/
constructor(position: Vector3, rotation: Quaternion) {
super();
constructor(litePhysics: LitePhysics, position: Vector3, rotation: Quaternion) {
super(litePhysics);
this._transform.setPosition(position.x, position.y, position.z);
this._transform.setRotationQuaternion(rotation.x, rotation.y, rotation.z, rotation.w);
}

View File

@@ -88,6 +88,8 @@ export class PhysXCharacterController implements ICharacterController {
* {@inheritDoc ICharacterController.addShape }
*/
addShape(shape: PhysXColliderShape): void {
// Add shape should sync latest position and world scale to pxController
this._updateShapePosition(shape._position, shape._worldScale);
// When CharacterController is disabled, set shape property need check pxController whether exist because of this._pxManager is null and won't create pxController
this._pxManager && this._createPXController(this._pxManager, shape);
this._shape = shape;
@@ -106,6 +108,17 @@ export class PhysXCharacterController implements ICharacterController {
this._scene?._removeColliderShape(shape._id);
}
/**
* {@inheritDoc ICollider.setCollisionLayer }
*/
setCollisionLayer(layer: number): void {
const actor = this._pxController?.getActor();
if (actor) {
this._physXPhysics._physX.setGroup(actor, layer);
}
}
/**
* {@inheritDoc ICharacterController.destroy }
*/
@@ -136,6 +149,8 @@ export class PhysXCharacterController implements ICharacterController {
this._pxController = pxManager._getControllerManager().createController(desc);
this._pxController.setUUID(shape._id);
this._updateNativePosition();
}
/**
@@ -156,7 +171,7 @@ export class PhysXCharacterController implements ICharacterController {
this._updateNativePosition();
}
private _updateNativePosition() {
private _updateNativePosition(): void {
const worldPosition = this._worldPosition;
if (this._pxController && worldPosition) {
Vector3.add(worldPosition, this._shapeScaledPosition, PhysXCharacterController._tempVec);

View File

@@ -61,6 +61,13 @@ export abstract class PhysXCollider implements ICollider {
outRotation.set(transform.rotation.x, transform.rotation.y, transform.rotation.z, transform.rotation.w);
}
/**
* {@inheritDoc ICollider.setCollisionLayer }
*/
setCollisionLayer(layer: number): void {
this._physXPhysics._physX.setGroup(this._pxActor, layer);
}
/**
* {@inheritDoc ICollider.destroy }
*/

View File

@@ -1,8 +1,9 @@
import { Quaternion, Vector3, version } from "@galacean/engine";
import { Quaternion, Vector3, Layer } from "@galacean/engine";
import {
IBoxColliderShape,
ICapsuleColliderShape,
ICharacterController,
ICollider,
ICollision,
IDynamicCollider,
IFixedJoint,
@@ -92,7 +93,7 @@ export class PhysXPhysics implements IPhysics {
if (runtimeMode == PhysXRuntimeMode.JavaScript) {
script.src = `https://mdn.alipayobjects.com/rms/afts/file/A*PXxaQrGL0XsAAAAAAAAAAAAAARQnAQ/physx.release.downgrade.js`;
} else if (runtimeMode == PhysXRuntimeMode.WebAssembly) {
script.src = `https://mdn.alipayobjects.com/rms/afts/file/A*0Qq8Rob3_5oAAAAAAAAAAAAAARQnAQ/physx.release.js`;
script.src = `https://mdn.alipayobjects.com/rms/afts/file/A*H4ElTYwBxwgAAAAAAAAAAAAAARQnAQ/physx.release.js`;
}
});
@@ -147,7 +148,7 @@ export class PhysXPhysics implements IPhysics {
onTriggerEnd?: (obj1: number, obj2: number) => void,
onTriggerStay?: (obj1: number, obj2: number) => void
): IPhysicsScene {
const manager = new PhysXPhysicsScene(
const scene = new PhysXPhysicsScene(
this,
physicsManager,
onContactBegin,
@@ -157,7 +158,7 @@ export class PhysXPhysics implements IPhysics {
onTriggerEnd,
onTriggerStay
);
return manager;
return scene;
}
/**
@@ -248,6 +249,20 @@ export class PhysXPhysics implements IPhysics {
return new PhysXSpringJoint(this, collider);
}
/**
* {@inheritDoc IPhysics.getColliderLayerCollision }
*/
getColliderLayerCollision(layer1: number, layer2: number): boolean {
return this._physX.getGroupCollisionFlag(layer1, layer2);
}
/**
* {@inheritDoc IPhysics.setColliderLayerCollision }
*/
setColliderLayerCollision(layer1: number, layer2: number, isCollide: boolean): void {
this._physX.setGroupCollisionFlag(layer1, layer2, isCollide);
}
private _init(physX: any): void {
const version = physX.PX_PHYSICS_VERSION;
const defaultErrorCallback = new physX.PxDefaultErrorCallback();

View File

@@ -31,16 +31,10 @@ export abstract class PhysXColliderShape implements IColliderShape {
_controllers: DisorderedArray<PhysXCharacterController> = new DisorderedArray<PhysXCharacterController>();
/** @internal */
_contractOffset: number = 0.02;
protected _physXPhysics: PhysXPhysics;
protected _worldScale: Vector3 = new Vector3(1, 1, 1);
protected _position: Vector3 = new Vector3();
protected _rotation: Vector3 = new Vector3();
protected _axis: Quaternion = null;
protected _physXRotation: Quaternion = new Quaternion();
private _shapeFlags: ShapeFlag = ShapeFlag.SCENE_QUERY_SHAPE | ShapeFlag.SIMULATION_SHAPE;
/** @internal */
_worldScale: Vector3 = new Vector3(1, 1, 1);
/** @internal */
_position: Vector3 = new Vector3();
/** @internal */
_pxMaterial: any;
/** @internal */
@@ -50,6 +44,13 @@ export abstract class PhysXColliderShape implements IColliderShape {
/** @internal */
_id: number;
protected _physXPhysics: PhysXPhysics;
protected _rotation: Vector3 = new Vector3();
protected _axis: Quaternion = null;
protected _physXRotation: Quaternion = new Quaternion();
private _shapeFlags: ShapeFlag = ShapeFlag.SCENE_QUERY_SHAPE | ShapeFlag.SIMULATION_SHAPE;
constructor(physXPhysics: PhysXPhysics) {
this._physXPhysics = physXPhysics;
}

View File

@@ -9,7 +9,8 @@ import {
PlaneColliderShape,
DynamicCollider,
Script,
ControllerCollisionFlag
ControllerCollisionFlag,
Layer
} from "@galacean/engine-core";
import { WebGLEngine } from "@galacean/engine-rhi-webgl";
import { PhysXPhysics } from "@galacean/engine-physics-physx";
@@ -322,4 +323,124 @@ describe("CharacterController", function () {
roleEntity.isActive = false;
controller.destroy();
});
it("collision group", () => {
const controller = roleEntity.getComponent(CharacterController);
const obstacleEntity = rootEntity.createChild("obstacle");
obstacleEntity.transform.position = new Vector3(0, 0, 2);
const obstacleCollider = obstacleEntity.addComponent(StaticCollider);
const triggerShape = new BoxColliderShape();
triggerShape.size = new Vector3(1, 1, 1);
triggerShape.isTrigger = true;
obstacleCollider.addShape(triggerShape);
class TriggerDetectionScript extends Script {
triggerEntered = false;
onTriggerEnter() {
this.triggerEntered = true;
}
reset() {
this.triggerEntered = false;
}
}
const triggerScript = obstacleEntity.addComponent(TriggerDetectionScript);
roleEntity.layer = Layer.Layer1;
obstacleEntity.layer = Layer.Layer2;
controller.move(new Vector3(0, 0, 2), 0.0001, 0.1);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(triggerScript.triggerEntered).toBe(true);
roleEntity.transform.position = new Vector3(0, 0, 0);
triggerScript.reset();
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, false);
controller.move(new Vector3(0, 0, 2), 0.0001, 0.1);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(triggerScript.triggerEntered).toBe(false);
// 恢复默认设置
engine.sceneManager.activeScene.physics.setColliderLayerCollision(1, 2, true);
});
it("should handle manual collision group setting with trigger", () => {
const controller = roleEntity.getComponent(CharacterController);
const obstacleEntity = rootEntity.createChild("obstacle");
obstacleEntity.transform.position = new Vector3(0, 0, 2);
const obstacleCollider = obstacleEntity.addComponent(StaticCollider);
const triggerShape = new BoxColliderShape();
triggerShape.size = new Vector3(1, 1, 1);
triggerShape.isTrigger = true;
obstacleCollider.addShape(triggerShape);
class TriggerDetectionScript extends Script {
triggerEntered = false;
onTriggerEnter() {
this.triggerEntered = true;
}
reset() {
this.triggerEntered = false;
}
}
const triggerScript = obstacleEntity.addComponent(TriggerDetectionScript);
roleEntity.layer = Layer.Layer1;
obstacleEntity.layer = Layer.Layer2;
controller.move(new Vector3(0, 0, 2), 0.0001, 0.1);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(triggerScript.triggerEntered).toBe(true);
roleEntity.transform.position = new Vector3(0, 0, 0);
triggerScript.reset();
controller.collisionLayer = Layer.Layer10;
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, false);
controller.move(new Vector3(0, 0, 2), 0.0001, 0.1);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(triggerScript.triggerEntered).toBe(false);
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, true);
// 恢复默认设置
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true);
});
it("keep entity position when disabled", () => {
roleEntity.transform.position = new Vector3(0, 0, 3);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
const controller = roleEntity.getComponent(CharacterController);
controller.enabled = false;
controller.enabled = true;
// @ts-ignore
controller._syncWorldPositionFromPhysicalSpace();
expect(roleEntity.transform.position.z).eq(3);
});
});

View File

@@ -1,7 +1,9 @@
import {
BoxColliderShape,
Collision,
DynamicCollider,
Entity,
Layer,
PlaneColliderShape,
Script,
SphereColliderShape,
@@ -9,8 +11,9 @@ import {
} from "@galacean/engine-core";
import { Vector3 } from "@galacean/engine-math";
import { PhysXPhysics } from "@galacean/engine-physics-physx";
import { LitePhysics } from "@galacean/engine-physics-lite";
import { WebGLEngine } from "@galacean/engine-rhi-webgl";
import { vi, describe, beforeAll, beforeEach, expect, it } from "vitest";
import { vi, describe, beforeAll, beforeEach, expect, it, afterEach } from "vitest";
class CollisionScript extends Script {
onTriggerEnter = vi.fn(CollisionScript.prototype.onTriggerEnter);
@@ -38,6 +41,22 @@ class MoveScript extends Script {
}
}
class CollisionDetectionScript extends Script {
collisionDetected = false;
onTriggerEnter() {
this.collisionDetected = true;
}
onCollisionEnter(other: Collision) {
this.collisionDetected = true;
}
reset() {
this.collisionDetected = false;
}
}
describe("physics collider test", function () {
let engine: WebGLEngine;
let rootEntity: Entity;
@@ -362,3 +381,165 @@ describe("physics collider test", function () {
expect(collider.shapes.length).eq(0);
});
});
describe("Collider Layer Collision Tests", () => {
describe("LitePhysics Layer Collision", () => {
let engine: WebGLEngine;
let rootEntity: Entity;
let physics: LitePhysics;
beforeAll(async () => {
physics = new LitePhysics();
engine = await WebGLEngine.create({
canvas: document.createElement("canvas"),
physics
});
rootEntity = engine.sceneManager.activeScene.createRootEntity("root");
});
it("should respect collision group settings", () => {
const entity1 = rootEntity.createChild("entity1");
const entity2 = rootEntity.createChild("entity2");
entity1.transform.position = new Vector3(0, 0, 0);
entity2.transform.position = new Vector3(0, 0, 0);
const collider1 = entity1.addComponent(DynamicCollider);
const shape1 = new BoxColliderShape();
shape1.size = new Vector3(1, 1, 1);
collider1.addShape(shape1);
const collider2 = entity2.addComponent(DynamicCollider);
const shape2 = new BoxColliderShape();
shape2.size = new Vector3(1, 1, 1);
collider2.addShape(shape2);
entity1.layer = Layer.Layer1;
entity2.layer = Layer.Layer2;
const script = entity1.addComponent(CollisionDetectionScript);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(script.collisionDetected).toBe(true);
script.reset();
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, false);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(script.collisionDetected).toBe(false);
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true);
});
it("should handle manual collision group setting in LitePhysics", () => {
const entity1 = rootEntity.createChild("entity1");
const entity2 = rootEntity.createChild("entity2");
entity1.transform.position = new Vector3(0, 0, 0);
entity2.transform.position = new Vector3(0, 0, 0);
const collider1 = entity1.addComponent(DynamicCollider);
const shape1 = new BoxColliderShape();
shape1.size = new Vector3(1, 1, 1);
collider1.addShape(shape1);
const collider2 = entity2.addComponent(StaticCollider);
const shape2 = new BoxColliderShape();
shape2.size = new Vector3(1, 1, 1);
shape2.isTrigger = true;
collider2.addShape(shape2);
const script = entity2.addComponent(CollisionDetectionScript);
entity1.layer = Layer.Layer1;
entity2.layer = Layer.Layer2;
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(script.collisionDetected).toBe(true);
script.reset();
collider1.collisionLayer = Layer.Layer10;
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, false);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(script.collisionDetected).toBe(false);
// 恢复默认设置
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer10, Layer.Layer2, true);
});
afterEach(() => {
const entities = rootEntity.children;
for (let i = entities.length - 1; i >= 0; i--) {
entities[i].destroy();
}
});
});
describe("PhysXPhysics Layer Collision", () => {
let engine: WebGLEngine;
let rootEntity: Entity;
beforeAll(async () => {
engine = await WebGLEngine.create({
canvas: document.createElement("canvas"),
physics: new PhysXPhysics()
});
rootEntity = engine.sceneManager.activeScene.createRootEntity("root");
});
it("should respect collision group settings", () => {
const entity1 = rootEntity.createChild("entity1");
const entity2 = rootEntity.createChild("entity2");
entity1.transform.position = new Vector3(0, 0, 0);
entity2.transform.position = new Vector3(0, 0, 0);
const collider1 = entity1.addComponent(DynamicCollider);
const shape1 = new BoxColliderShape();
shape1.size = new Vector3(1, 1, 1);
collider1.addShape(shape1);
const collider2 = entity2.addComponent(StaticCollider);
const shape2 = new BoxColliderShape();
shape2.size = new Vector3(1, 1, 1);
collider2.addShape(shape2);
entity1.layer = Layer.Layer1;
entity2.layer = Layer.Layer2;
const script = entity1.addComponent(CollisionDetectionScript);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(script.collisionDetected).toBe(true);
script.reset();
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, false);
// @ts-ignore
engine.sceneManager.activeScene.physics._update(1);
expect(script.collisionDetected).toBe(false);
engine.sceneManager.activeScene.physics.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true);
});
afterEach(() => {
const entities = rootEntity.children;
for (let i = entities.length - 1; i >= 0; i--) {
entities[i].destroy();
}
});
});
});

View File

@@ -8,6 +8,7 @@ import {
Entity,
HitResult,
Layer,
PhysicsScene,
Script,
SphereColliderShape,
StaticCollider
@@ -114,13 +115,16 @@ function setColliderProps(entity: Entity, isDynamic: boolean, isTrigger: boolean
describe("Physics Test", () => {
describe("LitePhysics", () => {
let engineLite: WebGLEngine;
let physics: LitePhysics;
let physicsScene: PhysicsScene;
beforeAll(async () => {
physics = new LitePhysics();
// Init engine with LitePhysics.
engineLite = await WebGLEngine.create({
canvas: document.createElement("canvas"),
physics: new LitePhysics()
physics
});
physicsScene = engineLite.sceneManager.activeScene.physics;
const rootEntityLitePhysics = engineLite.sceneManager.activeScene.createRootEntity("root_camera");
@@ -342,6 +346,34 @@ describe("Physics Test", () => {
expect(collisionTestScript.onTriggerExit).toHaveBeenCalledTimes(1);
});
describe("Collision Group Tests", () => {
it("should set and get collision group settings correctly", () => {
physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer1, true);
expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer1)).to.eq(true);
physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer2, false);
expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer2)).to.eq(false);
physicsScene.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true);
expect(physicsScene.getColliderLayerCollision(Layer.Layer1, Layer.Layer2)).to.eq(true);
});
it("should handle edge cases in collision group matrix", () => {
const maxGroup = Layer.Layer31;
physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, false);
expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(false);
physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, true);
expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(true);
});
it("should handle invalid collision groups correctly", () => {
const invalidGroup = -1;
// @ts-ignore
expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, false)).to.throw();
// @ts-ignore
expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, true)).to.throw();
});
});
afterEach(() => {
const root = engineLite.sceneManager.activeScene.findEntityByName("root");
root?.destroy();
@@ -350,6 +382,7 @@ describe("Physics Test", () => {
describe("PhysXPhysics", () => {
let enginePhysX: WebGLEngine;
let physicsScene: PhysicsScene;
beforeAll(async () => {
// Init engine with PhysXPhysics.
@@ -357,6 +390,8 @@ describe("Physics Test", () => {
canvas: document.createElement("canvas"),
physics: new PhysXPhysics()
});
physicsScene = enginePhysX.sceneManager.activeScene.physics;
const rootEntityPhysX = enginePhysX.sceneManager.activeScene.createRootEntity("root_camera");
const cameraEntityPhysX = rootEntityPhysX.createChild("camera");
@@ -498,6 +533,34 @@ describe("Physics Test", () => {
root.destroy();
});
describe("Collision Group Tests", () => {
it("should set and get collision group settings correctly", () => {
physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer1, true);
expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer1)).to.eq(true);
physicsScene.setColliderLayerCollision(Layer.Layer0, Layer.Layer2, false);
expect(physicsScene.getColliderLayerCollision(Layer.Layer0, Layer.Layer2)).to.eq(false);
physicsScene.setColliderLayerCollision(Layer.Layer1, Layer.Layer2, true);
expect(physicsScene.getColliderLayerCollision(Layer.Layer1, Layer.Layer2)).to.eq(true);
});
it("should handle edge cases in collision group matrix", () => {
const maxGroup = Layer.Layer31;
physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, false);
expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(false);
physicsScene.setColliderLayerCollision(maxGroup, Layer.Layer0, true);
expect(physicsScene.getColliderLayerCollision(maxGroup, Layer.Layer0)).to.eq(true);
});
it("should handle invalid collision groups correctly", () => {
const invalidGroup = -1;
// @ts-ignore
expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, false)).to.throw();
// @ts-ignore
expect(() => physicsScene.setColliderLayerCollision(invalidGroup, Layer.Layer0, true)).to.throw();
});
});
describe("Collision Test", () => {
it("Dynamic Trigger vs Dynamic Trigger", () => {
const physicsMgr = enginePhysX.physicsManager;

View File

@@ -1,6 +1,9 @@
import { defineProject } from "vitest/config";
export default defineProject({
server: {
port: 51204
},
optimizeDeps: {
exclude: [
"@galacean/engine",