分享一个zIndex的实现

由于从v3.0开始zIndex接口因为性能问题已经被移除,调整节点树的顺序需要使用setSiblingIndex替换。但是在2d游戏中zIndex的使用还是有一定的需求的,可以少用,但是不能完全没有。以下是个人的扩展zIndex属性的方案

import { isValid, Node } from 'cc';

declare module 'cc' {
    interface Node {
        /** 节点的渲染顺序 */
        zIndex: number,
    }
}

Object.defineProperty(Node.prototype, 'zIndex', {
    set(zIndex: number) {
        if (this.zIndex === zIndex || !isValid(this)) {
            return
        }
        this._zIndex = zIndex;
        let self = this as Node;
        if (self.parent) {
            const children = self.parent.children;
            let siblingIndex = binarySearch(children, zIndex);
            self.setSiblingIndex(siblingIndex);
        }
    },
    get(): number {
        return this._zIndex || 0;
    },
    configurable: true,
});

function binarySearch(children: Node[], zIndex: number): number {
    let left = 0;
    let right = children.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (children[mid].zIndex < zIndex) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left;
}

如果有更好的方案大家也可以提一下

感觉不太合适,如果将子节点设置后再addChild其他节点,你这里顺序就是乱的。

对,这个方案,如果已经设置了zIndex,后又加了其他节点的话,其他节点在加入时要手动设置一下zIndex
如果不想手动设置的话,还需要改下setParent,在setParent后调用设置zIndex = 0,然后上面的二分查找的.zIndex < zIndex改成<=
我这边默认用的时候子节点都会设置一遍zIndex了,所以没动setParent

zIndex: {

        get(this: Node) {

            return this.__zIndex == null ? this.getSiblingIndex() : this.__zIndex;

        },

        set(this: Node, val: number) {

            if (val == this.zIndex) return;

            this.__zIndex = val;

            resetSiblingIndexByZindex(this.parent);

        },

    }

这个resetSiblingIndexByZindex具体是什么,遍历子节点全排吗,如果子节点很多比如场景装饰和人物,人物移动改变zIndex的时候,每个都排的开销会不会很大

改了一版后续添加子节点不会受影响的

function setZIndex(node: Node, zIndex: number, force: boolean = false) {
    if ((node.zIndex === zIndex && !force) || !isValid(node)) {
        return
    }
    if (node.parent) {
        node.parent['_updateZIndex'] = true
        const children = node.parent.children;
        let siblingIndex = binarySearch(children, zIndex);
        node.setSiblingIndex(siblingIndex);
    }
    node['_zIndex'] = zIndex;
}

// 自定义zIndex
Object.defineProperty(Node.prototype, 'zIndex', {
    set(zIndex: number) {
        setZIndex(this, zIndex)
    },
    get(): number {
        return this._zIndex || 0;
    },
    configurable: true,
});

function binarySearch(children: Node[], zIndex: number): number {
    let left = 0;
    let right = children.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (children[mid].zIndex <= zIndex) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return left;
}

const oldSetParent = Node.prototype.setParent;
Node.prototype.setParent = function (value: Node | null, keepWorldTransform?: boolean) {
    oldSetParent.call(this, value, keepWorldTransform)
    if (value && value['_updateZIndex']) {
        setZIndex(this, this.zIndex, true)
    }
}
1赞

这个接口移除想骂人,,

是的,开销还好吧,没啥影响,从3.8.3用到3.8.5好几个项目了
function resetSiblingIndexByZindex(node: Node) {

const children = node.children;

const n = children.length;

const indexArray = Array.from({ length: n }, (_, i) => i);

indexArray.sort((a, b) => children[a].zIndex - children[b].zIndex);

for (let i = 0; i < n; i++) {

    children[indexArray[i]].setSiblingIndex(i);

}

return indexArray.map(i => children[i]);

}

我试过200多个子节点全排序 很轻松

我这边的应用场景是模拟经营,就是就是为了实现墙/人/墙然后中间还有各种装饰的遮挡,为了人物移动的时候实时动态遮挡,节点都在同一层,全排。。有点多,所以上面我的方案是二分查找然后仅对修改了zIndex的进行修改
image

以前处理过层级排序问题,当时使用的是插入排序+分帧处理。因为我的游戏场景层级变化不大,相对有序的情况下插入排序性能要好些。

插入排序+分帧处理的话,那可以不用zIndex了,排完根据索引setSiblingIndex就够了吧
也是个好方案,有时间我测一下我的项目哪个方法效率高点

只有一个人动插入排序好,我的是2.5d地图,几十个移动npc,只能全排

zIndex 的核心还是在于,改变渲染排序的情况下,不影响节点顺序。已经列入计划了。

4赞

我的绝大部分需求都是调整渲染顺序,并不在乎节点顺序。

甚至是 需要渲染顺序直接影响节点顺序。
就比如我需要将某个节点置顶渲染显示设置zIndex=9999,如果这个节点的父节点有20个子节点,那么这个节点的顺序下标应该从之前的任意下标变成最大下标19。

这个需求用 node.sortChildren 会更好(另一个需要添加的函数,哈哈)。