评论

小游戏首屏启动优化

我们在小游戏开发中,对于首屏加载采用的一些优化方案

一、优化启动的意义

衡量一个游戏好坏的一个很重要的标准就是留存,而启动时间直接决定了第一波玩家的流失率。当用户打开游戏,满怀期待的等待游戏开始。最好的情况是游戏在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

最后一次编辑于  2020-07-03  
点赞 16
收藏
评论

9 个评论

  • 知之无畏
    知之无畏
    2020-02-18

    感谢分享,但是我运用文中自主渲染首屏时遇到一些问题,我依据文中做法,

    1. 首先使用WebGL渲染首屏。
    2. 使用createUserInfoButton,显示一张图片。
    3. 使用showLoading,屏蔽触摸。

    在使用过程中,发现: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();
    
    
    
    2020-02-18
    赞同 1
    回复 1
    • 猫先生
      猫先生
      2020-07-03
      不建议使用wx.createUserInfoButton 来渲染首屏图片,因为wx.createUserInfoButton在不同操作系统上可能存在性能差异,推荐直接使用gl渲染首屏图片
      2020-07-03
      回复
  • 捧着水晶饭碗的吃货
    捧着水晶饭碗的吃货
    2023-05-11

    Unity转小游戏也可以参考这种思路嘛?

    2023-05-11
    赞同
    回复
  • Whu
    Whu
    2021-04-15

    你好,我使用上面同样的代码,在iOS上运行黑屏,Android和模拟器都可以正常渲染呢?

    2021-04-15
    赞同
    回复 2
    • Whu
      Whu
      2021-04-15
      //顶点着色器程序
      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);
      2021-04-15
      回复
    • Whu
      Whu
      2021-04-15
      iOS在横屏情况下有问题。
      2021-04-15
      回复
  • 小川
    小川
    2021-01-12

    你好,按照你的方式,我对游戏项目进行了优化:主包包括引擎分离和开放域,子包包含了逻辑的代码,在低版本报错,高版本没有问题,望能指导指导:https://forum.cocos.org/t/topic/104105

    2021-01-12
    赞同
    回复
  • 后海里的鱼
    后海里的鱼
    2020-09-27

    Laya这边首屏绘制在load类库和代码包之前如何操作呢?试了试getContext有很多错误?貌似不可行,还是说我的操作有问题?


    2020-09-27
    赞同
    回复
  • 1 y
    1 y
    2020-02-20

    您好,我们的项目也是用CocosCreator写的,我看了这篇文章后感觉终于有了优化方向,但是还是有一些疑问想请教一下:

    我复制了你“不依赖引擎直接绘制”里的代码,加上wx.createUserInfoButtonwx.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?

    2020-02-20
    赞同
    回复 5
    • 随心~倩
      随心~倩
      2020-04-26
      最后问题解决了吗
      2020-04-26
      回复
    • 1 y
      1 y
      2020-04-26回复随心~倩
      木有,最后放弃了……
      2020-04-26
      回复
    • 猫先生
      猫先生
      2020-07-03
      在代码gl.drawArrays(gl.TRIANGLES, 0, n);之后加上
      GameGlobal.screencanvas = GameGlobal.dycc || new _Canvas2.default();
      暴露全局canvas,可以供cocos复用
      2020-07-03
      1
      回复
    • 1 y
      1 y
      2020-12-18回复猫先生
      我直接把GameGlobal.dycc改成GameGlobal.screencanvas就可以了,我说的白屏只在微信开发者工具上发生,手机上跑是好的。我后来改进了一下,因为用UserInfoBtn显示图片还需要用Loading去遮挡,不太完美,于是我最后改成用webgl自己绘制了。
      2020-12-18
      回复
    • 1 y
      1 y
      2020-12-18回复随心~倩
      后来忘了回复了,解决了。
      2020-12-18
      回复
  • 简单如一
    简单如一
    2020-01-16

    有点东西,

    2020-01-16
    赞同
    回复
  • Zero_☀🌙
    Zero_☀🌙
    2020-01-16

    只能说wx.createUserInfoButton 和wx.showLoading用的很巧妙

    2020-01-16
    赞同
    回复
  • 2020-01-02

    感谢分享。

    2020-01-02
    赞同
    回复
登录 后发表内容