本系列教程指引:
- Cocos Creator ScrollView 优化系列-1-分帧加载
- Cocos Creator ScrollView 优化系列-2-可视区域渲染
- Cocos Creator ScrollView 优化系列-3-复用实现(待续)
- Cocos Creator ScrollView 优化系列-4-合批优化(待续)
本项目中所有图示、代码都在Github仓库中,如果需要运行验证,可直接拉下项目即可,不用自己手撸代码验证
👉👉https://github.com/zhitaocai/CocosCreator-ScrollVIewPlus👈👈
一、前言
在系列上一篇文章中,我们介绍了 「分帧加载」 的技术,最终达到下面效果
但是,在这个过程中,随着我们创建的节点越多,会发现 左下角 Draw call 一直在飙升。
这是因为我们的节点在创建并加入到 ScrollView 之后,就一直在显示了,哪怕这个节点不在我们的 ScrollView 的可视区域内,而这正是我们这次要重点解决的问题。
通过阅读本文,你将了解到如何利用 「可视区域渲染」 解决上述问题,最终实现 只渲染 ScrollView 可视区域内的 Item ,不再可视区域内的 Item 不渲染。效果如下:
PS:
1. 注意看左下角的 Draw call 参数
2. 因为录屏软件问题,所以 GIF 看上去可能有点卡顿,实际运行会流畅很多
当然,阅读完本文之后,你还可以 调整姿势,实现诸如下面这种功能:
二、可视区域渲染实现
在上一节中,我们提及到了 「可视区域渲染」 这个词语。在实现的时候,我们需要拆分两个技术点:
- 什么区域才是可视区域?
- 节点当前是否在可视区域呢?
2.1 计算 ScrollView 的可视区域
那么,什么区域才算是 ScrollView
的可视区域呢?
其实这反而是一个最简单的问题,因为 ScrollView 本质上是不会移动的,我们一直在移动的只是 ScrollView 中 Content 属性所指定的节点:
所以 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.Vec2
、 cc.Rect
等等,其实这些我们完全可以优化去掉,不生成引用对象,直接计算
比如:
例子中,我们直接占用了 childNode.opacity
,这是不妥的做法,因为你永远不应该在上层干涉底层的运作,而这里我们已经干涉了子节点的透明度,如果子节点也需要修改透明度,那么就很容易生成找到的Bug了
所以,这里我们可以优化一下
- 在进入ScrollView的时候,传递一个事件给子节点处理
- 在离开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组件那样子使用就可以:
/**
* 本Item进入ScrollView的时候回调
*/
onEnterSrcollView() {
this.node.opacity = 255;
}
/**
* 本Item离开ScrollView的时候回调
*/
onExitScrollView() {
this.node.opacity = 0;
}
恩,最后实现的就是我们本文一开始的效果图了:
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 仓库
- ...
5.2 「可视区域渲染」延伸
这节和5.1节就一个区别,就是没有了 ScrollView
的约束。对的,可视区域渲染是一个能大幅度降低渲染压力的一种方案。 比如:大地图游戏,我们只需要渲染可视区域内的内容就可以,不在可视区域内的物体,我们完全可以通过「碰撞检测」去剔除渲染,又或者「分组渲染」等等其他方案去实现。
核心思想还是:「可视区域渲染」,多理解琢磨这句话,并用到你的游戏上,相信你的游戏品质能更上一层楼
六、进入下一个章节
至此,我们的「可视区域渲染」基本告一段落了。下一个章节,我们见会讲述如何实现「节点复用」,敬请期待
本系列教程指引:
- Cocos Creator ScrollView 优化系列-1-分帧加载
- Cocos Creator ScrollView 优化系列-2-可视区域渲染
- 👉Cocos Creator ScrollView 优化系列-3-复用实现(待续)
- Cocos Creator ScrollView 优化系列-4-合批优化(待续)