0%

Unity优化基础

UGUI

动静分离

如果每个UI元素都独立一次drawcall的话,那显然会极大浪费渲染队列,因此UGUI在绘制UI时会将UI元素进行网格合并的操作,以减少DC一次绘制。

合并的出发点没有问题,但因此只要任何一个UI元素一变,网格就需要重新绘制,这样节省下来的DC全用在合并顶点上了,因此就需要UI元素的动静分离。UGUI中以Canvas为合并网格的基础,因此我们需要尽可能把动态UI元素放在单独的Canvas中,把静止常态的UI放在另一个Canvas中。

嵌套子Canvas也不会和父Canvas合并

重UI的分解

随着UI嵌套的越来越厚、越来越多,项目后期的一个UI对象可能会套着好几层UI面板,并且用的时候只显示某一个,隐藏其他的。因此虽然看起来没什么,但在对这个重UI实例化和销毁的时候会变得很慢,因此可以考虑把不必要的二级UI拆分成单独的对象,在需要时才实例化生成,而不是激活和隐藏。

不管分不分解,加载这些对象的CPU时间都是需要的,但本质上我们把集中的大段CPU作业拆解成了多个小段,以降低卡顿。当然,也要注意如果拆分不当,导致小元素频繁实例化和销毁也会造成性能浪费。

加载优化

UI加载并实例化需要做完:加载GameObject对象、网格合并、组件初始化、素材资源加载等任务,并不算轻松。因此可以考虑在CPU缓和的情况下预加载UI:例如提前加载AB包素材。或者像上一节中对UI进行激活和隐藏处理,而不是生成和销毁。激活和隐藏不用再改变内存,但是组件的重新enable也会带来一定开销,因此再极端一点可以考虑直接把UI移出屏幕并且关闭部分更新,以形成隐藏。

字体拆分

字体图集也是一项比较重的资源,而UI中如果同时有几种不同的字体,也会对内存造成一定负担。因此可以针对于特定使用场景,提取出特定词汇单独生成一个字体图集使用,比较常见的有登录场景字体、数值字体、常用字字体。

AI寻路

寻路算法中最经典的就是A星算法。虽然经典的最短路径算法有Dijkstra和Floyd,但是其复杂度分别是O(N2)和O(N3),空间复杂度都是O(N^2),因此根本不适合游戏这种大型路径搜索需求。并且更重要的一点是,游戏寻路根本不在乎是不是最短路径,只需要是一条能走通的路径就行,这就是A星算法的根本目标,其使用贪心算法寻找局部优解来寻路,平均时间复杂度为O(N logN)。然而,A星的具体实现中还有很多细节可以优化。

大场景分割

假如游戏场景很大,那么贪心搜索空间就会十分庞大,我们需要尽可能缩小搜索范围。回头想想,之所以我们要启用寻路算法,是因为出发点和目标点之间存在障碍物,不能直线到达。然而在大场景中,大部分路径是可以直接到达的,并不需要费劲寻路。

因此,我们可以把大场景分割成数个内部无障碍的区域。在区域内部,可以直接直线寻路。在区域外部,则是一个区域与区域之间的固定寻路 (从区域A到区域B,我们只需要提供一条固定路线即可)。而寻路既然已经不是动态的,那我们可以离线计算寻路路径,存储给实时使用,极大节省实时效率。

当然,区域外部一旦发生改变,这些离线结果都需要重新计算。

堆存储openlist节点

在贪心搜索局部最优解时,我们需要维护一个排序列表来方便获得最优节点 (最小损失的节点)。我们要么在每次插入新openlist节点时有序插入,要么每次确定最优节点时先排一次序。排序本身就是一件很慢的\(NlogN\)行为,因此不可能每次都排序。而如果是有序插入,例如二分查找插入点插入,那么需要\(log N\)的查找时间以及\(N\) 的数组移动时间。 为了避开数组移动的麻烦,同时保持有序插入的便捷,我们可以使用最小堆/优先队列来维护openlist节点列表,这样我们只有\(logN\)的插入时间,而不需要移动原数据。

openlist搜索规则优化 JPS

贪心的一个问题在于它是一步一个脚印去走的,因此即使是从(0,0)到(0,50)这样的直线路径,也会存储至少50个寻路节点,并且消耗50次插入时间,以及openlist空间包含这50个节点的所有邻居。那我们是不是能简化这种直线路径呢?

跳点算法 Jump Point Search (JPS) 框架上还是A星的框架,同样是搜索节点,加入openlist,从openlist中取出最优节点寻路。JPS主要改动在于它的搜索节点策略,它不像贪心一样每一步都将所有邻居加入openlist。JPS只在openlist记录那些关键的跳点节点,也就是直线的拐点,而对于平常的直线路径节点则不记录。

贪心搜索
JPS搜索

JPS具体规则较为复杂,可以参考上文链接,这里简单概括下两个主要思路:

一,检查当前节点是否是跳点。简而言之,跳点就是发生拐弯的节点。那么什么时候会发生拐弯呢?如下图,我们从绿色父节点A出发,一路沿途检查节点是否为跳点 (沿什么途则在后文),经过若干次检查后,我们开始检查橙色节点B。我们首先知道B有一个邻居黄色节点C,且B和C中间有障碍物。此时我们检查A到C的最短路径,发现必须要经过B节点拐弯去C点,这时候B就是C的跳点,C就是B的强迫邻居(forced neighbor),并且我们可以把B加入openlist。

Current是Parent到Forced Neighbor唯一最短路径的必经点,因此它是跳点。假如把黑色障碍去掉,那么Parent到黄色节点就有多条路径,就不存在跳点,也不存在Forced Neighbor。

同样,假如A到跳点B中间本身就需要在D点拐一次弯(即A与B非直线可达,D与B直线可达),那么D也是跳点。注意这里要求B是跳点,D才是跳点,要不然这个拐弯就没有意义了是不是。

因此整个跳点检查有点类似于栈模式,先确定最后一个跳点,然后才能认为之前的拐点是跳点。

绿色到橙色需要在蓝色拐弯,蓝色则也是跳点

二,往什么方向搜索跳点。现在我们知道了怎么检查一个点是否是跳点,但是去检查哪些点呢?由于JPS只关心直线走到尽头后的拐弯点,因此基本原则是先以直线方向一路检查是否为跳点,结束后再斜向进一步搜索内圈。具体细节则可另外搜索。

先直线搜索,不行再斜向进一步。

场景渲染优化

首先需要对资源情况摸一个底,检查一下场景的性能数据 :

  • 摄像机数量、实时灯光数量
  • 渲染路径
  • 运行时信息 Batch、SetPass Call、三角形面数、顶点数。
  • Memory Profiler:运行时内存分布情况
  • Profiler:CPU和GPU耗时分布
  • 打包体积

(以2023 URP sample Garden 为例) Batch在3000,setpass call在200,顶点数和面数在75W,内存占用在Resident 4.3G, Total Committed 18.8G

静态资产检查

使用UPR asset checker可以自动分析项目资产待优化的地方:

  • 音频:大部分音效可以开启单声道,降低采样频率至22050。优化后音频内存占用可以减少70M
  • 网格: