Unity Optimize 游戏优化
Apr 02, 2022内存优化
- 对象池
- 纹理压缩
- 模型顶点优化:
- AssetImported 载入优化:剔除没必要的数据,可能有切线,碰撞体。也可以选择压缩顶点等等。
- 美术手动降低顶点数量
- 规范小模型顶点少于300,可合并 Draw call。
- LOD : 根据模型距离摄像机的距离,使用不同精度的模型,降低渲染负荷。
- MipMap :用原有贴图,生成不同精度的 8 种贴图,用于不同远近大小的模型上,降低渲染负荷。
- 控制 AB 占用内存,不需要时,尽快卸载。
- 单面渲染
- 降低 Draw Call
- 静态批处理 static batching
- 能设为静态的物体就设为静态,合并 Draw call。
- 动态批处理
- 大量重复出现的物体,尽量将顶点数降低 300 以下,并缩放一致。
- 尽量重用材质,并将同一个材质用到的纹理设置为同一图集。
- 静态批处理 static batching
- 纹理压缩,按需压缩。
- 少用阴影,反光。减少实时光照,用光照贴图。
动态批处理
动态批处理是由 Unity 内存帮我们实现,需要满足一些条件才能使用。
- 最高只支持 900 个顶点的模型
- 当 Shader 中仅用到顶点位置,法线,UV 值这三种属性,批处理仅允许顶点数 300 以下
- 当 Shader 用到顶点位置,法线,UV0,UV1 和切向量,则批处理仅允许顶点数 180 以下
- 同类物品,不同缩放不会合并处理。
- 不同 Material 无法使用
UI 批处理
纹理压缩
由上至下,压缩程度原来越高
- RGBA32 :含透明通道,质量高,长宽无需求,内存占用大
- RGBA16 : 含透明通道,但是颜色阶梯明显,长宽无需求,内存占用偏低
- RGB16 :无透明通道,其他同上
- ETC1(RGB) + ETC(Alpha):用两张 ETC 图替代待 RGB + A,但需要手动写shader支持。长宽可不等长,但需要为 2^n 长度。内存占用低
- ETC1(RGB):同上,仅去除了 Alpha 通道
- PVRTC4 :无透明通道,质量低,长宽需要一致且 2^n,内存占用低
逻辑优化
- 降低 GetComponent 的使用次数
- 降低 text 的重绘,即每次赋值前,先判断 string 是否有变。
- 减少字符串 String 的拼接,若需要,可以用 StringBuildero
Monobehaviour / C# GC 优化
C# 内存管理池分为栈内存和堆内存。栈内存只要用于存储临时变量和值,堆内存则主要用于存储引用对象。栈上内存一般会随着函数的生命周期结束而回收,堆则需要 GC 触发时才遍历判断无用后(递归判断对象无法再被引用到时)再进行回收。所以 GC 的主要针对堆内存的。
当需要再对上存储数据时,步骤如下:
- 是否有足够的闲余空间,有则直接使用
- 不存在足够空间,则调用 GC 尝试腾出空间。如果此时已经足够,则使用。
- GC 后空间仍然不足,则会申请拓展堆内存空间,这一步会缓慢,然后再分配空间用于存储数据。
所以需要避免触发 GC 以及拓展空间的频率。需要注意的是,存储数据的空间是要连续的,GC 会带来内存碎片化,会出现的情况是:剩余空间符合需求,但是因为不是连续的,所以一样无法使用,需要申请新的空间,或者将已用空间进行重组。这样 GC 会因为自己的操作,让 GC 进行的更加频繁。
降低 GC 带来的影响
- 减少 GC 自动调用频率
- 手动控制 GC 的调用时间,避开帧率敏感期
减少 GC 自动调用
- MonoBehaviour 中的 Update,FixedUpate,LateUpdate和协程这种没帧都会执行的函数中,尽量减少创建引用对象,而使用缓存。
// before
private void Update()
{
List<int> l = new List<int>();
// do something with l
}
// Optimize
private List<int> l = new List<int>();
private void Update()
{
// do something with l
}
- 降低函数的调用次数。在使用函数时,应考虑函数是否有必要没帧调用,这个点出发,又有两种方式:
- bool 判断执行
- 间隔执行
// 某会产生 GC 的函数
private void Func() { ... }
// ---------------------------------
// 假设 Func 的效果只与位置变化有关。
private float cachePos = 0.0f;
private void Upate()
{
if (cachePos != transform.position.x)
{
cachePos = transform.position.x
Func();
}
}
// ---------------------------------
// 假设 Func 的效果没必要每帧执行
private float waitTime = 5.0f;
private float curTime = 0.0f;
private void Upate()
{
curTime += Time.deltaTime;
if (curTime > waitTime)
{
curTime = 0.0f;
Func();
}
}
参考文档
https://www.cnblogs.com/zblade/p/6445578.html
对象池
对于频繁生成和销毁的对象,使用对象池可以有效降低内存占用。现在对 Unity GameObject 对象回池的操作做性能对比,操作对象为一般背包格子,节点数 18,业务中会改到组件都一般为 Text,Image,Rectransform
public GameObject targetItem;
public GameObject oriTargetItem;
private List<GameObject> Items = new List<GameObject>();
public int testCount = 0;
private void Test()
{
List<GameObject> Items = new List<GameObject>();
int testCount = 1000
// default
for (int i = 0; i < testCount; ++i)
{
GameObject go = Instantiate(targetItem, this.transform, false);
Items.Add(go);
}
}
创建 1000 个用时:0.75s,内存占用:36.45m
同等量级下,对已创建的格子进行 Reset,仅操作 RectTranform
...
{
// Reset
int index = 0;
for (int i = 0; i < Items.Count; ++i)
{
SynNode(Items[i].transform, oriTargetItem.transform);
}
}
private void SynNode(Transform dirty, Transform template)
{
RectTransform d;
RectTransform t;
int childCount = dirty.childCount;
for (int i = 0; i < childCount; ++i)
{
d = template.transform.GetChild(i) as RectTransform;
t = dirty.transform.GetChild(i) as RectTransform;
d.anchoredPosition3D = t.anchoredPosition3D;
d.sizeDelta = t.sizeDelta;
d.anchorMin = t.anchorMin;
d.anchorMax = t.anchorMax;
d.pivot = t.pivot;
d.localRotation = t.localRotation;
d.localScale = t.localScale;
if (d.childCount > 0)
{
SynNode(d, t);
}
}
}
用时:0.04s,内存占用:36.47m,耗时降低:94.7%
private void Pool()
{
int index = 0;
for (int i = 0; i < Items.Count; ++i)
{
SynNode(Items[i].transform, oriTargetItem.transform);
}
}
private void SynNode(Transform dirty, Transform template)
{
SynRectTransform(dirty as RectTransform, template as RectTransform);
SynRawImage(dirty.GetComponent<RawImage>(), template.GetComponent<RawImage>());
SynButton(dirty.GetComponent<Button>(), template.GetComponent<Button>());
SynImage(dirty.GetComponent<Image>(), template.GetComponent<Image>());
SynText(dirty.GetComponent<Text>(), template.GetComponent<Text>());
int childCount = dirty.childCount;
for (int i = 0; i < childCount; ++i)
{
SynNode(dirty.GetChild(i), template.GetChild(i));
}
}
private void SynRectTransform(RectTransform dirty, RectTransform template)
{
dirty.anchoredPosition3D = template.anchoredPosition3D;
dirty.sizeDelta = template.sizeDelta;
dirty.anchorMin = template.anchorMin;
dirty.anchorMax = template.anchorMax;
dirty.pivot = template.pivot;
dirty.localRotation = template.localRotation;
dirty.localScale = template.localScale;
}
private void SynImage(Image dirty, Image template)
{
if (dirty == null || template == null)
{
return;
}
dirty.sprite = template.sprite;
dirty.color = template.color;
dirty.raycastTarget = template.raycastTarget;
dirty.enabled = template.enabled;
}
private void SynButton(Button dirty, Button template)
{
if (dirty == null || template == null)
{
return;
}
dirty.interactable = template.interactable;
dirty.enabled = template.enabled;
}
private void SynRawImage(RawImage dirty, RawImage template)
{
if (dirty == null || template == null)
{
return;
}
dirty.texture = template.texture;
dirty.color = template.color;
dirty.enabled = template.enabled;
}
private void SynText(Text dirty, Text template)
{
if (dirty == null || template == null)
{
return;
}
dirty.text = template.text;
dirty.color = template.color;
dirty.raycastTarget = template.raycastTarget;
dirty.enabled = template.enabled;
}
用时:0.71s,内存占用:36.47m,耗时降低:5.7%
可能是大量 GetComponent 操作带来的耗时增加,项目中是使用的脚本会对组件进行缓存,结合这点进行优化
private void Pool_UIBinding()
{
int index = 0;
for (int i = 0; i < Items.Count; ++i)
{
UIBinding template = oriTargetItem.GetComponent<UIBinding>();
UIBinding dirty = Items[i].GetComponent<UIBinding>();
for (int j = 0; j < dirty.NodesCount(); ++j)
{
UnityEngine.Object obj = dirty.QueryNodeIndex(j);
string tag = obj.name.Substring(0, 3);
switch (tag)
{
case UIBinding.TAG_NEG:
SynGameObject(obj as GameObject, template.QueryNodeIndex(j) as GameObject);
break;
case UIBinding.TAG_IMG:
SynImage(obj as Image, template.QueryNodeIndex(j) as Image);
break;
case UIBinding.TAG_BTN:
SynButton(obj as Button, template.QueryNodeIndex(j) as Button);
break;
case UIBinding.TAG_TXT:
SynRawImage(obj as RawImage, template.QueryNodeIndex(j) as RawImage);
break;
case UIBinding.TAG_RMG:
SynRawImage(obj as RawImage, template.QueryNodeIndex(j) as RawImage);
break;
}
if (obj as MonoBehaviour)
{
SynRectTransform((obj as MonoBehaviour).transform as RectTransform, (template.QueryNodeIndex(j) as MonoBehaviour).transform as RectTransform);
}
}
}
}
用时:0.096s,内存占用:36.49m,耗时降低:87.2%
虽然重置 Rectransform 进行了查找子节点,类型转换,属性值拷贝多个操作,耗时也相对预创建新 GameObject 有着显著的降低。可以猜想即使暴力补充上了 Image 和 Text 的还原操作,耗时不会增加太多。
Todo: 补充还原 Image 和 Text 的数据。
所以,GameObject 回池还原可以分为几种
- 暴力还原,遍历所有节点和组件
- 记录改变过的节点,只针对改过的进行还原
- 半暴力还原,随着业务进行,逐步补充业务可能中会修改的节点,即使该对象的生命周期内没走修改某个组件的属性。
无用节点全部隐藏,耗时:0.65 ,内存占用:36.9,耗时降低:13%,内存降低:略
无用节点全部删除,耗时:0.19,内存占用:6.14,耗时降低:75%,内存降低:83%
Comments