Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

How to Clone Flappy Bird in Swift

16,671 views

Published on

Published in: Mobile, Software
  • Hey guys! Who wants to chat with me? More photos with me here 👉 http://www.bit.ly/katekoxx
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Nice !! Download 100 % Free Ebooks, PPts, Study Notes, Novels, etc @ https://www.ThesisScientist.com
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Hello! Get Your Professional Job-Winning Resume Here - Check our website! https://vk.cc/818RFv
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • I have made a similar game but my optimization is terrible. I'm only getting ~20 fps. How can I improve this?
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

How to Clone Flappy Bird in Swift

  1. How To Clone FlappyBird in Swift
  2. A little introduction
  3. Who Am I? @giordanoscalzo https://github.com/gscalzo
  4. Who Am I? @giordanoscalzo https://github.com/gscalzo A developer
  5. Who Am I? @giordanoscalzo https://github.com/gscalzo An iOS developer
  6. Who Am I? @giordanoscalzo https://github.com/gscalzo A Swift beginner
  7. How to implement Hello World in Swift?
  8. println("Hello, World!")
  9. println("Hello, World!") Not exciting :-(
  10. far to be perfect
  11. but it was fun
  12. instructions
  13. git clone http://github.com/gscalzo/FlappySwift.git
  14. ./setup
  15. ./setup 1 ./setup 2 ./setup 3 ...
  16. walking skeleton ./setup 1
  17. class GameScene: SKScene { private var screenNode: SKSpriteNode! override func didMoveToView(view: SKView) { // ... }
  18. 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) }
  19. parallax layers ./setup 2
  20. override func didMoveToView(view: SKView) { //... Background(textureNamed: "background").addTo(screenNode).start() Ground(textureNamed: "ground").addTo(screenNode).start() }
  21. protocol Startable { func start() -> Startable func stop() -> Startable }
  22. class Background { private var parallaxNode: ParallaxNode! private let textureName: String init(textureNamed textureName: String) { } func addTo(parentNode: SKSpriteNode!) -> Background { return self } }
  23. 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 }
  24. extension Background : Startable { func start() -> Startable { parallaxNode.start(duration: 20.0) return self } func stop() -> Startable { parallaxNode.stop() return self } }
  25. class Ground { private var parallaxNode: ParallaxNode! private let textureName: String init(textureNamed textureName: String) { } func addTo(parentNode: SKSpriteNode!) -> Ground { return self } }
  26. 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 }
  27. extension Ground : Startable { func start() -> Startable { parallaxNode.start(duration: 5.0) return self } func stop() -> Startable { parallaxNode.stop() return self } }
  28. How to implement ParallaxNode?
  29. 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() { } }
  30. 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)) }
  31. 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 }
  32. func zPosition(zPosition: CGFloat) -> ParallaxNode { node.zPosition = zPosition return self } func addTo(parentNode: SKSpriteNode) -> ParallaxNode { parentNode.addChild(node) return self } func stop() { node.removeAllActions() }
  33. func start(#duration: NSTimeInterval) { node.runAction(SKAction.repeatActionForever(SKAction.sequence( [ SKAction.moveToX(-node.size.width/2.0, duration: duration), SKAction.moveToX(0, duration: 0) ] ))) }
  34. Our hero ./setup 3
  35. 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() }
  36. 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 } }
  37. 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 } }
  38. extension Bird : Startable { func start() -> Startable { animate() return self } func stop() -> Startable { node.physicsBody!.dynamic = false node.removeAllActions() return self } }
  39. private func animate(){ let animationFrames = textureNames.map { texName in SKTexture(imageNamed: texName) } node.runAction( SKAction.repeatActionForever( SKAction.animateWithTextures(animationFrames, timePerFrame: 0.1) )) }
  40. 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 } }
  41. Bird Physics 101
  42. Impulse
  43. func flap() { node.physicsBody!.velocity = CGVector(dx: 0, dy: 0) node.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 6)) }
  44. Pipes!
  45. Pipes! ./setup 4
  46. class GameScene: SKScene { override func didMoveToView(view: SKView) { //... Pipes(textureNames: ["pipeTop.png", "pipeBottom.png"]).addTo(screenNode).start() }
  47. 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 } }
  48. func start() -> Startable { let createAction = SKAction.repeatActionForever( SKAction.sequence( [ SKAction.runBlock { self.createNewPipe() }, SKAction.waitForDuration(3) ] ) ) parentNode.runAction(createAction, withKey: Pipes.createActionKey) return self }
  49. 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 }
  50. 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)) }
  51. 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!
  52. 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 }
  53. func start() { pipesNode.runAction(SKAction.sequence( [ SKAction.moveToX(finalOffset, duration: 6.0), SKAction.removeFromParent() ] )) }
  54. 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 }
  55. Contact ./setup 5
  56. 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 {
  57. 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()
  58. 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() }
  59. 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 } }
  60. 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() }
  61. 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() } } }
  62. extension GameScene: SKPhysicsContactDelegate { func didBeginContact(contact: SKPhysicsContact!) { } func didEndContact(contact: SKPhysicsContact!) { } }
  63. 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 } }
  64. 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 } }
  65. Final touches ./setup 6
  66. class GameScene: SKScene { private var actors: [Startable]! private var score: Score!
  67. 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() } }
  68. 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)" } }
  69. 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)
  70. func didEndContact(contact: SKPhysicsContact!) { //... case BodyType.gap.toRaw() | BodyType.bird.toRaw(): score.increase()
  71. 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)) }
  72. Into the Cave ./setup 7
  73. 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)
  74. 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"]) } }
  75. 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) } }
  76. extension PipePair { private func createPipe(#imageNamed: String) -> SKSpriteNode { let pipeNode = SKSpriteNode(imageNamed: imageNamed) pipeNode.shadowCastBitMask = BodyType.bird.toRaw() pipeNode.physicsBody = SKPhysicsBody.rectSize(pipeNode.size) {
  77. private func createNode(textureNamed: String, x: CGFloat) -> SKNode { let node = SKSpriteNode(imageNamed: textureNamed, normalMapped: true) node.lightingBitMask = BodyType.bird.toRaw()
  78. What if Michael Bay was the designer? ./setup 8
  79. Explosions!
  80. override func touchesBegan(touches: NSSet, withEvent event: UIEvent) { switch touches.count { case 1: bird.flap() default: shoot() } }
  81. 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) }
  82. Game menu
  83. Leaderboard
  84. Swifterims!
  85. Please: Fork and PRs github.com/gscalzo/FlappySwift
  86. credits SKLightNode tutorial http://www.ymc.ch/en/playing-with-ios-8-sprite-kit-and-sklightnode Assets for Cave version http://www.free-pobo.com
  87. Thank you!
  88. Questions?

×