[Unity3D游戏开发] Unity和C# 代码优化

代码优化


在商业游戏中代码优化是必不可少的,这极大的影响游戏运行的性能,以及代码的扩展性和可读性。应该具备良好的编码习惯,才能从初级程序走得更高更远。

1, Unity代码优化

1.1 缓存对象

不要在Update()方法中每帧获取组件对象,好的做法应该是在Start()或者Awake()中 获取并缓存下来,这样在Update()方法中就能直接使用了。缓存指的是在使用前就获得这个对象。

不过,也存在一种可能即Update()方法中存在条件判断,导致一段时间内无法达成,这样在Start()方法中获取的对象就没用了,这种使用get方法,只在第一次使用该对象时才会进行读取。

Rigbody rB;
Rigbody RB
{
    get
    {
      if(rB==null)
          rB=GetComponent<Rigbody>();
        return rB;
    }
}

代码解析:用属性中的get方法,只有rB为空时才会得到该组件,得到该组件之后rB不为空不会再次得到,也就是只得一次

1.1 减少脚本

Unity的脚本用起来很方便但是也比较消耗性能。脚本主要用于生命周期的方法,比如OnEnable()、Update()。创建多个对象就需要多个Update()方法。好的做法是通过一个管理类,在一个Update()方法中执行所有的对象。可以将Update()封装成一个事件,这样即使是非Mono类中也可以任意添加Update()方法了。

using System;
using UnityEngine;

public class Script_17_01 
{
    static public event Action onUpdate;//声明 事件onUpdate
    static Internal s_Internal;//声明 类Internal
    static Script_17_01()
    {
        s_Internal = new GameObject("_Event_").AddComponent<Internal>();
        GameObject.DontDestroyOnLoad(s_Internal.gameObject);
    }
    class Internal : MonoBehaviour
    {
        private void Update()
        {
            onUpdate?.Invoke();//委托调用
        }
    }
}

在任意位置添加事件:

Script_17_01.onUpdate+=()=>//委托添加方法参数
{
   Debug.Log("update");
};

1.3 减少Update()执行

可以将核心方法通过Time.frameCount取余,这样可以每3帧执行里面的方法而不是每帧调用。

private void Update()
{
    if(Time.FrameCount%3==0){}
}

更有效的手段是通过Application.targetFrameRate来强制设置当前帧率。当手指触摸屏幕后设置为69帧,发现10秒后没有继续点击设置为30帧。

private float m_LastInputTime;
private void Update()
{
   if(Time.time-m_LastInputTime>10f)
   {
     Application.targetFrameRat=30;
   }
   if (Input.GetMouseButton(0))
   {
    m_LastInputTime=Time.time;
     Application.targetFrameRat=60;
   }
}

1.4 缓存池

游戏中频繁创建与卸载会影响加载时间,对于频繁创建和转移的对象,推荐使用缓存池。从缓存池中移除对象只是暂时隐藏对象,使用时再启动。

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

public class Script_17_02 :MonoBehaviour
{
    ObjectPool<GameObject> m_ObjectPool;
    private void Start()
    {
        //创建缓存池,创建、获取、释放、销毁事件、默认于最大缓存池数量
        m_ObjectPool = new ObjectPool<GameObject>(Create, Get, Release, Destroy, true, 10, 20);
    }

    GameObject Create()
    {
        return new GameObject();
    }
    void Get(GameObject go)
    {
        go.SetActive(true);
    }
    void Release(GameObject go)
    {
        go.SetActive(false);
    }

    void Destroy(GameObject go)
    {
        go.SetActive(false);
    }

    List<GameObject> list = new List<GameObject>();
    private void Update()
    {
        if (Input.GetKeyUp(KeyCode.A))
        {
            list.Add(m_ObjectPool.Get());
        }
        if (Input.GetKeyUp(KeyCode.D))
        {
            //清除最后一个
            if (list.Count > 0)
            {
                int index = list.Count - 1;
                m_ObjectPool.Release(list[index]);
                list.RemoveAt(index);
            }
        }
    }
}

1.5 日志优化

通过Debug.unityLogger.logEnabled可以运行时动态开关日志,但是输出日志为字符串拼接的代码依然会产生堆内存。

using UnityEngine;

public class Script_17_03 :MonoBehaviour
{
    void Update()
    {
        Log.Print(Time.frameCount + " " + gameObject);
        //Debug.Log(Time.frameCount+" " + gameObject);
    }
}

public static class Log
{
    [System.Diagnostics.Conditional("ENABLE_DEBUG")]//通过宏来控制该方法的执行。
    static public void Print(object message)
    {
        Debug.Log(message);
    }
}

当前没有启动ENABLE_DEBUG宏所以不会执行Print()方法,可以在项目设置(projectsetting)添加宏,也可以通过代码动态的添加宏。

2. C#代码优化

值类型存储在栈上,引用类型存储在堆中,栈上同时存储着指向堆的引用类型的地址。

2.1 装箱和拆箱

值类型和引用类型可以相互转换,这会导致栈上的数据在堆上又生成一份,要避免装箱拆箱。

•避免使用ArrayList对象,因为它是用引用类型来保存数据的,好的做法是List这种泛型。

•使用协程时一定不要使用yield return 0,这样做会让值类型变成引用类型,好的做法是yield return null

2.2 字符串

•声明字符串再修改字符串会在堆上重新创建一个对象。大量修改字符应该使用StringBuilder,它在内存中会分配一块内存,当长度超过内存时会自动扩大一倍

•UI中需要显示角色属性,一般是值类型要变成引用类型,意味着进行一次装箱,减少开销的方法是避免在Update中每帧调用toString()方法

2.3 struct

struct是值类型,虽然和class很像,但是有很大区别,使用上,struct的构造函数必须传入参数,而且不能继承,成员对象不能设置为抽象和虚函数。结构体内也可以声明引用类型,在栈上保存的是指向堆中的地址。struct对象在复制赋值时会将栈上所有的数据进行赋值。所以struct适合少量数据的保存。

2.4 GC

自身的引用被断开即可满足“垃圾”的条件。

Unity勾选Use incremental GC则启用渐进式GC,这是一种异步执行的操作,这样就不会在一帧上卡住。

可以通过代码来设置GC和开启关闭GC。

3,Profiler 内存管理

Unity内存由3部分组成,分别是托管堆内存、非托管内存和、原生内存。托管内存前面介绍过很多,非托管内存是无法被GC的内存,相当于在C#启动不安全代码由C++分配的内存,它需要程序手动释放;最后是原生内存,Unity中所有资源都是原生内存。

3.1 内存泄漏

没有及时释放无用对象,或是它们被静态对象所引用。

3.2 耗时函数统计

点击Window—Analysis—Profiler打开Profile让,但是由于是在编辑器模式下提供的工具,所以不准确,Unity提供了Profiler(Standalone Process)独立程序。

3.4 堆内存分配

随着程序扩容,堆内存并不会删除之前产生的内存。string已经被释放,但是它在堆内存中依然占据容量,此时分配一块float[]数组,由于容量比string大所以堆内存会被扩充,只要当声明一块比string小的内存才可以分配到string的内存上,此时string此时的内存就会产生内存碎片。直到下次分配一块容量小于它的内存时才会分配这里。

3.6 Unity内存

堆内存是自己写代码产生的,只是Unity内存的一小部分。

新版Unity安装Memory Profiler工具,在package manager安装即可,可以看到内存占用信息。

4 着色器优化

从模型资源的角度,减少模型顶点数可以优化性能,当模型拉远时,可以优化片元着色器的执行数量,从而优化性能.

世界空间和纹理座标对精度要求比较高因此使用float,其他对精度要求不高的使用half。

关于着色器相关 后续更新……

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值