代码优化
文章目录
在商业游戏中代码优化是必不可少的,这极大的影响游戏运行的性能,以及代码的扩展性和可读性。应该具备良好的编码习惯,才能从初级程序走得更高更远。
1, Unity代码优化
1.1 缓存对象
不要在Update()方法中每帧获取组件对象,好的做法应该是在Start()或者Awake()中 获取并缓存下来,这样在Update()方法中就能直接使用了。缓存指的是在使用前就获得这个对象。
不过,也存在一种可能即Update()方法中存在条件判断,导致一段时间内无法达成,这样在Start()方法中获取的对象就没用了,这种使用get方法,只在第一次使用该对象时才会进行读取。
Rigbody rB;
Rigbody RB
{
get
{
if(rB==null)
rB=GetComponent<Rigbody>();
return rB;
}
}
代码解析:用属性中的get方法,只有rB为空时才会得到该组件,得到该组件之后rB不为空不会再次得到,也就是只得一次
1.1 减少脚本
Unity的脚本用起来很方便但是也比较消耗性能。脚本主要用于生命周期的方法,比如OnEnable()、Update()。创建多个对象就需要多个Update()方法。好的做法是通过一个管理类,在一个Update()方法中执行所有的对象。可以将Update()封装成一个事件,这样即使是非Mono类中也可以任意添加Update()方法了。
using System;
using UnityEngine;
public class Script_17_01
{
static public event Action onUpdate;//声明 事件onUpdate
static Internal s_Internal;//声明 类Internal
static Script_17_01()
{
s_Internal = new GameObject("_Event_").AddComponent<Internal>();
GameObject.DontDestroyOnLoad(s_Internal.gameObject);
}
class Internal : MonoBehaviour
{
private void Update()
{
onUpdate?.Invoke();//委托调用
}
}
}
在任意位置添加事件:
Script_17_01.onUpdate+=()=>//委托添加方法参数
{
Debug.Log("update");
};
1.3 减少Update()执行
可以将核心方法通过Time.frameCount取余,这样可以每3帧执行里面的方法而不是每帧调用。
private void Update()
{
if(Time.FrameCount%3==0){}
}
更有效的手段是通过Application.targetFrameRate来强制设置当前帧率。当手指触摸屏幕后设置为69帧,发现10秒后没有继续点击设置为30帧。
private float m_LastInputTime;
private void Update()
{
if(Time.time-m_LastInputTime>10f)
{
Application.targetFrameRat=30;
}
if (Input.GetMouseButton(0))
{
m_LastInputTime=Time.time;
Application.targetFrameRat=60;
}
}
1.4 缓存池
游戏中频繁创建与卸载会影响加载时间,对于频繁创建和转移的对象,推荐使用缓存池。从缓存池中移除对象只是暂时隐藏对象,使用时再启动。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class Script_17_02 :MonoBehaviour
{
ObjectPool<GameObject> m_ObjectPool;
private void Start()
{
//创建缓存池,创建、获取、释放、销毁事件、默认于最大缓存池数量
m_ObjectPool = new ObjectPool<GameObject>(Create, Get, Release, Destroy, true, 10, 20);
}
GameObject Create()
{
return new GameObject();
}
void Get(GameObject go)
{
go.SetActive(true);
}
void Release(GameObject go)
{
go.SetActive(false);
}
void Destroy(GameObject go)
{
go.SetActive(false);
}
List<GameObject> list = new List<GameObject>();
private void Update()
{
if (Input.GetKeyUp(KeyCode.A))
{
list.Add(m_ObjectPool.Get());
}
if (Input.GetKeyUp(KeyCode.D))
{
//清除最后一个
if (list.Count > 0)
{
int index = list.Count - 1;
m_ObjectPool.Release(list[index]);
list.RemoveAt(index);
}
}
}
}
1.5 日志优化
通过Debug.unityLogger.logEnabled可以运行时动态开关日志,但是输出日志为字符串拼接的代码依然会产生堆内存。
using UnityEngine;
public class Script_17_03 :MonoBehaviour
{
void Update()
{
Log.Print(Time.frameCount + " " + gameObject);
//Debug.Log(Time.frameCount+" " + gameObject);
}
}
public static class Log
{
[System.Diagnostics.Conditional("ENABLE_DEBUG")]//通过宏来控制该方法的执行。
static public void Print(object message)
{
Debug.Log(message);
}
}
当前没有启动ENABLE_DEBUG宏所以不会执行Print()方法,可以在项目设置(projectsetting)添加宏,也可以通过代码动态的添加宏。
2. C#代码优化
值类型存储在栈上,引用类型存储在堆中,栈上同时存储着指向堆的引用类型的地址。
2.1 装箱和拆箱
值类型和引用类型可以相互转换,这会导致栈上的数据在堆上又生成一份,要避免装箱拆箱。
•避免使用ArrayList对象,因为它是用引用类型来保存数据的,好的做法是List这种泛型。
•使用协程时一定不要使用yield return 0,这样做会让值类型变成引用类型,好的做法是yield return null
2.2 字符串
•声明字符串再修改字符串会在堆上重新创建一个对象。大量修改字符应该使用StringBuilder,它在内存中会分配一块内存,当长度超过内存时会自动扩大一倍。
•UI中需要显示角色属性,一般是值类型要变成引用类型,意味着进行一次装箱,减少开销的方法是避免在Update中每帧调用toString()方法。
2.3 struct
struct是值类型,虽然和class很像,但是有很大区别,使用上,struct的构造函数必须传入参数,而且不能继承,成员对象不能设置为抽象和虚函数。结构体内也可以声明引用类型,在栈上保存的是指向堆中的地址。struct对象在复制赋值时会将栈上所有的数据进行赋值。所以struct适合少量数据的保存。
2.4 GC
自身的引用被断开即可满足“垃圾”的条件。
Unity勾选Use incremental GC则启用渐进式GC,这是一种异步执行的操作,这样就不会在一帧上卡住。
可以通过代码来设置GC和开启关闭GC。
3,Profiler 内存管理
Unity内存由3部分组成,分别是托管堆内存、非托管内存和、原生内存。托管内存前面介绍过很多,非托管内存是无法被GC的内存,相当于在C#启动不安全代码由C++分配的内存,它需要程序手动释放;最后是原生内存,Unity中所有资源都是原生内存。
3.1 内存泄漏
没有及时释放无用对象,或是它们被静态对象所引用。
3.2 耗时函数统计
点击Window—Analysis—Profiler打开Profile让,但是由于是在编辑器模式下提供的工具,所以不准确,Unity提供了Profiler(Standalone Process)独立程序。
3.4 堆内存分配
随着程序扩容,堆内存并不会删除之前产生的内存。string已经被释放,但是它在堆内存中依然占据容量,此时分配一块float[]数组,由于容量比string大所以堆内存会被扩充,只要当声明一块比string小的内存才可以分配到string的内存上,此时string此时的内存就会产生内存碎片。直到下次分配一块容量小于它的内存时才会分配这里。
3.6 Unity内存
堆内存是自己写代码产生的,只是Unity内存的一小部分。
新版Unity安装Memory Profiler工具,在package manager安装即可,可以看到内存占用信息。
4 着色器优化
从模型资源的角度,减少模型顶点数可以优化性能,当模型拉远时,可以优化片元着色器的执行数量,从而优化性能.
世界空间和纹理座标对精度要求比较高因此使用float,其他对精度要求不高的使用half。
关于着色器相关 后续更新……