Gyroscope Camera Controller
Controls camera rotation using the device gyroscope sensor with smoothing and sensitivity settings. Includes calibration, axis locking, and fallback for devices without a gyroscope.
How to Use
Attach the GyroscopeCameraController script to your Main Camera
Adjust sensitivity and smoothSpeed in the inspector
Lock any axes you do not want the gyroscope to control
Enable useTouchFallback so the camera still works in the editor or on devices without a gyroscope
Call Calibrate() at runtime to reset the neutral orientation
Features
- Device gyroscope-based camera rotation
- Smoothing and sensitivity settings
- Calibration/reset function
- Optional axis locking
- Fallback for devices without gyroscope
When to Use This
Ideal for mobile AR-like experiences, 360-degree viewers, VR-lite games, and any mobile 3D game that wants device tilt to control the camera. Use for immersive exploration games, panoramic photo viewers, or as an alternative look control in mobile FPS games.
Common Mistakes
The gyroscope coordinate system differs from Unity's — the script handles this conversion, but if you modify the rotation math, test on both iOS and Android as gyro behavior varies. Always call Calibrate() when the player starts a level or the neutral position will be wherever the device was at app launch. The touch fallback sensitivity needs separate tuning from gyro sensitivity since they use completely different input ranges.
Source Code
using UnityEngine;
public class GyroscopeCameraController : MonoBehaviour
{
[Header("Gyroscope Settings")]
[SerializeField] private float sensitivity = 1f;
[SerializeField] private float smoothSpeed = 5f;
[Header("Axis Locking")]
[SerializeField] private bool lockX;
[SerializeField] private bool lockY;
[SerializeField] private bool lockZ;
[Header("Fallback")]
[SerializeField] private bool useTouchFallback = true;
[SerializeField] private float touchSensitivity = 0.2f;
public bool GyroAvailable { get; private set; }
public bool IsCalibrated { get; private set; }
private Gyroscope gyro;
private Quaternion calibrationOffset = Quaternion.identity;
private Quaternion targetRotation;
private Quaternion initialRotation;
// Conversion from gyro space to Unity space
private static readonly Quaternion gyroFix = new Quaternion(0f, 0f, 1f, 0f);
private Vector2 touchDelta;
private float fallbackYaw;
private float fallbackPitch;
private void Start()
{
initialRotation = transform.rotation;
if (SystemInfo.supportsGyroscope)
{
gyro = Input.gyro;
gyro.enabled = true;
GyroAvailable = true;
Calibrate();
}
else
{
GyroAvailable = false;
Debug.Log("Gyroscope not available. Using touch fallback.");
}
}
private void Update()
{
if (GyroAvailable)
{
UpdateGyro();
}
else if (useTouchFallback)
{
UpdateTouchFallback();
}
}
private void UpdateGyro()
{
Quaternion rawGyro = gyro.attitude;
Quaternion convertedRotation = ConvertGyroRotation(rawGyro);
Quaternion calibrated = calibrationOffset * convertedRotation;
targetRotation = calibrated;
ApplyRotation(targetRotation);
}
private Quaternion ConvertGyroRotation(Quaternion gyroAttitude)
{
return Quaternion.Euler(90f, 0f, 0f) * new Quaternion(
gyroAttitude.x, gyroAttitude.y,
-gyroAttitude.z, -gyroAttitude.w);
}
private void UpdateTouchFallback()
{
if (Input.touchCount == 1)
{
Touch touch = Input.GetTouch(0);
if (touch.phase == TouchPhase.Moved)
{
touchDelta = touch.deltaPosition;
fallbackYaw += touchDelta.x * touchSensitivity;
fallbackPitch -= touchDelta.y * touchSensitivity;
fallbackPitch = Mathf.Clamp(fallbackPitch, -80f, 80f);
}
}
// Mouse fallback for editor
if (Input.GetMouseButton(1))
{
fallbackYaw += Input.GetAxis("Mouse X") * touchSensitivity * 10f;
fallbackPitch -= Input.GetAxis("Mouse Y") * touchSensitivity * 10f;
fallbackPitch = Mathf.Clamp(fallbackPitch, -80f, 80f);
}
targetRotation = Quaternion.Euler(fallbackPitch, fallbackYaw, 0f);
ApplyRotation(targetRotation);
}
private void ApplyRotation(Quaternion rotation)
{
Vector3 euler = rotation.eulerAngles;
if (lockX) euler.x = initialRotation.eulerAngles.x;
if (lockY) euler.y = initialRotation.eulerAngles.y;
if (lockZ) euler.z = initialRotation.eulerAngles.z;
Quaternion final_rot = Quaternion.Euler(euler);
if (sensitivity != 1f)
{
final_rot = Quaternion.Slerp(initialRotation, final_rot, sensitivity);
}
transform.rotation = Quaternion.Slerp(
transform.rotation, final_rot, Time.deltaTime * smoothSpeed);
}
public void Calibrate()
{
if (!GyroAvailable) return;
Quaternion currentGyro = ConvertGyroRotation(gyro.attitude);
calibrationOffset = Quaternion.Inverse(currentGyro) * initialRotation;
IsCalibrated = true;
}
public void SetSensitivity(float value)
{
sensitivity = Mathf.Clamp(value, 0.1f, 3f);
}
public void SetSmoothSpeed(float value)
{
smoothSpeed = Mathf.Clamp(value, 1f, 20f);
}
private void OnDisable()
{
if (gyro != null)
gyro.enabled = false;
}
private void OnEnable()
{
if (gyro != null)
gyro.enabled = true;
}
}