找回密码
 立即注册
查看: 361|回复: 2

Unity 粒子系统内存优化

[复制链接]
发表于 2022-1-4 07:46 | 显示全部楼层 |阅读模式
废话不多说,由于项目中使用到大量的粒子特效,内存占用过高。所以今天聊一下如何优化Unity粒子特效内存。先上效果



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

一、前言




Unity 官方推荐优化方案

在Unity的官方优化建议中提到 特别优化 - Unity 手册 ,每一个ParticleSystem至少消耗 3500 byte 内存,如果可以把ParticleSystem的配置提取到某个数据载体中会更加有效。
通常,在游戏中,我们以一个特效(prefab)为最小缓冲单位,而一个特效中又会有NParticleSystem组件(粒子系统),在需要播放某个特效的时候将其取出,播放完毕后特效回池。也就是说,一帧内使用该特效的数量峰值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[ps]);
                        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[poolCount - 1];
            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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-1-4 07:50 | 显示全部楼层
有试过GPU粒子嘛
发表于 2022-1-4 07:55 | 显示全部楼层
讲道理,GPU粒子应该更适用[思考]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 23:31 , Processed in 0.067766 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表