acecase 发表于 2022-1-4 07:46

Unity 粒子系统内存优化

废话不多说,由于项目中使用到大量的粒子特效,内存占用过高。所以今天聊一下如何优化Unity粒子特效内存。先上效果



利用通用(ParticleSystem)粒子组件池动态组装特效

一、前言




Unity 官方推荐优化方案

在Unity的官方优化建议中提到 特别优化 - Unity 手册 ,每一个ParticleSystem至少消耗 3500 byte 内存,如果可以把ParticleSystem的配置提取到某个数据载体中会更加有效。
通常,在游戏中,我们以一个特效(prefab)为最小缓冲单位,而一个特效中又会有N个ParticleSystem组件(粒子系统),在需要播放某个特效的时候将其取出,播放完毕后特效回池。也就是说,一帧内使用该特效的数量峰值S就会产生ParticleSystem的数量峰值 S * N,且至少消耗 S * N * 3500B 字节的内存。
如果整个场景中只有一种特效情况,ParticleSystem内存消耗峰值就是 S * N * 3500B,并无优化空间。但是如果场景中存在其他类型的特效的情况,ParticleSystem内存消耗峰值将会是所有类型特效峰值相加。
那么,有没有办法使得不同类型的特效复用ParticleSystem组件呢?
简单来说,就是把缓冲池的最小粒度由特效转为ParticleSystem组件,在运行时,将ParticleSystem动态组装到不同类型的特效中。
二、制作特效

这一步特效部门按照正常流程制作特效,这里不详述。
三、提取ParticleSystem数据




ParticleSystem

1.需要提取哪些数据

a.Transform的数据,包括位置,旋转,缩放 这里面有 3个 Vector3 数据
b.ParticleSystem 的数据,粒子系统中有22个模块,在正常的一个粒子特效中并不是每一个模块都会用到,但是在预设中,每一个模块的数据都会占用内存,即使没有激活,这也是造成粒子系统高内存的原因之一。这些模块中除了少量的引用数据之外,其他数据均为结构体或者值类型,这些都是可以被提取的。
c.ParticleSystemRenderer 粒子系统的渲染组件,其实也是一个mono组件,只是在Inspector 中合并到ParticleSystem 内显示了。这个组件包含比较多的引用。后面会跟大家介绍如果提取。
2.怎么提取数据

    关于数据载体的选择,二进制,json,xml等等都可以,但是不要使用Unity 自带的ScriptableObject
    顺带一提,预设和ScriptableObject是一种相同序列化格式,都会把对象的所有组件数据全部序列化,做不到数据分离,占用内存比较大,这里不深究。有兴趣的同学可以把ProjectSetting ->Editor->AssetSerialization 设置为 Force Text。
   本文中推荐使用二进制(byte)来提取和序列化有需要的数据。
1.首先要封装一个字节读写类 Reader/Writer,用于将基本类型写入字节流和读取读取字节流,核心 BinaryWriter 和 BinaryReader
public class Writer
{
      MemoryStream m_Stream = null;
      BinaryWriter m_BinaryWriter = null;
      
      public Writer Write(int value)
      {
            m_binaryWriter.Write(value);
            return this;
      }
      public Writer Write(float value)
      {
            m_binaryWriter.Write(value);
            return this;
      }
      .......
}

public class Reader
{
      MemoryStream m_Stream = null;
      BinaryWriter m_BinaryReader = null;
      
      public Reader Read(ref int value)
      {
            value = m_BinaryReader .ReadInt32();
            return this;
      }
      public Reader Read(float value)
      {
            m_BinaryReader .ReadSingle(value);
            return this;
      }
      .......
}
在序列化整个粒子系统模块数据中除了基本的类型,int,string,float ,bool , List, enum(用 int)等等外还有一些特殊数据结构需要特殊处理,如:Vector2,Color, AnimationCurve, MinMaxCurve,Burst等等。
如:写入AnimationCurve 曲线
public Writer Write(AnimationCurve value)
{
   if (value == null)
      {
            Write(false);
                return this;
            }
            else
            {
                Write(true);
            }

            Write((int)value.preWrapMode);
            Write((int)value.postWrapMode);
            Write(value.length);

            for (int i = 0; i < value.length; i++)
            {
                Keyframe keyframe = value.keys;
                Write(keyframe);
            }

return this;
}

public Writer Write(Keyframe value)
{
   Write(value.time);
   Write(value.value);
   Write(value.inTangent);
   Write(value.outTangent);
   Write(value.inWeight);
   Write(value.outWeight);
   Write((int)value.weightedMode);
   return this;
}
读取:
public AnimationCurve ReadAnimationCurve()
      {
            bool isNotNull = ReadBool();
            if (!isNotNull)
            {
                return null;
            }


            AnimationCurve curve = new AnimationCurve();
            curve.preWrapMode = (WrapMode)ReadInt();
            curve.postWrapMode = (WrapMode)ReadInt();
            int len = ReadInt();

            for (int i = 0; i < len; i++)
            {
                Keyframe keyframe = ReadKeyframe();
                curve.AddKey(keyframe);
            }

            return curve;
      }

      public Keyframe ReadKeyframe()
      {
            Keyframe keyframe = new Keyframe();
            keyframe.time = ReadFloat();
            keyframe.value = ReadFloat();
            keyframe.inTangent = ReadFloat();
            keyframe.outTangent = ReadFloat();
            keyframe.inWeight = ReadFloat();
            keyframe.outWeight = ReadFloat();
            keyframe.weightedMode = (WeightedMode)ReadInt();
            return keyframe;
      }
2.将粒子系统的各模块的数据按顺序写入到字节流中
    //序列化一个粒子系统
    public static Writer SerializeParticleSystem(ParticleSystem particleSystem, ParticleSystemRenderer particleSystemRenderer)
    {
      Writer writer = new Writer();
      writer.Write(particleSystem.useAutoRandomSeed);
      writer.Write(particleSystem.randomSeed);

      particleSystem.main.Serialize(writer);
      particleSystem.shape.Serialize(writer);
      particleSystem.emission.Serialize(writer);
      particleSystem.customData.Serialize(writer);
      particleSystem.trails.Serialize(writer);
      particleSystem.noise.Serialize(writer);
      particleSystem.lights.Serialize(writer);
      particleSystem.textureSheetAnimation.Serialize(writer);
      particleSystem.subEmitters.Serialize(writer);
      particleSystem.trigger.Serialize(writer);
      particleSystem.collision.Serialize(writer);
      particleSystem.externalForces.Serialize(writer);
      particleSystem.rotationBySpeed.Serialize(writer);
      particleSystem.rotationOverLifetime.Serialize(writer);
      particleSystem.sizeBySpeed.Serialize(writer);
      particleSystem.sizeOverLifetime.Serialize(writer);
      particleSystem.colorBySpeed.Serialize(writer);
      particleSystem.colorOverLifetime.Serialize(writer);
      particleSystem.forceOverLifetime.Serialize(writer);
      particleSystem.inheritVelocity.Serialize(writer);
      particleSystem.limitVelocityOverLifetime.Serialize(writer);
      particleSystem.velocityOverLifetime.Serialize(writer);
      particleSystemRenderer.Serialize(writer);
      return writer;
    }


       //反序列化一个粒子系统
      public void InitParticleSystem(XParticlePoolObject particlePoolObject)
      {
            ParticleSystem particleSystem = particlePoolObject.Particle;
            if (particleSystem.isPlaying)
            {
                Debug.LogWarning($"{particleSystem.name} 特效正在播放,无法被初始化");
                return;
            }
            particleSystem.useAutoRandomSeed = reader.ReadBool();
            uint seed = (uint)reader.ReadInt();

            if (!particleSystem.useAutoRandomSeed)
                particleSystem.randomSeed = seed;

            particleSystem.main.Deserialize(reader);
            particleSystem.shape.Deserialize(reader);
            particleSystem.emission.Deserialize(reader);
            particleSystem.customData.Deserialize(reader);
            particleSystem.trails.Deserialize(reader);
            particleSystem.noise.Deserialize(reader);
            particleSystem.lights.Deserialize(reader);
            particleSystem.textureSheetAnimation.Deserialize(reader);
            particleSystem.subEmitters.Deserialize(reader);
            particleSystem.trigger.Deserialize(reader);
            particleSystem.collision.Deserialize(reader);
            particleSystem.externalForces.Deserialize(reader);
            particleSystem.rotationBySpeed.Deserialize(reader);
            particleSystem.rotationOverLifetime.Deserialize(reader);
            particleSystem.sizeBySpeed.Deserialize(reader);
            particleSystem.sizeOverLifetime.Deserialize(reader);
            particleSystem.colorBySpeed.Deserialize(reader);
            particleSystem.colorOverLifetime.Deserialize(reader);
            particleSystem.forceOverLifetime.Deserialize(reader);
            particleSystem.inheritVelocity.Deserialize(reader);
            particleSystem.limitVelocityOverLifetime.Deserialize(reader);
            particleSystem.velocityOverLifetime.Deserialize(reader);
            particlePoolObject.Renderer.Deserialize(reader);
      }
在ParticleSystem中也不是所有模块所有的数据都需要序列化,具体看项目的使用粒子特效的需求和制作规范,另外这个仅仅序列化仅仅对值类型实例化,引用类型将使用另一种方式处理,以下是主模块的初始化:
       //序列化主模块
      public static void Serialize(this MainModule module, Writer writer)
      {
            writer.Write(module.duration);
            writer.Write(module.playOnAwake);
            writer.Write(module.startRotationYMultiplier);
            writer.Write(module.startRotationZ);
            writer.Write(module.startRotationZMultiplier);
            writer.Write(module.flipRotation);
            writer.Write(module.startColor);
            writer.Write(module.gravityModifier);
            writer.Write(module.gravityModifierMultiplier);
            writer.Write((int)module.simulationSpace);
            writer.Write(module.startRotationY);
            writer.Write(module.useUnscaledTime);
            writer.Write((int)module.scalingMode);
            writer.Write(module.maxParticles);
            writer.Write((int)module.emitterVelocityMode);
            writer.Write((int)module.stopAction);
            writer.Write((int)module.cullingMode);
            writer.Write((int)module.ringBufferMode);
            writer.Write(module.simulationSpeed);
            writer.Write(module.startRotationXMultiplier);
            writer.Write(module.startRotationX);
            writer.Write(module.startRotationMultiplier);
            writer.Write(module.loop);
            writer.Write(module.prewarm);
            writer.Write(module.startDelay);
            writer.Write(module.startDelayMultiplier);
            writer.Write(module.startLifetime);
            writer.Write(module.startLifetimeMultiplier);
            writer.Write(module.startSpeed);
            writer.Write(module.startSpeedMultiplier);
            writer.Write(module.startSize3D);
            writer.Write(module.startSize);
            writer.Write(module.startSizeMultiplier);
            writer.Write(module.startSizeX);
            writer.Write(module.startSizeXMultiplier);
            writer.Write(module.startSizeY);
            writer.Write(module.startSizeYMultiplier);
            writer.Write(module.startSizeZ);
            writer.Write(module.startSizeZMultiplier);
            writer.Write(module.startRotation3D);
            writer.Write(module.startRotation);
            writer.Write(module.ringBufferLoopRange);
      }

      //反序列化主模块
      public static void Deserialize(this MainModule module, Reader reader)
      {
            module.duration = reader.ReadFloat();
            module.playOnAwake = reader.ReadBool();
            module.startRotationYMultiplier = reader.ReadFloat();
            module.startRotationZ = reader.ReadMinMaxCurve();
            module.startRotationZMultiplier = reader.ReadFloat();
            module.flipRotation = reader.ReadFloat();
            module.startColor = reader.ReadMinMaxGradient();
            module.gravityModifier = reader.ReadMinMaxCurve();
            module.gravityModifierMultiplier = reader.ReadFloat();
            module.simulationSpace = (ParticleSystemSimulationSpace)reader.ReadInt();
            module.startRotationY = reader.ReadMinMaxCurve();
            module.useUnscaledTime = reader.ReadBool();
            module.scalingMode = (ParticleSystemScalingMode)reader.ReadInt();
            module.maxParticles = reader.ReadInt();
            module.emitterVelocityMode = (ParticleSystemEmitterVelocityMode)reader.ReadInt();
            module.stopAction = (ParticleSystemStopAction)reader.ReadInt();
            module.cullingMode = (ParticleSystemCullingMode)reader.ReadInt();
            module.ringBufferMode = (ParticleSystemRingBufferMode)reader.ReadInt();
            module.simulationSpeed = reader.ReadFloat();
            module.startRotationXMultiplier = reader.ReadFloat();
            module.startRotationX = reader.ReadMinMaxCurve();
            module.startRotationMultiplier = reader.ReadFloat();
            module.loop = reader.ReadBool();
            module.prewarm = reader.ReadBool();
            module.startDelay = reader.ReadMinMaxCurve();
            module.startDelayMultiplier = reader.ReadFloat();
            module.startLifetime = reader.ReadMinMaxCurve();
            module.startLifetimeMultiplier = reader.ReadFloat();
            module.startSpeed = reader.ReadMinMaxCurve();
            module.startSpeedMultiplier = reader.ReadFloat();
            module.startSize3D = reader.ReadBool();
            module.startSize = reader.ReadMinMaxCurve();
            module.startSizeMultiplier = reader.ReadFloat();
            module.startSizeX = reader.ReadMinMaxCurve();
            module.startSizeXMultiplier = reader.ReadFloat();
            module.startSizeY = reader.ReadMinMaxCurve();
            module.startSizeYMultiplier = reader.ReadFloat();
            module.startSizeZ = reader.ReadMinMaxCurve();
            module.startSizeZMultiplier = reader.ReadFloat();
            module.startRotation3D = reader.ReadBool();
            module.startRotation = reader.ReadMinMaxCurve();
            module.ringBufferLoopRange = reader.ReadVector2();
      }
此处除了有特殊类型需要特殊处理后,还有一个属性需要特别注意,ParticleSystemRenderer的Apply Active Color Space(影响粒子的颜色),在Unity公开的源码中并没有找到这个属性的赋值方法,获取也只能通过Editor获取,如果在旧项目中已经大量用到该属性,那么需要将用到和未用到的特效分成两个池子,分别使用两个模板去实例化。后面在内存池的使用时会提到。
      //获取applyActiveColorSpace
      SerializedObject serializedObject = new SerializedObject(renderer);
      SerializedProperty property1 = serializedObject.FindProperty("m_ApplyActiveColorSpace");


3.处理引用类型
除了上述的基本类型中,粒子特效中还有其他引用类型,如 :Mesh,Texture,Transform,GameObject,Material 等等,这些引用类型存放到特效 Player 的Elements对象中。



播放器的序列化面板

到此粒子系统的数据提取和读取已经完成
3.子发射器(SubEmitter)模块

这个模块特别特殊,我觉得需要着重去介绍以下。因为SubEmitter是可以控制器其他粒子系统的,此处对子发射的引用关系改成了下标关系(前提是对一个特效下所有粒子进行排序)
      //处理子发射器数据
      //写入子发射器数目
      headWriter.Write(subEmitterCount);
      for (int i = 0; i < totalCount; i++)
      {
            ParticleSystem particleSystem = newParticleSystems;
            if (particleSystem.subEmitters.enabled && particleSystem.subEmitters.subEmittersCount > 0)
            {
                headWriter.Write(i);
                SubEmittersModule subEmitters = particleSystem.subEmitters;
                int count = subEmitters.subEmittersCount;
                headWriter.Write(count);
                if (count > 0)
                {
                  for (int j = 0; j < count; j++)
                  {
                        ParticleSystem ps = subEmitters.GetSubEmitterSystem(j);
                        if (ps == null)
                        {
                            headWriter.Write(-1);
                            continue;
                        }
                        headWriter.Write(newParticleSystemsIndex);
                        headWriter.Write(subEmitters.GetSubEmitterEmitProbability(j));
                        headWriter.Write((int)subEmitters.GetSubEmitterProperties(j));
                        headWriter.Write((int)subEmitters.GetSubEmitterType(j));
                  }
                }
            }
      }
也是因为这个SubEmitter关系,我们发现另一个问题,在原特效中,如果挂载粒子系统(ParticleSystem)的节点存在子级节点,我们就不能对此节点进行优化,但是它仍需要进入运行时的组装流程,因此我们需要将该节点描述为一个不需进行数据处理的粒子系统,但是仍旧参与子发射的关联操作。为此我们为粒子系统定义了三种组装模式。
    public enum XParticleMode
    {
      NONE = 0, //有子节点,不处理,但是参与子发射器关联
      DELETE, //ParticleSystem 从内存池中分配
      DATAONLY //该模式为有Animator或者Timeline动画设计,不移除节点,仅仅删除 ParticleSystem和ParticleSystemRender,动态装载数据
    }
四、内存池设计

上面说到由于 ParticleSystemRenderer 的 Apply Active Color Space,设计两个内存池,当然如果是新项目可以这个方面做出取舍,规定用或者不用这个参数,只要看起来是对的就是对的。为此还预制了两个粒子特效的模板。
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.ParticleSystem;
//粒子内存池
public class XParticlePool
{
    static Transform poolRoot = null;
    static UnityEngine.Object FxBase = null;
    static UnityEngine.Object FxBaseColorSpace = null;


    static List<XParticlePoolObject> ParticlePoolObjects = new List<XParticlePoolObject>();
    static List<XParticlePoolObject> UsingParticlePoolObjects = new List<XParticlePoolObject>();

    static List<XParticlePoolObject> ColorSpaceParticlePoolObjects = new List<XParticlePoolObject>();
    static List<XParticlePoolObject> ColorSpaceUsingParticlePoolObjects = new List<XParticlePoolObject>();
    public static Transform PoolRoot
    {
      get
      {
            if (poolRoot == null)
            {
                GameObject obj = new GameObject("XParticlePool");
                poolRoot = obj.transform;
                Object.DontDestroyOnLoad(obj);
            }

            return poolRoot;
      }
    }


    public static XParticlePoolObject Spawn()
    {
      XParticlePoolObject particlePoolObject = null;
      int poolCount = ParticlePoolObjects.Count;
      if (poolCount > 0)
      {
            particlePoolObject = ParticlePoolObjects;
            if (particlePoolObject.Particle != null && particlePoolObject.Particle.isPlaying)//部分特效没有停下来,很奇怪
            {
                particlePoolObject.Particle.Stop();
                particlePoolObject = CreatParticlePoolObject();
            }
            else
            {
                ParticlePoolObjects.RemoveAt(poolCount - 1);
                particlePoolObject.isUsing = true;
            }
            particlePoolObject.isUsing = true;

      }
      else
      {
            particlePoolObject = CreatParticlePoolObject();
            particlePoolObject.isUsing = true;
      }
      particlePoolObject.OnSpawn();
      UsingParticlePoolObjects.Add(particlePoolObject);
      return particlePoolObject;
    }

    public static XParticlePoolObject SpawnColorSpace()
    {
.....
    }

    public static XParticlePoolObject CreatParticlePoolObject(bool bApplyActiveColorSpace = false)
    {
......
    }

    public static void DeSpawn(XParticlePoolObject particlePoolObject)
    {
      particlePoolObject.OnDespawn();
      particlePoolObject.transform.SetParent(PoolRoot.transform, false);
      UsingParticlePoolObjects.Remove(particlePoolObject);
      ParticlePoolObjects.Add(particlePoolObject);
    }

    public static void DeSpawnColorSpace(XParticlePoolObject particlePoolObject)
    {
       .....
    }

}
五、播放特效

播放流程:

[*]加载该特效的序列化数据
[*]从缓冲池中实例化粒子模板并挂载到特效对应的节点上
[*]反序列化并且填充粒子系统数据
[*]重新关联父子发射器关系
[*]播放
   for (int i = particlePoolObjectDic.Count - 1; i >= 0; i--)
            {

                if (!particlePoolObjectDic.ContainsKey(i))
                  continue;

                XParticlePoolObject particlePoolObject = particlePoolObjectDic;

                if (particlePoolObject.Particle.main.playOnAwake)
                {
                  particlePoolObject.Particle.Play();
                }
            }
在播放特效的是此处有两点建议:

[*]建议延迟到帧末(LateUpdate)播放
[*]播放前检查从池中取出的粒子系统状态,如果是播放中,则取其他闲置粒子。
六、其他

//需要检测性能指标
public static string ToStringInfo()
    {
      StringBuilder stringBuilder = new StringBuilder();
      stringBuilder.Append("总粒子数:").Append(TotalPartcleCount).AppendLine();
      stringBuilder.Append("使用中粒子数:").Append(UsingParticleCount).Append(" ").Append("闲置的粒子数:").Append(UnusedParticleCount).AppendLine();
      stringBuilder.Append("粒子总数据量:").Append(TotalParticleDataCache).Append(" ").Append("内存:").Append(TotalCache).AppendLine();
      return stringBuilder.ToString();
    }

[*]如果特效使用了Animator动画并且控制粒子节点,需要做更多的兼容工作,此处不深究,可私信
[*]在Debug模式下建立单元测试,或者性能监控捕捉系统。
[*]如果在特效中用到特殊Shader或者脚本控制,材质控制(如:广告牌,Uv滚动等),也需要特殊处理。
[*]序列化可以使用可变长字节流序列化,牺牲最高位,描述下一个字节归属。
[*]本方案经过某上线项目1w多个特效验证可行,使得原本5000+的粒子数量下降到1000+左右,内存消耗至少降低3/4
[*]因为某些原因,方案中只做了抛砖引玉。大家如果有什么问题或者建议。欢迎私信或者加群讨论 Unity性能优化186109323

BlaXuan 发表于 2022-1-4 07:50

有试过GPU粒子嘛

HuldaGnodim 发表于 2022-1-4 07:55

讲道理,GPU粒子应该更适用[思考]
页: [1]
查看完整版本: Unity 粒子系统内存优化