近期在cocos creator 3.7.2开发微信小游戏过程中,发现微信小游戏的表现比其他平台速度明显慢,尤其在做一些有大量文本的页面时,每次加载都有明显的迟滞感,比在其他平台,如android下,甚至浏览器下都慢很多。花了几天时间优化,最终达到理想效果,比优化前速度提升10倍以上,甚至比原来android app还要快,具体来说:
1. 预编译参数优化
根据cocos creator官网的建议,在内存允许条件下,增加编译条件,就是动态合图:
macro.CLEANUP_IMAGE_CACHE = false;
dynamicAtlasManager.enabled = true;
dynamicAtlasManager.maxFrameSize = 2048;
实际效果不太明显,性能提升有限。
2. label组件优化
由于游戏中某个场景label比较多,经过测试就是label组件拖慢了整体速度,由于是功能所需,不愿意删除label,那么只有全力优化。
2.1 合并相邻label
为减少数量,同时又不减少显示内容,可以将显示风格一致的label用一个label代替。例如:可以一行本身多个label,组合成一个字符串用一个label代替。
还有纵向多行label用一个代替,设置多行字体行间距,是否截断、是否自动换行等等。纵向和横向对齐方式设置。通过合理设置可以到达原来多个label效果。
2.2 合理设置字体缓存模式
缺省情况下label的cache mode为none,这种情况下效率最低,但问题最少,在没有效率问题下,可以采用。采用char模式适合动态变化比较大的情况;bitmap的开销稍大,但兼容性不错,比如一些特殊符号,🥜🚀,类似于这些符号char模式支持比较差,而bitmap模式则没有问题。
2.3 字体本身优化
如果追求最好性能,可以选择位图字体,这样渲染效率最高。但是效果不怎么样,背景抠图和放大效果都不理想。
2.4 合理放置label顺序
这个环节最为关键。根据渲染的规则,同样条件可以合批渲染,但是有个限制就是这些节点必须在一个节点下,而且连续。那么为了管理需要很多时候label和sprite混排,以及分成多个node节点管理的方式有可能导致大量的Draw Call,也就是多次渲染,最终影响效率。优化后,Draw Call数量将减少一大半。
因此,优化label,将一个场景下尽可能多的label设置为相同属性,放置在同一个node下,并且连续放置,中间不要有其他控件。
2.5 减少label属性访问次数
为提高效率,减少了label数量也就减少属性访问次数,为防止对label.string的反复多次修改,可以将字符串单独处理,然后统一赋值给label.string。针对label合并后,多行内容设置问题,可以采用字符串数组进行管理,预先定义字符串数组,甚至可以是多维度:
private labelStrings: string[] = ['','','','','','']
然后对每行单独访问,最后统一赋值:
this.round.string = this.labelString.join('\n');
3. 启用节点常驻
通过以上方法优化后,对于复杂场景一些页面仍然感觉加载有时延,不够顺畅。这种情况下,可以继续优化,就是将一些页面通过常驻内存方式保留,这样每次调用时不用再次初始化,通过这种方式可以将场景中多次打开窗体的情况变得速度非常快。
3.1 设置常驻节点
核心思想就是将节点拖到常驻的root节点,通常为了播放音乐方便,游戏中会安排一个常驻节点,不妨叫gameRoot,将你的希望常驻节点从canvas直接拖到gameRoot下。另一种方式是代码实现:
director.addPersistRootNode(this.gameNode);
或者:
this.gameNode.parent = this.gameRoot;
gameRoot需要提前在页面编辑部分链接设置,在头部进行定义:
@property(Node)
public gameRoot: Node = null;
3.2 显示节点
思想就是将节点移动回到当前场景中
this.gameNode.parent = this.canvas;
这里面需要注意的是,this.gameNode可能因为本身场景的变化,导致this.gameNode的值变成null,这种情况下建议将gameNode保存到一个静态变量中,如定义一个同名的同类下变量,首次运行时保存到静态变量中,以后可以直接读取静态变量。
//定义常驻节点
private static gameNode: Node;
//定义判断是否第一次运行
private static ifFirstRun:Boolean = true;
..
..
//第一次运行赋值
if(GameClass.ifFirstRun){
GameClass.gameNode = this.gameNode;
GameClass.ifFirstRun = false;
}
..
..
//用静态变量实现移动节点
GameClass.gameNode.parent = this.canvas;
3.3 关闭常驻节点
显示完成后,实现关闭逻辑有两种方法:1是将节点移动到gameRoot
GameClass.gameNode.parent = this.gameRoot;
GameClass.gameNode.active = false;
那么每次重复打开窗体时,会反复设置parent和active属性,听起来很好,实际操作中发现仍然可能导致性能不够快。
第2种方式,不切换parent,直接把窗体移动到手机视角以外,需要显示时,直接移动回来即可,这种方式速度最快!但缺点是场景结束前必须把他移动到gameRoot中去,否则会被释放掉,下次场景加载时报错!
//移除窗体
gameClass.position = new math.Vec3(2000, 0, 0);
..
..
//显示窗体
gameClass.position = new math.Vec3(0, 0, 0);
4. 场景常驻模式
以上的方法采用后,整体速度就非常快了,重复打开的窗体显示速度都是毫秒级显示,用户感觉不到任何迟滞。但是,但是在特殊情况下,在场景的加载可能耗费较多时间,而游戏可能面临场景切换时候比较多,那能不能干脆把场景直接缓存呢?
在cocos 2d之前版本中有专门的场景切换方法,可以比较容易的保留和缓存场景,在cocos creator 3.0以后,这些方法都没有了,但是仍然可以代码实现缓存的效果,主要有几个要点:
4.1 添加到常驻节点
//定义场景静态变量
public static currentScenceCanvas:Node;
..
..
//首次运行需要设置为常驻节点
GameClass.currentScenceCanvas = director.getScene().getChildByName('Canvas');
director.addPersistRootNode(GameClass.currentScenceCanvas);
要点是只能添加Canvas节点,把它作为场景的root节点,如果场景文件中有和root平行的其他节点,建议移动到CanVas下面,否则不能也缓存。
4.2 使用静态变量
注意要使用静态变量,确保第一次运行时保存到静态变量中去。
4.3 场景显示和关闭
因为被缓存了,所以显示和关闭方法不能使用loadScene方法,只能用active控制。
//打开场景
GameClass.currentScenceCanvas.active = true;
..
..
//关闭场景
GameClass.currentScenceCanvas.active = false;
4.4 避免场景切换显示问题
这种方式切换场景可能导致切换后,短暂出现页面显示不正常情况,这是因为需要合理规划页面的显示顺序才能保证显示正确,建议代码:
director.loadScene('gameScene',(error, scene) => {
this.__enabled = true;
GameClass.currentScenceCanvas.active = false;
});
以上代码示例,确保在新场景已经正常加载后,再隐藏当前场景避免出现显示错误。
4.5 destroy有无必要
从以上优化和显示策略看,如果多个场景切换,有些场景缓存,有些不缓存,那么不缓存的场景调用了缓存场景后,是否需要手动释放空间呢?经过测试不太需要,系统会自己管理和回收内存,不会出现内存泄露情况;另一方面,如果强制destroy,会导致严重的报错情况!如果很多节点有相互关联和引用情况下,非常容易导致程序运行失败,建议不需要手动destroy当前场景。
下面是我的游戏示例,大家可以试试: