Unity游戏项目性能优化总结 (难度3 推荐4)

原文地址:

https://zhuanlan.zhihu.com/p/24392681

本文就Unity游戏项目性能优化作出了总结。包括Profile工具、Unity使用、机制设计、脚本编写等方面内容。
本文的测试机型皆为iPhone6。为方便找出瓶颈目标帧率先提高为60fps,后面再看实际情况是否限帧30fps。
本文的Unity版本为5.5.0f3或更新版本。

本文将持续更新。

Profiler工具

在Unity项目中,可能使用到的Profiler工具分3种:

  • 长期性能数据监控工具
  • Unity Profiler
  • XCode和Instruments

长期性能数据监控工具会至少每天都对游戏单局、或游戏资源进行自动化性能测试,并上报结果到服务器。能从“整体”去对比不同时段、不同版本间的性能差别。

游戏资源长期性能监控工具报表

Unity Profiler能定量地找到C#的GC Alloc问题;其Timeline视图也能从地整体(但不太定量)找到CPU瓶颈。

Unity Timeline Profiler

XCode的GPU Report视图能从整体(但不太定量)找到游戏的瓶颈阶段。当Frame Time中CPU大于GPU时,表示CPU是瓶颈,否则表示GPU是瓶颈;当Utilization中的TILER比RENDERER高时,表示顶点处理是GPU的瓶颈,否则表示像素处理是GPU的瓶颈。
帧率受限于瓶颈,应优先优化瓶颈阶段,非瓶颈阶段优化得再快都无法提高帧率。

GPU Report

XCode的Capture GPU frame功能能高效且定量地定位到GPU中shader的消耗。

XCode Capture GPU frame

Instruments的TimeProfiler能高效且定量地定位C#脚本(IL2CPP后的C++代码)的CPU占用,甚至包括部分Unity引擎代码的CPU占用函数消耗,而不必麻烦地添加BeginSample()、EndSample()。

Instruments Time Profiler

Unity使用/机制优化小结

GameObject的SpawnPool应支持“移出屏幕”功能

GameObject(比如特效)可能会被频繁的在“使用中”、“不使用”的状态间切换。我们的SpawnPool不应过快地把“刚刚不使用”的GameObject立刻Deactivate掉,否则会引起不必要的Deactivate/Activate的性能消耗。应有一个“从热变冷”的过程:“刚刚不使用”只是移出屏幕;只有“不使用一段时间”的GameObject,才会得以Deactivate。可能的实现方式如下:

/// On each timer, we try to make parts of "hot" items to be "cool" by deactivating them.
internal void OnTimer()
{
    if(teleportCache.items.Count > 0) {
        if(Time.realtimeSinceStartup - teleportCache.lastSpawnTime < SpawnPool.TeleportThenDeactivateDuration &&
            teleportCache.items.Count <= 3) {
            this.MoreLogInfo("this prefab is recently spawned, and the teleport cache is not too large, we don‘t deactivate these remaining items");
        }
        else {
            int deactivateCount = Mathf.Max(1, teleportCache.items.Count / 3);

            SpawnIdentity oneId;
            while(deactivateCount > 0) {
                --deactivateCount;
                oneId = teleportCache.items.Pop();
                if(null != oneId) {
                    this.MoreLogInfo("Deactivate from teleportCache:" + oneId);
                    oneId.gameObject.MoreSetActive(false, this);
                    oneId.gameObject.transform.SetParent(null, true);
                    deactivateCache.Push(oneId);

                    SpawnPoolProfiler.AddDeactivateCount(oneId.gameObject);
                }
            }
        }
    }
}

Transform的孩子不应过多

当Transform包含不该有的孩子Transform或其他组件时,为该Transform进行position、rotation赋值,会引起消耗,特别是包含粒子系统的时候。

对Transform进行rotation赋值时,由于其孩子包含粒子系统所产生的消耗

但考虑到切换Transform的parent本身也会有消耗,因此,我们对此也应有“从热变冷”的过程:“刚刚不使用”依然保留在父亲Transform里;只有“不使用一段时间”的GameObject,才从父亲Transform移出。

应减少粒子系统的Play()的调用次数

每次调用ParticleSystem.Play()都会有消耗,如果粒子系统本身没有明显“前摇”阶段,应先检查ParticleSystem.isPlaying,例子如下:

ParticleSystem ps;
for (int i = 0; i < num; ++i) {
    //m_particleSystemLst[i].Stop();
    ps = m_particleSystemLst[i];
    /// CAUTION! WE SHOULD CHECK isPlaying before calling Play()! OR, IT WILL AFFECT PERFORMANCE!
    if(!ps.isPlaying) {
        ps.Play();
    }
}

应减少每帧Material.GetXX()/Material.SetXX()的次数

每次调用Material.GetXX()或Material.SetXX()都会有消耗,应减少调用该API的频率。比如使用C#对象变量来记录Material的变量状态,从而规避Material.GetXX();在Shader里把多个uniform half变量合并为uniform half 4,从而把4个Material.SetXX()调用合并为1个Material.SetXX()。

应使用支持Conditional的日志输出机制

简单使用Debug.Log(ToString() + "hello " + "world");,其实参会造成CPU消耗及GC。使用支持Conditional的日志输出机制,则无此问题,只需在构建时,取消对应的编译参数即可。

/// MoreDebug.cs,带Conditional条件编译的日志输出机制
[Conditional("MORE_DEBUG_INFO")]
public static void MoreLogInfo(this object caller) { DoMoreLog(MoreLogLevel.Info, false, caller); }

/// 用户代码.cs,调用方简单正常调用即可。正式构建时,取消MORE_DEBUG_INFO编译参数。
this.MoreLogInfo("writerSize=" + writer.Position, "channelId=" + channelId);

脚本优化小结

依然需要减少GetComponent()的频率

即使在Unity5.5中,GetComponent()会有一定的GC产生,有少量的CPU消耗。如有可能,我们依然需要规避冗余的GetComponent()。另,自Unity5起,Unity已就.transform进行了cache,我们不需再为.transform担心,见《UNITY 5: API CHANGES & AUTOMATIC SCRIPT UPDATING》最后一段。

应减少UnityEngine.Object的null比较

因为Unity overwrite掉了Object.Equals(),《CUSTOM == OPERATOR, SHOULD WE KEEP IT?》也说过unityEngineObject==null事实上和GetComponent()的消耗类似,都涉及到Engine层面的机制调用,所以UnityEngine.Object的null比较,都会有少许的性能消耗。对于基础功能、调用栈叶子节点逻辑、高频功能,我们应少null比较,使用assertion来处理。只有在调用栈根节点逻辑,有必要的时候,才进行null比较。

上面C#代码对应的IL2CPP代码

而且,从代码质量来看,无脑的null保护也是不值得推崇的,因为其将错误隐藏到了更偏离错误根源的逻辑。理论上,当错误发生了,应尽早报错,从而帮助开发者能更快速地定位错误根源。所以,多用assertion,少用null保护,无论是对代码质量,还是代码性能,都是不错的实践。

应减少不必要的Transform.position/rotation等访问

每次访问Transform.position/rotation都有相应的消耗。应能cache就cache其返回结果。

应尽量减少创建C#堆内存对象

建议使用成员变量,或者Pool来规避高频创建C#堆内存对象的创建。而且堆内存对象创建本身就是个相对较慢的过程。

应为struct对象重载所有object函数

为了普适性,C#的struct的默认Equals()、GetHashCode()和ToString()都是较慢实现,甚至涉及反射。用户自定义的struct,都应重载上述3个函数,手动实现,比如:

public struct NetworkPredictId{
    int m_value;
    public override int GetHashCode(){
        return m_value;
    }

    public override bool Equals(object obj){
        return obj is NetworkPredictId && this == (NetworkPredictId)obj;
    }

    public override string ToString(){
        return m_value.ToString();
    }

    public static bool operator ==(NetworkPredictId c1, NetworkPredictId c2){
        return c1.m_value == c2.m_value;
    }

    public static bool operator !=(NetworkPredictId c1, NetworkPredictId c2){
        return c1.m_value != c2.m_value;
    }
}

如果可能,尽量用Queue/Stack来代替List

我们会习惯用List来实现数据集合的需求。但好一些情况下,我们事实上是不需对其进行随机访问,而仅仅是“增加”、“删除”操作。此时,我们应该使用增删复杂度都是O(1)的Queue或者Stack。

注意List常用接口复杂度

Add()常为O(1)复杂度,但超过Capacity时,为O(n)复杂度。故我们应注意合理地设置容器的初始化Capacity。
Insert()为O(n)复杂度。
Remove()为O(n)复杂度。RemoveAt(index)为O(n)复杂度,n=(Count - index)。故建议移除时应优先从尾部移除。当批量移除时,miloyip亦指出RemoveRange提高移除效率。

/// remove not exsiting items in O(n)
int oldCount = m_items.Count;
int newCount = 0;
Item oneItem;
for(int i = 0; i < oldCount; ++i){
    oneItem = m_items[i];
    if(CheckExisting(oneItem)){
        m_items[newCount] = oneItem;
        ++newCount;
    }
}
m_items.RemoveRange(newCount, removeCount);

应注意容器的初始化capacity

同理如上条目。另,Capacity增长时,除了O(n)的复杂度,也有GC消耗。

应尽量为类或函数声明为sealed

IL2CPP就sealed的类或函数会有优化,变虚函数调用为直接函数调用。详见《IL2CPP OPTIMIZATIONS: DEVIRTUALIZATION》

C#/CPP interop时,不需为blittable的变量声明为MarshalAs

某些数值类型,托管代码和原生代码的二进制表达方式一致,这些称为blittable数值类型。blittable数值类型在interop时为高效的简单内存拷贝,故应值得推崇。C#中的blittable数值类型为byte、int、float等,但注意不包括常用的bool、string。仅有blittable数值类型组成的数组或struct,也为blittable。

blittable的变量不应声明MarshalAs。
比如下面代码,

[DllImport(ApolloCommon.PluginName, CallingConvention = CallingConvention.Cdecl)]
private static extern ApolloResult apollo_connector_readUdpData(UInt64 objId, /*[MarshalAs(UnmanagedType.LPArray)]*/ byte[] buff, ref int size);

注释前后的IL2CPP代码如下图,右侧明显避免了marhal的产生。

详见《IL2CPP INTERNALS: P/INVOKE WRAPPERS》

减少Dictionary的冗余访问

我们常习惯编写这样的代码:

if(myDictionary.Contains(oneKey))
{
    MyValue myValue = myDictionary[oneKey];
   // ...
}

但其可减少冗余的哈希次数,优化为:

MyValue myValue;
if(myDictionary.TryGetValue(oneKey, out myValue))
{
    // ...
}

(TO BE CONTINUED...)

时间: 2024-12-27 01:41:14

Unity游戏项目性能优化总结 (难度3 推荐4)的相关文章

[Unity优化] Unity CPU性能优化 (难度3 推荐4)

原文地址: http://www.cnblogs.com/chwen/p/4396515.html 前段时间本人转战unity手游,由于作者(Chwen)之前参与端游开发,有些端游的经验可以直接移植到手游,比如项目框架架构.代码设计.部分性能分析,而对于移动终端而言,CPU.内存.显卡甚至电池等硬件因素,以及网络等条件限制,对移动游戏开发的优化带来更大的挑战. 这里就以unity4.5x版本为例,对Unity的优化方案做一个总结,有些是项目遇到的,也有些是看到别人写的不错拿来分享,算作一个整理,

Unity+NGUI性能优化方法总结

一共9招. 1 资源分离打包与加载 游戏中会有很多地方使用同一份资源.比如,有些界面会共用同一份字体.同一张图集,有些场景会共用同一张贴图,有些会怪物使用同一个Animator,等等.可以在制作游戏安装包时将这些公用资源从其它资源中分离出来,单独打包.比如若资源A和B都引用了资源C,则将C分离出来单独打一个bundle.在游戏运行时,如果要加载A,则先加载C:之后如果要加载B,因为C的实例已经在内存,所以只要直接加载B,让B指向C即可.如果打包时不将C从A和B分离出来,那么A的包里会有一份C,B

Unity NGUI性能优化

建议读者先看这篇博文:http://blog.csdn.net/zzxiang1985/article/details/43339273,有些技术已经变了,比如第1招,unity5的打包机制已经变许多了.不像其他招基本还是可以学习的,比如:透明通道分离,关闭texture read/write选项(其实其他资源得read/write选项也类似,比如动画资源等),减少场景中的GameObject数量,整理图集(一般是一个面板使用2个图集:当前面板一个图集+基本图集),使用多个UIPanel隔开dc

App之性能优化

一般来说,浏览器的内存泄漏对于 web 应用程序来说并不是什么问题.用户在页面之间切换,每个页面切换都会引起浏览器刷新.即使页面上有内存泄漏,在页面切换后泄漏就解除了.由于泄漏的范围比较小,因此常常被忽视. 但在移动端,内存泄漏就成了一个比较严重的问题.在单面应用中,用户不能刷新页面的,整个应用程序构建在一个页面上.在这种情况下泄漏会被累积,导致内存不被回收. Javascript中的垃圾回收机制类似于Java/C#这类语言中的回收机制: 一个对象不再被引用,即将被自动回收 具体回收时刻是我们无

UGUI batch 规则和性能优化

UGUI batch 规则和性能优化 (基础) Unity 绘图性能优化 - Draw Call Batching : http://docs.unity3d.com/Manual/DrawCallBatching.html 1.名词 1)Batch 单词 google 翻译的解释是:批量.批次. 2)Stats中的Batches参数:意思是batching后产生的批次数.(其实每一个批次就会调用一次DrawCall) 3)Unity官方文档中的batching是指batch操作和batchin

14.性能优化答疑二

一.使用perf 工具,看到的是十六进制地址而不是函数名. 查看perf最后欧,就会看到警告信息: Failed to open /opt/bitnami/php/lib/php/extensions/opcache.so, continuing without symbols 这说明,perf 找不到待分析进程依赖的库.当然,实际上这个案例中有很多依赖库都找不到,只不过,perf 工具本身只在最后一行显示警告信息,所以你只能看到这一条警告. 这个问题,其实也是在分析 Docker 容器应用时,

Android:应用开发进阶必经之路之性能优化(上)

前言 性能优化在一款产品的迭代过程中非常重要:程序实现了功能.还原产品原型只能保证程序能用,但如果要让用户更愿意使用,产品得好用.试想一下如果你开发的产品启动慢.页面显示需要长时间转圈加载.页面切换卡顿.黑白屏.用一会机器就发烫.耗内存.OOM.程序切换到后台后占用内存无法释放......,这些问题就像正在玩游戏时弹出提示框这类糟糕的用户体验一样让用户恼火,如果用户不得不使用你的产品,可能还会一直忍受:但如果有很多同类竞品,糟糕的用户体验会大大影响留存率.有时候产品在市场上的表现差,真不能全怪产

Unity优化之GC——合理优化Unity的GC (难度3 推荐5)

原文链接:http://www.cnblogs.com/zblade/p/6445578.html 最近有点繁忙,白天干活晚上抽空写点翻译,还要运动,所以翻译工作进行的有点缓慢 =.= 本文续接前面的unity的渲染优化,进一步翻译Unity中的GC优化,英文链接在下:英文地址 介绍: 在游戏运行的时候,数据主要存储在内存中,当游戏的数据不在需要的时候,存储当前数据的内存就可以被回收再次使用.内存垃圾是指当前废弃数据所占用的内存,垃圾回收(GC)是指将废弃的内存重新回收再次使用的过程. Unit

Unity性能优化(2)-官方文档简译

本文是Unity官方教程,性能优化系列的第二篇<Diagnosing performance problems using the Profiler window>的简单翻译. 简介 如果游戏运行缓慢,卡顿,我们知道游戏存在性能问题.在我们尝试解决问题前,需要先知道引起问题的原因.不同问题需要不同的解决方案.如果我们靠猜测或者其他项目的经验去解决问题,那么我们可能会浪费很多时间,甚至使得问题更严重. 这时我们需要性能分析,性能分析程序测量游戏运行时的各个方面性能.通过性能分析工具,我们能够透过