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($&#34;{particleSystem.name} 特效正在播放,无法被初始化&#34;);
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(&#34;m_ApplyActiveColorSpace&#34;);
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(&#34;XParticlePool&#34;);
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(&#34;总粒子数:&#34;).Append(TotalPartcleCount).AppendLine();
stringBuilder.Append(&#34;使用中粒子数:&#34;).Append(UsingParticleCount).Append(&#34; &#34;).Append(&#34;闲置的粒子数:&#34;).Append(UnusedParticleCount).AppendLine();
stringBuilder.Append(&#34;粒子总数据量:&#34;).Append(TotalParticleDataCache).Append(&#34; &#34;).Append(&#34;内存:&#34;).Append(TotalCache).AppendLine();
return stringBuilder.ToString();
}
[*]如果特效使用了Animator动画并且控制粒子节点,需要做更多的兼容工作,此处不深究,可私信
[*]在Debug模式下建立单元测试,或者性能监控捕捉系统。
[*]如果在特效中用到特殊Shader或者脚本控制,材质控制(如:广告牌,Uv滚动等),也需要特殊处理。
[*]序列化可以使用可变长字节流序列化,牺牲最高位,描述下一个字节归属。
[*]本方案经过某上线项目1w多个特效验证可行,使得原本5000+的粒子数量下降到1000+左右,内存消耗至少降低3/4
[*]因为某些原因,方案中只做了抛砖引玉。大家如果有什么问题或者建议。欢迎私信或者加群讨论 Unity性能优化186109323
有试过GPU粒子嘛 讲道理,GPU粒子应该更适用[思考]
页:
[1]