unity性能优化之关于脚本的优化与注意事项

写在前面:本文所述脚本优化就是多使用那种方式,尽量不要使用那种方式,那种方式会浪费资源,以及为什么会浪费资源,某些高效率的框架以及系统是怎样制作的

使用最快的方式获得组件

通常情况下,我们获取组件的方式中,最常用的方法之一就是GetComponent,但是GetComponent有几种变体,这几种变体的执行效率是不一样的,分别有GetComponent、GetComponent(string)、GetComponent(typeof(T)) ,这几种变体是有效率区别的,通过简单测试(网上也有很多测试结果)显示,GetComponent(T)<> > GetComponent(typeof(T)) >> GetComponent(string) ,前两者相比,差距并不是很明显,但是第三个的效率要明显低于前两者, 第三个变体一般应用于调试于诊断这种对于性能要求不是很高的场景,

移除空的回调定义

在通过unity创建脚本的时候,一般都会自动的创建上start、update等回调函数,开发人员有的时候可能会用到,但是更多的时候是用不到的,那么这种空的回调定义就会在一定程度对性能产生影响,为什么呢?
MonoBehaviour在场景中第一次实例化时,Unity会将任何定义号的回调添加到一个函数指针列表中,他会在关键时刻调用这个列表,然而即使这个列表中的函数体时空的,unity也会挂接到这些回调中,因此,由于引擎调用他们的开销,如果脚本中存在这种空的函数体,会消耗少量的CPU,想象一下,如果在你开发的项目中,存在成千上万的MonoBehaviour,里面有又很多这样空的回调定义,那么对于性能的影响有多大,再比如,当两个对象发生碰撞的时候,可能会产生一个粒子效果,并且创建一些浮动的伤害文字,播放声音特效等,这个时候对于性能的要求是很高的,因为CPU突然要进行许多复杂的更改,但是在当前帧结束之前,只有有限的事件完成这些更改,如果说这个过程花费的事件太长,就可能掉帧,因为在所有的update执行结束之前,渲染管线是不允许渲染新的帧的,此时,不管是空的start、update或者其他已经定义好的回调函数都会在这种关键时刻消减本来就紧张的时间预算。

缓存组件引用

当我们经常使用的GetComponent,很大的一个避讳就是,什么时候用,什么时候获取,用一次,获取一次,这样都是很不好的行为,实际上,对于将要使用的的组件,我们更好的方法是在初始化的时候获取并保存组件,然后再使用的时候拿来用就可以了,这样做的优点就是,消耗少量的内存,但是每次都会减少一些CPU开销;
同样来说,这种情况也适用于运行时决定计算的数据,不要每次都要求update去重新计算相同的值,而实在计算之后,保存起来,只有当需要更新这个值的时候再去计算;

Update、Coroutines、InvokeRepeating

这样的代码是不是很常见?

void Update () {
        text();
    }

实际上想来看看,对于这个text函数,我真正想要调用的频率有每一帧都调用的这种需求吗?这种情况起始就是以超过了需要的频率重复调用某段代码,现在来修改一下

private float _aiprocessDelay = 0.2f;
    private float _timer = 0.0f;

    // Update is called once per frame
    void Update () {
        _timer += Time.deltaTime;
        if (_timer >=_aiprocessDelay)
            text();
    }

这样的话,减少了调用的频率,也减少了CPu的负担
另一种方式如下

void Start () {
        UserService.Instance.OnLogin = OnLogin;
        StartCoroutine(text());
    }
IEnumerator text()
{
    
    for(int i = 0; i <10;i++)
    {
        Debug.Log("按下a");
        i += 0;
    }
    yield return new WaitForSeconds(1);
}

协程的概念都清楚,既然我们想要控制一下text的调用频率,使其不至于增加不必要的负担,那么适用协程就好了,如果使用协程,还有几点需要注意

  1. 首先,与标准函数调用相比,启动协程会带来额外的开销成本(大约是标准函数的三倍),还可能会分配一些内存把当前的状态保存下来,直到下一次调用,与此同时,协程所带来的开销并不是一次性的开销,随着协程不断的调用yield,这就会一次又一次的造成相同的开销成本,所以,如果你使用协程,你就需要很明确的知道,她所带来的好处是否大于成本
  2. 第二,协程的运行独立于update,所以不管组件是否禁用,都将继续调用协程,如果要大量的构建gameobject的话,协程可能会很笨拙
  3. 第三,协程的停止是在包含她的gameobject变成不活动的那一刻,如果说,这个gameobject被设置为不活动,然后再次被设置为活动,协程是不会自动的开启的

减少text调用频率的第三种方法:使用InvokeRepeat,实现方案很简单

void Start () {
        InvokeRepeating("text", 0f, _aiprocessDelay);
    }

但是它和协程的一个重要区别就是,InvokeReapting完全独立于MonoBehaviour和Gameobject的状态,如果想要停止调用InvokeReapting,第一种方法就是调用cancelInvoke,第二种就是销毁相关联的MonoBehaviour或者其父gameobject(禁用不会停止)

避免从GameObject种取出字符串属性

通常情况下,从对象种检索字符串属性与检索C#中的任何引用类型属性是相同的,不应该增加内存成本,但是从GameObeject中检索字符串属性是一种意外的本机-托管桥接的方式,(一方面会随着正常代码保存在堆栈中与管理C#代码放在同一个系统中进行管理,一方面又会随着C#代码-LL-机器语言),受次影响的两个属性是tag、name,但是tag有避免本机-托管桥接的方法,CompareTag()这个方法完全的避免了本机-托管桥接
请注意,向CompareTag传递字符串并不会导致运行时的内存分配,因为应用程序在初始化期间分配这样的硬编码字符串,在运行时只是去引用他们

使用合适的数据结构

不同的数据结构对于不同的场景有不同的效率,比如,如果仅仅是需要遍历一组对象,最好使用列表,如果需要快速找出那个对象映射到另一个对象,并且还能遍历数据,最好使用字典等等

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

在unity的早期版本(5.3甚至更早)Transform组建的引用通常在内存中是随机排列的,这意味着在多个transfor上迭代时很慢的,但是这样做的好处就是,修改一个gamobject的父节点为另一个对象并不会产生显著的性能下降;

但是在5.4以后,unity尝试将所有共享相同父元素的transform按照顺序存储在预先分配的内存缓冲区的内存中,并且在Hierarchy窗口中按照父元素下面的深度进行排序,这样的话,进行迭代时,速度更快,对于物理和动画等子系统特别有利,但是如果要在运行时将某个gameobject的父对象重新指定为另一个对象的话,父对象必须将新的子对象放入预先非陪的内存缓冲区中,并且根据深度重新对这血transform进排序,如果最开始的缓冲区不够大,还需要进行扩容,这样极大的增加了CPU的开销;

怎样避免呢? 在使用Instantiate()实例化新的gameobject的时候,有一个参数是设置实例化物体的父节点,默认值是null也就是放在Hierarchy窗口的根元素下,在Hierarchy窗口根元素下的所有transform都需要分破欸一个缓冲区来储存它当前的子元素以及以后可能添加的子元素,如果在实例化之后重新修改为另一个元素,那么他就会丢弃刚刚分配的缓冲区,为了避免这种情况,还是需要将这个parent参数提供给instantiate;
另一种降低这个过程的成本方法就是,让根transform预先分配一个大的缓冲区,如果能估计父元素包含的子transform数量,则可以节省大量不必要的内存分配

注意和缓存transform的变化

如果某transform发生了变化,不要着急去提交,或者直接去改变,例如position、rotation、scale等,因为这些属性会导致大量的未预料到的据称的乘法计算,为什么呢?因为这些值发生改变之后,会通过其父对象生成正确的transform表示,当所修改的对象在场景中的位置越深,这种计算量就越大,因此尽量修改locationposition、locationRoadtation、localScale
另一方面,如果不断的去更改Transform组件属性的另一个问题就是,他也会出发Collider、rigidbodylight、camera发送内部通知,因为这些组件也必须对你所更改过的transform进行相应的处理,所以应该在每一帧的最后去提交,如在fixedUpdate中进行每次修改的提交,当然也尽可能的去减少修改transform的次数

避免在运行时使用Find、SendMessage

众所周知,sendMessage、和find的方法很昂贵,要不惜一切代价去避免使用,下面给出几种可以避免使用这两个函数的用例

  1. 将引用分配给预先存在的对象
  2. 静态类
  3. 单例组件
  4. 全局信息传递系统
    实际上,不管是使用find获取组件,还是sendMessage发送消息,一般来说,我们想要实现的都可以归结于对象之间的通讯,可以看一下这两个全局信息传递系统
    单例的实现与注意事项

禁用未使用的脚本和对象

场景有的时候很繁忙,特别是在构建大型、开放的时节时,在update回调中,调用代码的对象越多,他的伸缩性就越差,游戏就越慢,然而如果许多正在处理的内容在玩家视野之外,实际上可以不用去处理它,直接禁用就好

通过可见性禁用对象

可以通过一种叫做视锥剔除的技术,来避免渲染对玩家相机试图不可见的对象,避免渲染隐藏在其他对象后面的对象,但是这些时渲染层面的优化,它不会影响在cpu上执行任务的组件,比如AI脚本,用户界面和游戏逻辑,所以需要我们自己控制这种行为,可以使用Became Visible和O你Became Invisible这两种方法来实现,
注意

  1. 由于可见或者不可见是与渲染管线进行通讯的,因此,当你想使用这两个函数的时候,需要确保,所判断的物体上应该有可渲染的组件,比如MeshRenderer或SkinnerMeshRenderer
  2. 可见性的操作不应该在摄像机所绑定的脚本上操作,而是在需要显示隐藏的那个物体上进行操作
  3. 当你把某个物体进行了禁用,那么不会再触发Visible,正常来讲应该把需要显示隐藏的物体当作子物体,而被显示隐藏的应该也是子物体,但是触发函数应该在父物体上进行操作
    在这里插入图片描述
 public GameObject cube;
	// Use this for initialization
    private void OnBecameVisible()
    {
        Debug.Log("可见");
        cube.gameObject.SetActive(true);
    }
    private void OnBecameInvisible()
    {
        Debug.Log("不可见");
        cube.gameObject.SetActive(false);
    }

通过距离禁用对象

正常情况下,在游戏中如果某个NPC距离我们的角色距离很远,实际上是不需要这个NPC做什么操作的,那么为了节省资源,可以规定NPC,如果监测到本身与角色的距离超过多远,就禁用自身,代码如下

public class distableAtDistance : MonoBehaviour {
    [SerializeField] GameObject _target;
    [SerializeField] float _maxDistance;
    [SerializeField] int _coroutineDelay;
	// Use this for initialization
	void Start () {
        StartCoroutine(check());
	}
	
	// Update is called once per frame
    
   IEnumerator check()
    {
        while(true)
        {
            float distSqrd = (this.transform.position - _target.transform.position).sqrMagnitude;
            if (distSqrd < _maxDistance * _maxDistance)
            {
                enabled = true;
            }
            else
            {
                enabled = false;
            }

            for(int i = 0; i < _coroutineDelay; i++)
            {
                yield return new WaitForEndOfFrame();
            }
        }
    }
}

使用距离的平方而不是距离

可以非常肯定的说,CPU更加擅长于计算浮点数的乘法,而不擅长于计算平方根,因此,如果可能的话,把使用的Distance()、magnitude所计算的两个vector距离转换为sqrMagnitude会减少大量的CPu开销,这样做的缺点是,精度可能不是那么准,因此,如果在精度允许不是那么准的前提下,可以这样修改。

最小化反序列行为

Unity中的序列化系统主要应用于场景、预制体、ScriptableObjects和各种资产类型,当对象被保存到磁盘的时候,会使用YAML格式将其转化为文本文件,然后可以再将其反序列化成原始对象类型,所有的GameObject以及其属性,都会在序列化预制体或者场景的时候进行序列化,包括其私有和受保护的字段,子物体等 这些序列化的数据会绑定在一个大的二进制数据文件中,也就是Unity内部的序列化文件,相对而言,从磁盘读取和反序列化数据是一个比较慢的过程,因此,所有的反序列化活动往往都伴随着比较大的性能成本。

这种反序列化一般发生在Resources.load的时候,由于需要加载的数据很多,并且每隔预制体的子组件都是序列化过的,因此层次结构越深,需要反序列化的数据就越多(带有很多空对象或者说每隔对象至少都包含一个Transform组件),虽然说如果某个数据已经加载过,第二次加载的时候会快很多,但是像这样加载一个打行的序列化数据集可能会在第一次加载的时候造成CPU显著峰值,如果在场景开始的时候立即需要他们,可能会延长加载时间,如果在运行的时候加载,可能会造成掉帧
解决办法

  1. 减少序列化对象 把所需要序列化的对象尽可能小或者分割成更小的数据块
  2. 异步加载序列化对象
  3. 保存已经加载过的序列化对象
  4. 将公共数据移入SCriptable Scriptable简述

叠加、异步地加载场景

加载场景的方式有以下几种

  1. 同步加载场景:阻塞主现程,一直到场景加载完毕,这样的方法对于玩家的体验来讲是很糟糕的
  2. 异步叠加式加载场景,使用SceneManager.LoadSceneAsync并传递LoadSceneMode.Additive,利用这个参数,当在某一时刻监测到玩家即将进入下一个章节的消息的时候,开始以部加载

同样的也可以卸载场景,从内存中清除出来,这样可以节省一些内存或者提升一些运行时性能,同样也可以通过异步的方式进行清除

自定义Update层

当Unity调用Update的时候,实际上时调用他的任何回调,都要经过前面所说的本机-托管的桥接,这是一个比较昂贵的任务,实际上,调用一千个Update所消耗的资源要比包含一千个常会函数的一个Update所花费的成本高很多,所以自定义Update是一个比较好的方式,代码如下

单例层



using UnityEngine;

public class SingletonComponent2<T> : MonoBehaviour where T : SingletonComponent2<T> // 这个   类型 where T   是一个基类约束,什么意思呢,当
    //使用SingletonComponent 的时候,其泛型的类型基类必须是 Monobehaviour ,这个约束必须在所有约束的前面
{
    //1.首先明确,在一个游戏的运行期间,最好只有一个事件通知系统,它负责所有事件的注册,分发,处理,以及删除,依据这个理由,
    //最好做成单例,只有当游戏释放的时候才销毁他
    //2. 又因为,注册这个的类型可能多种多样,因此使用泛型来代替
    //实现单例优先要一个静态的protect、private的字段 ,,一般来说以instance命名 为什么要是静态的是因为静态的属性或字段,只有在程序刚创建的时候创建一次
    private static T __Instance2;
    private bool _alive = true;

    //还需要有一个静态的受保护的属性获取方法,这里不能是private的,因为需要被继承的子类所用
    protected static SingletonComponent2<T> _Instance2
    {
        get
        {
            //首先要判断这个单例的对象是否已经被实现
            if (!__Instance2)
            {
                T[] managers = GameObject.FindObjectOfType(typeof(T)) as T[];
                if (managers != null)
                {
                    if (managers.Length == 1) //说明当前的这个管理者已经存在了
                    {
                        __Instance2 = managers[0];
                        return __Instance2;
                    }
                    else if (managers.Length > 1)
                    {
                        Debug.Log("在场景中,单例应该只存在一个");
                        for (int i = 0; i < managers.Length; i++)
                        {
                            T manager = managers[i];
                            Destroy(manager);
                        }
                    }
                }
                GameObject go = new GameObject(typeof(T).Name, typeof(T));
                __Instance2 = go.GetComponent<T>();
                DontDestroyOnLoad(__Instance2.gameObject);
            }
            return __Instance2;
        }
        set
        {
            __Instance2 = value as T;
        }
    }

    public static bool IsAlive
    {
        get
        {
            if (__Instance2 == null)
                return false;
            return __Instance2._alive;
        }
    }
}

管理层

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

public class GameLogicSingletonComponent : SingletonComponent2<GameLogicSingletonComponent> {
    public static GameLogicSingletonComponent Instance
    {
        get
        {
            return (GameLogicSingletonComponent)_Instance2;
        }
        set
        {
            _Instance2 = value;
        }
    }

    List<IUpdateable> _updateableObject = new List<IUpdateable>();

    public void RegisterUpdateableObject(IUpdateable obj)
    {
        if (!_updateableObject.Contains(obj))
        {
            _updateableObject.Add(obj);
        }
    }

    public void UnRegisterUpdateableObject(IUpdateable obj)
    {
        if(_updateableObject.Contains(obj))
        {
            _updateableObject.Remove(obj);
        }
    }

    private void Update()
    {
        float dt = Time.deltaTime;
        for(int i = 0; i<_updateableObject.Count;i++)
        {
            _updateableObject[i].OnUpdate(dt);
        }
    }
}

接口层

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

public interface IUpdateable {

    void OnUpdate(float dt);
}

实现层

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

public class UpdateableComponent : MonoBehaviour,IUpdateable {
    public virtual void OnUpdate(float dt)
    {
        throw new System.NotImplementedException();
    }

    // Use this for initialization
}

使用层

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

public class text : UpdateableComponent
{

	// Use this for initialization
	void Start () {
        GameLogicSingletonComponent.Instance.RegisterUpdateableObject(this);
	}
	
    public override void OnUpdate(float dt)
    {
        Debug.Log("");
    }
    // Update is called once per frame
    void Update () {
		
	}
}

记录自己这段时间所学,借鉴书籍 unity性能优化(第二版)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值