How to Clone Flappy Bird in Swift

Post on 29-Oct-2014

20 views 2 download

Tags:

description

 

Transcript of How to Clone Flappy Bird in Swift

How To Clone

FlappyBirdin

Swift

A little introduction

Who Am I?@giordanoscalzohttps://github.com/gscalzo

Who Am I?@giordanoscalzohttps://github.com/gscalzo

A developer

Who Am I?@giordanoscalzohttps://github.com/gscalzo

An iOS developer

Who Am I?@giordanoscalzohttps://github.com/gscalzo

A Swift beginner

How to implement Hello World in Swift?

println("Hello, World!")

println("Hello, World!")

Not exciting :-(

far to be perfect

but it was fun

instructions

git clonehttp://github.com/gscalzo/FlappySwift.git

./setup

./setup 1

./setup 2

./setup 3...

walking skeleton./setup 1

class GameScene: SKScene { private var screenNode: SKSpriteNode!

override func didMoveToView(view: SKView) { // ... }

override func didMoveToView(view: SKView) { screenNode = SKSpriteNode(color: UIColor.clearColor(), size: self.size) addChild(screenNode)

let backgroundNode = SKSpriteNode(imageNamed: "background") backgroundNode.anchorPoint = CGPointZero backgroundNode.position = CGPointZero screenNode.addChild(backgroundNode)

let groundNode = SKSpriteNode(imageNamed: "ground") groundNode.anchorPoint = CGPointZero groundNode.position = CGPointZero screenNode.addChild(groundNode)}

parallax layers./setup 2

override func didMoveToView(view: SKView) { //... Background(textureNamed: "background").addTo(screenNode).start() Ground(textureNamed: "ground").addTo(screenNode).start()}

protocol Startable { func start() -> Startable func stop() -> Startable}

class Background { private var parallaxNode: ParallaxNode! private let textureName: String

init(textureNamed textureName: String) { }

func addTo(parentNode: SKSpriteNode!) -> Background { return self }}

init(textureNamed textureName: String) { self.textureName = textureName}

func addTo(parentNode: SKSpriteNode!) -> Background { let width = parentNode.size.width let height = parentNode.size.height

parallaxNode = ParallaxNode(width: width, height: height, textureNamed: textureName).addTo(parentNode)

return self}

extension Background : Startable { func start() -> Startable { parallaxNode.start(duration: 20.0) return self }

func stop() -> Startable { parallaxNode.stop() return self }}

class Ground { private var parallaxNode: ParallaxNode! private let textureName: String

init(textureNamed textureName: String) { }

func addTo(parentNode: SKSpriteNode!) -> Ground { return self }}

init(textureNamed textureName: String) { self.textureName = textureName}

func addTo(parentNode: SKSpriteNode!) -> Ground { let width = parentNode.size.width let height = CGFloat(60.0)

parallaxNode = ParallaxNode(width: width, height: height, textureNamed: textureName).zPosition(5).addTo(parentNode) return self}

extension Ground : Startable { func start() -> Startable { parallaxNode.start(duration: 5.0) return self }

func stop() -> Startable { parallaxNode.stop() return self }}

How to implement ParallaxNode?

class ParallaxNode { private let node: SKSpriteNode!

init(width: CGFloat, height: CGFloat, textureNamed: String) { }

private func createNode(textureNamed: String, x: CGFloat) -> SKNode { }

func zPosition(zPosition: CGFloat) -> ParallaxNode { }

func addTo(parentNode: SKSpriteNode) -> ParallaxNode { }

func start(#duration: NSTimeInterval) { }

func stop() { }}

init(width: CGFloat, height: CGFloat, textureNamed: String) { let size = CGSizeMake(2*width, height) node = SKSpriteNode(color: UIColor.whiteColor(), size: size) node.anchorPoint = CGPointZero node.position = CGPointZero node.addChild(createNode(textureNamed, x: 0)) node.addChild(createNode(textureNamed, x: width))}

private func createNode(textureNamed: String, x: CGFloat) -> SKNode { let node = SKSpriteNode(imageNamed: textureNamed, normalMapped: true) node.anchorPoint = CGPointZero node.position = CGPoint(x: x, y: 0) return node}

func zPosition(zPosition: CGFloat) -> ParallaxNode { node.zPosition = zPosition return self}

func addTo(parentNode: SKSpriteNode) -> ParallaxNode { parentNode.addChild(node) return self}

func stop() { node.removeAllActions()}

func start(#duration: NSTimeInterval) { node.runAction(SKAction.repeatActionForever(SKAction.sequence( [ SKAction.moveToX(-node.size.width/2.0, duration: duration), SKAction.moveToX(0, duration: 0) ] )))}

Our hero./setup 3

override func didMoveToView(view: SKView) { physicsWorld.gravity = CGVector(dx: 0, dy: -3) //... bird = Bird(textureNames: ["bird1", "bird2"]).addTo(screenNode) bird.position = CGPointMake(30.0, 400.0) bird.start()}

class Bird { private let node: SKSpriteNode! private let textureNames: [String]

var position : CGPoint { set { node.position = newValue } get { return node.position } }

init(textureNames: [String]) { self.textureNames = textureNames node = createNode() }

func addTo(scene: SKSpriteNode) -> Bird{ scene.addChild(node) return self }}

extension Bird { private func createNode() -> SKSpriteNode { let birdNode = SKSpriteNode(imageNamed: textureNames.first!) birdNode.setScale(1.8) birdNode.zPosition = 2.0

birdNode.physicsBody = SKPhysicsBody(rectangleOfSize: birdNode.size) birdNode.physicsBody!.dynamic = true

return birdNode }}

extension Bird : Startable { func start() -> Startable { animate() return self }

func stop() -> Startable { node.physicsBody!.dynamic = false node.removeAllActions() return self }}

private func animate(){ let animationFrames = textureNames.map { texName in SKTexture(imageNamed: texName) }

node.runAction( SKAction.repeatActionForever( SKAction.animateWithTextures(animationFrames, timePerFrame: 0.1) ))}

func update() { switch node.physicsBody!.velocity.dy { case let dy where dy > 30.0: node.zRotation = (3.14/6.0) case let dy where dy < -100.0: node.zRotation = -1*(3.14/4.0) default: node.zRotation = 0.0 }}

Bird Physics 101

Impulse

func flap() { node.physicsBody!.velocity = CGVector(dx: 0, dy: 0) node.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 6))}

Pipes!

Pipes!./setup 4

class GameScene: SKScene { override func didMoveToView(view: SKView) { //... Pipes(textureNames: ["pipeTop.png", "pipeBottom.png"]).addTo(screenNode).start() }

class Pipes { // class let createActionKey = "createActionKey" // class variables not yet supported private class var createActionKey : String { get {return "createActionKey"} }

private var parentNode: SKSpriteNode! private let textureNames: [String]

init(textureNames: [String]) { self.textureNames = textureNames }

func addTo(parentNode: SKSpriteNode) -> Pipes { self.parentNode = parentNode return self }}

func start() -> Startable { let createAction = SKAction.repeatActionForever( SKAction.sequence( [ SKAction.runBlock { self.createNewPipe() }, SKAction.waitForDuration(3) ] ) )

parentNode.runAction(createAction, withKey: Pipes.createActionKey)

return self}

func stop() -> Startable { parentNode.removeActionForKey(Pipes.createActionKey)

let pipeNodes = parentNode.children.filter { (node: AnyObject?) -> Bool in (node as SKNode).name == PipePair.kind } for pipe in pipeNodes { pipe.removeAllActions() } return self}

private func createNewPipe() { PipePair(textures: textureNames, centerY: centerPipes()).addTo(parentNode).start()}

private func centerPipes() -> CGFloat { return parentNode.size.height/2 - 100 + 20 * CGFloat(arc4random_uniform(10))}

class PipePair { // class let kind = "PIPES" // class variables not yet supported class var kind : String { get {return "PIPES"} }

private let gapSize: CGFloat = 50 private var pipesNode: SKNode! private var finalOffset: CGFloat! private var startingOffset: CGFloat!

init(textures: [String], centerY: CGFloat){ pipesNode = SKNode() pipesNode.name = PipePair.kind

let pipeTop = createPipe(imageNamed: textures[0]) let pipeTopPosition = CGPoint(x: 0, y: centerY + pipeTop.size.height/2 + gapSize) pipeTop.position = pipeTopPosition pipesNode.addChild(pipeTop)

let pipeBottom = createPipe(imageNamed: textures[1]) let pipeBottomPosition = CGPoint(x: 0 , y: centerY - pipeBottom.size.height/2 - gapSize) pipeBottom.position = pipeBottomPosition pipesNode.addChild(pipeBottom)

let gapNode = createGap(size: CGSize( width: pipeBottom.size.width, height: gapSize*2)) gapNode.position = CGPoint(x: 0, y: centerY) pipesNode.addChild(gapNode)

finalOffset = -pipeBottom.size.width/2 startingOffset = -finalOffset}

func start() { pipesNode.runAction(SKAction.sequence( [ SKAction.moveToX(finalOffset, duration: 6.0), SKAction.removeFromParent() ] ))}

private func createPipe(#imageNamed: String) -> SKSpriteNode { let pipeNode = SKSpriteNode(imageNamed: imageNamed) return pipeNode}

private func createGap(#size: CGSize) -> SKSpriteNode { let gapNode = SKSpriteNode(color: UIColor.clearColor(), size: size) return gapNode}

Contact./setup 5

enum BodyType : UInt32 { case bird = 1 // (1 << 0) case ground = 2 // (1 << 1) case pipe = 4 // (1 << 2) case gap = 8 // (1 << 3)}

class GameScene: SKScene {

extension Bird { private func createNode() -> SKSpriteNode { //... birdNode.physicsBody = SKPhysicsBody(rectangleOfSize: birdNode.size) birdNode.physicsBody?.dynamic = true birdNode.physicsBody?.categoryBitMask = BodyType.bird.toRaw() birdNode.physicsBody?.collisionBitMask = BodyType.bird.toRaw() birdNode.physicsBody?.contactTestBitMask = BodyType.world.toRaw() | BodyType.pipe.toRaw() | BodyType.gap.toRaw()

Or better, using a builder closure...

extension Bird { private func createNode() -> SKSpriteNode { //... birdNode.physicsBody = SKPhysicsBody.rectSize(birdNode.size) { body in body.dynamic = true body.categoryBitMask = BodyType.bird.toRaw() body.collisionBitMask = BodyType.bird.toRaw() body.contactTestBitMask = BodyType.world.toRaw() | BodyType.pipe.toRaw() | BodyType.gap.toRaw() }

Handy builder closure extension

extension SKPhysicsBody { typealias BodyBuilderClosure = (SKPhysicsBody) -> ()

class func rectSize(size: CGSize, builderClosure: BodyBuilderClosure) -> SKPhysicsBody { let body = SKPhysicsBody(rectangleOfSize: size) builderClosure(body) return body }}

class Ground { func addTo(parentNode: SKSpriteNode!) -> Ground { //... groundBody.physicsBody = SKPhysicsBody.rectSize(size) { body in body.dynamic = false body.affectedByGravity = false body.categoryBitMask = BodyType.ground.toRaw() body.collisionBitMask = BodyType.ground.toRaw() }

extension PipePair { private func createPipe(#imageNamed: String) -> SKSpriteNode { pipeNode.physicsBody = SKPhysicsBody.rectSize(pipeNode.size) { body in body.dynamic = false body.affectedByGravity = false body.categoryBitMask = BodyType.pipe.toRaw() body.collisionBitMask = BodyType.pipe.toRaw() } } private func createGap(#size: CGSize) -> SKSpriteNode { gapNode.physicsBody = SKPhysicsBody.rectSize(size) { body in body.dynamic = false body.affectedByGravity = false body.categoryBitMask = BodyType.gap.toRaw() body.collisionBitMask = BodyType.gap.toRaw() } }}

extension GameScene: SKPhysicsContactDelegate { func didBeginContact(contact: SKPhysicsContact!) { }

func didEndContact(contact: SKPhysicsContact!) { }}

func didBeginContact(contact: SKPhysicsContact!) { let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

switch (contactMask) { case BodyType.pipe.toRaw() | BodyType.bird.toRaw(): log("Contact with a pipe") case BodyType.ground.toRaw() | BodyType.bird.toRaw(): log("Contact with ground") default: return }

}

func didEndContact(contact: SKPhysicsContact!) { let contactMask = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

switch (contactMask) { case BodyType.gap.toRaw() | BodyType.bird.toRaw(): log("Exit from gap") default: return }}

Final touches./setup 6

class GameScene: SKScene { private var actors: [Startable]! private var score: Score!

override func didMoveToView(view: SKView) { //... let bg = Background(textureNamed: "background").addTo(screenNode) let gr = Ground(textureNamed: "ground").addTo(screenNode) bird = Bird(textureNames: ["bird1", "bird2"]).addTo(screenNode) bird.position = CGPointMake(30.0, 400.0)

let pi = Pipes(textureNames: ["pipeTop.png", "pipeBottom.png"]).addTo(screenNode) actors = [bg, gr, pi, bird]

score = Score().addTo(screenNode)

for actor in actors { actor.start() }}

class Score { private var score: SKLabelNode! private var currentScore = 0

func addTo(parentNode: SKSpriteNode) -> Score { score = SKLabelNode(text: "\(currentScore)") score.fontName = "MarkerFelt-Wide" score.fontSize = 30 score.position = CGPoint(x: parentNode.size.width/2, y: parentNode.size.height - 40) parentNode.addChild(score) return self }

func increase() { currentScore += 1 score.text = "\(currentScore)" }}

func didBeginContact(contact: SKPhysicsContact!) { //... case BodyType.pipe.toRaw() | BodyType.bird.toRaw(): bird.pushDown() case BodyType.ground.toRaw() | BodyType.bird.toRaw(): for actor in actors { actor.stop() }

let shakeAction = SKAction.shake(0.1, amplitudeX: 20) screenNode.runAction(shakeAction)

func didEndContact(contact: SKPhysicsContact!) { //... case BodyType.gap.toRaw() | BodyType.bird.toRaw(): score.increase()

extension Bird { func flap() { if !dying { node.physicsBody!.velocity = CGVector(dx: 0, dy: 0) node.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 6)) } }

func pushDown() { dying = true node.physicsBody!.applyImpulse(CGVector(dx: 0, dy: -10)) }

Into the Cave./setup 7

override func didMoveToView(view: SKView) { let textures = Textures.cave() let bg = Background(textureNamed: textures.background).addTo(screenNode) let te = Ground(textureNamed: textures.ground).addTo(screenNode) bird = Bird(textureNames: textures.bird).addTo(screenNode) let pi = Pipes(textureNames: textures.pipes).addTo(screenNode)

struct Textures { let background: String let ground: String let pipes: [String] let bird: [String]

static func classic() -> Textures { return Textures( background: "background.png", ground: "ground.png", pipes: ["pipeTop.png", "pipeBottom.png"], bird: ["bird1", "bird2"]) }

static func cave() -> Textures { return Textures( background: "cave_background.png", ground: "cave_ground.png", pipes: ["cave_pipeTop.png", "cave_pipeBottom.png"], bird: ["bird1", "bird2"]) }}

extension Bird { private func addLightEmitter() { let light = SKLightNode() light.categoryBitMask = BodyType.bird.toRaw() light.falloff = 1 light.ambientColor = UIColor.whiteColor() light.lightColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5) light.shadowColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5) node.addChild(light) }}

extension PipePair { private func createPipe(#imageNamed: String) -> SKSpriteNode { let pipeNode = SKSpriteNode(imageNamed: imageNamed) pipeNode.shadowCastBitMask = BodyType.bird.toRaw() pipeNode.physicsBody = SKPhysicsBody.rectSize(pipeNode.size) {

private func createNode(textureNamed: String, x: CGFloat) -> SKNode { let node = SKSpriteNode(imageNamed: textureNamed, normalMapped: true) node.lightingBitMask = BodyType.bird.toRaw()

What if

Michael Baywas the designer?

./setup 8

Explosions!

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { switch touches.count { case 1: bird.flap() default: shoot() }}

extension GameScene { private func shoot(#emitterName: String, finalYPosition: CGFloat) { let fireBoltEmmitter = SKEmitterNode.emitterNodeWithName(emitterName) fireBoltEmmitter.position = bird.position fireBoltEmmitter.physicsBody = SKPhysicsBody.rectSize(CGSize(width: 20, height: 20)) { body in body.dynamic = true body.categoryBitMask = BodyType.bomb.toRaw() body.collisionBitMask = BodyType.bomb.toRaw() body.contactTestBitMask = BodyType.pipe.toRaw() } screenNode.addChild(fireBoltEmmitter)

fireBoltEmmitter.runAction(SKAction.sequence( [ SKAction.moveByX(500, y: 100, duration: 1), SKAction.removeFromParent() ])) }

private func shoot() { shoot(emitterName: "fireBolt", finalYPosition: 1000) }

Game menu

Leaderboard

Swifterims!

Please: Fork and PRsgithub.com/gscalzo/FlappySwift

credits

SKLightNode tutorialhttp://www.ymc.ch/en/playing-with-ios-8-sprite-kit-and-sklightnode

Assets for Cave versionhttp://www.free-pobo.com

Thank you!

Questions?