How to make flappy bird.

First, let’s talk about everything we need. This is from scratch, so we need to think of everything. We need some sprites. These sprites can represent the land, the background, the bird, and the pipes, of course. Cool, we have all of that fun stuff. Now, let’s talk about design.

You might notice that the ground moves faster than the background does, yet both are moving. This is a feature in games called Parallax scrolling. We’re going to implement that today, simply by making the floor scroll faster than the background sky.

But first, we have a problem. This game is an infinite sidescroller—and we don’t have an infinite sprite for our floor. How do we fix this problem?

Bueller?

We just make a couple of copies of the sprite as needed. Then, make each have the same animation: travel some distance (the width of the sprite) and then reset your position. This gives the illusion of infinite scrolling.

The first thing we need is a struct for scrolling. Make a new file called GameConstants and just have static let’s in there.

We’re going to usually need 2 + screenWidth / spriteWidth sprites to make this animation going to happen.  This is a general number that can apply to anything we’d like, and we can go over the explanation, if you’d like.

So, let’s make a subclass of SKNode and call it InfiniteRepeatingScrollingNode or something to that effect.. This subclass will take in 4 things: 1) the image name of the sprite texture; 2) the scroll speed—this should be a small number that we’ll somehow incorporate into our code. Let’s make a new type for this: 

typealias SKScrollSpeed = UInt8

Yay you guys have seen your first typealias. This is like a typedef. Woohoo! 

3) The height that you want this shit repeating at, and 4) The width of the screen, so we know how many to repeat. This (fortunately) takes into account iPhone screens that are miles wide. lol.

class InfiniteRepeatingScrollingNode: SKNode {
    convenience init(imageNamed: String, scrollSpeed: SKScrollSpeed, spriteHeight: CGFloat, screenWidth: CGFloat) {
        self.init()
        let spriteTexture = SKTexture(imageNamed: imageNamed)
        spriteTexture.filteringMode = .Nearest
        let spriteWidth = spriteTexture.size().width * GameConstants.spriteScale
        
        let moveSprite = SKAction.moveByX(-spriteWidth, y: 0,
                                          duration: NSTimeInterval((1.0 / CGFloat(scrollSpeed)) * spriteWidth))
        let resetSprite = SKAction.moveByX(spriteWidth, y: 0, duration: 0.0)
        let moveSpriteForever = SKAction.repeatActionForever(SKAction.sequence([moveSprite, resetSprite]))
        
        for (var i: CGFloat = 0.0; i < 2.0 + (screenWidth / spriteWidth); ++i) {
            let sprite = SKSpriteNode(texture: spriteTexture)
            sprite.setScale(GameConstants.spriteScale)
            sprite.position = CGPointMake(i * spriteWidth, sprite.size.height / 2.0 + spriteHeight)
            sprite.runAction(moveSpriteForever)
            self.addChild(sprite)
        }
    }
}

Filtering mode: SpriteKit will automatically make things that are far away a little bit blurrier. Apparently this gives the illusion of far away things. If we change it to .Nearest, this eliminates this stupid blurring effect. Because the sprite we are given is small, we up the scale by 2. This also helps boost the pixel-y effect. There are two animations: one moves it to the left by Width and the other moves it to the right by Width, immediately. We then combine them in an infinite animation. We then apply this to each of the sprites that we’ve copied. We set the scale, set the position, and then apply the action.

We then use SKNodes to group these together into one node. You’ll see throughout this app how nice it is to have hierarchy like this. We can basically treat all of the moving parts of one part.

So, now, instances of this class set everything for us. Let’s finish setting up our background in GameScene.swift

extension SKColor {
    class func skyColor() -> SKColor {
        return SKColor(red: GameConstants.skyRed,
                       green: GameConstants.skyGreen,
                       blue: GameConstants.skyBlue,
                       alpha: GameConstants.skyAlpha)
    }
}

class GameScene: SKScene {

    var moving: SKNode!
    
    override func didMoveToView(view: SKView) {
        self.backgroundColor = SKColor.skyColor()
        moving = SKNode()
        self.addChild(moving)
        
        let landScroller = InfiniteRepeatingScrollingNode(
                imageNamed: "land", scrollSpeed: GameConstants.groundSpeed,
                spriteHeight: 0, screenWidth: self.frame.size.width)
        let textureHeight = (landScroller.children[0] as SKSpriteNode).size.height
        let skyScroller = InfiniteRepeatingScrollingNode(
            imageNamed: "sky", scrollSpeed: GameConstants.skySpeed,
            spriteHeight: textureHeight, screenWidth: self.frame.size.width)
        
        moving.addChild(landScroller)
        moving.addChild(skyScroller)
    }

}

Run it—dooooooope! Okay awesome, next step. Let’s add the bird and some physics!

let birdTexture1 = SKTexture(imageNamed: "bird-01")
let birdTexture2 = SKTexture(imageNamed: "bird-02")
birdTexture1.filteringMode = .Nearest
birdTexture2.filteringMode = .Nearest
        
let flapForever = SKAction.repeatActionForever(SKAction.animateWithTextures([birdTexture1, birdTexture2], timePerFrame: GameConstants.birdFlapTime))
        
self.bird = SKSpriteNode(texture: birdTexture1)
bird.setScale(GameConstants.spriteScale)
bird.position = CGPoint(x: self.frame.size.width * GameConstants.birdXRelativeToScreenWidth, y: self.frame.size.height * GameConstants.birdYRelativeToScreenHeight)
bird.runAction(flapForever)
        
bird.physicsBody = SKPhysicsBody(circleOfRadius: bird.size.height / 2.0)
bird.physicsBody?.dynamic = true
bird.physicsBody?.allowsRotation = false
        
self.addChild(self.moving)
self.addChild(self.bird)

WHOOP the bird goes through the floor. What are we missing? Oops we need a floor.

let ground = SKNode()
ground.position = CGPointMake(0, landTextureHeight / 2.0)
ground.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(self.frame.size.width, landTextureHeight))
ground.physicsBody?.dynamic = false

How about when we tap, we move? That’s a nice thing to have.

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        self.bird.physicsBody?.velocity = CGVectorMake(0, 0)
        self.bird.physicsBody?.applyImpulse(CGVectorMake(0, 40))
    }

Some of the physics numbers are tweak able. We should change the gravity to -10 y. I’ve never played FlappyBird so I’m not sure how close this gets to the actual. But let’s role with it.

We need to set the velocity to zero before each impulse. This makes the game harder. Why?

Okay hang on, in flappy bird, the bird rotated downwards on falls and upwards on raises. Basically, the rotation was dependent on y-velocity. We can say. (WHICH AXIS BTW?)

    override func update(currentTime: NSTimeInterval) {
        self.bird.zRotation = self.bird.physicsBody!.velocity.dy * 0.003
    }

This looks retarded—he’s doing flips! We can help this by saying:

    override func update(currentTime: NSTimeInterval) {
        let dy = self.bird.physicsBody!.velocity.dy
        if (dy < 0) {
            self.bird.zRotation = max(-1, self.bird.physicsBody!.velocity.dy * 0.003)
        } else {
            self.bird.zRotation = min(0.5, self.bird.physicsBody!.velocity.dy * 0.001)
        }
    }

Much more reasonable! Let’s just make those constants in GameConstants and we’re good. Now, let’s talk pipes! Pipes are generated randomly, so we’ll need to set us up to handle them from a function which will randomly generate them. We can accomplish this with blocks.

// Pipes, texture and animations.
        self.pipes = SKNode()
        let pipeTextureBottom = SKTexture(imageNamed: "PipeUp")
        let pipeTextureTop = SKTexture(imageNamed: "PipeDown")
        pipeTextureBottom.filteringMode = .Nearest
        pipeTextureTop.filteringMode = .Nearest
        
        let distanceToMove = CGFloat(self.frame.size.width + 2.0 * pipeTextureBottom.size().width)
        let movePipes = SKAction.moveByX(-distanceToMove, y:0.0, duration: NSTimeInterval(0.01 * distanceToMove))
        let removePipes = SKAction.removeFromParent()
        let movePipesAndRemove = SKAction.sequence([movePipes, removePipes])
        
        let spawn = SKAction.runBlock { () -> Void in
            self.spawnPipes(bottom: pipeTextureBottom, top: pipeTextureTop, animation: movePipesAndRemove)
        }
        let delay = SKAction.waitForDuration(NSTimeInterval(2.0))
        let spawnThenDelay = SKAction.sequence([spawn, delay])
        self.runAction(SKAction.repeatActionForever(spawnThenDelay))

    func spawnPipes(#bottom: SKTexture, top: SKTexture, animation: SKAction) {
        let pipePair = SKNode()
        pipePair.position = CGPointMake(self.frame.size.width + bottom.size().width * 2, 0)
        let maxHeight = UInt32(self.frame.size.height / 4)
        let bottomHeight = arc4random() % maxHeight + maxHeight
        
        let bottomPipe = SKSpriteNode(texture: bottom)
        bottomPipe.setScale(GameConstants.spriteScale)
        bottomPipe.position = CGPointMake(0.0, CGFloat(bottomHeight))
        bottomPipe.physicsBody = SKPhysicsBody(rectangleOfSize: bottomPipe.size)
        bottomPipe.physicsBody?.dynamic = false
        
        let topPipe = SKSpriteNode(texture: top)
        topPipe.setScale(GameConstants.spriteScale)
        topPipe.position = CGPointMake(0.0, CGFloat(bottomHeight) + topPipe.size.height + GameConstants.pipeGap)
        topPipe.physicsBody = SKPhysicsBody(rectangleOfSize: topPipe.size)
        topPipe.physicsBody?.dynamic = false
        
        pipePair.addChild(bottomPipe)
        pipePair.addChild(topPipe)
        
        pipePair.runAction(animation)
        self.pipes.addChild(pipePair)
    }

Two problems: the pipes are in front of the ground. We can set this by letting zPosition of the pipes be something negative. Gotta be careful. Let’s talk about z levels. We want everything to be nice nice.

Okay cool. The second problem: WE HAVE NO COLLISIONS. No accountability whatsoever. No score. No nothing! Let’s take collisions incrementally. First, we’ll do collision with the floor. First, we set up some contact bit masks. Notice that, by default, everything collides with everything. The difference between collision and contact is the collision applies physics—contact just makes a function call.

    let birdCategory: UInt32 = 1 << 0
    let worldCategory: UInt32 = 1 << 1
    let pipeCategory: UInt32 = 1 << 2

        self.physicsWorld.contactDelegate = self

        bird.physicsBody?.categoryBitMask = self.birdCategory
        bird.physicsBody?.collisionBitMask = self.worldCategory | self.pipeCategory
        bird.physicsBody?.contactTestBitMask = self.worldCategory | self.pipeCategory

    func didBeginContact(contact: SKPhysicsContact) {
        if (contact.bodyA.categoryBitMask & (self.worldCategory | self.pipeCategory) != 0 ||
            contact.bodyB.categoryBitMask & (self.worldCategory | self.pipeCategory) != 0) {
            self.moving.speed = 0
        }
    }

The reason we’re combining all of the moving parts into the same node is so that we can stop the movement as necessary. We can also add an animation to say “HEY GAME OVER”

GO INCREMENTALLY

    func didBeginContact(contact: SKPhysicsContact) {
        if (self.moving.speed == 1) {
            if (contact.bodyA.categoryBitMask & (self.worldCategory | self.pipeCategory) != 0 ||
                contact.bodyB.categoryBitMask & (self.worldCategory | self.pipeCategory) != 0) {
                self.moving.speed = 0
                self.bird.physicsBody?.collisionBitMask = self.worldCategory  // What will this do?
                self.bird.speed = 0
                self.runAction(SKAction.sequence([SKAction.repeatAction(SKAction.sequence([SKAction.runBlock({
                    self.backgroundColor = SKColor(red: 1, green: 0, blue: 0, alpha: 1.0)
                }),SKAction.waitForDuration(NSTimeInterval(0.05)), SKAction.runBlock({
                    self.backgroundColor = SKColor.skyColor()
                }), SKAction.waitForDuration(NSTimeInterval(0.05))]), count:4), SKAction.runBlock({
                    self.canRestart = true
                })]), withKey: "flash")
            }
        }
    }

How can we implement points?

Well, first, let’s implement the labels and whatnot.

    var score: UInt = 0
    var scoreLabelNode: SKLabelNode!

        self.scoreLabelNode = SKLabelNode(fontNamed:"MarkerFelt-Wide")
        self.scoreLabelNode.position = CGPointMake(CGRectGetMidX(self.frame),
                                                   3 * self.frame.size.height / 4)
        self.scoreLabelNode.zPosition = 1
        self.scoreLabelNode.text = String(self.score)

Right, we implement score with a contact test. At the right edge of each pipe, in between the pipes, we have a contact test.

        let scoreContact = SKNode()
        scoreContact.position = CGPointMake(topPipe.size.width + self.bird.size.width / 2, CGRectGetMidY(self.frame))
        scoreContact.physicsBody = SKPhysicsBody(rectangleOfSize: CGSizeMake(bottomPipe.size.width, self.frame.size.height))
        scoreContact.physicsBody?.dynamic = false
        scoreContact.physicsBody?.categoryBitMask = self.scoreCategory
        scoreContact.physicsBody?.contactTestBitMask = self.birdCategory
        
        pipePair.addChild(bottomPipe)
        pipePair.addChild(topPipe)
        pipePair.addChild(scoreContact)

 else {
                self.score++
                self.scoreLabelNode.text = String(self.score)
                self.runAction(SKAction.sequence([SKAction.repeatAction(SKAction.sequence([SKAction.runBlock({
                    self.backgroundColor = SKColor(red: 0, green: 1, blue: 0, alpha: 1.0)
                }),SKAction.waitForDuration(NSTimeInterval(0.05)), SKAction.runBlock({
                    self.backgroundColor = SKColor.skyColor()
                }), SKAction.waitForDuration(NSTimeInterval(0.05))]), count:4)]), withKey: "flashGreen")
            }

Finally, 

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        if (self.moving.speed == 1) {
            self.bird.physicsBody?.velocity = CGVectorMake(0, 0)
            self.bird.physicsBody?.applyImpulse(CGVectorMake(0, 40))
        } else if self.canRestart {
            self.restart()
        }
    }
    
    func restart() {
        self.bird.position = CGPoint(x: self.frame.size.width * GameConstants.birdXRelativeToScreenWidth, y: self.frame.size.height * GameConstants.birdYRelativeToScreenHeight)
        self.bird.physicsBody?.velocity = CGVectorMake(0,0)
        self.bird.physicsBody?.collisionBitMask = self.worldCategory | self.pipeCategory
        self.bird.speed = 1.0
        self.bird.zRotation = 0.0
        
        self.pipes.removeAllChildren()
        
        self.score = 0
        self.scoreLabelNode.text = String(self.score)
        
        self.canRestart = false
        self.moving.speed = 1
	}

Things to do: proper score screen (included in Images.xcassets), handling ceiling events, progressive speed up, etc.