Unity高效编程经验50条分享
1.避免频繁创建临时对象
- 错误写法:obj.transform.position = pos;这种写法会在Lua中频繁返回transform对象导致gc
- 正确写法:创建一个静态方法来设置位置,例如
class LuaUtil {
static void SetPos(GameObject obj, float x, float y, float z) {
obj.transform.position = new Vector3(x, y, z);
}
}
Lua中调用LuaUtil.SetPos(obj, float x, float y, float z);
2.使用泛型必须生成对应类型的wrap文件
- 错误写法:泛型容器直接使用
- 正确写法:_GT(typeof(Dictionary<int, long>)然后生成对应的wrap文件,Lua调用避免产生GC
3.使用C#容器如果知道大概的容量尽量指定初始化大小避免自动扩容产生的GC,例如List、Dictionary、Array
- 错误写法:
Dictionary<ulong, int> buffTableIndexMap = new Dictionary<ulong, int>();
List list = new List();
for (int i = 0; i < 1000; i++)
{
list.Add(i);
} - 正确写法:Dictionary<ulong, int> buffTableIndexMap = new Dictionary<ulong, int>(1000);
List list = new List(1001);
for (int i = 0; i < 1000; i++)
{
list.Add(i);
}
4.注意非托管类型的手动释放
- 错误写法:
- Marshal.AllocHGlobal没有调用Marshal.FreeHGlobal
- 继承自IDisable接口的对象没有手动调用dispose方法
- 正确写法:
- 调用手动释放内存的接口方法
5.注意循环内部
- Unity携程
- 错误写法:大量的使用Coroutine
- 正确写法:使用Update自己管理状态
- 携程等待
- 错误写法:大量使用yield return new WaitForEndOfFrame();或者new WaitForSeconds(3f)或者new WaitForFixedUpdate()等
- 正确写法:保存一个new等待对象,重复使用
- New 对象
- 错误写法:循环内部不断new 对象
- 正确写法:使用对象池循环使用
- 频繁显/隐 SetActive
- 错误写法:频繁调用
- 正确写法:可以移到相机视野外或者设置透明度
- 频繁操作组件方法/属性
- GetComponent
- AddComponent
- Find/FindWithTag
- GetTag
- Camera.main 内部使用的FindGameObjectsWithTag()
- Object.name
- 错误写法:频繁调用
function Update()
local cube = CS.UnityEngine.GameObject.Find(“Cube”)
cube.transform:Rotate(CS.UnityEngine.Vector3(0, 90, 0) * CS.UnityEngine.Time.deltaTime)
end
void Update()
{
Camera.main.orthographicSize //Some operation with camera
} - 正确写法:第一次获取之后缓存之后复用
– 正确写法:在Start中获取组件并缓存
function Start()
self.cube = UnityEngine.GameObject.Find(“Cube”).transform
end
– 在Update中使用缓存的组件
function Update()
self.cube:Rotate(UnityEngine.Vector3(0, 90, 0) * UnityEngine.Time.deltaTime)
end
private Camera cam;
void Start()
{
cam = Camera.main;
}
void Update()
{
cam.orthographicSize;
}
- SendMessage
- 错误写法:直接使用SendMessage方法,内部调用了反射
- 正确写法:使用事件系统
6.字符串处理
- 大量字符串处理
- 错误写法:大量字符串拼接、切割
void Update()
{
text.text = "Player " + name + " has score " + score.toString();
} - 正确写法:尽量减少这种使用场景,用stringbuilder或者用ZString替代(原理:基于Span动态数组实现的0gc字符串方案)
void Start()
{
text = GetComponent();
builder = new StringBuilder(50);
}
void Update()
{
builder.Length = 0;
builder.Append("Player “);
builder.Append(name);
builder.Append(” has score ");
builder.Append(score);
text.text = builder.ToString();
}
- 错误写法:大量字符串拼接、切割
- 大量使用正则表达式
- 使用linq查询
- 错误写法:
var query = from item in myList
where item.SomeProperty == someValue
select item
linq会分配临时空间 - 正确写法:使用for循环遍历
- 错误写法:
7.C#、Lua数据传递交互
- 错误写法:把字符串当key传入,例如:
luaState.Push(MBuffMgr.UID_str);
luaState.Push(buff.UID);
或者
theSubTable[MBuffMgr.UID_str] = buff.UID; - 正确写法:使用数组而不是哈希表,例如
theSubTable[1] = buff.UID
或者使用枚举
theSubTable[(int)EBuffAttr.UID] = buff.UID
8.物理接口的使用
使用物理接口例如:Physics.Raycast、Physics.SphereCast、Physics.BoxCast和Physics.CapsuleCast等方法
- 错误写法:不注意检测长度和检测layer
- 正确写法:正确评估检测长度和设置必要的检测layer,减少不必要的物理计算量
9.同步异步接口使用
例如资源加载接口
- 不推荐写法:使用同步接口
- 推荐写法: 使用异步接口
10.分帧加载
- 错误写法:
length比较大
for (int i = 0; i < prefabs.Length; i++)
{
Instantiate(prefabs[i], parent);
} - 正确写法:
IEnumerator LoadPrefabs()
{
var waitForOneSecond = new WaitForSeconds(1);
for (int i = 0; i < prefabs.Length; i++)
{
yield return waitForOneSecond; // 每帧延迟1秒,可以根据需要调整
Instantiate(prefabs[i], parent);
}
}
11.注意Lua与C#交互传递Unity特有的值类型
Lua调用C#函数时,涉及到参数的传递和返回值的处理,这些操作都会产生性能开销。特别是当涉及到Unity特有的值类型(如Vector3、Quaternion等)时,性能消耗更大,因为需要进行类型转换
-
错误写法:
function Update()
local cube = CS.UnityEngine.GameObject.Find(“Cube”)
cube.transform.rotation = CS.UnityEngine.Quaternion.Euler(0, 45 * CS.UnityEngine.Time.deltaTime, 0)
end -
正确写法
function Start()
self.cubeTransform = CS.UnityEngine.GameObject.Find(“Cube”).transform
end
– 在Update中使用缓存的组件
function Update()
LuaUtil.SetRotation(self.cubeTransform, 0, 45 * UnityEngine.Time.deltaTime, 0)
end
C#的封装方法
public static SetRotation(transform, float x, float y, float z)
12.lua判空的写法
判空防止==误写成=
- 不推荐写法:if theMap == nil then
- 建议写法:if nil == theMap then
13.代码规范
建议按照约定熟成的方式,最大的收益方便搜索
- 驼峰命名
- 左右空格
- 枚举类型使用E开头
空格不要多也不要少
错误写法:table[i +1] =1
正确写法:table[i + 1] = 1
14.不要写立即数
影响代码阅读和统一修改
- 错误写法:
table[1] = 1 – 前面的1代表索引1,后面的1表示刷新标记 - 正确写法:
FlagIndex = 1
RefreshFlag = 1
table[FlagIndex] = RefresFlag
15.注意频繁调用函数参数量的控制
- 推荐写法
最好不要超过4个参数
16.优先使用static函数导出,减少使用成员方法导出
道理大家都懂
17.控制成员方法导出数量
不必要导出的函数用[NoToLuaAttribute]标记,减少lua虚拟机注册的函数数量
- 正确写法
[NoToLuaAttribute]
public void InsertBuffObj(MBuff buff, bool isTarget)
{
}
18.使用哈希值而不是字符串
针对Animator、Material或Shader属性进行内部寻址,推荐采用哈希的写法,例如Animator.StringToHash、Shader.PropertyToID,项目中写字符串都要谨慎,能不写就不写
- 错误写法
void Start()
{
anim = GetComponent();
anim.SetFloat(“Speed”, 1.0f);
} - 正确写法
void Start()
{
anim = GetComponent();
// 将字符串参数名转换为哈希值
int speedHash = Animator.StringToHash(“Speed”);
// 然后可以使用这个哈希值来设置参数值
anim.SetFloat(speedHash, 1.0f);
}
19.命名规范
- 不推荐使用2、4命名,可以用To、For
- 不要使用拼音,用最容易想到的单词
20.同时设置坐标和旋转
- 推荐写法
Transform.SetPositionAndRotation
GameObject.Instantiate(prefab, parent, position, rotation) - 不推荐写法
分开设置好几次属性
21.物理检测使用NoAlloc接口
- 错误写法
var layerMask = ~0;
if (Physics.Raycast(transform.position, transform.forward, m_Results, Mathf.Infinity, layerMask) > 0)
{
}
Physics2D. CircleCast();
- 正确写法
var layerMask = ~0;
if (Physics.RaycastNonAlloc(transform.position, transform.forward, m_Results, Mathf.Infinity, layerMask) > 0)
{
}
Physics2D. CircleCastNonAlloc();
22.数据配置
- 表格数据配置不要使用拼接的方式在运行时分割
- 数据存储不要采用json、xml等配置,推荐使用二进制或者scriptable object
23.不要出现空的Unity事件
- 错误写法:
void Update()
{
}
24.尽可能减少每帧运行的代码
考虑代码是否必要每帧执行,如果不需要的话可以控制调用频率
- 推荐写法
private int interval = 3;
void Update()
{
if (Time.frameCount % interval == 0)
{
Function();
}
}
25.避免初始化函数处理太复杂的逻辑
例如Awake、Start、OnEnable中处理复杂的逻辑,增加启动加载时间
26.调用统一的日志接口
调用统一接口方便宏定义开关管理
- 错误写法
print(“这是一条日志”)
Debug.log(“这是一条日志”) - 正确写法
LogManager.Print(“这是一条日志”)
27.避免运行时添加组件
在运行时AddComponent需要一些开销,因为要检查是否重复组件或者需要其他依赖组件
- 推荐做法
采用预制件
28.vector距离判断
如果只是距离判断推荐使用
- 不推荐写法
Vector3.Magnitude(vec) - 推荐写法
Vector3.SqrMagnitude(vec)
29.向量乘
向量乘需要耗时多一点,尽可能少的做向量运算
- 不推荐写法
Vector3 c = 3 * Vector3.one * 2; - 推荐写法
Vector3 c = 3 * 2 * Vector3.one;
30.shader数据精度
能用小的就不用大的
Float(32bit) (位置与纹理坐标信息)
Half(16bit)(纹理坐标与颜色信息)
Fixed(11bit)(颜色信息)(SRP下不支持)
31.除法转换成乘倒数
频繁的做除法运算,可以改成乘以倒数,属于指令集优化,硬件上乘以倒数比除执行效率高
- 不推荐写法
var res = a / b;
Var res = a / 5f; - 推荐写法
var res = a * (1 / b);
var res = a * 0.2f;
除法运算性能开销时间是乘法的100倍(官方说法)
32.双重循环
不得已要双重循环,大循环在内部
- 不推荐写法
for (int i = 0; i < numTests; i++)
{
for (int j = 0; j < 2; j++)
{
int k = i * j;
}
} - 推荐写法
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < numTests; j++)
{
int k = i * j;
}
}
[图片]
33.string操作
多次字符串操作,推荐使用stringbuilder,避免new string产生gc,如果stringbuilder能确定长度,最好先指定长度,也可以用ZString库
34.struct替代class
能用struct的就用struct,实在用不了再考虑class
35.系统自带默认值
例如Quaternion,Vector3等默认值全局静态只读变量,全局只有一个,减少一次new的操作
- 不推荐写法
Vector3 v3 = new Vector3(0, 0, 0); - 推荐写法
Vector3 v3 = Vector3.zero;
37.避免装拆箱
装箱:值类型->引用类型
int value = 11;
object obj = value;
拆箱:引用类型->值类型
object obj = 42;
int value = (int)obj;
- 不推荐写法
循环内部产生拆箱、装箱的写法
38.正确使用对应的数据结构
大量的数据查询操作
- 错误写法
for(int i=0; i<1000; i++)
{
if(list.Contains(i))
{
//todo
}
}
还有不要用for循环遍历查找luatable的数组表(数据量比较大) - 正确写法
用HashSet结构或者lua哈希表
39.循环修改值
- 错误写法
foreach(var item in array)
{
if(item.id == xxx)
{
item.xxx = xxx
}
} - 正确写法
for(int i = 0;i < array.count; i++)
{
if(array[i].id == xxx)
{
array[i].xxx = xxx
}
}
40.循环删除
倒序删除
- 推荐写法
// 假设有一个List
List myList = new List { 1, 2, 3, 4, 5 };
// 倒序删除所有偶数元素
for (int i = myList.Count - 1; i >= 0; i–) {
if (myList[i] % 2 == 0) {
myList.RemoveAt(i);
}
}
- 不推荐写法
正序删除
41.获取组件
在Unity中,GetComponent方法有三种重载方式:GetComponent()、GetComponent(Type type)和GetComponent(string type)。根据搜索结果,推荐使用泛型方式GetComponent()
- 推荐写法
GetComponent()
效率最高,耗时最少
42.跳过当前帧
- 不推荐写法
yield return new WaitForFrame(); - 推荐写法
yield return null;
yield return 0;
43.代码编写原则
- 推荐原则
- 尽早返回:如果函数在执行过程中遇到可以立即确定结果的条件,应立即返回,而不是继续执行更多代码才返回
- 避免深层嵌套:减少代码嵌套层数,可以提取函数,使用循环控制语句等方式来实现,警惕递归调用
- 单一出口点:尽量让函数只有一个返回点,减少代码复杂性,使得函数执行流程更加清晰
- 函数职责单一:确保一个函数只做一件事情,减少函数内部复杂度和耦合度,使得函数更容易理解和维护,功能通过若干单一函数组装
- 使用断言:检测函数参数的合法有效性,如果不合法则立即抛出异常或返回
44.避免单帧大循环逻辑处理
CPU瞬时压力过大,造成Big Jank
- 不推荐写法
for(int i = 0;i < 100;i ++)
{
}
- 推荐策略
- 分帧处理
- 算法处理,例如二叉树、四叉树、AOI等算法策略
45.碰撞检测
- 不推荐写法
collider碰撞检测 - 推荐写法
vector距离检测
46.异步编程
- 不推荐写法
void Start()
{
StartCoroutine(DelayedAction());
}
// 协程方法,延迟2秒后执行
IEnumerator DelayedAction()
{
yield return new WaitForSeconds(2.0f); // 等待2秒
Debug.Log(“Delayed action executed after 2 seconds”);
}
- 推荐写法
async void Start()
{
await Task.Delay(2000);
}
47.lua代码变量
- 错误写法
都写成全局变量
48.扩展原方法/类
扩展模板代码方法,或者引擎内部方法
- 错误写法
生成模板代码直接生成class - 正确写法
- 生成partial class的模板代码方法,以便提供扩展能力
- 使用扩展方法
49.谨慎频繁调用强制垃圾回收
- 不推荐写法
定期执行Resources.UnUsedAssets() 或者lua的collectgarbage(“collect”) - 推荐写法
- 资源采用引用计数管理和释放,不需要频繁粗暴调用Resources.UnUsedAssets(),会造成很大的帧率jank
- 手动在合适的时机调用gc方法
50.向量浮点数运算
- 不推荐写法
Vector3 pos1 = new Vector3(1,2,3);
Vector3 pos2 = new Vector3(4,5,6);
var pos3 = pos1 + pos2;
var pos4 = pos1 * 2f; - 推荐写法
var pos3 = new Vector3(pos1.x + pos2.x, pos1.y + pos2.y, …);
var pos2 = new Vector3(pos1.x * 2f, pos1.y * 2f, …);
51.lua UI变量查找
- 不推荐写法
find的方式 - 推荐写法
Editor中Prefab绑定,lua使用
52.tag比较
- 不推荐写法
gameObject.Tag == “MyTag”
属性访问,返回一个字符串跟另外一个字符串比较 - 推荐写法
gameObject.CompareTag(“MyTag”)
实用CompareTag方法,内部直接比较,避免一次字符串创建
53.能用localposition就不要用position
原因:获取position背后会做一系列计算,因为涉及到全局坐标,需求中能用localposition就用这个
持续更新,欢迎补充…