本篇文章的内容以各种tips为主,不间断更新
2022/2/4 最近更新:使用WeakReference弱引用监测对象生命周期
1.Unity Runtime Tips
在代码中指定,暂停当前帧
Debug.Break或者Debug.LogError然后在控制台勾选报错暂停。
快速查看非闭合的MeshCollider
关闭MeshRenderer组件,即可显示出Collider的网格。
强制去除当前脚本警告
#pragma warning disable 0168
此示例可以清除变量未使用警告,具体对应警告ID。并且该预编译指令只对本文件有效。
也可以包裹使用
#pragma warning disable 0168 .... #pragma warning restore 0168
一种更为简短的惰性字段初始化写法
object mObj; object Obj => mObj ??= new object();
之前一直这么写:
object Obj { get { mObj = mObj ?? new object(); return mObj; } }
对继承链非MonoBehaviour的类,使用外部单例类创建单例实例
在游戏编程精粹1中(一种自动的Singleton工具)提到了这种方法
通常我们使用Singleton<T>基类来进行单例封装,但若继承链不是直接继承自MonoBehaviour的,
就无法使用这种封装,需要重新手写单例。通过外部单例类,你可以继续使用封装好的单例逻辑,
只需要手写外部暴露字段和手动控制何时销毁即可:
public class OtherBaseClass : MonoBehaviour { } public class Foo : OtherBaseClass { public static Foo Instance => ExternalSingleton<Foo>.Instance; void Awake() { ExternalSingleton<Foo>.Initialization(this); } void OnDestroy() { ExternalSingleton<Foo>.Release(); } }
当然,单例类里处理初始化逻辑也可以,或者外部代码调单例类做Release也行,仅提供一个思路。
在Lambda迭代集合时拿到索引
使用Linq或者ForEach等方法迭代集合时,无法拿到索引,虽然可以外面再嵌一层IEnumerator,
但也可以用Enumerable.Range先创建数字集合再遍历,从而拿到索引,让代码简洁些
(注:此方法有性能问题,只建议在编辑器下使用)。
int[] myArr = new int[32]; Enumerable.Range(0, myArr.Length).Where(i => myArr[i].Foo);
无重复随机数创建
public int EliminateRepeatRandom(int last, int min, int max) { var current = Random.Range(min, max); if (last == current) return (current + (int)Mathf.Sign(Random.value) * Random.Range(min + 1, max - 1)) % max; else return current; }
取模循环
我们知道对数组长度取模可以做到循环内位移:
for (int i = 0, iMax = 10; i < iMax; i++) { Debug.Log(i % 5);//0,1,2,3,4,0,1,2.. }
但是只能支持++i这样正向位移,若为--i则需要另行处理。
而套用如下方式,负数也可通用:
for (int i = 0, iMax = 10; i < iMax; i++) { Debug.Log(((-i) + iMax) % 5);//0,4,3,2,1,0,4,3,2.. }
如果不想让循环数字从0开始,可以-n取模后+n:
int a = 3; int b = 10; for (int i = 0; i < 100; i++) { Debug.Log((i % (b - a)) + a); //3,4,5,6,7,8,9,3,4.. }
对常量表数据进行分组,并支持Mask判断
使用与运算可以判断该Mask是否包含目标字段:
int testMask = kSchoolStudent1 | kSchoolStudent2; Debug.Log((testMask & kSchoolStudent1) == kSchoolStudent1); //true
但常量表如果有分组功能会更实用些,因此要改下与运算逻辑(kSchoolStudents=kSchoolStudent1 | kSchoolStudent2...)
Debug.Log((testMask & kSchoolStudents) > 0); //true Debug.Log((testMask & kSchoolTeachers) > 0); //false
这样只需和分组数据进行与运算,并判断结果是不是>0,即可判断该Mask是否包含分组内的字段。
使用C# 7.0的二进制表示(binary literals)功能,设置Mask的值
C#7.0之后可以用0b开头表示二进制值,并可配合_下划线分割数字。下面就写一个Mask测试一下:
int mask = 0b_0000_0111; Debug.Log((mask & 2) == 2);//true Debug.Log((mask & 4) == 4);//true Debug.Log((mask & 8) == 8);//false Debug.Log((mask & 16) == 16);//false
序列化Property属性字段
建议在unity2020或以上版本使用,对字段名称显示有修正处理
[field: SerializeField] public int foo { get; private set; }
但使用这种方式很容易导致写自定义编辑器时找不到这个序列化字段名,
可以shift在unity编辑器下左键该字段进行查看。
使用Monitor.TryEnter代替lock关键字,做到线程超时监测
lock关键字可在一个线程进入后阻止其他线程进入
lock (mLockObj) { //do something.. }
但若该线程进入后一直没退出,则会造成其他线程始终无法进入。
因此使用Monitor.TryEnter,可支持超时监测(lock关键字其实就是Monitor):
try { bool noTimeout = Monitor.TryEnter(mLockObj, 5000); if (noTimeout) { //do something... } else { throw new TimeoutException(); } } finally { Monitor.Exit(mLockObj); }
经测试,相对于lock,会多一点性能开销。
null-forgiving operator使用空容忍操作符
应该是一个冷知识,在使用可空类型时编译器会提示要加null的if判断,但若确定一定不为空,
可加!提示编译器(其他情况下的警告有些也可用此方法避免)。
代码健壮性改善 - 在创建流stream时,指定是否可写
在创建流时,可指定writable参数以标名该流是否可写入,以增强代码健壮性
byte[] buffer = new byte[32]; Stream stream = new MemoryStream(buffer: buffer, writable: false);
代码健壮性改善 - 对于可能溢出操作,使用checked,unchecked代码块
C#编译器在处理静态可能溢出的代码时,会自动报错:
int a = int.MaxValue + 1;//报错
而对于动态的代码,则不会报错而且直接算作溢出处理:
int arg = int.MaxValue; int a = arg + 1; //不报错,直接溢出处理 Debug.Log(a);
因此,直接使用checked、unchecked代码块指名是否需要溢出,是一个较好的习惯:
checked//需要溢出检查 { //... } unchecked//不需要溢出检查 { //... }
使用更为安全的后台线程
c#创建线程时可选择前台或后台线程;当主线程结束时,前台线程堵塞,会导致主线程也堵塞,无法结束。
而设置为后台线程,当主线程结束时,后台线程即使堵塞也会直接结束。
开启后台线程;只需要在线程Start之前设置IsBackground字段即可:
mThread.IsBackground = true;
编辑器窗口滑动缩放(相较直接滑动滚轮步幅更小)
Alt+鼠标右键滑动
编辑器Scene窗口,使镜头始终追踪目标物体
选择需要被追踪的物体,按Shift+F即可
编辑器Scene窗口,便捷操控3D场景的方法
按住鼠标右键,wasdqe按键移动,分别对应3个轴向
在SceneView窗口按住鼠标右键的同时滑动滚轮;可以修改移动的幅度。
将选中物品立刻移动到编辑器相机位置
ctrl+shift+f
显示当前选中物体的网格
在gizmos中勾选Selection Wire
使用快捷键shift+h快速solo显示物体
不打开Prefab快速修改Prefab属性字段
在修改的字段上右键,选择Apply to Prefab \'..\'即可。
Color字段和Gradient字段添加HDR色彩支持
使用GradientUsage属性和ColorUsage属性,例如:
[ColorUsage(true, true)] public Color colorField;
而GradientUsage则需要2018.3以上版本:
[GradientUsage(hdr: true)] public Gradient myGradient;
打印Vector3类型,但不保留2位小数
vector3有一个重载,可以指定format,其中f10就是保留到小数10位
xx.position.ToString("f10")
比较浮点数一致
Mathf.Approximately
编辑器下被UNITY_EDITOR预编译指令包裹的序列化字段,打包时其值是否会被打包进去?
通过文本编辑器查看打包后的场景文件
这种丢失依赖的冗余字段值,并不会被打包进去(unity 2021.1)
删除Project面板里文件夹的展开状态
在Library/expandedItems,删除这个文件会重置展开状态
N卡显存占用查看
C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe
控制台内执行
Unity GI缓存目录
C:\Users\...\AppData\LocalLow\Unity\Caches\GiCache
unity资源商店缓存目录
C:\Users\...\AppData\Roaming\Unity
unity资源商店下载的资源包目录
...\AppData\Roaming\Unity\Asset Store-(具体unity版本).x\
Unity打包后Log日志文件存放路径
C:\Users\XXXX\AppData\LocalLow\DefaultCompany(项目所属的Company Name)
在Unity中使用快捷键重命名
按F2
Unity节点视图中快速定位回中心位置
按F(Animator、VisualEffectGraph等都支持)
从中间删除数组元素,或者复制数组中间元素
复制中间元素:ctrl+d
从中间删除数组元素:shift+delete
或者右键数组中某个元素也会出现操作选项
判断目标是否在相机的平截头体内
var planes = GeometryUtility.CalculateFrustumPlanes(Camera.main); var isContain = GeometryUtility.TestPlanesAABB(planes, bounds); if(isContain) { //in frustum.. }
使用时记得调用缓存数组的重载方法,以消除GC Alloc。
多场景加载卸载时,LightProbe光照探针残留问题
调用接口手动更新即可。
LightProbes.Tetrahedralize();
自定义ScriptableObject所生成对象的图标
只需要为原始ScriptableObject脚本对象赋上图标即可
清空StringBuilder
StringBuilder sb = new StringBuilder(); sb.Length = 0;
有多种方法,但修改Length属性效率最高,参考这位园友的测试:https://www.cnblogs.com/SpiderKevin/p/3891425.html
Process执行的程序当前目录不正确问题(例.bat文件在d盘,但.bat取到的当前目录是unity文件的目录)
var cacheDirectory = Directory.GetCurrentDirectory(); Directory.SetCurrentDirectory("your directory"); Process.Start("..."); Directory.SetCurrentDirectory(cacheDirectory);
执行之前设置一次当前目录即可。
获取当前项目中的所有程序集
AppDomain.CurrentDomain.GetAssemblies()
DOTS中NativeArray不同类型数组拷贝(例如float4x4[] > matrix4x4[])
Reinterpret方法可以对数组元素进行类型转换,然后调用CopyTo即可
NativeArray<float4x4> a = new NativeArray<float4x4>(3, Allocator.TempJob); a[0] = float4x4.TRS(new float3(1f, 2f, 3f), quaternion.identity, new float3(1f, 1f, 1f)); a[1] = float4x4.TRS(new float3(1f, 2f, 3f), quaternion.identity, new float3(1f, 1f, 1f)); a[2] = float4x4.TRS(new float3(1f, 2f, 3f), quaternion.identity, new float3(1f, 1f, 1f)); Matrix4x4[] b = new Matrix4x4[3]; a.Reinterpret<Matrix4x4>().CopyTo(b); for (int i = 0; i < b.Length; i++) { Debug.Log(b[i]); } a.Dispose();
DOTS中可通过NativeArray.Copy只拷贝部分改动元素,提升性能
NativeArray.Copy(src, srcIndex, dst, dstIndex, length);
对while进行死循环监测
我们可以增加一个结构体在外部对while进行死循环监测
WhileLimit结构体对象(可使用ref结构体):
using System; public ref struct WhileLimit { public static bool sReleaseMode = false; public const int kDefaultLimitNum = 99999; private int mCounter; private int mLimitNum; public WhileLimit(int limitNum) { mCounter = 0; mLimitNum = limitNum; } public bool Check(bool @bool) { if (sReleaseMode) return @bool;//打包时将该静态变量开启,跳过检查 ++mCounter; int limitNum = mLimitNum == 0 ? kDefaultLimitNum : mLimitNum; if (mCounter > limitNum) { throw new Exception("Infinite loop caught!"); } return @bool; } }
使用:
void Start() { WhileLimit whileLimit = new WhileLimit(); while (whileLimit.Check(true)) { //... }//运行后控制台报出异常:Infinite loop caught! }
使用WeakReference弱引用监测对象生命周期
c#提供了弱引用,我们可以用其监听对象是否被释放,弱引用WeakReference类似C++的weak_ptr:
//WeakRefTest.cs public class WeakRefTest : MonoBehaviour { System.Object mObj; WeakReference<System.Object> mWeakReference; private void Awake() { mObj = new object(); mWeakReference = new WeakReference<object>(mObj); } private void OnGUI() { GUILayout.Box("Try Get Object: " + mWeakReference.TryGetTarget(out object target)); if (GUILayout.Button("GC")) { mObj = null; System.GC.Collect(); } } }
如上测试脚本,点完GC后,一会后返回false。
2.Shader & Graphics Tips
脚本更改材质球参数后,需要点一下材质球才起效果
参数没改全,检查Keyword、OverrideTag等。如果是点开材质球又被改了回去,说明ShaderGUI用的字段没改。
URP自定义扩展ShaderGUI,Keyword关键字设置无效
BaseShaderGUI的MaterialChanged方法里会清空所有关键字,可能是为了防止造成无用关键字堆积。
需要先设置字段(例如加一些float字段)将Keyword开关值缓存,然后重写MaterialChanged方法,再读取缓存字段,
设置上Keyword即可。
快速反转距离图像的方法
在和一点求距离并作为色彩输出后会得到一个中心黑色周围白色的扩散圆,
但往往需要中心白色周围黑色的圆图像。并且需要对扩散范围进行约束。
那么可以通过smoothstep插值,并将插值函数的最小值最大值对调,就可以得到一个OneMinus反转的插值结果,
也就可以得到范围不是无限的白色边缘模糊圆点了:
float dist = distance(i.uv, 0.5);
float d = smoothstep(0.15, 0.1, dist);
return d;
解决Unity打光穿墙问题
Unity里实时光没有做遮挡判断,所以肯定会穿墙。Lightmap烘焙的情况下,可以用Area面光来打光
,它是面光源可以更好的模拟间接光照,并且有遮挡检测。而其他几种灯光烘焙情况下都会穿墙:
Rt绘制到指定Mipmap工具脚本
单独绘制到Rt的某个mipmap需要调SetRenderTarget、画Quad,比较麻烦。
Graphics.CopyTexture不行的也可以用这个方法。
写了一个简单的工具脚本:
解决Shader里Alpha混合的黑边问题
Shader里将贴图和Alpha通道进行混合时,若不预乘会有黑边问题。
直接Lerp混合(有黑边):
srcCol.rgb = lerp(srcCol, dstCol, dstCol.a);
可以用Alpha预乘解决该问题:
srcCol.rgb = dstCol.rgb * dstCol.a + (srcCol.rgb * (1.0 - dstCol.a));
对于静态物体,使用对象空间(Object Space Normal)法线防止Lod跳变
可以想象模型有一个面,法线贴图让其法线向上偏移45度。
此时增加一个lod级别,该面片与另外一个面合并,变成一个向上倾斜的新面,
若用切线空间则法线在原偏移上又向上偏移了45度;而对象空间则依然不变。
在HDRP下可直接设置(需烘焙对应的对象空间法线):
对于非金属材质使用Specular setup的shader模式
金属材质会有高光染色效果,而Specular setup的高光模式则没有。
例如制作下雨天湿滑路面不注意这种区别,会导致地面有奇怪的金属感。
(官方文档(链接)说的有点模糊,但实际测试两种setup mode表现效果确实是不一样的)
区别见下图(HDRP在Material Type选项中设置):
去除恼人的standard shader高光
我们在用standard shader调制反射质感时,会有一块高光区域非常碍眼。
可以在Specular Hightlights处将其关闭
Shader中直接通过M矩阵获得原点的世界坐标
float4 pivotWS = float4(unity_ObjectToWorld[0].w, unity_ObjectToWorld[1].w, unity_ObjectToWorld[2].w, 1.0);
从而代替做一次模型空间变换的做法:
float4 pivotWS = mul(unity_ObjectToWorld, (float4)0);
Shader中考虑用_SinTime、_CosTime进行优化
使用_Time内置参数时经常需要作为三角函数参数运算,但往往忽略了unity内置了正弦波和余弦波时间的参数。
https://docs.unity3d.com/462/Documentation/Manual/SL-BuiltinValues.html
使用Reoriented Normal Blend正确混合两张法线
相对于线性混合,Reoriented Normal Blend可以达到正确的混合结果,虽然会增加计算量(具体见知乎,selfshadow)。
Unity的新管线下内置了RNM混合函数,位于CommonMaterial.hlsl下,代码内容如下:
// ref http://blog.selfshadow.com/publications/blending-in-detail/
// ref https://gist.github.com/selfshadow/8048308
// Reoriented Normal Mapping
// Blending when n1 and n2 are already \'unpacked\' and normalised
// assume compositing in tangent space
real3 BlendNormalRNM(real3 n1, real3 n2)
{
real3 t = n1.xyz + real3(0.0, 0.0, 1.0);
real3 u = n2.xyz * real3(-1.0, -1.0, 1.0);
real3 r = (t / t.z) * dot(t, u) - u;
return r;
}
设置合理的各向异性过滤级别Aniso Level
贴图Aniso Level(Anisotropic filtering level)设置,可以改善mipmap贴图造成的远处模糊
但会增加图形硬件的性能开销,对该值要求不高的贴图,我们可以降低它的值进行优化
设置合理的Filter Mode
Bilinear 双线性过滤会对周围像素进行一次平均采样
Trilinear 三线性过滤会对前后Mipmap级别进行一次平均采样(即多一个z轴的线性过滤)
若贴图应用到工程后不会产生Mipmap导致的过渡硬边,可切换成Bilinear以节省性能
3.Unity Optimize Tips
遍历list或者数组时,缓存count,可以减少调用次数(10万次循环测试)
for (int i = 0, iMax = temp.Count; i < iMax; i++) temp[i].GetHashCode();
在极端情况下,直接用枚举器遍历字典会更快,而且不会产生GC
UnityEngine.Profiling.Profiler.BeginSample("-----1"); foreach (var item in temp) item.Value.GetHashCode(); UnityEngine.Profiling.Profiler.EndSample(); UnityEngine.Profiling.Profiler.BeginSample("-----2"); foreach (var item in temp.Values) item.GetHashCode(); UnityEngine.Profiling.Profiler.EndSample(); UnityEngine.Profiling.Profiler.BeginSample("-----3"); using (var handle = temp.GetEnumerator()) { while (handle.MoveNext()) handle.Current.Value.GetHashCode(); } UnityEngine.Profiling.Profiler.EndSample();
使用Instantiate初始化参数去实例对象
public class Foo : MonoBehaviour { void Start() { var sw = new Stopwatch(); sw.Start(); var go = new GameObject(); go.transform.position = Vector3.one; for (int i = 0; i < 10000; i++) { var instancedGO = Instantiate(go); instancedGO.transform.SetParent(transform); instancedGO.transform.localPosition = Vector3.zero; instancedGO.transform.localRotation = Quaternion.identity; instancedGO.transform.localScale = Vector3.one; //102ms //Instantiate(go, transform.position, transform.rotation, transform);//74ms } sw.Stop(); UnityEngine.Debug.Log("sw: " + sw.ElapsedMilliseconds); } }
可以实例化出来再赋值,也可以使用参数直接复制
但是1万次循环测试下两种方法差了20多ms
当.Net常用数据结构栈和队列都可以用时,优先考虑用栈
例如做对象池复用,因为Queue(队列)会维护_head和_tail两个变量,相较Stack(栈)数据结构操作略多,
Stack内部只有_size变量进行下标控制;因此Stack也会略快于Queue。
所以在处理对象池或是类似情况,可优先考虑用Stack。
使用SetPositionAndRotation,同时设置位置与旋转
在位置和旋转都需要设置的情况下,使用该方法可优于调用两次方法
transform.SetPositionAndRotation();
大于16个字符的情况下,设置StringBuilder的Capacity容量
C#的StringBuilder初始Capacity是16个字符,若使用时操作字符数大于16可手动设下参数,
防止重新分配Chunk。
StringBuilder sb = new StringBuilder(8192);
尽量使用ref直接返回结构体,而不是用返回值,防止额外拷贝
测试了返回值返回结构体和ref两种情况,100万次循环,ref快了10ms(结构体越大越明显)
float d = 0f; for (int i = 0; i < 100_0000; i++) { CustomStruct customStru = new CustomStruct(); customStru.A = i; customStru.B = i + 1; customStru.C = i + 2; //Calc1(ref customStru) 20ms customStru = Calc2(customStru);//30ms d = customStru.D; }
void Calc1(ref CustomStruct customStuct) //20ms { customStuct.D = customStuct.A + customStuct.B + customStuct.C; } CustomStruct Calc2(CustomStruct customStuct)//30ms { customStuct.D = customStuct.A + customStuct.B + customStuct.C; return customStuct; }
通过下标获得数组元素,用ref也比传统方法更快
ref TestStruct arr = ref mArr[i]; Debug.Log(arr.a); Debug.Log(arr.b);
虽然对于只读操作不太安全,但百万级的for循环下可以快10ms左右。
看IL也确实是非拷贝传递(左图ref形式):
使用string.Intern,string.IsInterned优化字符串
在c#中存在一个字符串驻留池的概念,静态的字符串会存放在池中,下次再遇到同样的字符串会用池内
的引用。以达到减少总字符串内存的目的。
例如如下情况,就会被识别并自动将"abc"加入到池中:
string str1 = "abc";
string str2 = "abc";
Debug.Log(object.ReferenceEquals(str1, str2));//true
但动态情况下则不行:
string str1 = "abc";
string strC = "c";
string str2 = "ab" + strC;
Debug.Log(object.ReferenceEquals(str1, str2)); //false
此时使用Intern手动指定加入池中,用IsInterned返回池字符串的引用:
string str1 = "abc";
string.Intern(str1);
string strC = "c";
string str2 = "ab" + strC;
str2 = string.IsInterned(str1) ?? str2;
//IsInterned方法若池内没有则返回null否则返回池内字符串引用
Debug.Log(object.ReferenceEquals(str1, str2)); //true
注意:驻留池中的字符串不要再用指针去修改,否则改到的将是驻留池中的地址。
对于需频繁改动数据的Mesh,调用MarkDynamic进行标记
调用MarkDynamic方法标记Mesh,以提示引擎内部优化:
meshObj.MarkDynamic();
使用__makeref关键字加速泛型类型的强制转换
__makeref为c#未公布关键字,相当于一个内部指针,结合__refvalue关键字,
可以转换成另一个指定类型的值,但是不能做相同字节数的类型转换,用来
强制转换泛型还是可以的,效率也比is(语法糖)更快:
void Foo_MakeRef<T>(T value) { TypedReference typedref = __makeref(value); int intValue = __refvalue(typedref, int); //前提是知道泛型一定为int传入,否则报错 intValue.GetHashCode(); }
和传统is进行类型转换测试对比:
for (int i = 0; i < 10_0000; i++) { //Foo_Is(i);//19ms Foo_MakeRef(i); //3ms }
此外,该关键字还可以为值类型的私有字段赋值:https://www.cnblogs.com/eaglet/archive/2010/05/26/1744213.html
显示实现struct结构的IEquatable接口字段,在比较时才没有GC
public struct MyStruct : IEquatable<MyStruct> { public int a; public int b; public int c; // bool IEquatable<MyStruct>.Equals(MyStruct other) // { // return a == other.a && b == other.b && c == other.c; // } //必须显示实现Equals接口方法才没有GcAlloc public bool Equals(MyStruct other) { return a == other.a && b == other.b && c == other.c; } //NoGcAlloc }
虽然显示实现相当于public方法了,但这样确实没有GC开销,隐式实现若加强制转换则一定有GC开销。
动态批次/GPU Instancing的优化方向
动态批次的合批操作依然是在渲染前处理,将网格较少的动态模型筛选后在渲染前使用CPU合并。
类似使用MeshBaker手动去合并动态对象,但是限制比MeshBaker多,所以存在关闭系统动态合批,手动处理的优化考量。
GPU Instancing需要硬件/底层接口支持,其内部类似于享元机制,通过索引访问差异的数据。
可以减少较多非GPU的额外开销,但缺点是绘制网格是固定的(如使用GPU Instancing代替以往Geom shader生成草坪,草的类型就被限定死几种了)。
在c#中使用内联特性(不保证一定会内联)
[MethodImpl(MethodImplOptions.AggressiveInlining)] void Foo()
在条件允许下,使用位运算代替乘除法
左移代替2为倍数的整数乘法:
var a = 50; var b = a << 1; //b = 100 50*2 b = a << 2; //b = 200 50*4
右移代替2为倍数的整数除法:
var a = 256; var b = a >> 1; //b = 128 256/2 b = a >> 4; //b = 64 256/4
经测试没有编译器优化的情况,并且有一定速度提升。
条件允许的情况下可用\'&与运算\'代替\'%取余\'运算
一个数二进制位都为1时,做与运算可以和取余一样的效果。并且
与运算速度会更快,因此可以做这一步优化。
例如只需要在某个区间内进行索引循环,但对循环区间没有具体要求:
for (int i = 0; i < n; i++) { Debug.Log((i & 7)); }//0,1,2,3,4,5,6,7,0,1,2,3,4,5,6...
如果用传统的取余会慢一点,1000万次for循环测试与运算114ms,取余143ms。
多Lerp操作可进一步公式化简
在调效果时往往会进行Lerp嵌套,例如:Lerp(Lerp(x,y,t)*2,y,t)
可将运算拆开进一步化简,例如0-1之间的抛物线是:
Lerp(Lerp(0,2,t), Lerp(2,0,t),t)
化简后是:4t(1-t)
关于Unity事件函数空调用
尽量少使用封装Unity事件的通用基类,这样会造成函数空调
unity会在C++层面检测是否实现某个事件函数(Start,Update,LateUpdate...etc)
如果没有这个函数则不会加入调用列表中
使用CustomSampler代替Profiler.BeginSample
虽然CustomSample需要额外创建一个sampler对象,但采样结果更准确。
链接:https://docs.unity3d.com/ScriptReference/Profiling.CustomSampler.html
public class ExampleClass : MonoBehaviour { CustomSampler sampler; void Start() { sampler = CustomSampler.Create("MyCustomSampler"); } void Update() { sampler.Begin(); // do something that takes a lot of time sampler.End(); } }
使用layerCullDistances和layerShadowCullDistances优化剔除
对于不重要的物件,我们可以设置不同相机Layer的剔除距离以提前剔除:
var distances = Enumerable .Repeat(Camera.main.farClipPlane, 32) .ToArray(); distances[12] = 3f;//Layer12的剔除距离为3 testCamera.layerCullDistances = distances;//!一定要以数组赋值,否则无效 testCamera.layerCullSpherical = true;//是否以球形为基准剔除
而对于平行光的阴影,也可以设置layerShadowCullDistances参数以设置灯光不同Layer的剔除距离:
testLight.layerShadowCullDistances = distances;
使用localPosition代替position
调用Position时背后会有许多操作执行,在代码允许的情况下可使用localPosition代替position
使用质数作为延迟量可避免最小公倍数出现的情况
例如设计某BOSS的AI时,特殊弹幕A的发射延迟为7,特殊弹幕B的发射延迟为11。
那么将低概率出现这两种特殊弹幕被同时发射的情况。
使用Matrix MultiplyPoint3x4而不是MultiplyPoint
对于非投影矩阵,使用MultiplyPoint3x4进行变换更快。
详见官方文档:https://docs.unity3d.com/ScriptReference/Matrix4x4.MultiplyPoint3x4.html
在条件允许下,使用CopyTo将List无GC的转为数组,而不是ToArray
var tempArray = new int[3]; void Test() { list.CopyTo(tempArray);//0 GC }
避免直接调用Camera.main
Camera.main内部会去调用FindGameObjectWithTag,1万次循环的测试下造成了1ms左右的开销
而缓存后大约在0.2ms
mCacheCamera = mCacheCamera ?? Camera.main; Profiler.BeginSample("Foo"); for (int i = 0; i < 10000; i++) mCacheCamera.GetHashCode(); Profiler.EndSample();
直接使用点乘进行向量投影
向量投影其内部通过点乘公式实现;但Unity默认的库会额外加一些安全性判断代码,若追求效率可直接改为点乘处理:
var value = Vector3.Dot(point, onNormal);
实时反射探针优化
如果需求必须使用实时反射探针,可以将其更新模式设为脚本驱动,在渲染前对反射内容进行模型lod替换或shader、材质上的替换
可达到一定优化作用。
阴影优化
在场景中我们可以使用lod模型来投射阴影,场景中对于一些阴影对画面美观影响不大的模型,我们可以关闭这些模型的阴影投射
来进行优化。
使用Canvas的Additional Shader Channels功能优化,避免多材质球打断UI合批
在uGUI中,ui元素使用不同材质球会打断合批。但往往又需要相同的shader特效
不同的参数(例如:包含多个元素的径向溶解动画、包含多个元素的Glitch效果)
因此可以借助Additional Shader Channels功能,将Shader特效的参数在设置UIVertex处传入,
从而避免打断合批。
uGUI可避免使用Image.FillAmount,用shader或LocalScale代替
FillAmount会导致网格重绘,若需要优化可考虑别的实现方式
uGUI Mask与RectMask2D使用上的取舍
uGUI的Mask组件可以做ScrollRect内滑动遮罩,或其他一些面板遮罩的情况。
除了Mask组件unity还提供RectMask2D组件,可做softness软边处理。而对于优化,总结如下:
1)当前Canvas及屏幕内只有一个Mask需求,用RectMask2D组件
2)Mask组件可以跨Mask合批,即多个Mask内的内容可以合批。当有多个Mask需求可以用Mask组件
参考出处(已测):https://blog.csdn.net/akak2010110/article/details/80964416
对于非重要音频勾选LoadInBackground后台加载
对于非重要音频(如一些环境音效)勾选LoadInBackground后台加载,并配合Preload Audio Data选项自动在场景进入后
执行后台加载(关于音频优化可阅读这位园友总结:https://www.cnblogs.com/nickcan/p/13019161.html)
注:若背景音乐的LoadType选用Streaming,则需要避免LoadInBackground和Preload Audio Data,
因为高质量音频可能会很大,需要从硬盘流式加载直接读,也就不需要这些预先缓存的优化操作;其次流式加载的
音频解码任务会被分配到一个新的线程上,会和常规音频线程分开执行。
(https://docs.unity3d.com/cn/2018.4/ScriptReference/AudioClipLoadType.Streaming.html)。
使用mipmap可视化工具查看当前分辨率下最佳匹配texel(纹素)是多少
例如某中距离物件在当前距离下应匹配512px的第4级mipmap,但因原始贴图尺寸不同,实际匹配了1024px的第4级mipmap。
此时若使用mipmap可视化工具则可检查当前可视距离下最佳mipmap尺寸是多少,
从而确定用多少分辨率合适,其次检查当前贴图分辨率是否存在浪费的情况。
下例使用ColoredMipmap插件并在HDRP管线下测试,实际上就是把尺寸写死进图片mipmap:
4.Unity Editor Tips
快速查看Serialize Field的路径字段
创建SerializeField需要得到该字段的yaml路径,按住shift鼠标右键字段,选择 Print Property Path即可打印。
通过设置横向布局,实现UI右对齐
此时该按钮为右对齐,手动调节按钮宽度即可
GUILayout.BeginHorizontal(); GUILayout.Label("", GUILayout.ExpandWidth(true)); if (GUILayout.Button("Right Align Button", GUILayout.Width(90f))) { //... } GUILayout.EndHorizontal();
在Editor下获得时间
EditorApplication.timeSinceStartup
获得编译器自打开到当前的时间。
Editor下获得滚轮滑动值
Event.current.delta
https://docs.unity3d.com/ScriptReference/Event-delta.html
注意要先判断当前鼠标按钮id,滚轮的id是1
获取Project面板中当前选中物体的路径
AssetDatabase.GetAssetPath(Selection.activeObject);
取Unity当前目录的路径(是Unity自身安装目录,取当前项目目录请用Directory.GetCurrentDirectory())
EditorApplication.applicationPath
EditorApplication.applicationContentsPath
显示对话框
EditorUtility.DisplayDialog
UnityDatabase拷贝文件
AssetDatabase.CopyAsset(需要拷贝文件,目标目录);
1.只能拷贝单个文件,不能拷贝目录
2.目标必须是目录路径,不能是文件路径
Editor状态下设置Scene窗口的相机位置
SceneView.lastActiveSceneView.pivot = ...;
SceneView.lastActiveSceneView.Repaint();
Editor下使用右键菜单
使用GenericMenu可以直接调出Unity的右键菜单
Editor下检查prefab是否都打了标签/读写标签Labels
AssetDatabase.GetLabels
AssetDatabase.SetLabels
Editor下拿到当前拖拽对象(跨窗口)
DragAndDrop.objectReferences
示例脚本:
if (Event.current.type == EventType.DragUpdated || Event.current.type == EventType.DragPerform) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (Event.current.type == EventType.DragPerform) { DragAndDrop.AcceptDrag(); var dragItem = DragAndDrop.objectReferences[0]; Debug.Log("dragItem: " + dragItem); } Event.current.Use(); }
如果是多选拖拽,数组里的就是所有选中拖拽的对象,否则就是一个。
Editor下控制控件的焦点
通常只要把focus设为空,就可以取消焦点
GUI.FocusControl("");
一些特殊的情况,比如弹出性组件,需要知道控件是否被改变过,而非值是否改变过
可以这么做,通过GetNameOfFocusedControl拿到Focus的name进行比较
var oldFocus = GUI.GetNameOfFocusedControl(); var changedIndex = EditorGUI.Popup(rect, index, array); var newFocus = GUI.GetNameOfFocusedControl(); if (newFocus != oldFocus) { //Do something... GUI.FocusControl(""); }
Editor下获得\'剪切\',\'拷贝\',\'撤销\'等命令
可以使用commandName获得
Event.current.commandName == "Copy"
EditorWindow窗口大小锁死后没有边框的解决方法
var window = GetWindow(typeof(MyWindow), true); window.minSize = new Vector2(960, 540); window.maxSize = window.minSize;
用GetWindow创建窗口时,第二个参数填true。创建为独立的工具窗口,即可恢复边框
Editor下Dirty掉当前修改过的场景对象内容
一般非场景对象用
EditorUtility.SetDirty(target);
而场景对象则用
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
Editor下BeginScrollView报错InvalidCastException
和调用顺序有关,也是Layout/Repaint和输入之间的问题。
改变调用先后顺序有可能解决。
在Editor下绘制自定义光标
比如缩放需要绘制缩放的自定义光标,滑动又需要滑动的
使用下方法即可绘制,Rect自定义Cursor区域的Rect。
需要每次OnGUI更新都调,而非只Add一次
EditorGUIUtility.AddCursorRect(Rect, MouseCursor.Pan);
Editor下双击打开资源(ScriptableObject/Console Log...)
通过挂载全局特性[OnOpenAsset]实现,可以用于双击ScriptableObject打开特定面板,还可以用于控制台日志的跳转重写。
[OnOpenAsset] public static bool OnOpenAsset(int instanceId, int line) { var obj = EditorUtility.InstanceIDToObject(instanceId); return true; }
Editor下的LayerMaskField
Unity并没有提供LayerMask控件,其Layer控件返回的只是层编号
下面是一个扩展的LayerMaskField:
public static LayerMask LayerMaskField(string label, LayerMask layerMask) { List<string> layers = new List<string>(); List<int> layerNumbers = new List<int>(); for (int i = 0; i < 32; i++) { string layerName = LayerMask.LayerToName(i); if (layerName != "") { layers.Add(layerName); layerNumbers.Add(i); } } int maskWithoutEmpty = 0; for (int i = 0; i < layerNumbers.Count; i++) { if (((1 << layerNumbers[i]) & layerMask.value) > 0) maskWithoutEmpty |= (1 << i); } maskWithoutEmpty = EditorGUILayout.MaskField(label, maskWithoutEmpty, layers.ToArray()); int mask = 0; for (int i = 0; i < layerNumbers.Count; i++) { if ((maskWithoutEmpty & (1 << i)) > 0) mask |= (1 << layerNumbers[i]); } layerMask.value = mask; return layerMask; }