Unity3D心得分享

本篇文章的内容以各种tips为主,不间断更新

 

2024/12/04 最近更新:调用RequestCharactersInTexture缓存字体以提升性能, 设置transform.hierarchyCapacity,防止分配拷贝内存以优化性能

 

 

1.Unity Runtime Tips


 

比较两个集合结构上是否相等

int[] array1 = { 1, 2, 3 };
int[] array2 = { 1, 2, 3 };
Debug.Log(((IStructuralEquatable)array1).Equals(array2, StructuralComparisons.StructuralEqualityComparer)); //output True

(出处:https://www.cnblogs.com/dudu/p/dotnetfx-IStructuralEquatable.html)

 

 

null-forgiving operator使用空容忍操作符

应该是一个冷知识,在使用可空类型时编译器会提示要加null的if判断,但若确定一定不为空,

可加!提示编译器(其他情况下的警告有些也可用此方法避免)。

 

 

取模循环

我们知道对数组长度取模可以做到循环内位移:

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..
}

 

 

注释中使用<c>..</c>标记注释内代码,IDE编辑器会用特殊字体显示。

/// <summary>
/// e.g: <c>foo.bar = 42</c>
/// </summary>

 

 

注释中使用<see cref="MyClass" />属性关联相关类并让IDE创建超链接(放在summary内)

/// <see cref="MyClass"/>

异常也可以使用cref(放在summary外):

/// <exception cref="ArgumentNullException">如果参数为 null,则抛出此异常。</exception>

 

 

在注释中创建无序表:

/// <summary>
/// <list type="bullet">
/// <item><description>first</description></item>
/// <item><description>second</description></item>
/// </list>
/// </summary>
public string foobar;

 

 

在注释中使用超链接:

/// <summary>
/// hyper link:
/// <a href="https://www.baidu.com">foobar</a>
/// </summary>
public string foobar;

 

 

注释中使用<remark>进行详细描述

/// <summary>
/// 这是简要描述
/// </summary>
/// <remarks>
/// 这是详细描述
/// </remarks>
public string foobar;

 

 

使用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

 

 

使用?.快速判断Func<bool>类对象是否为空

Func<int, bool> func = null;
if (func?.Invoke(12) ?? false)
    Debug.Log("NotNull and True!");
else
    Debug.Log("Null!");

 

 

使用C# 8的数组切片语法糖

经测试Unity2021版本可使用

取最后一个元素:

int[] a = new int[] { 1, 2, 3 };
Debug.Log(a[^1]);//3

遍历前3个元素:

var myarr = new string[] { "a", "b", "c", "d", "e" };
foreach (var item in myarr[0..3])
    Debug.Log(item);
//a,b,c

 

 

序列化Property属性字段

建议在unity2020或以上版本使用,对字段名称显示有修正处理

[field: SerializeField] public int foo { get; private set; }

但使用这种方式很容易导致写自定义编辑器时找不到这个序列化字段名,

可以shift在unity编辑器下左键该字段进行查看。 

 

 

高性能序列化Property属性字段

unity2022以上版本可以使用createProperty等属性实现高性能的Prop序列化

会用到PropertyBag新的序列化系统,DontCreateProperty特性可以省略掉内部get、set方法的生成

[field: SerializeField, DontCreateProperty]
[CreateProperty]
public float floatValue { get; set; }

 

 

使用PropertyBag序列化

unity2022以上版本可直接给类标记GeneratePropertyBag使用该序列化系统,

该系统避免反射,效率优于之前的编辑器字段序列化方式

[GeneratePropertyBag]
public class Class1 : MonoBehaviour
{
    [SerializeField] private int a;
    [SerializeField] private int b;
    [SerializeField] private int c;
}

 

 

构建C#自增常量表

class ConstantTable
{
    private static uint s_Counter;

    public static readonly uint ID1 = GenerateID();
    public static readonly uint ID2 = GenerateID();
    public static readonly uint ID3 = GenerateID();
    public static readonly uint ID4 = GenerateID();
    public static readonly uint ID5 = ResetAndGenerateID(1001);
    public static readonly uint ID6 = GenerateID();
    public static readonly uint ID7 = GenerateID();


    private static uint GenerateID()
    {
        return s_Counter++;
    }

    private static uint ResetAndGenerateID(uint baseId)
    {
        s_Counter = baseId;
        return s_Counter++;
    }
}


void Test()
{
    Debug.Log(ConstantTable.ID1);//0
    Debug.Log(ConstantTable.ID2);//1
    Debug.Log(ConstantTable.ID3);//2
    Debug.Log(ConstantTable.ID4);//3
    Debug.Log(ConstantTable.ID5);//1001
    Debug.Log(ConstantTable.ID6);//1002
    Debug.Log(ConstantTable.ID7);//1003
}

 

 

使用TypeId类型ID,构建类关联常量表

虽说用nameof且用字符串常量表也可以替代,但数字ID在网络传输等方面更常用,因此该方法仍有实用性。

public static class TypeCounter
{
    public static uint s_TypeCount;
}

static class TypeId<T>
{
    public static uint value = TypeCounter.s_TypeCount++;
}

public class Item1 { }

public class Item2 { }

public static class ItemsConstant
{
    public static readonly uint kItem1Id = TypeId<Item1>.value;
    public static readonly uint kItem2Id = TypeId<Item2>.value;
}

public class TestClass : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log(ItemsConstant.kItem1Id);//0
        Debug.Log(ItemsConstant.kItem2Id);//1
    }
}

 

 

多次调用反射查找CustomAttribute的情况下,缓存以提升性能

//缓存前:
var _ = class1.GetType().GetCustomAttribute<Attr1Attribute>();
//缓存后:
var _ = mCache[class1.GetType()];

 

 

活用Linq.Aggregate拼接字符串

(string, string)[] a = { ("张三", "北京"), ("李四", "北京"), ("王五", "上海") };

StringBuilder sb = GenericPool<StringBuilder>.Get();
a.Aggregate(sb, (x, y) =>
{
    x.AppendJoin(",", y.Item1, y.Item2);
    x.AppendLine();
    return x;
}, m => m);
Debug.Log(sb.ToString());
GenericPool<StringBuilder>.Release(sb);
//张三,北京
//李四,北京
//王五,上海

 

 

使用SerializeReference序列化多态字段或接口

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public abstract class AbstractNumber { }

[Serializable]
public class One : AbstractNumber { public string oneStr; }
[Serializable]
public class Two : AbstractNumber { public string twoStr; }
[Serializable]
public class Three : AbstractNumber { public string threeStr; }

public class Number : MonoBehaviour
{
    [SerializeReference]/*注意此处*/
    public List<AbstractNumber> numbers = new List<AbstractNumber>()
    {
        new One(),new Two(),new Three()
    };
}
View Code

 

 

使用1e-x科学计数法表示Epsilon(最小误差)

1e后面跟的数表示10的-几次方,例如:

1e-5 = 0.000001

因此可以用科学计数法直接定义最小误差常量:

public const float Epsilon = 1e-5f;

 

 

对while进行死循环监测

我们可以增加一个结构体在外部对while进行死循环监测

WhileLimit结构体对象(可使用ref结构体):

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;
    }
}
View Code

使用:

void Start()
{
    WhileLimit whileLimit = new WhileLimit();
    while (whileLimit.Check(true))
    {
        //...
    }//运行后控制台报出异常:Infinite loop caught!
}

 

 

B序列为特征序列[1,2,3],A序列为完整序列[1,2,3,4,5,1,2,3],判断A序列中有多少B序列特征

List<int> A = new List<int> { 1, 2, 3 };
List<int> B = new List<int> { 1, 2, 3, 4, 5, 1, 2, 3 };

int count = 0;
int bIndex = 0;

for (int i = 0; i < B.Count; ++i)
{
    if (B[i] == A[bIndex])
    {
        ++bIndex;
        if (bIndex == A.Count)
        {
            ++count;
            bIndex = 0;
        }
    }
    else
    {
        bIndex = 0;
    }
}
Debug.LogFormat("A集合中有{0}个B集合的特征", count);
//Return 2

 

 

构建插入有序集合

示例:

        Insert(10f);
        Insert(20f);
        Insert(3f);
        Debug.Log("arr count: " + count + " str: " + string.Join(", ", dists));
        //arr count: 7 str: 20, 10, 3...

代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestClass : MonoBehaviour
{
    float[] dists = new float[16];
    int count;


    void Start()
    {
        Insert(10f);
        Insert(20f);
        Insert(3f);
        Insert(2f);
        Insert(6f);
        Insert(1f);
        Insert(15f);
        Debug.Log("arr count: " + count + " str: " + string.Join(", ", dists));
        //arr count: 7 str: 20, 15, 10, 6, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0
    }

    void Insert(float value)
    {
        int j = dists.Length - 1;
        while (j > 0 && value > dists[j - 1])
        {
            dists[j] = dists[j - 1];
            --j;
        }
        dists[j] = value;
        ++count;
    }
}
View Code

优点是实现某些算法时可以优先考虑使用,缺点是不支持移除。

 

 

调用RequestCharactersInTexture缓存字体以提升性能

UnityEngine.UI.Text text;
text.font.RequestCharactersInTexture("qwertyuiopasdfghjklzxcvbnmQWERYTUIOPASDFGHJKLZXCVBNM1234567890~`!@#$%^&*()_+{}[]:;\"'/.,?><");

 

 

对于存储大量信息的ScriptableObject,标记PreferBinarySerialization优化

see also: https://docs.unity3d.com/cn/2018.1/ScriptReference/PreferBinarySerialization.html

[PreferBinarySerialization]
public class CustomData : ScriptableObject
{
    public float[] lotsOfFloatData = new[] { 1f, 2f, 3f };
    public byte[] lotsOfByteData = new byte[] { 4, 5, 6 };
}

 

 

避免string.format每帧调用导致GC

尽量用字符串插值表达式,没有GC问题。

string foxJump = "The quick brown fox jumps over the lazy dog.";
string message = $"{foxJump}";

少数必须要用string.format的情况可考虑用span或fixed栈分配方案。

 

 

快速查看非闭合的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; } }

 

 

一种简练的循环写法

int num = 10;
while (--num > -1)
    Debug.Log(num);//9,8,7...0
int num = -1;
while (++num != 3)
    Debug.Log(num);//0,1,2

 

 

不考虑性能的情况下,图像快速遍历

for (int i = 0; i < 16; i++)
{
    var y = i / 4;
    var x = i % 4;
    Debug.Log("x: " + x + " y: " + y);
}

 

 

在Lambda迭代集合时拿到索引

使用Linq或者ForEach等方法迭代集合时,无法拿到索引,此时可以用Enumerable.Range

先创建数字集合再遍历,从而拿到索引(注:此方法有性能问题,只建议在编辑器下使用):

int[] myArr = new int[32];
Enumerable.Range(0, myArr.Length).Where(i => myArr[i].Foo);

 

 

以矩阵形式获取或打印调试数据

我们往往要对一维的数据进行调试,这里给出一套快速以矩阵形式输出的模板,以OnGUI为例:

void OnGUI()
{
    int[] a = Enumerable.Range(0, 32).ToArray();
    for (int i = 0, k = 0, c = 5, r = Mathf.CeilToInt(a.Length / (float)c); i < r; ++i)
    {
        GUILayout.BeginHorizontal();
        for (int j = 0; j < c; ++j)
        {
            if (k > a.Length - 1) break;
            GUILayout.Box(a[k++].ToString());
        }
        GUILayout.EndHorizontal();
    }
}

 

 

使用异或进行正负同符号判断

浮点数(BitConverter或指针方法除外)和无符号类型通过下列方式:

float x = -0.232f;
float y = -0.1f;
if (!(x < 0f ^ y < 0f))
    Debug.Log("符号相同!");

性能测试:

if (Mathf.Approximately(Mathf.Sign(x), Mathf.Sign(y)))
    i.GetHashCode();//200ms

if (!(x < 0f ^ y < 0f))
    i.GetHashCode();//50ms

if ((x <= 0f && y <= 0f) || (x > 0f && y > 0f))
    i.GetHashCode();//50ms

有符号类型直接位移比较符号位:

int x = -12;
int y = -18;

if (x >> 31 == y >> 31)
    Debug.Log("符号相同!");

注意:不能二个值以上异或,例如a^b^c并不能比较三个值是否一致,但两个值异或已可应用于大多数情况。 

 

 

不创建临时集合不递归的情况下遍历Transform及所有子节点

private void ForEachTransformAndChildren(Transform root, Func<Transform, bool> onBody)
{
    if (!onBody(root)) return;

    Transform current = root.GetChild(0);

    do
    {
        if (!onBody(current)) break;

        if (current.childCount > 0)
        {
            current = current.GetChild(0);
        }
        else
        {
            do
            {
                Transform parent = current.parent;
                int siblingIndex = current.GetSiblingIndex();

                if (siblingIndex == parent.childCount - 1)
                {
                    current = parent;
                }
                else
                {
                    current = parent.GetChild(siblingIndex + 1);
                    break;
                }
            } while (current != root);
        }
    } while (current != root);
}

 

 

前后两次无重复随机

last参数为上一次随机的结果,该方法保证下一次随机结果不重样

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;
}

 

 

带权重随机

P=((x*weight) / num) ^ n

public struct WeightItem
{
    public float weight;//权重填写0-1之间的值
    public int value;
}

private int WeightRandom(WeightItem[] weightItems)
{
    const float kEps = 0.00001f;

    float sumWeight = 0;
    for (int i = 0; i < weightItems.Length; i++)
        sumWeight += weightItems[i].weight;

    float randomValue = Random.Range(0f, sumWeight);

    float atte = 0f;
    for (int i = 0; i < weightItems.Length; i++)
    {
        float min = atte;
        atte += weightItems[i].weight;
        float max = atte;

        if (randomValue > min && randomValue < max + kEps)
        {
            return weightItems[i].value;
        }
    }

    throw new System.Exception();//代码不会到达该处
}

 

 

创建额外集合,随机遍历数组元素

注意:用洗牌算法不行,会改变原集合。

这里使用前后调换思想,虽然不是真随机,但可以打乱遍历顺序并保证每个元素都被遍历到:

int[] tmp = new int[] { 1, 2, 3, 4, 5, 6 };

bool isOdd = (tmp.Length & 1) == 1;
for (int i = 0, mid = tmp.Length / 2, r = 0; i < tmp.Length; i++)
{
    int invI = tmp.Length - (i + 1);

    if (!isOdd && i == mid)
        r ^= 1;

    r ^= 1;

    int iRandom = r == 0 ? invI : i;
    Debug.Log(iRandom);//0,4,2,3,1,5(打印的是索引)
}

稍加修改,也可实现更加随机的遍历,不过代码较冗杂(或许可以用某种数学序列):

public static readonly int[] kSequence = new int[] { 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0 };
private void Start()
{
    int[] tmp = new int[] { 1, 2, 3, 4, 5, 6 };

    bool isOdd = (tmp.Length & 1) == 1;
    for (int i = 0, mid = tmp.Length / 2, r = 0; i < tmp.Length; i++)
    {
        int invI = tmp.Length - (i + 1);

        if (i < mid) r = (r + 1) % kSequence.Length;
        else if ((isOdd && i > mid + 1) || (!isOdd && i > mid)) r = ((r - 1) + kSequence.Length) % kSequence.Length;

        int iRandom = (isOdd && i == mid) ? i : kSequence[r] == 0 ? invI : i;
        Debug.Log("iRandom: " + iRandom);//5,4,2,3,1,0(打印的是索引)
    }
}

 封装版本:

public ref struct OutOfOrderDecorator
{
    private int mLength;
    private int mR;
    private int mMid;
    private bool isOdd;


    public static OutOfOrderDecorator Create(int length, int originSeed = 0)
    {
        return new OutOfOrderDecorator()
        {
            mLength = length,
            mMid = length / 2,
            isOdd = (length & 1) == 1,
            mR = originSeed
        };
    }

    public int Get(int i)
    {
        int invI = mLength - (i + 1);

        if (!isOdd && i == mMid) mR ^= 1;
        mR ^= 1;
        return mR == 0 ? invI : i;
    }
}
OutOfOrderDecorator
public ref struct OutOfOrderStableDecorator
{
    public static readonly int[] kSequence = new int[] { 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0 };

    private int mLength;
    private int mR;
    private int mMid;
    private bool isOdd;


    public static OutOfOrderStableDecorator Create(int length, int originSeed = 0)
    {
        return new OutOfOrderStableDecorator()
        {
            mLength = length,
            mMid = length / 2,
            isOdd = (length & 1) == 1,
            mR = originSeed
        };
    }

    public int Get(int i)
    {
        int invI = mLength - (i + 1);

        if (i < mMid) mR = (mR + 1) % kSequence.Length;
        else if ((isOdd && i > mMid + 1) || (!isOdd && i > mMid)) mR = ((mR - 1) + kSequence.Length) % kSequence.Length;

        return (isOdd && i == mMid) ? i : kSequence[mR] == 0 ? invI : i;
    }
}
OutOfOrderStableDecorator

 

 

连续区间内随机取N个数字不重复

缺点是最后一次获取导致概率不平均,但并不严重。

/// <summary>
/// 连续区间内随机取N个数字不重复
/// </summary>
/// <param name="min">区间最小值(Inclusive)</param>
/// <param name="max">区间最大值(Exclusive)</param>
/// <param name="count">取数字个数</param>
/// <param name="result">储存数组</param>
void PeekRange(int min, int max, int count, int[] result)
{
    int range = max - min;
    count = Mathf.Min(range, count);
    for (int i = 0, j = range / count; i < count; i++)
    {
        result[i] = min + Random.Range(i * j, i + 1 == count ? range : (i + 1) * j);
    }

    for (int i = count - 1; i >= 0; i--)
    {
        int j = Random.Range(0, count) % (i + 1);
        (result[i], result[j]) = (result[j], result[i]);
    }
}

调用:

int[] result = new int[3];
PeekRange(0, 30, 3, result);
Debug.Log(string.Join(',', result));//23,3,10

 

 

对常量表数据进行分组,并支持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是否包含分组内的字段。

 

 

使用Array.Clear快速重置数组元素

int[] tmp = new int[] { 1, 2, 3 };
Array.Clear(tmp, 0, 3);//0,0,0

注:Array.Fill也可优先考虑使用。 

 

 

使用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,会多一点性能开销。 

 

 

代码健壮性改善 - 在创建流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//不需要溢出检查
{
    //...
}

 

 

使用IReadonlyList进行List协变操作,暴露弱类型供外部使用

IList不支持协变,而IReadOnlyList支持,因此可配合其协变;将集合数据暴露给外部使用:

public class ClassA { }
public class ClassB : ClassA { }

private List<ClassB> mList;
public IReadOnlyList<ClassA> ExposeList => mList;

参考:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/covariance-contravariance/variance-in-generic-interfaces

 

 

使用WeakReference弱引用监测对象生命周期

c#提供了弱引用,我们可以用其监听对象是否被释放,弱引用WeakReference类似C++的weak_ptr

注:UnityEngine.Object对象不支持弱引用监测

object obj = null;
WeakReference weakReference = new WeakReference(obj);
//weakReference.IsAlive ...

 

 

在属性set访问器中封装跳出逻辑

我们在属性set访问器赋值时,常常每次都要比较对象是否为空、对象一致性等,可以封装该逻辑多次调用,

例如uGUI中这么做:

internal static class SetPropertyUtility
{
    public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
    {
        if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
            return false;

        currentValue = newValue;
        return true;
    }
}

Image.cs:

public Sprite overrideSprite
{
    get { return activeSprite; }
    set
    {
        if (SetPropertyUtility.SetClass(ref m_OverrideSprite, value))
        {
            SetAllDirty();
            TrackSprite();
        }
    }
}

 

 

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

 

 

 判断目标是否在相机的平截头体内

var planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
var isContain = GeometryUtility.TestPlanesAABB(planes, bounds);
        
if(isContain)
{
    //in frustum..
}

使用时记得调用缓存数组的重载方法,以消除GC Alloc。

 

 

多场景加载卸载时,LightProbe光照探针残留问题

调用接口手动更新即可。

LightProbes.Tetrahedralize();

 

 

清空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);

 

 

不同场景区域可挂载Audio Reverb Zone组件,提升临场感

该组件内含大量混响预设,保持Audio Source在区域内即可激活

 

 

编辑器窗口滑动缩放(相较直接滑动滚轮步幅更小)

Alt+鼠标右键滑动

 

 

编辑器Scene窗口,使镜头始终追踪目标物体

选择需要被追踪的物体,按Shift+F即可

 

 

编辑器Scene窗口,便捷操控3D场景的方法

按住鼠标右键,wasdqe按键移动,分别对应3个轴向

在SceneView窗口按住鼠标右键的同时滑动滚轮;可以修改移动的幅度。

 

 

将选中物品立刻移动到编辑器相机位置

Ctrl+Shift+F

 

 

导入FBX相机fov动画

FBX导入面板勾选Animated Custom Properties

Maya还可以支持导入自定义属性动画,非Maya会有不兼容的情况,但相机fov动画都可以导入

 

 

显示当前选中物体的网格

在Gizmos中勾选Selection Wire

 

 

使用快捷键shift+h快速solo显示物体

 

 

地面、墙壁吸附 —— 表面快速对齐

选中物品按住Shift+Ctrl,此时物品中间会出现一个方块,此时要按住这个方块拖动,不要按住移动轴拖动,即可吸附。

选择Center锚点对其模式为边缘吸附,选择Pivot锚点对其模式为中心点吸附(unity2021.2.6):

 

 

选中当前对象的情况下按S,可以快速修改锚点设置

 

 

不打开Prefab快速修改Prefab属性字段

在修改的字段上右键,选择Apply to Prefab '..'即可。

 

 

打包AssetBundle时,网格信息丢失

关闭Optimize Mesh Data选项的勾选

 

 

编辑器下被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

或者右键数组中某个元素也会出现操作选项

 

 

自定义ScriptableObject所生成对象的图标

只需要为原始ScriptableObject脚本对象赋上图标即可

 

2.Shader & Graphics Tips


 

ShaderGraph编写CustomFunction时,定义/调用光照内置变量提示redefinition

使用Unity内部的宏UNIVERSAL_LIGHTING_INCLUDED来加以判断:

void CelRendering_float(float3 normalWS, out half3 lightingCol)
{
#ifdef UNIVERSAL_LIGHTING_INCLUDED
    float nDotL = dot(normalWS, _MainLightPosition.xyz);
    nDotL = nDotL * 0.5 + 0.5;
    lightingCol = half3(nDotL, 0, 0);
#else
    lightingCol = 0;
#endif
}

 

 

URP自定义扩展ShaderGUI,Keyword关键字设置无效(该操作新版本中似乎已修改)

BaseShaderGUI的MaterialChanged方法里会清空所有关键字,可能是为了防止造成无用关键字堆积。

需要先设置字段(例如加一些float字段)将Keyword开关值缓存,然后重写MaterialChanged方法,再读取缓存字段,

设置上Keyword即可。

 

 

ShaderGraph中快速预览某个节点渲染效果

快捷键:Ctrl+T

 

 

快速反转距离图像的方法

在和一点求距离并作为色彩输出后会得到一个中心黑色周围白色的扩散圆,

但往往需要中心白色周围黑色的圆图像。并且需要对扩散范围进行约束。

那么可以通过smoothstep插值,并将插值函数的最小值最大值对调,就可以得到一个OneMinus反转的插值结果,

也就可以得到范围不是无限的白色边缘模糊圆点了:

float dist = distance(i.uv, 0.5);
float d = smoothstep(0.15, 0.1, dist);
return d;

 

 

解决Unity打光穿墙问题

Unity里实时光没有做遮挡判断,所以肯定会穿墙。Lightmap烘焙的情况下,可以用Area面光来打光

,它是面光源可以更好的模拟间接光照,并且有遮挡检测。而其他几种灯光烘焙情况下都会穿墙:

 

 

解决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中直接通过矩阵获得相机Forward方向向量

float3 cameraForward = -UNITY_MATRIX_V[2].xyz;

 

 

Shader中判断当前相机是否为正交相机

UNITY_MATRIX_P[3][3] == 1

 

 

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以节省性能

 

 

RenderTexture可以用RGB565和R8格式优化内存占用

这是目前多平台上兼容性最好的2种格式,RGB565会有一些深度损失。

RenderTexture.GetTemporary(w, h, 16, RenderTextureFormat.R8);
RenderTexture.GetTemporary(w, h, 16, RenderTextureFormat.RGB565);

 

 

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();

 

 

重写Dictionary字典一致性比较器,优化key的检索

字典在构造函数中可以传入自定义的一致性比较器IEqualityComparer,以控制内部桶的分布,

对于lv1_dungeon02_enemy1这类可以从字符串看出分组的key,则可从比较器角度进行优化

(可参考:https://thedeveloperblog.com/iequalitycomparer,测试好像快了一倍)

很多项目将配置表数据长期静置于内存中,进行比较器优化反而更容易获得性能。

Dictionary<int, int> dict = new Dictionary<int, int>(capacity, customComparer);

 

 

List的RemoveAt优先考虑从末尾元素移除

在某些情况下,将新增元素的操作改为Insert(0, newItem)而不是Add(newItem),并使用RemoveAt(list.Count-1)进行移除

会比RemoveAt(0)移除快更多(粗略测试下来400ms vs 5ms),因为对首个数据进行移除会涉及较多的内存拷贝

 

 

设置StringComparison参数,忽略大小写进行字符串判断

string x = "A";
string y = "a";
if (x.Equals(y, System.StringComparison.OrdinalIgnoreCase)) { }
if (x.Contains(y, System.StringComparison.OrdinalIgnoreCase)) { }

 

 

忽略符号进行字符串检索

有时需要对路径信息进行检索("aa\bb\cc/dd".Contains("cc\dd")),但需要先做一次正反斜杠统一的操作。

若不需要较严谨的判断逻辑,可封装该方法后使用:

private static bool ComplexContains(string source, string value)
{
    var index = CultureInfo.InvariantCulture.CompareInfo.IndexOf
        (source, value, CompareOptions.IgnoreSymbols);
    return index != -1;
}

出处:https://stackoverflow.com/questions/15178922/complex-contains-string-comparison

 

 

使用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,同时设置位置与旋转

在位置和旋转都需要设置的情况下,使用该方法可优于调用两次方法

(另外引擎内部在SetPosition/Rotation时会调用SendMessage,而用该接口则不会)

transform.SetPositionAndRotation();

 

 

设置transform.hierarchyCapacity,防止分配拷贝内存以优化性能

例如预期某个父物体会有 100 个子物体,可以直接设置好该值:

transform.hierarchyCapacity = 100;

 

 

对于Unity对象,优先使用InstanceID作为字典Key值

InstanceID可以保证运行时的唯一性,并且比GUID更短。

dict.Add(unityObj1.GetInstanceID(), unityComp1);
dict.Add(unityObj2.GetInstanceID(), unityComp2);

来自官方分享:https://www.youtube.com/watch?v=_wxitgdx-UI

 

 

修改Unity里同时可以听到声音的最大数量以优化,默认值是32

configuration = AudioSettings.GetConfiguration();
configuration.numRealVoices = 16;
AudioSettings.Reset(configuration);

 

 

大于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 readonly方式优化

ref readonly TestStruct arr = ref mArr[i];
Debug.Log(arr.a);
Debug.Log(arr.b);

使用ref可以在IL阶段非拷贝传递,对于只读可使用ref readonly

 

 

使用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();

 

 

对于动态创建又无需修改的Mesh,调用UploadMeshData传到GPU端

UploadMeshData接口可以删除CPU端Mesh所占的内存,将Mesh传至GPU端(这里可能意指GPU缓存的内存区域)

缺点是Mesh无法再次编辑:

Mesh mesh = new Mesh();
mesh.UploadMeshData(true);

 

 

使用__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()

 

 

使用Conditional特性在编译时去除Debug方法调用

Debug调试打印会产生拼接字符串等开销。

我们可以通过[Conditional]特性并在打包时动态生成一个Debug类,屏蔽调试代码。

namespace UnityEngine
{
    public static class Debug
    {
        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void Log(string content)
        {
        }
    }
}

 

 

在条件允许下,使用位运算代替乘除法

左移代替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)

修改后可支持EaseIn、EaseOut:4 * t^a * (1-t)^b

 

 

关于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);

 

 

叉乘判断左右的二维向量优化版本

若只有判断左右的需求,可直接用求秩的方法对二维向量使用,效率更高

void Update()
{
    var relative = b.position - a.position;
    relative = relative.normalized;
    var vec1 = new Vector2(a.forward.x, a.forward.z);
    var vec2 = new Vector2(relative.x, relative.z);

    Debug.Log(det(vec1, vec2));//greate 0 or less 0.
}
float det(Vector2 vector1, Vector2 vector2)
{
    return vector1.x * vector2.y - vector1.y * vector2.x;
}

 

 

实时反射探针优化

如果需求必须使用实时反射探针,可以将其更新模式设为脚本驱动,在渲染前对反射内容进行模型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


 

Animator在Editor状态下预览工具

 

Unity3D中Console控制台的扩展

 

 

Editor下避免“Getting control 0's position in a group...”报错

按钮点击完后调用GUIUtility.ExitGUI()接口

 

 

从脚本模板创建csharp脚本文件

myTemplate.cs.txt模板文件:

public class #SCRIPTNAME# : MonoBehaviour
{
}

创建代码:

const string kPath= "Assets/Editor/ScriptTemplates/myTemplate.cs.txt";
ProjectWindowUtil.CreateScriptAssetFromTemplateFile(kPath, "MyTemp.cs");

 

 

快速查看Serialize Field的路径字段

创建SerializeField需要得到该字段的yaml路径,按住shift鼠标右键字段,选择 Print Property Path即可打印。

 

 

通过设置横向布局,实现UI右对齐

GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.Button("Right Align Button");
GUILayout.EndHorizontal();

 

 

Hightlighter.Hightlight高亮提示GUI控件

using UnityEditor;
using UnityEngine;

public class WindowTest : EditorWindow
{
    [MenuItem("Tools/WindowTest")]
    public static void Setup()
    {
        GetWindow(typeof(WindowTest));
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Hightlight"))
            Highlighter.Highlight("WindowTest", "baz");

        if (GUILayout.Button("Stop"))
            Highlighter.Stop();

        GUILayout.BeginHorizontal();
        GUILayout.Button("foo");
        GUILayout.Button("bar");
        GUILayout.Button("baz");
        GUILayout.Button("qux");
        GUILayout.EndHorizontal();
    }
}
View Code

 

 

使用ObjectFactory进行编辑器下GameObject创建、组件挂载等操作

使用该接口会自动去做Undo注册操作。

ObjectFactory.CreateGameObject("temp");

参考:https://docs.unity3d.com/ScriptReference/ObjectFactory.html

 

 

在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;
}

 

posted @ 2014-06-16 08:32  HONT  阅读(6905)  评论(1编辑  收藏  举报