Unity程序员要注意的编码规范
Unity程序员如何写好代码,写代码的过程中要注意的哪些些点,今天给大家分享一些经验规则,通过遵守这些规则作出明智的架构决策,确保更高的团队开发效率和稳定的代码。
避免抽象类
我们在开发中经常喜欢抽象,其实抽象得过程中往往会产生设计过度和抽象过度,而这些抽象得代码可能会令人难以理解,让人不容易看出整体得代码脉络,尤其是你团队的新进成员,而且这通常会让分析和讨论变得更加困难。这类型的代码通常会促生更多代码,导致更长的编译时间(包括有更多IL去翻译的IL2CPP build),而且最终的代码可能会更慢。
对啦!这里有个游戏开发交流小组里面聚集了一帮热爱学习游戏的零基础小白,也有一些正在从事游戏开发的技术大佬,欢迎你来交流学习。
理解Unity main Loop, Update与FixedUpdate
我们做游戏的时候要理解游戏引擎中的mainLoop,要对Unity的帧(玩家)循环有着足够的理解。例如,知道Awake,OnEnable,Update和其他函数是什么时候被调用的相当重要。同时要知道游戏开发中的动画,运动,都是是基于帧率来进行驱动。你可以在Unity文档中找到更多信息。
Update和FixedUpdate之间的差别极其重要。FixedUpdate方法被项目的Fixed Timestep值控制。该值默认为0.02(50Hz)。
这意味着Unity保证FixedUpdate方法每秒调用50次,因而一帧中有多次调用是可能的。如果你的帧数下降了,这个问题可能会更加严重,因为一帧中FixedUpdate调用的次数变多了。这将可能导致我们平时称之为“死亡螺旋”的恶性循环发生,因为它有时会展现出严重故障的形态。
物理系统更新是FixedUpdate是一部分,所以有大量物理内容的游戏可能会在这个地方挣扎。一些项目也使用FixedUpdate来运行各种游戏系统,因此请小心地检查及避免这类性能问题的发生。由于这些原因,我们强烈推荐你将项目的Fixed Timestep值设置成与你的目标帧数高度接近的数值。
选择合适的帧数
大家做游戏的时候经常会遇到一个问题,就是帧率很流畅能到60FPS但是手机发烫的厉害,不一会电就没有了。这种情况说明了虽然手机的CPU+GPU拼尽全力能绘制到60FPS,但是CPU占用率会比较高,有些游戏,或者有些手机配置可以适当的降低60FPS的标准,选择一个合适的目标帧数。举例来讲,移动项目常常需要在流畅的帧数和电池消耗及过热降频之间取得一个平衡。以60 FPS运行一个大量占用CPU和/或GPU资源的游戏会导致更短的续航时间和更快的过热。对于大部分项目来说,以30 FPS为目标是一个完全合情合理的妥协。你也可以考虑通过在运行中修改Application.targetFrameRate属性动态调整帧数。
在Unity 2019.3 beta开始支持的全新的On-Demand Rendering功能可以让你降低渲染的频率同时保证其他系统,如输入系统,不受影响。
在脚本或动画中没有将帧率考虑进去很常见。不要假设更新时间是一个常量,在Update中用Time.deltaTime并在FixedUpdate中用Time.fixedDeltaTime。
避免同步加载
有时候我们开发游戏的时候,发现某个时刻帧率降到了1,然后后面又恢复了。通常是因为把加载资源放Unity主线程中同步执行。因此,将你的游戏设计成健壮的异步加载来避免这些性能高峰与卡死。使用AssetBundle.LoadFromFileAsync()和AssetBundle.LoadAssetAsync()而不是AssetBundle.LoadFromFile()和AssetBundle.LoadAsset()。
将你的项目设计成异步的还有其他的好处。用户交互可以保持流畅。与服务器的认证和交换过程可以与场景和资源的加载同时进行,因而减少整体的启动和加载时间。完全异步的设计还意味着将来将你的项目移植到全新的Addressable Asset System会更加简单。
使用预分配对象池
内存池技术在游戏中非常常见,比如游戏战斗,京城会重复的创建与销毁大量的对象,当对象频繁创建和摧毁的时候(合适例子是子弹或是NPC),使用回收重用的预分配对象池。尽管每次重用重置对象的特定组件可能会消耗一定的CPU,这仍然比新建一个对象要节省性能。这次方式同样极大地减少了你项目中托管内存分配的次数。
尽可能减少使用标准behavior方法
所有自定义的行为都继承于一个定义了Update()、Awake()、Start()以及其他方法的抽象类。如果一个行为不需要用到这些方法(完整的列表可以在Unity官方文档中找到),请完全移除它而不是留着一个空函数体。否则这个空函数将仍被Unity调用。这些方法会导致小量的CPU开销,主要是因为从原生代码(C++)调用托管代码(C#)的消耗。
这对于Update()方法来讲特别麻烦。如果你的项目里有大量对象都有Update()方法,这些CPU的开销可能会上升到对性能有影响的地步。考虑使用“管理者模式”:一个或多个管理者类实现一个Update()方法来负责所有对象的更新。这会极大减少原生代码和托管代码之间的通信次数。更多细节请看这篇博客文章。
也一样决定你项目中那些系统需要每帧更新。游戏系统通常可以运行在更低的频率,轮流更新。一个简单的例子就是两个系统在每两帧交替更新。
我们亦推荐使用时间分片,既一个负责大量对象的系统将更新工作分摊到多个帧中,在每一帧中仅更新一部分对象。这种设计同样可以帮助减少CPU负担。
这个更高级的形态可能是你实现一个复杂的“更新预算管理系统”。你项目中的各个系统都被分配了一个每帧最大的时间预算,然后每个系统实现一个基于标准接口的管理类来在给定时间内执行尽可能多的工作。这个方法可以在整个项目的生命周期中非常好地帮助管理CPU的高峰负担。遵循这种设计模式的系统也可以变得更加灵活。
不要在Update里面去频繁的找节点或组件
我们可以尽可能地把Update中要用的GameObject或组件都通过变量缓存起来。这些API调用如GameObject.Find(),GameObject.GetComponent()以及存取Camera.main可能会非常昂贵,所以避免在Update()方法中调用它们。正确的做法是在Start()中调用它们并保存调用的结果。
避免运行中的字符串操作
避免在运行中执行包括连接在内的字符串操作。这些操作将导致大量托管内存分配,从而导致垃圾回收的高峰。只有在字符串确实改变了的情况下才重新生成字符串(例如玩家得分),而不是每帧都更新。你也可以使用StringBuilder类来显著减少内存分配的次数。避免字符串的加法,你很难回答出来 string a = “a” + “b” + 4; 这个过程中到底产生了多少个string对象。
避免不必要的debug日志
非必要的debug日志经常会导致项目性能的高峰。像Debug.Log()这类API会继续产生日志,甚至是在非开发build中,这往往使开发者大吃一惊。为了避免这种情况,请考虑将这些API像Debug.Log()的调用封装进你自己的类中,在方法上使用Conditional属性[Conditional("ENABLE_LOGS")]。如果没有定义这个Conditional的属性,那么这个方法和它的所有调用都会被丢弃。
不要在重点代码部分使用LINQ查询
尽管因为其强大的功能和易用性,使用LINQ查询很有诱惑力,请避免在关键部分(例如,常规更新)中使用它们。因为它们可能会生成大量内存分配以及占用大量CPU资源。如果你必须要使用它们,保持警惕并限制它们只在偶尔的情况下使用,譬如关卡初始化,只要它们不造成不必要的CPU高峰。
避免静态数据解析
项目经常会处理存储在像JSON或XML这种可读格式文件里的数据。这不仅从服务器下载的数据中很常见,在处理内置静态数据中也很常见。这类解析可能会很慢且往往产生大量托管内存分配。正确的做法是使用ScriptableObject以及自定义编辑器工具来处理游戏中内置的静态数据。
相关阅读:用Scriptable Object来架构你游戏的3个好办法
用不分配内存的API
Unity有一些API会产生托管内存分配,比如Component.GetComponents()。像这种返回一个数组的API是在内部分配内存的。有时候它们会有不产生托管内存分配的替代者,这时永远要使用不产生托管内存分配的。许多Physics API有全新的不产生托管内存分配的替代品,例如,用Physics.RaycastNonAlloc()而不是Physics.RaycastAll()。
好,今天的Unity程序员编码规范就介绍到这里,还有更多的一些技巧,掌握这些技巧主要是要注意一些基本的原理原则,而编程技巧就是围绕这些基本的原理原则。同时平常我们多积累,多思考,能写出高效的优雅的代码。关注我,可以学习到更多的Unity架构师的实战教程。