- 物理刚体挖洞之分块 !Cocos Creator !
减少多边形计算!画饼分之~ 效果预览 [图片] 回顾 在 物理挖洞之链条!实现!(含视频讲解) 中介绍了用 [代码]PolyBool[代码] 和链条组件([代码]cc.PhysicsChainCollider[代码])实现物理挖洞的方法。 虽说这种方案可能不是最佳方案,但里面有一种 [代码]evenodd[代码] 的思想,觉得不错的。 [图片] 在 物理挖洞之链条!优化!(含视频讲解) 中介绍了几个优化的地方。 其中,单位化的思想和平滑移动的思想在后续一直被使用。 [图片] 不过,多边形链条组件有一个问题,容易穿透。 接着,经过多次查找和分析,在物理挖洞之多边形!实现! 中介绍用多边形碰撞组件([代码]cc.PhysicsPolygonCollider[代码])去实现物理挖洞。 整体思路是,先用 [代码]Clipper[代码] 去计算多边形 (效率比 [代码]PolyBool[代码] 高),接着用 [代码]poly2tri[代码] 将多边形分割成多个三角形,最后用多边形刚体填充。 [图片] 但是呢,[代码]poly2tri[代码] 限制比较多,物理挖洞之多边形!填坑! 中介绍了填坑之路。 并利用 [代码]mask[代码] 的 [代码]graphics[代码] 实现好看的纹理。 [图片] 当然,还有群内小伙伴们讨论分享的3D效果,在上面的基础上,修改了一个物理挖洞之3D效果,感谢各位小伙伴的分享! [图片] 强烈建议按顺序阅读上面几篇文章,有助于更好的理解这篇的文章哦! 实现原理 整体思路是对区域进行分块,点击的时候判断是对哪个区域块有操作,再对这些区域块进行多边形计算,最后再绘制所有的多边形。 [图片] 这里与物理挖洞之多边形!实现! 中的区别是少了一步 [代码]poly2tri[代码],这是怎么做到的? 首先得明白一点,之前使用 [代码]poly2tri[代码] 是因为会有内多边形出现。 所以,在分块的时候,只要满足分块的尺寸小于挖洞的尺寸,这样就不会出现内多边形了。 [图片] 如何判断点击的是哪个区域呢? 在初始化的时候,用一个2D矩形([代码]cc.Rect[代码])数组记录每一个分块的信息。 [代码]private _rects: cc.Rect[] = []; [代码] [图片] 当点击的时候会生成一个多边形(参考物理挖洞之链条!优化! 中的触摸平滑连续)数据。 对于这个多边形的每个点,计算出坐标 [代码]x[代码] 和 [代码]y[代码] 的最大值和最小值。 然后就可以算出这个的多边形的矩形([代码]aabb (Axis-Aligned Bounding Box)[代码])。 [代码]let xMin = Number.MAX_SAFE_INTEGER, xMax = Number.MIN_SAFE_INTEGER, yMin = Number.MAX_SAFE_INTEGER, yMax = Number.MIN_SAFE_INTEGER; // 计算最小最大值 xMin = p.x < xMin ? p.x : xMin; yMin = p.y < yMin ? p.y : yMin; xMax = p.x > xMax ? p.x : xMax; yMax = p.y > yMax ? p.y : yMax; // 得出矩形 const rect_r = cc.Rect.fromMinMax(cc.v2(xMin, yMin), cc.v2(xMax, yMax)); [代码] [图片] 再用这个矩形和初始化矩形做一次相交判断,这样就可以粗略的确定要计算的块了。 [代码]for (let index = 0; index < this._rects.length; index++) { const rect = this._rects[index]; if (rect.intersects(rect_r)) { this.polyEx.pushCommand('polyDifference', [regions, index]) } } [代码] [图片] 多边形计算用的是 [代码]Clipper[代码] ,使用接口可以参考官网或者物理挖洞之多边形!。 [代码]// polyDifference(poly: cc.Vec2[], index: number) { // 计算新的多边形 // https://sourceforge.net/p/jsclipper/wiki/documentation const cpr = new ClipperLib.Clipper(ClipperLib.Clipper.ioStrictlySimple); const subj_paths = this._polys[index]; const clip_paths = [this._convertVecArrayToClipperPath(poly)] cpr.AddPaths(subj_paths, ClipperLib.PolyType.ptSubject, true); cpr.AddPaths(clip_paths, ClipperLib.PolyType.ptClip, true); const subject_fillType = ClipperLib.PolyFillType.pftEvenOdd; const clip_fillType = ClipperLib.PolyFillType.pftEvenOdd; const solution = new ClipperLib.Paths(); cpr.Execute(ClipperLib.ClipType.ctDifference, solution, subject_fillType, clip_fillType); this._polys[index] = solution || []; [代码] 在所有分块计算之后,最后整体绘制多边形碰撞体和纹理。 [代码]// private draw() { ctx.clear(); for (let index = 0; index < this._polys.length; index++) { const polygons = this._polys[index]; for (let index2 = 0; index2 < polygons.length; index2++) { const polygon = polygons[index2]; let c = this._physicsPolygonColliders[_physicsPolygonColliders_count]; c.points = this._convertClipperPathToVecArray(polygon); c.apply(); for (let index3 = 0; index3 < c.points.length; index3++) { const p = c.points[index3]; if (index3 === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y); } ctx.close(); } } ctx.fill(); [代码] 当然,群([代码]859642112[代码])内小伙伴 [代码]@吴先生[代码] 也实现了这个分块,分块计算多边形同时,也进行分块绘制,欢迎加群一起讨论! 小结 生命不息,挖坑不止! 以上为白玉无冰使用 [代码]Cocos Creator v2.3.3[代码] 开发[代码]"物理挖洞之分块!"[代码]的技术分享。如果对你有点帮助,欢迎分享给身边的朋友。 天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。人之为学有难易乎?学之,则难者亦易矣;不学,则易者亦难矣。 --《为学》 [图片] 原文链接 完整代码(见readme) 原创文章导航
2020-06-17 - 小游戏首屏启动优化
一、优化启动的意义 衡量一个游戏好坏的一个很重要的标准就是留存,而启动时间直接决定了第一波玩家的流失率。当用户打开游戏,满怀期待的等待游戏开始。最好的情况是游戏在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