2025-12-02 20:38:32 +09:00

665 lines
20 KiB
C#

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<KeyCode, RawKey> _mapping;
static KeyMapping()
{
_mapping = new Dictionary<KeyCode, RawKey>(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<RawKey> rawKeys = new List<RawKey>();
[System.NonSerialized] private List<KeyCode> unityKeys = new List<KeyCode>();
[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<CameraPreset> cameraPresets = new List<CameraPreset>();
[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<StreamDeckServerManager>();
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<InputHandler>();
if (inputHandler == null)
{
inputHandler = gameObject.AddComponent<InputHandler>();
}
}
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<CameraPreset>();
}
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<CameraPreset, bool> 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<string, object>)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
}