Parkgate Pigeons

  1. Create a new SpriteKit game called ParkgatePigeons, using Swift as the language, SpriteKit as the Game Technology and for the iPhone device
  2. Set the game to use portrait orientation only: in the project navigator (left hand side of Xcode) select the project. Under the deployment info settings, ensure only the Portrait device orientation is checked.
  3. Add a new Swift file to the project, and name it Pigeon.swift
  4. Download and unzip the assets for the game, then copy them into your project (ensuring you check the 'copy items if needed' checkbox )
  5. In the Pigeon.swift file add an import statement to import the SpriteKit framework:
    import SpriteKit
  6. Add the code shown below, which will create a protocol - we will use this to enable the pigeon to let the game know when it has been killed:
    protocol PigeonDelegate{
        func pigeonKilled()
    }
  7. In the same file, (but outside the protocol) add a swift class, extending SKSpriteNode which will represent a pigeon, and within the class add a variable which will be used to represent the game (i.e. the delegate):
    class Pigeon : SKSpriteNode {
        var delegate : PigeonDelegate?
    }
  8. The class needs to be instantiated when created with the correct image, and it also needs to be able to respond to touch events. Add the following method within the class:
    init(){
        let texture = SKTexture(imageNamed: "pigeon-alive.png")
        super.init(texture: texture, color: UIColor.clear, size: texture.size())
        name = "pigeon"
        isUserInteractionEnabled = true
    }
  9. Xcode will be showing an error, saying that a required initializer is not present, using the 'Fix-It' option on the error in Xcode, add the code to resolve this (shown below):
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    (although the code in the newly added method indicates that if it is called an error will be thrown, we do not need to worry about it, unless we decide to implement some form of game state preservation at a later date)
  10. Finally add the code below to handle killing the pigeon when it is touched:
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        physicsBody = SKPhysicsBody(rectangleOf: size)
            
        //call the pigeonKilled Method, only if the delegate exists
        delegate?.pigeonKilled()
    }

Setting up the game scene

  1. Within GameViewController.swiftModify the supportedInterfaceOrientations() method to appear as shown below:
    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
        return UIInterfaceOrientationMask.Portrait
    }
  2. Also within GameViewController.swift, inside the viewDidLoad() method, before the final line, add this code to ensure the scenes size matches that of the portrait view:
    scene.size = skView.frame.size
  3. Open the GameScene.swift file and modify the class declaration to read as follows:
    class GameScene: SKScene, PigeonDelegate {
  4. Within GameScene.swift delete the touchesBegan and update functions and their contents.
  5. Xcode will show another warning, as we have not implemented the delegate method (to handle the killing of a pigeon). In the class, but outside any other method implement the pigeonKilled method as shown below:
    func pigeonKilled() {
            
        }
    We will come back to this later.
  6. The game needs to keep track of how often we will send (seed) new pigeons across the screen, how long the level lasts and which level we are on. Set these up in at the top of the GameScene class with some default values:
    var seedInterval = 1.0
    var level = 1
    var levelDuration = 10
    
  7. Also add the following variables to keep track of the number of pigeons, the score, how many pigeons have been hit, the timer and the score label:
    private var pigeonCount = 0
    private var score = 0
    private var hits = 0
    private var seedTimer = Timer()
    private var scoreLabel = SKLabelNode(fontNamed: "Chalkduster")
    private var levelLabel = SKLabelNode(fontNamed: "Chalkduster")
  8. Now the relevant variables are in place, go back to the pigeonKilled method, and update it as follows:
    func pigeonKilled() {
        score += 10
        hits += 1
        scoreLabel.text = "Score: \(score)"
    }
  9. Next add a new method called seedPigeon which, when called will create, position and then animate a pigeon across the screen. The vertical position (height of flight) and starting point (left or right side of the screen) will be randomised:
    @objc func seedPigeon(){
        pigeonCount += 1
        
        //randomly decide to fly the other way (1 in 2)
        let flyRightToLeft = (1 == arc4random_uniform(2))
        
        let sprite = Pigeon()
        
        //define possible starting positions
        let leftStartPosition = 0 - (sprite.frame.size.width * 0.5)
        let rightStartPosition = frame.size.width + (0.5 * sprite.frame.size.width)
        
        //set the starting position (either offset all the way to the right, or the sprite's width offset to the left)
        //this line of code is essentially a compact form of an 'if' statement known as a ternary operator
        let xPosition = flyRightToLeft ? rightStartPosition : leftStartPosition
        
        //if flying the pother way, we can get a -1 value to modify the flying direction
        let directionModifier = flyRightToLeft ? -1.0 : +1.0
        
        //get a position between 100 pixels high and 400 to start the pigeon
        var yPosition = CGFloat(arc4random_uniform(300) + 200)
        
        //offset for tall screens
        yPosition += frame.maxX - 480.0
        
        //set the position
        sprite.position = CGPoint(x: xPosition, y: yPosition)
        
        //get a random duration for animation
        let randomduration = Float((arc4random_uniform(100) + 1)/100)
        
        //make faster as game progresses (shorter duration)
        let durationIncrement = abs((100.0 - Float(pigeonCount)) / 100.0)
        
        //combine random duration and increment
        var duration = randomduration + durationIncrement + 0.5
        
        //determine flight movement on X axis
        var flightMotion : CGFloat =  frame.size.width + (2.0 * sprite.size.width)
        
        //modify direction
        flightMotion *= CGFloat(directionModifier)
        
        //create a flying animation
        let fly = SKAction.moveBy(x: flightMotion, y: CGFloat(0.0), duration: TimeInterval(duration))
        
        //run animation
        sprite.run(fly)
        
        //place it on screen
        addChild(sprite)
        
        //ensure the pigeon delegates to the game when killed
        sprite.delegate = self
    }
    Note that the function declaration has to be prefixed with @objc to that we can call it using a timer later on
  10. We will use a timer to fly the pigeons, and another one with a delay the length of the level to stop seeding them, We need to makes sense to write the stop seeding method first, so add that as follows:
    @objc func stopSeeding(){
        seedTimer.invalidate()
    }
  11. Next add the method to start the game off and fire the timers:
    func startGame(){ 
        //reset counters
        hits = 0
        score = 0
        pigeonCount = 0
        
        levelLabel.text = "Level: \(level)"
        
        //introduce the level
        let introLabel = SKLabelNode(fontNamed: "Chalkduster")
        introLabel.text = "Level \(level)"
        introLabel.fontSize = 40
        introLabel.position = CGPoint(x: frame.width/2, y: frame.height/2)
        introLabel.horizontalAlignmentMode = .center
        introLabel.color = SKColor(white: 0, alpha: 1)
        addChild(introLabel)
        
        //create series of animations
        let wait = SKAction.wait(forDuration: 0.5)
        let fadeOut = SKAction.fadeAlpha(to: 0.0, duration: 1.0)
        //this animation will set off the timers
        let start = SKAction.run({
            self.seedTimer = Timer.scheduledTimer(timeInterval: self.seedInterval, target: self, selector: #selector(GameScene.seedPigeon), userInfo: nil, repeats: true)
            Timer.scheduledTimer(timeInterval: TimeInterval(self.levelDuration), target: self, selector: #selector(GameScene.stopSeeding) , userInfo: nil, repeats: false)
            })
        
        //run the animations in order
        introLabel.run(SKAction.sequence([wait,fadeOut,start]))
    }
    
  12. Once the pigeons have stopped seeding, we will need to run a check to see if the level has been completed. Add a ga,e over method as follows:
    @objc func gameOver(){     
       if hits == pigeonCount{
           level += 1
           startGame()
       }
    }
  13. We also need to allow time for the last pigeons to fly across the screen before calling the @objc gameOver method, so add the following code to the stopSeeding Method:
    func stopSeeding(){
        seedTimer.invalidate()
        Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(GameScene.gameOver), userInfo: nil, repeats: false)
    }
  14. Finally we need to add code to the didMove(to:) method to setup the scene - its background, and labels as well as start the game. Delete any existing content from the didMove(to:) method so it appears as shown below:
    override func didMove(to view: SKView) {
     
        backgroundColor = UIColor.black
        
        let background = SKSpriteNode(imageNamed: "background.png")
        background.position = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
        let scaleFactor = background.size.width / frame.size.width
        background.size.width = frame.size.width
        background.size.height = background.size.height / scaleFactor
        background.zPosition = -10
        addChild(background)
        
        scoreLabel.text = "Score: 0"
        scoreLabel.fontSize = 12
        scoreLabel.position = CGPoint(x: 10, y: 10)
        scoreLabel.horizontalAlignmentMode = .left
        addChild(scoreLabel)
        
        levelLabel.text = "Level: \(level)"
        levelLabel.fontSize = 12
        levelLabel.position = CGPoint(x: frame.width-10, y: 10)
        levelLabel.horizontalAlignmentMode = .right
        addChild(levelLabel)
        
        startGame()
    }
  15. Run the game and it should be playable

The next stage is to implement level selection and adapt the game so it gets harder. Continue with the Level selection tutorial