Fix : 풀스크린 에디터 버그 제거 및 카메라 조작감 개선

This commit is contained in:
user 2026-01-12 23:10:15 +09:00
parent 16625c95fa
commit c1ae4b4379
3 changed files with 398 additions and 160 deletions

View File

@ -74,7 +74,9 @@ namespace FullscreenEditor {
/// <param name="containerWindow">The ContainerWindow to freeze the repaints.</param>
/// <param name="freeze">Wheter to freeze or unfreeze the container.</param>
protected void SetFreezeContainer(ContainerWindow containerWindow, bool freeze) {
containerWindow.InvokeMethod("SetFreezeDisplay", freeze);
// SetFreezeDisplay was removed in Unity 6 - skip if not available
if (containerWindow.HasMethod("SetFreezeDisplay", new[] { typeof(bool) }))
containerWindow.InvokeMethod("SetFreezeDisplay", freeze);
}
/// <summary>Method that will be called just before creating the ContainerWindow for this fullscreen.</summary>

View File

@ -4,6 +4,7 @@ using UnityRawInput;
using System.Linq;
using Unity.Cinemachine;
using Streamingle;
using KindRetargeting;
public class CameraManager : MonoBehaviour, IController
{
@ -130,6 +131,11 @@ public class CameraManager : MonoBehaviour, IController
public HotkeyCommand hotkey;
[System.NonSerialized] public bool isEditingHotkey = false;
// 프리셋별 초기 상태 저장
[System.NonSerialized] public Vector3 savedPosition;
[System.NonSerialized] public Quaternion savedRotation;
[System.NonSerialized] public bool hasSavedState = false;
public CameraPreset(CinemachineCamera camera)
{
virtualCamera = camera;
@ -138,6 +144,21 @@ public class CameraManager : MonoBehaviour, IController
}
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
@ -148,31 +169,49 @@ public class CameraManager : MonoBehaviour, IController
#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;
[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;
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 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;
#endregion
#region Properties
@ -186,15 +225,24 @@ public class CameraManager : MonoBehaviour, IController
InitializeInputHandler();
InitializeRawInput();
InitializeCameraPresets();
// StreamDeckServerManager 찾기
streamDeckManager = FindObjectOfType<StreamDeckServerManager>();
streamDeckManager = FindAnyObjectByType<StreamDeckServerManager>();
if (streamDeckManager == null)
{
Debug.LogWarning("[CameraManager] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다.");
}
}
private void Start()
{
// Start에서 아바타 머리 다시 찾기 (다른 스크립트들이 초기화된 후)
if (useAvatarHeadAsTarget && avatarHeadTransform == null)
{
FindAvatarHead();
}
}
private void OnDestroy()
{
if (RawInput.IsRunning)
@ -263,16 +311,71 @@ public class CameraManager : MonoBehaviour, IController
{
preset.hotkey.InitializeUnityKeys();
}
Set(0);
// 초기 카메라 상태 저장
if (currentCamera != null && !isInitialStateSet)
// 모든 프리셋의 초기 상태 저장
foreach (var preset in cameraPresets.Where(p => p?.IsValid() == true))
{
initialPosition = currentCamera.transform.position;
initialRotation = currentCamera.transform.rotation;
isInitialStateSet = true;
if (!preset.hasSavedState)
{
preset.SaveCurrentState();
}
}
// 아바타 머리 찾기
FindAvatarHead();
Set(0);
}
private void FindAvatarHead()
{
if (!useAvatarHeadAsTarget) return;
// CustomRetargetingScript를 가진 아바타 찾기
var retargetingScripts = FindObjectsByType<CustomRetargetingScript>(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<Animator>();
}
if (animator != null)
{
avatarHeadTransform = animator.GetBoneTransform(HumanBodyBones.Head);
if (avatarHeadTransform != null)
{
Debug.Log($"[CameraManager] 아바타 머리를 회전 중심점으로 설정: {avatarHeadTransform.name}");
return;
}
}
}
Debug.LogWarning("[CameraManager] 활성화된 아바타의 Head 본을 찾을 수 없습니다. 수동 타겟을 사용하거나 원점을 사용합니다.");
}
/// <summary>
/// 현재 회전 중심점을 반환합니다.
/// 우선순위: 아바타 머리 > 수동 타겟 > 원점
/// </summary>
private Vector3 GetRotationCenter()
{
if (useAvatarHeadAsTarget && avatarHeadTransform != null)
{
return avatarHeadTransform.position;
}
else if (manualRotationTarget != null)
{
return manualRotationTarget.position;
}
return Vector3.zero;
}
@ -312,12 +415,36 @@ public class CameraManager : MonoBehaviour, IController
#endregion
#region Camera Controls
/// <summary>
/// 현재 카메라 위치에서 각도와 거리를 초기화합니다.
/// </summary>
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;
var virtualCamera = currentCamera;
if (virtualCamera == null) return;
if (currentCamera == null) return;
// Alt+Q로 초기 위치로 복원
if (Input.GetKey(KeyCode.LeftAlt) && Input.GetKeyDown(KeyCode.Q))
@ -326,141 +453,166 @@ public class CameraManager : MonoBehaviour, IController
return;
}
HandleRotation(virtualCamera);
HandlePanning(virtualCamera);
HandleZooming(virtualCamera);
HandleOrbiting(virtualCamera);
HandleInput();
UpdateCameraPosition();
}
private void HandleRotation(CinemachineCamera virtualCamera)
private void HandleInput()
{
Transform cameraTransform = virtualCamera.transform;
if (inputHandler.IsRightMouseHeld())
// 입력 우선순위 처리: Orbit > AltRightZoom > Zoom > Rotation > Panning
if (inputHandler.IsOrbitActive())
{
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;
}
HandleOrbiting();
}
else if (inputHandler.IsCtrlRightZoomActive())
{
HandleCtrlRightZoom();
}
else if (inputHandler.IsZoomActive())
{
HandleDragZoom();
}
else if (inputHandler.IsRightMouseHeld())
{
HandleRotation();
}
else if (inputHandler.IsMiddleMouseHeld())
{
HandlePanning();
}
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;
// 휠 줌은 항상 처리
HandleWheelZoom();
}
/// <summary>
/// 우클릭 드래그: 수평 + 수직 회전 (현재 비활성화 - Alt+우클릭 사용)
/// </summary>
private void HandleRotation()
{
// 우클릭만으로는 회전하지 않음 - Alt+우클릭(Orbit)으로 대체
}
/// <summary>
/// Alt + 우클릭: 자유 궤도 회전 (X, Y축)
/// </summary>
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);
}
/// <summary>
/// 휠클릭 드래그: 패닝 (초점 이동)
/// </summary>
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;
}
/// <summary>
/// 마우스 휠: 줌
/// </summary>
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);
}
/// <summary>
/// Ctrl + 좌클릭 드래그: 줌
/// </summary>
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);
}
/// <summary>
/// Ctrl + 우클릭 드래그: 줌 (밀기 = 확대, 당기기 = 축소)
/// </summary>
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);
}
/// <summary>
/// 스무딩을 적용하여 카메라 위치 업데이트
/// </summary>
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
{
isRotating = false;
horizontalAngle = targetHorizontalAngle;
verticalAngle = targetVerticalAngle;
currentDistance = targetDistance;
focusPoint = targetFocusPoint;
}
}
private void HandlePanning(CinemachineCamera virtualCamera)
{
if (!inputHandler.IsMiddleMouseHeld()) return;
// 구면 좌표계에서 카메라 위치 계산
float horizontalRad = horizontalAngle * Mathf.Deg2Rad;
float verticalRad = verticalAngle * Mathf.Deg2Rad;
Vector2 panDelta = inputHandler.GetLookDelta();
if (panDelta.sqrMagnitude < float.Epsilon) return;
Vector3 offset = new Vector3(
Mathf.Sin(horizontalRad) * Mathf.Cos(verticalRad),
Mathf.Sin(verticalRad),
Mathf.Cos(horizontalRad) * Mathf.Cos(verticalRad)
) * currentDistance;
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;
currentCamera.transform.position = focusPoint + offset;
currentCamera.transform.LookAt(focusPoint);
}
private void RestoreInitialCameraState()
{
if (!isInitialStateSet || currentCamera == null) return;
if (currentPreset == null || !currentPreset.hasSavedState) return;
currentCamera.transform.position = initialPosition;
currentCamera.transform.rotation = initialRotation;
// 회전 중심점 초기화
rotationCenter = new Vector3(0f, initialPosition.y, 0f);
currentPreset.RestoreSavedState();
// 각도 상태 다시 초기화
InitializeOrbitState();
}
#endregion
@ -494,15 +646,18 @@ public class CameraManager : MonoBehaviour, IController
currentPreset = newPreset;
UpdateCameraPriorities(newPreset.virtualCamera);
// 오빗 상태 초기화 (각도, 거리 계산)
InitializeOrbitState();
OnCameraChanged?.Invoke(oldPreset, newPreset);
// 스트림덱에 카메라 변경 알림 전송
if (streamDeckManager != null)
{
streamDeckManager.NotifyCameraChanged();
}
Debug.Log($"[CameraManager] 카메라 전환 완료: {newCameraName}");
}

View File

@ -8,14 +8,28 @@ public class InputHandler : MonoBehaviour
private bool isMiddleMouseHeld;
private bool isOrbitActive;
private bool isZoomActive;
private bool isCtrlRightZoomActive;
// 현재 활성화된 입력 모드 (충돌 방지)
private InputMode currentMode = InputMode.None;
private enum InputMode
{
None,
Orbit, // Alt + 우클릭 또는 Alt + 좌클릭
Zoom, // Ctrl + 좌클릭
CtrlRightZoom, // Ctrl + 우클릭
Rotation, // 우클릭
Pan // 휠클릭
}
// 카메라 컨트롤 시스템 참조
private CameraControlSystem cameraControlSystem;
private void Start()
{
// CameraControlSystem 찾기
cameraControlSystem = FindObjectOfType<CameraControlSystem>();
cameraControlSystem = FindAnyObjectByType<CameraControlSystem>();
if (cameraControlSystem == null)
{
Debug.LogWarning("[InputHandler] CameraControlSystem을 찾을 수 없습니다.");
@ -24,11 +38,72 @@ public class InputHandler : MonoBehaviour
private void Update()
{
// 마우스 버튼 상태 업데이트
isRightMouseHeld = Input.GetMouseButton(1);
isMiddleMouseHeld = Input.GetMouseButton(2);
isOrbitActive = Input.GetKey(KeyCode.LeftAlt) && Input.GetMouseButton(0); // Alt + 좌클릭으로 궤도 회전
isZoomActive = Input.GetKey(KeyCode.LeftControl) && Input.GetMouseButton(0); // Ctrl + 좌클릭으로 줌
// 마우스 버튼 Raw 상태
bool leftMouse = Input.GetMouseButton(0);
bool rightMouse = Input.GetMouseButton(1);
bool middleMouse = Input.GetMouseButton(2);
bool altKey = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt);
bool ctrlKey = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
// 모든 마우스 버튼이 해제되면 모드 리셋
if (!leftMouse && !rightMouse && !middleMouse)
{
currentMode = InputMode.None;
}
// 입력 우선순위에 따라 모드 결정 (이미 활성화된 모드가 없을 때만)
if (currentMode == InputMode.None)
{
if (altKey && (rightMouse || leftMouse))
{
currentMode = InputMode.Orbit;
}
else if (ctrlKey && rightMouse)
{
currentMode = InputMode.CtrlRightZoom;
}
else if (ctrlKey && leftMouse)
{
currentMode = InputMode.Zoom;
}
else if (rightMouse)
{
currentMode = InputMode.Rotation;
}
else if (middleMouse)
{
currentMode = InputMode.Pan;
}
}
// 현재 모드에 따라 상태 설정
isOrbitActive = (currentMode == InputMode.Orbit) && (rightMouse || leftMouse) && altKey;
isZoomActive = (currentMode == InputMode.Zoom) && leftMouse && ctrlKey;
isCtrlRightZoomActive = (currentMode == InputMode.CtrlRightZoom) && rightMouse && ctrlKey;
isRightMouseHeld = (currentMode == InputMode.Rotation) && rightMouse;
isMiddleMouseHeld = (currentMode == InputMode.Pan) && middleMouse;
// 모드가 해제되면 None으로 전환
if (currentMode == InputMode.Orbit && ((!rightMouse && !leftMouse) || !altKey))
{
currentMode = InputMode.None;
}
else if (currentMode == InputMode.CtrlRightZoom && (!rightMouse || !ctrlKey))
{
currentMode = InputMode.None;
}
else if (currentMode == InputMode.Zoom && (!leftMouse || !ctrlKey))
{
currentMode = InputMode.None;
}
else if (currentMode == InputMode.Rotation && !rightMouse)
{
currentMode = InputMode.None;
}
else if (currentMode == InputMode.Pan && !middleMouse)
{
currentMode = InputMode.None;
}
// 마우스 위치 업데이트
lastMousePosition = Input.mousePosition;
@ -39,6 +114,12 @@ public class InputHandler : MonoBehaviour
public bool IsMiddleMouseHeld() => isMiddleMouseHeld;
public bool IsOrbitActive() => isOrbitActive;
public bool IsZoomActive() => isZoomActive;
public bool IsCtrlRightZoomActive() => isCtrlRightZoomActive;
/// <summary>
/// 현재 어떤 입력 모드도 활성화되지 않았는지 확인합니다.
/// </summary>
public bool IsIdle() => currentMode == InputMode.None;
public Vector2 GetLookDelta()
{