Implementing Accelerometer Control for Mobile Games
Tilt your phone as a gamepad — an intuitive control method for racing games, arcades, and mazes. Many developers integrate it in a day, then spend weeks polishing: smoothing latency, fighting drift, configuring dead zones, and calibrating. Especially acute are gyroscope drift issues and unstable readings across devices. We offer a turnkey implementation with guaranteed responsiveness and predictable behavior on all hardware. We'll evaluate your project in one day. Contact us to get a demo.
Proper sensor fusion gives 10x better control quality than raw accelerometer. A ready-made module cuts development costs by 2–3 times compared to in-house implementation — you save budget on R&D and focus on gameplay. According to the Apple Core Motion documentation, combining accelerometer, gyroscope, and magnetometer yields the best results.
Why Raw Accelerometer Doesn't Work
Raw accelerometer includes gravity. On a flat table: (x: 0, y: 0, z: -9.81). When you tilt the device, the gravity vector spreads across axes, breaking control. The correct source is Device Motion / Linear Acceleration — data without gravity. But it has noise and slow gyroscope drift. Sensor fusion combines accelerometer, gyroscope, and magnetometer to get a clean tilt angle. On iOS this is CoreMotion, on Android — SensorManager with Rotation Vector algorithm.
How to Avoid Gyroscope Drift?
Gyroscope drift arises from integrating angular velocity. The solution is to combine it with the accelerometer (complementary filter or Kalman filter). On iOS we use CMAttitude with xArbitraryZVertical, on Android — getRotationMatrixFromVector. For critical scenes we also involve the magnetometer.
Complementary Filter vs Kalman Filter
To smooth sensor data, two main approaches are used: complementary filter and Kalman filter. The complementary filter (alpha = 0.98 for gyroscope, 0.02 for accelerometer) is simple to implement and gives <5 ms latency on iOS. Kalman filter is 30% more accurate but more computationally expensive. We choose based on genre: for shooters — Kalman, for casual games — complementary.
| Parameter | Complementary | Kalman |
|---|---|---|
| Accuracy | High | Very high |
| Latency | <5 ms | ~10 ms |
| Complexity | Low | Medium |
| Performance | Fast | Demanding |
Implementation on iOS (Swift)
let motionManager = CMMotionManager() motionManager.deviceMotionUpdateInterval = 1.0 / 60.0 motionManager.startDeviceMotionUpdates( using: .xArbitraryZVertical, to: OperationQueue.main ) { [weak self] motion, _ in guard let motion = motion else { return } self?.applyTilt( pitch: Float(motion.attitude.pitch), roll: Float(motion.attitude.roll) ) } Implementation on Android (Kotlin)
private var baselineAttitude: FloatArray? = null private val currentRotationMatrix = FloatArray(16) // In SensorEventListener.onSensorChanged for TYPE_ROTATION_VECTOR: val rotationMatrix = FloatArray(9) SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) val orientationAngles = FloatArray(3) SensorManager.getOrientation(rotationMatrix, orientationAngles) val pitch = orientationAngles[1] val roll = orientationAngles[2] val calibratedPitch = pitch - (baselineAttitude?.get(0) ?: 0f) val calibratedRoll = roll - (baselineAttitude?.get(1) ?: 0f) gameEngine.setTilt(calibratedPitch, calibratedRoll) How to Perform Calibration?
Calibration fixes the neutral position at start or on button press. Without it, control will be offset.
fun calibrate() { baselineAttitude = floatArrayOf(currentPitch, currentRoll) } Save the baseline in SharedPreferences — to avoid recalibration on next launch.
Detailed calibration algorithm
- Capture neutral position (pitch=0, roll=0).
- Save baseline (average over 100 ms).
- Subtract baseline from current angles.
- Apply low-pass filter to the difference.
- Update baseline on each new launch.
Smoothing: Low-Pass Filter and Dead Zone
A simple exponential filter removes hand jitter. A dead zone of ±5° eliminates unintended movement.
struct LowPassFilter { var value: Float = 0 let alpha: Float mutating func update(_ newValue: Float) -> Float { value = alpha * newValue + (1 - alpha) * value return value } } func applyDeadZone(_ value: Float, threshold: Float = 0.087) -> Float { guard abs(value) > threshold else { return 0 } let sign: Float = value > 0 ? 1 : -1 return sign * (abs(value) - threshold) } Non-linear sensitivity (power function) gives precise control at small angles and fast response at large angles.
Comparison: iOS vs Android
| Parameter | iOS (CoreMotion) | Android (Rotation Vector) |
|---|---|---|
| Latency | ~5–10 ms | ~10–15 ms |
| Calibration | Built-in | Manual via baseline |
| Drift | Minimal | Compensated by magnetometer |
| Integration ease | High | Medium |
Parameters for Different Genres
| Genre | Alpha (low-pass) | Dead zone | Non-linearity |
|---|---|---|---|
| Racing | 0.3-0.4 | ±5° | 1.5 |
| Arcade | 0.2 | ±3° | 1.2 |
| Shooter | 0.6-0.7 | ±2° | 1.0 |
Integration Process
- Sensor initialization — 60 Hz.
- Data acquisition — pitch/roll, orientation correction.
- Calibration — neutral position.
- Filtering — low-pass.
- Dead zone — eliminate jitter.
- Non-linear sensitivity — angle mapping.
- Output to game engine.
What's Included
- Source code for the module (Swift, Kotlin, C#).
- Integration documentation.
- Device testing recommendations.
- Support during store submission.
- Genre-specific fine-tuning.
Timeline
Basic control — 3–5 working days. With genre polishing — 1–2 weeks. Contact us for an accurate estimate — over 10 years of experience guarantees quality. Get a demo and evaluate the controls.







