using UnityEngine; using UnityEngine.Rendering.Universal; using System.Collections; using System.Collections.Generic; using UnityRawInput; using System.Linq; using Unity.Cinemachine; using Streamingle; using KindRetargeting; 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 : ISerializationCallbackReceiver { public string presetName = "New Camera Preset"; public CinemachineCamera virtualCamera; public HotkeyCommand hotkey; [System.NonSerialized] public bool isEditingHotkey = false; // 마우스 조작 허용 여부 [Tooltip("이 카메라에서 마우스 조작(회전, 팬, 줌 등)을 허용할지 여부")] public bool allowMouseControl = true; // 직렬화 버전 체크용 (기존 데이터 마이그레이션) [SerializeField, HideInInspector] private bool _initialized = false; public void OnBeforeSerialize() { } public void OnAfterDeserialize() { // 기존 데이터(버전 업 전)는 _initialized가 false이므로 기본값 적용 if (!_initialized) { allowMouseControl = true; _initialized = true; } } // 프리셋별 초기 상태 저장 [System.NonSerialized] public Vector3 savedPosition; [System.NonSerialized] public Quaternion savedRotation; [System.NonSerialized] public bool hasSavedState = false; public CameraPreset(CinemachineCamera camera) { virtualCamera = camera; presetName = camera?.gameObject.name ?? "Unnamed Camera"; hotkey = new HotkeyCommand(); allowMouseControl = true; } public bool IsValid() => virtualCamera != null && hotkey != null; public void SaveCurrentState() { if (virtualCamera == null) return; savedPosition = virtualCamera.transform.position; savedRotation = virtualCamera.transform.rotation; hasSavedState = true; } public void RestoreSavedState() { if (!hasSavedState || virtualCamera == null) return; virtualCamera.transform.position = savedPosition; virtualCamera.transform.rotation = savedRotation; } } #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")] [SerializeField, Range(0.5f, 10f)] private float rotationSensitivity = 2f; [SerializeField, Range(0.005f, 0.1f)] private float panSpeed = 0.02f; [SerializeField, Range(0.05f, 0.5f)] private float zoomSpeed = 0.1f; [SerializeField, Range(1f, 20f)] private float orbitSpeed = 10f; [Header("Smoothing")] [SerializeField, Range(0f, 0.95f)] private float movementSmoothing = 0.1f; [SerializeField, Range(0f, 0.95f)] private float rotationSmoothing = 0.1f; [Header("Zoom Limits")] [SerializeField] private float minZoomDistance = 0.5f; [SerializeField] private float maxZoomDistance = 50f; [Header("Rotation Target")] [Tooltip("체크하면 아바타 머리를 자동으로 찾아 회전 중심점으로 사용합니다.")] [SerializeField] private bool useAvatarHeadAsTarget = true; [Tooltip("수동으로 회전 중심점을 지정합니다. (useAvatarHeadAsTarget이 false일 때 사용)")] [SerializeField] private Transform manualRotationTarget; [Header("Camera Blend Transition")] [Tooltip("블렌드 전환 사용 여부 (크로스 디졸브)")] [SerializeField] private bool useBlendTransition = false; [Tooltip("블렌드 전환 시간 (초)")] [SerializeField, Range(0.1f, 2f)] private float blendTime = 0.5f; // 블렌드용 렌더 텍스처와 카메라 private RenderTexture blendRenderTexture; private Camera blendCamera; private CinemachineCamera currentCamera; private InputHandler inputHandler; private CameraPreset currentPreset; // 오빗 카메라 상태 (각도 기반) private float horizontalAngle; private float verticalAngle; private float currentDistance; private Vector3 focusPoint; // 타겟 값 (스무딩용) private float targetHorizontalAngle; private float targetVerticalAngle; private float targetDistance; private Vector3 targetFocusPoint; // 아바타 머리 추적 private Transform avatarHeadTransform; // 스트림덱 연동 private StreamDeckServerManager streamDeckManager; // 블렌드 전환 상태 private Coroutine blendCoroutine; private bool isBlending = false; #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 = FindAnyObjectByType(); if (streamDeckManager == null) { Debug.LogWarning("[CameraManager] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다."); } } private void Start() { // Start에서 아바타 머리 다시 찾기 (다른 스크립트들이 초기화된 후) if (useAvatarHeadAsTarget && avatarHeadTransform == null) { FindAvatarHead(); } } private void OnDestroy() { if (RawInput.IsRunning) { RawInput.OnKeyDown -= HandleRawKeyDown; RawInput.Stop(); } // 블렌드 리소스 정리 CameraBlendController.Reset(); if (blendRenderTexture != null) { blendRenderTexture.Release(); Destroy(blendRenderTexture); blendRenderTexture = null; } if (blendCamera != null) { Destroy(blendCamera.gameObject); blendCamera = null; } } 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; 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(); } // 모든 프리셋의 초기 상태 저장 foreach (var preset in cameraPresets.Where(p => p?.IsValid() == true)) { if (!preset.hasSavedState) { preset.SaveCurrentState(); } } // 아바타 머리 찾기 FindAvatarHead(); Set(0); } private void FindAvatarHead() { if (!useAvatarHeadAsTarget) return; // CustomRetargetingScript를 가진 아바타 찾기 var retargetingScripts = FindObjectsByType(FindObjectsSortMode.None); foreach (var script in retargetingScripts) { if (script == null || !script.gameObject.activeInHierarchy) continue; // targetAnimator가 설정되어 있으면 사용 Animator animator = script.targetAnimator; // targetAnimator가 null이면 같은 GameObject의 Animator 시도 if (animator == null) { animator = script.GetComponent(); } if (animator != null) { avatarHeadTransform = animator.GetBoneTransform(HumanBodyBones.Head); if (avatarHeadTransform != null) { Debug.Log($"[CameraManager] 아바타 머리를 회전 중심점으로 설정: {avatarHeadTransform.name}"); return; } } } Debug.LogWarning("[CameraManager] 활성화된 아바타의 Head 본을 찾을 수 없습니다. 수동 타겟을 사용하거나 원점을 사용합니다."); } /// /// 현재 회전 중심점을 반환합니다. /// 우선순위: 아바타 머리 > 수동 타겟 > 원점 /// private Vector3 GetRotationCenter() { if (useAvatarHeadAsTarget && avatarHeadTransform != null) { return avatarHeadTransform.position; } else if (manualRotationTarget != null) { return manualRotationTarget.position; } return Vector3.zero; } #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 InitializeOrbitState() { if (currentCamera == null) return; focusPoint = GetRotationCenter(); targetFocusPoint = focusPoint; Vector3 direction = currentCamera.transform.position - focusPoint; currentDistance = direction.magnitude; targetDistance = currentDistance; if (currentDistance > 0.01f) { direction.Normalize(); horizontalAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg; verticalAngle = Mathf.Asin(Mathf.Clamp(direction.y, -1f, 1f)) * Mathf.Rad2Deg; } targetHorizontalAngle = horizontalAngle; targetVerticalAngle = verticalAngle; } private void HandleCameraControls() { if (!IsValidSetup) return; if (currentCamera == null) return; // 현재 프리셋이 마우스 조작을 허용하지 않으면 스킵 if (currentPreset != null && !currentPreset.allowMouseControl) return; // Alt+Q로 초기 위치로 복원 if (Input.GetKey(KeyCode.LeftAlt) && Input.GetKeyDown(KeyCode.Q)) { RestoreInitialCameraState(); return; } HandleInput(); UpdateCameraPosition(); } private void HandleInput() { // 입력 우선순위 처리: Orbit > AltRightZoom > Zoom > Rotation > Panning if (inputHandler.IsOrbitActive()) { HandleOrbiting(); } else if (inputHandler.IsCtrlRightZoomActive()) { HandleCtrlRightZoom(); } else if (inputHandler.IsZoomActive()) { HandleDragZoom(); } else if (inputHandler.IsRightMouseHeld()) { HandleRotation(); } else if (inputHandler.IsMiddleMouseHeld()) { HandlePanning(); } // 휠 줌은 항상 처리 HandleWheelZoom(); } /// /// 우클릭 드래그: 수평 + 수직 회전 (현재 비활성화 - Alt+우클릭 사용) /// private void HandleRotation() { // 우클릭만으로는 회전하지 않음 - Alt+우클릭(Orbit)으로 대체 } /// /// Alt + 우클릭: 자유 궤도 회전 (X, Y축) /// private void HandleOrbiting() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 회전 속도 targetHorizontalAngle += delta.x * rotationSensitivity; targetVerticalAngle -= delta.y * rotationSensitivity; targetVerticalAngle = Mathf.Clamp(targetVerticalAngle, -80f, 80f); } /// /// 휠클릭 드래그: 패닝 (초점 이동) /// private void HandlePanning() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 거리에 비례하여 패닝 속도 조절 - 0.2배 float speedMultiplier = targetDistance * panSpeed * 0.175f; // 카메라의 로컬 축 기준으로 초점 이동 Vector3 right = currentCamera.transform.right; Vector3 up = currentCamera.transform.up; Vector3 panOffset = (-right * delta.x - up * delta.y) * speedMultiplier; targetFocusPoint += panOffset; } /// /// 마우스 휠: 줌 /// private void HandleWheelZoom() { float scroll = inputHandler.GetZoomDelta(); if (Mathf.Abs(scroll) < 0.01f) return; // 거리에 비례하여 줌 속도 조절 (더 자연스러운 느낌) - 0.4배 float zoomDelta = scroll * zoomSpeed * targetDistance * 0.4f; targetDistance -= zoomDelta; targetDistance = Mathf.Clamp(targetDistance, minZoomDistance, maxZoomDistance); } /// /// Ctrl + 좌클릭 드래그: 줌 /// private void HandleDragZoom() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; float zoomDelta = delta.y * zoomSpeed * targetDistance * 0.5f; targetDistance -= zoomDelta; targetDistance = Mathf.Clamp(targetDistance, minZoomDistance, maxZoomDistance); } /// /// Ctrl + 우클릭 드래그: 줌 (밀기 = 확대, 당기기 = 축소) /// private void HandleCtrlRightZoom() { Vector2 delta = inputHandler.GetLookDelta(); if (delta.sqrMagnitude < float.Epsilon) return; // 위로 밀면 확대 (거리 감소), 아래로 밀면 축소 (거리 증가) float zoomDelta = delta.y * zoomSpeed * targetDistance * 0.5f; targetDistance -= zoomDelta; targetDistance = Mathf.Clamp(targetDistance, minZoomDistance, maxZoomDistance); } /// /// 스무딩을 적용하여 카메라 위치 업데이트 /// private void UpdateCameraPosition() { float dt = Time.deltaTime; // 스무딩 적용 if (movementSmoothing > 0.01f) { float smoothSpeed = (1f - movementSmoothing) * 15f; horizontalAngle = Mathf.Lerp(horizontalAngle, targetHorizontalAngle, dt * smoothSpeed); verticalAngle = Mathf.Lerp(verticalAngle, targetVerticalAngle, dt * smoothSpeed); currentDistance = Mathf.Lerp(currentDistance, targetDistance, dt * smoothSpeed); focusPoint = Vector3.Lerp(focusPoint, targetFocusPoint, dt * smoothSpeed); } else { horizontalAngle = targetHorizontalAngle; verticalAngle = targetVerticalAngle; currentDistance = targetDistance; focusPoint = targetFocusPoint; } // 구면 좌표계에서 카메라 위치 계산 float horizontalRad = horizontalAngle * Mathf.Deg2Rad; float verticalRad = verticalAngle * Mathf.Deg2Rad; Vector3 offset = new Vector3( Mathf.Sin(horizontalRad) * Mathf.Cos(verticalRad), Mathf.Sin(verticalRad), Mathf.Cos(horizontalRad) * Mathf.Cos(verticalRad) ) * currentDistance; currentCamera.transform.position = focusPoint + offset; currentCamera.transform.LookAt(focusPoint); } private void RestoreInitialCameraState() { if (currentPreset == null || !currentPreset.hasSavedState) return; currentPreset.RestoreSavedState(); // 각도 상태 다시 초기화 InitializeOrbitState(); } #endregion #region Camera Management public void Set(int index) { // 블렌드 전환 사용 시 if (useBlendTransition) { SetWithBlend(index); return; } SetImmediate(index); } /// /// 크로스 디졸브 블렌드와 함께 카메라 전환 /// public void SetWithBlend(int index, float? customBlendTime = null) { if (isBlending) return; if (!ValidateCameraIndex(index)) return; // 같은 카메라로 전환 시도 시 무시 if (currentPreset != null && cameraPresets.IndexOf(currentPreset) == index) return; // 기존 코루틴 중단 if (blendCoroutine != null) { StopCoroutine(blendCoroutine); CameraBlendController.EndBlend(); } blendCoroutine = StartCoroutine(BlendTransitionCoroutine(index, customBlendTime ?? blendTime)); } /// /// 즉시 카메라 전환 (페이드 없음) /// public void SetImmediate(int index) { Debug.Log($"[CameraManager] 카메라 {index}번으로 전환 시작 (총 {cameraPresets?.Count ?? 0}개)"); if (!ValidateCameraIndex(index)) 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); // 오빗 상태 초기화 (각도, 거리 계산) InitializeOrbitState(); OnCameraChanged?.Invoke(oldPreset, newPreset); // 스트림덱에 카메라 변경 알림 전송 if (streamDeckManager != null) { streamDeckManager.NotifyCameraChanged(); } Debug.Log($"[CameraManager] 카메라 전환 완료: {newCameraName}"); } private bool ValidateCameraIndex(int index) { if (cameraPresets == null) { Debug.LogError("[CameraManager] cameraPresets가 null입니다!"); return false; } if (index < 0 || index >= cameraPresets.Count) { Debug.LogError($"[CameraManager] 잘못된 인덱스: {index}, 유효 범위: 0-{cameraPresets.Count - 1}"); return false; } return true; } /// /// 블렌드용 카메라와 렌더 텍스처 생성 /// private void EnsureBlendCamera() { if (blendCamera != null) return; // 메인 카메라 찾기 Camera mainCamera = Camera.main; if (mainCamera == null) { Debug.LogError("[CameraManager] 메인 카메라를 찾을 수 없습니다."); return; } // 블렌드용 카메라 생성 GameObject blendCamObj = new GameObject("BlendCamera"); blendCamObj.transform.SetParent(mainCamera.transform.parent); blendCamera = blendCamObj.AddComponent(); // 메인 카메라 설정 복사 blendCamera.CopyFrom(mainCamera); blendCamera.depth = mainCamera.depth - 1; // 메인 카메라보다 먼저 렌더링 blendCamera.enabled = false; // 수동으로 렌더링할 것임 // URP 카메라 데이터 복사 (포스트 프로세싱 등) var mainCameraData = mainCamera.GetUniversalAdditionalCameraData(); var blendCameraData = blendCamera.GetUniversalAdditionalCameraData(); if (mainCameraData != null && blendCameraData != null) { blendCameraData.renderPostProcessing = mainCameraData.renderPostProcessing; blendCameraData.antialiasing = mainCameraData.antialiasing; blendCameraData.antialiasingQuality = mainCameraData.antialiasingQuality; blendCameraData.renderShadows = mainCameraData.renderShadows; blendCameraData.requiresColorOption = mainCameraData.requiresColorOption; blendCameraData.requiresDepthOption = mainCameraData.requiresDepthOption; blendCameraData.dithering = mainCameraData.dithering; blendCameraData.stopNaN = mainCameraData.stopNaN; blendCameraData.volumeLayerMask = mainCameraData.volumeLayerMask; blendCameraData.volumeTrigger = mainCameraData.volumeTrigger; } Debug.Log("[CameraManager] 블렌드 카메라 생성 완료 (URP 설정 포함)"); } /// /// 블렌드용 렌더 텍스처 생성/갱신 /// private void EnsureBlendRenderTexture() { int width = Screen.width; int height = Screen.height; // 이미 적절한 크기의 텍스처가 있으면 재사용 if (blendRenderTexture != null && blendRenderTexture.width == width && blendRenderTexture.height == height) { return; } // 기존 텍스처 해제 if (blendRenderTexture != null) { blendRenderTexture.Release(); Destroy(blendRenderTexture); } // 새 렌더 텍스처 생성 blendRenderTexture = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32); blendRenderTexture.name = "CameraBlendRT"; blendRenderTexture.Create(); Debug.Log($"[CameraManager] 블렌드 렌더 텍스처 생성: {width}x{height}"); } /// /// 크로스 디졸브 블렌드 전환 코루틴 /// private IEnumerator BlendTransitionCoroutine(int targetIndex, float duration) { isBlending = true; // 블렌드 카메라/텍스처 준비 EnsureBlendCamera(); EnsureBlendRenderTexture(); if (blendCamera == null || blendRenderTexture == null) { Debug.LogError("[CameraManager] 블렌드 카메라 또는 텍스처 생성 실패"); SetImmediate(targetIndex); isBlending = false; blendCoroutine = null; yield break; } // 메인 카메라의 현재 위치/회전 복사 (Cinemachine이 제어하는 실제 카메라) Camera mainCamera = Camera.main; if (mainCamera != null) { blendCamera.transform.position = mainCamera.transform.position; blendCamera.transform.rotation = mainCamera.transform.rotation; blendCamera.fieldOfView = mainCamera.fieldOfView; } // 블렌드 카메라로 현재 화면을 렌더 텍스처에 렌더링 blendCamera.targetTexture = blendRenderTexture; blendCamera.enabled = true; blendCamera.Render(); blendCamera.enabled = false; Debug.Log($"[CameraManager] 블렌드 시작 - Duration: {duration}s"); // 블렌딩 시작 - BlendAmount = 0 (이전 카메라 A만 보임) CameraBlendController.StartBlend(blendRenderTexture); // 한 프레임 대기 - 블렌딩이 적용된 상태로 렌더링되도록 yield return null; // 카메라 전환 (이제 BlendAmount=0이므로 A만 보임) SetImmediate(targetIndex); // 블렌드 진행: 0 → 1 (A에서 B로) float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); // 부드러운 이징 적용 (SmoothStep) t = t * t * (3f - 2f * t); CameraBlendController.BlendAmount = t; yield return null; } CameraBlendController.BlendAmount = 1f; // 블렌딩 종료 CameraBlendController.EndBlend(); Debug.Log("[CameraManager] 블렌드 완료"); isBlending = false; blendCoroutine = null; } 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 }