using UnityEngine; using UnityEditor; using UnityEngine.UIElements; using UnityEditor.UIElements; using System.Collections.Generic; [CustomEditor(typeof(AvatarOutfitController))] public class AvatarOutfitControllerEditor : Editor { private const string UxmlPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/AvatarOutfitControllerEditor.uxml"; private const string UssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/AvatarOutfitControllerEditor.uss"; private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; private VisualElement avatarsContainer; private Label avatarsTitleLabel; private AvatarOutfitController controller; public override VisualElement CreateInspectorGUI() { controller = (AvatarOutfitController)target; var root = new VisualElement(); var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath); var uss = AssetDatabase.LoadAssetAtPath(UssPath); if (commonUss != null) root.styleSheets.Add(commonUss); if (uss != null) root.styleSheets.Add(uss); var uxml = AssetDatabase.LoadAssetAtPath(UxmlPath); if (uxml != null) uxml.CloneTree(root); BuildAvatarsSection(root); Undo.undoRedoPerformed += OnUndoRedo; root.RegisterCallback(_ => Undo.undoRedoPerformed -= OnUndoRedo); return root; } private void OnUndoRedo() { if (controller == null) return; serializedObject.Update(); RebuildAvatarList(); } #region Avatars List private void BuildAvatarsSection(VisualElement root) { var section = root.Q("avatarsSection"); if (section == null) return; var header = new VisualElement(); header.AddToClassList("list-header"); avatarsTitleLabel = new Label($"아바타 ({controller.avatars.Count})"); avatarsTitleLabel.AddToClassList("list-title"); header.Add(avatarsTitleLabel); var addBtn = new Button(AddAvatar) { text = "+ 아바타 추가" }; addBtn.AddToClassList("list-add-btn"); header.Add(addBtn); section.Add(header); avatarsContainer = new VisualElement(); section.Add(avatarsContainer); RebuildAvatarList(); } private void RebuildAvatarList() { if (avatarsContainer == null || controller == null) return; avatarsContainer.Clear(); if (avatarsTitleLabel != null) avatarsTitleLabel.text = $"아바타 ({controller.avatars.Count})"; if (controller.avatars.Count == 0) { var empty = new Label("아바타가 없습니다. '+ 아바타 추가' 버튼을 눌러 추가하세요."); empty.AddToClassList("list-empty"); avatarsContainer.Add(empty); return; } for (int i = 0; i < controller.avatars.Count; i++) { avatarsContainer.Add(CreateAvatarElement(i)); } avatarsContainer.Bind(serializedObject); } private VisualElement CreateAvatarElement(int avatarIndex) { var avatar = controller.avatars[avatarIndex]; var item = new VisualElement(); item.AddToClassList("list-item"); item.AddToClassList("avatar-item"); // Header row var headerRow = new VisualElement(); headerRow.AddToClassList("list-item-header"); var indexLabel = new Label($"{avatarIndex + 1}"); indexLabel.AddToClassList("list-index"); headerRow.Add(indexLabel); var nameField = new TextField(); nameField.value = avatar.avatarName; nameField.AddToClassList("list-name-field"); int ai = avatarIndex; nameField.RegisterValueChangedCallback(evt => { Undo.RecordObject(target, "Rename Avatar"); controller.avatars[ai].avatarName = evt.newValue; EditorUtility.SetDirty(target); }); headerRow.Add(nameField); // Reorder buttons var upBtn = new Button(() => SwapAvatars(ai, ai - 1)) { text = "\u25B2" }; upBtn.AddToClassList("list-reorder-btn"); upBtn.SetEnabled(avatarIndex > 0); headerRow.Add(upBtn); var downBtn = new Button(() => SwapAvatars(ai, ai + 1)) { text = "\u25BC" }; downBtn.AddToClassList("list-reorder-btn"); downBtn.SetEnabled(avatarIndex < controller.avatars.Count - 1); headerRow.Add(downBtn); var deleteBtn = new Button(() => DeleteAvatar(ai)) { text = "X" }; deleteBtn.AddToClassList("list-delete-btn"); headerRow.Add(deleteBtn); item.Add(headerRow); // Avatar fields var fields = new VisualElement(); fields.AddToClassList("list-fields"); var listProp = serializedObject.FindProperty("avatars"); var avatarProp = listProp.GetArrayElementAtIndex(avatarIndex); // Outfits sub-list int outfitCount = avatar.outfits?.Length ?? 0; var outfitsFoldout = new Foldout { text = $"의상 ({outfitCount})", value = true }; outfitsFoldout.AddToClassList("outfit-foldout"); var outfitsContainer = new VisualElement(); outfitsContainer.AddToClassList("outfits-container"); if (avatar.outfits != null && avatar.outfits.Length > 0) { var outfitsProp = avatarProp.FindPropertyRelative("outfits"); for (int oi = 0; oi < avatar.outfits.Length; oi++) { outfitsContainer.Add(CreateOutfitElement(avatarIndex, oi, outfitsProp.GetArrayElementAtIndex(oi))); } } else { var emptyOutfit = new Label("의상이 없습니다."); emptyOutfit.AddToClassList("list-empty"); outfitsContainer.Add(emptyOutfit); } var addOutfitBtn = new Button(() => AddOutfit(ai)) { text = "+ 의상 추가" }; addOutfitBtn.AddToClassList("list-add-btn"); addOutfitBtn.style.marginTop = 4; outfitsContainer.Add(addOutfitBtn); outfitsFoldout.Add(outfitsContainer); fields.Add(outfitsFoldout); item.Add(fields); return item; } private VisualElement CreateOutfitElement(int avatarIndex, int outfitIndex, SerializedProperty outfitProp) { var avatar = controller.avatars[avatarIndex]; var outfit = avatar.outfits[outfitIndex]; bool isCurrentOutfit = avatar.CurrentOutfitIndex == outfitIndex; var outfitItem = new VisualElement(); outfitItem.AddToClassList("outfit-item"); if (isCurrentOutfit) outfitItem.AddToClassList("outfit-item--current"); // Outfit header var outfitHeader = new VisualElement(); outfitHeader.AddToClassList("outfit-header"); var outfitIdx = new Label($"{outfitIndex + 1}"); outfitIdx.AddToClassList("list-index"); outfitHeader.Add(outfitIdx); var outfitNameField = new TextField(); outfitNameField.value = outfit.outfitName; outfitNameField.AddToClassList("list-name-field"); int ai = avatarIndex, oi = outfitIndex; outfitNameField.RegisterValueChangedCallback(evt => { Undo.RecordObject(target, "Rename Outfit"); controller.avatars[ai].outfits[oi].outfitName = evt.newValue; EditorUtility.SetDirty(target); }); outfitHeader.Add(outfitNameField); if (isCurrentOutfit) { var currentLabel = new Label("[착용중]"); currentLabel.AddToClassList("outfit-current-label"); outfitHeader.Add(currentLabel); } var upOutfitBtn = new Button(() => SwapOutfits(ai, oi, oi - 1)) { text = "\u25B2" }; upOutfitBtn.AddToClassList("list-reorder-btn"); upOutfitBtn.SetEnabled(outfitIndex > 0); outfitHeader.Add(upOutfitBtn); var downOutfitBtn = new Button(() => SwapOutfits(ai, oi, oi + 1)) { text = "\u25BC" }; downOutfitBtn.AddToClassList("list-reorder-btn"); downOutfitBtn.SetEnabled(outfitIndex < avatar.outfits.Length - 1); outfitHeader.Add(downOutfitBtn); var deleteOutfitBtn = new Button(() => DeleteOutfit(ai, oi)) { text = "X" }; deleteOutfitBtn.AddToClassList("list-delete-btn"); outfitHeader.Add(deleteOutfitBtn); outfitItem.Add(outfitHeader); // Outfit fields var outfitFields = new VisualElement(); outfitFields.AddToClassList("outfit-fields"); var clothingField = new PropertyField(outfitProp.FindPropertyRelative("clothingObjects"), "의류 오브젝트"); outfitFields.Add(clothingField); var hideField = new PropertyField(outfitProp.FindPropertyRelative("hideObjects"), "숨길 오브젝트"); outfitFields.Add(hideField); // 변신 효과 Foldout (이 의상으로 변경해 들어올 때 재생) var fxFoldout = new Foldout { text = "변신 효과", value = false }; fxFoldout.AddToClassList("transform-fx-foldout"); var fxField = new PropertyField(outfitProp.FindPropertyRelative("transformEffect"), ""); fxFoldout.Add(fxField); outfitFields.Add(fxFoldout); if (Application.isPlaying) { var testBtn = new Button(() => controller.SetAvatarOutfit(ai, oi)) { text = isCurrentOutfit ? "현재 의상" : "▶ 이 의상으로 변경" }; testBtn.AddToClassList("list-add-btn"); testBtn.style.marginTop = 2; testBtn.SetEnabled(!isCurrentOutfit && !controller.IsAvatarTransforming(ai)); outfitFields.Add(testBtn); } outfitItem.Add(outfitFields); return outfitItem; } #endregion #region Avatar Actions private void AddAvatar() { Undo.RecordObject(target, "Add Avatar"); controller.avatars.Add(new AvatarOutfitController.AvatarData("새 아바타")); EditorUtility.SetDirty(target); serializedObject.Update(); RebuildAvatarList(); } private void DeleteAvatar(int index) { Undo.RecordObject(target, "Delete Avatar"); controller.avatars.RemoveAt(index); EditorUtility.SetDirty(target); serializedObject.Update(); RebuildAvatarList(); } private void SwapAvatars(int a, int b) { Undo.RecordObject(target, "Reorder Avatars"); (controller.avatars[a], controller.avatars[b]) = (controller.avatars[b], controller.avatars[a]); EditorUtility.SetDirty(target); serializedObject.Update(); RebuildAvatarList(); } private void AddOutfit(int avatarIndex) { Undo.RecordObject(target, "Add Outfit"); var avatar = controller.avatars[avatarIndex]; var outfits = avatar.outfits != null ? new List(avatar.outfits) : new List(); outfits.Add(new AvatarOutfitController.OutfitData { outfitName = "새 의상" }); avatar.outfits = outfits.ToArray(); EditorUtility.SetDirty(target); serializedObject.Update(); RebuildAvatarList(); } private void SwapOutfits(int avatarIndex, int a, int b) { var avatar = controller.avatars[avatarIndex]; if (avatar.outfits == null) return; if (a < 0 || a >= avatar.outfits.Length || b < 0 || b >= avatar.outfits.Length) return; if (a == b) return; // SerializedProperty 로만 처리해 직렬화 상태와 런타임 상태 간 동기 문제를 방지 serializedObject.Update(); var listProp = serializedObject.FindProperty("avatars"); var avatarProp = listProp.GetArrayElementAtIndex(avatarIndex); var outfitsProp = avatarProp.FindPropertyRelative("outfits"); // MoveArrayElement 는 인접 요소 이동이면 스왑과 동일 효과 outfitsProp.MoveArrayElement(a, b); var currentIdxProp = avatarProp.FindPropertyRelative("currentOutfitIndex"); if (currentIdxProp != null) { int cur = currentIdxProp.intValue; if (cur == a) currentIdxProp.intValue = b; else if (cur == b) currentIdxProp.intValue = a; } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(target); RebuildAvatarList(); } private void DeleteOutfit(int avatarIndex, int outfitIndex) { Undo.RecordObject(target, "Delete Outfit"); var outfits = new List(controller.avatars[avatarIndex].outfits); outfits.RemoveAt(outfitIndex); controller.avatars[avatarIndex].outfits = outfits.ToArray(); EditorUtility.SetDirty(target); serializedObject.Update(); RebuildAvatarList(); } #endregion }