介绍

随着项目的进行,许多最开始制定的 UI 结构没有严格执行,导致一部分 UI 变得不规范,如何批量查找并处理这些不规范的 UI,最简单的方式就是使用脚本批量处理。

环境

  • Unity 2017.4.0f2

需求

其实最重要的工作是先理清需求,即 UI 的规范。

  • 支持 Windows 与 macOS
  • 使用相机渲染 UI,以便与特效共同显示
  • 指定相机参数,如正交、大小、位置、近裁面、远裁面、深度等等
  • UI 与相机的结构
  • UI 所在的层级
  • UI 特效所在的层级

有了需求,那么编码处理就很容易了。

原理

所有的 UI 都是 Prefab,只要对 Prefab 上的属性进行检查,所有不合规范的属性修改正确即可。

将 Prefab 实例化到场景内,对 Prefab 进行修改,再将 Prefab 保存即可。

注意事项

Windows 与 macOS 支持

只有一处与路径相关的地方需要进行替换处理。在 Windows 下,从文件系统获得的路径需要转换为 Unix 路径,即将 \ 替换为 / 即可,Unity 内部 API 使用 / 风格路径。

Prefab 惰式保存

由于 Prefab 实例化后即使不做任何修改保存,也有可能会产生变化,例如 RectTransform 受当前游戏视图大小影响之类的。

所以尽可能减少改动的地方,同时增加主动改动的标记,只有发生主动改动时才会去保存 Prefab。

Transform UI 根结点

如果希望 UI 的根结点统一为 Transform,而不是 RectTransform,那么需要新建空结点,将原有 UI 子结点移动到新建空结点下。

由于原有根结点上的组件上依然有很多其他子结点的引用,不可以直接新建组件的方式处理,引用会复制不全,因此这里使用反射调用 Unity 内置功能:Copy & Paste Component。这是一个取巧的办法,非常有效。

Canvas 排序与移位

由于 UI 中可能存在 UI 与特效混合显示的问题,会新建若干个 Canvas 与特效进行层级调整。为了在编辑器下方便调整,需要将 Canvas 拉开间隔,调整其 Z 值使其互相远离。

Canvas 属性

  • 统一调整个 ScreenSpaceCamera。
  • 关闭 PixelPerfect
  • 调整 UI 与相机的 Transform 先后关系
  • PanelDistance
  • SortingLayer
  • SortingOrder
  • Additional Shader Channels

相机处理

基本上就是上面列出的需要调整的属性:正交、大小、位置、近裁面、远裁面、深度等等。

需要尽可能的统一,去除如反走样、HDR、Occlusion Culling 等功能。

Transform 子结点属性

  • 位置坐标向下取整,Z 强制修改为 0
  • 旋转坐标取整
  • 局部缩放取整并强制设为 1 或 -1
  • 大小取整
  • 层级必须为 UI 或 UIEffects

代码

将以下代码保存为 cs 文件放在 Editor 目录下即可使用,建议根据实际的需要进行裁剪优化。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;

public static class UIStructureEditor
{
    public static string UIDirRoot = "Assets/UI";

    public static Vector3 CameraLocalPosition = new Vector3(960f, 540f, 0f);
    public static float CameraSize = 540f;
    public static float CameraNearClipPlane = 0f;
    public static float CameraFarClipPlane = 100f;
    public static Rect CameraRect = new Rect(0f, 0f, 1f, 1f);
    public static float CameraDepth = 5f;

    public static string CanvasSortingLayer = "Default";
    public static int UILayer = LayerMask.NameToLayer("UI");
    public static int UIEffectLayer = LayerMask.NameToLayer("UIEffect");

    [MenuItem("Tools/UI/Structure/Process All")]
    public static void ProcessAll()
    {
        var sw = new System.Diagnostics.Stopwatch();
        sw.Start();

        var files = Directory.GetFiles(UIDirRoot, "*.prefab", SearchOption.AllDirectories);
        foreach (var file in files)
        {
            string path = file.Replace("\\", "/").Replace(Application.dataPath, "Assets");
            ProcessUI(AssetDatabase.LoadAssetAtPath<GameObject>(path));
        }

        sw.Stop();
        Debug.Log(string.Format("Total used time {0}ms", sw.ElapsedMilliseconds));
    }

    [MenuItem("Tools/UI/Structure/Process Selected")]
    public static void ProcessSelected()
    {
        ProcessUI(Selection.activeGameObject);
    }

    public static void ProcessUI(GameObject prefab)
    {
        var go = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
        go = ProcessPrefab(go);
        bool hasChanged = false;

        hasChanged |= ProcessRoot(go);
        hasChanged |= ProcessCanvases(go);
        hasChanged |= ProcessLayers(go);

        if (hasChanged)
        {
            PrefabUtility.ReplacePrefab(go, prefab, ReplacePrefabOptions.Default);
        }

        Object.DestroyImmediate(go);
    }

    public static GameObject ProcessPrefab(GameObject go)
    {
        // Skip [RequireComponent(typeof (RectTransform))] component
        if (go.transform is RectTransform && go.GetComponent<LayoutElement>() == null && go.GetComponent<Graphic>() == null)
        {
            var newGo = new GameObject(go.name);
            newGo.SetActive(false);

            while (go.transform.childCount > 0)
            {
                var child = go.transform.GetChild(0);
                child.SetParent(newGo.transform, false);
            }

            var comps = go.GetComponents<Component>();
            foreach (var comp in comps)
            {
                if (comp == null || comp is Transform || comp is CanvasRenderer)
                {
                    continue;
                }

                bool success = true;
                success &= UnityEditorInternal.ComponentUtility.CopyComponent(comp);
                success &= UnityEditorInternal.ComponentUtility.PasteComponentAsNew(newGo);
                if (!success)
                {
                    Debug.LogError(string.Format("Failed to copy component {0} in {1}", comp.GetType().FullName,
                        go.name));
                }
            }

            Object.DestroyImmediate(go);
            return newGo;
        }

        return go;
    }

    private static bool ProcessRoot(GameObject go)
    {
        return EnsureTransform(go.transform, Vector3.zero, Quaternion.identity, Vector3.one);
    }

    private static bool ProcessCanvases(GameObject go)
    {
        bool hasChanged = false;
        var canvases = GetSubCanvases(go);
        for (int i = 0; i < canvases.Count; i++)
        {
            float planeDistance =
                Mathf.Ceil((canvases.Count - i) * ((CameraFarClipPlane - CameraNearClipPlane) / (canvases.Count + 1)));
            hasChanged |= ProcessCanvas(canvases[i], planeDistance, i);
        }

        return hasChanged;
    }

    private static bool ProcessCamera(GameObject go)
    {
        bool hasChanged = false;
        hasChanged |= EnsureTransform(go.transform, CameraLocalPosition, Quaternion.identity, Vector3.one);

        var camera = go.GetComponent<Camera>();
        if (camera.clearFlags != CameraClearFlags.Depth)
        {
            camera.clearFlags = CameraClearFlags.Depth;
            hasChanged = true;
        }

        int layer = 1 << LayerMask.NameToLayer("UI") | 1 << LayerMask.NameToLayer("UIEffect");
        if (camera.cullingMask != layer)
        {
            camera.cullingMask = layer;
            hasChanged = true;
        }

        if (!camera.orthographic)
        {
            camera.orthographic = true;
            hasChanged = true;
        }

        if (camera.orthographicSize != CameraSize)
        {
            camera.orthographicSize = CameraSize;
            hasChanged = true;
        }

        if (camera.nearClipPlane != CameraNearClipPlane)
        {
            camera.nearClipPlane = CameraNearClipPlane;
            hasChanged = true;
        }

        if (camera.farClipPlane != CameraFarClipPlane)
        {
            camera.farClipPlane = CameraFarClipPlane;
            hasChanged = true;
        }

        if (camera.rect != CameraRect)
        {
            camera.rect = CameraRect;
            hasChanged = true;
        }

        if (camera.depth != CameraDepth)
        {
            camera.depth = CameraDepth;
            hasChanged = true;
        }

        if (camera.renderingPath != RenderingPath.UsePlayerSettings)
        {
            camera.renderingPath = RenderingPath.UsePlayerSettings;
            hasChanged = true;
        }

        if (camera.targetTexture != null)
        {
            camera.targetTexture = null;
            hasChanged = true;
        }

        if (camera.useOcclusionCulling)
        {
            camera.useOcclusionCulling = false;
            hasChanged = true;
        }

        if (camera.allowHDR)
        {
            camera.allowHDR = false;
            hasChanged = true;
        }

        if (camera.allowMSAA)
        {
            camera.allowMSAA = false;
            hasChanged = true;
        }

        if (camera.allowDynamicResolution)
        {
            camera.allowDynamicResolution = false;
            hasChanged = true;
        }

        var comps = go.GetComponents<Component>();
        foreach (var comp in comps)
        {
            if (comp is Transform || comp is Camera)
            {
                continue;
            }

            Object.DestroyImmediate(comp);
            hasChanged = true;
        }

        return hasChanged;
    }

    private static bool ProcessCanvas(GameObject go, float planeDistance, int index)
    {
        bool hasChanged = false;

        hasChanged |= ProcessCanvasChildren(go);

        var canvas = go.GetComponent<Canvas>();
        if (canvas.renderMode != RenderMode.ScreenSpaceCamera)
        {
            canvas.renderMode = RenderMode.ScreenSpaceCamera;
            hasChanged = true;
        }

        if (canvas.pixelPerfect)
        {
            canvas.pixelPerfect = false;
            hasChanged = true;
        }

        if (canvas.worldCamera == null)
        {
            Camera camera;

            var t = go.transform.parent.Find("Camera");
            if (t != null)
            {
                camera = t.GetComponent<Camera>();
            }
            else
            {
                var cameraGo = new GameObject("Camera");
                cameraGo.transform.SetParent(go.transform.parent);
                cameraGo.transform.SetSiblingIndex(Mathf.Max(canvas.transform.GetSiblingIndex() - 1, 0));
                camera = cameraGo.AddComponent<Camera>();
            }

            canvas.worldCamera = camera;
            hasChanged = true;
        }

        hasChanged |= ProcessCamera(canvas.worldCamera.gameObject);

        if (canvas.planeDistance != planeDistance)
        {
            canvas.planeDistance = planeDistance;
            hasChanged = true;
        }

        if (canvas.sortingLayerName != CanvasSortingLayer)
        {
            canvas.sortingLayerName = CanvasSortingLayer;
            hasChanged = true;
        }

        if (canvas.sortingOrder != index)
        {
            canvas.sortingOrder = index;
            hasChanged = true;
        }

        if (canvas.additionalShaderChannels != AdditionalCanvasShaderChannels.None)
        {
            canvas.additionalShaderChannels = AdditionalCanvasShaderChannels.None;
            hasChanged = true;
        }

        return hasChanged;
    }

    private static bool ProcessCanvasChildren(GameObject go)
    {
        bool hasChanged = false;

        var stack = new Stack<Transform>();
        stack.Push(go.transform);

        while (stack.Count > 0)
        {
            var t = stack.Pop();
            for (int i = t.childCount - 1; i >= 0; i--)
            {
                stack.Push(t.GetChild(i));
            }

            if (t.GetComponent<Canvas>() != null)
            {
                continue;
            }

            hasChanged |= ProcessTransform(t);
        }

        return hasChanged;
    }

    private static bool ProcessTransform(Transform t)
    {
        bool hasChanged = false;

        var processedLocalEulerAngles = Round(t.localEulerAngles);
        if (t.localEulerAngles != processedLocalEulerAngles)
        {
            t.localEulerAngles = processedLocalEulerAngles;
            hasChanged = true;
        }

        var processedLocalScale = Round(t.localScale);
        if (t.localScale != processedLocalScale)
        {
            t.localScale = processedLocalScale;
            hasChanged = true;
        }

        var rectTransform = t as RectTransform;
        if (rectTransform != null)
        {
            var anchoredPosition3D = rectTransform.anchoredPosition3D;
            var processedAnchoredPosition3D = new Vector3
            {
                x = Mathf.Round(anchoredPosition3D.x),
                y = Mathf.Round(anchoredPosition3D.y),
                z = 0f
            };
            if (anchoredPosition3D != processedAnchoredPosition3D)
            {
                rectTransform.anchoredPosition3D = processedAnchoredPosition3D;
                hasChanged = true;
            }

            var sizeDelta = rectTransform.sizeDelta;
            var processedSizeDelta = new Vector2
            {
                x = Mathf.Round(sizeDelta.x),
                y = Mathf.Round(sizeDelta.y)
            };
            if (sizeDelta != processedSizeDelta)
            {
                rectTransform.sizeDelta = processedSizeDelta;
                hasChanged = true;
            }

            Vector3 localScale = rectTransform.localScale;
            if ((localScale.x != -1f && localScale.x != 1f) ||
                (localScale.y != -1f && localScale.y != 1f) ||
                localScale.z != 1f)
            {
                rectTransform.localScale = Vector3.one;
                hasChanged = true;
            }
        }
        else
        {
            var processedLocalPosition = Round(t.localPosition);
            if (t.localPosition != processedLocalPosition)
            {
                t.localPosition = processedLocalPosition;
                hasChanged = true;
            }
        }

        return hasChanged;
    }

    private static bool ProcessLayers(GameObject go)
    {
        bool hasChanged = false;

        var stack = new Stack<Transform>();
        stack.Push(go.transform);

        while (stack.Count > 0)
        {
            var t = stack.Pop();
            for (int i = t.childCount - 1; i >= 0; i--)
            {
                stack.Push(t.GetChild(i));
            }

            if (t.gameObject.layer != UILayer && t.gameObject.layer != UIEffectLayer)
            {
                t.gameObject.layer = UILayer;
                hasChanged = true;
            }
        }

        return hasChanged;
    }

    private static List<GameObject> GetSubCanvases(GameObject go)
    {
        var result = new List<GameObject>();

        for (int i = 0; i < go.transform.childCount; i++)
        {
            var child = go.transform.GetChild(i);
            if (child.GetComponent<Canvas>() != null)
            {
                result.Add(child.gameObject);
            }
        }

        return result;
    }

    private static bool EnsureTransform(Transform t, Vector3 localPosition, Quaternion localRotation,
        Vector3 localScale)
    {
        bool hasChanged = false;

        if (t.localPosition != localPosition)
        {
            t.localPosition = localPosition;
            hasChanged = true;
        }

        if (t.localRotation != localRotation)
        {
            t.localRotation = localRotation;
            hasChanged = true;
        }

        if (t.localScale != localScale)
        {
            t.localScale = localScale;
            hasChanged = true;
        }

        return hasChanged;
    }

    private static Vector3 Round(Vector3 origin)
    {
        origin.x = Mathf.Round(origin.x);
        origin.y = Mathf.Round(origin.y);
        origin.z = Mathf.Round(origin.z);
        return origin;
    }
}