Create Space Invaders with Swift and Sprite Kit: Finishing Gameplay

Final product image
What You'll Be Creating

In the previous part of this series, we made the invaders move, the player and invaders fire bullets, and implemented collision detection. In the fourth and final part of this series, we will add the ability to move the player using the accelerometer, manage the levels, and ensure the player dies when hit by a bullet. Let's get started.

1. Finishing the Player Class

Step 1: Adding Properties

Add the following properties to the Player class below the canFire property.

The invincible property will be used to make the player temporarily invincible when it loses a life. The lives property is the number of of lives the player has before being killed.

We are using a property observer on the lives property, which will be called each time its value is set. The didSet observer is called immediately after the new value of the property is set. By doing this, each time we decrement the lives property it automatically checks if lives is less than zero, calling the kill method if it is. If the player has lives left, the respawn method is invoked. Property observers are very handy and can save a lot of extra code.

Step 2: respawn

The respawn method makes the player invincible for a short amount of time and fades the player in and out to indicate that it is temporarily invincible. The implementation of the respawn method looks like this:

We set invincible to true and create a number of SKAction objects. By now, you should be familiar with how the SKAction class works.

Step 3: die

The die method is fairly simple. It checks whether invincible is false and, if it is, decrements the lives variable.

Step 4: kill

The kill method resets invaderNum to 1 and takes the user back to the StartGameScene so they can begin a new game.

This code should be familiar to you as it is nearly identical to the code we used to move to the GameScene from the StartGameScene. Note that we force unwrap the scene  to access the scene's size and scaleMode properties.

This completes the Player class. We now need to call the die  and kill methods in the didBeginContact(_:) method.

We can now test everything. A quick way to test the die method is by commenting out the moveInvaders call in the update(_:) method. After the player dies and respawns three times, you should be taken back to the StartGameScene.

To test the kill method, make sure the moveInvaders call is not commented out. Set the invaderSpeed property to a high value, for example, 200. The invaders should reach the player very quickly, which results in an instant kill. Change invaderSpeed back to once you're finished testing.

2. Finishing Firing Invaders

As the game stands right now, only the bottom row of invaders can fire bullets. We already have the collision detection for when a player bullet hits an invader. In this step, we will remove an invader that is hit by a bullet and add the invader one row up to the array of invaders that can fire. Add the following to the didBeginContact(_:) method.

We've removed the NSLog statement and first check if contact.bodyA.node?.parent and contact.bodyB.node?.parent are not nil. They will be nil if we have already processed this contact. In that case, we return from the function.

We calculate the invadersPerRow as we have done before and set theInvader to firstBody.node, casting it to an Invader. Next, we get the newInvaderRow by subtracting 1 and the newInvaderColumn, which stays the same.

We only want to enable invaders to fire if the newInvaderRow is greater than or equal to 1, otherwise we would be trying to set an invader in row to be able to fire. There is no row 0 so this would cause an error.

Next, we enumerate through the invaders, looking for the invader that has the correct row and column. Once it is found, we append it to the invadersWhoCanFire array and call stop.memory to true so the enumeration will stop early.

We need to find the invader that was hit with a bullet in the invadersWhoCanFire array so we can remove it. Normally, arrays have some kind of functionality like an indexOf method or something similar to accomplish this. At the time of writing, there is no such method for arrays in the Swift language. The Swift Standard Library defines a find function that we could use, but I found a method in the sections on generics in the Swift Programming Language Guide that will accomplish what we need. The function is aptly named findIndex. Add the following to the bottom of GameScene.swift.

If you are curious about how it this function works, then I recommend you read more about generics in the Swift Programming Language Guide.

Now that we have a method we can use to find the invader, we invoke it, passing in the invadersWhoCanFire array and theInvader. We check if invaderIndex isn't equal to nil and remove the invader from the invadersWhoCanFire array using the removeAtIndex(index: Int) method.

You can now test whether it works as it should. An easy way would be to comment out where the call to player.die in the didBeginContact(_:) method. Make sure you remove the comment when you are done testing. Notice that the program crashes if you kill all the invaders. We will fix this in the next step.

The application crashes, because we have a SKAction repeatActionForever(_:) calling for invaders to fire bullets. At this point, there are no invaders left to fire bullets so the games crashes. We can fix this by checking the isEmpty property on the invadersWhoCanFire array. If the array is empty, the level is over. Enter the following in the fireInvaderBullet method.

The level is complete, which means we increment invaderNum, which is used for the levels. We also invoke levelComplete, which we still need to create in the steps coming up.

3. Completing a Level

We need to have a set number of levels. If we don't, after several rounds we will have so many invaders they won't fit on the screen. Add a property maxLevels to the GameScene class.

Now add the levelComplete method at the bottom of GameScene.swift.

We first check to see if invaderNum is less than or equal to the maxLevels we have set. If so, we transition to the LevelCompletScene, otherwise we reset invaderNum to 1 and call newGame. LevelCompleteScene does not exist yet nor does the newGame method so let's tackle these one at a time over the next two steps.

4. Implementing the LevelCompleteScene Class

Create a new Cocoa Touch Class named LevelCompleteScene that is a sublclass of SKScene. The implementation of the class looks like this:

The implementation is identical to the StartGameScreen class, except for we set the name property of startGameButton to "nextlevel". This code should be familiar. If not, then head back to the first part of this tutorial for a refresher.

5. newGame

The newGame method simply transitions back to the StartGameScene. Add the following to the bottom of GameScene.swift.

If you test the application, you can play a few levels or lose a few games, but the player has no way to move and this makes for a boring game. Let's fix that in the next step.

6. Moving the Player Using the Accelerometer

We will use the accelerometer to move the player. We first need to import the CoreMotion framework. Add an import statement for the framework at the top of GameScene.swift.

We also need a couple of new properties.

Next, add a method setupAccelerometer at the bottom of GameScene.swift.

Here we set the accelerometerUpdateInterval, which is the interval in seconds for providing updates to the handler. I found 0.2 works well, you can try different values if you wish. Inside the handler; a closure, we get the accelerometerData.acceleration, which is a structure of type CMAcceleration.

We are only interested in the x property and we use numeric type conversion to cast it to a CGFloat for our accelerationX property.

Now that we have the accelerationX property set, we can move the player. We do this in the didSimulatePhysics method. Add the following to the bottom of GameScene.swift.

Invoke setupAccelerometer in didMoveToView(_:) and you should be able to move the player with the accelerometer. There's only one problem. The player can move off-screen to either side and it takes a few seconds to get him back. We can fix this by using the physics engine and collisions. We do this in the next step.

7. Restricting the Player's Movement

As mentioned in the previous step, the player can move off-screen. This is a simple fix using Sprite Kit's physics engine. First, add a new CollisionCategory named EdgeBody.

Set this as the player's collisionBitMask in its init method.

Lastly, we create a physicsBody on the scene itself. Add the following to the didMoveToView(view: SKView) method in GameScene.swift.

We initialize a physics body by invoking init(edgeLoopFromRect:), passing in the scene's frame. The initializer creates an edge loop from the scene's frame. It is important to note that an edge has no volume or mass and is always treated as if the dynamic property is equal to false. Edges may also only collide with volume-based physics bodies, which our player is.

We also set the categoryBitMask to CollisionCategories.EdgeBody. If you test the application, you might notice that your ship can no longer move off-screen, but sometimes it rotates. When a physics body collides with another physics body, it is possible that this results in a rotation. This is the default behavior. To remedy this, we set allowsRotation to false in Player.swift.

8. Star Field

Step 1: Creating the Star Field

The game has a moving star field in the background. We can create the start field using Sprite Kit's particle engine.

Create a new file and select Resource from the iOS section. Choose SpriteKit Particle File as the template and click Next. For the Particle template choose rain and save it as StarFieldClick Create to open the file in the editor. To see the options, open the SKNode Inspector on the right right.


Instead of going through every setting here, which would take a long time, it would be better to read the documentation to learn about each individual setting. I won't go into detail about the settings of the start field either. If you are interested, open the file in Xcode and have a look at the settings I used.

Step 2: Adding the Star Field to the Scenes

Add the following to didMoveToView(_:) in StartGameScene.swift.

We use an SKEmitterNode to load the StarField.sks file, set its position and give it a low zPosition. The reason for the low zPosition is to make sure it doesn't prevent the user from tapping the start button. The particle system generates hundreds of particles so by setting it really low we overcome that problem. You should also know that you can manually configure all the particle properties on an SKEmitterNode, although it is much easier to use the editor to create an .sks file and load it at runtime.

Now add the star field to GameScene.swift and LevelCompleteScene.swift. The code is exactly the same as above.

9. Implementing the PulsatingText Class

Step 1: Create the PulsatingText Class

The StartGameScene and LevelCompleteScene have text that grows and shrinks repeatedly. We will subclass SKLabeNode and use a couple of SKAction instances to achieve this effect.

Create a New Cocoa Touch Class that is a subclass of SKLabelNode, name it PulsatingText, and add the following code to it.

One of the first things you may have noticed is that there is no initializer. If your subclass doesn't define a designated initializer, it automatically inherits all of its superclass designated initializers.

We have one method setTextFontSizeAndPulsate(theText:theFontSize:), which does exactly what it says. It sets the SKLabelNode's text and fontSize properties, and creates a number of SKAction instances to make the text scale up and then back down, creating a pulsating effect.

Step 2: Add PulsatingText to StartGameScene

Add the following code to StartGameScene.swift in didMoveToView(_:).

We initialize a PulsatingText instance, invaderText, and invoke setTextFontSizeAndPulsate(theText:theFontSize:) on it. We then set its position and add it to the scene.

Step 3: Add PulsatingText to LevelCompleteScene

Add the following code to LevelCompleteScene.swift in didMoveToView(_:).

This is exactly the same as the previous step. Only the text we are passing in is different.

10. Taking the Game Further

This completes the game. I do have some suggestions for how you could further expand upon the game. Inside the images folder, there a three different invader images. When you are adding invaders to the scene, randomly choose one of these three images. You will need to update the invader's initializer to accept an image as a parameter. Refer to the Bullet class for a hint.

There is also a UFO image. Try to make this appear and move across the screen every fifteen seconds or so. If the player hits it, give them an extra life. You may want to limit the number of lives they can have if you do this. Lastly, try to make a HUD for the players lives.

These are just some suggestions. Try and make the game your own.

Conclusion

This brings this series to a close. You should have a game that closely resembles the original Space Invaders game. I hope you found this tutorial helpful and have learned something new. Thanks for reading.

Tags:

Comments

Related Articles