一、优化启动的意义
衡量一个游戏好坏的一个很重要的标准就是留存,而启动时间直接决定了第一波玩家的流失率。当用户打开游戏,满怀期待的等待游戏开始。最好的情况是游戏在1-2秒内给与反馈,或者能让用户进行下一步操作。一般首次打开,由于首包需要从服务器下载,都会有一个等待过程。在这个等待的过程中,用户的忍耐度是慢慢降低的。如果游戏在2-5秒之后才进入可用的状态,首屏留存就会受到影响。最后如果游戏超过5秒甚至更久才显示首屏,这时用户的耐心可能完全消失,有一部分用户可能会退出重新进入,但更多的用户会放弃使用。
根据小游戏整体启动留存率分析,Android玩家的首屏打开留存率约为85%。这是什么意思呢,就是玩家从点击小游戏到能看到首屏的渲染界面大约有15%的玩家流失。对于首次玩某款小游戏的玩家,由于本地没有版本缓存,留存率会明显低很多。据统计,仅代码包加载阶段新玩家流失率就达到20%。(以上数据来源微信小游戏性能优化指南)
二、晒数据
以下数据来自我们游戏优化前后的数据对比
三、启动性能优化
小游戏启动加载时序
以下是官方给出的启动时序图和优化建议
1.首包优化上面,我们可以完全按照官方的建议,尽量减少首包的大小,由于我们对引擎有定制,所以暂未使用引擎插件能力,我们首包中只存放了引擎及基础的启动代码,大小为1.5M。如果使用引擎插件功能,这个大小可以缩减到300K。
2.我们在首包中仅放入了游戏引擎的代码和一些必要的资源。这时候游戏尚不能完整运行,因为游戏的逻辑代码在子包中,需要进一步的加载。但是这时我们要尽快让游戏给出反馈,也就是显示首屏。首屏的内容绘制我们有两种方案:1)依赖游戏引擎绘制 2)不依赖引擎直接绘制。
1)依赖游戏引擎进行绘制
我们利用引擎进行绘制,要做到资源尽量少,能够满足绘制一个启动图和一个进度条就可以了。
a.对于使用CocosCreator制作的游戏:我们可以在游戏启动的时候, 对于第一个场景那里使用动态创建场景的方式,
这个动态创建的场景,只使用放在首包里的一些资源。
b.对于使用Laya制作的游戏:我们把原本放在工程代码里的入口代码提取出来,完成Stage的初始化。这样我们就可以做绘制了。
2)不依赖引擎直接绘制
在第一种方案中,优化的方向也是尽量减少第一个场景的资源。但是忽略了一个很耗时的过程,引擎初始化。这一步经测试,iOS在100ms以内,安卓在1-2s。如果能把安卓这1-2s的时间优化,想想都兴奋。
为了使首屏等待时间减少到极致,在引擎初始化之前,我们自己来渲染第一帧。我们的游戏使用的Cocos Creator引擎,默认使用WebGL。我们绘制了一个最简单的黑色三角形作为游戏的第一帧。
//顶点着色器程序
var VSHADER_SOURCE =
"attribute vec4 a_Position;" +
"void main() {" +
//设置坐标
"gl_Position = a_Position; " +
"} ";
//片元着色器var FSHADER_SOURCE =
"void main() {" +
//设置颜色
"gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);" +
"}";//获取canvas元素
GameGlobal.dycc = wx.createCanvas();//获取绘制二维上下文
var gl = dycc.getContext('webgl');
//编译着色器
var vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, VSHADER_SOURCE);
gl.compileShader(vertShader);
var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, FSHADER_SOURCE);
gl.compileShader(fragShader);
//合并程序
var shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertShader);
gl.attachShader(shaderProgram, fragShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
//获取坐标点
var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position');
var n = initBuffers(gl,shaderProgram);
if(n<0){
console.log('Failed to set the positions');
}
// 清除指定<画布>的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清空 <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
function initBuffers(gl,shaderProgram) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3;//点的个数
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create the butter object");
return -1;
}
//将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
//向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
//获取坐标点
var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position');
//将缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
有了第一帧的绘制,用户可以很快进入到游戏内,不会长时间的在官方白色的加载进度屏那里等待。我们还需要做一些其他处理,不然玩家看到的将是一个黑屏。后续的处理也有两种方案,一种就是如果你对WebGL比较熟悉,可以自行完成更复杂的绘制。另外一种方案就是巧妙利用官方提供的功能。我们这里详细说下后面一种方案。
为了盖住黑屏,我们需要绘制一张启动图。这时候我们使用官方提供的接口wx.createUserInfoButton
来创建一个满屏的按钮,按钮的背景图就是我们的启动图。但是这时候如果用户点击屏幕,就会造成提示用户授权,这不是我们想要的。接着我们使用官方的另外一个接口wx.showLoading
,创建一个模态加载弹窗就可以解决这个问题了。有了这样一个首屏,剩下的就是尽快加载子包,开始真正的游戏内容。
四、Demo
希望大家也都能探索一下首屏加载的过程。我们提供了一个简单的demo示例,通过链接即可下载到。打开demo中的index.js文件,里面有操作步骤说明。
laya的例子:
链接: https://pan.baidu.com/s/18RS9HWpmp5l0V3WGQdNvzw
提取码: bdji
cocos的例子:
链接: https://pan.baidu.com/s/1c_UPwdlEFuqprmG-QGMvpQ
提取码: jggt
五、结语
以上就是我们的启动优化方案,欢迎各位游戏开发者交流或提出宝贵的建议,对游戏充满热爱的小伙伴也可以选择加入我们大禹网络。HR邮箱:guyifen@dayukeji.com
感谢分享,但是我运用文中自主渲染首屏时遇到一些问题,我依据文中做法,
在使用过程中,发现:IOS上加载完代码后,能够立马显示 UserInfoButton 的图片,但是在IOS上,却要卡很久,估摸要5-6s才能看到图片,效率远远不如直接初始化引擎显示场景速度来得快。请问是我哪里没有搞对,还是实际上就是这么回事儿?
整个项目除却改动过楼主的index.js文件以外,其余均为楼主百度网盘项目内原版文件。
index.js全部代码如下: window.screenOrientation = "portrait"; //-------------------------------------------------------------楼主的首屏渲染代码 var VSHADER_SOURCE = "attribute vec4 a_Position;" + "void main() {" + //设置坐标 "gl_Position = a_Position; " + "} "; //片元着色器 var FSHADER_SOURCE = "void main() {" + //设置颜色 "gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);" + "}";//获取canvas元素 GameGlobal.dycc = wx.createCanvas();//获取绘制二维上下文 var gl = dycc.getContext('webgl'); //编译着色器 var vertShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vertShader, VSHADER_SOURCE); gl.compileShader(vertShader); var fragShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fragShader, FSHADER_SOURCE); gl.compileShader(fragShader); //合并程序 var shaderProgram = gl.createProgram(); gl.attachShader(shaderProgram, vertShader); gl.attachShader(shaderProgram, fragShader); gl.linkProgram(shaderProgram); gl.useProgram(shaderProgram); //获取坐标点 var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position'); var n = initBuffers(gl,shaderProgram); if(n<0){ console.log('Failed to set the positions'); } // 清除指定<画布>的颜色 gl.clearColor(0.0, 0.0, 0.0, 1.0); // 清空 <canvas> gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLES, 0, n); function initBuffers(gl,shaderProgram) { var vertices = new Float32Array([ 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 ]); var n = 3;//点的个数 //创建缓冲区对象 var vertexBuffer = gl.createBuffer(); if(!vertexBuffer){ console.log("Failed to create the butter object"); return -1; } //将缓冲区对象绑定到目标 gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer); //向缓冲区写入数据 gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW); //获取坐标点 var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position'); //将缓冲区对象分配给a_Position变量 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); //连接a_Position变量与分配给它的缓冲区对象 gl.enableVertexAttribArray(a_Position); console.log("呵呵"); return n; } //-------------------------------------------------------------------------- //创建userInfoBtn和loading var showUserInfoBtn = function(){ var wx = window.wx; var screenInfo = wx.getSystemInfoSync(); var btn = wx.createUserInfoButton({ type:'image', image:'res/shotMan.png',//随便找的一张图片 style:{ left:0, top:0, width:screenInfo.screenWidth, height:screenInfo.screenHeight, backgroundColor:'#ff0000', borderColor:'#ffffff', borderWidth:1, borderRadius:0, color:'#ffffff', textAlign:'center', fontSize:10, lineHeight:10 } }); wx.showLoading({ tile:'加载中...', mask:true }); setTimeout(function(){ wx.hideLoading(); btn.hide(); },3000) } showUserInfoBtn();
Unity转小游戏也可以参考这种思路嘛?
你好,我使用上面同样的代码,在iOS上运行黑屏,Android和模拟器都可以正常渲染呢?
var VSHADER_SOURCE =
"attribute vec4 a_Position;" +
"void main() {" +
//设置坐标
"gl_Position = a_Position; " +
"} ";
//片元着色器
var FSHADER_SOURCE =
"void main() {" +
//设置颜色
"gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);" +
"}";//获取canvas元素
function initBuffers(gl,shaderProgram) {
var vertices = new Float32Array([
0.0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3;//点的个数
//创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if(!vertexBuffer){
console.log("Failed to create the butter object");
return -1;
}
//将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
//向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);
//获取坐标点
var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position');
//将缓冲区对象分配给a_Position变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
//连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
GameGlobal.screencanvas = wx.createCanvas() || new _Canvas2.default();
var gl = screencanvas.getContext('webgl');
//编译着色器
var vertShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertShader, VSHADER_SOURCE);
gl.compileShader(vertShader);
var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragShader, FSHADER_SOURCE);
gl.compileShader(fragShader);
//合并程序
var shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertShader);
gl.attachShader(shaderProgram, fragShader);
gl.linkProgram(shaderProgram);
gl.useProgram(shaderProgram);
//获取坐标点
var a_Position = gl.getAttribLocation(shaderProgram, 'a_Position');
var n = initBuffers(gl,shaderProgram);
if(n<0){
console.log('Failed to set the positions');
}
// 清除指定<画布>的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 清空 <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);
你好,按照你的方式,我对游戏项目进行了优化:主包包括引擎分离和开放域,子包包含了逻辑的代码,在低版本报错,高版本没有问题,望能指导指导:https://forum.cocos.org/t/topic/104105
Laya这边首屏绘制在load类库和代码包之前如何操作呢?试了试getContext有很多错误?貌似不可行,还是说我的操作有问题?
您好,我们的项目也是用CocosCreator写的,我看了这篇文章后感觉终于有了优化方向,但是还是有一些疑问想请教一下:
我复制了你“不依赖引擎直接绘制”里的代码,加上wx.createUserInfoButton和wx.showLoading,放在了game.js的最开始位置,目的是希望在require所有其他代码前(比如各种wrapper、settings.js还有CocosCreator引擎代码等)执行,尽可能早地显示画面给用户看,但是最后运行的结果,却是一片漆黑。
我想到了微信文档里说,wx.createCanvas只有在第一次调用时,创建的才是屏上Canvas,之后创建的都是屏下Canvas。而你代码里有这么一句GameGlobal.dycc = wx.createCanvas(),我把它放在require引擎代码之前,那么引擎代码调用wx.createCanvas时就不是第一次了,那么游戏里所有的画面都绘制在屏下的Canvas上了。
于是我找到了引擎代码调用wx.createCanvas的地方,也就是GameGlobal.screencanvas = GameGlobal.screencanvas || new _Canvas2.default(),我把它改成了GameGlobal.screencanvas = GameGlobal.screencanvas || GameGlobal.dycc || new _Canvas2.default(),即把GameGlobal.dycc赋值给引擎作为屏上Canvas使用。但运行后又发现问题,我的游戏画面全变白色了。
我猜测可能是GameGlobal.dycc设置了shader导致引擎初始化的时候没有设置成正确的shader之类的问题,但我也不懂WebGL,不知道应该怎么改。
所以,我想在这里请教一下,我应该如何改才能不影响到之后引擎的渲染?
对了还有,我看这段代码虽然只是简单的画个三角形,但又是生成顶点又是设置着色器的,感觉还是有点复杂。请问有没有更简单的方式触发首屏渲染?比如像2d那样fillRect一下就好的?
ps:我不清楚getContext这一函数对canvas做了什么事,我如果把getContext('webgl')改成getContext("2d"),然后调用fillRect,把后面WebGL相关代码注释掉,然后还是把GameGlobal.dycc赋值给引擎作为屏上Canvas使用,结果就报“This device does not support webgl”错误。我按照字面上理解getContext不就是取了一个context吗?怎么会影响到canvas?
GameGlobal.screencanvas = GameGlobal.dycc || new _Canvas2.default();
暴露全局canvas,可以供cocos复用
有点东西,
只能说wx.createUserInfoButton 和wx.showLoading用的很巧妙
感谢分享。