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

标签: Cocos Creator 3.8