How to make a Flappy bird Game Sprite Kit

how to make a sprite kit game in xcode and how to make a line drawing game with spritekit swift and how to make a game with spritekit
Dr.KiranArora Profile Pic
Published Date:27-10-2017
Your Website URL(Optional)
SpriteKit Basics After an entire chapter of theory, we have finally reached the chapter where we will be creating a game. I am sure that this is a moment you have been waiting for and your fingers are aching to write some code and make a game. In this chapter, you will create a small and basic game using SpriteKit. We will see how to create the main menu of the game, and you will learn how to transition from the main menu scene to the gameplay scene, where all of the gameplay code will be written. In the gameplay scene, we will add sprites, such as the background and hero, first. We will then create a small physics engine to make the hero move around. Then, we will add in the enemies, and move them too. Next, we will make the hero and the enemies shoot at each other. We will detect collision between the hero's rockets and the enemies, and between the enemies' bullets and the hero. For each enemy the hero shoots, we will get one point, but if any of the enemies go past the left of the screen, it will be game over. If the current high score is greater than the previous score saved, then your current score will be saved as the new high score. Once the game is over, the player can click on the button to go back to the main menu to start the game. I hope you are excited Let's finally jump in. The topics covered in this chapter are as follows: • Introduction to SpriteKit and SKScene • Adding a main menu scene and a gameplay scene • Adding and moving the Hero sprite • Creating interactivity with touches • A simple physics engine • Spawning enemies 109 SpriteKit Basics • Firing hero rockets and enemy bullets • Collision detection • Scoring and game over conditions • Displaying, saving, and retrieving the score Introduction to SpriteKit and SKScene We have already seen in Chapter 1, Getting Started, how to create a SpriteKit project. Just to jog your memory, I will show you once again how to create a project. Click on Xcode, and then on Create a new Xcode Project. Then, on the left-hand side panel, navigate under iOS, and then under Application and select Game. Then click on Next. Give a new name to the project. Select the language as Swift, the game technology as SpriteKit, the device as iPad, and then click on Next. Select the location where you want the project folder to be created and click on Create. You will see that the majority of the project structure remains similar to the SingleView project we saw in the previous chapter. We have theGameScene.sks, GameScene.swift, andGameViewController.swift files: • GameScene.sks: This is a serializedSpriteKitScene file. This is used to create SKScenes visually without writing code. So, for example, you can drag-and-drop images and design them as buttons, and by clicking on them, you can make them perform different functions. But since we will be writing it all in code, we won't be using this file to create the interface of the game. • GameScene.swift: This is inherited from SKScene. SKScenes are the building blocks of games. This class is called once the application view is loaded. You can create SKScene files to create the main menu Scene, gameplay scene, options scene, and so on. In fact, we will later rename theGameScene.swift file toMainMenuScene.swift and create a new scene calledGamePlayScene, where will write our gameplay code. 110 Chapter 4 • GameViewController.swift: This class is similar to the ViewController.swift file we saw in the previous chapter. In theMain. Storyboard file, you will see that there is aGameViewControllerScene file instead ofViewController, but the structure is very similar to it. If you click on GameViewController, you can see that it calls theGameViewController class on the Utility panel under the Identity inspector. Refer to the preceding diagram. Now, open theGameViewController.swift file. You will see some new functions and some older functions that we saw inViewController.swift. You will also notice that SpriteKit has been imported. We will need to import SpriteKit in all the classes in which we want to use its features. All classes and objects that are part of SpriteKit start with the prefix SK, so SpriteKit Scenes areSKScene, sprites are SKSpriteNode, and so on. ASpriteKitNode orSKNode is the basic building block required for creating any content in SpriteKit, but unlike anSKScene orSKSpriteNode, it doesn't draw any visual content. However, bothSKScene andSKSpriteNode are child classes ofSKNode. So, ifSKScene is the building block of any game,SKNode is the basic building block of SpriteKit itself. A detailed explanation is provided under the SpriteKit section in Chapter 1, Getting Started. After importing the SpriteKit, we see that an extension of SKNode is created with a class function calledunarchivedFromFile, which takes a string and returns an SKNode. The following function is used to load the.sks file we saw earlier: extension SKNode class func unarchiveFromFile(file : NSString) - SKNode? if let path = NSBundle.mainBundle().pathForResource(file, ofType: "sks") var sceneData = NSData(contentsOfFile: path, options: .DataReadingMappedIfSafe, error: nil) var archiver = NSKeyedUnarchiver(forReadingWithData: sceneData) archiver.setClass(self.classForKeyedUnarchiver(), forClassName: "SKScene") let scene = archiver.decodeObjectForKey(NSKeyedArchiveRoot ObjectKey) as GameScene archiver.finishDecoding() return scene else return nil 111 SpriteKit Basics After the extension, we see the actual class ofGameViewController. It is still inheriting fromUIViewController. Similar to theViewController.swift file, the first function called here is viewDidLoad, which is the function that is called as soon as the view gets loaded. Thesuper.viewDidLoad function is called, which calls the viewDidLoad of the parent class. Then, theGameScene.sks file is loaded using the extension that was created earlier. Theif let statement checks if the object scene is empty or not. If it is not empty, then the code in theif block will be executed. extension: In Swift, you can add functionality to an existing class. In the preceding case, we are adding a new function called unarchivedFromFile to theSKNode class, which unarchives files and returns an SKScene. This function is used to unarchive theSKS file in the viewDidLoad function in the following code. if let: This checks if the object scene is empty or not. If it is not empty, then the code in theif block will be executed. as: This operator is used to downcast SKView since it is actually a subclass ofUIView. override func viewDidLoad() super.viewDidLoad() if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene // Configure the view. let skView = self.view as SKView skView.showsFPS = true skView.showsNodeCount = true / Sprite Kit applies additional optimizations to improve rendering performance / skView.ignoresSiblingOrder = true / Set the scale mode to scale to fit the window / scene.scaleMode = .AspectFill skView.presentScene(scene) 112 Chapter 4 A new variable,skView is created and the current view is assigned to it by type casting it as an SKView since the root view ofGameViewController is an SKView. Then, theshowsFPS andshowsNodeCount properties of the scene are set totrue, which will show the FPS and Node Count on the bottom-right of the screen. TheignoreSiblingOrder property is set totrue, meaning that if one or more objects are at the same depth, then it won't prioritize between them and all objects will be drawn at the same depth. The value of Z order, or depth order, decides which object is at the front and which object is at the back of the screen. The object with the smallest Z value is kept at the back of the screen and the object the with highest value is the closest to the screen. If no Z order value is assigned to an object, SpriteKit will assume that this object is above the previous object added. That is why in all games, the background is added first so that it is at the lowest Z order and other objects such as the hero are added next. If you were to add the hero first and then the background, the hero would be at the lowest Z order and the background image that covers the whole screen would be above it. You might think that there is something wrong with the code or SpriteKit since the hero is not being displayed only the background. The fact is that the hero is there but he is behind the background. So, be careful about Z orders as this may lead to bugs or unexpected results in games. After setting the order, we can set thescaleMode property of the scene. Here, by default, it has been set toAspectFill. There are four modes:AspectFill,Fill, AspectFit, andResizeFill. • AspectFill: This is the default mode when you create a new project. In this scale, both x and y scale factors are calculated and the large-scale factor is chosen to fill the view and maintain aspect ratio of the image. This will lead to cropping of the scene. Let us create a project and place characters at the top-right and bottom-left corners, and observe what happens when we move from the landscape mode to the portrait mode. 113 SpriteKit Basics In the landscape mode, both characters are displayed as they should be. One at the bottom left and the other at the top-right corner of the screen, as shown in the following screenshot: But in the portrait mode, they have gone out of bounds of the screen, as shown in the following screenshot: • Fill: Both x and y axes are scaled to fill the view. The view is the region that is shown once you click on the view in theMain.Storyboard file. The aspect ratio of the image will change both in terms of the width and height, to fill the view. If we do the same test with.Fill, once again in landscape mode, the images appear to be normal, but in the portrait mode, the two images are in their respective locations, but they are squashed to fit, as shown in the following screenshot: 114 Chapter 4 • AspectFit: Instead of the upper scale factor, the lower scale factor will be chosen to maintain the aspect ratio of the scene. This may lead to letterboxing of the scene, but all the content of the scene will be displayed and will be visible in view. With this mode, once again everything looks fine in the landscape mode, but then in the portrait mode, the image is not at all squared; the whole scene is scaled down to fit to the width of the screen. This will cause letterboxing on the top and bottom of the screen, as shown in the following screenshot: 115 SpriteKit Basics • ResizeFill: The scene is not scaled at all. It is just resized to fit the view. The images will maintain the original size and aspect ratios. Here, since the aspect ratio and scale are maintained, the bottom-left image is shown at the right position but the top-right image goes out of bounds of the screen, as shown in the following screenshot: Looking at the preceding screenshots of the four modes, we can see that not all sizes fit all. You need to tinker with the scale mode to best suit the needs of your game. Since our game is primarily designed to be played in the landscape mode, we will just disable the portrait mode. So, in the main project node, disable the Portrait mode and Upside Down by unchecking it in the General tab, as shown in the following screenshot: 116 Chapter 4 We will also be usingResizeFill as we will be providing separate images for Retina and NonRetina assets of the devices, so that aspect ratio is not affected, resulting in nice full-screen images instead of cropped or scaled images. So, in the GameViewController class, change the scale mode toResizeFill as follows: / Set the scale mode to scale to fit the window / scene.scaleMode = .ResizeFill Finally, the scene is loaded and presented using thepresentScene function of the skView object. Once theGameScene.swift file is presented, the didMoveToView function is called, which, as we saw in Chapter 1, Getting Started, shows theSKLabelNode label showing the Hello, World text, and each time you click on the screen, thetouchesBegan function gets called, and at the location of the touch, a SKSpriteNode is created and an action is run on it. There are three new functions: theshouldAutoRotate function, which is set totrue, and will rotate the view if the device is rotated; the supportedInterfaceOrientation function, which checks for the orientation and aligns view accordingly; and theprefersStatusBarHidden function, which hides status bar elements, such as network and battery indicators on the top of the screen. You can enable or disable them depending upon your needs. We will now change theGameScene to start making our game. Adding a main menu scene Let us make some changes to theGameScene class to make it our main menu scene: 1. Rename the file to MainMenuScene.swift by selecting the file in the project hierarchy in the project navigator. 2. Change the name of the class in the file to MainMenuScene. 3. Delete all the lines of code insidedidMoveToView. 4. In thetouchesBegan function, delete the code related to the adding of the sprite and the running of the action upon it. 5. Delete theupdate function as it is not required for the main menu scene. If required, we will add it later. 117 SpriteKit Basics TheMainMenuScene.swift file should look like the following code snippet, as we deleted all the code from thedidMoveToView function and modified the touchedBegan function: import SpriteKit class MainMenuScene: SKScene override func didMoveToView(view: SKView) override func touchesBegan(touches: NSSet, withEvent event: UIEvent) / Called when a touch begins / for touch: AnyObject in touches let location = touch.locationInNode(self) Delete theGameScene.sks file from the project hierarchy by moving it to Trash. We also need to make some changes to theGameViewController.swift file as well. 1. Delete the extension created for SKNode 2. Remove theif let scene line and the opening and closing bracket since we will be calling theMainMenuScene class directly through code. 3. Replace the above line withlet scene = MainMenuScene(size: view. bounds.size). The SKScene constructor takes the size of the screen, so here we get it from thebounds.size property of the view. 4. Change.AspectFill to.ResizeFill. The rest of the file can remain the same. Now the viewDidLoad function should look like the following code: override func viewDidLoad() super.viewDidLoad() let scene = MainMenuScene(size: view.bounds.size) 118 Chapter 4 // Configure the view. let skView = self.view as SKView skView.showsFPS = true skView.showsNodeCount = true / Sprite Kit applies additional optimizations to improve rendering performance / skView.ignoresSiblingOrder = true / Set the scale mode to scale to fit the window / scene.scaleMode = .ResizeFill skView.presentScene(scene) Let's start adding content to the main menu scene next. In theMainMenuScene.swift file in the didMoveToView function, we will first add the background image, then we will add a label that will display the name of the game, and then we will finally add the play button, which, if clicked, will launch GamePlayScene and start the game. To add the background image, add the following code: let BG = SKSpriteNode(imageNamed: "BG") BG.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2) self.addChild(BG) We create a constant variable calledBG and assign an image set namedBG to it. Then, we position this image. For positioning the image, we need the size of the view. It is very simple to get the width and height of the view. We create a new constant called viewSize of typeCGView and assignview.bounds.size to it. So, add the following at the start of thedidMoveToView function: let viewSize:CGSize = view.bounds.size We can now set the position ofBG. To set the position, we assignBG.position equal to half of the width and half of the height of the size of the view. Whenever we need to assign or create a newCGPoint variable, we have to call CGPoint, and in the brackets provide thex andy values separated by a comma. Thex value needs to be prefixed with x and then a colon and, similarly, they value needs to be prefixed with y followed by a colon. 119 SpriteKit Basics For the background to get displayed, we will have to call theaddChild function on self, and pass in the background created. If you run the game now, it will give errors as we have still not assigned an actual image to the project. For this, go to theResources folder of this chapter and copy all the assets onto the desktop. This will contain all the assets that will be used in this chapter. Now, go to theImages.xcassets file in your project navigator, and right-click on the panel and select New Image Set, as shown in the following screenshot. A new file called Image will be created. Select and rename it toBG. When we callBG while creating the sprite for the background, we are actually referring to this file. So, if you name it incorrectly, the code will give errors. The file has placeholders for 1x, 2x, and 3x images. Since we are making the game for the iPad, there are only two resolutions we have to worry about; the 1024 x 768 and 2048 x 1536 resolutions. Our BG is also of the same two resolutions. In theResources folder, look for image files Bg.png andBg2.png. DragBg.png to the 1x box and Bg2.png to the 2x box, as shown in the following screenshot: 120 Chapter 4 Now you can run the application and the screen will display the background image in all its glory. On the simulator, you can select iPad2 or iPadAir, and you will see the image will fill the entire screen, as shown in the following screenshot. Make sure you are running in the landscape mode. Next we will add the label to display the name of the game. Labels are used to display text onto the screen. Add the following code to display the label right after we added the BG to the scene: let myLabel = SKLabelNode(fontNamed:"Chalkduster") myLabel.text = "Ms.TinyBazooka" myLabel.fontSize = 65 myLabel.position = CGPoint(x: viewSize.width/2, y: viewSize.height 0.8) self.addChild(myLabel) 121 SpriteKit Basics We create a new constant calledmyLabel and call the constructor ofSKLabelNode. It takes the name of the font we want to use to create the text, so we pass in Chalkduster, which is one of the default fonts in Mac. InmyLabel.text, we pass in the actual text that we want to display. Next we assign the size of the font, its position, and add it to the current class as a child. To create text, we don't have to create an image set but we have to have the font in the system as it is automatically taken from the system's font directory. Let us create the play button next. In theResources folder, you will find playBtn. png andplayBtn2.png. Similar to how we created an image set forBG, create one for the play button by naming the file playBtn. Drag theplayBtn.png image to 1x and playBtn2.png to 2x. For all the assets in theResources folder, you will find two of each, one with the filename and the second one ending with a 2 at the end. Make sure, henceforth, that the regular filename asset is assigned to 1x and one with the2 at the end of the file is assigned to 2x. Now appropriate images are assigned to theplayBtn image set. Add the following code right under where we added the code for the label: let playBtn = SKSpriteNode(imageNamed: "playBtn") playBtn.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2) self.addChild(playBtn) = "playBtn" TheplayBtn image set is also a regular SKSpriteNode, so similar to how we added BG, we will assign theplayBtn image set to theplayBtn constant. Position it at the center of the view and then add it to the view. In addition to what we do usually, I have also assigned a name to theplayBtn constant so that we can refer to it later, if needed. It is not necessary that you assign a string; you can even assign an integer value if you wish. It should be named something that you find easy to remember and associate the constant with. 122 Chapter 4 Now, if you build and run the project, it should look like the following screenshot: Next, we will add code in thetouchesbegan function to check whether the play button was pressed. In thetouchesbegan function, we first check whether any object was touched on the screen, and then, if it was touched, we get the location of the touch. After getting the location, we add the following code: let _node:SKNode = self.nodeAtPoint(location) if( == "playBtn") let scene = GamePlayScene(size: self.size) self.view?.presentScene(scene) We create a new constant called_node of typeSKNode and get the node that is at the touched location. We then check whether the name of the node pressed isplayBtn, if it is pressed, then we create a constant namedscene and assignGameplayScene to it, and then present the scene like we presentedMainMenuScene in the GameViewController class. Since we have not createdGamePlayScene, you will get an error. Don't worry, we will be creating it in the next section. 123 SpriteKit Basics The question mark afterself.view checks if the view is empty or not. If it is empty, it will give an error, but since the view exists, it won't give an error. Let us create the gameplay scene so that we don't get errors saying that it doesn't exist. Adding a gameplay scene All this time, we have been modifying the files already included with the base project. Now we will create a new file in the project. Right-click on the base project folder and click on New File: In the left panel, select iOS and select the Swift file, and then click on Next. It will ask for the name of the file, call it GamePlayScene and click on Create. This will create an empty Swift file. Add the following code in it. This is the basic structure required whenever you create a new scene file: import SpriteKit class GamePlayScene: SKScene required init?(coder aDecoder: NSCoder) fatalError("init(coder:) has not been implemented") // required init override init(size: CGSize) super.init(size: size) //init function //class end 124 Chapter 4 We first import SpriteKit, and then we create the class using theclass keyword followed by the name of the class and inherit fromSKScene. Then we have therequired init function after that. Since superclassSKScene implemented it, it has to be included in all the subclasses. This is a requirement, so there is no avoiding it, but we won't be using it for anything as we will be using the regularinit function. The regularinit function of an SKScene takes in the size of the view. Then we have to make sure we call thesuper.init function and pass the size of the view in it as well. That's it, and we are ready to add some gameplay code in this class. You can check in theMainMenuScene.swift file that there are no errors and code is building properly. In theGamePlayScene.swift file, first we have to create a global variable for theviewSize. So, between the class and the requiredinit function, addlet viewSize:CGSize to makeviewSize a global variable. Also, we uselet instead ofvar as we know that the size of the view won't change in the middle of the game. Since we are not initializing the constant here, we have to use an exclamation mark at the end to tell Swift that we will initialize it and we know that the type that we will initialize will beCGSize. InitializeviewSize equal to the size that got passed in theinit function. Add the following line aftersuper.init is called: viewSize = size Adding a background and a hero We are going add the background first, so we can copy and paste the same code from theMainMenuScene into theinit function right after where we initializedviewSize: let BG = SKSpriteNode(imageNamed: "BG") BG.position = CGPoint(x: viewSize.width/2, y: viewSize.height/2) self.addChild(BG) Next we will add the hero sprite. Similar to how we created theBG image asset, create a new asset calledhero and assignhero.png andhero2.png to the 1x and 2x slots. 125 SpriteKit Basics Next we want the hero to be a global variable aswell, as we will need to refer to her outside of theinit function. So, right under where we created theviewSize property, add the following line of code at the top of the class: let hero:SKSpriteNode Next, in theinit function after where we addedBG, add the following code: hero = SKSpriteNode(imageNamed: "hero") hero.position = CGPoint(x: viewSize.width/4, y: viewSize.height/2) self.addChild(hero) Here, as usual, we assign the image sethero to the constant, assign the position, and add it to the scene. Similar to how we positioned the background, we position the hero, but instead of adding the hero in the center, we place her at a distance of one fourth theviewSize from the left of the screen. Updating the position of the hero Next, let's update the position of the hero. Let us add gravity to the scene so that she starts falling down once the game starts. For updating her position, we will use the update function. Theupdate function gets called as soon as the class gets created and it gets called 60 times a second. So, add theupdate function to the class as follows. Call aupdateHero() function in it, we will define this function shortly: override func update(currentTime: CFTimeInterval) updateHero() Create a new global constant after thelet hero line calledgravity of typeCGPoint and initialize it withx value0 and ay value of–1 as gravity only affects in the negative y direction: let gravity = CGPoint(x:0.0, y: -1.0) We will also create a new function calledupdateHero in which we will write all the code to update the hero's position. Create this function under theupdate function and don't forget to call this function in theupdate function, otherwise the hero's position won't be updated. func updateHero() hero.position.y += gravity.y 126 Chapter 4 In theupdateHero function, we are decrementing they position of the hero in each update. Eventually, she will fall through the bottom of screen. To make her stay within the bounds, we check whether she is about to go beyond the screen and place her back at the bottom edge of the screen. To do this, add the following in theheroUpdate function right under where we decrement her position: if(hero.position.y - hero.size.height/2 = 0) hero.position.y = hero.size.height/2 else if (hero.position.y + hero.size.height/2 = viewSize.height) hero.position.y = viewSize.height - hero.size.height/2 In the first if block, we check whether the bottom of the hero has crossed the bottom of the screen. If so, then we place the hero's origin at half her height from the bottom of the screen. In theelse if block, we check whether the top of the hero has crossed the top of the screen. If so, then we place her at half her height from the top of the screen. Now, if you build and run the game and press play, the hero will be at one fourth of the distance from the left of the screen and will stop once she reaches the bottom of the screen. 127 SpriteKit Basics Adding player controls We will now add player controls by using thetouchesbegan function. If the player taps the left side of the screen, the hero get pushed up and will then start falling again due to gravity, and if the player taps the right of the screen, the hero will fire rockets. For detecting touches, add thetouchesBegan function under theupdate function as follows: override func touchesBegan(touches: NSSet, withEvent event: UIEvent) / Called when a touch begins / for touch: AnyObject in touches let location = touch.locationInNode(self) //touchesBegan This is obviously the same function that we used inMainMenuScene to detect touches on the play button. Since we are just going to be checking the location of the touch, we don't require the object we touched, for now at least. To detect which side of the screen was tapped, add the following code under where we get the location of the touches in thefor in loop: if(location.x viewSize.width/2) println("GamePlayScene touchedLeftSide ") else if(location.x viewSize.width/2) println("GamePlayScene touchedRightSide ") We check whether thex value of the touched location is less than half of the width of the screen. If so, then we print out that the left side of the screen was touched, otherwise we check whether thex value of the touched location was greater than half of the width of the screen, then in that case, we can conr fi m that the right of the screen was touched. Now, to push the hero up in the air, we give her a small thrust each time the player touches the left of the screen. Add a global variable calledthrust of typeCGPoint and initialize bothx andy values to zero as follows: var thrust = CGPointZero 128