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); root.schedule.Execute(UpdatePlayModeState).Every(200); 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)); } } private VisualElement CreateAvatarElement(int avatarIndex) { var avatar = controller.avatars[avatarIndex]; bool isCurrent = Application.isPlaying && controller.CurrentAvatar == avatar; var item = new VisualElement(); item.AddToClassList("list-item"); item.AddToClassList("avatar-item"); if (isCurrent) item.AddToClassList("list-item--active"); // 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); if (isCurrent) { var activeLabel = new Label("[Current]"); activeLabel.AddToClassList("list-active-label"); headerRow.Add(activeLabel); } // 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); var avatarObjField = new PropertyField(avatarProp.FindPropertyRelative("avatarObject"), "아바타 오브젝트"); fields.Add(avatarObjField); // 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 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); 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) { var avatar = controller.avatars[index]; if (EditorUtility.DisplayDialog("아바타 삭제", $"아바타 '{avatar.avatarName}'을(를) 삭제하시겠습니까?", "삭제", "취소")) { 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 DeleteOutfit(int avatarIndex, int outfitIndex) { var outfit = controller.avatars[avatarIndex].outfits[outfitIndex]; if (EditorUtility.DisplayDialog("의상 삭제", $"의상 '{outfit.outfitName}'을(를) 삭제하시겠습니까?", "삭제", "취소")) { 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 #region Play Mode State private void UpdatePlayModeState() { if (avatarsContainer == null || controller == null) return; if (!Application.isPlaying) return; for (int i = 0; i < avatarsContainer.childCount && i < controller.avatars.Count; i++) { var item = avatarsContainer[i]; bool isCurrent = controller.CurrentAvatar == controller.avatars[i]; if (isCurrent && !item.ClassListContains("list-item--active")) item.AddToClassList("list-item--active"); else if (!isCurrent && item.ClassListContains("list-item--active")) item.RemoveFromClassList("list-item--active"); } } #endregion }