尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity编辑器插件开发:自动化Cocos Creator UI资产迁移方案

Unity编辑器插件开发:自动化Cocos Creator UI资产迁移方案 1. 项目概述从Cocos到Unity的资产迁移痛点如果你是从Cocos Creator转向Unity开发的团队或个人大概率都经历过一个让人头疼的阶段UI界面或场景布局的还原。Cocos Creator的节点树结构和坐标系统与Unity的GameObject层级和RectTransform虽然概念上相通但在具体实现和参数上存在不少差异。手动将一个复杂界面上几十上百个控件的精确位置、旋转、缩放、锚点以及父子关系从Cocos项目里一个个“抄”到Unity里不仅耗时耗力还极易出错特别是当UI需要频繁迭代时这种重复劳动简直是一场噩梦。我最近就接手了一个老项目的移植任务核心需求就是把Cocos Creator 2.x版本的一套成熟UI主题Theme完整地迁移到Unity UGUI中。这里的“Theme”不仅仅指皮肤样式更包括了所有UI控件的类型、位置、尺寸、层级关系也就是关键的parent父链等完整的布局信息。手动操作了第一个界面后我就意识到必须寻找自动化方案。市面上虽然有一些通用的格式转换工具但往往对Cocos特有的.fire场景文件或自定义导出格式支持不佳尤其是对复杂的、嵌套的父子节点链parent chain的还原经常丢三落四。于是我决定自己动手开发一个Unity编辑器插件目标很明确一键解析Cocos Creator导出的布局数据文件并在Unity编辑器中自动、准确地重建出整个UI层级结构确保每个节点的本地坐标、锚点、尺寸乃至其在整个节点树中的位置即parent关系都得到完美还原。这个插件不仅要解决“有没有”的问题更要解决“准不准”和“快不快”的问题。经过几轮迭代目前这个工具已经能稳定处理我们项目中绝大多数复杂界面效率提升超过90%。接下来我就把这套方案的实现思路、核心细节、踩过的坑以及最终成型的插件设计毫无保留地分享出来。2. 核心思路与方案选型为什么选择编辑器插件面对Cocos到Unity的资产迁移通常有几条路可以走手动重建、编写运行时解析脚本、或者开发编辑器工具。我们需要根据需求做出权衡。手动重建是最直接也是最笨的方法适用于控件极少、且无需后续同步的简单场景。但对于一个完整的游戏项目这显然不可行违背了我们追求效率的初衷。运行时解析指的是将Cocos的布局数据如JSON打包进游戏资源游戏启动时动态创建UI。这种方法有一定灵活性但缺点也很明显1) 无法在编辑器中“所见即所得”地进行微调和预览不利于美术和策划协作2) 运行时动态创建会带来性能开销和内存管理复杂度3) 无法利用Unity编辑器强大的预制体Prefab系统进行资源管理。编辑器插件则完美弥补了上述方案的不足。它运行在Unity Editor环境下可以直接操作场景中的GameObject实现“一键导入立即可见”。生成的结果是静态的、可编辑的Prefab或场景对象完全融入Unity的工作流。开发团队可以立即在此基础上进行材质替换、动画添加、逻辑绑定等后续工作。更重要的是插件可以封装复杂的解析逻辑提供友好的操作界面如一个自定义的Inspector窗口或工具栏按钮让非技术人员也能轻松使用。因此我的选择是开发一个Unity Editor Window插件。它的核心工作流程是数据准备从Cocos Creator项目中通过脚本或工具导出所需UI场景的节点树信息通常是一个结构化的JSON文件。数据解析插件读取这个JSON文件将其反序列化为内存中的数据结构。映射与创建遍历数据结构根据节点类型Sprite, Label, Button等映射到对应的Unity UGUI组件Image, Text, Button等。层级与变换还原最关键的一步按照数据中的parent索引信息重建出正确的父子层级关系并计算每个节点在Unity坐标系下的RectTransform参数anchoredPosition,sizeDelta,anchorMin/Max,pivot等。资产关联尝试根据资源路径或名称关联或提示用户关联对应的Sprite、Font等资源。这个方案的核心挑战在于坐标系与变换系统的转换以及父子链的准确重建。下面我们就深入这两个核心细节。3. 核心细节解析坐标系转换与Parent链重建3.1 坐标系差异从Cocos的“左下角原点”到Unity的“中心锚点”这是所有空间信息转换的基础如果这里错了所有控件的位置都会乱套。Cocos Creator (2.x) 通常使用笛卡尔坐标系原点(0, 0)在屏幕左下角。一个节点Node的位置Position是其**锚点Anchor相对于父节点包围盒Bounding Box**的位置。锚点默认为(0.5, 0.5)即中心。尺寸Size是节点内容的大小。Unity UGUI 使用基于锚点Anchors和轴心Pivot的矩形变换系统。一个RectTransform的位置anchoredPosition是其轴心点相对于锚点参考位置的偏移。原点概念相对灵活但anchoredPosition的(0,0)通常意味着轴心点与锚点重合。转换的关键我们不能简单地做坐标值的直接赋值。需要结合父节点的尺寸、当前节点的锚点、以及Unity中我们期望的锚点预设进行一系列计算。实操中的计算逻辑以节点位于父节点中心为例假设Cocos数据中一个节点node的position为(x, y)anchor为(ax, ay)归一化值0~1size为(width, height)。其父节点parent的size为(pWidth, pHeight)。计算节点在Cocos坐标系下的实际包围盒节点左下角在世界坐标系中的位置相对于父节点左下角worldX x - ax * width但这并不是我们需要的。更通用的思路是计算节点轴心点假设为(px, py)Cocos中通常也是(0.5,0.5)在父节点坐标系下的归一化位置。节点轴心点在父节点内的局部位置localPivotX x,localPivotY y(因为Cocos的position就是锚点相对于父节点包围盒的位置而锚点通常就是轴心)。将局部位置转换为归一化坐标相对于父节点尺寸normalizedX localPivotX / pWidth,normalizedY localPivotY / pHeight。注意这里可能需要处理原点差异Cocos左下角Unity的锚点系统。映射到Unity的RectTransform在Unity中我们通常将UGUI控件的锚点Anchors预设为(0.5, 0.5)即中心对齐这样anchoredPosition就是控件中心相对于父节点中心的偏移。因此我们需要将Cocos中计算出的normalizedX和normalizedY其参考系是父节点左下角为(0,0)右上角为(1,1)转换到以中心为(0,0)的坐标系。unityNormalizedX normalizedX - 0.5unityNormalizedY normalizedY - 0.5那么anchoredPosition.x unityNormalizedX * pWidth,anchoredPosition.y unityNormalizedY * pHeight。同时sizeDelta可以直接设置为(width, height)。但要注意sizeDelta的含义与锚点设置强相关。当锚点是对角线拉伸模式时sizeDelta的含义会变。为了简化初期我们可以固定锚点为(0.5, 0.5)此时sizeDelta就是控件的实际尺寸。注意这是一个高度简化的模型。实际项目中Cocos节点的锚点可能不是(0.5,0.5)也可能使用了“拉伸”模式。Unity这边也可能需要不同的锚点预设来适配不同的布局需求。因此在插件中这部分转换逻辑需要设计得足够灵活最好能通过配置映射表来处理不同的锚点对应关系。3.2 Parent链重建维系节点世界的血脉还原单个控件的位置固然重要但还原整个UI树的结构才是插件的灵魂。Cocos导出的数据中每个节点都应该包含一个指向其父节点的标识符如parentIndex、parentId或嵌套的children数组。我们的插件必须能正确处理这种关系。实现策略两次遍历法这是最稳妥的方法。第一次遍历解析JSON数据为每一个节点数据创建一个轻量的“模板”对象一个C#类实例其中包含节点的所有属性id, name, type, transform, parentId等并将所有模板存储在一个以id为键的Dictionarystring, NodeTemplate中。同时建立子节点ID列表的关联。第二次遍历首先创建所有根节点parentId为空或特定的根节点ID。然后通过递归或队列的方式根据Dictionary中存储的父子关系在Unity中实例化GameObject并设置其transform.parent属性。这里的关键是必须先创建父对象才能将其设置为子对象的父级。通过Dictionary可以快速通过parentId找到已创建的父GameObject。节点路径映射如果数据中除了ID还包含了节点的完整路径如Canvas/Panel/Button我们也可以利用Unity的Transform.Find方法但这种方法在节点重名时容易出错不如ID索引可靠。实操心得处理循环引用一定要在代码中加入对循环引用的检测防止死循环导致编辑器卡死。处理缺失父节点当parentId指向一个不存在的节点时要有容错机制比如将其作为根节点创建并在控制台输出警告方便排查数据问题。保持名称唯一性在Unity中同一父级下的子对象不能重名。而Cocos中可能存在重名节点。插件在创建GameObject时需要处理重名问题例如添加后缀(1),(2)或者使用唯一ID作为名称的一部分确保能正确创建。4. 插件设计与实现详解有了核心算法我们需要将其包装成一个用户友好的Unity编辑器插件。我将插件主要分为四个模块数据模型、核心转换器、编辑器界面和资源处理器。4.1 数据模型设计定义沟通的桥梁首先我们需要定义C#类来对应Cocos导出的JSON结构。这里假设我们导出的JSON结构如下示例{ version: 1.0, designResolution: {width: 1920, height: 1080}, nodes: [ { uuid: abc123, name: RootPanel, type: Widget, position: {x: 960, y: 540}, size: {width: 1920, height: 1080}, anchor: {x: 0.5, y: 0.5}, pivot: {x: 0.5, y: 0.5}, parent: null, children: [def456] }, { uuid: def456, name: StartButton, type: Button, position: {x: 100, y: 200}, size: {width: 200, height: 80}, anchor: {x: 0, y: 1}, pivot: {x: 0.5, y: 0.5}, parent: abc123, spriteFrame: ui/button_normal } ] }对应的C#数据模型可能如下[System.Serializable] public class CocosNodeData { public string uuid; public string name; public string type; // Sprite, Label, Button, etc. public Vector2 position; public Vector2 size; public Vector2 anchor; public Vector2 pivot; public string parent; // 父节点的uuid如果为null或空字符串则是根节点 public Liststring children; // 子节点uuid列表 public string spriteFrame; // 图片资源路径 public string fontName; // 字体资源 // ... 其他自定义属性 } [System.Serializable] public class CocosSceneData { public string version; public Vector2 designResolution; public ListCocosNodeData nodes; }使用JsonUtility或Newtonsoft.Json库可以轻松地将JSON文本反序列化成这些对象。4.2 核心转换器CocosToUnityConverter的实现这是插件的心脏一个静态类或单例负责协调整个转换流程。using UnityEngine; using UnityEditor; using System.Collections.Generic; using System.IO; public static class CocosToUnityConverter { public static GameObject ConvertScene(CocosSceneData sceneData, string importPath) { if (sceneData null || sceneData.nodes null || sceneData.nodes.Count 0) { Debug.LogError(场景数据无效或为空。); return null; } // 1. 创建根容器通常是一个Canvas或普通的GameObject GameObject rootGo new GameObject(ImportedUI_ Path.GetFileNameWithoutExtension(importPath)); // 如果是UI可以自动添加Canvas和CanvasScaler Canvas canvas rootGo.AddComponentCanvas(); canvas.renderMode RenderMode.ScreenSpaceOverlay; CanvasScaler scaler rootGo.AddComponentCanvasScaler(); scaler.referenceResolution new Vector2(sceneData.designResolution.x, sceneData.designResolution.y); scaler.uiScaleMode CanvasScaler.ScaleMode.ScaleWithScreenSize; // 2. 构建节点映射字典和父子关系字典 Dictionarystring, CocosNodeData nodeDataDict new Dictionarystring, CocosNodeData(); Dictionarystring, Liststring childrenDict new Dictionarystring, Liststring(); Dictionarystring, GameObject createdGameObjectDict new Dictionarystring, GameObject(); foreach (var node in sceneData.nodes) { nodeDataDict[node.uuid] node; if (!string.IsNullOrEmpty(node.parent)) { if (!childrenDict.ContainsKey(node.parent)) childrenDict[node.parent] new Liststring(); childrenDict[node.parent].Add(node.uuid); } } // 3. 递归创建节点 foreach (var node in sceneData.nodes) { if (string.IsNullOrEmpty(node.parent)) { // 创建根节点其父对象是上面创建的rootGo CreateNodeRecursive(node.uuid, nodeDataDict, childrenDict, createdGameObjectDict, rootGo.transform, sceneData.designResolution); } } // 4. 后处理资源绑定、组件配置等 PostProcess(createdGameObjectDict, nodeDataDict); return rootGo; } private static void CreateNodeRecursive(string nodeId, Dictionarystring, CocosNodeData nodeDataDict, Dictionarystring, Liststring childrenDict, Dictionarystring, GameObject createdGameObjectDict, Transform parentTransform, Vector2 designResolution) { if (!nodeDataDict.TryGetValue(nodeId, out CocosNodeData data)) return; // 创建GameObject GameObject go new GameObject(data.name); go.transform.SetParent(parentTransform, false); // 重要先设置父级再计算位置 // 添加对应的Unity组件 UnityEngine.UI.MaskableGraphic graphicComponent null; switch (data.type.ToLower()) { case sprite: case button: // Button通常也是一个Image var image go.AddComponentUnityEngine.UI.Image(); graphicComponent image; // 资源路径映射这里可以先留空或设置一个默认白色纹理 // image.sprite LoadSprite(data.spriteFrame); break; case label: case text: var text go.AddComponentUnityEngine.UI.Text(); graphicComponent text; text.text data.name; // 可以先使用节点名或从数据中读取text属性 // text.font LoadFont(data.fontName); break; // ... 处理其他类型 default: // 默认创建一个空节点 break; } // 设置RectTransform - 这是最核心的部分 RectTransform rt go.GetComponentRectTransform(); if (rt null) rt go.AddComponentRectTransform(); // 调用坐标转换函数详见3.1节这里需要实现 ApplyCocosTransformToRectTransform(data, rt, designResolution, parentTransform); // 记录已创建的对象 createdGameObjectDict[nodeId] go; // 递归创建子节点 if (childrenDict.ContainsKey(nodeId)) { foreach (var childId in childrenDict[nodeId]) { CreateNodeRecursive(childId, nodeDataDict, childrenDict, createdGameObjectDict, go.transform, designResolution); } } } private static void ApplyCocosTransformToRectTransform(CocosNodeData cocosData, RectTransform rt, Vector2 designResolution, Transform parentTransform) { // 简化版假设Cocos锚点为(0.5,0.5)Unity也预设为(0.5,0.5)的中心锚点 rt.anchorMin new Vector2(0.5f, 0.5f); rt.anchorMax new Vector2(0.5f, 0.5f); rt.pivot new Vector2(cocosData.pivot.x, cocosData.pivot.y); // 传递轴心点 // 计算anchoredPosition // 注意这里需要根据cocosData.anchor进行更复杂的计算以下为简化示例 // 假设父节点就是设计分辨率大小且原点在中心 float parentWidth designResolution.x; float parentHeight designResolution.y; if (parentTransform ! null parentTransform.GetComponentRectTransform() ! null) { var parentRT parentTransform.GetComponentRectTransform(); parentWidth parentRT.rect.width; parentHeight parentRT.rect.height; } // 将Cocos坐标原点在父节点左下角转换到Unity中心原点坐标系 // 这是一个需要根据项目具体坐标系调整的关键函数 float normalizedX cocosData.position.x / parentWidth; float normalizedY cocosData.position.y / parentHeight; // 假设Cocos原点在左下角Unity锚点中心在(0.5,0.5) // 那么Cocos的(0,0)对应Unity的(-0.5*parentWidth, -0.5*parentHeight) float unityX (normalizedX - 0.5f) * parentWidth; float unityY (normalizedY - 0.5f) * parentHeight; rt.anchoredPosition new Vector2(unityX, unityY); rt.sizeDelta new Vector2(cocosData.size.x, cocosData.size.y); } private static void PostProcess(Dictionarystring, GameObject goDict, Dictionarystring, CocosNodeData dataDict) { // 遍历所有创建的对象进行资源绑定、按钮事件占位等操作 foreach (var kvp in goDict) { var go kvp.Value; var data dataDict[kvp.Key]; // 示例尝试加载并设置图片 if (!string.IsNullOrEmpty(data.spriteFrame) go.GetComponentUnityEngine.UI.Image() ! null) { // 这里需要实现一个资源加载器根据路径在项目中查找Sprite // Sprite sprite ResourceManager.LoadSprite(data.spriteFrame); // go.GetComponentUnityEngine.UI.Image().sprite sprite; } // 可以在这里添加更多后处理逻辑如设置Text的字体、颜色等 } } }4.3 编辑器界面Editor Window集成为了让美术和策划人员也能使用我们需要一个简单的界面。using UnityEditor; using UnityEngine; public class CocosImporterWindow : EditorWindow { private TextAsset jsonFile; // 拖拽赋值 private string importRootName ImportedUI; private Vector2 scrollPos; [MenuItem(Tools/Cocos to Unity Importer)] public static void ShowWindow() { GetWindowCocosImporterWindow(Cocos Importer); } private void OnGUI() { GUILayout.Label(Cocos Creator UI Import Tool, EditorStyles.boldLabel); EditorGUILayout.Space(); jsonFile (TextAsset)EditorGUILayout.ObjectField(Cocos JSON File, jsonFile, typeof(TextAsset), false); importRootName EditorGUILayout.TextField(Root Object Name, importRootName); EditorGUILayout.Space(); GUI.enabled jsonFile ! null; if (GUILayout.Button(一键导入UI, GUILayout.Height(30))) { ImportUI(); } GUI.enabled true; EditorGUILayout.Space(); EditorGUILayout.HelpBox(使用步骤\n1. 从Cocos Creator导出UI场景为JSON。\n2. 将JSON文件拖入上方框。\n3. 点击‘一键导入UI’。\n4. 生成的UI会出现在当前场景中。, MessageType.Info); } private void ImportUI() { if (jsonFile null) { EditorUtility.DisplayDialog(错误, 请先选择Cocos导出的JSON文件。, 确定); return; } string jsonContent jsonFile.text; CocosSceneData sceneData null; try { // 使用JsonUtility或Newtonsoft.Json解析 sceneData JsonUtility.FromJsonCocosSceneData(jsonContent); } catch (System.Exception e) { Debug.LogError($解析JSON文件失败: {e.Message}); EditorUtility.DisplayDialog(解析错误, $JSON文件格式可能不正确:\n{e.Message}, 确定); return; } if (sceneData null) { Debug.LogError(解析后的场景数据为空。); return; } // 调用核心转换器 GameObject root CocosToUnityConverter.ConvertScene(sceneData, jsonFile.name); if (root ! null) { root.name importRootName; // 选中新创建的对象 Selection.activeGameObject root; Debug.Log($UI导入成功根节点: {root.name}); } else { Debug.LogError(UI导入失败。); } } }4.4 资源路径映射与处理Cocos中的资源路径如ui/button_normal无法直接用于Unity。我们需要一个映射机制。方案一约定命名规则。要求Cocos和Unity项目中的资源名称保持一致或可预测插件在导入时在指定的资源目录如Resources或通过AssetDatabase中按名称查找。方案二使用映射表。创建一个ScriptableObject资源里面存储了Cocos资源路径到Unity资源Sprite,Font的映射关系。插件导入时查询这个表进行关联。方案三手动后处理。插件生成“白模”UI所有Image组件使用默认白色纹理Text使用默认字体。然后由开发或美术人员在Unity编辑器中利用Prefab Variant或直接替换的方式批量或逐个替换资源。这种方法分离了“结构”和“皮肤”在某些工作流下更灵活。在我的项目中我采用了方案一和方案三的结合。对于已知的、通用的UI元素如通用按钮、图标通过命名规则自动关联对于特殊的主题资源则生成后手动替换并保存为Prefab作为最终的UI资产。5. 常见问题与排查技巧实录在实际开发和使用这款插件的过程中我遇到了不少坑。这里把典型问题和解决方法记录下来希望能帮你省点时间。5.1 节点位置错乱或偏移这是最常见的问题根本原因几乎都是坐标系转换计算错误。症状所有控件的位置都不对整体偏移或者旋转/缩放异常。排查步骤确认原点首先搞清楚你的Cocos项目使用的坐标系原点在哪里是左下角(0,0)还是中心(0,0)查看Cocos导出的原始位置数据找一个已知位置的控件比如屏幕正中心的按钮看它的坐标值是多少。验证锚点和轴心在Cocos编辑器中查看问题节点的锚点Anchor和轴心点Pivot设置。在Unity中检查生成的RectTransform的anchorMin/Max和pivot值是否与之一致。不一致是导致偏移的主因。简化测试创建一个最简单的测试场景Cocos里只有一个位于(0,0)锚点为(0,0)尺寸为(100,100)的方块。导出后看插件生成的位置。然后逐步修改锚点、轴心、位置观察变化与你的转换公式计算结果对比。打印调试在ApplyCocosTransformToRectTransform函数中对关键计算步骤添加Debug.Log输出中间变量如计算前后的坐标值与预期值对比。解决技巧不要试图用一个万能公式覆盖所有情况。最好根据Cocos中常见的几种锚点预设如左上、中上、居中、拉伸等分别编写对应的转换函数。在插件中增加一个“调试模式”复选框。勾选后会在每个生成的控件上添加一个Gizmo绘制出它在Cocos坐标系下的理论位置框方便在Scene视图中直观对比。5.2 Parent链断裂或顺序错误症状节点没有出现在正确的父级下或者兄弟节点之间的层级顺序Z序/Sibling Index不对。排查步骤检查数据源确认导出的JSON数据中每个节点的parent字段值是否正确指向其父节点的ID。检查是否有节点的parent指向了一个不存在的ID或者形成了循环引用A的父是BB的父又是A。检查创建顺序确保你的创建算法是“先父后子”。使用我推荐的“两次遍历字典索引”法可以有效避免这个问题。检查Unity中的Parent设置go.transform.SetParent(parentTransform, false);这里的第二个参数worldPositionStays设置为false非常重要。如果设为true子对象会尝试保持世界坐标不变这可能会干扰我们精心计算的局部坐标。解决技巧在创建节点前先对节点数据列表进行一次拓扑排序检查确保没有循环依赖。在创建完成后遍历整个生成树打印每个节点的路径和层级与Cocos编辑器的节点树对比。5.3 资源丢失或关联错误症状图片显示为粉色Missing文字显示为系统默认字体。排查步骤路径映射确认插件中资源查找的逻辑。是直接在Resources文件夹里按名字找还是有一个映射表打印出插件尝试加载的完整资源路径。资源是否存在在Unity的Project窗口搜索插件试图加载的资源名看是否存在。注意文件名大小写、后缀名Cocos可能没有后缀Unity需要.png。资源类型确认Cocos中的spriteFrame对应的是Unity中的SpriteTexture Type为Sprite 2D and UI而不是普通的Texture。解决技巧实现一个“资源预检查”功能。在导入开始前插件先扫描JSON中用到的所有资源名然后在项目资产中查找将找不到的资源列表输出到控制台让用户提前处理。提供“默认资源”配置。可以为Image和Text指定一个默认的Sprite和Font当找不到指定资源时使用避免出现粉色块。5.4 性能问题与编辑器卡顿症状导入一个包含数百个节点的复杂界面时编辑器响应变慢或卡死。排查步骤避免频繁的AssetDatabase操作如果在导入过程中频繁调用AssetDatabase.LoadAssetAtPath或Resources.Load来查找资源会非常慢。尽量在导入前批量收集资源引用。减少不必要的GameObject操作每帧创建大量GameObject并立即修改其组件会触发大量的序列化和其他编辑器开销。解决技巧使用协程分帧创建对于超大型UI可以将创建过程用编辑器协程EditorApplication.update回调拆分到多帧中执行避免主线程阻塞。进度条反馈使用EditorUtility.DisplayProgressBar显示导入进度让用户知道插件还在工作而非卡死。缓存资源引用构建一个资源名称到UnityEngine.Object的缓存字典避免重复查找。5.5 扩展性如何处理自定义组件Cocos项目中可能使用了自定义组件这些组件在Unity中没有直接对应物。解决方案在插件的节点类型映射系统中增加一个“自定义类型”处理接口。定义一个接口ICustomComponentHandler包含方法void Process(GameObject go, CocosNodeData data)。为每种需要特殊处理的Cocos类型创建一个实现了该接口的处理器类如CustomRichTextHandler。在插件中维护一个类型到处理器的字典。当遇到未知或自定义类型时尝试查找对应的处理器进行处理。如果找不到则生成一个普通的GameObject并将其原始数据以JSON字符串的形式保存在一个自定义的MonoBehaviour脚本中供后续手动处理。开发这个插件的过程是一个不断与细节较劲、不断理解两个引擎差异的过程。最大的体会是没有“绝对正确”的转换只有“最适合当前项目工作流”的转换。一开始不必追求100%的自动化和完美还原先解决80%的重复劳动结构、位置剩下的20%特殊资源、动画、逻辑交给手动调整性价比最高。当你的插件能稳定、准确地还原出UI骨架时它就已经是一个强大的生产力工具了。
返回列表