您的位置:首页 > 移动开发 > Unity3D

学习unity的一些经验和见解

2015-08-05 16:59 363 查看
个人认为Unity相比于其他引擎易用性较好的原因主要有:

基于组件(Component)的关卡设计更符合直觉

Unity通过将系统或用户的脚本抽象为可重用的组件(Component)并附着在游戏对象上来控制游戏的行为。相较于传统的基于脚本的开发方式,关卡设计师可以更灵活、更快速地搭建界面和关卡,有一种“搭积木”的感觉。虽然这种设计牺牲了一部分扩展性(例如难以实现嵌套Prefab),但对初学者来讲是非常友好的。

虚幻引擎4.7版本的更新也效仿Unity,向组件化的方向靠拢,使关卡结构更容易理解和维护。

使用Mono作为脚本运行的平台

C#/Mono相比C++和其他脚本语言,有更好的稳定性和抽象能力,有容易使用的.NET框架和易于移植的各类开源库,相对完善的语言服务(如GC和反射)也使开发复杂逻辑容易许多。虽然降低了门槛的同时也让低质量的代码更容易产生,但不管对于初学者还是老鸟来说,我觉得都是利大于弊的。

引擎本身的功能相对简单 + 丰富的Asset Store插件

和虚幻等超级引擎相比,Unity提供的功能算是非常基础的,组件数量和各组件可配置的内容都不多,所以在学习的时候更容易产生比较直观的感受,不至于迷失在细节当中。另一方面,Asset Store模式的成功造就了大量功能强大的第三方插件,填补了Unity开发中的各种空白,进一步降低了开发门槛。

而难于精通方面,我觉得主要原因在于:

对综合能力要求高

首先,不仅对Unity,对任何游戏引擎或者对游戏开发本身来说,想要精通都是很难的一件事。因为游戏客户端开发本身是一项综合性非常强的活动,整合的技术非常多,例如:

建模
关卡制作
脚本逻辑
网络通信
平台特性集成
动画制作
特效制作
工作流集成
调试和优化

而对于将这些技术粘合在一起的Unity工程师来说,虽然不需要精通方方面面,但将团队成员的工作高效率高质量地结合在一起,也是非常考验其能力的,只有长期地,全方面地参与整个开发过程并了解团队成员的工作方式,才能逐渐成长为一名优秀的Unity工程师。

举例来说,Unity工程师需要:

与设计和美术团队沟通,评估设计对于游戏性能的影响,实现原型,进行各种性能测试。
与服务器工程师沟通,确定互相之间的接口和协议内容的细节。
Unity本身没有一个Gameplay Framework,场景管理、游戏数据管理等相对底层的框架都需要Unity工程师来搭建,如何减少团队中其他成员的错误实践,是主程序的责任,也很考验其架构能力和对Unity Runtime的理解。
一些特效需要自己编写Shader才能实现。
动画师和美术团队产生的资产,根据项目的需要常常要自己写Pipeline来导入和进行优化处理。一部分还需要以合适的方式打包,供客户端增量下载,需要对Unity的资源管线有较好的理解才可以。而且看似简单的决定之中,常常蕴含了很多性能上的考量。
根据关卡设计师的需要,制作编辑器扩展工具,提高其工作效率。这方面的文档稀少,同时也需要对Unity独特的序列化机制有比较深的理解才行。
想利用iOS和Android以及各种平台特有的功能时,需要针对特定的平台编写一些Native插件,如本地Push通知,自定义系统键盘,系统弹窗等等。要懂一些iOS开发和Android开发的知识才能驾驭。

有些做Unity的工程师可能只是搭建关卡,写一些控制脚本,而优秀的Unity工程师的价值往往在于其能够承担更多的团队职责。所以说想要精通这些,真的需要付出很多的时间和努力才行。

难在细节

任何事想精通,细节都是非常重要的,比如:

内存管理

避免和排查脚本中的内存泄漏。例如没有清空的委托和静态闭包造成的引用。
优化GC,了解Mono和.NET GC算法的差别。比如Unity采用的是较老版本的Boehm GC,不分世代,GC分页为1KB,内存碎片无法合并,单线程,缺少LOH,托管堆一旦扩大就很难向系统返还内存。如果按照.NET的思路优化GC,有时候并没有理想的效果。而Unity最近引入的IL2CPP运行时,采用了新版本的Boehm GC,算法又有所改变,优化策略也应适当调整。
了解Unity的内存模型,哪些资源分配在Native堆上,哪些分配在Mono的托管堆上。Native上不同种类的资源分别用哪些方法能够释放干净。Native堆上的引用计数是如何工作的。如何减轻Unity自动释放场景时的压力。AssetBundle的内存结构是怎么样的,各个部分如何不依赖GC而精准地释放。
暂时隐藏起来的图片或物体如何暂时性地释放,在显示时又如何重新加载回内存。
了解哪些API和操作会分配内存,什么时候使用值类型更好或更不好。这都需要对C#有很深入的理解。

网络和下载

AssetBundle如何打包,什么样的图片用什么粒度打包效率最高。
如果使用Json反序列化数据,怎么才能避免内存碎片降低整个App的性能。
不断更新的素材需要增量下载,Unity内置的下载方式瓶颈在哪,怎么自己实现一个比Unity内置API更高效的下载机制和更精准的缓存控制机制。

脚本执行

能够将较复杂的操作(如反序列化数据,较重的IO操作)放在后台线程执行,再调度回主线程更新游戏界面,以避免UI卡顿。Unity的线程优先级又是怎样的?
能否理解Unity Coroutine的迭代器本质,怎样对Coroutine中的异常进行处理,如何使Coroutine具有返回值,Coroutine启动时将分配多少内存,为什么复杂的Coroutine会使用更多的内存,如何将多个Coroutine合并为一个从而消除内存分配。
在与Native插件(如iOS插件)交互时,如何让C#与Objective C或Java共享内存,从而减少大型数据封送造成的CPU负担。
iOS平台上AOT异常的根本原因是什么,有哪几种。值类型和泛型的组合更容易引发AOT异常的原因是什么,如何绕过。怎么安全的使用Linq,使用C#标准事件为什么会触发AOT异常,怎样避免。Mono AOT的trampoline又是什么,哪种风格的代码更容易耗尽trampoline并引发AOT异常。
Unity的C#编译器有哪些弱点,哪些代码通过Visual Studio编译成DLL再放到Unity中可以提高执行效率和内存使用效率。
场景加载缓慢等Unity内部表现出来的问题如何从自身脚本下手进行优化。或者说不同性能问题在自己代码中的Critical Path都是哪些部分。

渲染

Draw Call是什么。不同AssetBundle中的小图片如何Batch到一个Draw Call中。
移动平台常用的Tile-based GPU有哪些弱点,如何避免。
Retina等高清屏幕上制作2D游戏时,如何动态为图片生成最小的Mesh网格以节约fill rate。

团队协作

自己编写Gameplay框架的情况下,如何控制队友代码中的内存泄漏。
使用版本管理系统时会产生哪些难以解决的冲突,如何建立开发规则。自己开发的框架或工具是否能有效避免队友间发生冲突。
如何实现资源管线的自动化。
如何将各种奇葩动画编辑器的输出转换成Unity标准的动画资源。
制作编辑器扩展时,是否能正确序列化复杂的数据结构。能否让自己的工具和脚本也实现所见即所得,让队友更快的搭建场景。
是否会使用Gizmo和Handle来扩展场景编辑器。
当不得不修改MonoBehaviour的定义时,怎样让已经上线的老版本中的数据正确地反序列化到新版本。
要有能力编写模块划分明确,依赖关系合理清晰的可重用代码和组件,作为公司的资产加速新项目的开发。

这些都是Unity开发中会不断面对的问题,如果不能从始至终中控制住这些细节,积累起来往往会使团队效率底下,难以产出高质量的应用。

所以我觉得Unity难以精通之处就在于对细节知识的把握,以及在整合团队价值的过程中如何做到扬长避短。Unity开发团队中常常不是所有人都会这个引擎有很深刻的认识,大家术业有专攻,有人搭场景,有人做后端,对自己不熟悉的领域,难免有错误的认知和实践。我们尚可以通过时间和努力精通细节,然而到头来真正缺少的,其实是团队成员间的信任所带来的沟通成本的降低。

【更新】

回答一些朋友的疑问。

关于资料来源

细节问题很难有系统的资料来源,而像AssetBundle Dynamic Batch这种几乎找不到答案的问题只能自己慢慢摸索。在此列举几个最主要的知识来源。

官方手册。例如搜索Unity Optimization可以找到官方在GPU,CPU和Mobile方面的几篇优化手册。官方资料常常包含最核心也最容易被忽略的原则,深入理解往往会有新的收获。
官方博客。博客上不时会有一些技术类文章,如关于IL2CPP和序列化机制的知识几乎只能看那几篇博客。Technology
Unite的视频和Slide。对某些领域有比较深入的探讨,特别在内存管理,AssetBundle和代码组织方面。YouTube和SlideShare上搜Unite或Unity能找到。值得注意的是Unite的分会场,如日本和韩国,有时会有一些更加深入的分析。如 slideshare.net
的页面
Unity的Mono fork:Unity-Technologies/mono · GitHub ,关于GC和AOT方面是第一手的资料。比如gc的配置,Enumerable类的实现如何导致Linq容易触发AOT异常,以及泛型CompareExchange中存在的JIT
Hack导致C#事件和一些线程同步操作触发AOT异常等等。另外GC方面也可以参考ivmai/bdwgc · GitHub ,有详细的机制解释。
UI的源代码:Unity-Technologies / UI ,修改优化后可以直接集成到游戏中,很方便。
随Unity安装的PDB调试文件。Unity的安装目录下其实是有Editor和Player当中所有C++源码的PDB文件的,而且居然都是private PDB。需要探查Unity内部数据结构和过程实现的时候,通过WinDbg配合这些PDB文件调试Unity进程可以获得很多最底层的信息。当然,如果公司买了Unity的源码就不必这样麻烦了。

关于优化策略

不只是Unity,优化程序最重要的原则就是先测量。而且在没有丰富经验和自信的情况下,不要自己写测量代码,而要依赖Unity自己Profiler和Profiler API。这里只说一些Unity特有的内容。

使用Profiler时,切忌猜测。一定要弄清各种数据的精确涵义,如Self %,Self ms,GC Alloc等,如果弄不清楚,优化常常是南辕北辙。另外诊断CPU Spike时,一定要打开Deep Profile,否则只能看到误导性很强的表面数据。找到真正的Hot Line才能着手优化改善性能。

当Hot Line在自己的代码中时,可以尝试将CPU密集的操作分派到后台线程,然后在需要与Unity API交互时调度回来。粒度较好的操作可以尝试用Coroutine分派到其他帧分别执行。大量GC Alloc造成的Spike需要重新设计内存分配策略,小对象(目前版本是小于1KB)较多的时候也可以尝试预先扩大托管堆(如分配很多小于1KB的缓冲区,然后再释放),这样可以加速后续内存分配。因为Boehm GC的堆扩展策略是时间线性而非空间线性的,所以每次扩大后的容量都是翻倍的,需要注意。

而当Hot Line在Unity API中时,常常是自己的错误实践造成的,需要重新审视设计。一方面要减少昂贵API的调用次数,一方面要降低Unity内部处理数据的规模。例如场景加载缓慢时,可能需要简化场景本身,然后在场景启动后再手动、增量地加载场景中的其他内容。

另外要了解一些底层知识。例如App启动时性能较差的原因,在非AOT平台上可能是因为大量的JIT编译造成的,而在AOT平台上则可能是因为初始化代码过于复杂导致CPU缓存命中率很低,和操作系统频繁地Page Fault。这也是为什么启动代码一定要精简,并且要尽量实现批处理。

GPU方面,如果没有复杂的特效,瓶颈常常在Draw Call和Fill Rate上。Draw Call需要Batch,能共享的材质一定要共享。Fill Rate的问题通常在高分辨率的2D游戏中比较明显,Profiler中的Transparency渲染占比很大时就应该着手优化。GPU优化策略上Unity相比其他引擎并没有很多特异的方面,准确测量的基础上通常能找到比较通用的解决方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: