How To Make a Game Like Space Invaders with SpriteKit and Swift: Part 1

Learn how to make a game like Space Invaders using Apple’s built-in 2D game framework: Sprite Kit! By Ryan Ackermann.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Controlling Ship Movements with Device Motion

You might be familiar with UIAccelerometer, which has been available since iOS 2.0 for detecting device tilt. However, UIAccelerometer was deprecated in iOS 5.0, so iOS 10 apps should use CMMotionManager, which is part of Apple's CoreMotion framework.

The CoreMotion library has already been added to the starter project, so there's no need for you to add it.

Your code can retrieve accelerometer data from CMMotionManager in two different ways:

  1. Pushing accelerometer data to your code: In this scenario, you provide CMMotionManager with a block that it calls regularly with accelerometer data. This doesn't fit well with your scene's update() method that ticks at regular intervals of 1/60th of a second. You only want to sample accelerometer data during those ticks — and those ticks likely won't line up with the moment that CMMotionManager decides to push data to your code.
  2. Pulling accelerometer data from your code: In this scenario, you call CMMotionManager and ask it for data when you need it. Placing these calls inside your scene's update() method aligns nicely with the ticks of your system. You'll be sampling accelerometer data 60 times per second, so there's no need to worry about lag.

Your app should only use a single instance of CMMotionManager to ensure you get the most reliable data. To that effect, declare and initialize the following property at the top of GameScene:

 
let motionManager = CMMotionManager()

Now, add the following code to didMove(to:), right after the contentCreated = true line:

motionManager.startAccelerometerUpdates()

This new code kicks off the production of accelerometer data. At this point, you can use the motion manager and its accelerometer data to control your ship's movement.

Add the following method just below moveInvaders(forUpdate:):

func processUserMotion(forUpdate currentTime: CFTimeInterval) {
  // 1
  if let ship = childNode(withName: kShipName) as? SKSpriteNode {
    // 2
    if let data = motionManager.accelerometerData {
      // 3
      if fabs(data.acceleration.x) > 0.2 {
        // 4 How do you move the ship?
        print("Acceleration: \(data.acceleration.x)")
      }
    }
  }
}

Dissecting this method, you'll find the following:

  1. Get the ship from the scene so you can move it.
  2. Get the accelerometer data from the motion manager. It is an Optional, that is a variable that can hold either a value or no value. The if let data statement allows to check if there is a value in accelerometerData, if is the case assign it to the constant data in order to use it safely within the if's scope.
  3. If your device is oriented with the screen facing up and the home button at the bottom, then tilting the device to the right produces data.acceleration.x > 0, whereas tilting it to the left produces data.acceleration.x < 0. The check against 0.2 means that the device will be considered perfectly flat/no thrust (technically data.acceleration.x == 0) as long as it's close enough to zero (data.acceleration.x in the range [-0.2, 0.2]). There's nothing special about 0.2, it just seemed to work well for me. Little tricks like this will make your control system more reliable and less frustrating for users.
  4. Hmmm, how do you actually use data.acceleration.x to move the ship? You want small values to move the ship a little and large values to move the ship a lot. For now, you just print out the acceleration value.

Finally, add the following line to the top of update():

processUserMotion(forUpdate: currentTime)

Your new processUserMotion(forUpdate:) now gets called 60 times per second as the scene updates.

Build and run - but this time, be sure that you run on a physical device like an iPhone. You won't be able to test the tilt code unless you are running the game on an actual device.

As you tilt your device, the ship won't move - however you will see some print statements in your console log like this:

Acceleration: 0.280059814453125
Acceleration: 0.255386352539062
Acceleration: 0.227584838867188
Acceleration: -0.201553344726562
Acceleration: -0.2618408203125
Acceleration: -0.280426025390625
Acceleration: -0.28662109375

Here you can see the acceleration changing as you tilt the device back and forth. Next, let's use this value to make the ship move - through the power of Sprite Kit physics!

Translating Motion Controls into Movement via Physics

Sprite Kit has a powerful built-in physics system based on Box 2D that can simulate a wide range of physics like forces, translation, rotation, collisions, and contact detection. Each SKNode, and thus each SKScene and SKSpriteNode, has an SKPhysicsBody attached to it. This SKPhysicsBody represents the node in the physics simulation.

Add the following code right before the final return ship line in makeShip():

// 1
ship.physicsBody = SKPhysicsBody(rectangleOf: ship.frame.size)

// 2
ship.physicsBody!.isDynamic = true

// 3
ship.physicsBody!.affectedByGravity = false

// 4
ship.physicsBody!.mass = 0.02

Taking each comment in turn, you'll see the following:

  1. Create a rectangular physics body the same size as the ship.
  2. Make the shape dynamic; this makes it subject to things such as collisions and other outside forces.
  3. You don't want the ship to drop off the bottom of the screen, so you indicate that it's not affected by gravity.
  4. Give the ship an arbitrary mass so that its movement feels natural.

Now replace the println statement in processUserMotion(forUpdate:) (right after comment // 4) with the following:

ship.physicsBody!.applyForce(CGVector(dx: 40 * CGFloat(data.acceleration.x), dy: 0))

The new code applies a force to the ship's physics body in the same direction as data.acceleration.x. The number 40 is an arbitrary value to make the ship's motion feel natural.

Build and run your game and try tilting your device left or right; Your ship will fly off the side of the screen, lost in the deep, dark reaches of space. If you tilt hard and long enough in the opposite direction, you might get your ship to come flying back the other way. But at present, the controls are way too flaky and sensitive. You'll never kill any invaders like this!

An easy and reliable way to prevent things from escaping the bounds of your screen during a physics simulation is to build what's called an edge loop around the boundary of your screen. An edge loop is a physics body that has no volume or mass but can still collide with your ship. Think of it as an infinitely-thin wall around your scene.

Since your GameScene is a kind of SKNode, you can give it its own physics body to create the edge loop.

Add the following code to createContent() right before the setupInvaders() line:

physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)

The new code adds the physics body to your scene.

Build and run your game once more and try tilting your device to move your ship, as below:

space_invaders_player_movement

What do you see? If you tilt your device far enough to one side, your ship will collide with the edge of the screen. It no longer flies off the edge of the screen. Problem solved!

Depending on the ship's momentum,you may also see the ship bouncing off the edge of the screen, instead of just stopping there. This is an added bonus that comes for free from Sprite Kit's physics engine — it's a property called restitution. Not only does it look cool, but it is what's known as an affordance since bouncing the ship back towards the center of the screen clearly communicates to the user that the edge of the screen is a boundary that cannot be crossed.