How to Clone Flappy Bird in Swift
-
Upload
giordano-scalzo-scalzo -
Category
Mobile
-
view
24 -
download
2
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?