先说垃圾回收这个老大难问题。Unity的Mono或IL2CPP环境里,GC触发时会冻结主线程,角色瞬间变PPT。对策嘛,首先得把堆分配降下来。比如循环体内尽量避免用LINQ,别图省事写什么"foreach(var item in list.Where(x=>x.isActive))",这每帧能生成一堆迭代器对象。改成老式for循环加if判断,内存分配直接归零。另外字符串拼接也别用"+"号,曾经有个UI模块因为频繁拼接待机文本,GC Alloc每秒飙到2MB,换成StringBuilder后立马降到200字节。
对象生命周期管理是另一个重灾区。好多新人爱在Update里用GetComponent<T>,这玩意儿本质是递归查找,帧率不掉才怪。正确的做法是在Awake里缓存引用,连Transform也别放过。我见过最离谱的案例是某个技能系统每帧调用GetComponent<Animator>二十多次,改成缓存后CPU耗时从8ms砍到0.5ms。要是遇到必须动态获取的情况,可以考虑弄个字典做组件池,用GameObject实例ID当钥匙,比全局Find快十倍不止。
数据结构的选择也很有讲究。Unity的Mathf函数虽然方便,但大量向量运算还是得用Unity.Mathematics里的float3。最近做特效粒子系统时对比过,同样处理十万个坐标点,Vector3需要120ms还伴随GC,换成float4只花40ms且零分配。值类型结构体一定要用readonly修饰,这样JIT编译器能直接内联操作。记得给结构体实现IEquatable接口,避免装箱带来的堆分配。
协程用不好反而会变成性能黑洞。Yield return new WaitForSeconds这种每次都会创建新对象,应该改成复用静态对象。有个取巧的办法是自定义YieldInstruction,比如战斗系统需要等待0.5秒时,可以预定义static readonly WaitForSeconds waitHalfSec = new WaitForSeconds(0.5f)。遇到需要每帧检测的条件,别用while循环配Yield return null,改成在Update里通过状态机控制,CPU缓存命中率能提升三成。
内存方面要注意资源加载的坑。Resources文件夹用多了会导致启动变慢,现在更推荐Addressables系统。之前有个场景切换卡顿问题,追查发现是Resources.UnloadUnusedAssets在作祟,后来改成按需加载+引用计数管理,内存波动从500MB降到50MB。对于脚本自身,记得把空数组改成Array.Empty<T>,连方法内静态字段也要警惕,这些都会在内存里赖着不走。
最后安利下调试利器——Unity Profiler的Deep Profile模式。有次查个神秘卡顿,发现是某个NPC脚本在OnTriggerStay里疯狂计算路径,改成触发进入时才计算就解决了。现在URP管线还支持Frame Debugger实时查看DrawCall合并情况,配合Scriptable Render Pipeline能自动剔除重叠渲染。记住优化不是毕其功于一役,要像老中医号脉那样定期用Profiler抓数据,毕竟实际设备上的表现和编辑器差着十条街呢。
总之C优化就像雕玉,得顺着Unity的性子来。从堆分配管控到数据局部性优化,每个环节省出0.1ms,叠加上百万次调用就是质变。保持代码洁癖,养成性能敏感思维,你的游戏至少能多扛三年设备迭代。
2817

被折叠的 条评论
为什么被折叠?



