using UnityEngine; using System.Collections.Generic; using UnityRawInput; using System.Linq; using Unity.Cinemachine; using Streamingle; public class CameraManager : MonoBehaviour, IController { #region Classes private static class KeyMapping { private static readonly Dictionary _mapping; static KeyMapping() { _mapping = new Dictionary(RawKeySetup.KeyMapping); } public static bool TryGetRawKey(KeyCode keyCode, out RawKey rawKey) { return _mapping.TryGetValue(keyCode, out rawKey); } public static bool TryGetKeyCode(RawKey rawKey, out KeyCode keyCode) { var pair = _mapping.FirstOrDefault(x => x.Value == rawKey); keyCode = pair.Key; return keyCode != KeyCode.None; } public static bool IsValidRawKey(RawKey key) { return _mapping.ContainsValue(key); } } [System.Serializable] public class HotkeyCommand { public List rawKeys = new List(); [System.NonSerialized] private List unityKeys = new List(); [System.NonSerialized] public bool isRecording = false; [System.NonSerialized] private float recordStartTime; [System.NonSerialized] private const float MAX_RECORD_TIME = 2f; public void StartRecording() { isRecording = true; recordStartTime = Time.time; rawKeys.Clear(); } public void StopRecording() { isRecording = false; InitializeUnityKeys(); } public void UpdateRecording() { if (!isRecording) return; if (Time.time - recordStartTime > MAX_RECORD_TIME) { StopRecording(); return; } foreach (KeyCode keyCode in System.Enum.GetValues(typeof(KeyCode))) { if (Input.GetKeyDown(keyCode) && KeyMapping.TryGetRawKey(keyCode, out RawKey rawKey)) { if (!rawKeys.Contains(rawKey)) { rawKeys.Add(rawKey); } } } bool allKeysReleased = rawKeys.Any() && rawKeys.All(key => !Input.GetKey(KeyMapping.TryGetKeyCode(key, out KeyCode keyCode) ? keyCode : KeyCode.None)); if (allKeysReleased) { StopRecording(); } } public void InitializeUnityKeys() { unityKeys.Clear(); if (rawKeys == null || !rawKeys.Any()) return; foreach (var rawKey in rawKeys) { if (KeyMapping.TryGetKeyCode(rawKey, out KeyCode keyCode) && keyCode != KeyCode.None) { unityKeys.Add(keyCode); } } } public bool IsTriggered() { if (isRecording) return false; if (rawKeys == null || !rawKeys.Any()) return false; bool allRawKeysPressed = rawKeys.All(key => RawInput.IsKeyDown(key)); if (allRawKeysPressed) return true; if (unityKeys.Any()) { return unityKeys.All(key => Input.GetKey(key)); } return false; } public override string ToString() => rawKeys?.Any() == true ? string.Join(" + ", rawKeys) : "설정되지 않음"; } [System.Serializable] public class CameraPreset { public string presetName = "New Camera Preset"; public CinemachineCamera virtualCamera; public HotkeyCommand hotkey; [System.NonSerialized] public bool isEditingHotkey = false; public CameraPreset(CinemachineCamera camera) { virtualCamera = camera; presetName = camera?.gameObject.name ?? "Unnamed Camera"; hotkey = new HotkeyCommand(); } public bool IsValid() => virtualCamera != null && hotkey != null; } #endregion #region Events public delegate void CameraChangedEventHandler(CameraPreset oldPreset, CameraPreset newPreset); public event CameraChangedEventHandler OnCameraChanged; #endregion #region Fields [SerializeField] public List cameraPresets = new List(); [Header("Camera Control Settings")] private float rotationSensitivity = 2f; private float panSpeed = 0.02f; private float zoomSpeed = 0.1f; private float orbitSpeed = 10f; private CinemachineCamera currentCamera; private InputHandler inputHandler; private CameraPreset currentPreset; private Vector3 rotationCenter = Vector3.zero; private float currentOrbitDistance = 0f; private float currentOrbitHeight = 0f; private Vector3 rotationStartPosition; private bool isRotating = false; // 초기 카메라 상태 저장 private Vector3 initialPosition; private Quaternion initialRotation; private bool isInitialStateSet = false; // 스트림덱 연동 private StreamDeckServerManager streamDeckManager; #endregion #region Properties private bool IsValidSetup => currentCamera != null && inputHandler != null; public CameraPreset CurrentPreset => currentPreset; #endregion #region Unity Messages private void Awake() { InitializeInputHandler(); InitializeRawInput(); InitializeCameraPresets(); // StreamDeckServerManager 찾기 streamDeckManager = FindObjectOfType(); if (streamDeckManager == null) { Debug.LogWarning("[CameraManager] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다."); } } private void OnDestroy() { if (RawInput.IsRunning) { RawInput.OnKeyDown -= HandleRawKeyDown; RawInput.Stop(); } } private void Update() { if (!IsValidSetup) return; UpdateHotkeyRecording(); HandleCameraControls(); HandleHotkeys(); } private void UpdateHotkeyRecording() { foreach (var preset in cameraPresets) { if (preset?.hotkey?.isRecording == true) { preset.hotkey.UpdateRecording(); } } } #endregion #region Initialization private void InitializeInputHandler() { inputHandler = GetComponent(); if (inputHandler == null) { inputHandler = gameObject.AddComponent(); } } private void InitializeRawInput() { if (!RawInput.IsRunning) { RawInput.Start(); RawInput.WorkInBackground = true; } RawInput.OnKeyDown += HandleRawKeyDown; } private void InitializeCameraPresets() { if (cameraPresets == null) { cameraPresets = new List(); } if (!cameraPresets.Any()) { return; } foreach (var preset in cameraPresets.Where(p => p?.hotkey != null)) { preset.hotkey.InitializeUnityKeys(); } Set(0); // 초기 카메라 상태 저장 if (currentCamera != null && !isInitialStateSet) { initialPosition = currentCamera.transform.position; initialRotation = currentCamera.transform.rotation; isInitialStateSet = true; } } #endregion #region Input Handling private void HandleRawKeyDown(RawKey key) { if (key == default(RawKey)) return; TryActivatePresetByInput(preset => { if (preset?.hotkey == null) return false; return preset.hotkey.IsTriggered(); }); } private void HandleHotkeys() { if (Input.anyKeyDown) { TryActivatePresetByInput(preset => { if (preset?.hotkey == null) return false; return preset.hotkey.IsTriggered(); }); } } private void TryActivatePresetByInput(System.Func predicate) { var matchingPreset = cameraPresets?.FirstOrDefault(predicate); if (matchingPreset != null) { Set(cameraPresets.IndexOf(matchingPreset)); } } #endregion #region Camera Controls private void HandleCameraControls() { if (!IsValidSetup) return; var virtualCamera = currentCamera; if (virtualCamera == null) return; // Alt+Q로 초기 위치로 복원 if (Input.GetKey(KeyCode.LeftAlt) && Input.GetKeyDown(KeyCode.Q)) { RestoreInitialCameraState(); return; } HandleRotation(virtualCamera); HandlePanning(virtualCamera); HandleZooming(virtualCamera); HandleOrbiting(virtualCamera); } private void HandleRotation(CinemachineCamera virtualCamera) { Transform cameraTransform = virtualCamera.transform; if (inputHandler.IsRightMouseHeld()) { if (!isRotating) { // 회전 시작 시 현재 상태 저장 isRotating = true; rotationStartPosition = cameraTransform.position; rotationCenter = new Vector3(0f, rotationStartPosition.y, 0f); // 현재 카메라의 회전 중심점으로부터의 거리와 높이 계산 Vector3 toCamera = cameraTransform.position - rotationCenter; currentOrbitDistance = new Vector3(toCamera.x, 0f, toCamera.z).magnitude; currentOrbitHeight = toCamera.y; } Vector2 lookDelta = inputHandler.GetLookDelta(); if (lookDelta.sqrMagnitude < float.Epsilon) return; // 현재 회전값을 오일러 각도로 가져오기 Vector3 currentEuler = cameraTransform.eulerAngles; // Y축 회전만 적용 (수평 회전) float newY = currentEuler.y + lookDelta.x * rotationSensitivity; // 회전 적용 (X축 회전은 0으로 고정) Quaternion targetRotation = Quaternion.Euler(0f, newY, 0f); // 오비탈 회전을 위한 새로운 위치 계산 Vector3 orbitPosition = targetRotation * Vector3.back * currentOrbitDistance; orbitPosition.y = currentOrbitHeight; // 높이 유지 // 회전 중심점을 기준으로 새로운 위치 설정 cameraTransform.position = rotationCenter + orbitPosition; cameraTransform.rotation = targetRotation; } else { isRotating = false; } } private void HandlePanning(CinemachineCamera virtualCamera) { if (!inputHandler.IsMiddleMouseHeld()) return; Vector2 panDelta = inputHandler.GetLookDelta(); if (panDelta.sqrMagnitude < float.Epsilon) return; Transform cameraTransform = virtualCamera.transform; // 이동 적용 (카메라의 right와 up 방향으로 이동) Vector3 right = cameraTransform.right * -panDelta.x * panSpeed; Vector3 up = cameraTransform.up * -panDelta.y * panSpeed; cameraTransform.position += right + up; } private void HandleZooming(CinemachineCamera virtualCamera) { if (inputHandler.IsZoomActive()) { // Ctrl + 좌클릭으로 줌 Vector2 lookDelta = inputHandler.GetLookDelta(); if (lookDelta.sqrMagnitude < float.Epsilon) return; Transform cameraTransform = virtualCamera.transform; Vector3 forward = cameraTransform.forward * lookDelta.y * zoomSpeed * 10f; cameraTransform.position += forward; } else { // 마우스 휠로 줌 float zoomDelta = inputHandler.GetZoomDelta(); if (Mathf.Abs(zoomDelta) <= 0.1f) return; Transform cameraTransform = virtualCamera.transform; Vector3 forward = cameraTransform.forward * zoomDelta * zoomSpeed; cameraTransform.position += forward; } } private void HandleOrbiting(CinemachineCamera virtualCamera) { if (!inputHandler.IsOrbitActive()) return; Vector2 orbitDelta = inputHandler.GetLookDelta(); if (orbitDelta.sqrMagnitude < float.Epsilon) return; Transform cameraTransform = virtualCamera.transform; // 현재 회전값을 오일러 각도로 가져오기 Vector3 currentEuler = cameraTransform.eulerAngles; // X축 회전값을 -80도에서 80도 사이로 제한하기 위해 360도 형식에서 변환 float currentX = currentEuler.x; if (currentX > 180f) currentX -= 360f; // 새로운 회전값 계산 float newX = currentX - orbitDelta.y * orbitSpeed; float newY = currentEuler.y + orbitDelta.x * orbitSpeed; // X축 회전 제한 (-80도 ~ 80도) newX = Mathf.Clamp(newX, -80f, 80f); // 회전 적용 Quaternion targetRotation = Quaternion.Euler(newX, newY, 0f); cameraTransform.rotation = targetRotation; // 원점으로부터의 거리 유지 float distance = cameraTransform.position.magnitude; // 새로운 위치 계산 (원점으로부터의 거리 유지) Vector3 newPosition = targetRotation * Vector3.back * distance; cameraTransform.position = newPosition; } private void RestoreInitialCameraState() { if (!isInitialStateSet || currentCamera == null) return; currentCamera.transform.position = initialPosition; currentCamera.transform.rotation = initialRotation; // 회전 중심점 초기화 rotationCenter = new Vector3(0f, initialPosition.y, 0f); } #endregion #region Camera Management public void Set(int index) { Debug.Log($"[CameraManager] 카메라 {index}번으로 전환 시작 (총 {cameraPresets?.Count ?? 0}개)"); if (cameraPresets == null) { Debug.LogError("[CameraManager] cameraPresets가 null입니다!"); return; } if (index < 0 || index >= cameraPresets.Count) { Debug.LogError($"[CameraManager] 잘못된 인덱스: {index}, 유효 범위: 0-{cameraPresets.Count - 1}"); return; } var newPreset = cameraPresets[index]; if (!newPreset.IsValid()) { Debug.LogError($"[CameraManager] 프리셋이 유효하지 않습니다 - 인덱스: {index}"); return; } var oldPreset = currentPreset; var newCameraName = newPreset.virtualCamera?.gameObject.name ?? "Unknown"; currentPreset = newPreset; UpdateCameraPriorities(newPreset.virtualCamera); OnCameraChanged?.Invoke(oldPreset, newPreset); // 스트림덱에 카메라 변경 알림 전송 if (streamDeckManager != null) { streamDeckManager.NotifyCameraChanged(); } Debug.Log($"[CameraManager] 카메라 전환 완료: {newCameraName}"); } private void UpdateCameraPriorities(CinemachineCamera newCamera) { if (newCamera == null) { Debug.LogError("[CameraManager] 새 카메라가 null입니다!"); return; } if (currentCamera != null) { currentCamera.Priority = 0; } currentCamera = newCamera; currentCamera.Priority = 10; } #endregion #region Hotkey Management public void StartRecordingHotkey(int presetIndex) { if (cameraPresets == null || presetIndex < 0 || presetIndex >= cameraPresets.Count) return; var preset = cameraPresets[presetIndex]; if (!preset.IsValid()) return; foreach (var otherPreset in cameraPresets) { if (otherPreset?.hotkey?.isRecording == true) { otherPreset.hotkey.StopRecording(); } } preset.hotkey.StartRecording(); } public void StopRecordingHotkey(int presetIndex) { if (cameraPresets == null || presetIndex < 0 || presetIndex >= cameraPresets.Count) return; var preset = cameraPresets[presetIndex]; if (preset?.hotkey?.isRecording == true) { preset.hotkey.StopRecording(); } } #endregion #region Camera Data // 카메라 목록 데이터 반환 (HTTP 요청 시 직접 호출됨) public CameraListData GetCameraListData() { var presetList = cameraPresets.Select((preset, index) => new CameraPresetData { index = index, name = preset?.virtualCamera?.gameObject.name ?? $"Camera {index}", isActive = currentPreset == preset, hotkey = preset?.hotkey?.ToString() ?? "설정되지 않음" }).ToArray(); return new CameraListData { camera_count = cameraPresets.Count, presets = presetList, current_index = currentPreset != null ? cameraPresets.IndexOf(currentPreset) : -1 }; } // 현재 카메라 상태 데이터 반환 public CameraStateData GetCurrentCameraState() { if (currentPreset == null) return null; var currentIndex = cameraPresets.IndexOf(currentPreset); return new CameraStateData { current_index = currentIndex, camera_name = currentPreset.virtualCamera?.gameObject.name ?? "Unknown", preset_name = currentPreset.presetName, total_cameras = cameraPresets.Count }; } [System.Serializable] public class CameraPresetData { public int index; public string name; public bool isActive; public string hotkey; } [System.Serializable] public class CameraListData { public int camera_count; public CameraPresetData[] presets; public int current_index; } [System.Serializable] public class CameraStateData { public int current_index; public string camera_name; public string preset_name; public int total_cameras; } #endregion #region IController Implementation public string GetControllerId() { return "camera_controller"; } public string GetControllerName() { return "카메라 컨트롤러"; } public object GetControllerData() { return GetCameraListData(); } public void ExecuteAction(string actionId, object parameters) { switch (actionId) { case "switch_camera": if (parameters is int cameraIndex) { Set(cameraIndex); } else if (parameters is System.Dynamic.ExpandoObject expando) { var dict = (IDictionary)expando; if (dict.ContainsKey("camera_index") && dict["camera_index"] is int index) { Set(index); } } break; case "get_camera_list": // 카메라 목록은 GetControllerData()에서 반환됨 break; default: Debug.LogWarning($"[CameraManager] 알 수 없는 액션: {actionId}"); break; } } #endregion }