using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json; using UnityEditor; using UnityEditor.AnimatedValues; using UnityEngine; using UnityEngine.UI; using VketCloudGUITools.Runtime; using VketCloudGUITools.Serialization; using VketCloudGUITools.Utilities; namespace VketCloudGUITools.Editor { public class VCEditorCanvasExporter : EditorWindow { /// /// CanvasのExport前のCheck時のエラーor警告メッセージ、およびメッセージ発生対象 /// public class CheckMessage { /// /// メッセージ /// public string message; /// /// メッセージ発生対象 /// public UnityEngine.Object obj; public Action onDrawAutoFixButton = null; } public class AutoApplyAutoFixSettings { public bool activate = false; public bool safe = false; public bool major = false; public bool removeComponent = false; public bool hierarchical = false; public bool animation = false; public bool generateAutoLayer = false; } private const string prefix = "VketCloudGUIToolsExporter_"; private const string helliodorDataPathKey = prefix + "HeliodorDataPath"; private const string unityDataPathKey = prefix + "UnityDataPath"; string heliodorDataPath = ""; string unityDataPath = ""; bool zFlip = true; Canvas targetCanvas = null; List criticalMessages = new List(); List errorMessages = new List(); List warningMessages = new List(); VCLayerList autoGeneratedLayerList = null; bool delayClearMessages = false; bool autoFixResolved = false; string delayMessage = string.Empty; double updateTime = 0; private AutoFixErrorHelper _autoFixErrorHelper = null; private AnimBool _autoApplyAutoFixExpand = null; private AutoApplyAutoFixSettings _autoApplyAutoFix = new AutoApplyAutoFixSettings(); private bool advancedOptions = false; public AutoFixErrorHelper AutoFixError { get => _autoFixErrorHelper; set => _autoFixErrorHelper = value; } Vector2 scrollPosition; readonly Type[] allowedGUIComponents = new Type[] { typeof(RectTransform), typeof(CanvasRenderer), typeof(Button), typeof(Image), typeof(Text), typeof(Slider), typeof(VCTransform), typeof(VCGUIContent), typeof(VCButton), typeof(VCImage), typeof(VCText), typeof(VCSlider), typeof(VCComment), }; readonly Type[] allowedCanvasComponents = new Type[] { typeof(RectTransform), typeof(Canvas), typeof(VCCanvas), typeof(CanvasScaler) }; readonly Type[] allowedLayerComponents = new Type[] { typeof(RectTransform), typeof(VCLayerList) }; private void OnEnable() { heliodorDataPath = EditorPrefs.GetString(helliodorDataPathKey); unityDataPath = EditorPrefs.GetString(unityDataPathKey, "Assets"); EditorApplication.hierarchyChanged += OnExternalChange; Undo.undoRedoPerformed += OnExternalChange; _autoFixErrorHelper = new AutoFixErrorHelper(this); _autoApplyAutoFixExpand = new AnimBool(); _autoApplyAutoFixExpand.valueChanged.AddListener(Repaint); } private void OnDisable() { Undo.undoRedoPerformed -= OnExternalChange; EditorApplication.hierarchyChanged -= OnExternalChange; EditorPrefs.SetString(helliodorDataPathKey, heliodorDataPath); EditorPrefs.SetString(unityDataPathKey, unityDataPath); _autoApplyAutoFixExpand.valueChanged.RemoveListener(Repaint); } private void OnInspectorUpdate() { Repaint(); } private void OnExternalChange() { Repaint(); } [MenuItem("VketCloudGUITools/GUI Exporter")] private static void Create() { GetWindow("VketCloud GUI Exporter"); } private void OnGUI() { using (new GUILayout.HorizontalScope()) { heliodorDataPath = EditorGUILayout.TextField("Export Canvas Path", heliodorDataPath); if (GUILayout.Button("...", GUILayout.Width(EditorGUIUtility.singleLineHeight))) { var path = EditorUtility.SaveFolderPanel("Select Export Folder", heliodorDataPath, ""); if (!string.IsNullOrEmpty(path)) heliodorDataPath = path; } } targetCanvas = EditorGUILayout.ObjectField("Target Canvas", targetCanvas, typeof(Canvas), true) as Canvas; advancedOptions = EditorGUILayout.BeginFoldoutHeaderGroup(advancedOptions, "Advanced Options"); if (advancedOptions) { EditorGUI.indentLevel++; using (new GUILayout.HorizontalScope()) { unityDataPath = EditorGUILayout.TextField("Assets Output Path", unityDataPath); if (GUILayout.Button("...", GUILayout.Width(EditorGUIUtility.singleLineHeight))) { var path = EditorUtility.SaveFolderPanel("Select Assets Output Folder", unityDataPath, ""); if (IsDataPath(path)) { path = "Assets" + StlipDataPath(path); } if (!string.IsNullOrEmpty(path)) unityDataPath = path; } } autoGeneratedLayerList = EditorGUILayout.ObjectField("Auto Generated Layer", autoGeneratedLayerList, typeof(VCLayerList), true) as VCLayerList; // Error check (Auto cycle) if (EditorApplication.timeSinceStartup - updateTime >= 1.0 && Event.current.type == EventType.Layout) { ClearMessages(); if (targetCanvas != null) { DoErrorCheck(); updateTime = EditorApplication.timeSinceStartup; } } EditorGUI.indentLevel--; } // Draw Warnings & Auto Fix DrawErrorWarningList(); // Export Button EditorGUI.BeginDisabledGroup(criticalMessages.Count > 0); if (targetCanvas != null && GUILayout.Button("Export .json")) { DoExport(targetCanvas); } EditorGUI.EndDisabledGroup(); } private void DoErrorCheck() { var vcCanvas = targetCanvas.GetComponent(); bool ok = false; if (vcCanvas == null) { ok = EditorUtility.DisplayDialog("注意", "対象はVCGUICanvasではありません。\n変換処理の可能性があるため、対象はコピーされます。", "OK", "キャンセル"); if (ok) { ConvertCancasToVCGUICanvas(); } else { targetCanvas = null; } } else if (vcCanvas != null && targetCanvas != null) { // Canvas自体のエラーチェック CanvasErrorCheck(targetCanvas); // Canvasの子要素のエラーチェック CanvasChildErrorCheck(); // 名前の重複を確認 CheckNameUniquness(); } } private void ConvertCancasToVCGUICanvas() { var sourceTargetCanvas = targetCanvas; targetCanvas = Instantiate(targetCanvas); // Instantiateすると名前に(Clone)が勝手につくので戻す targetCanvas.name = sourceTargetCanvas.name; targetCanvas.transform.SetParent(sourceTargetCanvas.transform.parent); targetCanvas.transform.localPosition = sourceTargetCanvas.transform.localPosition; targetCanvas.transform.localRotation = sourceTargetCanvas.transform.localRotation; targetCanvas.transform.localScale = sourceTargetCanvas.transform.localScale; var vcCanvas2 = targetCanvas.gameObject.AddComponent(); vcCanvas2.CanvasType = CanvasType.LandScape; sourceTargetCanvas.gameObject.SetActive(false); Selection.activeObject = targetCanvas; } private void CanvasChildErrorCheck() { for (int i = 0; i < targetCanvas.transform.childCount; ++i) { Transform child = targetCanvas.transform.GetChild(i); var canvasRenderer = child.GetComponent(); var slider = child.GetComponent(); var childRectTransform = child.GetComponent(); if (childRectTransform != null && (slider != null || canvasRenderer != null)) { AddCriticalMessages("Canvasの直下にGUIコンテンツが存在しています。\nGUIコンテンツはレイヤーが必要です。\n手動でレイヤーに移動するか、[Auto Fix]からレイヤーを自動生成して移動してください。\n[Auto Fix]では、GUIコンテンツの座標などがズレてしまう場合があるため、手動調整を忘れないでください。", child, AutoFixError.Canvas.MoveChildToAutoGeneratedLayer(child, targetCanvas, autoGeneratedLayerList, l => autoGeneratedLayerList = l)); if (childRectTransform != null) ContentErrorCheck(childRectTransform); } else { var childRect = child.GetComponent(); if (childRect == null) { AddWarningMessage($"{child.name}[Canvas]直下に、RectTransformではないものがあります。UIではないため、出力時には無視されます。", child, AutoFixError.Misc.RemoveOrConvertToRectTransform(child)); } else if (childRect != null) { LayerErrorCheck(childRect); } } } } private void CheckNameUniquness() { Dictionary layerListNameCount = new Dictionary(); foreach (Transform decendant in targetCanvas.transform.GetAllDecendants()) { var path = AnimationUtility.CalculateTransformPath(decendant, targetCanvas.transform); if (!layerListNameCount.ContainsKey(path)) { layerListNameCount[path] = 0; } layerListNameCount[path]++; } foreach (var kv in layerListNameCount) { var name = kv.Key; var count = kv.Value; if (count > 1) { AddWarningMessage($"{name}[Content]名前が重複しています。\nPath : {name}\nは同じ階層に{count}個あります。\n予想できない動作を引き起こす可能性があるため、避けてください。", null); } } } /// /// エラー&警告メッセージの一覧を表示 /// private void DrawErrorWarningList() { using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition)) { // Undo注意喚起 if (criticalMessages.Count > 0 || warningMessages.Count > 0 || errorMessages.Count > 0) { EditorGUILayout.HelpBox("間違えて[Auto Fix]を押した場合、あわてずにUndoをしてください。Undoに対応済みです。", MessageType.Info); } else if (targetCanvas != null) { EditorGUILayout.HelpBox("エラーメッセージはありません。", MessageType.None); } _autoApplyAutoFixExpand.target = EditorGUILayout.Foldout(_autoApplyAutoFixExpand.target, "Auto Fix Settings"); if (EditorGUILayout.BeginFadeGroup(_autoApplyAutoFixExpand.faded)) { GUILayout.BeginVertical(GUI.skin.box); _autoApplyAutoFix.activate = EditorGUILayout.Toggle("Acticate", _autoApplyAutoFix.activate); var defaultLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 180f; EditorGUILayout.LabelField("Auto Apply Auto Fix", "Rules"); _autoApplyAutoFix.safe = EditorGUILayout.Toggle("Allow: Safe", _autoApplyAutoFix.safe); _autoApplyAutoFix.major = EditorGUILayout.Toggle("Allow: Major Change", _autoApplyAutoFix.major); _autoApplyAutoFix.hierarchical = EditorGUILayout.Toggle("Allow: Hierarchical", _autoApplyAutoFix.hierarchical); _autoApplyAutoFix.removeComponent = EditorGUILayout.Toggle("Allow: Remove Component", _autoApplyAutoFix.removeComponent); _autoApplyAutoFix.animation = EditorGUILayout.Toggle("Allow: Animation", _autoApplyAutoFix.animation); _autoApplyAutoFix.generateAutoLayer = EditorGUILayout.Toggle("Allow: Generate Auto Layer", _autoApplyAutoFix.generateAutoLayer); EditorGUIUtility.labelWidth = defaultLabelWidth; GUILayout.EndVertical(); } EditorGUILayout.EndFadeGroup(); // 致命的なエラーメッセージ using (var criticalScope = new EditorGUILayout.VerticalScope()) { if (Event.current.type == EventType.Repaint) { EditorGUI.DrawRect(criticalScope.rect, new Color(1f, 0f, 0f, 0.25f)); } if (criticalMessages.Count > 0) { EditorGUILayout.HelpBox("この色のメッセージは、修正が必要ですが、その順番によって結果が変わります。\n解決するまでExportが実行できません。", MessageType.Error); } foreach (var message in criticalMessages) { using (new EditorGUILayout.VerticalScope(GUI.skin.box)) { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox(message.message, MessageType.Error); if (message.onDrawAutoFixButton != null) { message.onDrawAutoFixButton(); } } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Select", GUILayout.ExpandWidth(false))) { Selection.activeObject = message.obj; } EditorGUILayout.ObjectField(message.obj, typeof(UnityEngine.Object), allowSceneObjects: true); } } PostDrawMessage(message); EditorGUILayout.Space(); } } // 警告メッセージ if (warningMessages.Count > 0) { EditorGUILayout.HelpBox("この色のメッセージは、無視しても自動修正されてHeliodorで再現可能に出力されます。\n[Auto Fix]を押すか、再インポート時に修正済みの値になります。", MessageType.Warning); } foreach (var message in warningMessages) { using (new EditorGUILayout.VerticalScope(GUI.skin.box)) { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox(message.message, MessageType.Warning); if (message.onDrawAutoFixButton != null) { message.onDrawAutoFixButton(); } } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Select", GUILayout.ExpandWidth(false))) { Selection.activeObject = message.obj; } EditorGUILayout.ObjectField(message.obj, typeof(UnityEngine.Object), allowSceneObjects: true); } } PostDrawMessage(message); EditorGUILayout.Space(); } // エラーメッセージ if (errorMessages.Count > 0) { EditorGUILayout.HelpBox("この色のメッセージは、Heliodorで正確に再現できない設定を警告しています。\n[Auto Fix]を押すとHeliodorで可能な動きの設定になります。\n自動修正でレイアウトが変わる可能性があるため、必ず再確認してください。", MessageType.Error); } foreach (var message in errorMessages) { using (new EditorGUILayout.VerticalScope(GUI.skin.box)) { using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.HelpBox(message.message, MessageType.Error); if (message.onDrawAutoFixButton != null) { message.onDrawAutoFixButton(); } } using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Select", GUILayout.ExpandWidth(false))) { Selection.activeObject = message.obj; } EditorGUILayout.ObjectField(message.obj, typeof(UnityEngine.Object), allowSceneObjects: true); } } EditorGUILayout.Space(); PostDrawMessage(message); } scrollPosition = scrollView.scrollPosition; } if (Event.current.type == EventType.Repaint) { if (delayClearMessages) { delayClearMessages = false; ClearMessages(); AddWarningMessage(delayMessage, null); } } } public void DoExport(Canvas targetCanvas) { // 出力対象が選んでない if (targetCanvas == null) { EditorUtility.DisplayDialog("VketCloud GUI Exporter", "Canvasを指定してください", "OK"); return; } // 出力対象にエラーが残っている if (criticalMessages.Count > 0) { EditorUtility.DisplayDialog("VketCloud GUI Exporter", "「致命的なエラー」が残っています。赤背景の警告に、すべて対応してください。", "OK"); return; } // Canvasを解析してJson元データのCanvasDefを組み立てる Canvas canvasGameObject; VCCanvasDef canvasDef; VCCanvas vCanvas; ExportCanvas(targetCanvas, out canvasGameObject, out canvasDef, out vCanvas); // 保存先および関連パラメタを確定 // CanvasTypeはCanvasJsonファイル内ではなく、ファイル名/フォルダ名で確定される DestroyImmediate(canvasGameObject.gameObject); var path = EditorUtility.SaveFilePanel("Save Canvas JSON", heliodorDataPath, targetCanvas.name + ".json", "json"); // 出力先が選ばれなかったので中断 if (string.IsNullOrEmpty(path)) return; // CanvasTypeを出力先ディレクトリから確定する var canvasType = Runtime.CanvasType.None; var directoryNames = path.Replace('\\', '/').Split('/').Reverse(); for (int i = 0; i < directoryNames.Count(); ++i) { var name = directoryNames.ElementAt(i); if (Regex.IsMatch(name, "landscape", RegexOptions.IgnoreCase)) { canvasType = Runtime.CanvasType.LandScape; break; } else if (Regex.IsMatch(name, "portrait", RegexOptions.IgnoreCase)) { canvasType = Runtime.CanvasType.Portrait; break; } } // Portrait設定と出力ディレクトリの不一致がないか確認する var landscapeCheck = vCanvas.CanvasType == CanvasType.LandScape && canvasType != CanvasType.LandScape; var portraitCheck = vCanvas.CanvasType == CanvasType.Portrait && canvasType != CanvasType.Portrait; var folderCheck = landscapeCheck || portraitCheck; // 出力ディレクトリがPortrait設定と不一致のとき、安全のためユーザーに確認する if (folderCheck && !EditorUtility.DisplayDialog("確認", $"{vCanvas.CanvasType}用Canvasを{canvasType}用のディレクトリに出力します。", "OK", "キャンセル")) return; // Json出力 WriteJsonToFile(canvasDef, path); EditorUtility.DisplayDialog("VketCloud GUI Exporter", "JSONの出力が完了しました。", "OK"); } private static void WriteJsonToFile(VCCanvasDef canvasDef, string path) { // Json出力 JsonSerializer serializer = new JsonSerializer() { Formatting = Formatting.Indented, }; serializer.Converters.Add(new SingleLineJsonConverter(typeof(int[]), typeof(float[]))); serializer.FloatFormatHandling = FloatFormatHandling.String; using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write)) using (var streamWriter = new StreamWriter(fileStream)) using (var writer = new JsonTextWriter(streamWriter)) { serializer.Serialize(writer, canvasDef); } // エンコーディング統一 if (File.Exists(path)) { var str = File.ReadAllText(path); str = str.Replace("\\/", "/"); using (var sw = new StreamWriter(path, false, new UTF8Encoding(false))) sw.Write(str); } } private void ExportCanvas(Canvas targetCanvas, out Canvas canvasGameObject, out VCCanvasDef canvasDef, out VCCanvas vCanvas) { canvasGameObject = Instantiate(targetCanvas); canvasGameObject.name = "OutputCanvas"; //JSON出力 var zMpy = zFlip ? -1 : 1; canvasDef = new VCCanvasDef(); canvasDef.Z = (int)(canvasGameObject.transform.localPosition.z * zMpy); vCanvas = canvasGameObject.GetComponent(); if (vCanvas != null) { canvasDef.Version = vCanvas.Version; canvasDef.Z = vCanvas.Z; } canvasDef.Layout = new List(); foreach (Transform layer in canvasGameObject.transform) { ExportContents(zMpy, canvasDef, layer); } var scripts = new List(); foreach (var textAsset in vCanvas.Scripts) { var assetPath = AssetDatabase.GetAssetPath(textAsset); // Assets/*/Canvas/~ Canvasまでをスキップ assetPath = assetPath.Substring(assetPath.IndexOf("/Canvas/",StringComparison.CurrentCulture) + 1); scripts.Add(assetPath); CopyAssets(textAsset); } canvasDef.Scripts = scripts.Distinct().ToList(); } private void ExportContents(int zMpy, VCCanvasDef canvasDef, Transform layer) { var rectTransform = layer.GetComponent(); var vLayoutList = layer.GetComponent(); if (rectTransform != null) { var layerDef = new VCLayerDef(); layerDef.Name = layer.name; layerDef.Show = layer.gameObject.activeSelf; layerDef.Z = (int)(rectTransform.localPosition.z * zMpy); layerDef.SpreadMode = vLayoutList.SpreadMode; layerDef.AutoLoading = vLayoutList.AutoLoading; layerDef.Components = vLayoutList.Components; layerDef.Mask = ExportLayerMask(vLayoutList); layerDef.Gui = new List(); foreach (Transform child in layer.transform) { var childRect = child.gameObject.GetComponent(); if (childRect == null) continue; // 関係しうる全てのコンポーネントをチェック var vcTransform = child.gameObject.GetComponent(); var button = child.GetComponent