Unity 모바일 가이로스코프 조준 시스템 구현: 보정·스무딩·드리프트 대응
Unity C#에서 모바일 Gyroscope 센서로 FPS·TPS 조준을 구현하는 방법을 정리합니다. 센서 좌표 변환, 기준 자세 보정, Slerp 스무딩, 데드존·감도·드리프트 대응까지 실전 코드와 함께 설명합니다.

모바일 가이로스코프 조준은 기기의 회전값을 카메라 회전에 직접 적용하는 기능이지만 센서 좌표계와 게임 좌표계가 다르므로 보정 없이 연결하면 축이 뒤집히거나 화면 회전 시 조준 방향이 틀어진다. Unity에서는 기준 자세를 저장한 뒤 상대 회전을 계산하고 Quaternion.Slerp와 데드존으로 미세 떨림을 억제하는 방식이 안정적이다.
모바일 가이로스코프 조준 시스템의 구성
모바일 FPS·TPS의 자이로 조준은 터치 드래그를 완전히 대체하기보다 정밀 조준을 보조하는 입력으로 설계하는 경우가 많다. 사용자는 화면을 빠르게 돌릴 때는 터치를 사용하고 적의 머리 높이를 맞추는 작은 보정에는 기기 기울기를 사용한다.
구현 전에 입력 데이터를 다음 세 단계로 분리하면 디버깅과 튜닝이 쉬워진다.
Input.gyro.attitude에서 기기의 절대 회전 쿼터니언을 읽는다.- 조준 시작 시점의 회전을 기준 자세로 저장하고 현재 상대 회전을 구한다.
- 상대 회전에서 피치와 요를 추출해 감도, 데드존, 스무딩, 상하 각도 제한을 적용한다.

가이로스코프 조준은 왜 축이 뒤집히거나 흔들리는가?
가이로스코프 조준 문제는 대개 센서 값 자체보다 좌표계와 보정 기준을 혼동해서 발생한다. Gyroscope.attitude는 쿼터니언으로 전달되며 모바일 운영체제의 센서 좌표계와 Unity의 좌표계는 축 방향과 손잡이가 일치하지 않을 수 있다.
| 증상 | 주요 원인 | 대응 방법 |
|---|---|---|
| 좌우 회전 방향이 반대 | 축 부호 또는 좌표계 변환 오류 | 변환 쿼터니언과 축 부호를 검증한다. |
| 시작 직후 카메라가 튄다 | 현재 자세를 바로 월드 회전에 적용 | 조준 시작 시 기준 자세를 저장하고 상대 회전을 사용한다. |
| 정지 상태에서도 시점이 미세하게 움직인다 | 센서 노이즈와 손 떨림 | 데드존과 Slerp 스무딩을 적용한다. |
| 세로·가로 화면에서 조작감이 다르다 | 화면 방향 변화를 반영하지 않음 | 지원 방향별로 보정값을 테스트하고 입력을 재보정한다. |
상대 회전은 기준 쿼터니언 q_ref와 현재 쿼터니언 q_now의 역회전을 곱해 계산한다.
이 값은 “조준을 시작한 순간에서 기기가 얼마나 회전했는가”를 뜻한다. 카메라에 절대 자세를 넣는 것보다 플레이어가 화면을 잡은 자세에 자연스럽게 반응한다.
Unity C#에서 가이로스코프 조준을 어떻게 구현할까?
아래 예제는 Unity 2022.3 LTS 기준의 Input.gyro API를 사용한다. 기존 Input Manager 기반 프로젝트에서 빠르게 적용하기 좋으며 새 프로젝트에서 Unity Input System을 사용하더라도 보정·필터링 구조는 동일하다.
1. 센서를 활성화하고 기준 자세를 저장한다
GyroAimController를 카메라 또는 카메라 피벗 오브젝트에 붙인다. cameraPivot은 요 회전을 담당하고 cameraTransform은 피치 회전을 담당하도록 분리했다.
using UnityEngine;
public class GyroAimController : MonoBehaviour
{
[SerializeField] private Transform cameraPivot;
[SerializeField] private Transform cameraTransform;
[SerializeField] private float sensitivity = 1.2f;
[SerializeField] private float smoothingSpeed = 14f;
[SerializeField] private float deadZoneDegrees = 0.08f;
[SerializeField] private float minPitch = -75f;
[SerializeField] private float maxPitch = 75f;
private Gyroscope gyro;
private Quaternion referenceAttitude;
private float yaw;
private float pitch;
private bool isGyroEnabled;
private void Start()
{
gyro = Input.gyro;
gyro.enabled = SystemInfo.supportsGyroscope;
isGyroEnabled = gyro.enabled;
if (isGyroEnabled)
{
Recenter();
}
}
public void Recenter()
{
if (!isGyroEnabled)
{
return;
}
referenceAttitude = ToUnityRotation(gyro.attitude);
}
}
SystemInfo.supportsGyroscope 확인은 필수다. 에디터, 일부 저가형 기기, 센서 권한이 제한된 환경에서는 자이로가 없을 수 있으므로 터치 조준을 기본 입력으로 유지해야 한다.
2. 모바일 센서 회전을 Unity 회전으로 변환한다
모바일 센서 쿼터니언을 Unity 공간에 맞추는 대표적인 변환은 다음과 같다.
private static Quaternion ToUnityRotation(Quaternion attitude)
{
return new Quaternion(
attitude.x,
attitude.y,
-attitude.z,
-attitude.w);
}
이 변환은 널리 쓰이는 시작점이지만 모든 프로젝트의 카메라 계층과 화면 방향에 완전히 동일하게 적용되지는 않는다. 실제 기기에서 위·아래·왼쪽·오른쪽으로 각각 천천히 돌려 보며 축과 부호를 검증해야 한다. 한 축이 반대라면 해당 축의 부호만 바꾸고 여러 축을 무작정 뒤집는 방식은 피한다.
3. 상대 회전에 데드존과 Slerp를 적용한다
Update에 아래 로직을 추가한다. 상대 회전을 오일러 각도로 읽은 다음 180도 경계를 넘는 값을 음수로 정규화한다.
private void Update()
{
if (!isGyroEnabled)
{
return;
}
Quaternion currentAttitude = ToUnityRotation(gyro.attitude);
Quaternion relativeRotation = Quaternion.Inverse(referenceAttitude) * currentAttitude;
Vector3 euler = relativeRotation.eulerAngles;
float rawPitch = NormalizeAngle(euler.x);
float rawYaw = NormalizeAngle(euler.y);
if (Mathf.Abs(rawPitch) < deadZoneDegrees)
{
rawPitch = 0f;
}
if (Mathf.Abs(rawYaw) < deadZoneDegrees)
{
rawYaw = 0f;
}
float targetPitch = Mathf.Clamp(-rawPitch * sensitivity, minPitch, maxPitch);
float targetYaw = rawYaw * sensitivity;
float interpolation = 1f - Mathf.Exp(-smoothingSpeed * Time.deltaTime);
pitch = Mathf.Lerp(pitch, targetPitch, interpolation);
yaw = Mathf.Lerp(yaw, targetYaw, interpolation);
cameraPivot.localRotation = Quaternion.Euler(0f, yaw, 0f);
cameraTransform.localRotation = Quaternion.Euler(pitch, 0f, 0f);
}
private static float NormalizeAngle(float angle)
{
return angle > 180f ? angle - 360f : angle;
}
보간 계수는 프레임 레이트에 덜 의존하도록 지수 감쇠 형태로 계산했다.
여기서 k는 smoothingSpeed, Δt는 Time.deltaTime이다. 단순히 speed * deltaTime을 쓰는 방식보다 서로 다른 프레임 레이트에서 체감이 일관적이다.

감도와 스무딩 값은 어떻게 조정해야 할까?
가이로 감도는 화면 크기보다 플레이어가 기기를 잡는 자세와 게임의 전투 거리 영향을 크게 받는다. 따라서 하나의 고정값보다 설정 메뉴에서 개별 조정할 수 있게 만드는 편이 좋다.
| 파라미터 | 권장 시작값 | 높일 때의 효과 | 주의점 |
|---|---|---|---|
sensitivity | 0.8~1.5 | 작은 기울기에도 빠르게 조준 | 과하면 손 떨림이 증폭된다. |
smoothingSpeed | 10~18 | 입력 지연이 줄어든다 | 너무 높으면 노이즈가 보인다. |
deadZoneDegrees | 0.05~0.15 | 미세 흔들림 제거 | 너무 크면 미세 조준이 둔해진다. |
minPitch, maxPitch | -75~75 | 상하 시야 범위 확대 | 90도에 가까우면 방향 감각이 나빠질 수 있다. |
저격 조준경처럼 확대 배율이 높은 UI에서는 기본 시점보다 sensitivity를 낮추는 것이 일반적이다. 예를 들어 4배율 스코프에 0.45f의 보정 계수를 곱하면 작은 손목 움직임으로 정밀하게 조준할 수 있다.
드리프트와 재보정 UX를 설계하는 방법
attitude 기반 상대 회전은 기준 자세를 저장하므로 자이로 값 누적만으로 회전하는 방식보다 드리프트 영향이 작다. 다만 플레이어가 자세를 바꾸거나 화면 회전 상태가 바뀌면 현재 중심이 불편하게 느껴질 수 있다.
재보정은 다음 조건에서 제공하는 것이 좋다.
- 설정 화면에서
자이로 중심 맞춤버튼을 누른 경우 - 조준 버튼을 길게 눌러 중심 맞춤을 요청한 경우
- 화면 방향이 변경되어 입력 축을 다시 확인해야 하는 경우
- 앱이 일시 정지 상태에서 복귀한 경우
재보정은 현재 방향을 화면 중앙으로 되돌리는 기능이므로 카메라가 향하는 월드 방향까지 갑자기 변경해서는 안 된다. 위 예제처럼 referenceAttitude만 현재 센서 값으로 교체하면 된다.
성능과 입력 처리에서 주의할 점
자이로 값 읽기와 쿼터니언 연산은 일반적인 모바일 게임에서 비용이 매우 작다. 성능 문제는 센서 처리보다 카메라 후처리, UI 리빌드, 조준경 렌더 텍스처에서 발생하는 경우가 많다.
다만 다음 원칙은 지키는 편이 좋다.
- 센서 값은
Update에서 읽고 물리 이동은FixedUpdate에서 별도로 처리한다. - 자이로 비활성 상태에서는 즉시 반환해 불필요한 회전을 적용하지 않는다.
- 입력 설정값은
PlayerPrefs또는 프로젝트의 설정 저장 시스템에 보관한다. - 기기별 센서 차이를 고려해 Android와 iOS 실기기에서 축 방향, 세로·가로 모드를 모두 검증한다.
자주 묻는 질문 (FAQ)
Unity Input.gyro와 새 Input System 중 무엇을 써야 하나요?
기존 프로젝트에서 자이로 조준만 추가한다면 Input.gyro가 간단하다. 입력 재바인딩, 여러 기기 입력 통합, 테스트 자동화가 중요한 프로젝트라면 새 Input System의 센서 디바이스 지원을 검토하는 편이 낫다.
자이로 조준을 켜면 터치 조준은 꺼야 하나요?
아니다. 일반적으로 터치는 큰 회전, 자이로는 미세 조준을 맡긴다. 두 입력의 최종 요·피치 값을 합치되 자이로 감도는 조준 상태와 스코프 배율에 따라 별도로 조정한다.
가이로스코프가 없는 기기는 어떻게 처리하나요?
SystemInfo.supportsGyroscope가 false이면 자이로 토글을 비활성화하고 터치 조준만 제공한다. 가속도계만으로 자이로 조준을 대체하면 빠른 회전 추적과 정밀도가 떨어질 수 있다.
정리
모바일 가이로스코프 조준의 핵심은 절대 센서 회전을 카메라에 바로 넣지 않고 기준 자세 대비 상대 회전으로 변환하는 것이다. 여기에 좌표계 검증, 데드존, 프레임 레이트 독립 스무딩, 재보정 UX를 더하면 FPS와 TPS에서 사용할 수 있는 안정적인 자이로 조준 시스템을 만들 수 있다.



