IOS/Swift: Simulate 2D Object Physics with Device Motion

Itsuki
6 min readOct 8, 2023

IOS devices such as iPhones and iPads come with built-in motion sensors that are capable of detecting changes in acceleration, rotation, and magnetic field. It is used in all kinds of applications such as fitness tracking, health-related matters, utilities, games, and more.

In this article, I will show you how to simulate a 2D object, specifically acceleration and rotation, based on the motion of device. We will be using the Core Motion Framework, a framework for processing accelerometer, gyroscope, pedometer, and environment-related events provided by Apple.

Before we start, make sure you have a REAL IOS Device. Motion Sensors are not available through simulators.

Getting Start

Let’s start off with an empty Game Template and choose SpriteKit for Game Technology and Select Integrate GameplayKit.

I know, we are not actually building a game. However, this will make our life much easier for two reason.

  1. We can easily create object with physics
  2. CMMotionManager will not notify you when it updates the motion related data. You must explicitly check it when you need it. However, SKScene provide this wonderful func update(_ currentTime: TimeInterval) {} that is called every fame and we can override it and get the data we need within it.

We will not be creating the scene from the SpriteKit Scene file so delete those. Rename GameScene to AccelerometerGameScene and ou should only be left with the following in your root folder.

--AppDelegate
--AccelerometerGameScene(swift file, not the scene file)
--GameViewController
--Main
--LaunchScreen
--Assests

Also remove all the pre-written code from the GameViewController and GameScene file.

Accelerometer

Set Up GameScene

We will first simulate a ball moving based on the tilt of the device.

The basic idea here is that we will use the Accelerometer to get the acceleration in the x and y direction and set it to be the gravity of our game world.

First of all, just so that our object won’t fall out of the scene, let’s create a boundary. To differentiate it between the object, we will give those different categoryBitMasks, a property defining the type of object when considering collisions.

Let’s define an enum CollisionType so that we can refer to it later also instead of just hard coding all the numbers.

enum CollisionType: UInt32 {
case object = 1
case wall = 2
}

In your AccelerometerGameScene.swift , add the following functions.

override func didMove(to view: SKView) {
setUpBounds()
}


private func setUpBounds() {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)
self.physicsBody?.isDynamic = false
self.physicsBody?.categoryBitMask = CollisionType.wall.rawValue
}

This will create a boundary over the edges of the scene on all sides.

Now, let’s add the ball to the scene.

override func didMove(to view: SKView) {
setUpBounds()
createBall()
}

private func createBall() {
// Create shape node to use during mouse interaction
let w = (self.size.width + self.size.height) * 0.02
let ball = SKShapeNode.init(circleOfRadius: w)
ball.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
ball.zPosition = 1
ball.strokeColor = SKColor.green
ball.physicsBody = SKPhysicsBody(circleOfRadius: w)
ball.physicsBody?.allowsRotation = false
ball.physicsBody?.linearDamping = 0.5

ball.physicsBody?.isDynamic = true
ball.physicsBody?.categoryBitMask = CollisionType.object.rawValue
ball.physicsBody?.collisionBitMask = CollisionType.wall.rawValue

self.addChild(ball)

}

If you run the app now, you will notice that the ball is falling. This is because the scene’s physics world has a default gravity roughly equivalent to that of the Earth. Since we only want the ball fall down when the user tilts their device down, add the line below somewhere in func didMove(to:) .

physicsWorld.gravity = .zero

Also, you might notice that the screen is rotating when you tilt your device. We will limit our device interface to be portrait only to eliminate this behavior. Add the following to GameViewController.swift .

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}

Set Up Accelerometer

Before we move onto the code part of the sensor, let’s first take a look at how the coordinate axes are defined on the devices.

As you can see, since we are simulating in 2D, we will only need acceleration in the x and y axis.

To use the Core Motion Framework I mentioned above, we will be using a class called CMMotionManager, an object for starting and managing motion services.

import CoreMotion to your AccelerometerGameScene.swift , and add the following property to your class AccelerometerGameScene .

private var motionManager: CMMotionManager!

And then in your func didMove(to:) create the Manager object and ask it to start collecting accelerometer data.

override func didMove(to view: SKView) {
motionManager = CMMotionManager()
motionManager.startAccelerometerUpdates()

self.physicsWorld.gravity = .zero

setUpBounds()
createBall()
}

The last thing to do is to sample motion data inside our update() method, checking to see what the current tilt data is.

override func update(_ currentTime: TimeInterval) {
if let accelerometerData = motionManager.accelerometerData {
physicsWorld.gravity = CGVector(dx: accelerometerData.acceleration.x * 7, dy: accelerometerData.acceleration.y * 7)
}
}

You can set the constant to whatever you like, it will just change the speed of the object movement.

Run the app and you will see the ball moving around as you tilt the device!

You can’t really see me tilting my device but you get the idea!

Gyroscope

To simulate rotations with gyroscope data, we only need to make couple minor adjustments to the code above.

First, in your func didMove(to:) instead of starting collecting accelerometer data, we will start gyroscope.

override func didMove(to view: SKView) {
motionManager = CMMotionManager()
motionManager.startGyroUpdates()

self.physicsWorld.gravity = .zero
setUpBounds()
createBall()
}

We will also allow object rotation. Of course, you won’t notice any rotation for a circle, so I change the object to a square.

let w = (self.size.width + self.size.height) * 0.03
let square = SKShapeNode.init(rectOf: CGSize(width: w, height: w), cornerRadius: w*0.2)
square.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
square.zPosition = 1
square.strokeColor = SKColor.red
square.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: w, height: w))
square.physicsBody?.allowsRotation = true
square.physicsBody?.linearDamping = 0.5

square.physicsBody?.isDynamic = true
square.physicsBody?.categoryBitMask = CollisionType.object.rawValue
square.physicsBody?.collisionBitMask = CollisionType.wall.rawValue

self.addChild(square)

Lastly, change the func update() to rotate the object based on the rotation rate.

override func update(_ currentTime: TimeInterval) {
if let gyroData = motionManager.gyroData {
let rotateAction = SKAction.rotate(byAngle: gyroData.rotationRate.z * 0.1, duration: motionManager.gyroUpdateInterval)
square.run(rotateAction)

}

}

You might notice that for rotation, we are actually using the data for z axis instead of x and y like we did for accelerometer. If you head back to the coordinate axes image above, since our scene is fully 2D, we will not care about any angular acceleration alone x and y axis.

That’s all the changes we have to make. If you run your app now, you should see your square rotating as you rotate your device.

Use Processed Data

I have showed you how you can simulate the gravity and rotation separately using raw data in 2D. If we try to do the same thing in 3D, you might notice that the raw values may have bias that might adversely affect the usage of the data. This is when we will want to use the processed values provided by Core Motion Framework. For example, a processed accelerometer value reflects only the acceleration caused by the user and not the acceleration caused by gravity.

Here is a quick example on how to use processed accelerometer and gyroscope data in place of the raw.

Within the GameScene.swift:

func didMove(to view: SKView) {}

// ...
motionManager.startDeviceMotionUpdates()
// ...

and in func update(_ currentTime: TimeInterval) {}



if let data = motionManager.deviceMotion {
// for rotation data
let rotateAction = SKAction.rotate(byAngle: data.rotationRate.z * 0.1, duration: motionManager.gyroUpdateInterval)
// in place of
// let rotateAction = SKAction.rotate(byAngle: gyroData.rotationRate.z * 0.1, duration: motionManager.gyroUpdateInterval)


// for acceleration data
physicsWorld.gravity = CGVector(dx: data.userAcceleration.x * 7, dy: data.userAcceleration.y * 7)
// in place of
// physicsWorld.gravity = CGVector(dx: accelerometerData.acceleration.x * 7, dy: accelerometerData.acceleration.y * 7)

}

RotationRate is pretty straight forward. However, the processed acceleration Data actually contains two part, gravity and userAcceleration. And the total acceleration of the device is equal to gravity plus the userAcceleration (acceleration the user imparts to the device ). Since we only want to simulate the physics based on user’s action, we will be using userAcceleration in our case.

You can check out more on how to get processed event data here.

I have also upload the source code to GitHub. Feel free to clone it and play around.

They are many other cools things you can do with motion sensor!

Thank you for reading and Keep Exploring!

--

--