using UnityEngine; using UnityEditor; using UnityRawInput; using System.Collections.Generic; using System.Linq; using Unity.Cinemachine; [CustomEditor(typeof(CameraManager))] public class CameraManagerEditor : Editor { private HashSet currentKeys = new HashSet(); private bool isApplicationPlaying; private bool isListening = false; // Foldout 상태 private bool showCameraPresets = true; private bool showControlSettings = true; private bool showSmoothingSettings = true; private bool showZoomSettings = true; private bool showRotationTarget = true; private bool showBlendSettings = true; // SerializedProperties private SerializedProperty rotationSensitivityProp; private SerializedProperty panSpeedProp; private SerializedProperty zoomSpeedProp; private SerializedProperty orbitSpeedProp; private SerializedProperty movementSmoothingProp; private SerializedProperty rotationSmoothingProp; private SerializedProperty minZoomDistanceProp; private SerializedProperty maxZoomDistanceProp; private SerializedProperty useAvatarHeadAsTargetProp; private SerializedProperty manualRotationTargetProp; private SerializedProperty useBlendTransitionProp; private SerializedProperty blendTimeProp; // 스타일 private GUIStyle headerStyle; private GUIStyle sectionBoxStyle; private GUIStyle presetBoxStyle; private void OnEnable() { isApplicationPlaying = Application.isPlaying; // SerializedProperties 가져오기 rotationSensitivityProp = serializedObject.FindProperty("rotationSensitivity"); panSpeedProp = serializedObject.FindProperty("panSpeed"); zoomSpeedProp = serializedObject.FindProperty("zoomSpeed"); orbitSpeedProp = serializedObject.FindProperty("orbitSpeed"); movementSmoothingProp = serializedObject.FindProperty("movementSmoothing"); rotationSmoothingProp = serializedObject.FindProperty("rotationSmoothing"); minZoomDistanceProp = serializedObject.FindProperty("minZoomDistance"); maxZoomDistanceProp = serializedObject.FindProperty("maxZoomDistance"); useAvatarHeadAsTargetProp = serializedObject.FindProperty("useAvatarHeadAsTarget"); manualRotationTargetProp = serializedObject.FindProperty("manualRotationTarget"); useBlendTransitionProp = serializedObject.FindProperty("useBlendTransition"); blendTimeProp = serializedObject.FindProperty("blendTime"); } private void OnDisable() { StopListening(); } private void InitializeStyles() { if (headerStyle == null) { headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12, margin = new RectOffset(0, 0, 5, 5) }; } if (sectionBoxStyle == null) { sectionBoxStyle = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(10, 10, 10, 10), margin = new RectOffset(0, 0, 5, 5) }; } if (presetBoxStyle == null) { presetBoxStyle = new GUIStyle(GUI.skin.box) { padding = new RectOffset(8, 8, 8, 8), margin = new RectOffset(0, 0, 3, 3) }; } } private void StartListening() { if (!isApplicationPlaying) { currentKeys.Clear(); isListening = true; Debug.Log("키보드 입력 감지 시작"); } } private void StopListening() { isListening = false; } public override void OnInspectorGUI() { serializedObject.Update(); InitializeStyles(); CameraManager manager = (CameraManager)target; EditorGUILayout.Space(5); // 카메라 컨트롤 설정 섹션 DrawControlSettingsSection(); // 스무딩 설정 섹션 DrawSmoothingSection(); // 줌 제한 섹션 DrawZoomLimitsSection(); // 회전 타겟 섹션 DrawRotationTargetSection(); // 블렌드 전환 섹션 DrawBlendTransitionSection(); EditorGUILayout.Space(10); // 카메라 프리셋 섹션 DrawCameraPresetsSection(manager); // 키 입력 감지 로직 HandleKeyListening(manager); serializedObject.ApplyModifiedProperties(); } private void DrawControlSettingsSection() { EditorGUILayout.BeginVertical(sectionBoxStyle); showControlSettings = EditorGUILayout.Foldout(showControlSettings, "Camera Control Settings", true, EditorStyles.foldoutHeader); if (showControlSettings) { EditorGUILayout.Space(5); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(rotationSensitivityProp, new GUIContent("회전 감도", "마우스 회전 감도 (0.5 ~ 10)")); EditorGUILayout.PropertyField(panSpeedProp, new GUIContent("패닝 속도", "휠클릭 패닝 속도 (0.005 ~ 0.1)")); EditorGUILayout.PropertyField(zoomSpeedProp, new GUIContent("줌 속도", "마우스 휠 줌 속도 (0.05 ~ 0.5)")); EditorGUILayout.PropertyField(orbitSpeedProp, new GUIContent("오빗 속도", "궤도 회전 속도 (1 ~ 20)")); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void DrawSmoothingSection() { EditorGUILayout.BeginVertical(sectionBoxStyle); showSmoothingSettings = EditorGUILayout.Foldout(showSmoothingSettings, "Smoothing", true, EditorStyles.foldoutHeader); if (showSmoothingSettings) { EditorGUILayout.Space(5); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(movementSmoothingProp, new GUIContent("이동 스무딩", "카메라 이동 부드러움 (0 ~ 0.95)")); EditorGUILayout.PropertyField(rotationSmoothingProp, new GUIContent("회전 스무딩", "카메라 회전 부드러움 (0 ~ 0.95)")); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void DrawZoomLimitsSection() { EditorGUILayout.BeginVertical(sectionBoxStyle); showZoomSettings = EditorGUILayout.Foldout(showZoomSettings, "Zoom Limits", true, EditorStyles.foldoutHeader); if (showZoomSettings) { EditorGUILayout.Space(5); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(minZoomDistanceProp, new GUIContent("최소 줌 거리", "카메라 최소 거리")); EditorGUILayout.PropertyField(maxZoomDistanceProp, new GUIContent("최대 줌 거리", "카메라 최대 거리")); // 경고 표시 if (minZoomDistanceProp.floatValue >= maxZoomDistanceProp.floatValue) { EditorGUILayout.HelpBox("최소 줌 거리는 최대 줌 거리보다 작아야 합니다.", MessageType.Warning); } EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void DrawRotationTargetSection() { EditorGUILayout.BeginVertical(sectionBoxStyle); showRotationTarget = EditorGUILayout.Foldout(showRotationTarget, "Rotation Target", true, EditorStyles.foldoutHeader); if (showRotationTarget) { EditorGUILayout.Space(5); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(useAvatarHeadAsTargetProp, new GUIContent("아바타 머리 사용", "활성화하면 아바타 머리를 회전 중심으로 사용")); EditorGUI.BeginDisabledGroup(useAvatarHeadAsTargetProp.boolValue); EditorGUILayout.PropertyField(manualRotationTargetProp, new GUIContent("수동 회전 타겟", "수동으로 지정하는 회전 중심점")); EditorGUI.EndDisabledGroup(); if (useAvatarHeadAsTargetProp.boolValue) { EditorGUILayout.HelpBox("런타임에 CustomRetargetingScript를 가진 아바타의 Head 본을 자동으로 찾습니다.", MessageType.Info); } EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void DrawBlendTransitionSection() { EditorGUILayout.BeginVertical(sectionBoxStyle); showBlendSettings = EditorGUILayout.Foldout(showBlendSettings, "Camera Blend Transition", true, EditorStyles.foldoutHeader); if (showBlendSettings) { EditorGUILayout.Space(5); EditorGUI.indentLevel++; EditorGUILayout.PropertyField(useBlendTransitionProp, new GUIContent("블렌드 전환 사용", "카메라 전환 시 크로스 디졸브 효과 사용")); EditorGUI.BeginDisabledGroup(!useBlendTransitionProp.boolValue); EditorGUILayout.PropertyField(blendTimeProp, new GUIContent("블렌드 시간", "크로스 디졸브 소요 시간 (초)")); EditorGUI.EndDisabledGroup(); if (useBlendTransitionProp.boolValue) { EditorGUILayout.HelpBox("URP Renderer에 CameraBlendRendererFeature가 추가되어 있어야 합니다.", MessageType.Info); } EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); } private void DrawCameraPresetsSection(CameraManager manager) { EditorGUILayout.BeginVertical(sectionBoxStyle); // 헤더 EditorGUILayout.BeginHorizontal(); showCameraPresets = EditorGUILayout.Foldout(showCameraPresets, $"Camera Presets ({manager.cameraPresets.Count})", true, EditorStyles.foldoutHeader); GUILayout.FlexibleSpace(); // 프리셋 추가 버튼 if (GUILayout.Button("+ 프리셋 추가", GUILayout.Width(100), GUILayout.Height(20))) { var newCamera = FindFirstObjectByType(); if (newCamera != null) { manager.cameraPresets.Add(new CameraManager.CameraPreset(newCamera)); EditorUtility.SetDirty(target); } else { EditorUtility.DisplayDialog("알림", "Scene에 CinemachineCamera가 없습니다.", "확인"); } } EditorGUILayout.EndHorizontal(); if (showCameraPresets) { EditorGUILayout.Space(5); if (manager.cameraPresets.Count == 0) { EditorGUILayout.HelpBox("카메라 프리셋이 없습니다. '+ 프리셋 추가' 버튼을 눌러 추가하세요.", MessageType.Info); } else { // 프리셋 리스트 for (int i = 0; i < manager.cameraPresets.Count; i++) { DrawPresetItem(manager, i); } } } EditorGUILayout.EndVertical(); } private void DrawPresetItem(CameraManager manager, int index) { var preset = manager.cameraPresets[index]; // 활성 프리셋 표시를 위한 배경색 bool isActive = Application.isPlaying && manager.CurrentPreset == preset; if (isActive) { GUI.backgroundColor = new Color(0.5f, 0.8f, 0.5f); } EditorGUILayout.BeginVertical(presetBoxStyle); GUI.backgroundColor = Color.white; // 프리셋 헤더 EditorGUILayout.BeginHorizontal(); // 인덱스 번호 GUILayout.Label($"{index + 1}", EditorStyles.boldLabel, GUILayout.Width(20)); // 프리셋 이름 필드 EditorGUI.BeginChangeCheck(); preset.presetName = EditorGUILayout.TextField(preset.presetName, GUILayout.MinWidth(100)); if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(target); } // 활성 표시 if (isActive) { GUILayout.Label("[Active]", EditorStyles.miniLabel, GUILayout.Width(50)); } GUILayout.FlexibleSpace(); // 위/아래 버튼 EditorGUI.BeginDisabledGroup(index == 0); if (GUILayout.Button("▲", GUILayout.Width(25))) { SwapPresets(manager, index, index - 1); } EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(index == manager.cameraPresets.Count - 1); if (GUILayout.Button("▼", GUILayout.Width(25))) { SwapPresets(manager, index, index + 1); } EditorGUI.EndDisabledGroup(); // 삭제 버튼 GUI.backgroundColor = new Color(1f, 0.5f, 0.5f); if (GUILayout.Button("X", GUILayout.Width(25))) { if (EditorUtility.DisplayDialog("프리셋 삭제", $"프리셋 '{preset.presetName}'을(를) 삭제하시겠습니까?", "삭제", "취소")) { manager.cameraPresets.RemoveAt(index); EditorUtility.SetDirty(target); return; } } GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(3); // 가상 카메라 필드 EditorGUI.BeginChangeCheck(); preset.virtualCamera = (CinemachineCamera)EditorGUILayout.ObjectField( "Virtual Camera", preset.virtualCamera, typeof(CinemachineCamera), true); if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(target); } // 마우스 조작 허용 설정 EditorGUI.BeginChangeCheck(); preset.allowMouseControl = EditorGUILayout.Toggle( new GUIContent("Allow Mouse Control", "이 카메라에서 마우스 조작(회전, 팬, 줌)을 허용할지 여부"), preset.allowMouseControl); if (EditorGUI.EndChangeCheck()) { EditorUtility.SetDirty(target); } // 핫키 설정 UI EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Hotkey", GUILayout.Width(70)); if (preset.hotkey.isRecording) { GUI.backgroundColor = new Color(1f, 0.9f, 0.5f); EditorGUILayout.LabelField("키를 눌렀다 떼면 저장됩니다...", EditorStyles.helpBox); GUI.backgroundColor = Color.white; } else { // 핫키 표시 string hotkeyDisplay = preset.hotkey?.ToString() ?? "설정되지 않음"; EditorGUILayout.LabelField(hotkeyDisplay, EditorStyles.textField, GUILayout.MinWidth(80)); if (GUILayout.Button("Record", GUILayout.Width(60))) { foreach (var otherPreset in manager.cameraPresets) { otherPreset.hotkey.isRecording = false; } preset.hotkey.isRecording = true; preset.hotkey.rawKeys.Clear(); StartListening(); } if (GUILayout.Button("Clear", GUILayout.Width(50))) { preset.hotkey.rawKeys.Clear(); EditorUtility.SetDirty(target); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.Space(2); } private void SwapPresets(CameraManager manager, int indexA, int indexB) { var temp = manager.cameraPresets[indexA]; manager.cameraPresets[indexA] = manager.cameraPresets[indexB]; manager.cameraPresets[indexB] = temp; EditorUtility.SetDirty(target); } private void HandleKeyListening(CameraManager manager) { if (isListening) { var e = Event.current; if (e != null) { if (e.type == EventType.KeyDown && e.keyCode != KeyCode.None) { // 마우스 버튼 제외 if (e.keyCode != KeyCode.Mouse0 && e.keyCode != KeyCode.Mouse1 && e.keyCode != KeyCode.Mouse2) { AddKey(e.keyCode); e.Use(); } } else if (e.type == EventType.KeyUp && currentKeys.Contains(e.keyCode)) { var recordingPreset = manager.cameraPresets.FirstOrDefault(p => p.hotkey.isRecording); if (recordingPreset != null) { recordingPreset.hotkey.isRecording = false; EditorUtility.SetDirty(target); StopListening(); Repaint(); } e.Use(); } } } } private void AddKey(KeyCode keyCode) { if (!currentKeys.Contains(keyCode)) { currentKeys.Add(keyCode); var recordingPreset = ((CameraManager)target).cameraPresets.FirstOrDefault(p => p.hotkey.isRecording); if (recordingPreset != null) { // KeyCode를 RawKey로 변환 var rawKeys = new List(); foreach (var key in currentKeys) { if (RawKeySetup.KeyMapping.TryGetValue(key, out RawKey rawKey)) { rawKeys.Add(rawKey); } else { Debug.LogWarning($"맵핑되지 않은 키: {key}"); } } recordingPreset.hotkey.rawKeys = rawKeys; EditorUtility.SetDirty(target); Repaint(); } } } }