512 lines
17 KiB
C#
512 lines
17 KiB
C#
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<KeyCode> currentKeys = new HashSet<KeyCode>();
|
|
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<CinemachineCamera>();
|
|
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<RawKey>();
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
} |