cocos MouseJoint2D组件的点击穿透问题
cocos 的 MouseJoint2D组件是引擎自带的可以很方便的实现对物理刚体的拖拽、抛摔等物理交互操作。
但是最近在做《指尖跳动+》时发现,当多个刚体在z轴上重叠时,经常会点击穿透,就是我想拖动上层刚体时,它拖动的是下层的刚体,并且表现不稳定(有时上层有反应,有时没有)。
查看源码发现:
engine/cocos/physics-2d/box2d-wasm/joints/mouse-joint.ts 文件中是这样的:
// ......
private _enableTouch (v: boolean): void {
const canvas = find('Canvas');
if (canvas) {
const cb = v ? canvas.on : canvas.off;
cb.call(canvas, NodeEventType.TOUCH_START, this.onTouchBegan, this);
cb.call(canvas, NodeEventType.TOUCH_MOVE, this.onTouchMove, this);
cb.call(canvas, NodeEventType.TOUCH_END, this.onTouchEnd, this);
cb.call(canvas, NodeEventType.TOUCH_CANCEL, this.onTouchEnd, this);
}
}
onTouchBegan (event: Touch): void {
this._isTouched = true;
const target = this._touchPoint.set(event.getUILocation());
const world = (PhysicsSystem2D.instance.physicsWorld as B2PhysicsWorld);
const colliders = world.testPoint(target);
if (colliders.length <= 0) return;
const body = colliders[0].body;
body!.wakeUp();
const comp = this._jointComp as MouseJoint2D;
comp.connectedBody = body;
this._init();
this.setMaxForce(comp.maxForce * body!.getMass());
this.setTarget(target);
}
// ......
onTouchBegan 中通过world.testPoint(target)位置检测 检测到覆盖这个点的所有碰撞体,然后取colliders[0]。
问题就在colliders[0],testPoint得到的碰撞体数组并不是按z轴层级排序的,所以colliders[0]并不一定是最顶层碰撞体。
找到问题就好修改了,上面文件中的类 B2MouseJoint,在 engine/cocos/physics-2d/framework/components/joints/joint-2d.ts 中通过工厂类 IPhysicsSelector 实例化为属性 _joint:
protected override onLoad (): void {
if (!EDITOR_NOT_IN_PREVIEW) {
this._joint = createJoint(this.TYPE);
this._joint.initialize(this);
this._body = this.getComponent(RigidBody2D);
}
}
然后我们要用的组件MouseJoint2D 又继承自这个Joint2D,所以我们只需要自定义一个组件 MouseJoint 继承MouseJoint2D,然后重写_joint的onTouchBegan回调即可:
@ccclass('MouseJoint')
@menu('Custom Script/Common/MouseJoint')
export class MouseJoint extends MouseJoint2D {
onLoad() {
super.onLoad();
this._joint.onTouchBegan = this.onTouchBegan.bind(this._joint);
}
onTouchBegan (event: Touch): void {console.log('onTouchBegan')
this._isTouched = true;
const target = this._touchPoint.set(event.getUILocation());
const world = (PhysicsSystem2D.instance.physicsWorld as B2PhysicsWorld);
const colliders = world.testPoint(target);
if (colliders.length <= 0) return;
//这里遍历所有testPoint检测到的节点并递归往父级追溯其真实在节点树中的层级,并排序
const sortedColliders = [...colliders].sort((a, b) => {
// 生成节点路径标识符
const getNodePath = (node: Node) => {
let path = '', current = node;
while (current.parent) {//追溯父级的层级
path = `${current.getSiblingIndex()}:${path}`;//拼接成层级链
current = current.parent;
}
return path;
};
// 通过字符串比较层级链 实现排序
return getNodePath(b.node).localeCompare(getNodePath(a.node));
});
const body = sortedColliders[0].body;
body!.wakeUp();
const comp = this._jointComp as MouseJoint2D;
comp.connectedBody = body;
this._init();
this.setMaxForce(comp.maxForce * body!.getMass());
this.setTarget(target);
}
}
另外,结合 mouse-joint.ts 中的 _enableTouch 里的全局监听,这也解释了在一个刚体上加了组件后,其他刚体也可以拖动了的原因。
所以MouseJoint2D组件,只需要添加一个就可以了,如果你想让它和其他组件一样只在一个节点上生效,可以重写_enableTouch 和 onTouchBegan :
@ccclass('MouseJoint')
@menu('Custom Script/Common/MouseJoint')
export class MouseJoint extends MouseJoint2D {
@property({ tooltip: "仅本节点有效" })
onlySelf: boolean = true; //增加是否仅本节点有效标识
onLoad() {
console.log('onLoad')
super.onLoad();
this._joint.onTouchBegan = this.onTouchBegan.bind(this._joint);
this._joint._enableTouch = this._enableTouch.bind(this._joint);
}
private _enableTouch(v: boolean): void {
console.log('_enableTouch')
const comp = this._jointComp as MouseJoint2D;
const canvas = comp.onlySelf?comp.node:find('Canvas');
if (canvas) {
const cb = v ? canvas.on : canvas.off;
cb.call(canvas, NodeEventType.TOUCH_START, this.onTouchBegan, this);
cb.call(canvas, NodeEventType.TOUCH_MOVE, this.onTouchMove, this);
cb.call(canvas, NodeEventType.TOUCH_END, this.onTouchEnd, this);
cb.call(canvas, NodeEventType.TOUCH_CANCEL, this.onTouchEnd, this);
}
}
onTouchBegan(event: Touch): void {
console.log('onTouchBegan')
this._isTouched = true;
const comp = this._jointComp as MouseJoint2D;
const target = this._touchPoint.set(event.getUILocation());
let body = comp.node.getComponent(RigidBody2D);
if (!comp.onlySelf) {
const world = (PhysicsSystem2D.instance.physicsWorld as B2PhysicsWorld);
const colliders = world.testPoint(target);
if (colliders.length <= 0) return;
const sortedColliders = [...colliders].sort((a, b) => {
// 生成节点路径标识符
const getNodePath = (node: Node) => {
let path = '', current = node;
while (current.parent) {
path = `${current.getSiblingIndex()}:${path}`;
current = current.parent;
}
return path;
};
// 通过字符串比较路径
return getNodePath(b.node).localeCompare(getNodePath(a.node));
});
body = sortedColliders[0].body;
}
body!.wakeUp();
comp.connectedBody = body;
this._init();
this.setMaxForce(comp.maxForce * body!.getMass());
this.setTarget(target);
}
}
增加一个编辑器属性 onlySelf 来控制组件是否仅对本节点有效。
本文涉及相关源码文件:
engine/cocos/physics-2d/framework/components/joints/ 下的:
joint-2d.ts
mouse-joint-2d.ts
engine/cocos/physics-2d/box2d-wasm/joints/ 下的:
joint-2d.ts
mouse-joint.ts
engine/cocos/physics-2d/framework/ 下的:
physics-selector.ts