UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 性能优化
随着Live2D在项目研发中被广泛使用，其性能优化的需求已经不容忽视。笔者通过模型资源、Mesh、RenderTexture、Material和CPU耗时这5个方面来阐述优化的过程，并且最终实现了低端机上6个模型30帧的效果，值得大家参考。 优化结果 测试机型：低端机——红米4X 测试样例：同样的6个模型（游戏中同屏最多6个模型） 版本：3.2.05 CPU优化主要在CubismModel.Update、CubismModel.OnRenderObject； 内存优化主要在Texture2D、Mesh、RenderTexture、Material以及Mono内存分配。 接下来介绍从模型资源、Mesh、RenderTexture、Material和CPU耗时5个方面来优化性能。 1、模型资源 项目前期没有制定Live2D美术规范，美术导出文件的时候，直接使用自动布局顶点的方式，而Live2D自动生成Mesh的时候对于每个部件的内外边界各生成一份顶点，发现如下问题： 1、模型Mesh数量太多 2、模型的总顶点数达到6K左右 后续跟美术沟通之后，对Live2D模型（后续的模型都指Live2D模型）的导出规定如下： 1、手动布局顶点 2、ArtMesh数量控制在100以内（之前的模型由于改动的工作量太大，暂时不做修改） 3、模型顶点数控制在2k面以内（游戏最多有6个模型） 4、Edit Texture Altas中贴图大小1024*1024，自动布局且Margin为5px 5、为了避免多贴图导致DrawCall升高，一个模型只使用一张贴图 2小时后，通过Live2D Cubism Editor->File->Model Statistics查看优化结果如下： 性能数据测试结果如下： 2、Mesh 测试发现游戏一个模型的Mesh数量在80-200之间，同屏最多有6个模型，通过UWA GOT测试发现Mesh峰值达到107MB（27380个），Material数量达到10.2MB（14968个），其中Unlit（Instance）数量14563个明显异常。 研究CubismRenderer.cs发现如下问题： 1、运行时创建了两份Mesh内存 2、Mesh资源未销毁，导致内存泄漏 3、顶点色每帧都会重新赋值 针对问题1，经测试发现，改成一份Mesh并没有明显的CPU变化，Mesh内存减少一半，由于每一帧都有顶点变化，_meshFilter.mesh = FrontMesh每帧700次的CPU消耗也可以省去。 针对问题2，可以在OnDestroy中调用Destroy接口销毁Mesh。 针对问题3，使用标记控制是否更新顶点色，减少顶点色赋值的0.5ms左右CPU消耗。 另外值得一提的是，Mesh中使用了顶点色处理透明和颜色变化，但是同一个Mesh的顶点色全部相同，可以想到在Shader中控制颜色，实际上为了动态合批不得不使用顶点色，合批之后控制在10个DrawCall左右，不合批每个模型DrawCall数量达到上百个。 3、RenderTexture CubismMaskTexture.cs中创建的RenderTexture大小默认为1024*1024，项目中遮罩效果大部分使用在眼睛，在实际测试之后有如下优化： 1、将GlobalMaskTexture.asset的Size改成128，实际数值可以根据项目需要修改。 2、CubismMaskTexture.cs中RemoveSource函数加上如下检查，在没有遮罩模型的时候可以释放RenderTexture。 if(Sources.Count == 0) { if(_renderTexture != null) { RenderTexture.ReleaseTemporary(_renderTexture); _renderTexture = null; } } 3、CubismMaskTexture.cs中OnDisable()和RefreshRenderTexture()函数中同样添加释放RT的接口。 4、CubismMaskController.cs做如下修改，避免RenderTexture无法释放，同时避免切换模型且未销毁时CubismMaskCommandBuffer.Lateupdate持续CPU消耗。 修改后： 4、Material 通过UWA GOT测试发现材质数量也达到1w多个，研究发现材质有明显的泄漏问题，解决方案如下： 1、CubismRenderer.cs中Material属性 public Material Material { get { return MeshRenderer.material; } set { MeshRenderer.material = value; } } 修改成： public Material Material { get { return MeshRenderer.sharedMaterial; } set { MeshRenderer.sharedMaterial = value; } } 2、模型导入引擎后，将相同材质的MeshRender缓存，在运行时的Start函数中实例化材质，对所有同材质的MeshRender赋值同一个实例，离线实例化的问题时可能出现两个以上相同的模型共享材质。 3、模型销毁的时候同时将实例化的材质销毁。 5、CPU耗时 通过UnityProfiler发现CPU消耗主要在： 1、CubismModel.OnRenderObject[9.92ms] 一部分时间消耗在CubismCoreDll.UpdateModel(Ptr)调用，该接口为Live2D底层封装暂时无法修改，只能通过减少Mesh数量减少CPU时间。 另一部分消耗DynamicDrawableData.ReadFrom(UnmanagedModel)，通过分析代码发现这里只是复制数据到DynamicDrawableData，则可以省去该CPU消耗. 同时在使用DynamicDrawableData的逻辑中用UnmanagedModel替代。 2、CubismModel.Update[9.64ms] 这个接口最终调用到CubismRenderController.cs文件的OnDynamicDrawableData接口，其逻辑主要是同步底层数据变化同时更新Mesh信息，优化思路如下： 一方面更新是否可见，渲染顺序，透明度，顶点位置信息. 其中从C++底层获取数据的时候每一次都会进行范围合法性检查，此处可以在循环外对数组进行统一检查，有部分Mesh不可见，可以在Mesh不可见的时候避免更新。 具体逻辑如下： ///CubismUnmanagedArrayView.cs public unsafe T this[int index] { get { return Address[index]; } } ///CubismRenderController.cs private void OnDynamicDrawableData(CubismModel sender, CubismUnmanagedModel unmanagedModel) { var dataDrawables = unmanagedModel.Drawables; var iLen = dataDrawables.Count; var flags = dataDrawables.DynamicFlags; var opacities = dataDrawables.Opacities; var renderOrders = dataDrawables.RenderOrders; var vertexPositions = dataDrawables.VertexPositions; if(!flags.IsValid) { throw new InvalidOperationException("flags Array is empty, or not valid."); } if (!opacities.IsValid) { throw new InvalidOperationException("opacities Array is empty, or not valid."); } if (!renderOrders.IsValid) { throw new InvalidOperationException("renderOrders Array is empty, or not valid."); } if (flags.Length < iLen) { throw new InvalidOperationException(string.Format("flags Array Length[{0}] < iLen[{1}]", flags.Length, iLen)); } if (opacities.Length < iLen) { throw new InvalidOperationException(string.Format("opacities Array Length[{0}] < iLen[{1}]", opacities.Length, iLen)); } if (renderOrders.Length < iLen) { throw new InvalidOperationException(string.Format("renderOrders Array Length[{0}] < iLen[{1}]", renderOrders.Length, iLen)); } // Get drawables. var renderers = Renderers; // Handle render data changes. for (var i = 0; i < iLen; ++i) { var curRenderer = renderers[i]; var curFlags = flags[i]; // Skip completely non-dirty data. if (curFlags.HasAnyFlag()) { // Update visibility. if (curFlags.HasVisibilityDidChangeFlag()) { curRenderer.OnDrawableVisiblityDidChange(curFlags.HasIsVisibleFlag()); } // Update render order. if (curFlags.HasRenderOrderDidChangeFlag()) { curRenderer.OnDrawableRenderOrderDidChange(renderOrders[i]); } // Update opacity. if (curFlags.HasOpacityDidChangeFlag()) { curRenderer.OnDrawableOpacityDidChange(opacities[i]); } // Update vertex positions. if (curFlags.HasVertexPositionsDidChangeFlag()) { curRenderer.OnDrawableVertexPositionsDidChange(vertexPositions[i]); } } if (curRenderer.UpdateVisibility()) { curRenderer.UpdateRenderOrder(); curRenderer.UpdateVertexColors(); curRenderer.UpdateVertexPositions(); } } // Pass draw order changes to handler (if available). var drawOrderHandler = DrawOrderHandlerInterface; if (drawOrderHandler != null) { var senderDrawables = sender.Drawables; var drawOrders = dataDrawables.DrawOrders; for (var i = 0; i < iLen; ++i) { var curData = flags[i]; if (curData.HasDrawOrderDidChangeFlag()) { drawOrderHandler.OnDrawOrderDidChange(this, senderDrawables[i], drawOrders[i]); } } } dataDrawables.ResetDynamicFlags(); } ///CubismRenderer.cs internal unsafe void OnDrawableVertexPositionsDidChange(Core.Unmanaged.CubismUnmanagedFloatArrayView newVertexPositions) { if (!newVertexPositions.IsValid) { throw new InvalidOperationException("srcVertexPositions Array is empty, or not valid."); } // Copy vertex positions. var iLen = newVertexPositions.Length >> 1; if (newVertexPositions.Length < iLen) { throw new InvalidOperationException(string.Format("newVertexPositions Array Length[{0}] < iLen[{1}]", newVertexPositions.Length, iLen)); } if (VertexPositions.Length != iLen) { Debug.LogErrorFormat("TranslateVertexPositions dont same length iLen={0}|dstMesh.vertexCount={1}" , iLen, VertexPositions.Length); } // Copy vertex positions. fixed (Vector3* pDstVertexPositions = VertexPositions) { for (var v = 0; v < iLen; ++v) { var pDst = (pDstVertexPositions + v); var offset = v << 1; pDst->x = newVertexPositions[offset]; pDst->y = newVertexPositions[offset | 1]; } } // Set swap flag. SetNewVertexPositions(); } 另一方面同步Mesh的信息，源代码使用了双缓冲Mesh优化性能，由于每一帧都有顶点变化，则每帧调用700次左右的Meshes[BackMesh].colors = VertexColors和MeshFilter.mesh = mesh占用大约一半的时间，这里我改成一份Mesh并没有发现明显的CPU变化，Mesh内存减少一半，同时MeshFilter.mesh = mesh的CPU消耗也可以省去。 总结 在低端机红米4X上： CPU主要耗时优化到45%（30.2ms->13.5ms） 内存优化到32%（13.6MB->4.3MB） 内存和材质泄漏已解决，同时Mesh内存大量减少。 到此Live2D的优化告一段落，在低端机上6个模型已经可以达到30帧。 封面图来源于网络 https://www.live2d.com/zh-CHS/about/ 这是侑虎科技第659篇文章，感谢作者晨星供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者也是U Sparkle活动参与者，UWA欢迎更多开发朋友加入U Sparkle开发者计划，这个舞台有你更精彩！
不知不觉，UWA在游戏性能优化领域耕耘了三年。我们相继推出了三项性能优化产品和服务：UWA驻场深度优化、UWA线上深度性能测评和本地性能测试工具UWA GOT （及其Online服务）。截止到目前，我们已经深度优化了近90款游戏，线上性能测评服务了2000多款游戏、VR和AR项目。 随着优化的项目和拜访的团队越来越多，我们逐渐意识到性能瓶颈不仅是技术层面所致，也来源于开发流程。在游戏项目的精品化和重度化趋势中，任何一个中间环节产生了资源浪费，对最终结果的影响都可能难以预估和弥补。所以，我们推出了UWA性能保障体系，我们认为性能优化不该局限于以前的“头疼医头、脚疼医脚”的救火行为，而应该将其扩展为质量保障体系的一环来保证研发团队更快地发现问题、解决问题，从而加快项目的研发效率。 在介绍UWA性能保障体系之前，我们先来看看项目的性能瓶颈是如何形成的，为什么它会成为多数研发团队的困扰。 项目性能在开发阶段的表现 项目的性能问题并非一开始就产生，也不是某一天突然出现的，而是在研发过程中日积月累的产物。在绝大多数的项目开发流程中，其性能耗时的走势大致如下图所示。 蓝色区域为研发期，一般为游戏立项后的功能开发和堆量阶段。在这一阶段中，功能开发是研发团队的主要目的，很少有团队在这一阶段会关注性能问题，即便是有QA的团队，这一阶段多数也是在测试功能的合理性和正确性，于是性能问题会在这里大量堆积，从0到1，再从1到100。 红色区域为上线测试和优化阶段，现在的项目研发团队多数都集中在这个阶段来进行优化，这时候主要功能已基本开发完成，后续是功能的调整、完善和调优；同时伴随大量玩家的涌入，性能问题被极速放大。卡顿、发热和崩溃问题是这一阶段开发团队和运营团队最为头疼的问题，熬夜、通宵也往往是“家常便饭”。 绿色区域是经过几轮测试和优化后，项目整体性能趋于稳定的状态。一般只要研发团队熬过了红色区域的优化阶段就可以顺利上线公测了。当然，后续也会有新版本的更新、新功能和内容的加入，所以不少项目的性能问题依然会再次涌现，即再经历前面的蓝色和红色区域，依次往复。 以上就是目前绝大多数游戏研发团队正在经历的过程，之所以会出现这种情况，其实是源于目前研发团队多数采用“瀑布流”式的开发习惯，具体如下图所示。 从预案设计，到程序开发、美术开发和玩家测试，根据反馈优化性能和调整方案，直至游戏上线。在项目内容和功能开发的过程中，虽然QA团队会对项目不断测试，但基本上都是围绕功能和数值，针对性能相关的测试寥寥无几。而随着项目的精品化程度要求越来越高、同品类竞争项目的数量越来越多，这种开发方式在性能方面的弊端暴露无疑： 问题一：性能优化时间紧、难度大 在开发流程图中的红色区域，是性能问题暴露的重灾区。这一阶段中，项目团队的制作人、策划和运营团队都在不断催促程序对性能进行优化，以期快速达到流畅的表现效果。原因很直接也很现实：玩家用户不等人。但是，短时间内修复长时间积累的大量性能问题，这其实是非常困难的，甚至可以说是一个悖论：如果研发团队拥有在短时间内就可以力挽狂澜的技术实力，那么在之前的开发过程中，也不会留下大量的性能缺口。 问题二：研发时间更长 研发团队在蓝色区域开发时，会习惯性地将问题（特别是性能问题）的解决后置，希望在后续的测试和优化阶段中集中处理，这是因为除了保证功能的研发效率之外，不少研发团队潜意识中对问题的解决持有这样的态度：同样一个问题，现在还是以后修复其实工作量都一样。这里我们想借用Scrum之父Jeff Sutherland在《SCRUM：用一半的时间做两倍的事》这本书的原话，他非常强调“问题一旦发现就立刻解决”的重要性： “几年前我在加州曾和Palm公司的开发人员聊过天。他们开发出最早一批被大家认为“个人数字助理”的产品，即现在的通讯设备。当时他们自动把工作中做过的每件事都一一加以记录。他们记录的其中一件事是，解决程序Bug要花费的时间，也就是软件开发人员在开发自己的程序导致系统出问题时，得花多久的时间解决。每一次电脑都会自动追踪这件事。 现在假设某天测试人员在试图把Matt（一位研发人员）的程序整合到系统时，却侦测到了缺陷。Matt和大多数程序开发人员一样，都不想马上回头把程序改好，而是答应事后会修改，接着就继续写新的程序去了。 在大多数企业里，这样的测试动作甚至不会在写出程序的同一天进行，可能是等到程序写出来几周、几个月后才进行测试，问题都是到了那个时候才被发现。但是，Palm公司每天都针对所有程序都进行自动测试，因此一有问题就会立刻察觉。 测试人员检测全公司每一位“Matt”们，亦即数百名开发人员，并分析马上修改程序所花费的时间，和数周后才修复程序所花费的时间有何不同。不要忘了，软件是一种颇为复杂、牵连甚广的产物，你觉得上述两种改正的方式所花费的时间会相差多少？ 答案是相差了二十四倍。假如程序在出现缺陷的当天被改正，这或许得花一个小时；但是数周后才被改正，就必须花费二十四小时。这和Bug是大是小、是复杂是简单并没有关系。” 他在大量的实际项目中发现，同样的问题、同样的人去解决，但时间点的不同往往会让解决效率大相径庭。这种情况，在我们现在的项目研发过程中比比皆是。 问题三：团队士气低落 研发过程中的红色区域可以说是相当“磨人”的：卡顿、发热等问题造成的用户流失会给开发团队带来极大的压力。功能不好可以修正，数值不好可以调整，但真正麻烦的是明知道性能有问题却无从下手。这种有力使不出的无助感才最磨人，团队的士气也就在这一次次的测试中消磨殆尽。人心散了，队伍就不好带了。 关于UWA性能保障体系 在UWA不断优化游戏性能问题的同时，我们也慢慢地摸索出了一套游戏性能的保障体系，相继推出了三项性能优化产品和服务（驻场深度优化、线上深度性能测评和UWA GOT本地测试工具），可以满足不同研发阶段的优化需求。我们希望不仅从技术层面优化性能问题，同样也能从流程层面来优化开发效率。 最早推出的UWA现场深度优化服务旨在解决研发周期中红色区域的性能问题。我们用最直接的方法帮助研发团队快速解决问题。性能流畅、顺利上线——这正是处于这个阶段的团队最急切的需求。目前，UWA技术团队能在10~15天内完成一份200多页的性能优化报告，其中涵盖了CPU、内存和GPU等全方位的问题瓶颈分析和相应的解决方案。根据我们报告中的建议进行优化，多数项目的性能均可在1个月内大幅提升。目前我们深度优化过的项目近90款，几乎覆盖了行业内所有的游戏品类。但随着优化项目的数量逐渐增多，我们也意识到了这种服务存在一定的局限性： 1）每一款项目的深度优化，我们都需花费大量的时间和精力来测试和分析，导致我们只能服务于少数客户，而无法让广大研发团队受益； 2）有该需求的项目的上线日期迫在眉睫，迫于压力，研发团队无法对一些框架问题做大量甚至结构性的调整，对于他们来说性能重要，但维稳更重要。 因此，我们推出了UWA线上性能测评服务。通过该服务，研发团队可以在UWA官网上提交项目进行性能检测，24小时之内就会收到一份详细的性能测评报告。参考性能简报（测评报告中某一页面）中的优化任务队列，研发团队即可逐条定位和优化。 虽然线上测评服务还无法达到驻场深度优化的精准，但却大幅提升了UWA的易用性：打个包上传就可以坐等报告了。迄今为止，已经有2000多个研发团队使用过线上性能测评服务。该服务更适合于研发团队从研发中期就开始使用，将更多的问题在中期进行解决，而不是全部放到后期优化阶段。这不仅可以缩短整体的优化时间，也可以让研发团队在上线测试后对突发问题更加游刃有余。 但是，我们认为这还是不够快。对于技术团队来说，永远希望在改变一行代码、一个资源的下一秒就能看到它带来的变化。所以，我们在去年推出了本地测评工具UWA GOT，今年又推出了UWA GOT Online在线测评服务。它的特点就是快和方便，且足以满足日常的检测需求。研发团队可以直接在本地进行测试，并在测试后的几分钟之内就可以对性能趋势进行查看和分析。 同时，通过UWA GOT Online的云端分析引擎，可以快速对性能问题进行定位，并收到详尽的优化建议。目前该工具已经涵盖了重要的性能参数，如FPS、内存、能耗、资源信息、逻辑代码和引擎模块重要参数等，不仅方便技术团队使用，更重要的是QA团队也能从项目研发的早中阶段开始，定期地对自己的项目版本进行性能跟踪，一旦发现某个版本上的某个重要性能指标出现了问题，就可以及时反馈给研发团队进行完善和修复。 以上就是UWA性能保障体系的三项产品和服务，下面我们从性能优化的效率、精准度和覆盖范围三方面来简单地对其特点进行总结，以方便大家更容易地理解。 在效率上： 驻场深度优化的问题解决能力最深入，通过现场对项目进行分析，为团队提供定制化的解决方案，但耗时相对较长，一轮完整的执行需要10~15天；线上深度测评的问题能力次之，但时间大为缩短，24小时左右就可以得到性能测评报告；UWA GOT本地测评工具更加倾向于发现问题，加入了UWA GOT Online分析服务后，进一步提升了它的解决问题能力，但它的特点在于快和方便。 精准度和功能覆盖面： 驻场深度优化毫无疑问最为精准全面，几乎CPU、GPU、内存和引擎各个功能模块都可以覆盖到；线上深度测评次之；UWA GOT在这两方面都要弱于其他两个功能，但其在线分析（UWA GOT Online）功能让其在精准度方面大为提升。 UWA性能保障体系的三种产品各有其使用特点：UWA GOT适合于研发团队对于版本性能的及时监控，类似于我们经常佩戴的一些检测心率等人体指标的运动手环；UWA线上深度测评适合于研发团队定期对项目性能进行检测，类似于我们平时的“体检”；UWA驻场深度优化，则是在问题较为严重且时间非常紧迫的情况下的解决方案，类似于“做手术”。 UWA性能保障体系 我们希望通过性能保障体系来完善研发团队的研发流程，从而方便研发团队及时发现和解决问题。通过UWA性能保障体系，可以让研发流程并形成如下几个快速反馈的闭环： （1）对于QA团队，无论是研发、测试还是上线阶段，都可以针对性能进行及时监控； （2）对于技术团队，性能问题及时发现、及时修复，具体问题具体解决； （3）对于策划团队，性能问题背后的设计需求可及时调整和变更。 当上面的闭环逐步迭代流转起来之后，可以达到如下效果，性能耗时在开发过程中的趋势可以变成如下方式： 这样一方面可以将后期的优化工作前置，另一方面及时的问题解决可以极大减少后期的优化时间，加快游戏的整体研发进度。 为什么要做UWA性能保障体系 “防范大于救火”，这是UWA性能保障体系的意义。至于为何我们要这么做，我想通过下面这则故事来进行解释，大家读完之后自然就明白了。 扁鹊是春秋战国时的名医，他有两个哥哥，三兄弟都精通医术。魏文王曾问扁鹊：“你们家兄弟三人，都精于医术，谁的医术是最好的呢？” 扁鹊回答：“大哥最好，二哥差些，我是三人中最差的一个。”魏王不解地说：“但是你的名气却是最大的啊。”扁鹊解释说： “大哥治病，是在病情发作之前，那时候病人自己还不觉得有病，但大哥就下药铲除了病根，使他的医术难以被人认可，所以没有名气，只是在我们家中被推崇备至。 我的二哥治病，是在病初起之时，症状尚不十分明显，病人也没有觉得痛苦，二哥就能药到病除，使乡里人都认为二哥只是治小病很灵。 我治病，都是在病情十分严重之时，病人痛苦万分，病人家属心急如焚。此时，他们看到我在经脉上穿刺，用针放血，或在患处敷以毒药以毒攻毒，或动大手术直指病灶，使重病人病情得到缓解或很快治愈，所以我名闻天下。” 魏王大悟。 项目的研发需要持续改善，而解决问题的最佳时机就是在你看到问题的当下，而非在问题发生之后。
提升市场竞争力，这些细节你都把握了吗？
优化工作向来是个复杂系统的工程，且不说前期的框架预设、大量的版本迭代、美术和性能的需求平衡，单从每个版本的上万帧测试数据中定位真正的性能瓶颈，其工作量就已可想而知。 无论项目是否复杂或简单，当我们开展优化工作前，这些问题都需在胸中有丘壑。那么如何井然有序地定位和排查性能问题呢？我们定位到了性能的真正瓶颈了吗？今天小编以一个优化前期的MOBA手游的UWA性能测评报告为例，深入分析该项目在真机上的运行情况，并讲解如何通过报告中的功能模块更高效地定位性能瓶颈，希望能对大家的项目有所参考。 一、关于性能简报 在这次优化讲解的时候，我们将从报告的“性能简报”模块入手（每个项目性能报告的首页），该模块作为UWA性能评测报告中的一大模块，将引擎性能模块的数据更有序、更核心地汇总成一个完整的优化体系，建议大家在优化项目之初先仔细参阅该模块的数据。点击这里可以快速了解UWA性能简报。 在性能简报的第一个模块，可以直观查看到最近测试的项目报告中的重要参数、其优化效果和性能变化趋势。 从性能排名的模块中，我们提供了测评项目在国内数千款测评项目中，同设备和同类型的横向排名，从而帮助大家更客观地发现项目中的优势和不足。 引擎模块的性能排名 内存模块的性能排名 UWA解读：该项目在CPU模块和内存上均有大量的提升空间。CPU模块的压力主要来自于引擎的渲染和UI，物理、粒子和动画，逻辑代码较为严峻。内存模块中各个资源的占用内存普遍都较高，Mono堆内存分配也较大。 在性能简报的最下方，是UWA针对本次性能提测所提供的分析与建议，在这里将所有引起以上参数开销的性能问题逐一列出，并通过精准的页面链接跳转定位到问题源头，协助开发者进行有效地排查。其中，标红部分的问题较为明显，建议研发团队优先处理。 优化任务队列是我们提供给用户的性能优化切入点，下面我们将逐一分析上图中罗列的问题。 二、优化队列详剖析之内存篇 2.1 内存占用 本次报告的内存占用峰值为660.3MB，高于行业同类型同设备，当前行业均值为305MB，因此需要研发团队密切关注。特别的，如果要保证在一些1GB内存的设备比如iPhone 6或6 Plus上不闪退，我们建议尽量不要超过280MB。 内存的主要构成来自资源内存和Mono堆内存，在此我们做下详细分析。 2.1.1 资源内存之纹理 （内存超标） 排查方向： 1） 纹理的分辨率、格式 如性能简报中显示，RGBA32和ARGB32都不是Android原生支持的格式，不但内存占用较大，且加载效率也低下，建议尝试转成ETC1等硬件支持的格式。如果效果不满意也可以尝试通过Dither算法，对某些高精度的纹理进行处理，转成RGBA4444或RGB565格式，以降低内存占用。 补充说明：在图表中我们也看到不少N/A资源：一般是new出来没有命名的纹理资源，建议在这些纹理new出来之后，给.name赋值，以方便进行纹理资源管理。 2）在查看纹理的具体使用情况时，我们同时发现其冗余情况较为严重。下图为按峰值倒序排序的资源列表截图，可以看到数量峰值大于1的纹理有100多个，当两张纹理的名字、尺寸、格式等属性都相同，UWA就会将其视为具有高风险冗余的资源。 关于冗余的排查优化方法： a) 检查AssetBundle（即打包的时候就有冗余）中是否有冗余，该问题可通过AseetBundle资源检测服务来定位； b) 检查AssetBundle重复加载情况，是否有AseetBundle反复加载导致的缓存漏洞。 2.1.2 资源内存之网格 （内存超标） 排查方向： 1）表格中顶点数大于5000的属于超大网格，需要进一步排查是否有必要。 按Vertex数量进行排序，可以看到第一页的网格顶点数都在1w左右，这个会造成一定程度的内存压力。 2）上图中也可发现，大量网格含有Tangent属性，Tangent属性一般为导入引擎时生成，会增大网格的内存占用，如果实际上不需要Tangent属性，可在Inspector面板中修改设置，关闭Tangent导入选项。 列表中显示Tangent数量为-1的网格，表示Read/Write选项被关闭了，所以统计不到。在此，UWA建议研发团队提交一个临时的打开该选项的版本，来检查网格的属性是否合理。 3）存在冗余的问题，解决方法同纹理 另外，我们在排查资源清单的时候发现了大量的内置资源，在这里提醒大家关于内置资源的处理方法可参考这篇文档：零冗余解决方案（其他类型的资源可借鉴） 2.1.3 动画资源（内存超标） 根据性能简报的提示页面转到动画文件的使用信息列表，如下图所示： 对于大于200KB且时长较短的动画资源，UWA认为可以进一步排查。这里看到资源列表按照内存占用排序，前3页内存占用大小都超过了200KB，建议使用一些精度处理的方法来进行优化：通过降低浮点数精度的方法来减小动画文件的大小。需要注意的是，这种方案会导致动画精度一定程度上的损失，需要大家衡量下结果。 2.1.4 粒子系统内存 （数量和内存超标） 排查方向： 1）Active的粒子系统数量过多，UWA建议在中低端设备上不超过50个，可以考虑通过一些设备分级的策略做改善（剔除某些在部分机型上表现力较差的粒子系统） 2）缓存的数量过大（UWA建议不超过600个），如下图中实际粒子的使用走势图所示，蓝线表示内存中的粒子系统数量，紫线表示实际上Active的量，从而得知大量粒子系统始终没有Active过（表格中显示Active数量峰值为0），但在运行时都被加载了。 在下图中，对于始终未被Active的粒子系统，我们建议研发团队针对配表优化。 2.1.5 材质（数量超标） 对于Material资源的把控主要在于数量而非内存，UWA建议材质数量控制在300以内。 排查方向：冗余 对于Instance类型的冗余Material资源，两种优化方法： 1、尝试通过《使用MaterialPropertyBlock来替换Material属性操作》方法对其进行优化； 2、当我们修改了一些特定GameObject的资源属性时，引擎会为该GameObject自动实例化一份资源供其使用，比如Material、Mesh等。以Material为例，我们在研发时经常会有这样的做法：在角色被攻击时，改变其Material中的属性来得到特定的受击效果。这种做法则会导致引擎为特定的GameObject重新实例化一个Material，后缀会加上（Instance）字样。其本身没有特别大的问题，但是当有改变Material属性需求的GameObject越来越多时，其内存中的冗余数量则会大量增长。一般情况下，资源属性的改变情况都是固定的，并非随机出现。比如，假设GameObject受到攻击时，其Material属性改变随攻击类型的不同而有三种不同的参数设置。那么，对于这种需求，我们建议直接制作三种不同的Material，在Runtime情况下通过代码直接替换对应GameObject的Material，而非改变其Material的属性。这样，成百上千的Instance Material在内存中消失了，取而代之的则是这三个不同的Material资源。 对于非Instance类型的冗余Material，建议研发团队通过资源检测服务来查看AssetBundle中是否存在冗余资源。 以上是主流资源的使用情况和分析建议，下面我们再关注下报告中检测到的其他资源的使用情况： 2.1.6 字体（内存超标） 主要问题：冗余 2.1.7 Shader 排查问题和关注方向： 1）Shader冗余 2）部分常用的Shader是否可以做成常驻的，考虑做法是Shader加载后放在脚本中，引用之后不卸载； 3）Shader的内存占用往往不会造成明显的问题，但是数量如果过大，容易导致Shader.Parse在中低端上的高耗时； 4）资源列表中查到有不少Standard Shader，如下图所示，由于其会带来不必要的Shader计算开销，从而大幅增加GPU中对应像素的计算压力。对此，建议研发团队尝试对其用普通的Shader来替换。 2.2 Mono 堆内存 2.2.1 单次分配过大 报告显示部分函数的Mono堆内存单次分配较大，建议大家通过详细的堆内存报告中定位堆内存分配过大的时刻。这里推荐用UWA GOT针对部分函数进行打点拆分。 2.2.2 堆内存泄露 经分析，报告也存在堆内存泄露的风险，我们通过对比相同游戏场景（均为副本）的不同采样点，发现某些函数存在堆内存驻留的问题，如下图运行到16000帧和4000帧时，游戏场景均为战斗副本中，但是函数Asset.WaitReadFileBywww有1.99MB的堆内存驻留，经过与研发团队讨论，初步判断这里疑似泄露。 其他函数也存在堆内存泄露的情况，建议研发团队逐一排查。 三、优化队列详剖析之引擎篇 3.1 渲染模块 渲染模块的开销较大，我们建议直接从堆栈信息中定位。通过性能简报提示，我们查看Camera.Render的堆栈信息如下所示，其中蓝色底纹的是需要大家关注的重点部分，即主要的渲染开销瓶颈。 RenderForwardAlpha.Render是指半透明渲染的耗时，在这层函数中，我们看到有两个高耗时的函数： 1）Mesh.DrawVBO和CreateVBO 这个函数在半透明下，就表示绘制半透明网格的耗时，半透明网格一般就是UI、场景里的半透明物件，还有半透明的Mesh特效。如果只是一帧其实问题不大，但如果是频繁开销，则需要研发团队特别注意。 经和研发团队的交流讨论，我们了解到的确存在Mesh特效过大的情况，因此我们建议是否可以设定一些设备分级的策略。 2）Material.SetPassFast Material.SetPassFast是Unity引擎在渲染过程中Material的轮循切换开销，一般在Unity 5.0~Unity 5.3版本中出现（项目版本是Unity 5.3.5）。根据下图中的堆栈走势可发现这种情况是几乎每一帧都发生的，有渲染的地方就会有Material的切换。 从问题图中可以看出，在运行的21000帧中，Material.SetPassFast一共被调用118万次，频率较高。建议研发团队在报告中的具体信息页面查看材质是否有过多“冗余”的材质出现（本文2.1.5中有提及），尽可能降低材质的使用冗余度。 3.2 UI模块 从图表中可以发现UIPanel.LateUpdate()的重建较高，这块需要大家关注下UI制作是否合理，由于这块内容和研发团队的制作方法紧密相关，需要具体问题具体分析。所以详细的交流探讨细节我们不在此展开，建议同样有着这方面困惑的开发者可以参阅UWA Blog上关于UI的视频：Unity UI性能优化，这里我们提到了面对多种UI性能优化的技巧。 3.3 物理模块 从UI模块可知，研发团队使用的是NGUI，在每个Panel上都放置了一个Rigidbody，所以当UI Widgets摆在同一深度并存在相互叠加的情况时，会造成较多不必要的Contacts。建议研发团队看下是否能把UI层之间的碰撞检测关掉。 另外，在Unity 5.3.5版本上，如果想进一步降低物理模块的开销，在完全没有使用物理的情况下，可以将Fixed Timestep设置为0.05或0.1均可，降低它被调用的频率。同时，尽可能优化其他模块耗时，让每帧的总体耗时尽可能降低。另外，需要注意的是，修改Fixed Timestep也会影响FixedUpdate的调用，在修改之前一定要检测项目中是否有使用FixedUpdate。 3.4 粒子模块 粒子系统的耗时多与Active的粒子数量有关，这块建议结合粒子模块的内存问题（文中2.1.4有提及）一并优化。 四、GPU优化 最后还需要关注的是GPU问题，UWA在测试的时候发现该游戏在某些高端设备上耗电发热明显。 在GPU性能的Overdraw页面，我们可以看到GPU信息如下图所示： 下图为该帧下的截图，可以看出，特效和UI的屏幕占比过大，且存在UI叠层的概率较高。因此，研发团队需要特别关注技能特效和UI界面的制作情况，避免给GPU造成大量的计算浪费。 五、优化任务队列总结 1）AssetBundle的依赖打包方式 解决资源内存占用过大的问题 2）Mono堆内存分配 解决Mono堆内存大小和GC卡顿的情况 3）Mesh特效 解决渲染模块的耗时问题和GPU压力过大问题 以上就是该游戏的诊断内容，我们从UWA的性能简报出发，围绕CPU、内存、GPU三大模块的性能问题展开，通过数据报告的查看对比，整理出了一条较为完整的优化思路，希望能对大家的自身项目有所启发。目前，UWA的线上测评服务已经为数千款项目提供了解决方案，我们希望能帮到更多游戏开发者节约优化的成本，提升项目的性能表现力，迎接“精品”时代的挑战！
当项目进入优化阶段时，我该如何根据项目的实际情况安排优化工作的进度？针对每个模块的优化做哪些工作才最高效呢？怎样的排查才算全面到位，是否还有一些我们平时忽视却很致命的优化细节呢？今天，UWA就推荐一个可以快速掌握上手的“分析框架”，帮助大家更有章法地开展优化工作。 我们结合了UWA性能测评报告中的“性能简报”功能，梳理了研发团队需要关注的各个引擎模块，以及针对各个引擎模块大家可以着手排查的维度，简化示意图如下所示： 细看在这个分析框架图，我们不难发现，在实际优化过程中不仅要从宏观上分析项目运行时的整体趋势，包括总体CPU性能、引擎各模块性能等，也需要从微观上深入剖析运行时的代码堆栈、资源管理等情况，以确保百密无一疏。 我们以渲染模块的优化为例做解释说明，上图中显示可以从以下两方面入手： 一、分析渲染模块的总体性能 如下图所示，我们需要在UWA测评报告的“渲染模块性能”页面中关注一些重要参数的使用情况，比如DrawCall和Triange的峰值、半透明渲染和不透明渲染的使用趋势等。 二、查看Camera.Render函数的代码堆栈 Camera.Render的代码堆栈中包含了大部分渲染相关的函数操作，通过总体堆栈信息、指定场景堆栈信息、指定帧堆栈信息，配合运行截图，研发团队能更全面、更精准地定位性能瓶颈。 看到这里你也许有了新的疑问，UWA提供的分析框架图中的信息量太大，并且还伴随着大量繁琐的细节问题的定位，我该怎么快速上手呢？小编建议大家快来解锁“UWA性能简报”! 如上图所示，性能简报就渲染模块中的各个问题分别进行罗列，并建议开发者分别通过以下三个维度去优化： 1）查看Camera.Render代码堆栈 2）减小同屏渲染面片数 3）优化Draw Call数量峰值 再例如，当通过LoadFromFile加载的次数过多时，性能简报不仅明确指出通过该API加载过高的AssetBundle总数，并指引研发团队跳转至对应的分析页面做进一步的排查。 下图为点击蓝色超链接后的跳转页面，详细罗列了通过LoadFromFile加载的AssetBundle的具体使用情况。 目前，UWA分析引擎中的性能规则已经超过1000种，它将引擎性能模块的数据有序地汇总成一个完整的优化体系，通过“分析与建议”中提供的精准优化方案，帮助研发团队达到对症下药的优化效果。 临危不惧，在于胸中自有章法； 高手优化，在于胸中自有UWA！ 解锁更多UWA性能测评功能，请戳：https://www.uwa4d.com/#feature UWA合作案例 关于UWA 由侑虎科技开发的游戏/VR应用性能优化平台，目前提供 1）性能诊断与优化 2）资源检测与分析 3）UWA GOT 三大工具，帮助开发者在短时间内大幅度提升性能表现；同时其搭建的知识分享的博客和答疑解惑的互动平台使广大开发者收益，UWA以实力和态度诠释对性能优化的定义：问题的答案永远“在现场”，解决你的性能问题，才叫“优化”。
四月份的博客也欠下了，大部分业余的时间花在准备UWA DAY 2018的分享上面。当时也说了，做这次分享也算实现了我去年的一个“小愿望”，借这个机会认识了不少做技术的朋友。回到杭州之后，也有不少人跟我要PPT，讨论一些分享中的细节。在准备分享的时候，因为时间关系，也将一些计划扩展开的内容删除掉了。所以这次想再根据准备时候的逐字稿整理一篇博客，以作记录，也为没有参加UWA DAY 2018的朋友提供参考。 所以这篇博客的主题还是以这次分享的核心内容为主线，顺便把PPT的内容也提供出来（以引用块或者(补)的方式标注，以方便听过分享的朋友阅读）。 题目和提纲 这次分享的题目是《Connecting The Dots——基于团队的持续优化之道》。优化是一件琐碎而又繁复的事情，它通常是一个又一个的点，而我们如果要让一个比如说30人甚至50人的大型团队，花1到2年的时间去开发一款大型的手游项目，不但需要了解和掌握这些优化的细节，而且要借助团队的力量在整个游戏开发的生命周期过程中不断地进行持续优化，才能够让优化效果可以保持下去。 在聊具体的分享内容之前，我想先抛出这样一个问题——在优化这样的过程中，程序应该承担一个什么样的角色？ 这也是和我刚才所想要聊的持续优化关联性非常大的问题。在优化过程中，程序可能有两种角色，一种是像救火员这样的角色，哪里冒火了，就去扑灭它。我自己也有过作为救火员去做优化的经历。另外一种工作方式是像园丁一样，可能不需要特别紧急地在最后关头才处理那些性能问题，而是从项目的初期就开始规划整个项目的性能指标，然后让整个团队可以按照指标的要求合理地产出美术资源、编写代码，从而让整个产品一直处于一个比较健康的状态。 救火员的职责很有挑战性，同时也比较容易做出成就感，比如我也有过用1到2周时间让游戏的性能有非常大提升的经历。而园丁在做的事情可能看上去比较普通，都是些日常且琐碎的事情。在游戏开发中，这两种角色并不冲突，而是同时存在的。但是在我的理解里，对于性能更友好的状态，是让团队里的成员都在以一个园丁的角色在工作，从根本上来保持游戏的性能一直处于一个较好的状态。因为——每一个神级优化的背后，都隐藏着一个2B的bug！ 那这次分享我就想从这样三个方面来聊一下我所理解的以“园丁”的角色来让团队进行继续优化的方式。 首先是美术资源的优化，美术资源是一个会对客户端运行效率产生非常重要影响元素之一，我想和大家聊一下，怎么样让我们团队可以在美术资源产出方面一直保持一个比较好的状态。然后我想回归到程序的本职工作，从一个更加宏观的角度来讨论一下程序代码的部分要怎么样去做一些优化，对于代码质量和一些底层模块的设计，如何去做一些提前的思考和设计。第三部分，团队开发效率的优化，这块可能看上去跟性能优化的题主没有直接的关系，但是在我看来优化不仅仅是游戏运行效率优化，而且应该包括整个团队开发效率的优化，这也是在项目开发过程中非常重要的部分。 1.美术资源优化 首先来看一下美术资源优化的部分。 大家在日常开发中可以感受到，对于美术资源部分，美术和程序，尤其是要进行性能优化的程序，他们的关注点会有不同。美术更多的是关注美术效果是不是足够的好，而程序可能更多的关注美术资源在设备上的运行效率是否可以达到目标帧率，这其中就有一些目标导向上的冲突。我认为，解决这一冲突的一个非常重要的手段是尽早来建立合理的美术资源制作规范。 这其实是一个美术资源制作的过程，首先通过在立项时制作Demo来确定适合的美术资源规范，然后通过给美术灌输效率意识、提供辅助工具让美术在制作资源的过程中可以严格按照规范进行，最后是要借助QA的力量对产出的资源进行检查，确保达标。 1.1 规范制定 为什么制定美术规范这么重要呢？我想借自己之前的一个项目中的真实经历来聊一下。 《无尽战区·觉醒》这款手游是我作为主程带领团队开发的第一款手游项目。当这个项目进行到中期的时候，收包了一批场景的资源。在使用这些资源的时候发现它们的运行效率明显偏低，Profile发现面数和DrawCall都超标了。面数很简单，使用减面工具进行减面即可，DrawCall也使用类似Unity的Static Batching的方式进行优化，但是结果发现一些场景的DrawCall只能从400减少到300多，并不能达到预期。仔细检查发现很多模型无法合并的原因是使用了端游常用的一种贴图制作方法——四方连续的贴图。这种方式虽然可以使用很小的贴图尺寸通过tiling制作出精细的效果，但是对于DrawCall敏感的手游来说并不合适。最终项目又请了好几个外派美术进行资源的整合和制作，导致多花费了几个周时间和几十万的美术成本。虽然对于大公司来说几十万的美术成本不算什么，但是我依然觉得这是我作为一个主程的失职——因为没有提前和美术沟通好制作方法，导致了这样的问题发生。所以在出来创业的项目中，在项目最初期Demo制作完毕之后，客户端团队就和美术一起制作了非常详细的美术资源制作规范。 制定规范的步骤大致可以整理为如下的几个部分： 在制定美术规范的第一步是要进行游戏信息的收集，因为不同的游戏类型以及镜头视角会对美术资源的制作产生非常大的影响，比如2.5D和3D自由视角的游戏是有不同的制作和优化方式。 在收集了足够的信息之后，需要和美术敲定一些大的技术方向，比如线性空间、HDR和动态光影等。线性空间是团队应当提前关注的一个点，我个人的观点是在写实的游戏风格中，能上线性空间还是尽量使用线性空间，对于美术效果的提升还是很有帮助的。在正确的基础上，才更容易出正确的效果。Unity目前的版本和OpenGL ES 3.0的普及率，个人观点是完全可以在移动平台上使用线性空间的。当然这也要考虑具体项目的内容以及开发团队成员的能力和经验等因素。HDR的开关在Unity中还是比较简单的，但在性能方面对于带宽的影响比较大，收益也不小，建议追求效果的团队提前考虑。动态光影是我们研发团队的美术特别想追求的一个效果，但是因为我们项目今年要上线，而且要考虑低配的效果，所以程序一直卡着不让场景使用全实时的动态阴影，而是尽量使用烘焙的方案。 确定目标机型就不多说了，考虑主流的高中低三档，建议提前购买一些设备方便后续的性能测试。我对于这三档的基本分类大致如下（这个也可以参考UWA测试中的机型选择）： 高档：大部分iOS设备，主流的安卓旗舰设备； 中档：少部分希望支持的iOS设备，比如目前的iPhone 5s、6/6s，ipad mini等，1-3年前的旗舰以及1-2年的1500元左右的安卓设备； 低档：1-3年前的千元机。 接下来是要针对面数和DrawCall这两个核心点，对整体的标准进行定义，并将这些标准分配到场景、角色、特效、UI等资源分类上。因为这些不同类型的资源是由不同的美术同学产出的，他们之间通常不会去沟通各自的性能消耗，因此需要针对每部分进行规则的细化。 场景部分强调一下贴图“像素密度”的概念。在制作场景的时候需要提前定义贴图使用的像素密度，否则会导致贴图精度过高或者不够等问题。所谓的“像素密度”是指在正常的游戏视角下，一个一立方米的Cube应当使用多少尺寸像素的贴图。这个在场景制作的初期应当定义好，美术才好去使用正确的贴图尺寸，否则到后期优化可能发现大量超标的贴图在使用，如果此时已经进行了贴图的合并，美术修改的工作量会非常大。 角色部分的规范需要把面数和DrawCall的标准细化到每个角色中，这时候需要和策划沟通期望的同屏显示人数，根据不同的游戏类型会有很大的差异。比如《崩坏3》不会同时有很多战斗单元，因此可以给每个角色非常高的精度，而像《御龙在天》这样的国战游戏，追求百人同屏的效果，对于每个角色的消耗限制就会比较大。 骨骼数量的部分最好在前期和美术沟通好，比如手指骨骼的减少和合并，脚部骨骼的简化，来减少CS骨骼的数量，给飘带等Bone骨骼留出足够的空间。因此美术规范制定的过程并不是单纯的程序给美术添加制作限制，而是一个和美术一起来讨论如何在有限的资源下制作出更好效果的过程。 UI制作规范是我们初期没有特别关注的部分，也因此踩了一些坑： UI组件数量较多导致加载顿卡。我们有些复杂界面有上千个GameObject，几百个UI组件，这是非常恐怖的。我们测试发现UI的Prefab加载的时间消耗在Unity 5.5.6上和UI组件的数量几乎成线性关系。这里补充一张我们测试的GameObject数量和加载时长的测试结果图（横轴是GameObject个数，纵轴是单纯的Prefab加载的时间消耗，单位秒，测试环境：小米Max2）： 因此建议其他团队提前考虑UI部分的异步加载和分块加载。（或者升级引擎到5.6.5+或者2017.3+，对于Prefab的加载速度有了很大的提升） UI中的粒子特效建议提前考虑异步加载，我们最初直接放在了UI的Prefab中，不但因为Unity不支持Prefab的嵌套导致维护麻烦，而且因为每一个ParticleSystem的初始化都占用一个几乎固定的时长（我们自己测试在PC上也大约有3ms左右），导致UI初始化的时候非常卡。使用一个间接的引用，就可以比较容易地做到既方便更新又可以异步加载。 最后针对标准，要进行真机的压力测试，并且在组内推广，形成大家都认可的美术资源制作规范。 1.2 美术资源制作 在确定好美术资源的制作规范之后，就是需要在美术铺量制作的阶段让美术可以按照规范进行资源的制作，在这个阶段程序需要注意去做的事情我觉得有两点： 帮助美术建立效率意识； 为美术提供便利的制作和检查工具。 在我们这样的一个初创团队里，没有经验丰富的TA存在，而且有部分美术同学之前来自外包团队，对于游戏运行时的效率意识比较薄弱，因此需要客户端程序同学不断和美术沟通来灌输对于游戏运行效率的关注。通常的做法包括日常的沟通、规范和技术点的分享等。 而在提供便利的工具方面，除了教会美术使用Unity已经提供的那些性能检查工具（Batches和面数查看、Overdraw、Mipmaps、Wireframe等渲染模式）之外，我们也为美术提供了非常多的开发和检查工具。 从截图中可以看到我们为美术提供了大量的工具，选择几个来着重介绍一下： 1.2.1 场景镜头同步功能 在场景资源制作的时候，需要美术去关注的除了常规的DrawCall之外，还有Overdraw、Mipmap以及面数。Unity已经在Scene View下提供了这三种渲染方式的检查工具，但是在我们的使用中发现，由于游戏运行时镜头规则的复杂和多变，导致美术在Scene View下无法准确判断是否存在资源不合理的情况。因此美术提出希望可以在Game View下查看这些状态。 最初的时候我们也是想在Game View下实现这些不同的渲染方式，并且已经集成了OverDraw的检测方式，基于的也是钱康来建议的Unite2017/OverdrawMonitor。但是后来觉得这种方式会影响美术对于游戏的操作，程序实现起来也要多花一些时间，比如Mipmap的实现效果就不是很满意。后来就想到另外一个思路——在Scene View下来做镜头和Game View下的镜头同步。结果就非常简单，只需要为Game View下的Camera身上添加一个Component就好： namespace ThorProfile { public class SyncSceneView : MonoBehaviour { #if UNITY_EDITOR private SceneView view = null; // Use this for initialization void Awake() { view = SceneView.lastActiveSceneView; } private void LateUpdate() { if (view != null) { view.LookAt(transform.position, transform.rotation, 0f); } } private void OnDestroy() { if (view != null) { view.LookAt(transform.position, transform.rotation, 5f); } } #endif } } 核心的代码只有LateUpdate中的那一句："view.LookAt(transform.position, transform.rotation, 0f);"，实现的效果就是正常操作游戏，Scene View下的Camera可以一直跟随移动，视角和Game View下非常接近。（有需要动态图自己使用上述代码进行试验即可。） 1.2.2 批量烘焙功能（补） Unity的烘焙在Unity 5.X的版本速度还是比较低，我们虽然为美术专门购买了CPU强劲的烘焙机，但是比如在制作大世界的时候，因为场景拆分得比较细所以需要一次性连续烘焙多个场景，于是为美术提供了批量烘焙的功能。之前在知乎上也有朋友问过类似问题，代码非常简单，直接贴一下，需要的自取好了： [MenuItem("美术工具/烘焙选中场景(同步)")] public static void BakeSelectedScenes() { Object[] selectedAsset = Selection.GetFiltered(typeof(SceneAsset), SelectionMode.DeepAssets); foreach (Object obj in selectedAsset) { string scenePath = AssetDatabase.GetAssetPath(obj); Debug.Log("开始烘焙场景: " + scenePath); EditorSceneManager.OpenScene(scenePath); Lightmapping.Bake(); EditorSceneManager.SaveOpenScenes(); //如果有更新Prefab的需求，可以放这里。 EditorSceneManager.SaveOpenScenes(); Debug.Log(scenePath + " 场景烘焙完成！"); } } 使用同步的方式烘焙，会卡住Unity，但是烘焙速度应该会有些提升（没有对比过……）。 1.2.3 场景合法性检查（补） 我们为美术添加了场景的合法性检查工具，因为我们对于场景中的相机设置等有些特别的要求。这块跟具体项目相关，只罗列一下我们在检查的内容以及要检查它们的原因。 顶点格式，对于带有color等逻辑上不需要的数据的部分给出警告。因为我们的场景是采用静态合批的，如果有资源不小心导入了color等不需要的顶点数据，会导致整个合并之后的mesh变大很多。 地形数据，我们使用了T2M的工作流程，美术使用Terrain进行地表的制作，然后通过T2M插件导出成mesh，为了保留原始的编辑数据便于以后修改和调整，Terrain会保留在原始场景里，但是在发布场景里不允许带有这块数据，即使Disable也不行，会对场景加载带来额外负担。 光源参数，不允许设置光源的ShadowType的Resolution属性，必须为Use Quality Settings。在做UWA的深度优化的时候，我们发现有些情况下ShadowMap的尺寸从8M增加到了32M，检查后发现是美术为了更好的效果自己调整了这个参数导致的。脱离程序高中低效果控制的配置是不被允许的。 物理碰撞体，在目前的手游上，我们的希望是尽量少地使用物理，因此我们无论是在寻路还是在动态阻挡方面都尽量少地使用物理，同样在场景里禁止摆放MeshCollider组件，对于其他类型的Collider组件也进行检查，对于我们项目通常都是不需要的。 摄像机设置，我们游戏中镜头是完全由程序逻辑控制的，因此不允许在场景中遗留用于预览的Camera组件。 就像之前说的那样，这部分非常零散，通常是在项目开发中不断发现的各种问题通过统一的检查工具来让美术在上传之前进行自查，确保资源提交到svn上的时候就是正确的。 1.3 美术资源检查 在美术大量产出资源的过程中，除了美术自查之外，还需要其他职位的同事同时对美术资源进行检查。 在我们项目中，对于美术资源的检查主要有三个部分： 程序检查。程序会根据发现的性能问题进行针对性的检查，比如会使用Profile、FrameDebug工具等进行问题的排查。 我们也是UWA产品的深度用户，购买了专业会员，几乎每个月都会提交一份安卓版本的包让UWA团队帮忙进行在线的性能诊断与优化，频繁的优化周期内可能会每周提交一份包。去年的年底也邀请UWA团队来公司针对我们项目进行了深度优化，发现和解决了不少美术资源的问题。 QA每周的性能测试。每周QA团队会在周版本之后，生成《性能测试报告》，以邮件的方式发送到全公司所有人的邮箱里。 这里以我们的《性能测试报告》为例来说一下QA团队进行性能监测的内容。 1.3.1 包体大小监控 我们经历过在测试上线之前要进行包体大小优化的情况，因此将包体大小的监控放到了每周的性能测试报告里。 我们会统计整体包体大小、资源占用大小、按照场景、UI、角色、特效等分类之后统计各自的大小，以及如果包体大小有变动，主要原因是什么。这部分使用的工具主要是基于打包时候产生的中间文件来统计分析。如下图所示，左侧是按照文件夹分类的列表，右侧是选中的节点下的所有文件，可以进行排序、关键字过滤，以及多选统计等操作。 在进行资源大小统计和分析方面，我们也开发了一套在Unity内的根据资源的引用关系进行分析的工具，统计对象为所有要打包出去的资源，可以查看资源之间的引用关系、被引用关系、资源消耗统计（粒子系统数量、GameObject数量等）。借助这个工具可以方便地进行一些不再需要的资源筛查等分析工作。 1.3.2 游戏帧率统计 另外一块需要持续关注的是游戏在设备上的运行帧率。 我们会分别在高配和低配机器上对战斗外、不同的战斗类型进行帧率的统计，并和之前的记录进行对比。 1.3.3 具体资源的检查 在具体资源的检查方面，由程序提供尽量简便的测试和统计工具，由QA进行自动化的检测，主要包括场景资源检查、场景合法性检查、技能特效检查、UWA GOT性能数据等部分。 场景部分主要是统计Batch和面数。我们制作了一个工具，按照填写的检查点在默认镜头下统计四个方向的数据，同时截图记录。这个工具也提供了跳转到指定坐标直接查看检查的功能。 前文已经介绍了场景的合法性的部分，这里是由QA每周进行一次合法性检查，对于不合法的场景反馈给美术进行修改。 特效检查主要集中在Drawcall、描述以及粒子数量这几个比较常规的方面，同样会列出不合格的特效让美术修改。 对于特效的Overdraw的统计，我们是针对技能进行的，因为技能释放过程中会有镜头的轨迹，因此这种方式更加合理。工具的功能是逐个释放技能，然后记录技能释放过程中最大的那帧Overdraw的数据和截图，方便美术排查。 我们也让QA团队在时间充裕的情况下使用UWA的GOT工具进行性能数据的记录，方便程序进行对比和检查。对于内存的统计数据也会从Overview测试中获取。 PPT截图的右下角是工具的制作者在我们组内的外号。标注在这里也说想说明这些工具的开发工作是整个团队一起来分工协作完成的，而不仅仅是分享者的功劳。 1.4 小结 第一部分的最后，我们做一下小结。经过规范制定、规范执行以及资源检查这些步骤，美术资源的优化就形成了一个闭环，这个闭环中的各个环节是由不同职位的同事协作来完成的，程序在其中起到了穿针引线的作用。在规范制定阶段，是由程序和美术主导，同时注意从策划和运营等处收集游戏设计的信息；在美术制作阶段的主角是美术，程序则为美术提供更好用的检查工具辅助美术自查；资源检查阶段由QA主导，程序负责提供便利的自动化检查工具，美术则需要对发现的问题进行修正，同时这个阶段的检查结果也可能反馈到规范制定的内容上，对一些规范细节可能会进行修正和调整。 2. 程序代码优化 在聊完美术资源的优化之后，我们回归到程序的部分，从一个比较宏观的角度来看一下程序代码的优化部分。 Donald Knuth在他的一篇文章里说： We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. 这段话是那句很有名的——“过早优化是万恶之源”的出处。首先我很赞同这句话，在没有必要的情况下进行盲目的优化浪费时间而且有可能有负面作用，但是正如这句话后半句所说，在少量的情况下，我们也不应该放过那些会产生严重影响的机会，而这些地方，往往是踩了坑之后才知道的，比如前文所说的美术大量使用四方连续的贴图这样的例子。所以这部分我想聊的主题内容着眼在“过早优化是万恶之源”的另一面，在程序中越早关注我觉得收益越高的部分。 底层模块。这点比较容易理解，即便是在做好层次划分的情况下，越底层的模块对于上层的影响越大，因此早期花费较多的时间和精力在重要的底层模块上，对于后期的优化可能会有意想不到的收益。 代码质量。我觉得保证团队的代码质量，对于优化和开发效率有着潜移默化的重要影响。 全员参与(补)。在一些团队里，优化主要由那么1-2个资深的同学主导进行，而在我们的团队里，优化的工作是一件全员参与的事情。 2.1 底层模块 首先来看下底层模块，这一部分的重要性不言而喻，我想举两个例子来说明一下它们设计得好坏对于我们项目的影响。 其中一个是我觉得我们做得比较好的地方——Lua与C#的职责划分，另外一个是我们踩了很多坑的资源管理模块。 2.1.1 Lua与C#的职责划分 Lua与C#的职责划分可以使用这样一张图来描述： 这里涉及到的语言有C#、Lua和C三种，在项目最初期，我们就决定将大部分的业务逻辑放在Lua层来做。这样做的原因和大部分使用Lua的项目一样——为了保证大部分的业务逻辑可以被Hotfix以及Patch更新。同时我们客户端团队有大量的Python脚本使用经验，因此对于Lua来说上手也是没有特别大的问题。 当决定了核心的业务逻辑存放位置的时候，数据的存放也就比较明了了。我们遵循的一个设计理念是——让数据尽量靠近它的使用者。 数据越靠近最终的使用者，中间需要进行转换的CPU和内存消耗就越小。因此我们使用Lua这种原生就支持数据存储的方式，即Lua Table的方式来存储客户端使用的数据。 接下来是网络部分。在项目最初的时候我也考虑过是否应当将网络放置在C#层，因为对于Unity引擎来说，C#是更加原生的语言，对于一些库的支持也特别方便。但是经过一些思考和讨论之后，我们决定将网络放在C层，这样做的原因和数据放置在Lua层是一样的。因为对于客户端来说，网络是数据的来源和发送者，相当于一个数据源的角色，因此也应该更加靠近它的使用者。不放在Lua层的原因是网络传输中还是有不少计算量的，加密、内存拷贝等，因此放在C层效率更高。同时我们在C层集成了一些Lua中缺少的扩展库。 选择放在C#层的部分有这么几种： 引擎功能，这个毋庸置疑，Unity本身就是通过C#来提供引擎接口的； 计算密集型逻辑，对于一些可能涉及到CPU消耗的计算密集的逻辑，封装成C#的接口供Lua调用； Tick逻辑，即一些需要持续Update的逻辑，放在C#层通过Component的Update函数来实现； 交互频繁的逻辑，一些需要Lua和C#频繁交互的逻辑放置在了C#层来实现，同样封装成接口供Lua调用。 这样的设计依据另外一个设计理念——将Unity作为引擎层来使用，让C#层尽量少地关注具体的业务逻辑。 在这样的设计下，C#和Lua之间的交互就非常清晰： C#通过tolua# warp出来的接口让Lua调用，Lua是逻辑的驱动者； C#提供每帧一次的Update/LateUpdate调用，具体内部需要的分发由Lua自己来做，大量的间隔逻辑通过Timer模块来实现，减少每帧的tick逻辑； C#对于Lua的感知仅仅是一些异步操作的回调，比如按键点击事件、异步加载完成的回调逻辑。 在这样的设计之下，UWA对我们项目进行深度测试的时候给出的测试报告结论如下： UWA团队告诉我们在他们测试的重度项目里，这个数据已经是非常好的了，我们项目在这块也只使用自己开发的调用次数统计工具进行常规的优化，在项目后期并没有花费太多的时间。 同事增练开发的调用次数统计工具 这里提一下跨语言编程的时候关于对象/资源生命周期的设计理念：谁创建谁销毁。 简单明了，如果一个对象是由Lua创建的，那它一定要由Lua来显示地负责销毁；而如果一个对象是由C#逻辑来创建的，那一定由C#来进行销毁。只有这样才能够避免生命周期错乱导致的泄露或者提前销毁的错误，对于泄露的检查也更加明了。 2.1.2 资源管理模块 我们项目中的资源管理模块是我觉得由于最初设计不够导致后期踩了很多坑的一个部分，其中一个表现就是每次游戏要进行测试上线之前都要花时间解决加载模块的各种奇怪问题。我们熬夜修复的问题有这么一些： 最初的版本因为一些迭代导致资源的引用计数存在问题，出现了Asset被卸载了但是又被使用的情况，再次尝试加载资源就报错了。后来又发现非常严重的内存泄露，表现是几乎所有的资源都残留在了内存=_=……为了修复泄露问题，我们将底层AssetBundle的管理从原来的Unload(false)修改为了Unload(true)，虽然解决了泄露问题，但是需要上层逻辑有一些迭代工作。后续我们还尝试处理同一个资源在异步加载过程中有同步加载请求的问题。 现在回头来看，对于资源管理模块可以进行反思的内容有如下几点： 最初的时候由于团队内对于Unity的经验不是很足，面对资源管理模块这个非常重要的部分，想法是借助比较成熟的开源框架来弥补经验上的缺失。所以在大致了解了基本原理之后，选择了KSFramework这套开源框架。它对于资源管理模块有一套基于Loader设计的封装，我们又根据自己的需求和发现的问题进行了一些迭代工作。在初期编辑器模式下，这套东西帮助我们快速建立了Demo和推进前期功能的开发，但是也隐藏了很多设备上的问题。这应当说是非常标准的技术债务，只是没想到需要付出这么高昂的利息。 在后期的维护中，因为技术团队的扩张和一些“不可抗力”的原因，这个模块先后经手三个负责人的维护，在交接以及讨论中因为理念不同也产生过一些设计上的误解，埋下了一些问题。 最后，因为编辑器模式下没有使用异步加载的方式，因此运行逻辑和设备上是不同的，导致很多异步的问题在真正进行大范围的真机测试的时候才暴露出来，需要在比较高压的条件下进行修复，带来了很多挑战。 最后，想说的一个点是——即使面临很大的压力，对于一些奇怪的问题，不要尝试用一些临时手段进行掩盖和容错的方式进行处理，而是尽量地去找到问题产生的根源，从根本上进行解决。 有时候在没搞清楚根本原因的情况下贸然通过“补洞”的方式来进行问题的修复，可能会把坑埋得更加深，让问题更难复现和排查。我在项目中就经历过这样的情况，也都是血与泪的教训。 2.2 代码质量 代码质量的重要性我依然想讲一个我在《无尽战区·觉醒》这样一款手游开发项目中的例子来进行说明。 在项目中后期，我们进行Python层性能优化的时候发现：dict这样的属性访问占用非常高。用过Python的同学可能都知道这是Python中进行属性访问的方式。排查调用源发现很多优化的点都是类似上面这样的局部变量的优化。（图中的代码只是示例，并非真实代码。） 而对于每一个入职的同事，在进入公司的Python课程里，都会学习到在脚本语言中尽量使用局部变量来进行性能优化的方法和原理。然而在真正的项目开发中，还是会有很多人忽略这种优化，这是代码质量偏低的一种表现。 在接下来的三四天时间里，我们不断地Profile、修改，对dict性能消耗比较大的地方使用局部变量的方式进行性能优化之后，整个脚本的性能有了大约10%-20%的性能提升。这是非常大的一个优化了，而且完全是无损的优化。如果我们的开发人员可以在日常的开发中就注意维护代码质量，对于这些优化时间的消耗就可以节省掉不少。 在我看来，在项目开发中可以提升程序团队的代码质量的方式包括如下几个方面： 针对性的培训和定期的技术分享。技术分享可能会花费挺多的时间，但是在时间相对宽松的研发期坚持进行技术分享还是会给团队带来有多正向的收益。我们一年多的创业时间内，技术分享大约做了十几场，虽然和大厂的分享相比不算很多，但是在促进团队技术进步、提升代码和设计质量等方面还是起到了很好的作用。 代码Review。也有不少人和我讨论过在团队内进行大范围的Code Review的可行性。首先Code Review对于提升代码质量肯定是有很大帮助的，但是从我个人的项目经验来说，要在手游这样一个需要快速开发迭代的团队里推行严格的Code Review代价还是非常大的。比如工作压力比较大的情况下，我们一个同事可能会在一天产出上千行的Lua代码，如果想要另外一个同样有这样大工作压力的同事抽出时间来进行完整的Review，几乎是一件不可能的事情。因此我们选择只在关键节点进行Review，包括核心代码和线上Bug修复代码，以及新同事入职的第一个月提交的代码。我们有过一次集体Review和迭代的过程，对于项目中会由多人共同维护的一段逻辑，大家都花时间进行迭代，然后分享自己迭代的思路。这种方式虽然会花费团队挺多时间，但是偶尔针对特定代码进行还是比较有效果的，可以统一大家对于关键部分代码的设计理念和使用方式。 静态分析工具。这块我们在使用的有LuaChecker和UnityEngineAnalyzer，针对代码进行检查，可以发现一些优化的点。 2.3 全员参与的优化（补） 我们客户端程序团队在进行优化的时候和一些团队不同的做法是大家都针对自己负责的部分进行优化。这样做和团队自身的特点有关，我们客户端团队对于一个创业团队来说算是经验和技术能力都不错的一个团队，每个成员都有多年的游戏开发经验。因此每一个同事都负责一些比较底层的模块，也会负责各个玩法系统的开发，是一个纵向的结构。总结起来，让所有同事都参与优化的好处主要有： 让团队中的每个人建立优化意识； 每个人作为自己负责模块的优化负责人； 组织专门的优化周期，横向对比，互相学习。 组织专门进行优化周期的Evernote记录 3. 团队开发效率优化 终于来到第三部分，也就是之前说的看上去和性能优化并没有直接关系的团队开发效率优化。 在聊这部分之前，我想让读者思考一个问题——我们为什么要做优化？ 是为了让游戏的运行更加流程？让游戏更加省流量？更省电？让游戏包体更小？这些都是我们进行优化的目标，但归根结底，我们做这些优化的目标都是——为玩家提供更好的游戏体验。 所以在我看来，如果一个优化，无论使用多么高超的技巧，如果它的优化结果无法直接或者间接地被玩家感受到，那这个优化可能就只是一个程序员的“自嗨”，无法为游戏提供真正的价值。反过来说，如果我们可以优化团队的开发效率，让团队有更多的时间来开发新的功能、制作更多的游戏细节，那对于游戏来说也是一种优化。 因此在我看来，进行团队效率的优化是一件非常重要的事情，也是程序的职责之一。我主要想从这样三个方面来聊一下如何进行团队开发效率的优化：工作流的构建、程序团队、策划团队。 3.1 工作流的构建 我觉得在项目中构建更好、更顺畅的工作流可以很大地提升整体团队的工作效率。我以我们团队现在一个功能的完成流程为例来分享一下我们团队使用的工作流。 策划提前和程序、美术沟通需求的可行性，在可行性确定之后，通过Redmine这样的管理软件提单，将需求详细地描述在任务单里； 我们在Redmine中集成了Webhook的功能，当有任务提出的时候，Redmine会通过钉钉的接口通知到对应的程序； 程序根据自己手头的工作安排进行排期和功能实现，当任务单完成并进行自测之后，会将代码提交到svn上，同时将Redmine上的单子修改为“已完成”的状态，状态的变更会同样通知到相应的策划和QA； SVN通过SVN hook的方式，自动触发Jenkins的Lua代码编译指令，Jenkins调用我们部署在公司内网的一套分布式打包服务，进行脚本编译。我们团队中只有程序有Lua代码的svn访问权限，其他职位统一使用编译好的Lua bytes code。 当打包完成之后，分布式的打包服务会调用钉钉接口将完成消息通知到特定的群里。 策划需要进行导表、更新服务器，或者QA同事需要进行安卓/iOS打包的时候，都是通过Jenkins进行请求，Jenkins继续调用分布式打包服务进行打包，并将结果通知到群里。 Jenkins上的部分服务 对于Jenkins部分，提醒一下要做好权限控制，对于其他职位可能需要的，尽量避免参数式的执行方式，而是以多个任务的方式提供。而程序部分则可以尽量灵活地使用参数进行构建。对于发布版本打包、分支创建等功能，通过权限控制不要让策划/美术/QA误操作点击到。 我们的分布式打包服务是基于Python构建的，通过简单的RPC服务进行内网跨机器的互联，通过argparse模块进行参数化的提供，方便扩展： def ParseArgs(args): parser = argparse.ArgumentParser(description = 'Build App') parser.add_argument('-p', '--platform', choices=('android', 'ios',), required = True) parser.add_argument('-c', '--channel') #all, xiaomi parser.add_argument('-b', '--build-type', choices=('dev', 'pub',), default="dev") parser.add_argument('-sp', '--spmark') parser.add_argument('-hm', '--headmark') parser.add_argument('--non-sdo-server', action = 'store_true') parser.add_argument('--nopatch', action = 'store_true') parser.add_argument('--onlyab', action = 'store_true') parser.add_argument('--uwashipping', action = 'store_true') #单独为uwa测试准备的发布参数，临时添加 parser.add_argument('--make-base', action = 'store_true') parser.add_argument('--il2cpp', action = 'store_true') parser.add_argument('-xp', '--xcode-profile-type', choices=('development', 'addhoc', 'appstore',), default="development") buildArgs = parser.parse_args(args) 我觉得这样的工作流的好处主要有： 程序将更多的事情推出去，交给工具，自己可以更加专注在程序开发的工作上； 其他职位拥有更多的自主权，在不需要程序参与的情况下可以完成自己的很多工作； 通过钉钉这样的IM的通知功能，将轮询的消息变成通知，不再需要等待和关注Jenkins任务的完成进度，完成之后自然就会收到通知。 3.2 提高程序开发效率 这块基本都在PPT里了，不赘述了，其中调试工具部分再次推荐一下：Hdg Remote Debug这样的设备调试工具，关于Lua的部分在3月份的博客中已经说得非常详细了，也不再重复。 3.3 策划工作效率优化 策划工作效率的优化部分想讲两个切身经历的事情。一个是非常小的一个优化，帮助策划实现NPC坐标从Unity中拷贝到Excel中。 我们因为开发周期比较紧，而且服务器需要一些NPC的位置数据做验证，因此没有在Unity内部为策划实现NPC编辑器，而是需要策划手动去Excel表里填写。这里就有一个填写坐标的过程，最初的时候策划手动填写非常费时间，而且容易出错，后来帮助他们实现了一个点击GameObject节点拷贝坐标到粘贴板的功能，策划使用后表示极大地提升了填写NPC表格的工作效率。 有时候程序只需要通过很简单的代码就可以帮助其他职位的同事解决一些工作中的痛点，提高工作效率。 第二件事情是之前在大公司工作的时候的一个亲身经历。当时在带新人做mini项目，一个新人策划就在公司的KM知识分享平台上提了一个问题——他表示现在的策划填表的工作效率很低，需要经历这样几个复杂的步骤： 在Excel中编辑数据，然后提交到SVN上，通过导表将数据转换成程序代码读取的资源，然后更新服务器，更新客户端，启动客户端连接服务器才能查看结果，这些步骤要花费大约10-20分钟的时间。他问能否编写完数据之后就可以直接在游戏内看到结果？ 当时的我作为自以为在游戏行业已经有几年工作经验的“过来人”，看到新人策划有这样的疑问，心里其实是有一些嘲讽的。所以去“耐心”地回复他：对于客户端来说，可以做到本地导表然后不重启客户端就可以直接Reload数据查看结果，但是如果你不把数据上传到svn上，服务器如何知道你本地修改的结果？这就像那样一个笑话： “是这样的，张总, 您在家里的电脑上按了ctrl+c，然后在公司的电脑上再按ctrl+v是肯定不行的。即使同一篇文章也不行。不不，多贵的电脑都不行。” 这个笑话后来的结果是自己成了一个笑话，因为虽然时代的发展，网络硬盘等云服务的普及，也有了跨电脑进行粘贴拷贝的功能……张总不再需要很贵的电脑就可以实现自己的操作。 这个故事的发展和这个笑话有些相似，在大约半年之后，我和工作室的另外一个同事将rpyc这样一套中间件引入公司并基于它实现了跨进程的外挂式编辑框架。基于这套框架就实现了策划在编辑器内编辑完数据，只需要点击重载数据的按钮，就可以自动更新本地的客户端和指定ip的一台服务器中的数据，不再需要提交到svn，甚至不需要重启客户端就可以看到修改之后的结果。 经过样的改进之后，之前需要10-20分钟左右时间的操作，现在只需要2-5秒就可以实现，极大地提升了策划的工作效率。我和那位同事也因此拿了当年公司内部的技术分享奖。 这个故事对于我的触动还蛮大的，因为最初我所嘲讽的一个新人的想法，最终由我和另外的一个同事一起进行了实现，这对于我来说也是一种讽刺。因此在之后我再听到策划或者其他职位的一些看上去“异想天开”的想法的时候，不会急于反驳或者指出其中的漏洞，而是先想想是否自己的思路被自己了解的技术所禁锢，是否有别的方式可以真的实现这些想法。 通过这两个故事我想表达一个观点，对于程序在团队效率优化方面应当承担什么样的角色？借用《蜘蛛侠1》里非常有名的那句话来说——能力越大，责任越大。 因为程序是整个团队中最了解技术和开发的人，也最有能力开发一些工具或者引入一些方法让整个团队的工作效率得到提升，因此也应该肩负起相应的责任。 4. 总结 最后，我们聊了这么多，进行一些总结。 我在游戏行业里也做了五六年，特别是自己在创业的这一年多的时间，让我更加深切地感受到游戏开发非常符合这样的冰山理论。 浮在冰面上的这一部分是玩家可以感受到的游戏内容，比如精致的美术资源、有趣的玩法，而在这之下，有更多无法被玩家直接感受到的内容，比如被迭代掉的玩法。而今天我们所聊的这些优化的内容，比如美术规范、代码质量、团队的工作流构建，它们大都是水面以下，无法被玩家切身感受到的部分。但它们又是如此地重要，是整座冰山不可或缺的一部分。 就像我之前所说，通过刚才的分享大家应该也可以感受到，这些优化的内容非常的琐碎繁杂，就像散布在各个地方的一个又一个点，是团队的协作让这些点可以连接成线，形成类似于美术资源的规范制定、规范执行和规范检查这样的闭环，而在整个游戏的开发周期过程中通过团队持之以恒地去做这些事情，让这些线连接成一张大网，将水聚拢在周围凝结成冰，托起了整座冰山，使得海面上可以被玩家感受到的内容越来越多，这就是我眼中基于团队的持续优化之道。 最后的最后，我还是想把我在上次分享中也说过的一句话送给大家。 这一年多的创业经历让我更加深刻地体会到游戏开发是一件艰难而且辛苦的事情，有一些朋友或者同事也找我聊作为一个程序进行游戏开发的迷茫，我自己内心也曾有过彷徨和纠结。因为很多事情太过琐碎，带给我们的成就感可能也会偏低。但是我也发现，现在做过几年游戏行业之后，依然留在游戏行业中的人心中都有着对于游戏发自肺腑的热爱和激情。它们或从小就喜欢游戏，或曾经被游戏感动过心中最为柔软的那个部分，在游戏行业内坚持做这些着看似平凡的工作。 所以，我想把这句话送给所有依然在坚持的游戏开发者们——不忘初心，不愧平凡，相信通过团队的协作和坚持不懈的努力，可以给这份平凡以不凡！ 谢谢！ 这是侑虎科技第396篇文章，感谢作者贾伟昊供稿。欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者个人主页：https://zhuanlan.zhihu.com/p/36930662，同时，作者也是U Sparkle活动参与者哦，UWA欢迎更多开发朋友加入U Sparkle开发者计划，这个舞台有你更精彩！
UWA GOT v1.1 | 支持本地管理深度测评、全新的UWA API、兼容Unity 2017.3
UWA GOT是UWA研发的本地性能优化工具，自推出以来，收到了大量游戏开发团队的关注和反馈。今天推出的UWA GOT新版本兼容了Unity 2017.3、添加了全新的UWA API、还支持在Unity Editor下直接上传项目进行在线深度评测的功能，在不断完善功能的同时，我们也希望能给大家带来更友好的操作体验。 一、Editor下直接上传项目 相较于UWA GOT的本地测试，UWA性能诊断与优化是UWA提供的在线性能评测模式，它能提供更丰富的数据检测和更深度的性能分析。在最新的UWA GOT中，用户可以直接在Editor中将项目的测试包提交到UWA官网（www.uwa4d.com），并在本地浏览器内查看报告内容。 具体操作流程如下： 1）通过UWA GOT菜单中的OnlineSubmit即可打开登录界面： 2）登陆后即会显示账号的项目列表以及各个项目的当前状态，所有项目按照是否为管理员权限进行了分组。 3）在拥有管理员权限的项目列表中，点击某项目后即可进入以下的项目提交界面。 4）在该界面上选择机型、上传安装包、填写测试要求以及验证码后，即可进行项目提交。同时，登录UWA官网也可查询到项目目前的测试情况，并且在24小时内即可得到深度的性能分析报告。 5）点击“Done”，即可在本地浏览器查看报告。 二、全新的UWA API 在最新版本的UWA GOT中，我们添加了新的UWA API，研发团队可完全通过脚本控制测试流程。对于VR游戏或应用，可尝试自定义UI按钮来控制测试的开启和关闭。 UWAEngine.StaticInit public static void StaticInit(); 该函数可用于：通过脚本动态挂载UWA_Android，而不需要提前手动将UWA_Android的Prefab放入场景。 UWAEngine.Start public static void Start(Mode mode) 该函数可用于：通过脚本动态开启指定类型的测试，而不需要手动点击右上角的UI按钮。 UWAEngine.Stop public static void Stop() 该函数可用于：通过脚本动态关闭当前的测试，而不需要手动点击Stop按钮。 说明： 1）目前所有的UWA API（包括之前的UWAEngine.Push/PopSample）都添加了条件编译：[Conditional("ENABLE_PROFILER")]。因此，当发布为非Development版本时，UWA API不会参与编译，从而对运行效率无任何影响。 2）UWAEngine.Start/Stop在一次游戏运行中只有第一次调用会生效，无法反复使用。 三、其他更新 1）新版本针对Unity 2017.3做了兼容方面的优化。 2）由于目前UWA GOT暂不支持IL2CPP，该版本在SDK集成界面中增加了当前Script Backend的提示，并支持一键切换到Mono版本。（在此特别感谢用户Ellan为我们提出的宝贵建议） 以上即为最新版本的UWA GOT功能介绍，小编建议大家及时在官网下载最新版本，以体验更完整的功能。 关于UWA GOT UWA GOT 全称 Game Optimization Toolkit，是由侑虎科技研发的本地性能测评工具。它既能方便研发团队在本地直接进行真机测试，也支持在Unity Editor中搭建本地服务器来接收和分析性能数据。整个测试过程中，研发团队能自定义测试时间和流程，几分钟后即可查看性能数据，同时还提供深度的数据分析，如脚本中的主要函数耗时、堆内存的具体分配堆栈等高级分析功能，从此为大家的项目性能又增添一份安全堡垒！ UWA GOT 目前提供 Performance Overview、Mono Memory Analysis 和 Runtime Asset Tracker三大功能。 关于UWA GOT更多功能请查看：搞定这些问题，一个插件就够了！
UWA GOT是UWA研发的本地性能优化工具，自推出以来，收到了不少游戏开发团队的关注和反馈，感谢大家的支持，目前我们也一直在对该工具的新功能进行紧锣密鼓地更新中。在最新的UWA GOT中，我们不仅提升本地服务器的数据导入速度，降低内存占用，还增加了两大实用的功能： 支持UWA API，用于检测自定义代码段CPU耗时 支持导出CSV文件 一、支持UWA API 为了更快速地定位CPU的瓶颈所在，新版本中提供了UWA API，能够帮助开发人员统计自定义代码段CPU耗时，该功能仅在Overview模式下起效。API的具体含义及用法如下： UWAEngine.PushSample/PopSample public static void PushSample(string sampleName); public static void PopSample(); 参数sampleName表示自定义的函数标签，UWA Engine会对PushSample和PopSample之间的代码段统计CPU开销，并在UWA GOT中的统计面板中进行显示，该API支持嵌套调用。其具体用法如下： UWAEngine.PushSample(“MyCode”); // some code … UWAEngine.PopSample(); 最终在Overview界面中，可以看到自定义的函数标签，及其具体耗时（下图中A~E都是自定义函数标签）。 注意事项：请确保PushSample和PopSample是成对使用的。如果两者之间使用了return语句提前退出代码段（或者在协程中使用yield return提前跳出代码段），则会造成PushSample和PopSample的配对不准确，从而导致数据错误。 另外，请注意在同一帧中PushSample和PopSample的调用次数不宜过多。初步统计，在中低端的设备上，10000次的调用会导致接近50ms的额外开销。 二、支持CSV文件导出 为了方便用户进行数据的二次统计和处理，新版本的UWA GOT中提供了数据的CSV导出功能，可以导出当前面板中的显示数据。 在任意模式下，界面的最下方可以看到一个“Save as CSV”的按钮，如下图所示： 以下是上图中Shader资源的一个导出示例。生成的CSV格式如下，其中的Depth表示在面板中的层级次序，在Mono和Overview模式下可用于还原函数的调用关系。 以上即为最新版本的UWA GOT功能介绍，小编建议大家及时在官网下载最新版本，以体验更完整的功能。 https://www.uwa4d.com/index.html#download 关于UWA GOT UWA GOT 全称 Game Optimization Toolkit，是由侑虎科技研发的本地性能测评工具。它既能方便研发团队在本地直接进行真机测试，也支持在Unity Editor中搭建本地服务器来接收和分析性能数据。整个测试过程中，研发团队能自定义测试时间和流程，几分钟后即可查看性能数据，同时还提供深度的数据分析，如脚本中的主要函数耗时、堆内存的具体分配堆栈等高级分析功能，从此为大家的项目性能又增添一份安全堡垒！ UWA GOT 目前提供 Performance Overview、Mono Memory Analysis 和 Runtime Asset Tracker三大功能。 关于UWA GOT更多功能请查看：搞定这些问题，一个插件就够了！
这是一篇着重介绍如何写好Lua代码，并做好优化工作的技术文章。文中笔者并没有介绍那些通俗的优化方法，而是首先总结了自己在优化过程中常见的性能问题，并通过实际优化案例的分析，来为大家提供更为实在的参考。 大家好，我是舒航，现任职于心动网络，主要负责《仙境传说RO：守护永恒的爱》（以下简称“RO”）的优化工作。今天主要想和大家分享一下，我这段时间在优化Lua的CPU性能方面的一些经验。 上一篇文章主要讲的是写一个LuaProfiler的思路，即如何快速定位到内存和CPU问题。而这一篇则着重于介绍如何写好Lua代码，以及优化的一些经验。在我优化RO和隔壁项目组手游的CPU性能时，发现CPU性能问题非常个性化，两个项目遇到的问题截然不同。而通用的Lua性能优化的Tips，往往并不能解决问题。所以这篇文章的前半部分是我总结的优化过程中常见的性能问题，后半部分是实际优化案例的一个分析，希望这样的形式更能帮到大家。 常见优化 首先需要说明的是，在我们项目中用的是Lua5.3，并且没有用到LuaJIT。 常见优化主要是参考了Lua作者Roberto Ierusalimschy所写的Lua性能优化建议： https://www.lua.org/gems/sample.pdf 一、使用局部变量 这个点是绝大部分Lua性能问题产生的原因，尤其是和一个频繁调用的函数组合，会产生大量的开销。总的来说，我们在一个频繁调用的函数内，应该避免重复地去查询Table，应该把Table内我们常用的值缓存起来。 这里的值通常有两种: 1）函数 2）对象 虽然Lua是first-class，但是我发现实际使用中，大家意识上还是认为函数有别于其他值。所以分开来列举，方便大家对号入座。 使用局部变量是优化Lua性能的一条重要准则。 不管是函数还是对象，不管是Lua自身的库，还是我们自己写的，在Lua中都是储存在_G中的某个节点下。当我们调用一个函数/对象的时候，Lua首先会去他们的表中查找到这个函数/对象，而我们使用局部变量的话，由于Lua的局部变量是储存在寄存器(这里的寄存器不对应CPU的寄存器)内的，所以这个访问就会快很多。更严重的是，如果使用的是父类的函数/对象，还会触发__index，这样不仅会有额外的耗时还有内存开销。 对于不熟悉Lua的开发者来说，容易忽视局部变量的使用，而有些时候这会造成比较严重的性能问题。比较常见的就是写UI的同学在一个定时器或者一个循环内，使用GetComponent来获取组件然后进行一些操作。由于没有用变量缓存起来，每次用完了就抛弃，再用再获取。这不仅会造成很大的耗时，还会造成很大的内存开销，而写UI的工作又容易被大家所忽视，所以目前为止，我发现大部分项目这一块都有优化的空间。 这里提供了一段缓存Lua函数的代码(摘自Roberto原文): function memoize(f) local mem = {} setmetatable(mem, { __mode = "kv" }) return function(x) local r = mem[x] if r == nil then r = f(x) mem[x] = r end return r end end 二、使用对象池 现在大部分团队，在使用Lua的时候，一般只用来写一些UI界面。而UI界面经常交给项目组的新人来做，以熟悉项目代码，所以这里的性能问题很容易被大家所忽视。这里经常遇到的性能问题一般出现在复杂滚动列表的重用、消息协议的重用、以及C#对象的重用中。 在我帮隔壁项目组优化性能的时候，就发现一个有趣的现象。他们项目Lua使用的不重度，但是平均每帧分配内存3.4KB，而RO高峰时期的内存分配也只有0.5KB，是RO的17倍。每帧Lua耗时在0.47ms，是RO的20%-30%。总体来说，对于普通使用Lua的团队来说这里面已经有很大的性能问题了，只是总量比较少，影响不是那么大。 而造成这么大性能问题的原因只有两个，一个是在一个定时器内频繁地给GameObject设位置，一个是每隔几帧收发一条移动消息。最后优化成，设位置时避免了重复创建LuaVector3，移动消息做了个简单的对象池。 优化了这两条以后，不管是每帧分配内存，还是CPU耗时都降低了50%。而在所有的项目中，除了使用局部变量，其他优化中最显著的就是使用对象池了。因为正确地使用对象池能有效地减少创建新对象的过程，在Lua中也就是newindex这个函数的耗时明显减少。newindex减少了以后，不仅避免了频繁开辟回收内存，还能避免额外的CPU开销。尤其是使用对象池缓存一个经常复用的对象时，这样的优化更明显，比如说LuaVector3、GameObject、滚动列表中的对象。 三、字符串拼接使用table.concat 这个问题在很多优化Tips里经常提到，相信大家也不陌生。究其原因，是因为Lua的String是内部复用的，当我们创建字符串的时候，Lua首先会检查内部是否已经有相同的字符串了，如果有直接返回一个引用，如果没有才创建。这使得Lua中String的比较和赋值非常地快速，因为只要比较引用是否相等、或者直接赋值引用就可以了。而另一方面这个机制使得Lua在拼接字符串上开销比较大，因为在一些直接持有字符串buffer的语言中，往往可以直接把后者的buffer插入到前者的buffer的末尾，而Lua先要遍历查找是否有相同的字符串，然后会把后者整个拷贝然后拼接。 尽管是这样，我们还是有一些手段来优化Lua的字符串拼接，使用table.concat来代替。通过查看Lua的源码可以发现，这里的原理主要是table.concat只会创建一块buffer，然后在此拼接所有的字符串，实际上是在用table模拟buffer；而用“..”来连接则每次都会产生一串新的字符串，开辟一块新的buffer。这个问题在《Programming in Lua》一书也有提及： local buff = "" for line in io.lines() do buff = buff .. line .. "\n" end -- 这段代码读取一个350kb大小文件的时候需要约1分钟，同时会移动（开辟后又被回收）50GB内存 这个问题确实真实出现过，我有一次发现的一个性能问题就是因为写聊天界面的同学，每次都会用旧的聊天字符串，拼接上新增的聊天字符串得到一串新的聊天字符串。以致于当聊天信息过猛的时候，只要几分钟内存就会爆炸。但是相比较前面两点，这个问题对内存的影响更大，对CPU耗时的影响比较小。 四、优化配置表 --如果polyline内有一百万个点，则下面的表结构会占用95kb内存 polyline = { {x = 1, y = 2}, {x = 3, y = 4}, {x = 5, y = 6}, ... } --这样优化可以减少到65kb --只是我们不得不使用polyline[1][3]来代替比较直观的polyline[1].x了。 polyline = { {1, 2}, {3, 4}, {5, 6}, ... } --这样优化最终可以减少到24kb --我们必须使用polyline.x[2]来代替polyline[2].x polyline = { x = {1, 3, 5 ... }, y = {2, 4, 6 ... } } 对于我们RO来说，table配置表就占了几十兆的内存，每一点小的优化都能节省很多内存空间。通过平时定义配置表的谨慎小心，再配合之前UWA的这篇文章Lua配置表存储优化方案，能给项目带来非常可观的优化空间。 五、细枝末节 以下是一些比较小的优化，但确实出现过。所以列举出来供大家参考，另外如果想写出完美的Lua代码的话强烈建议读一下Roberto的原文。 1）使用闭包代替loadstring() loadstring(string.format("return %d", i)) function oploadstring (k) return function () return k end end loadstring总是会动态地编译传入的字符串，而这个操作是非常耗时的。有时候我们可以使用闭包的形式来达到一样的效果，而且开销更小。 2）使用pairs代替next next总是从第一个非nil值开始遍历，直到找到一个非nil值，在删除一个表或者一个足够空的表时，很慢。使用next删除一个10万个元素的表时，耗时约为20秒。而使用pairs仅需要0.04秒。 3）不要在for循环中创建表和闭包 4） 重用coroutine --lua中的coroutine本身是不支持重用机制的，但是我们可以使用一些小技巧来实现它。 co = coroutine.create(function (f) while f do f = coroutine.yield(f()) end end) 5）用C实现一些功能 如果性能实在吃紧，但是有苦于不能使用LuaJIT，可以用C实现一些功能，这样可以使模块的性能可优化的空间大大增加。需要注意的是，使用C越多，则LuaJIT能优化的越少，这两者是不能叠加的。目前我们项目中并没有使用到C来实现一些模块，一来是纯用Lua性能并没有那么吃紧，二来是如果使用C来实现一些功能就失去了使用Lua的意义。反正都不能热更，那还不如直接用C#实现。只要小心一些，用Lua还是用C写不会是性能的瓶颈。 实际案例分析 广度搜索的Lua写法优化 通用的优化经验相信大家都有所了解，这里再给大家分享一段我自己写Lua代码的优化过程。这段代码是求解一个无向图中的最短路径问题。在一个773个顶点，812条边的无向图中，求任意一点到原点(一个特殊点，即local origin = 10000)的最短路径。这个无向图储存在一个table中，每个顶点有个全局ID作为key，children记录了该顶点的邻接点，示意如下： graph = { [ID] = { children = { ID1, ID2, ID3 } }, [ID1] = { ... }, [ID2] = { ... }, ... } 原始代码： local origin = 10000 function Util.BFS(fpIdx) local visited = {} local Q = {} local firstNode = BFSNode.new(nil, fpIdx) Q[#Q + 1] = firstNode visited[fpIdx] = firstNode while #Q > 0 do local node = Q[1] table.remove(Q, 1) local v = graph[node.ID] for i=1, #v.children do local id = v.children[i] if id == origin then local bfsNode = BFSNode.new(node, id) bfsNode:Log() return bfsNode:GetPath() end if not visited[id] then local bfsNode = BFSNode.new(node, id) visited[id] = bfsNode Q[#Q + 1] = bfsNode end end end return nil end 一、第一次优化 由于我自作聪明，为了提供Log和GetPath这些方法。将节点包装成了一个“BFSNode类”的形式，事实上是存在了一个table内。但是这部分的内存开销和CPU耗时都是非常大的，第一次优化主要就是将这个地方简化了。简化的代码应该是大家实现BFS算法的常用版本了，CPU耗时和内存开销都有所下降了。优化后性能数据如下: local origin = 10000 local visited = {} local queue = {} local remove = table.removoe local node local id = 0 function Util.BFS(fpIdx) visited = {} queue = {} queue[#queue + 1] = fpIdx visited[fpIdx] = -1 while #queue > 0 do node = remove(queue, 1) local v = graph[node] for i=1, #v.children do id = v.children[i] if id == origin then return Util.GetPath(visited, id, node) end if not visited[id] then visited[id] = node Q[#Q + 1] = id end end end return nil end function Util.GetPath(visited, targetIdx, parentIdx) local path = { targetIdx } while parentIdx > 0 do path[#path + 1] = parentIdx parentIdx = visited[parentIdx] end return path end 二、第二次优化 第一次优化后，CPU平均耗时1ms，平均内存开销1.4KB都处于可接受的范围了，但是这个代码还是有优化空间的。因为上面代码中visited没有得到复用，我们可以用一段简单的代码复用visited。这次优化做到了773次寻路总内存开销在740KB，visited和Q的开销约为50KB，其余的约700KB内存全部用于构造Path数组，此外没有内存浪费了。如果还需要优化的话就比较难了，不是一些简单的代码能实现的。而对于我们实际应用中，优化到这种程度已经完全够用了。优化后的代码和数据如下： local noneIdx = 0 local invaildIdx = -1 local origin = 10000 local node local id = 0 local g = graph local GetPath = Util.GetPath local ReuseTable = Util.ReuseTable local head = 0 local last = 1 function Util.BFS(fpIdx) head = 0 last = 1 Q[last] = fpIdx visited[fpIdx] = noneIdx while last > head do head = head + 1 node = Q[head] local v = g[node] for i=1, #v.children do id = v.children[i] if id == origin then local path = GetPath(visited, id, node) ReuseTable(visited) return path end if not visited[id] or visited[id] == invaildIdx then visited[id] = node last = last + 1 Q[last] = id end end end ReuseTable(visited) return nil end function Util.GetPath(visited, targetIdx, parentIdx) local path = { targetIdx } while parentIdx > 0 do path[#path + 1] = parentIdx parentIdx = visited[parentIdx] end return path end function Util.ReuseTable(t) for k,_ in pairs(t) do t[k] = invaildIdx end end 三、了解SLua和C#交互 上面的优化示例局限于Lua的写法和用法层面，虽然这是项目中常见的一类优化点，但是我们的Unity项目中，Lua优化还有另一个大头——Lua和C#交互。而要优化这部分，就先要明白Lua和C#交互时发生了什么。因为笔者用的是SLua，所以这里分析Lua和C#交互就以SLua为例了。其中obj为Lua持有的一个C#的GameObject对象，当我们执行以下代码的时候，就产生了两次Lua和C#交互。 local pos = obj.transform.position 这次Lua和C#交互的过程中，全部的流程如上表。简述一下流程就是: 1）取得元表中存储的C#函数指针 2）调用C#函数 3）返回结果 以我的经验来说，LuaCPU性能往往是和内存的开辟密不可分的。所以我们关注的重点在第三步，怎么返回结果的。SLua有一个缓存机制，已经返回过的对象会缓存在一个Map中，并且有一个唯一的索引作为Key。返回的对象会先在C#的Map中查找，如果找到了则返回这个索引。然后用这个索引去Lua的Table中查找对应的值，这个值就是最后返回给Lua的值。这个值是一个Table，其中的元表包含了所有导出的C#函数。而值类型，因为它性质的特殊，每次返回都会新生成一个索引。也就是C#里多缓存一次，Lua里多缓存一个Table。所以从Lua中访问C#中的值类型时，要注意Lua内存的开销。除去常见的Vector2/3/4、Quaternion、Color等常用的值类型做了特殊处理，不经过缓存，其他的值类型都要注意这一点。 不仅是取值，还有设值的时候也要非常小心，上面提到的我们隔壁项目组遇到的问题就类似下面这行代码，在每帧调用的Update里创建LuaVector3。因为调用这个函数的时候，C#中取LuaVector3是分别取的这个Table下标为1\2\3的三个值包装为C#的Vector3，然后设给position。所以当设完值以后这个LuaVector3由于没有任何人引用(虽然看起来是复制给了一个对象)，就会被回收掉，每一帧都会新建一个Table然后回收一个Table。 function Test:Update() obj.transform.position = LuaVector3.New(0, 0, 0) end 这是侑虎科技第299篇原创文章，感谢作者舒航供稿，欢迎转发分享，未经作者授权请勿转载。当然，如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 作者也是U Sparkle活动参与者，UWA欢迎更多开发朋友加入U Sparkle开发者计划，这个舞台有你更精彩！
【求知探新】《为谁而炼金》UI界面加载性能分析
【求知探新】是UWA新推出的栏目。在UWA团队做性能优化的过程中，常常会遇到一些未知的问题，在这里我们将分享UWA研究这些问题的完整过程。当然需要说明的是，一个好的问题没有标准的答案，在此也欢迎大家来积极拍砖！ 今天分享的内容，来自UWA对DeNA《为谁而炼金》手游的UI界面加载性能分析。 一、问题描述 研发团队反馈部分UI界面的交互反应速度较慢，具体操作如下图：点击主页面（左图）的“英灵”按钮，UI界面会进入过渡界面（中图），最终进入“英灵”界面（右图），整个过程中打开速度较慢，大约4秒钟。 二、问题复现 我们在红米Note2（低端）和三星S6（中高端）设备上分别进行了尝试，点击该按钮后，UI界面出现确实较慢（4秒钟左右）。 三、问题分析 我们首先通过Profiler来进行测试，下图为红米Note2设备上的性能分析。我们发现，在点击“英灵”按钮后，UI开启过程中主要经过两个阶段：红框部分为资源加载期，蓝框部分为UI界面加载期。可以看到，虽然都是Loading.UpdatePreloading函数在加载，但其每帧的开销均很小，在蓝框期间，游戏的总耗时更是小于33ms。这说明研发团队对于此处的UI界面加载为异步加载。 上图为资源加载期的CPU耗时，下图为UI界面加载期的CPU耗时 对此，我们的建议如下： （1） 加载切换由于是异步加载，且每帧的CPU利用率过低，因此，建议研发团队对特定UI尝试同步加载来减少UI的切换时间； （2） 同步加载可能会造成切换界面运行得“不流畅”，对此，研发团队也可以考虑加大每帧异步加载的资源数量，从而减少红框和蓝框处的等待时间。 同时，我们在UWA性能报告中发现了每次点击UI后，均有规律性的单帧高开销（Profiler中存在耗时卡顿，但未发现该具体耗时项，我们推测是Profiler统计时出现了跳帧问题），如下图红框所示。经过定位，其52%的开销来自于Resources.UnloadUnusedAssets API，如下图性能堆栈中的GarbageCollectAssetsProfiler所示。对此，建议研发团队尽可能避免频繁的Resources.UnloadUnusedAssets，根据资源用量每5~10分钟调用一次即可。 四、结论 该UI界面加载耗时较大主要有两方面组成： （1）大量资源的异步加载，导致每帧CPU利用率较低； （2）Resources.UnloadUnusedAssets API的不合理调用。 对此，建议研发团队根据第三节中的详细说明进行进一步地检测和完善。 文末，感谢DeNA对于UWA团队的认可，并同意将以上研究过程中的性能数据在此分享。同时，我们也欢迎大家在UWA问答社区（answer.uwa4d.com）积极提交研发过程中遇到的问题。也许随着时间的流逝，科技的进步，答案将变得廉价，但问题会变得更有价值，因为提问和研究将比回答更有力量。 PS：《为谁而炼金》是一款富有壮大的故事情节以及高度的战略性的RPG战棋游戏，喜欢这款手游的童鞋们可以在官方网站上预约啦：http://fwg.mobage.cn/
Hello, 各位Unity开发者，无论您是正用着UWA的工具、还是阅读着UWA的相关技术文章亦或是在UWA博客中寻找一个问题的解答，我们都倍感荣幸能和你在此相遇。本篇可以说是UWA产品服务的向导，认真读完它，您会了解UWA究竟是做什么的，并且如何利用UWA提供的资源和工具。在与UWA的日益接触中，您会发现它不仅能为你们项目的性能保驾护航，还能少走很多弯路，缩短优化的时间。 Part 1. 优化工具 Q：UWA提供哪些可以快速提升性能的工具？ 这个要从UWA的定义说起，UWA由侑虎科技开发的游戏/VR应用性能优化平台，目前提供 1）线上性能测评 2）UWA GOT 3）UWA GOT Online 三大工具。下面我们将分别介绍。 一、线上性能测评 1、性能检测与优化 通过真人真机对项目测试，提供针对性的性能报告和优化方案，既能从宏观上分析项目运行时的整体趋势，包括总体CPU性能、GC调用频率和引擎各模块性能等，也能从微观上深入剖析内存管理、资源使用以及逻辑代码在运行时的效率瓶颈。研发团队通过对报告提供的方案进行针对性的优化，能在短期内提升项目的运行效率。 入手方法：该服务主要通过在UWA官网上提交项目的测试包（apk、ipa等，无需工程源码）来完成。由UWA工作人员测试分析后，会提供针对该项目的性能报告和分析建议，可以在此参考报告demo。 该工具分为免费版和收费版，免费版一个月可以使用一次。强烈推荐大家来试试免费版哦！ 2、资源检测与分析 反馈资源的打包情况，包括具体包含的资源情况、详细的依赖关系和资源的冗余程度等。用户通过优化报告指出的冗余问题来实现减小包体、减小内存并提升加载速度的目的。 入手指南：用户在UWA官网上传AssetBundle包（无需游戏包），一旦上传，UWA将自动开启检测并返回结果，一般时长在30分钟以内（视AB包体量而定），可以在此参考报告demo。 该工具永久免费使用，无使用次数限制。 二、本地性能测试工具：UWA GOT UWA GOT 全称 Game Optimization Toolkit，是由侑虎科技研发的本地性能测评工具。它既能方便研发团队在本地直接进行真机测试，也支持在Unity Editor中搭建本地服务器来接收和分析性能数据。整个测试过程中，研发团队能自定义测试时间和流程，几分钟后即可查看性能数据，同时还提供深度的数据分析，如脚本中的主要函数耗时、堆内存的具体分配堆栈等高级分析功能，从此为大家的项目性能又增添一份安全堡垒！ 具体功能说明：戳此传送门 入手指南：戳此传送门 价格：RMB399，包括维护和更新 三、UWA GOT Online UWA GOT Online是基于其本地测评模式上的线上同步功能，研发团队可以将本地测评的数据快速同步到UWA官网，生成在线测评报告。除了“本地化”和“快”这两大天然优势，我们目前提供了四大性能检测模式：总体性能检测、Mono堆内存分析、运行时资源检测和Lua性能分析来满足大家的优化需求。 具体功能说明：戳此传送门 入手指南：戳此传送门 Part 2. 干货大全 一、引擎把握篇 性能卡顿、内存泄露、资源冗余...这些开发中遇到的疑难杂症大部分都能在这里找到解决方法，它们无不通过大量的项目深度优化和周而反复的测试验证。如果你想避开一些不必要的坑，请务必啃下这些硬货！ 1.1 Unity性能优化进无止境——CPU篇 从渲染、UI、加载等几个重要维度分析了目前大部分项目中存在的CPU性能问题和解决方案 1.2 Unity性能优化进无止境之内存篇（基础篇） 谈优化必谈内存，该文深入浅出地分析了Unity项目的内存占用 1.3 Unity性能优化进无止境之内存篇（进阶篇） 内存泄露、Mono无效堆内存、资源冗余...你关注的内存问题都在这里！ 1.4 AssetBundle打包机制详解（5.x） （4.x 版本的API接口有所差别，请看这里） 1.5 AssetBundle内存管理机制 AssetBundle真是个绝妙的设计，但是想说爱你不容易！ 1.6 Profiler中常见的高CPU耗时函数 1.7 代码热更新（dll反射） 亘古不变的话题了 1.8 加载模块详解（纹理篇） 加载龟速？是时候来点加速度了！ 1.9 加载模块详解（网格篇） 1.10 加载模块详解（Shader篇） 1.11 加载模块详解（动画资源） 二、提升：工欲善其事，必先利其器 在这里不仅有效果拔群、性能出众的插件帮助大家省心省力，也有卧虎藏龙的民间高手抛钻引玉给大家分享自己的优化技巧。 1、UI相关 2.1.1 UGUI 降低填充率技巧两则 2.1.2 如何优化UGUI的ScrollRect 2.1.3 UGUI表情系统解决方案 2.1.4 UGUI 文本多语言方案 2、制作相关 2.2.1 毛发及眼球的渲染技术 2.2.2 游戏中的角色渲染技术之皮肤篇 2.2.3 卡通渲染及其相关技术总结 2.2.4 Unity移动端动态阴影总结 2.2.5 合并Shader系列 | 如何合并渲染状态 2.2.6 如何打造高逼格耀斑效果 2.2.7 Unity线性空间下移动设备上烘焙变暗问题处理 2.2.8 Unity 5烘培场景带来的“爱”与“愁” 2.2.9 GPU Skinning 加速骨骼动画 2.2.10 如何精简Unity中使用的字体文件 2.2.11 使用MaterialPropertyBlock来替换Material属性操作 2.2.12 Unity图片优化神器 - Dither算法进阶方案 2.2.13 布料系统原理浅析和在Unity手游中的应用 2.2.14 聊聊Unity里的嵌套Prefab 2.2.15 如何实现更真实的软阴影效果 2.2.16 开启HDR时，半透明渲染画面异常的解决方案 2.2.17 使用Fast Shadow Receiver优化渲染效率 2.2.18 Unity+ iPhone X适配方案 2.2.19 Unity手游开发札记——使用Shader进行UGUI的优化 2.2.20 iOS下拆分Unity图集的透明通道（不用TP） 2.2.21 Journey中的沙丘渲染及其Shader实现 2.2.22 扩展Unity模型编辑器 2.2.23 如何在Unity中实现Stochastic Screen Space Reflection效果 2.2.24 高效的iPhone X适配技术方案（UGUI和NGUI） 2.2.25 如何大幅优化NGUI的堆内存分配 2.2.26 如何在Unity中实现Stochastic Screen Space Reflection效果 2.2.27 基于Unity引擎的Bent Normal实现 2.2.28 Unity移动平台下的烘焙使用及优化 2.2.29 基于2D SDF的体积字实现 2.2.30 Unity如何实现投影阴影效果 2.2.31 Unity引擎后处理性能优化方案解析 2.2.32 利用多进程并行化加速Unity资源构建 2.2.33 Unity引擎iOS增量编译方法解析 3、插件&工具相关 2.3.1 插件 | 那些你不得不用的好插件！——工具篇 2.3.2 插件 | 你的项目和大作只差一个插件的距离！——美术效果篇 2.3.3 插件 | 好插件让你事半功倍！——资源篇 2.3.4 技巧 | Unity中Avatar换装实现 2.3.5 插件 | Unity一键图集生成工具，附源码 (基于NGUI和TexturePacker) 2.3.6 插件 | Unity游戏界面解决方案: PSD For UGUI 2.3.7 插件 | ShaderForge插件分享 2.3.8 技巧 | ScreenSpaceShadowMask Blur推荐 2.3.9 插件 | USequencer系列 |初识 2.3.10 插件 | USequencer 准确显示瞬时事件和持续事件 2.3.11 插件 | USequencer 缤纷事件、事件间吸附 2.3.12 屏幕特效插件分析 | FxPro & Beautify 2.3.13 屏幕后处理特效性能分析—运动模糊 2.3.14 Unity插件开发基础—浅谈序列化系统 2.3.15 如何高效定位Unity安卓开发中的闪退问题 2.3.16 ACT技能编辑器的制作经验分享 4、内存相关 2.4.1 Lua性能优化—Lua内存优化 2.4.2 用好Lua+Unity，让性能飞起来——Lua与C#交互篇 2.4.3 用好Lua+Unity，让性能飞起来—LuaJIT性能坑详解 2.4.4 Unity项目常见Lua解决方案性能比较 2.4.5 Lua配置表存储优化方案 2.4.6 Unity匿名函数的堆内存优化 2.4.7 给调皮的AssetBundle加上面向对象式加载调试管理 2.4.8 Unity 5.x AssetBundle零冗余解决方案 2.4.9 AssetBundle进阶内存优化（Unity 4.x） 2.4.10 巧用UWA资源检测，轻松剔除冗余资源！ 2.4.11 听说你的项目中仅这些资源就卡成了翔？看看他们怎么做！ 2.4.12 Lua的CPU开销性能优化 2.4.13 我所理解的委托和匿名函数 5、研发实录 2.5.1 我们用50次游戏性能的深度优化，总结出了五条“毒鸡汤” 2.5.2 Unity手游开发札记—大世界动态加载实战 2.5.3 Unity手游iOS内存分析和测试 2.5.4 Unity 协程运行时的监控和优化 2.5.5 Unity 游戏的String interning优化 2.5.6 Unity动画文件优化探究 2.5.7 动画重定向技术分析及其在Unity中的应用 2.5.8 VR交互动画短片《拾梦老人》的开发经历 2.5.9 独立游戏《Abi》场景耗时分析 2.5.10 Optimize GameObject选项对骨骼Transform的影响 2.5.11 《为谁而炼金》UI界面加载性能分析 2.5.12 Unity中ShaderLab内存优化 2.5.13 使用UWA GOT优化Unity性能和内存 2.5.14 客户端配置文件优化策略 2.5.15 Unity中字体名对依赖关系的影响 2.5.16 《小米超神》技术总监王啸予：重度MOBA的优化之路 2.5.17 基于团队的持续优化之道 2.5.18 《Exploring in UE4》多线程机制详解 6、行业参考 2.6.1 UWA发布 | 2017 Unity手游体检蓝皮书 — ARPG篇 2.6.2 UWA发布 | 2017 Unity手游体检蓝皮书 — MMORPG篇 2.6.3 UWA发布 | 2017 Unity手游体检蓝皮书 — MOBA篇 2.6.4 Unity版本使用情况统计报告 7、案例精讲 2.7.1 UWA助力独立游戏开发！《江湖X：汉家江湖》性能诊断精讲！ 2.7.2 一次MMO手游的性能诊断全纪录 2.7.3 RPG |《星辰奇缘》测评精讲 2.7.4 ARPG|《王者之剑2》性能数据精讲 2.7.5 RPG |《仙剑奇侠传3D回合》性能精讲 2.7.6 ACT |《艾尔战记》性能精讲 2.7.7 MMO |《六龙争霸》测评精讲 2.7.8 FPS |《全民枪战》测评精讲 2.7.10 FPS手游《战地先锋》性能案例精讲 2.7.11 MOBA手游《小米超神》案例精讲 2.7.12 UWA官方Demo新增《小米超神》，全面揭秘重度手游的性能表现！ 8、学术论文 2.8.1 基于功耗预算的实时绘制 2.8.2 微表面模型－PBR渲染管线的材质 2.8.3 基于物理的渲染—更精确的微表面分布函数GGX 2.8.4 基于物理的渲染—基于球面调和基的实时全局光照明 2.8.5 基于物理的渲染—基于Haar小波基的实时全局光照明 2.8.6 基于物理的渲染－用真实的环境光照亮物体 2.8.7 基于物理的渲染—HDR Tone Mapping 2.8.8 CG中的深度学习 |Siggraph 2017 相关论文总结 Part 3. UWA 问答社区 所有的已知，在未知面前不过是冰山一角。遇到问题怎么办，投石问路！正所谓不学不问，不问不知。一个高质量的提问是解决问题的一半。如果你真心想解决某个问题，不妨和大家一起来讨论下这些话题吧。 3.1 资源放到resource下面有什么优势和劣势 3.2 如何降低动画文件的浮点数精度 3.3 AssetBundle 粒度规划 3.4 数据模板表打包以及加载方式怎么做可以兼顾加载速度和热更新表结构 3.5 资源引用最后导致应用包体大小的疑惑 3.6 AssetBundle未加载完使用错误 3.7 项目初期如何确定美术规范 ... UWA问答社区地址：answer.uwa4d.com 关于我们的官方QQ群（465082844） 如果你也想做出高颜值的代码，如果你也期待能让项目走向性能巅峰，如果你本人也乐于分享从善如流，欢迎加入UWA群；与其闭门造车不如群策群力；比起形而上学，我们更乐于解决实际问题。 Part 4. UWA 自学素材 UWA不仅举办了多次技术优化日，也积极活跃在网络。我们尽己所能，将技术的触角传递到每个角落。以下就是我们留下的足迹： 4.1 8.10 直播回顾 | 一个小时内，我们用UWA GOT解决了这些问题... 4.2 UWA 六月直播季 | 6.29 Unity UI模块中的优化案例精讲 4.3 UWA 六月直播季 | 6.22 Unity中动画系统性能优化方案回顾 4.4 UWA 六月直播季 | 6.15 Unity引擎渲染效率全解析 4.5 UWA 六月直播季 | 6.8 移动游戏加载性能和内存管理全解析 4.6 5.4 UWA直播回顾：Unity游戏的代码堆内存优化 4.7 NGUI性能优化技巧（上） 4.8 NGUI性能优化技巧（下） 4.9 UGUI美术制作——如何实现次时代美术效果 4.10 UGUI性能优化技巧 4.11 请让我用一小时，将你的项目性能提升一个高度！ 4.12 UWA优化日厦门站回顾 | 资深TA教你如何将艺术概念转化为渲染代码 4.13 不喧哗，自有声—记UWA优化日深圳站（附完整技术PPT） 4.14 记一次冷雨寒风日的UWA优化日（内附技术PPT） 4.15 You should be here ！| 附UWA优化日技术PPT Part 5. 虚幻引擎学习之路 我们会将UWA内部学习过的Unreal相关技术文档进行总结和整理，并陆续发布，计划发布两大系列文章，第一个系列是功能篇，主要讲述我们学习过程中所了解到的Unreal引擎功能特色；第二个系列是性能篇，重点分析Unreal引擎在多个平台（特别是移动平台）上的性能效率。 5.1 虚幻引擎学习之路：渲染模块之光照系统 5.2 虚幻引擎学习之路：渲染模块之材质系统 5.3 虚幻引擎学习之路：相机图像后处理 5.4 虚幻引擎学习之路：渲染模块之全局光照明 5.5 虚幻引擎学习之路：动画模块之基础篇 5.6 虚幻引擎学习之路：动画模块进阶篇之动画融合 5.7 虚幻引擎学习之路：动画模块进阶篇（二） 5.8 虚幻引擎学习之路：粒子系统篇（一） 5.9 虚幻引擎学习之路：粒子系统篇（二） 5.10 虚幻引擎学习之路：新粒子系统Niagara 5.11 虚幻引擎学习之路：基于Unreal引擎的大地形加载研究 与其他世面文章不同的是，我们会在讲解中揉入和Unity引擎相似功能的对照说明，以方便大家更快地对其进行理解和掌握。这里，需要郑重说明的是，我们之所以这么做并不是为了比较Unreal引擎和Unity引擎谁更强大，二者都是当今世界上伟大的引擎。我们这么做是因为发现这可以极大地提高对于Unreal引擎的学习效率。 到这里便是UWA迄今为止所有在Blog以及微信上推送的技术博文汇总，我们将其梳理，是为了让一些经典好文能够重现，让知识能常流通，同时也是为了方便大家日后查找。本篇文章作为UWA Blog里的目录将随时更新，建议大家Mark起来，有空没空随手刷上个两篇，结合自身的研发项目多做测试加深理解，UWA相信一定事半功倍。 最后也要感谢所有向UWA供稿的开发者，感谢你们将这么棒的想法分享出来，从而帮助到更多的研发团队。当然如果你也有好的想法或者研发路上的心得体验，也可以分享给我们哦（support@uwa4d.com），知识的百科全书需要你我共筑。