Cocos plist 粒子一键导入 Unity:Editor 工具实现与参数映射

一、背景与目标

项目从Cocos2d-x迁移至Unity时,美术在Cocos中调试好的粒子效果以.plist参数文件形式存在。迁移到Unity后,需要在ParticleSystem中逐项手动复现这些参数,效率低且容易与原效果产生偏差。

本文实现一个Unity Editor工具:通过菜单栏选择.plist文件,自动解析参数并生成对应的ParticleSystem Prefab,实现Cocos粒子到Unity的自动化迁移。

二、plist文件结构

Cocos粒子文件本质是XML格式的键值对配置。以下是dropOfBlock.plist的关键片段:

xml 复制代码
<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)的倍数。因此需要做一次换算:

csharp 复制代码
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个粒子:

csharp 复制代码
em.rateOverTime = main.maxParticles / life;

四、工具实现

4.1 Editor程序集与菜单注册

脚本需放置于 Assets/Editor/ 目录下。Unity 会将该目录下的脚本编译为独立的编辑器程序集,打包时自动剔除,且仅在编辑器环境生效。这是必要的,因为工具依赖 UnityEditor 命名空间下的 API(如文件面板、Prefab 写入),这些 API 不能在运行时调用。

菜单栏注册通过 MenuItem 特性完成,编译后 Unity 会自动在对应菜单项下生成入口:

csharp 复制代码
[MenuItem("Tools/导入 Cocos 粒子")]
public static void Import() { ... }

4.2 处理流程

工具执行顺序如下:

  1. 弹出系统文件选择框,获取.plist路径;
  2. 解析XML,将键值对存入字典;
  3. 临时创建GameObject并添加ParticleSystem组件;
  4. 按映射表设置Main、Shape、Emission等模块参数;
  5. 根据textureFileName查找贴图,创建Alpha Blended材质并赋给Renderer;
  6. 调用PrefabUtility.SaveAsPrefabAsset将对象序列化为Prefab资源;
  7. 调用Object.DestroyImmediate删除场景中的临时对象;
  8. 调用AssetDatabase.Refresh刷新资源数据库。
    4.3 完整代码
csharp 复制代码
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,游戏启动时该粒子会在场景中持续播放。

七、验证方法

工具正确性可通过以下步骤验证:

  1. Console输出 "导入完成" 日志,且无报错;
  2. Assets/Resources/Prefabs/目录下生成对应.prefab文件,场景中无残留对象;
  3. 双击Prefab,Inspector面板中的参数与plist原文件逐项核对一致;
  4. 点击ParticleSystem预览按钮,观察到粒子按预期爆发并受重力下坠;
  5. 与原Cocos工程效果并排对比,速度、大小、方向基本一致。

八、后续扩展

当前版本已覆盖基础参数映射,后续可补充:

  • finishParticleSize映射至Size over Lifetime模块,实现粒子尺寸随生命周期变化;
  • startColor / finishColor映射至Color over Lifetime,实现颜色渐变;
  • 批量处理目录下所有plist文件;
  • 基于EditorWindow实现可视化参数预览。
相关推荐
SmalBox1 小时前
04-03-源码-构建映射与依赖分析(BuildMap)
unity3d·游戏开发
淡海水2 小时前
03-02-架构篇-Boehm GC架构解析
unity·架构·c#·gc·boehm
甲维斯13 小时前
我把“赶 Deadline”和AI做成了一款游戏
人工智能·游戏开发
xcLeigh21 小时前
Unity基础:Instantiate实例化——在运行时动态创建游戏物体
游戏·unity·游戏引擎·教程·instantiate
SmalBox1 天前
04-02-源码-资源收集器(AssetBundleCollector)
unity3d·游戏开发
字节暗面1 天前
客户端上报不可信:游戏服务端数据校验与对局重演设计
c#·游戏开发·服务端校验·反作弊
字节暗面1 天前
游戏服务端如何校验客户端上报数据?四层校验思路详解
游戏开发
甲维斯2 天前
我用AI做了一款「三国吃鸡」,开局孔明灯,落地青龙刀 !
人工智能·游戏·游戏开发
SmalBox2 天前
04-01-源码-编辑器总览
unity3d·游戏开发