




















































Let's start using the first framework by implementing a nice clone of Flappy Bird with the help of this article by Giordano Scalzo, the author of Swift by Example.
(For more resources related to this topic, see here.)
Only someone who has been living under a rock for the past two years may not have heard of Flappy Bird, but to be sure that everybody understands the game, let's go through a brief introduction.
Flappy Bird is a simple, but addictive, game where the player controls a bird that must fly between a series of pipes. Gravity pulls the bird down, but by touching
the screen, the player can make the bird flap and move towards the sky, driving
the bird through a gap in a couple of pipes. The goal is to pass through as many pipes as possible.
Our implementation will be a high-fidelity tribute to the original game, with the same simplicity and difficulty level. The app will consist of only two screens—a clean menu screen and the game itself—as shown in the following screenshot:
Let's start implementing the skeleton of our game using the SpriteKit game template.
For implementing a SpriteKit game, Xcode provides a convenient template, which prepares a project with all the useful settings:
First of all, let's add CocoaPods; write the following code in the Podfile:
use_frameworks! target 'FlappySwift' do pod 'Cartography', '~> 0.5' pod 'HTPressableButton', '~> 1.3' end
Then install CocoaPods by running the pod install command. As usual, we are going to implement the UI without using Interface Builder and the storyboards. Go to AppDelegate and add these lines to create the main ViewController:
func application(application: UIApplication, didFinishLaunchingWithOptions
launchOptions: [NSObject: AnyObject]?) -> Bool { let viewController = MenuViewController() let mainWindow = UIWindow(frame: UIScreen.mainScreen().bounds) mainWindow.backgroundColor = UIColor.whiteColor() mainWindow.rootViewController = viewController mainWindow.makeKeyAndVisible() window = mainWindow return true }
The MenuViewController, as the name suggests, implements a nice menu to choose between the game and the Game Center:
import UIKit import HTPressableButton import Cartography class MenuViewController: UIViewController { private let playButton = HTPressableButton(frame: CGRectMake(0, 0, 260, 50), buttonStyle: .Rect) private let gameCenterButton = HTPressableButton(frame: CGRectMake(0, 0, 260, 50), buttonStyle: .Rect) override func viewDidLoad() { super.viewDidLoad() setup() layoutView() style() render() } }
As you can see, we are using the usual structure. Just for the sake of making the UI prettier, we are using HTPressableButtons instead of the default buttons.
Despite the fact that we are using AutoLayout, the implementation of this custom button requires that we instantiate the button by passing a frame to it:
// MARK: Setup private extension MenuViewController{ func setup(){ playButton.addTarget(self, action: "onPlayPressed:", forControlEvents: .TouchUpInside) view.addSubview(playButton) gameCenterButton.addTarget(self, action: "onGameCenterPressed:", forControlEvents: .TouchUpInside) view.addSubview(gameCenterButton) } @objc func onPlayPressed(sender: UIButton) { let vc = GameViewController() vc.modalTransitionStyle = .CrossDissolve presentViewController(vc, animated: true, completion: nil) } @objc func onGameCenterPressed(sender: UIButton) { println("onGameCenterPressed") } }
The only thing to note is that, because we are setting the function to be called when the button is pressed using the addTarget() function, we must prefix the designed methods using @objc. Otherwise, it will be impossible for the Objective-C runtime to find the correct method when the button is pressed. This is because they are implemented in a private extension; of course, you can set the extension as internal or public and you won't need to prepend @objc to the functions:
// MARK: Layout extension MenuViewController{ func layoutView() { layout(playButton) { view in view.bottom == view.superview!.centerY - 60 view.centerX == view.superview!.centerX view.height == 80 view.width == view.superview!.width - 40 } layout(gameCenterButton) { view in view.bottom == view.superview!.centerY + 60 view.centerX == view.superview!.centerX view.height == 80 view.width == view.superview!.width - 40 } } }
The layout functions simply put the two buttons in the correct places on the screen:
// MARK: Style private extension MenuViewController{ func style(){ playButton.buttonColor = UIColor.ht_grapeFruitColor() playButton.shadowColor = UIColor.ht_grapeFruitDarkColor() gameCenterButton.buttonColor = UIColor.ht_aquaColor() gameCenterButton.shadowColor = UIColor.ht_aquaDarkColor() } } // MARK: Render private extension MenuViewController{ func render(){ playButton.setTitle("Play", forState: .Normal) gameCenterButton.setTitle("Game Center", forState: .Normal) } }
Finally, we set the colors and text for the titles of the buttons. The following screenshot shows the complete menu:
You will notice that on pressing Play, the app crashes. This is because the template is using the view defined in storyboard, and we are directly using the controllers.
Let's change the code in GameViewController:
class GameViewController: UIViewController { private let skView = SKView() override func viewDidLoad() { super.viewDidLoad() skView.frame = view.bounds view.addSubview(skView) if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene { scene.size = skView.frame.size skView.showsFPS = true skView.showsNodeCount = true skView.ignoresSiblingOrder = true scene.scaleMode = .AspectFill skView.presentScene(scene) } } }
We are basically creating the SKView programmatically, and setting its size just as we did for the main view's size.
If the app is run now, everything will work fine.
You can find the code for this version at https://github.com/gscalzo/FlappySwift/tree/the_menu_is_ready.
Let's kick-start the game by implementing the background, which is not as straightforward as it might sound.
SpriteKit is a powerful, but easy-to-use, game framework introduced in iOS 7.
It basically provides the infrastructure to move images onto the screen and interact with them.
It also provides a physics engine (based on Box2D), a particles engine, and basic sound playback support, making it particularly suitable for casual games.
The content of the game is drawn inside an SKView, which is a particular kind of UIView, so it can be placed inside a normal hierarchy of UIViews.
The content of the game is organized into scenes, represented by subclasses of SKScene. Different parts of the game, such as the menu, levels, and so on, must be implemented in different SKScenes. You can consider an SK in SpriteKit as an equivalent of the UIViewController.
Inside an SKScene, the elements of the game are grouped in the SKNode's tree which tells the SKScene how to render the components.
An SKNode can be either a drawable node, such as SKSpriteNode or SKShapeNode; or something to be applied to the subtree of its descendants, such as SKEffectNode or SKCropNode.
Note that SKScene is an SKNode itself.
Nodes are animated using SKAction.
An SKAction is a change that must be applied to a node, such as a move to a particular position, a change of scaling, or a change in the way the node appears. The actions can be grouped together to create actions that run in parallel, or wait for the end of a previous action.
Finally, we can define physics-based relations between objects, defining mass, gravity, and how the nodes interact with each other.
That said, the best way to understand and learn SpriteKit is by starting to play with it. So, without further ado, let's move on to the implementation of our tiny game. In this way, you'll get a complete understanding of the most important features of SpriteKit.
In the previous section, we implemented the menu view, leaving the code similar to what was created by the template. With basic knowledge of SpriteKit, you can now start understanding the code:
class GameViewController: UIViewController { private let skView = SKView() override func viewDidLoad() { super.viewDidLoad() skView.frame = view.bounds view.addSubview(skView) if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene { scene.size = skView.frame.size skView.showsFPS = true skView.showsNodeCount = true skView.ignoresSiblingOrder = true scene.scaleMode = .AspectFill skView.presentScene(scene) } } }
This is the UIViewController that starts the game; it creates an SKView to present the full screen. Then it instantiates the scene from GameScene.sks, which can be considered the equivalent of a Storyboard. Next, it enables some debug information before presenting the scene.
It's now clear that we must implement the game inside the GameScene class.
To simulate the depth of the in-game world, we are going to use the technique of parallax scrolling, a really popular method wherein the farther images on the game screen move slower than the closer images.
In our case, we have three different levels, and we'll use three different speeds:
Before implementing the scrolling background, we must import the images into our project, remembering to set each image as 2x in the assets.
The assets can be downloaded from https://github.com/gscalzo/FlappySwift/blob/master/assets.zip?raw=true.
The GameScene class basically sets up the background levels:
import SpriteKit class GameScene: SKScene { private var screenNode: SKSpriteNode! private var actors: [Startable]! override func didMoveToView(view: SKView) { screenNode = SKSpriteNode(color: UIColor.clearColor(), size: self.size) addChild(screenNode) let sky = Background(textureNamed: "sky", duration:60.0).addTo(screenNode) let city = Background(textureNamed: "city", duration:20.0).addTo(screenNode) let ground = Background(textureNamed: "ground", duration:5.0).addTo(screenNode) actors = [sky, city, ground] for actor in actors { actor.start() } } }
The only implemented function is didMoveToView(), which can be considered the equivalent of viewDidAppear for a UIVIewController.
We define an array of Startable objects, where Startable is a protocol for making the life cycle of the scene, uniform:
import SpriteKit protocol Startable { func start() func stop() }
This will be handy for giving us an easy way to stop the game later, when either we reach the final goal or our character dies. The Background class holds the behavior for a scrollable level:
import SpriteKit class Background { private let parallaxNode: ParallaxNode private let duration: Double init(textureNamed textureName: String, duration: Double) { parallaxNode = ParallaxNode(textureNamed: textureName) self.duration = duration } func addTo(parentNode: SKSpriteNode) -> Self { parallaxNode.addTo(parentNode) return self } }
As you can see, the class saves the requested duration of a cycle, and then it forwards the calls to a class called ParallaxNode:
// Startable extension Background : Startable { func start() { parallaxNode.start(duration: duration) } func stop() { parallaxNode.stop() } }
The Startable protocol is implemented by forwarding the methods to ParallaxNode.
The idea of implementing scrolling is really straightforward: we implement a node where we put two copies of the same image in a tiled format. We then place the node such that we have the left half fully visible. Then we move the entire node to the left until we fully present the left node. Finally, we reset the position to the original one and restart the cycle.
The following figure explains this algorithm:
import SpriteKit class ParallaxNode { private let node: SKSpriteNode! init(textureNamed: String) { let leftHalf = createHalfNodeTexture(textureNamed, offsetX: 0) let rightHalf = createHalfNodeTexture(textureNamed, offsetX: leftHalf.size.width) let size = CGSize(width: leftHalf.size.width + rightHalf.size.width, height: leftHalf.size.height) node = SKSpriteNode(color: UIColor.whiteColor(), size: size) node.anchorPoint = CGPointZero node.position = CGPointZero node.addChild(leftHalf) node.addChild(rightHalf) } func zPosition(zPosition: CGFloat) -> ParallaxNode { node.zPosition = zPosition return self } func addTo(parentNode: SKSpriteNode) -> ParallaxNode { parentNode.addChild(node) return self } }
The init() method simply creates the two halves, puts them side by side, and sets the position of the node:
// Mark: Private private func createHalfNodeTexture(textureNamed: String, offsetX: CGFloat) -> SKSpriteNode { let node = SKSpriteNode(imageNamed: textureNamed, normalMapped: true) node.anchorPoint = CGPointZero node.position = CGPoint(x: offsetX, y: 0) return node }
The half node is just a node with the correct offset for the x-coordinate:
// Mark: Startable extension ParallaxNode { func start(#duration: NSTimeInterval) { node.runAction(SKAction.repeatActionForever(SKAction.sequence( [ SKAction.moveToX(-node.size.width/2.0, duration: duration), SKAction.moveToX(0, duration: 0) ] ))) } func stop() { node.removeAllActions() } }
Finally, the Startable protocol is implemented using two actions in a sequence. First, we move half the size—which means an image width—to the left, and then
we move the node to the original position to start the cycle again.
This is what the final result looks like:
You can find the code for this version at https://github.com/gscalzo/FlappySwift/tree/stage_with_parallax_levels.
This article has given an idea on how to go about direction that you need to build a clone of the Flappy Bird app. For the complete exercise and a lot more, please refer to Swift by Example by Giordano Scalzo.
Further resources on this subject: