Unity游戏优化(第2版)学习记录2

第2章

一、使用最快的方法获取组件(GetComponent)

public class T2_1 : MonoBehaviour
{
    public Transform canvas;

    private int numTests = 100000;
    private GameObject parent = null;
    
    private void Awake()
    {
        parent = new GameObject();
        parent.name = "parent";
        parent.transform.SetParent(canvas);
        parent.AddComponent<RectTransform>();
        parent.transform.localScale = Vector3.one;
        parent.SetActive(false);

        for(int i = 0;i < numTests;i++)
        {
            GameObject go = new GameObject();
            go.name = "son_" + i.ToString();
            go.transform.SetParent(parent.transform);
            go.transform.localScale = Vector3.one;
            go.AddComponent<RectTransform>();
            go.AddComponent<Image>();
        }
    }

    void Start()
    {
        float startTime_1 = Time.realtimeSinceStartup;
        for (int i = 0; i < numTests; i++)
        {
            parent.transform.GetChild(i).GetComponent<Image>();
        }
        float endTime_1 = Time.realtimeSinceStartup;
        print("cost time: " + (endTime_1 - startTime_1).ToString());



        float startTime_2 = Time.realtimeSinceStartup;
        for (int i = 0; i < numTests; i++)
        {
            parent.transform.GetChild(i).GetComponent(typeof(Image));
        }
        float endTime_2 = Time.realtimeSinceStartup;
        print("cost time: " + (endTime_2 - startTime_2).ToString());



        float startTime_3 = Time.realtimeSinceStartup;
        for (int i = 0; i < numTests; i++)
        {
            parent.transform.GetChild(i).GetComponent("Image");
        }
        float endTime_3 = Time.realtimeSinceStartup;
        print("cost time: " + (endTime_3 - startTime_3).ToString());
    }
}

GetComponent函数有3种变体,GetComponent、GetComponent(typeof(T))和GetComponent(string)

以上代码,是对每个GetComponent()重载进行了10万测试。调用次数虽然远远大于普通项目中的合理次数,但有助于清晰地比较这几个方法的性能消耗

结果如图,其中,GetComponent方法只比GetComponent(typeof(T))方法快一点,而GetComponent(string)方法明显比其他两个方法慢得多。因此,可以相当安全地使用GetComponent()方法基于类型的版本,因为它们的性能好,性能差距也小,还要尽量避免使用GetComponent(string)方法。

二、移除空的回调定义

public class T2_2_1 : MonoBehaviour{ }
public class T2_2_2 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start(){ }

    // Update is called once per frame
    void Update(){ }
}

以上分别是有回调和没有回调的两个脚本,分别创建10000个GameObject挂载T2_2_1脚本和10000个GameObject挂载T2_2_2脚本,默认隐藏。

打开Profiler,取消Rendering、Animation、GarbageCollector、Others的性能消耗显示(隐藏的是比Script消耗大的项,方便观察),然后运行游戏,分别显示10000个挂载了T2_2_1脚本的GameObject和10000个挂载了T2_2_2脚本的GameObject,效果如下:

可以看出,当显示10000个挂载了T2_2_1脚本的GameObject时,CPU并没有十分明显的性能消耗。但是,当显示10000个挂载了T2_2_2脚本的GameObject时,CPU突然增加了大量的性能消耗,并且一直性能消耗不会降下来,即使Start函数和Update函数都是空函数,也一样会使用CPU的性能。
虽然实际使用中一般不会有10000个空的回调函数,但是可以养成一个习惯,如果有空的回调函数,记得顺手移除。

三、缓存组件引用

尽量避免反复获取组件的引用,例如在Update函数中使用GetComponent函数获取组件,这会存在CPU的性能开销,并且是不必要的开销。我们可以消耗少量内存,在游戏初始化时(或游戏物体被创建时)将需要的组件保存到一个变量中,以便后续的使用。

四、共享计算输出

让多个对象共享某些计算结果,可节省性能开销,当然,只有这些计算都生成相同的结果时,此方法才有效。

五、Update、Coroutines和InvokeRepeating

尽量不要在Update函数中以超出需要的频率调用某段代码,这会卡住主线程。一个提高性能的好方法是减少这段代码的调用频率,改为每隔一段时间(例如每隔几秒)才运行一次。
也可以改为使用Coroutines,值得注意的是,使用协程会不可避免的增加额外的内存和性能消耗(性能消耗大约时普通函数的3倍),并且协程的运行独立于MonoBehaviour组件中Update回调的触发,不管组件是否禁用,都将继续调用协程。另外,协程会在包含它的GameObject变成不活动的那一刻自动停止,不管是当前对象还是父对象被设置为不活动,当GameObject再次被设置为活动时,协程不会重新启动。
每隔一段时间调用的函数,还可以改为使用InvokeRepeating函数进行(注意,此处函数必须为无参数的函数)使用InvokeRepeating函数比使用协程的性能开销成本略小。

六、更快的GameObject空引用检查

很多时候会出现这样一段代码:
if (gameObject != null)
{
//DoSomething
}
这时候可以使用System.Object.ReferenceEquals()函数代替,会稍微快一点。这是因为,GameObject和MonoBehaviour是特殊对象,因为它们在内存中有两个表示:一个表示存在于管理C#代码的相同系统管理的内存中,C#代码是用户编写的(托管代码),而另一个表示存在于另一个单独处理的内存空间中(本机代码),数据可以在这两个内存空间之间移动,但是每次这种移动都会导致额外的CPU开销和可能的额外内存分配。
然而,一些基本的测试显示在Intel Core i5 3570K处理器上,任何一个空引用检查方法仍然只消耗纳秒,因此,除非在执行大量的空引用检查,否则最多只能提升很少的性能

七、避免从GameObject取出字符串属性

从GameObject中检索字符串属性是另一种跨越本机-托管桥接的微妙方式,GameObject中受此行为影响的两个属性是tag和name。幸运的是,tag属性常用于比较,而GameObject提供了CompareTag()方法,这是比较tag属性的另一种方法,它完全避免了本机-托管桥接。但是,name属性没有对应的方法,因此应该尽可能使用tag属性。
提示:向CompareTag传递字符串字面量(如”Player”)不会导致运行时内存分配,因为应用程序在初始化期间分配这样的硬编码字符串,在运行时只是引用它们。

八、使用合适的数据结构

软件开发中一个常见的性能问题就简单地为了便利而使用不适当的数据结构来解决问题,最常用的两种数据结构时列表(List)和字典(Dictionary)。
如果希望遍历一组对象,最好使用列表,因为它实际上是一个动态数组,对象或引用在内存中彼此相邻,因此迭代导致的缓存丢失最小;如果两个对象相互关联,且希望快速获取、插入、删除这些关联,最好使用字典。
然而,数据结构通常需要同时处理两种情况:快速找出哪个对象映射到另一个对象,同时还能遍历组,在这些情况下,通常最好在列表和字典中同时存储数据,以便更好地支持这种行为。这需要额外的内存开销来维护多个数据结构,插入和删除操作需要每次从数据结构中添加和删除对象,但迭代列表(通常更常发生)的好处和迭代字典形成鲜明的对比

九、避免运行时修改Transform的父节点

Unity5.4版本前Transform组件的引用通常时在内存中随机排列,Unity5.4后Transform组件的内存布局发生了很大的改变,Transform组件的父-子关系操作起来更像动态数组,因此Unity尝试将所有共享相同父元素的Transform按顺序存储在预先分配的内存缓冲区内的内存中。
Unity5.4后,如果将一个GameObject的父对象重新指定为另一个对象,父对象必须将新的子对象放入预先分配的内存缓冲区中,并根据新的深度对所有这些Transform排序,另外,如果父对象没有预先分配足够的空间来容纳新的子对象,就必须扩展缓冲区,以便以深度优先的顺序容纳新的子对象及其所有的子对象。
因此,通过GameObject.Instantiate()实例化新的GameObject时,应该同时设置其Transform属性。如果在实例化之后立即将Transform的父对象重新修改为另一个对象,它将丢弃刚才在Hierarchy分配的缓冲区
另一种降低这个过程成本的方法是让根Transform在需要之前就预先分配一个更大的缓冲区,这样就不需要再同一帧中拓展缓冲区,给它重新指定另一个GameObject到缓冲区中。这可以通过修改Transfrom组件的hierarchyCapacity属性来实现。如果能够预估父元素包含的子Transfrom数量,就可以节省大量不必要的内存分配。

十、注意缓存Transform的变化

Transform组件只存储与其父组件相关的数据,这意味着访问和修改Transform组件的position、rotation、scale属性会导致大量未预料到的矩阵乘法计算,从而通过其父Transform为对象生成正确的Transform表示。对象在Hierarchy窗口中的位置越深,确定最终结果需要进行的计算就越多。
这意味着使用localPosition、localRotation、localScale的相关成本相对较小。但是,如果将数学计算从世界空间更改为本地空间,会使原本很简单的问题变得复杂,所以,为了更容易解决复杂的3D问题,牺牲一点性能是值得的。
另外,不断更改Transform组件属性的另一个问题是,也会向组件(如Collider、Rigidbody、Light和Camera)发送内部通知,这些组件也必须进行处理,相应地更新。

十一、避免在运行时使用Find()和SendMessage()方法

众所周知,SendMessage()方法和GameObject.Find()方法非常昂贵,可以使用GetComponent()方法替换SendMessage()方法来进行优化,也可以采取多种方法来解决这个问题:
1、将引用分配给预先存在的对象
解决对象间通信问题的一个简单方法是使用Unity内置的序列化系统。
在多人合作的项目中,当艺术家、设计师和程序员都在修补同一种产品时,因为每个人的计算机科学和软件编程知识水平差别很大,他们中的一些人更不愿意修改代码文件。因此可以使用[SerializeField]属性将其显示给Inspector窗口。对于Inspector窗口,该值现在将表现为一个公共字段,允许通过编辑器界面方便地更改它,同时能将数据安全地封装在代码库的其他部分中。
即把public int num;写成[SerializeField] private int num;
需要注意的是,并不是所有对象都可以序列化并显示在Inspector窗口中。Unity可以序列化所有的基本数据类型(int,float,string,bool),各种内置类型(Vector3、Quaternion等);可以序列化枚举、类、结构和包含其他可序列化类型的各种数据结构(如List)。但是,它无法序列化静态字段、只读字段、属性和字典。
2、静态类
这种方法涉及在任何时候创建一个对整个代码库全局可访问的类。静态类比较难调试,代码对静态类的更改可以在任何位置和任何点发生,如果要替换它,则需要对调用到此静态类的每个类进行修改。
尽管存在这些缺点,但它是迄今为止最容易理解和实现的解决方案。通常可用于实现单例模式。需要注意的是,单例模式并不一定是全局可访问的对象,单例模式最重要的特性是一次只存在一个对象实例;此外,静态类中的每个方法、属性和字段都必须附上static关键字,这意味着在内存中永远只驻留该对象的一个实例,也意味着它的公共方法和字段可以从任何地方访问。
3、单例组件

public class SingletonClass : MonoBehaviour
{
    private static SingletonClass _instance;
    
    public static SingletonClass Instance
    {
        get
        {
            if(_instance == null)
            {
                GameObject go = new GameObject("Singleton", typeof(SingletonClass));
                _instance = go.GetComponent<SingletonClass>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
        protected set
        {
            _instance = value;
        }
    }

    public void PrintInfo()
    {
        print("print info");
	}

	private void Start()
    {
        _instance = GetComponent<SingletonClass>();
        isAlive = true;
        DontDestroyOnLoad(gameObject);
	}
}

这个类的工作方式是在第一次被访问时创建一个包含组件的GameObject。因为希望它是一个全局、持久的对象,所以需要在创建时调用DontDestroyOnLoad()。有时允许不使用DontDestroyOnLoad()函数,这样在每次场景切换后,都可以重新实例化单例类,并且其属性回归初始值,这完全取决于需求。
需要注意的是,尽量减少在OnDestroy()函数中调用单例类。对象的销毁是随机发生的,如果任何对象试图在其OnDestroy()函数中使用单例组件执行任何操作,它们可能正在调用SingletonComponent对象的实例属性。但是,如果单例组件在此之前已经被销毁,那么在应用程序关闭期间将创建单例组件的新实例。
这可能会破坏场景文件,因为单例组件的实例会留着场景中。如果发生这种情况,Unity将抛出以下错误信息:
Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)
(有些对象在关闭场景时没有清理干净。(是否从OnDestroy中生成了新的GameObjects?))
明显的解决办法就是在任何MonoBehaviour组件的OnDestroy()回调中都不要调用单例对象,或者单例对象不在get方法中生成新的GameObject。
还有一种方法,就是在单例类中增加一个isAlive的bool类型变量,当单例类被销毁后,变量设置为false,最后,任何对象在其自身的OnDestroy()方法中调用单例对象时,必须先验证isAlive的状态,部分代码片段:

public bool isAlive = true;
private void OnDestroy() { isAlive = false; }
private void OnApplicationQuit() { isAlive = false; }

public class Test_SingleTonClass : MonoBehaviour
{
    private void OnDestroy()
    {
        if(SingletonClass.Instance.isAlive)
        {
            SingletonClass.Instance.PrintInfo();
        }        
    }
}

这将确保在单例对象销毁后不会再生成新的对象。

十二、全局消息传递系统

(以下为书中代码)
Message类:

public class Message {
    public string type;
    public Message() { type = this.GetType().Name; }
}

实现消息传递系统MessageSystem类:
using System.Collections.Generic;
using UnityEngine;

public delegate bool MessageHandlerDelegate(Message message);

public class MessagingSystem : SingletonComponent<MessagingSystem> {
    public static MessagingSystem Instance
    {
        get { return ((MessagingSystem)_Instance); }
        set { _Instance = value; }
    }

    private Dictionary<string, List<MessageHandlerDelegate>> _listenerDict = new Dictionary<string, List<MessageHandlerDelegate>>();
    private Queue<Message> _messageQueue = new Queue<Message>();
    private const int _maxQueueProcessingTime = 16667;
    private System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch();

    public bool AttachListener(System.Type type, MessageHandlerDelegate handler) {
        if (type == null) {
            Debug.Log("MessagingSystem: AttachListener failed due to having no " + 
                      "message type specified");
            return false;
        }

        string msgType = type.Name;
        if (!_listenerDict.ContainsKey(msgType)) {
            _listenerDict.Add(msgType, new List<MessageHandlerDelegate>());
        }

        List<MessageHandlerDelegate> listenerList = _listenerDict[msgType];
        if (listenerList.Contains(handler)) {
            return false; // listener already in list
        }

        listenerList.Add(handler);
        return true;
    }

    public bool DetachListener(System.Type type, MessageHandlerDelegate handler) {
        if (type == null) {
            Debug.Log("MessagingSystem: DetachListener failed due to having no " + 
                      "message type specified");
            return false;
        }

        string msgType = type.Name;

        if (!_listenerDict.ContainsKey(type.Name)) {
            return false;
        }

        List<MessageHandlerDelegate> listenerList = _listenerDict[msgType];
        if (!listenerList.Contains(handler)) {
            return false;
        }
        listenerList.Remove(handler);
        return true;
    }

    public bool QueueMessage(Message msg) {
        if (!_listenerDict.ContainsKey(msg.type)) {
            return false;
        }
        _messageQueue.Enqueue(msg);
        return true;
    }

    public bool TriggerMessage(Message msg) {
        string msgType = msg.type;
        if (!_listenerDict.ContainsKey(msgType)) {
            Debug.Log("MessagingSystem: Message \"" + msgType + "\" has no listeners!");
            return false; // no listeners for message so ignore it
        }

        List<MessageHandlerDelegate> listenerList = _listenerDict[msgType];

        for (int i = 0; i < listenerList.Count; ++i) {
            if (listenerList[i](msg))
                return true; // message consumed by the delegate
        }
        return true;
    }

    void Update() {
        timer.Start();
        while (_messageQueue.Count > 0) {
            if (_maxQueueProcessingTime > 0.0f) {
                if (timer.Elapsed.Milliseconds > _maxQueueProcessingTime) {
                    timer.Stop();
                    return;
                }
            }

            Message msg = _messageQueue.Dequeue();
            if (!TriggerMessage(msg)) {
                Debug.Log("Error when processing message: " + msg.type);
            }
        }
    }
}

实现自定义消息:

public class CreateEnemyMessage : Message {
	
}

using UnityEngine;

public class EnemyCreatedMessage : Message {

    public readonly GameObject enemyObject;
    public readonly string enemyName;

    public EnemyCreatedMessage(GameObject enemyObject, string enemyName) {
        this.enemyObject = enemyObject;
        this.enemyName = enemyName;
    }
}

消息注册:

public class KillAllEnemiesMessage : Message {

}

using System.Collections.Generic;
using UnityEngine;

public class EnemyManagerWithMessagesComponent : MonoBehaviour {

    private List<GameObject> _enemies = new List<GameObject>();
    [SerializeField] private GameObject _enemyPrefab;

    void Start() {
        MessagingSystem.Instance.AttachListener(typeof(CreateEnemyMessage), this.HandleCreateEnemy);
        MessagingSystem.Instance.AttachListener(typeof(KillAllEnemiesMessage), this.HandleKillAllEnemies);
    }

    bool HandleCreateEnemy(Message msg) {
        CreateEnemyMessage castMsg = msg as CreateEnemyMessage;
        string[] names = { "Tom", "Dick", "Harry" };
        GameObject enemy = GameObject.Instantiate(_enemyPrefab, 5.0f * Random.insideUnitSphere, Quaternion.identity);
        string enemyName = names[Random.Range(0, names.Length)];
        enemy.gameObject.name = enemyName;
        _enemies.Add(enemy);
        MessagingSystem.Instance.QueueMessage(new EnemyCreatedMessage(enemy, enemyName));
        return true;
    }

    bool HandleKillAllEnemies(Message msg) {
        KillAll();
        return true;
    }

    public void AddEnemy(GameObject enemy) {
        if (_enemies.Contains(enemy)) {
            return;
        }
        _enemies.Add(enemy);
    }

    public void KillAll() {
        foreach (GameObject enemy in _enemies) {
            GameObject.Destroy(enemy);
        }
    }

    void OnDestroy() {
        if (MessagingSystem.IsAlive) {
            MessagingSystem.Instance.DetachListener(typeof(EnemyCreatedMessage), this.HandleCreateEnemy);
        }
    }
}
using UnityEngine;

public class EnemyCreatedListenerComponent : MonoBehaviour {

	void Start () {
		MessagingSystem.Instance.AttachListener(typeof(EnemyCreatedMessage), HandleEnemyCreated);
	}
	
	bool HandleEnemyCreated(Message msg) {
        EnemyCreatedMessage castMsg = msg as EnemyCreatedMessage;
        Debug.Log(string.Format("A new enemy was created! {0}", castMsg.enemyName));
        return true;
    }

    void OnDestroy() {
        if (MessagingSystem.IsAlive) {
            MessagingSystem.Instance.DetachListener(typeof(EnemyCreatedMessage), this.HandleEnemyCreated);
        }
    }
}

最后,测试一下消息传递系统,按下键盘数字1生成敌人,并通过消息传递系统输出Log消息,按下键盘数字2则删除所有敌人。代码如下:

using UnityEngine;

public class SingletonComponentTestInput : MonoBehaviour {
    [SerializeField] private GameObject _enemyPrefab;

    void Update() {
        if (Input.GetKeyDown(KeyCode.Alpha1)) {
            EnemyManagerSingletonComponent.Instance.CreateEnemy(_enemyPrefab);
        } else if (Input.GetKeyDown(KeyCode.Alpha2)) {
            EnemyManagerSingletonComponent.Instance.KillAll();
        }
    }
}

总结消息传递系统:
我们终于构建了一个功能齐全的全局消息传递系统,所有对象都可以与之交互,并使用它彼此之间发生消息。这种方法的一个有用特性是它与类型无关,这意味着消息发送者和侦听者甚至不需要从任何特定的类派生,来与消息传递系统进行交互;派生时,只要类提供了消息类型和匹配函数签名的委托函数即可,所以普通类和MonoBehaviour都可以访问它。
对MessageSystem类进行基准测试,会发现它能在一帧中处理数百条(不是数千条)消息,而CPU开销最少(当然,这取决于CPU)。不管将一条消息分发给100个不同的侦听器,还是将100条消息分发给一个侦听器,CPU的使用情况基本上是相同的。
有很多方法可以增强消息传递系统,以提供将来可能需要的更有用的功能:
 允许消息发送者在消息传递给侦听器之前建议延迟(以时间或帧数的形式)
 允许消息侦听器为它接收消息的紧急程度定义一个优先级,与等待相同消息类型的其他侦听器相比。如果该侦听器注册的时间比其他侦听器晚,这就是侦听器跳到队列前面的一种方法
 实现一些安全检查来处理这样的情况:当正在处理特定类型的消息,消息侦听器就添加到该类消息的消息侦听器列表中。目前,由于委托列表会在TriggerEvent()方法中迭代时被AttachListener()修改,因此C#会抛出EnumerationException异常

十三、禁用未使用的脚本和对象

1、通过可见性禁用对象
有时,希望组件或GameObject在不可见时禁用。Unity带有内置的渲染功能,以避免渲染对玩家的相机视图不可见的对象(通过被称为“视锥剔除”的技术),避免渲染隐藏在其他对象后面的对象(遮挡剔除),但这些只是渲染层面的优化。它不会影响在CPU上执行任务的组件,比如AI脚本、用户界面和游戏逻辑。我们必须自己控制这种行为。
解决这个问题的一个好方法是使用OnBecameVisible()和OnBecameInvisible()回调。由于可见性回调必须与渲染管线通信,因此GameObject必须附加一个可渲染的组件,例如MeshRenderer或SkinnedMeshRenderer。必须确保希望接收可见性回调的组件也与可渲染对象连接在同一个GameObject上,而不是连接到其父或子GameObject上,否则它们可能不会调用。
请注意,禁用包含可渲染对象的GameObject或它的父对象之一,就不可能调用OnBecameVisible(),因为调用OnBecameInvisible()时已经把带有可渲染组件的GameObject禁用了,现在摄像机没有图形表示来查看和触发回调。
应该将组件放在一个子GameObject上,并用脚本禁用它,使可渲染的对象始终可见(或者找到另一种方法重新启用它)

2、通过距离禁用对象
如果组件或GameObject距离玩家足够远,以至于几乎看不到它们,此时希望可以禁用它们。这类活动的一个很好的候选是漫游的AI生物,我们想要在远处看到它们,但处理任何操作都不需要它,它可以闲着,直到我们走近。

using System.Collections;
using UnityEngine;

public class SetActiveByDistance : MonoBehaviour
{
    [SerializeField] GameObject _target;
    [SerializeField] float _maxDistance;
    [SerializeField] int _coroutineFrameDelay;

    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(DisableAtADistance());
    }

    IEnumerator DisableAtADistance()
    {
        while(true)
        {
            float distSqrd = (transform.position - _target.transform.position).sqrMagnitude;
            if(distSqrd < _maxDistance * _maxDistance)
            {
                enabled = true;
            }
            else
            {
                enabled = false;
            }
            for(int i = 0;i < _coroutineFrameDelay;i++)
            {
                yield return new WaitForEndOfFrame();
            }
        }
    }
}

十四、使用距离的平方而不是距离

可以肯定的说,CPU比较擅长将浮点数相乘,但是不擅长计算它们的平方根。因此,可以使用距离的平方来代替计算距离
例如,如下代码:

float distance = (transform.position - other.transform.position).Distance();
if (distance < targetDistance)
{
    //To something
}

可以用下面的代码替换,得到几乎一致的结果:

float distanceSqrd = (transform.position - other.transform.position).sqrMagnitude;
if (distanceSqrd < (targetDistance * targetDistance))
{
    //To something
}

**几乎一致,而不是完全一致的原因是浮点精度。**使用距离的平方可能会失去一些使用平方根的精度,因为该值调整为具有不同密度的可表示数字区域。大多数情况下,它非常接近,不会引起注意。如果这个小的精度损失不重要,那么应该考虑这个性能技巧,否则应该忽略这个技巧。

十五、最小化反序列化行为

Unity的序列化系统主要用于场景、预制件、ScriptableObjects和各种资产类型(往往派生自ScriptableObject)。当其中一种对象类型保存到磁盘时,就使用YAML(Yet Another Markup Language,另一种标记语言)格式将其转换为文本文件,稍后可以将其反序列化为原始对象类型。在运行时从磁盘读取和反序列化数据是一个非常慢的过程(相对而言),因此所有的反序列化活动都伴随着显著的性能成本。

这种反序列化在调用Resource.load()时发生,用于在名为Resource的文件夹中查找文件路径。一旦数据从磁盘加载到内存中,以后重新加载相同的引用会快得多,但是在第一次访问时总是需要磁盘活动。
当然,需要反序列化的数据集越大,此过程所需时间就越长。由于预制组件的每个组件都是反序列化的,因此,层次结构越深,需要反序列化的数据就越多。这对于具有很深层次结构的预制,以及带有许多空GameObject对象的预制来说是一个问题(空GameObject对象至少包含一个Transform组件)。
优化方法:
1、减少序列化对象
我们的目标应该是使序列化的对象尽可能小,或者将它们分割成更小的数据块,然后一块一块地组合在一起。

2、异步加载序列化对象
可以通过Resource.LoadAsync()以异步方式加载预制及其它序列化的内容,这将把从磁盘读取的任务转移到工作线程上,从而减轻主线程的负担。

3、在内存中保存之前加载的序列化对象
序列化对象一旦从磁盘加载,就会保留在内存中,可以通过Resources.Unload()释放内存空间。如果应用程序的内存预算中有很多剩余内存,则可以选择将这些数据保存在内存中,这是一种风险策略,应该只在必要时才这样做。

4、将公共数据移入ScriptableObject
可以将公共的数据序列化到ScriptableObject中,然后加载并使用它。

十六、叠加、异步地加载场景

对于场景加载,可能希望减少性能影响,让玩家继续操作下去,因此,可以使用SceneManager.LoadSceneAsync()并传递LoadSceneMode.Additive,通过叠加的方式加载场景。
例如,在关卡游戏中,当玩家接近下一个关卡时,可以在适当的时候开始异步加载下一个场景,同样的,在玩家在下一个关卡时,如果确定已通过的关卡不会再使用,则可以通过SceneManager.UnloadSceneAsync()卸载场景。值得注意的是,卸载场景时会导致许多对象被销毁,这可能会释放大量内存并触发垃圾回收。在使用这个技巧时,有效地使用内存也很重要。

十七、创建自定义的Update()层

本章第4点讨论过Unity Engine的特性,当Unity调用Update()(或其它的Unity的回调)时,都要经过前面提到的本机-托管的桥接。

换句话说,一个脚本执行1000个单独的Update()回调的处理成本比执行一个Update()及1000个常规函数的回调要高。 调用Update()数千次的工作量并不是CPU很容易承担的,这主要是因为桥接。

因此,可以自定义实现一个Update()层,自定义实现的Update()层还可以实现更多的功能,例如提供优先级系统,如果检测到当前帧花费的时间太长,就可以跳过低优先级任务。还有许多其他的可能性。
如果项目已经有一定的规模,这样的更改可能十分耗时,并且可能会在更新子系统以利用一组完全不同的依赖项时引入大量的bug。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值