美文网首页
Cocos Creator 源码解读:siblingIndex

Cocos Creator 源码解读:siblingIndex

作者: 文弱书生陈皮皮 | 来源:发表于2021-08-04 12:05 被阅读0次

    前言

    本文基于 Cocos Creator 2.4.5 撰写。

    🎉 普天同庆

    来了来了,《源码解读》系列文章终于又来了!

    👾 温馨提醒

    本文包含大段引擎源码,使用大屏设备阅读体验更佳!

    Hi There!

    节点(cc.Node)作为 Cocos Creator 引擎中最基本的单位,所有组件都需要依附在节点上。

    同时节点也是我们日常开发中接触最频繁的东西。

    我们经常会需要「改变节点的排序」来完成一些效果(如图像的遮挡)。

    A Question?

    😕 你有没有想过:

    节点的排序是如何实现的?

    Oops!

    🤯 我在分析了源码后发现:

    节点的排序并没有想象中那么简单!

    😹 渣皮语录

    听皮皮一句劝,zIndex 的水太深,你把握不住!


    正文

    节点顺序 (Node Order)

    🤔 如何修改节点的顺序?

    首先,在 Cocos Creator 编辑器中的「层级管理器」中,我们可以随意拖动节点来改变节点的顺序。

    拖动排序

    🤨 但是,在代码中我们要怎么做呢?

    我最先想到的是节点的 setSiblingIndex 函数,然后是节点的 zIndex 属性。

    我猜大多数人都不清楚这两个方案有什么区别。

    那么接下来就让我们深入源码,一探究竟!

    siblingIndex

    「siblingIndex」即「同级索引」,意为「同一父节点下的兄弟节点间的位置」。

    siblingIndex 越小的节点排越前,索引最小值为 0,也就是第一个节点的索引值。

    需要注意的是,实际上节点并没有 siblingIndex 属性,只有 getSiblingIndexsetSiblingIndex 这两个相关函数。

    注:本文统一使用 siblingIndex 来代指 getSiblingIndexsetSiblingIndex 函数。

    另外,getSiblingIndexsetSiblingIndex 函数是由 cc._BaseNode 实现的。

    💡 cc._BaseNode

    大家对这个类可能会比较陌生,简单来说 cc._BaseNodecc.Node 的基类。

    此类「定义了节点的基础属性和函数」,包括但不仅限于 setParentaddChildgetComponent 等常用函数...

    📝 源码节选:

    函数:cc._BaseNode.prototype.getSiblingIndex

    getSiblingIndex() {
      if (this._parent) {
        return this._parent._children.indexOf(this);
      } else {
        return 0;
      }
    },
    

    函数:cc._BaseNode.prototype.setSiblingIndex

    setSiblingIndex(index) {
      if (!this._parent) {
        return;
      }
      if (this._parent._objFlags & Deactivating) {
        return;
      }
      var siblings = this._parent._children;
      index = index !== -1 ? index : siblings.length - 1;
      var oldIndex = siblings.indexOf(this);
      if (index !== oldIndex) {
        siblings.splice(oldIndex, 1);
        if (index < siblings.length) {
          siblings.splice(index, 0, this);
        } else {
          siblings.push(this);
        }
        this._onSiblingIndexChanged && this._onSiblingIndexChanged(index);
      }
    },
    

    [源码] base-node.js#L514: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/utils/base-node.js#L514

    🕵️ 做了什么?

    扒拉源码后发现,siblingIndex 的本质其实很简单。

    那就是「当前节点在父节点的 _children 属性中的下标(位置)」。

    getSiblingIndex 函数返回的是「当前节点在父节点的 _children 属性中的下标(位置)」。

    setSiblingIndex 函数则是设置「当前节点在父节点的 _children 属性中的下标(位置)」。

    💡 cc._BaseNode.prototype._children

    节点的 _children 属性其实就是节点的 children 属性。

    children 属性是一个 getter,返回的是自身的 _children 属性。

    另外 children 属性没有实现 setter,所以你直接给 children 属性赋值是无效的。

    zIndex

    「zIndex」是「用来对节点进行排序的关键属性」,它决定了一个节点在兄弟节点之间的位置。

    zIndex 的值介于 cc.macro.MIN_ZINDEXcc.macro.MAX_ZINDEX 之间。

    另外,zIndex 属性是在 cc.Node 内使用 Cocos 定制版 gettersetter 实现的。

    📝 源码节选:

    属性: cc.Node.prototype.zIndex

    // 为了减少篇幅,已省略部分不相关代码
    zIndex: {
      get() {
        return this._localZOrder >> 16;
      },
      set(value) {
        if (value > macro.MAX_ZINDEX) {
          value = macro.MAX_ZINDEX;
        } else if (value < macro.MIN_ZINDEX) {
          value = macro.MIN_ZINDEX;
        }
        if (this.zIndex !== value) {
          this._localZOrder = (this._localZOrder & 0x0000ffff) | (value << 16);
          this.emit(EventType.SIBLING_ORDER_CHANGED);
          this._onSiblingIndexChanged();
        }
      }
    },
    

    [源码] CCNode.js#L1549: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/CCNode.js#L1549

    🕵️ 做了什么?

    扒拉源码后发现,zIndex 的本质其实也很简单。

    那就是「返回或设置节点的 _localZOrder 属性」。

    🧐 没那么简单!

    有趣的是,在 getter 中并没有直接返回 _localZOrder 属性,而是返回了 _localZOrder 属性右移(>>)16 位后的数值。

    setter 中设置 _localZOrder 属性时也并非简单的赋值,又是进行了一顿位操作:

    这里我们以二进制数的视角来分解该函数内的位操作。

    1. 通过 & 0x0000ffff 取出原 _localZOrder 的「低 16 位」;
    2. 将目标值 value「左移 16 位」;
    3. 将左移后的 value 作为「高 16 位」与原 _localZOrder 的「低 16 位」合并;
    4. 最后得到一个「32 位的二进制数」并赋予 _localZOrder

    😲 嗯?

    慢着!_localZOrder 又是干啥用的?咋这么绕!

    别急,答案在后面~

    排序 (Sorting)

    细心的朋友应该发现了,siblingIndex 和 zIndex 的源码中都没有包含实际的排序逻辑。

    但是它们都有一个共同点:「最后都调用了自身的 _onSiblingIndexChanged 函数」。

    _onSiblingIndexChanged

    📝 源码节选:

    函数:cc.Node.prototype._onSiblingIndexChanged

    _onSiblingIndexChanged() {
      if (this._parent) {
        this._parent._delaySort();
      }
    },
    

    🕵️ 做了什么?

    _onSiblingIndexChanged 函数内则是调用了「父节点」的 _delaySort 函数。

    _delaySort

    📝 源码节选:

    函数:cc.Node.prototype._delaySort

    _delaySort() {
      if (!this._reorderChildDirty) {
        this._reorderChildDirty = true;
        cc.director.__fastOn(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
      }
    },
    

    🕵️ 做了什么?

    一顿操作顺藤摸瓜后发现,真正进行排序的地方是「父节点」的 sortAllChildren 函数。

    💡 盲生,你发现了华点!

    值得注意的是,_delaySort 函数中的 sortAllChildren 函数调用不是立即触发的,而是会在下一次 update(生命周期)后触发。

    延迟触发的目的应该是为了避免在同一帧内的重复调用,从而减少不必要的性能损耗。

    sortAllChildren

    📝 源码节选:

    函数:cc.Node.prototype.sortAllChildren

    // 为了减少篇幅,已省略部分不相关代码
    sortAllChildren() {
      if (this._reorderChildDirty) {
        this._reorderChildDirty = false;
        // Part 1
        var _children = this._children, child;
        this._childArrivalOrder = 1;
        for (let i = 0, len = _children.length; i < len; i++) {
          child = _children[i];
          child._updateOrderOfArrival();
        }
        eventManager._setDirtyForNode(this);
        // Part 2
        if (_children.length > 1) {
          let child, child2;
          for (let i = 1, count = _children.length; i < count; i++) {
            child = _children[i];
            let j = i;
            for (;
              j > 0 && (child2 = _children[j - 1])._localZOrder > child._localZOrder;
              j--
            ) {
              _children[j] = child2;
            }
            _children[j] = child;
          }
          this.emit(EventType.CHILD_REORDER, this);
        }
        cc.director.__fastOff(cc.Director.EVENT_AFTER_UPDATE, this.sortAllChildren, this);
      }
    },
    

    [源码] CCNode.js#L3680: https://github.com/cocos-creator/engine/blob/2.4.5/cocos2d/core/CCNode.js#L3680

    上半部分 (Part 1)

    随着一步步深入,我们终于来到了关键部分。

    现在让我们琢磨琢磨这个 sortAllChildren 函数。

    进入该函数的前半段,映入眼帘的是一行赋值语句,将 _childArrivalOrder 属性设(重置)为 1

    紧跟其后的是一个 for 循环,遍历了当前节点的所有「子节点」,并一一执行「子节点」的 _updateOrderOfArrival 函数。

    🤨 嗯?这个 _updateOrderOfArrival 函数又是何方神圣?

    _updateOrderOfArrival

    📝 源码节选:

    函数:cc.Node.prototype._updateOrderOfArrival

    _updateOrderOfArrival() {
      var arrivalOrder = this._parent ? ++this._parent._childArrivalOrder : 0;
      this._localZOrder = (this._localZOrder & 0xffff0000) | arrivalOrder;
      this.emit(EventType.SIBLING_ORDER_CHANGED);
    },
    

    🕵️ 做了什么?

    显而易见的是,_updateOrderOfArrival 函数的作用就是「更新节点的 _localZOrder 属性」。

    🥱 该函数中同样也使用了位操作:

    同上,以二进制数的视角来进行分解这里的位操作。

    1. 将父节点的 _childArrivalOrder(前置)自增 1,并赋予 arrivalOrder(如无父节点则为 0);
    2. 通过 & 0xffff0000 取出当前节点的 _localZOrder 的「高 16 位」;
    3. arrivalOrder 作为「低 16 位」与当前节点的 _localZOrder 的「高 16 位」合并;
    4. 最后得到一个新的「32 位的二进制数」并赋予当前节点的 _localZOrder 属性。

    🤔 看到这里你是不是已经开始迷惑了?

    别担心,答案即将揭晓!

    下半部分 (Part 2)

    sortAllChildren 函数的下半部分就比较好理解了。

    基本就是通过「插入排序(Insertion Sort)」来「排序当前节点的 _children 属性(子节点数组)」。

    其中主要根据子节点的 _localZOrder 属性的值来进行排序,_localZOrder 属性值小的子节点排前面,反之排后面。

    排序的关键 (Key of sorting)

    🤔 分析完源码后发现,节点的排序并没有想象中那么简单。

    我们可以先得出几个结论:

    1. siblingIndex 是节点在父节点的 children 属性中的下标;
    2. zIndex 是一个独立的属性,和 siblingIndex 没有直接联系;
    3. siblingIndex 和 zIndex 的改变都会触发排序;
    4. siblingIndex 和 zIndex 共同组成了节点的 _localZOrder
    5. zIndex 的权重比 siblingIndex 大;
    6. 节点的 _localZOrder 直接决定了节点的最终顺序。

    siblingIndex 如何影响排序 (How siblingIndex affects sorting)

    我们前面有提到:

    • getSiblingIndex 函数「返回了当前节点在父节点的 _children 属性中的下标(位置)」。
    • setSiblingIndex 函数「设置了当前节点在父节点的 _children 属性中的下标(位置),并通知父节点进行排序」。

    随后在父节点的 sortAllChildren 函数中的上半部分,会以这个下标作为节点 _localZOrder 的低 16 位。

    🧐 所以我们可以这样理解:

    siblingIndex 是元素下标,在排序过程中,其决定了 _localZOrder 的「低 16 位」。

    zIndex 如何影响排序 (How zIndex affects sorting)

    我们前面有提到:

    • zIndexgetter「返回了 _localZOrder 的高 16 位」。
    • zIndexsetter「设置了 _localZOrder 的高 16 位,并通知父节点进行排序」。

    🧐 所以我们可以这样理解:

    zIndex 实际上只是一个躯壳,其本质是 _localZOrder 的「高 16 位」。

    _localZOrder 如何决定顺序 (How _localZOrder works)

    父节点的 sortAllChildren 函数中根据子节点的 _localZOrder 大小来进行最终排序。

    我们可以将 _localZOrder 看做一个「32 位二进制数」,其由 siblingIndex 和 zIndex 共同组成。

    但是,为什么说「zIndex 的权重比 siblingIndex 大」呢?

    因为 zIndex 决定了 _localZOrder 的「高 16 位」,而 siblingIndex 决定了 _localZOrder 的「低 16 位」。

    所以,只有在 zIndex 相等的情况下,siblingIndex 的大小才有决定性意义。

    而在 zIndex 不相等的情况下,siblingIndex 的大小就无所谓了。

    🌰 举个栗子

    这里有两个 32 位二进制数(伪代码):

    • A: 0000 0000 0000 0001 xxxx xxxx xxxx xxxx
    • B: 0000 0000 0000 0010 xxxx xxxx xxxx xxxx

    由于 B 的「高 16 位」(0000 0000 0000 0010)比 A 的「高 16 位」(0000 0000 0000 0001)大,所以无论他们的「低 16 位」中的 x 是什么,B 都会永远大于 A。

    实验一下 (Experiment)

    我们可以写个小组件来测试下 siblingIndex 和 zIndex 对于 _localZOrder 的影响。

    📝 一顿打码:

    const { ccclass, property, executeInEditMode } = cc._decorator;
    
    @ccclass
    @executeInEditMode
    export default class Test_NodeOrder extends cc.Component {
    
      @property({ displayName: 'siblingIndex' })
      get siblingIndex() {
        return this.node.getSiblingIndex();
      }
      set siblingIndex(value) {
        this.node.setSiblingIndex(value);
      }
    
      @property({ displayName: 'zIndex' })
      get zIndex() {
        return this.node.zIndex;
      }
      set zIndex(value) {
        this.node.zIndex = value;
      }
    
      @property({ displayName: '_localZOrder' })
      get localZOrder() {
        return this.node._localZOrder;
      }
    
      @property({ displayName: '_localZOrder (二进制)' })
      get localZOrderBinary() {
        return this.node._localZOrder.toString(2).padStart(32, 0);
      }
    
    }
    

    场景一 (Scene 1)

    在 1 个节点下放置了 1 个子节点。

    🖼 子节点的排序信息:

    zIndex 0

    一般来说,由于节点的 _childArrivalOrder 是从 1 开始的,并且在计算时会先自增 1

    所以子节点的 _localZOrder 的「低 16 位」总会比其 siblingIndex 大 2 个数。

    场景二 (Scene 2)

    在 1 个节点下放置了 1 个子节点,并将子节点的 zIndex 设为 1

    🖼 子节点的排序信息:

    zIndex 1

    可以看到,仅仅将节点的 zIndex 属性设为 1,其 _localZOrder 就高达 65538

    🔠 大概的计算过程如下(极为抽象的伪代码):

    1. zIndex = 1 = 0b0000000000000001
    2. siblingIndex = 0
    3. arrivalOrder = 1 + (siblingIndex + 1)
    4. arrivalOrder = 0b0000000000000010
    5. _localZOrder = (zIndex << 16) | arrivalOrder
    6. _localZOrder = 0b00000000000000010000000000000000 | 0b0000000000000010
    7. _localZOrder = 0b00000000000000010000000000000010 = 65538
    

    📝 继续简化后的伪代码:

    _localZOrder = (zIndex << 16) | (siblingIndex + 2)
    

    💡 By the way

    当一个节点没有父节点时,它的 arrivalOrder 永远是 0

    其实此时它是啥已经不重要了,毕竟没有父节点的节点本来就不可能会被排序。

    场景三 (Scene 3)

    在同 1 个节点下放置了 6 个子节点,将所有子节点的 zIndex 都设为 0

    🎥 各个子节点的排序信息:

    zIndex 0 & siblingIndex 0~5

    场景四 (Scene 4)

    在同 1 个节点下放置了 6 个子节点,将这 6 个子节点的 zIndex 设为 05

    🎥 各个子节点的排序信息:

    zIndex 0~5

    可以看到,zIndex 的值会直接体现在 _localZOrder 的「高 16 位」;每当 zIndex 增加 1_localZOrder 就会增加 65537

    所以说 siblingIndex 怎么可能打得过 zIndex

    场景五 (Scene 5)

    在同 1 个节点下放置了 6 个子节点,将这 6 个子节点的 zIndex 设为 05

    🎥 修改第 6 个子节点的 siblingIndex04,其排序信息:

    zIndex 5 & siblingIndex 0~4

    可以看到,此时无论我们怎么修改第 6 个子节点的 siblingIndex,它都会自动变回 5(也就是同级节点中的最大值)。

    因为这个子节点的 zIndex 在其同级节点之中有着绝对的优势。

    不太对劲 (Something wrong)

    😲 这里有一个看起来不太对劲的现象!

    比如,当我们把 siblingIndex5 修改为 0 时,_localZOrder 也相应从 327687 变成 327682;但是当 siblingIndex 自动变回 5 时,_localZOrder 也还是 327682,并没有变回 327687

    🤔 为什么会这样?

    原因其实很简单:

    当我们修改节点的 siblingIndex 时会触发排序,排序过程中会「根据节点当前时刻的 siblingIndex 和 zIndex 生成新的 _localZOrder」;

    最后在父节点的 sortAllChildren 函数中会根据子节点的 _localZOrder 来对 _children 数组进行排序,此时「子节点的 siblingIndex 也会被动更新」,「但是 _localZOrder 却没有重新生成」。

    但是,由于 zIndex 存在「绝对优势」,这种“奇怪的现象”其实并不会影响到节点的正常排序~

    总结 (Summary)

    分析完源码后,我们来总结一下。

    在代码中修改节点顺序的方法主要有两种:

    1. 修改节点的 zIndex 属性
    2. 通过 setSiblingIndex 函数设置

    无论使用以上哪种方法,最终都会「通过 zIndex 和 siblingIndex 的组合作为依据来进行排序」。

    在多数情况下,「修改节点的 zIndex 属性会使其 setSiblingIndex 函数失效」。

    这无形中增加了编码时的心智负担,也增加了问题排查的难度。

    引擎内的用法 (Usage in engine)

    出于好奇,我在引擎源码中搜了搜,想看看引擎内部有没有使用到 zIndex 属性。

    结果是:只有几处与「调试」相关的地方使用到了节点的 zIndex 属性。

    Usage in engine

    例如:预览模式下,左下角的 Profiler 节点。

    Profiler Node

    以及碰撞组件的调试框等等,这里就不在赘述了。

    建议 (Suggestion)

    所以,为了避免一些不必要的 BUG 和逻辑冲突。

    我的建议是:

    「少用甚至不用 zIndex,而优先使用 siblingIndex 相关函数。」

    🥴 听皮皮一句劝,zIndex 的水太深,你把握不住!


    传送门

    微信推文版本

    个人博客:菜鸟小栈

    开源主页:陈皮皮

    Eazax Cocos 游戏开发工具包


    更多分享

    《Cocos Creator 性能优化:DrawCall》

    《在 Cocos Creator 里画个炫酷的雷达图》

    《用 Shader 写个完美的波浪》

    《在 Cocos Creator 中优雅且高效地管理弹窗》

    《JavaScript 内存详解 & 分析指南》

    《Cocos Creator 编辑器扩展:Quick Finder》

    《JavaScript 原始值与包装对象》

    《Cocos Creator 源码解读:引擎启动与主循环》


    公众号

    菜鸟小栈

    😺 我是陈皮皮,一个还在不断学习的游戏开发者,一个热爱分享的 Cocos Star Writer。

    🎨 这是我的个人公众号,专注但不仅限于游戏开发和前端技术分享。

    💖 每一篇原创都非常用心,你的关注就是我原创的动力!

    Input and output.

    相关文章

      网友评论

          本文标题:Cocos Creator 源码解读:siblingIndex

          本文链接:https://www.haomeiwen.com/subject/htkuvltx.html