美文网首页creatorCocosCreator优化CocosCreator
Cocos Creator ScrollView 优化系列-2-

Cocos Creator ScrollView 优化系列-2-

作者: 天煞魔猎手 | 来源:发表于2019-07-14 22:44 被阅读0次

    本系列教程指引:

    1. Cocos Creator ScrollView 优化系列-1-分帧加载
    2. Cocos Creator ScrollView 优化系列-2-可视区域渲染
    3. Cocos Creator ScrollView 优化系列-3-复用实现(待续)
    4. Cocos Creator ScrollView 优化系列-4-合批优化(待续)

    本项目中所有图示、代码都在Github仓库中,如果需要运行验证,可直接拉下项目即可,不用自己手撸代码验证

    👉👉https://github.com/zhitaocai/CocosCreator-ScrollVIewPlus👈👈

    一、前言

    在系列上一篇文章中,我们介绍了 「分帧加载」 的技术,最终达到下面效果

    Framing Load

    但是,在这个过程中,随着我们创建的节点越多,会发现 左下角 Draw call 一直在飙升

    这是因为我们的节点在创建并加入到 ScrollView 之后,就一直在显示了,哪怕这个节点不在我们的 ScrollView 的可视区域内,而这正是我们这次要重点解决的问题。

    通过阅读本文,你将了解到如何利用 「可视区域渲染」 解决上述问题,最终实现 只渲染 ScrollView 可视区域内的 Item ,不再可视区域内的 Item 不渲染。效果如下:

    Visiable Area Render

    PS:
    1. 注意看左下角的 Draw call 参数
    2. 因为录屏软件问题,所以 GIF 看上去可能有点卡顿,实际运行会流畅很多

    当然,阅读完本文之后,你还可以 调整姿势,实现诸如下面这种功能:

    Effect

    二、可视区域渲染实现

    在上一节中,我们提及到了 「可视区域渲染」 这个词语。在实现的时候,我们需要拆分两个技术点:

    1. 什么区域才是可视区域?
    2. 节点当前是否在可视区域呢?

    2.1 计算 ScrollView 的可视区域

    那么,什么区域才算是 ScrollView 的可视区域呢?

    其实这反而是一个最简单的问题,因为 ScrollView 本质上是不会移动的,我们一直在移动的只是 ScrollView 中 Content 属性所指定的节点:

    ScrollView Content Node

    所以 ScrollView 的可视区域就是 ScrollView 本身位置和大小,转换为世界坐标系下, ScrollView 的「可视区域」(也可以叫做碰撞包围盒)就是如下实现代码:

    // 获取 ScrollView Node 的左下角坐标在世界坐标系中的坐标
    let svLeftBottomPoint = scrollView.node.parent.convertToWorldSpaceAR(
      cc.v2(
        scrollView.node.x - scrollView.node.anchorX * scrollView.node.width,
        scrollView.node.y - scrollView.node.anchorY * scrollView.node.height
      )
    );
    
    // 求出 ScrollView 可视区域在世界坐标系中的矩形(碰撞盒)
    let svBBoxRect: cc.Rect = cc.rect(
      svLeftBottomPoint.x,
      svLeftBottomPoint.y,
      scrollView.node.width,
      scrollView.node.height
    );
    

    2.2 判断 ScrollView Content Node 的子节点是否在可视区域内

    知道了 ScrollVIew 的可视区域了,那么,我们又如何检查 ScrollView Content 节点下的子节点是否在「可视区域」内呢?

    要知道 ScrollView 的 Content 属性是可以指定不同的 Node ,这个 Node 有可能挂载了不同布局方式的 Layout 组件(水平布局、垂直布局、网格布局),也有可能不挂 Layout 组件,也有可能就是随便乱摆等等的各种情况。

    那么问题来了,情况那么复杂,我们又应该怎么计算 ScrollView Content 中的某个子节点在 ScrollView 的「可视区域」内呢?

    大部分同学可能就是具体情况具体分析,比如

    • 如果Content Node 采用水平 Layout 的时候,用计算x偏移量去判断
    • 如果Content Node 采用垂直 Layout 的时候,用计算y偏移量去判断
    • ......

    不得不说,这是一种办法,救急实用,但是问题是不太通用,考虑不全面,比较难以直接复用。

    要知道我们现在正在使用的是伟大的 Cocos Creator 游戏引擎呢!那么为什么我们不用游戏该有的碰撞算法方式去计算呢?碰撞的话,我才不管你是怎么排列的呢,是不是!

    万幸的是,上一步中,我们知道了 ScrollView 在世界坐标系的「可视区域」(碰撞包围盒),那么如果我们也能求出 Content 中各个子节点在世界坐标系下的碰撞包围盒,然后逐个判断一下是否碰撞,那么岂不是就知道了 Content中的子节点是否在 ScrollView 的「可视区域」内了?!~

    好吧,其实好像也没什么难的,就是转换一下思路,然后代码就出来了:

    // 遍历 ScrollView Content 内容节点的子节点
    scrollView.content.children.forEach((childNode: cc.Node) => {
    
        // 对每个子节点的包围盒做和 ScrollView 可视区域包围盒做碰撞判断
        // 如果相交了,那么就显示,否则就隐藏
        if (childNode.getBoundingBoxToWorld().intersects(svBBoxRect)) {
            if (childNode.opacity != 255) {
                childNode.opacity = 255;
                // childNode.emit("on_enter_scroll_view");
            }
        } else {
            if (childNode.opacity != 0) {
                childNode.opacity = 0;
                // childNode.emit("on_exit_scroll_view");
            }
        }
    });
    

    然后我们只需要在 ScrollView 滚动的时候,一直计算,那么就可以实现「可视区域渲染」了

    onEnable() {
        this.node.on("scrolling", this._onScrollingDrawCallOpt, this);
    }
    
    onDisable() {
        this.node.off("scrolling", this._onScrollingDrawCallOpt, this);
    }
    
    private _onScrollingDrawCallOpt() {
        if (this.content.childrenCount == 0) {
            return;
        }
        // 上文提及到的碰撞检测代码
        // ...
    }
    

    至此,好像就已经完成了基本的「可视区域渲染」的功能了!~

    三、优化

    当然,上面几段代码还是比较简单的,但是它已经把核心的思想(碰撞检测)传递出来了,剩下的我们再优化一下就可以了。

    比如:

    我们每次碰撞检测都创建了不少对象 cc.Vec2cc.Rect 等等,其实这些我们完全可以优化去掉,不生成引用对象,直接计算

    比如:

    例子中,我们直接占用了 childNode.opacity ,这是不妥的做法,因为你永远不应该在上层干涉底层的运作,而这里我们已经干涉了子节点的透明度,如果子节点也需要修改透明度,那么就很容易生成找到的Bug了

    所以,这里我们可以优化一下

    1. 在进入ScrollView的时候,传递一个事件给子节点处理
    2. 在离开ScrollView的时候,传递一个事件给子节点处理

    恩,也就是上面代码中注释掉的两行代码。但是,我注释掉了!因为伟大的 Cocos Creator 游戏引擎是组件化开发的呢~,我们为什么不把事件转换为组件呢?

    Talk is cheap, show me the code.

    // 遍历 ScrollView Content 内容节点的子节点
    // 对每个子节点的包围盒做和 ScrollView 可视区域包围盒做碰撞判断
    scrollView.content.children.forEach((childNode: cc.Node) => {
        // 没有绑定指定组件的子节点不处理
        let itemComponent = childNode.getComponent(ScrollViewPlusItem);
        if (itemComponent == null) {
            return;
        }
    
        // 如果相交了,那么就显示,否则就隐藏
        if (childNode.getBoundingBoxToWorld().intersects(svBBoxRect)) {
            if (!itemComponent.isShowing) {
                itemComponent.isShowing = true;
                itemComponent.publishOnEnterScrollView();
            }
        } else {
            if (itemComponent.isShowing) {
                itemComponent.isShowing = false;
                itemComponent.publishOnExitScrollView();
            }
        }
    });
    

    ScrollViewPlusItem 组件代码如下:

    @ccclass
    export default class ScrollViewPlusItem extends cc.Component {
        @property({
            type: [cc.Component.EventHandler],
            tooltip: "进入ScrollView时回调"
        })
        onEnterScorllViewEvents: cc.Component.EventHandler[] = [];
    
        @property({
            type: [cc.Component.EventHandler],
            tooltip: "离开ScrollView时回调"
        })
        onExitScorllViewEvents: cc.Component.EventHandler[] = [];
    
        /**
         * 当前是否在展示中
         *
         * 1. 在进入和离开ScrollView期间,为true
         * 2. 在离开ScrolLView期间,为false
         */
        isShowing: boolean = false;
    
        /**
         * Item 进入 ScrollView 的时候回调
         */
        publishOnEnterScrollView() {
            if (this.onEnterScorllViewEvents.length == 0) {
                return;
            }
            this.onEnterScorllViewEvents.forEach(event => {
                event.emit([]);
            });
        }
    
        /**
         * Item 离开 ScrollView 的时候回调
         */
        publishOnExitScrollView() {
            if (this.onExitScorllViewEvents.length == 0) {
                return;
            }
            this.onExitScorllViewEvents.forEach(event => {
                event.emit([]);
            });
        }
    }
    
    

    然后ScrollView的Content的子节点接可以挂在这个 ScrollViewPlusItem 组件,然后绑定事件了。恩,就像Button组件那样子使用就可以:

    ScrollViewPlusItem
    /**
     * 本Item进入ScrollView的时候回调
     */
    onEnterSrcollView() {
        this.node.opacity = 255;
    }
    
    /**
     * 本Item离开ScrollView的时候回调
     */
    onExitScrollView() {
        this.node.opacity = 0;
    }
    

    恩,最后实现的就是我们本文一开始的效果图了:

    Visiable Area Render

    PS:完整代码可以参考 Github 项目的 ScrollViewPlus 以及 ScrollViewPlusItem 组件

    四、总结

    看完上面的做法,那么我们总结一下:

    这算是 ScrollView 「可视区域渲染」 比较完善的通用解决方案吗?

    然而并不是!

    如果我们细想的话,就会发现,上面计算 ScrollView 的「可视区域」时,我们只考虑了 anchor(锚点),忽略了 ScrollView 自身的 scale (缩放)rotation(旋转)skew(倾斜) 等几个属性,要知道,这几个属性同样会影响 ScrollView 「可视区域」。

    但是因为实际场合中,我们比较少会改动这几个 ScrollView 属性,所以我就没有实现,但是现在指出来了,相信大家应该知道如何继续完善下去,我偷懒了,大家加油~

    再总结一下:

    如果我们正常使用 ScrollView (不做sacle、rotation、skew修改)的话,本文提及到的方法和代码是够用了

    PS:这里特别再特别指出,如果开启了 ScrollView 的 Content 节点挂载了 Layout 组件并且开启了 Affected by Scale 属性,那么此种方案还需要再优化一下的~

    五、延伸

    5.1 ScrollView 「可视区域渲染」延伸

    既然知道了 ScrollView 的「可视区域」以及节点是否在「可视区域」内,那么除了做渲染优化,我们还可以做:

    • 加载优化。比如,只有Item进入到可视区域内,我们才加载该Item的网络图片之类的逻辑,效果类似上面的演示图那样子
    • 高级动效。比如,下面的这些高级动效,都可以实现,具体代码可以见我的 Github 仓库
      Effect
    • ...

    5.2 「可视区域渲染」延伸

    这节和5.1节就一个区别,就是没有了 ScrollView 的约束。对的,可视区域渲染是一个能大幅度降低渲染压力的一种方案。 比如:大地图游戏,我们只需要渲染可视区域内的内容就可以,不在可视区域内的物体,我们完全可以通过「碰撞检测」去剔除渲染,又或者「分组渲染」等等其他方案去实现。

    核心思想还是:「可视区域渲染」,多理解琢磨这句话,并用到你的游戏上,相信你的游戏品质能更上一层楼

    六、进入下一个章节

    至此,我们的「可视区域渲染」基本告一段落了。下一个章节,我们见会讲述如何实现「节点复用」,敬请期待

    本系列教程指引:

    1. Cocos Creator ScrollView 优化系列-1-分帧加载
    2. Cocos Creator ScrollView 优化系列-2-可视区域渲染
    3. 👉Cocos Creator ScrollView 优化系列-3-复用实现(待续)
    4. Cocos Creator ScrollView 优化系列-4-合批优化(待续)

    相关文章

      网友评论

        本文标题:Cocos Creator ScrollView 优化系列-2-

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