using UnityEngine; using UnityEditor; using UnityEngine.UIElements; using UnityEditor.UIElements; using Unity.Cinemachine; [CustomEditor(typeof(CameraManager))] public class CameraManagerEditor : Editor { private const string UxmlPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/CameraManagerEditor.uxml"; private const string UssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/CameraManagerEditor.uss"; private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; private VisualElement presetsContainer; private Label presetsTitleLabel; private CameraManager manager; public override VisualElement CreateInspectorGUI() { manager = (CameraManager)target; var root = new VisualElement(); // Stylesheets var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath); var uss = AssetDatabase.LoadAssetAtPath(UssPath); if (commonUss != null) root.styleSheets.Add(commonUss); if (uss != null) root.styleSheets.Add(uss); // UXML template var uxml = AssetDatabase.LoadAssetAtPath(UxmlPath); if (uxml != null) uxml.CloneTree(root); // Conditional UI logic SetupZoomValidation(root); SetupFOVValidation(root); SetupRotationTargetLogic(root); SetupBlendTransitionLogic(root); // Preset list (dynamic) BuildPresetsSection(root); // Play mode active state polling root.schedule.Execute(UpdatePlayModeState).Every(200); // Undo/redo Undo.undoRedoPerformed += OnUndoRedo; root.RegisterCallback(_ => Undo.undoRedoPerformed -= OnUndoRedo); return root; } private void OnUndoRedo() { if (manager == null) return; RebuildPresetList(); } #region Validation private void SetupZoomValidation(VisualElement root) { var warning = root.Q("zoomWarning"); if (warning == null) return; var minProp = serializedObject.FindProperty("minZoomDistance"); var maxProp = serializedObject.FindProperty("maxZoomDistance"); void UpdateVisibility(SerializedProperty _) { serializedObject.Update(); bool show = serializedObject.FindProperty("minZoomDistance").floatValue >= serializedObject.FindProperty("maxZoomDistance").floatValue; warning.style.display = show ? DisplayStyle.Flex : DisplayStyle.None; } root.TrackPropertyValue(minProp, UpdateVisibility); root.TrackPropertyValue(maxProp, UpdateVisibility); UpdateVisibility(null); } private void SetupFOVValidation(VisualElement root) { var warning = root.Q("fovWarning"); if (warning == null) return; var minProp = serializedObject.FindProperty("minFOV"); var maxProp = serializedObject.FindProperty("maxFOV"); void UpdateVisibility(SerializedProperty _) { serializedObject.Update(); bool show = serializedObject.FindProperty("minFOV").floatValue >= serializedObject.FindProperty("maxFOV").floatValue; warning.style.display = show ? DisplayStyle.Flex : DisplayStyle.None; } root.TrackPropertyValue(minProp, UpdateVisibility); root.TrackPropertyValue(maxProp, UpdateVisibility); UpdateVisibility(null); } private void SetupRotationTargetLogic(VisualElement root) { var targetField = root.Q("manualRotationTargetField"); var info = root.Q("rotationTargetInfo"); if (targetField == null || info == null) return; var useHeadProp = serializedObject.FindProperty("useAvatarHeadAsTarget"); void UpdateState(SerializedProperty _) { serializedObject.Update(); bool useHead = serializedObject.FindProperty("useAvatarHeadAsTarget").boolValue; targetField.SetEnabled(!useHead); info.style.display = useHead ? DisplayStyle.Flex : DisplayStyle.None; } root.TrackPropertyValue(useHeadProp, UpdateState); UpdateState(null); } private void SetupBlendTransitionLogic(VisualElement root) { var settingsGroup = root.Q("blendSettingsGroup"); var modeInfo = root.Q("blendModeInfo"); var rendererWarning = root.Q("blendRendererWarning"); if (settingsGroup == null || modeInfo == null || rendererWarning == null) return; var useBlendProp = serializedObject.FindProperty("useBlendTransition"); var useRealtimeProp = serializedObject.FindProperty("useRealtimeBlend"); void UpdateState(SerializedProperty _) { serializedObject.Update(); bool useBlend = serializedObject.FindProperty("useBlendTransition").boolValue; bool useRealtime = serializedObject.FindProperty("useRealtimeBlend").boolValue; settingsGroup.SetEnabled(useBlend); rendererWarning.style.display = useBlend ? DisplayStyle.Flex : DisplayStyle.None; if (useBlend) { modeInfo.style.display = DisplayStyle.Flex; modeInfo.text = useRealtime ? "실시간 모드: 블렌딩 중 두 카메라 모두 실시간으로 렌더링됩니다.\n성능 비용이 증가하지만 이전 카메라도 움직입니다." : "스냅샷 모드: 이전 화면을 캡처한 후 블렌딩합니다.\n성능 효율적이지만 이전 화면이 정지합니다."; } else { modeInfo.style.display = DisplayStyle.None; } } root.TrackPropertyValue(useBlendProp, UpdateState); root.TrackPropertyValue(useRealtimeProp, UpdateState); UpdateState(null); } #endregion #region Preset List private void BuildPresetsSection(VisualElement root) { var section = root.Q("presetsSection"); if (section == null) return; // Header var header = new VisualElement(); header.AddToClassList("presets-header"); presetsTitleLabel = new Label($"Camera Presets ({manager.cameraPresets.Count})"); presetsTitleLabel.AddToClassList("presets-title"); header.Add(presetsTitleLabel); var addBtn = new Button(AddPreset) { text = "+ 프리셋 추가" }; addBtn.AddToClassList("preset-add-btn"); header.Add(addBtn); section.Add(header); // Container presetsContainer = new VisualElement(); section.Add(presetsContainer); RebuildPresetList(); } private void RebuildPresetList() { if (presetsContainer == null || manager == null) return; presetsContainer.Clear(); if (presetsTitleLabel != null) presetsTitleLabel.text = $"Camera Presets ({manager.cameraPresets.Count})"; if (manager.cameraPresets.Count == 0) { var empty = new Label("카메라 프리셋이 없습니다. '+ 프리셋 추가' 버튼을 눌러 추가하세요."); empty.AddToClassList("preset-empty"); presetsContainer.Add(empty); return; } for (int i = 0; i < manager.cameraPresets.Count; i++) { presetsContainer.Add(CreatePresetItem(i)); } } private VisualElement CreatePresetItem(int index) { var preset = manager.cameraPresets[index]; bool isActive = Application.isPlaying && manager.CurrentPreset == preset; var item = new VisualElement(); item.AddToClassList("preset-item"); if (isActive) item.AddToClassList("preset-item--active"); // --- Header row --- var headerRow = new VisualElement(); headerRow.AddToClassList("preset-item-header"); var indexLabel = new Label($"{index + 1}"); indexLabel.AddToClassList("preset-index"); headerRow.Add(indexLabel); var nameField = new TextField(); nameField.value = preset.presetName; nameField.AddToClassList("preset-name-field"); int idx = index; nameField.RegisterValueChangedCallback(evt => { Undo.RecordObject(target, "Rename Camera Preset"); manager.cameraPresets[idx].presetName = evt.newValue; EditorUtility.SetDirty(target); }); headerRow.Add(nameField); if (isActive) { var activeLabel = new Label("[Active]"); activeLabel.AddToClassList("preset-active-label"); headerRow.Add(activeLabel); } // Up button var upBtn = new Button(() => SwapPresets(index, index - 1)) { text = "\u25B2" }; upBtn.AddToClassList("preset-reorder-btn"); upBtn.SetEnabled(index > 0); headerRow.Add(upBtn); // Down button var downBtn = new Button(() => SwapPresets(index, index + 1)) { text = "\u25BC" }; downBtn.AddToClassList("preset-reorder-btn"); downBtn.SetEnabled(index < manager.cameraPresets.Count - 1); headerRow.Add(downBtn); // Delete button var deleteBtn = new Button(() => DeletePreset(index)) { text = "X" }; deleteBtn.AddToClassList("preset-delete-btn"); headerRow.Add(deleteBtn); item.Add(headerRow); // --- Property fields --- var fields = new VisualElement(); fields.AddToClassList("preset-fields"); var cameraField = new ObjectField("Virtual Camera") { objectType = typeof(CinemachineCamera), allowSceneObjects = true, value = preset.virtualCamera }; int ci = index; cameraField.RegisterValueChangedCallback(evt => { Undo.RecordObject(target, "Change Camera Preset Camera"); manager.cameraPresets[ci].virtualCamera = evt.newValue as CinemachineCamera; EditorUtility.SetDirty(target); }); fields.Add(cameraField); var mouseToggle = new Toggle("Allow Mouse Control") { tooltip = "이 카메라에서 마우스 조작(회전, 팬, 줌)을 허용할지 여부", value = preset.allowMouseControl }; int mi = index; mouseToggle.RegisterValueChangedCallback(evt => { Undo.RecordObject(target, "Toggle Mouse Control"); manager.cameraPresets[mi].allowMouseControl = evt.newValue; EditorUtility.SetDirty(target); }); fields.Add(mouseToggle); item.Add(fields); return item; } private void AddPreset() { var newCamera = FindFirstObjectByType(); if (newCamera != null) { Undo.RecordObject(target, "Add Camera Preset"); manager.cameraPresets.Add(new CameraManager.CameraPreset(newCamera)); EditorUtility.SetDirty(target); RebuildPresetList(); } else { EditorUtility.DisplayDialog("알림", "Scene에 CinemachineCamera가 없습니다.", "확인"); } } private void DeletePreset(int index) { var preset = manager.cameraPresets[index]; if (EditorUtility.DisplayDialog("프리셋 삭제", $"프리셋 '{preset.presetName}'을(를) 삭제하시겠습니까?", "삭제", "취소")) { Undo.RecordObject(target, "Delete Camera Preset"); manager.cameraPresets.RemoveAt(index); EditorUtility.SetDirty(target); RebuildPresetList(); } } private void SwapPresets(int a, int b) { Undo.RecordObject(target, "Reorder Camera Presets"); (manager.cameraPresets[a], manager.cameraPresets[b]) = (manager.cameraPresets[b], manager.cameraPresets[a]); EditorUtility.SetDirty(target); RebuildPresetList(); } #endregion #region Play Mode State private void UpdatePlayModeState() { if (presetsContainer == null || manager == null) return; if (!Application.isPlaying) return; for (int i = 0; i < presetsContainer.childCount && i < manager.cameraPresets.Count; i++) { var item = presetsContainer[i]; bool isActive = manager.CurrentPreset == manager.cameraPresets[i]; if (isActive && !item.ClassListContains("preset-item--active")) item.AddToClassList("preset-item--active"); else if (!isActive && item.ClassListContains("preset-item--active")) item.RemoveFromClassList("preset-item--active"); } } #endregion }