一、背景与目标
项目从Cocos2d-x迁移至Unity时,美术在Cocos中调试好的粒子效果以.plist参数文件形式存在。迁移到Unity后,需要在ParticleSystem中逐项手动复现这些参数,效率低且容易与原效果产生偏差。
本文实现一个Unity Editor工具:通过菜单栏选择.plist文件,自动解析参数并生成对应的ParticleSystem Prefab,实现Cocos粒子到Unity的自动化迁移。
二、plist文件结构
Cocos粒子文件本质是XML格式的键值对配置。以下是dropOfBlock.plist的关键片段:
<dict>
<key>duration</key><real>0.15</real>
<key>maxParticles</key><integer>80</integer>
<key>particleLifespan</key><real>0.8</real>
<key>speed</key><real>200</real>
<key>gravityy</key><real>-500</real>
<key>textureFileName</key><string>dropOfBlockTexture0.png</string>
</dict>
其中<key>标签后紧跟一个值节点,可能为浮点数<real>、整数<integer>或字符串<string>。Cocos运行时读取这些键值对驱动粒子系统。转换工具的输入即为该文件,解析后按对应关系写入Unity ParticleSystem的各模块属性。
三、参数映射关系
Unity ParticleSystem采用模块化结构,每个模块独立控制一类行为。核心映射如下:
| Cocos plist 字段 | 取值 | 含义 | Unity 模块与属性 |
|---|---|---|---|
| duration | 0.15 | 发射持续时间(秒),结束后不再产生新粒子 | Main → Duration |
| maxParticles | 80 | 场上同时存在的最大粒子数 | Main → Max Particles |
| particleLifespan | 0.8 | 每个粒子存活时间(秒) | Main → Start Lifetime |
| speed | 200 | 粒子初速度 | Main → Start Speed |
| speedVariance | 15 | 初速度随机浮动范围 | Main → Start Speed(最小~最大) |
| startParticleSize | 20 | 粒子初始大小 | Main → Start Size |
| gravityy | -500 | 纵向重力(负值向下) | Main → Gravity Modifier |
| angleVariance | 360 | 发射方向随机范围(360=四面八方) | Shape → Circle |
| textureFileName | dropOfBlockTexture0.png | 粒子贴图文件名 | Renderer → Material |
以下两个映射需要额外换算,直接照搬数值会导致效果偏差。
3.1 重力单位换算
Cocos的gravityy为其坐标系下的加速度值;
Unity的Gravity Modifier并非绝对加速度,而是物理世界默认重力(-9.81)的倍数。因此需要做一次换算:
main.gravityModifier = gravityY / -9.81f; // -500 / -9.81 ≈ 51
若直接填入-500,粒子将以500倍重力加速度下坠,瞬间脱离屏幕范围。
3.2 发射速率推导
plist中不包含"每秒发射数量"字段。Cocos的发射速率由maxParticles与particleLifespan隐式推导得出:
场上同时存在的最大粒子数为80,单个粒子存活0.8秒,稳态发射速率即为80 / 0.8 = 100 个 / 秒。
Unity的Emission模块需要显式设置该值,否则默认10/s的速率在0.15秒的发射窗口内只能生成1~2个粒子:
em.rateOverTime = main.maxParticles / life;
四、工具实现
4.1 Editor程序集与菜单注册
脚本需放置于 Assets/Editor/ 目录下。Unity 会将该目录下的脚本编译为独立的编辑器程序集,打包时自动剔除,且仅在编辑器环境生效。这是必要的,因为工具依赖 UnityEditor 命名空间下的 API(如文件面板、Prefab 写入),这些 API 不能在运行时调用。
菜单栏注册通过 [MenuItem] 特性完成,编译后 Unity 会自动在对应菜单项下生成入口:
[MenuItem("Tools/导入 Cocos 粒子")]
public static void Import() { ... }
4.2 处理流程
工具执行顺序如下:
- 弹出系统文件选择框,获取
.plist路径; - 解析
XML,将键值对存入字典; - 临时创建
GameObject并添加ParticleSystem组件; - 按映射表设置
Main、Shape、Emission等模块参数; - 根据
textureFileName查找贴图,创建Alpha Blended材质并赋给Renderer; - 调用
PrefabUtility.SaveAsPrefabAsset将对象序列化为Prefab资源; - 调用
Object.DestroyImmediate删除场景中的临时对象; - 调用
AssetDatabase.Refresh刷新资源数据库。
4.3 完整代码
using System.Collections.Generic;
using System.IO;
using System.Xml;
using UnityEditor;
using UnityEngine;
public static class CocosParticleImporter
{
[MenuItem("Tools/导入 Cocos 粒子")]
public static void Import()
{
string path = EditorUtility.OpenFilePanel("选择 Cocos 粒子 plist", "", "plist");
if (string.IsNullOrEmpty(path)) return;
var p = ParsePlist(path);
var go = new GameObject("CocosParticle_" + Path.GetFileNameWithoutExtension(path));
var ps = go.AddComponent<ParticleSystem>();
var main = ps.main;
main.duration = Get(p, "duration", 0.15f);
main.maxParticles = (int)Get(p, "maxParticles", 80);
main.startLifetime = Get(p, "particleLifespan", 0.8f);
main.startSpeed = Get(p, "speed", 200f);
main.startSize = Get(p, "startParticleSize", 20f);
main.loop = false;
main.playOnAwake = false;
var shape = ps.shape;
shape.enabled = true;
shape.shapeType = ParticleSystemShapeType.Circle;
shape.radius = 0f;
// 重力单位换算:Cocos 的 gravityy 是绝对加速度,Unity 的 Gravity Modifier 是物理重力(-9.81)的倍数
float gravityY = Get(p, "gravityy", 0f);
main.gravityModifier = gravityY / -9.81f;
// 发射速率:plist 无此字段,按 maxParticles / lifespan 推导
float life = Get(p, "particleLifespan", 0.8f);
var emission = ps.emission;
emission.enabled = true;
emission.rateOverTime = main.maxParticles / life;
string texName = GetString(p, "textureFileName", "");
if (!string.IsNullOrEmpty(texName))
{
string texPath = Path.Combine(Path.GetDirectoryName(path), texName);
if (File.Exists(texPath))
{
string dst = "Assets/Resources/effect/" + texName;
File.Copy(texPath, dst, true);
AssetDatabase.Refresh();
var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(dst);
// 贴图:Alpha Blended shader 才能正确显示透明 PNG
var mat = new Material(Shader.Find("Mobile/Particles/Alpha Blended"));
mat.mainTexture = tex;
go.GetComponent<ParticleSystemRenderer>().material = mat;
}
}
Directory.CreateDirectory("Assets/Resources/Prefabs");
string prefabPath = "Assets/Resources/Prefabs/" + go.name + ".prefab";
// 存 Prefab 后必须删除场景临时对象,否则会残留在 GameScene
PrefabUtility.SaveAsPrefabAsset(go, prefabPath);
Object.DestroyImmediate(go);
AssetDatabase.Refresh();
Selection.activeObject = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
Debug.Log("[CocosParticle] 导入完成: " + prefabPath);
}
static Dictionary<string, string> ParsePlist(string path)
{
var dict = new Dictionary<string, string>();
var xml = new XmlDocument();
xml.Load(path);
var nodes = xml.DocumentElement.SelectSingleNode("dict").ChildNodes;
for (int i = 0; i < nodes.Count - 1; i++)
{
if (nodes[i].Name == "key")
dict[nodes[i].InnerText.Trim()] = nodes[i + 1].InnerText.Trim();
}
return dict;
}
static float Get(Dictionary<string, string> d, string key, float def)
=> d.TryGetValue(key, out var v) ? float.Parse(v) : def;
static string GetString(Dictionary<string, string> d, string key, string def)
=> d.TryGetValue(key, out var v) ? v : def;
}
五、材质与透明渲染
碎片图为带透明背景的PNG,若使用Particles/Standard Unlit shader,透明区域会渲染为不透明色块。需选用Mobile/Particles/Alpha Blended shader,其在片元阶段根据alpha通道执行半透明混合,透明像素才能正确镂空。
六、常见问题
6.1 粒子仅生成单个大方块
原因:Emission模块未设置rateOverTime,默认值过低。解决方法如3.2节所述,按maxParticles / particleLifespan推导。
6.2 gravityModifier编译报错
ParticleSystem.gravityModifier 为 float 类型,并非独立模块结构。早期代码将其当作模块访问 enabled、gravity 属性会导致编译失败。正确做法是通过 main.gravityModifier 直接赋值。
6.3 临时对象残留于场景
SaveAsPrefabAsset仅将对象序列化到磁盘,不会自动删除场景实例。若不调用DestroyImmediate,游戏启动时该粒子会在场景中持续播放。
七、验证方法
工具正确性可通过以下步骤验证:
- Console输出 "导入完成" 日志,且无报错;
Assets/Resources/Prefabs/目录下生成对应.prefab文件,场景中无残留对象;- 双击
Prefab,Inspector面板中的参数与plist原文件逐项核对一致; - 点击
ParticleSystem预览按钮,观察到粒子按预期爆发并受重力下坠; - 与原
Cocos工程效果并排对比,速度、大小、方向基本一致。
八、后续扩展
当前版本已覆盖基础参数映射,后续可补充:
finishParticleSize映射至Size over Lifetime模块,实现粒子尺寸随生命周期变化;startColor / finishColor映射至Color over Lifetime,实现颜色渐变;- 批量处理目录下所有
plist文件; - 基于
EditorWindow实现可视化参数预览。
