마우스 기반 플레이어 시야 및 회전 제어 구조
목차
1. 시스템 요구 사항
플레이어는 마우스 입력에 따라 시점을 자유롭게 회전할 수 있어야 한다.
수평 회전은 제한 없이 가능해야 하며, 수직 회전은 일정 각도 범위 내에서 제한되어야 한다.
이는 카메라 뒤집힘과 과도한 상하 시야 왜곡을 방지하기 위함이다.
비행 상태에서는 지상 상태보다 더 넓은 수직 회전 범위를 허용해야 한다.
이동 시스템과 자연스럽게 연동되어야 하며, 비행 시 플레이어가 상하 방향을 더 넓게 바라볼 수 있어야 한다.
카메라 회전은 UI가 활성화된 상태에서는 작동하지 않아야 하며, 마우스 감도는 설정 UI의 슬라이더 값을 기반으로 동적으로 변경 가능해야 한다.
카메라 시스템은 이동 시스템과 결합되지만, 회전 계산 로직은 독립적으로 유지되어야 한다.
2. 설계 목표
- 수평/수직 회전 분리 누적
- 수직 회전 각도 제한
- 비행 상태에 따른 시야 범위 확장
- 설정 UI와 감도 연동
- 카메라 회전과 플레이어 회전 역할 분리
3. 흐름도
[Input(Update)]
▼
[SettingUi_.MouseSensitivity()]
▼
[CameraRotation()]
├─ Mouse X/Y 입력 수집
├─ 회전값 누적
├─ 상태 기반 수직 각도 제한
└─ transform.localEulerAngles 적용
입력은 Update에서 수집된다.
감도는 SettingUi_에서 동적으로 조회된다.
회전값은 누적 계산되며, 상태에 따라 수직 각도 제한 범위가 달라진다.
4. 구현
4.1. 싱글톤과 런타임 유지
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
else Destroy(this.gameObject);
}카메라 역시 플레이어와 동일하게 씬 전환 시 유지되어야 하므로 DontDestroyOnLoad를 사용한다.
중복 생성을 방지하기 위해 싱글톤 구조를 사용한다.
카메라는 게임 전체에서 단 하나의 시점 기준이므로, 다중 인스턴스는 허용되지 않는다.
4.2. Update에서 감도 동기화 및 회전 처리
void Update()
{
if (SettingUi_.instance != null)
rotationSpeed = SettingUi_.instance.MouseSensitivity();
CameraRotation();
}Update에서 회전 처리를 수행하는 이유는 마우스 입력이 프레임 단위로 갱신되기 때문이다.
FixedUpdate는 물리 기반 틱 호출이므로 시점 제어에는 적합하지 않다.
SettingUi_.instance.MouseSensitivity()를 매 프레임 호출하는 것은 설정 UI에서 슬라이더를 실시간으로 조정할 수 있게 하기 위함이며, Mathf.Lerp를 사용해 10f에서 600f까지 보간된 감도를 반환한다.
이 기능은 C#의 메서드 호출로, 싱글톤 인스턴스를 통해 글로벌 액세스를 제공하지만 커플링 위험이 있어서 의존성 주입으로 대체할 수 있다.
이 코드는 감도를 동적으로 적용하며, 게임플레이에서 플레이어가 설정을 변경할 때 즉시 시야 회전 속도가 변한다.
4.3. 마우스 회전 계산
void CameraRotation()
{
if (uiManager.uiAllDeactive)
{
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
rotationX += mouseX * rotationSpeed * Time.deltaTime;
rotationY += mouseY * rotationSpeed * Time.deltaTime;
...uiManager.uiAllDeactive 조건은 UiManager 싱글톤을 통해 UI 비활성화 상태를 확인하며, UI가 열릴 때 회전을 차단해 입력 충돌을 방지한다.
Input.GetAxis("Mouse X")와 Input.GetAxis("Mouse Y")는 유니티의 Input 클래스 메서드로, 마우스 움직임을 보간된 부동소수점 값으로 반환하며, GetAxisRaw보다 부드러운 입력을 제공하는 장점이 있지만, 단점은 보간으로 인해 약간의 지연이 발생할 수 있다는 점인데, 시야 제어의 자연스러움을 위해 선택했다.
rotationX와 rotationY를 누적하는 방식은 매 프레임 이전 회전에 덧붙여 연속적인 회전을 구현하며, Time.deltaTime을 곱해 프레임 독립성을 확보한다.
Time.deltaTime은 유니티의 정적 속성으로, 프레임 간 시간을 측정하며 고속/저속 기기에서 일관된 속도를 보장하는 장점이 있지만, 매우 낮은 프레임 시 부정확할 수 있다.
if (!PlayerController.instance.isFlying)
{
if (rotationY < -15) rotationY = -15;
else if (rotationY > 15) rotationY = 15;
}
else
{
if (rotationY < -80) rotationY = -80;
else if (rotationY > 80) rotationY = 80;
}
...CameraRotation에서 수직 회전 제한을 담당하는 부분이다.
수직 회전을 제한하는 이유는 카메라가 뒤집히거나 과도하게 상하로 회전하는 것을 방지하기 위함이다.
지상 상태에서는 ±15도로 제한한다.
이는 일반적인 3인칭 시점에서 과도한 상하 회전을 방지하기 위한 값이다.
비행 상태에서는 ±80도로 확장한다.
비행은 공간 이동이 자유롭기 때문에 상하 시야를 더 넓게 허용한다.
이 조건은 PlayerController의 상태를 참조하지만, 회전 계산 자체는 카메라 내부에서 수행한다.
transform.localEulerAngles = new Vector3(-rotationY, rotationX, 0);CameraRotation에서 최종 회전 적용을 담당하는 부분이다.
localEulerAngles를 사용하는 이유는 부모 오브젝트 기준 회전을 적용하기 위함이다.
카메라가 플레이어의 자식 구조라면, 월드 좌표가 아닌 로컬 회전을 적용하는 것이 자연스럽다.
수직 회전에 마이너스를 붙이는 이유는 마우스 Y축 입력과 카메라 상하 방향의 직관적 일치를 맞추기 위함이다.
Euler 방식은 구현이 직관적이지만, 극단적 회전에서 짐벌락 문제가 발생할 수 있다.
현재 시스템은 수직 회전 범위를 제한하고 있으므로 문제가 발생하지 않는다.
5. 개발 의도
이 카메라 시스템은 단순 회전 구현이 아니라, 이동 시스템과 상태 기반으로 연동되는 구조다.
지상과 비행 상태에 따라 수직 시야 제한을 다르게 적용함으로써 조작 체감이 자연스럽게 변화한다.
감도는 설정 UI와 실시간으로 연결되어 플레이어 설정이 즉시 반영된다.
카메라 회전은 입력 계층과 상태 계층을 참조하지만, 이동 계산과는 독립적으로 유지된다.
이는 역할 분리를 유지하기 위한 설계다.
전체적으로 이 시스템은 '입력 → 감도 적용 → 누적 회전 계산 → 상태 기반 제한 → 로컬 회전 적용' 파이프라인을 가진다.
