いまさらFrappyBirdを作ってみる③

前回は背景と床をScenenに配置しました。

今回は残りの土管や背景色などを追加し、読みにくくなってきたのでコンポーネントごとに切り出してリファクタリングを行いました。

土管を配置する

コード

var pipes:SKNode!

override func didMove(to view: SKView) {
   ...省略
    pipes = SKNode()
    moving.addChild(pipes)
    
    let spawn = SKAction.run(spawnPipes)
    let delay = SKAction.wait(forDuration: TimeInterval(2.5))
    let spawnThenDelay = SKAction.sequence([spawn, delay])
    let spawnThenDelayForever = SKAction.repeatForever(spawnThenDelay)
    self.run(spawnThenDelayForever)
}

func spawnPipes() {
    let deviceWidth = self.frame.size.width
    let deviceHeight = self.frame.size.height
    
    let groundTextureHeight = SKTexture(imageNamed: "land").size().height
    
    let pipeTextureDown = SKTexture(imageNamed: "PipeDown")
    pipeTextureDown.filteringMode = .nearest
    let pipeTextureUp = SKTexture(imageNamed: "PipeUp")
    pipeTextureUp.filteringMode = .nearest
    
    //土管が移動して消える
    let distanceToMove = CGFloat(deviceWidth + 2.0 * pipeTextureUp.size().width)
    let movePipes = SKAction.moveBy(x: -distanceToMove, y:0.0, duration:TimeInterval(0.01 * distanceToMove))
    let removePipes = SKAction.removeFromParent()
    let movePipesAndRemove = SKAction.sequence([movePipes, removePipes])
    
    let pipePair = SKNode()
    pipePair.position = CGPoint( x: deviceWidth + pipeTextureUp.size().width * 2, y:  0)
    pipePair.zPosition = -10
    
    //上から伸びるパイプ
    let pipeDown = SKSpriteNode(texture: pipeTextureDown)
    pipeDown.setScale(3.5)
    pipeDown.position = CGPoint(x: 0.0, y: deviceHeight - pipeDown.size.height / 2)

    //下から伸びるパイプ
    let pipeUp = SKSpriteNode(texture: pipeTextureUp)
    pipeUp.setScale(3.5)
    pipeUp.position = CGPoint(x: 0.0, y: pipeUp.size.height / 2 - groundTextureHeight)
    
    pipePair.addChild(pipeDown)
    pipePair.addChild(pipeUp)
    pipePair.run(movePipesAndRemove)
    
    //画面からはみ出している分だけ親のpipePairNodeのY座標を動かしてパイプの位置を変更
    pipePair.position.y = pipePair.position.y + CGFloat.random(in: 0...pipeUp.frame.height / 2)

    pipes.addChild(pipePair)
}

SKAction.runについて

本家のコードをみて、runにメソッド渡してる?今まで、アニメーションさせるために作られたメソッドなのかと考えていたので、そんなことできるのかと疑問に思ったのですが、定義元を確認すると以下のように定義されていました。

open class func run(_ block: @escaping () -> Void) -> SKAction

クロージャ で定義されていたのでそのまま処理を渡すことができます。実際に処理を走らせる時はSKAction.sequenceに配列を渡して処理を行いますが、sequenceに入れられたQueueを1つづつ取り出すみたいな感じなんだと思います。

パイプを上下させる処理

こちら本家で何やってるのかよくわからなかったので、わかりやすいように処理を置き換えてしまいました。

画像のようにパイプの半分を画面内に表示させてpipePairNodeに突っ込んでいます。

そして、pipePairNodeの位置はパイプの半分の高さをランダムで移動させることでパイプが上下する実装を実現させています。

背景色の修正

今までほっぽらかしていた背景色を設定します、ゲームの世界の初期化関連の処理をメソッド化して以下のように切り出しました。

private func setupFieldProperty() {
    self.physicsWorld.gravity = CGVector( dx: 0.0, dy: -5.0)
    self.physicsWorld.contactDelegate = self
    self.anchorPoint = CGPoint(x:0, y:0)

    self.backgroundColor = SKColor(red: 81.0/255.0, green: 192.0/255.0, blue: 201.0/255.0, alpha: 1.0)
}

リファクタ

didMoveの中が物凄い読みにくくなってきたので処理を分割してリファクタリングを行いました。

この背景周りとパイプをポーネントに分けてみたんですが、これが果たして最良なのは微妙なところです。

GameScene

class GameScene: SKScene {
    lazy var bird: BirdSprite = {
        let position = CGPoint(x: self.frame.midX, y: self.frame.midY)
        var birdTexture = [SKTexture(imageNamed: "bird-1"),
                           SKTexture(imageNamed: "bird-2"),
                           SKTexture(imageNamed: "bird-3"),
                           SKTexture(imageNamed: "bird-4")]
        
        
        birdTexture = birdTexture.map{
            //スケールの拡大を行った時にレンダリングで綺麗に補完する
            $0.filteringMode = .nearest
            return $0
        }
        return BirdSprite(texture: birdTexture,
                          animateDuration: 0.2,
                          position: position)
    }()
    
    lazy var movingBackground: MovingBackgroundNode = {
        //床のもろもろ---------------------------------------------
        let groundTexture = SKTexture(imageNamed: "land")
        groundTexture.filteringMode = .nearest
        
        let twiceGroundTextureWidth = groundTexture.size().width * 2.0

        let moveGroundSprite = SKAction.moveBy(x: -twiceGroundTextureWidth, y: 0, duration: TimeInterval(0.02 * twiceGroundTextureWidth))
        let resetGroundSprite = SKAction.moveBy(x: twiceGroundTextureWidth, y: 0, duration: 0.0)
        let moveGroundSpritesForever = SKAction.repeatForever(SKAction.sequence([moveGroundSprite,resetGroundSprite]))
        
        //背景の空のもろもろ------------------------------------------
        let skyTexture = SKTexture(imageNamed: "sky")
        skyTexture.filteringMode = .nearest
        
        let twiceSkyTextureWidth = skyTexture.size().width * 2.0
        
        let moveSkySprite = SKAction.moveBy(x: -twiceSkyTextureWidth, y: 0, duration: TimeInterval(0.1 * twiceSkyTextureWidth))
        let resetSkySprite = SKAction.moveBy(x: twiceSkyTextureWidth, y: 0, duration: 0.0)
        let moveSkySpritesForever = SKAction.repeatForever(SKAction.sequence([moveSkySprite,resetSkySprite]))
        
       return MovingBackgroundNode(groundTexture: groundTexture,
                                   groundAnimation: moveGroundSpritesForever,
                                   skyTexture: skyTexture,
                                   skyAnimation: moveSkySpritesForever,
                                   deviceWidth: self.frame.size.width)
    }()

    lazy var pipes: MovingPipeNode = {
        let pipeTextureDown = SKTexture(imageNamed: "PipeDown")
        pipeTextureDown.filteringMode = .nearest
        let pipeTextureUp = SKTexture(imageNamed: "PipeUp")
        pipeTextureUp.filteringMode = .nearest
        
        let deviceWidth = self.frame.size.width
        let deviceHeight = self.frame.size.height
              
        let groundTextureHeight = SKTexture(imageNamed: "land").size().height
        
        return MovingPipeNode(pipeUpTexture: pipeTextureUp,
                              pipeDownTexture: pipeTextureDown,
                              groundHeight: groundTextureHeight,
                              deviceWidth: deviceWidth,
                              deviceHeight: deviceHeight)
    }()
    
    override func didMove(to view: SKView) {
        setupFieldProperty()
        self.addChild(bird)
        self.addChild(movingBackground)
        
        movingBackground.addChild(pipes)
        pipes.startAnimation()
    }
    
    private func setupFieldProperty() {
        self.physicsWorld.gravity = CGVector( dx: 0.0, dy: -5.0)
        self.physicsWorld.contactDelegate = self
        self.anchorPoint = CGPoint(x:0, y:0)

        self.backgroundColor = SKColor(red: 81.0/255.0, green: 192.0/255.0, blue: 201.0/255.0, alpha: 1.0)
    }
    
    override func update(_ currentTime: TimeInterval) {}
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        bird.fly()
    }
}

extension GameScene: SKPhysicsContactDelegate {}

MovingBackgroundNode

class MovingBackgroundNode: SKNode {
    
    init(groundTexture: SKTexture, groundAnimation: SKAction, skyTexture: SKTexture, skyAnimation: SKAction, deviceWidth: CGFloat) {
        super.init()
        let twiceGroundTextureWidth = groundTexture.size().width * 2.0
        let twiceSkyTextureWidth = skyTexture.size().width * 2.0
        //端末の横幅と画像の横幅で計算をして動く道を敷き詰める
        for i in 0 ..< 2 + Int(deviceWidth / twiceGroundTextureWidth) {
            let i = CGFloat(i)
            let sprite = SKSpriteNode(texture: groundTexture)
            sprite.setScale(2.0)
            sprite.position = CGPoint(x: i * sprite.size.width, y: sprite.size.height / 2.0)
            sprite.run(groundAnimation)
            self.addChild(sprite)
        }
        
        for i in 0 ..< 2 + Int(deviceWidth / twiceSkyTextureWidth) {
            let i = CGFloat(i)
            let sprite = SKSpriteNode(texture: skyTexture)
            sprite.setScale(2.0)
            sprite.zPosition = -20
            sprite.position = CGPoint(x: i * sprite.size.width, y: sprite.size.height / 2.0 + groundTexture.size().height * 2.0)
            sprite.run(skyAnimation)
            self.addChild(sprite)
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        //NOP
        fatalError("init(coder:) has not been implemented")
    }
}

MovingPipeNode

class MovingPipeNode: SKNode {
    let pipeUpTexture: SKTexture
    let pipeDownTexture: SKTexture
    let groundHeight: CGFloat
    let deviceWidth: CGFloat
    let deviceHeight: CGFloat
    
    init(pipeUpTexture: SKTexture, pipeDownTexture:SKTexture, groundHeight: CGFloat, deviceWidth: CGFloat, deviceHeight: CGFloat){
        self.pipeUpTexture = pipeUpTexture
        self.pipeDownTexture = pipeDownTexture
        self.groundHeight = groundHeight
        self.deviceHeight  = deviceHeight
        self.deviceWidth = deviceWidth
        
        super.init()
    }
    
    func startAnimation(){
        let spawn = SKAction.run(spawnPipes)
        let delay = SKAction.wait(forDuration: TimeInterval(2.5))
        let spawnThenDelay = SKAction.sequence([spawn, delay])
        let spawnThenDelayForever = SKAction.repeatForever(spawnThenDelay)
        self.run(spawnThenDelayForever)
    }
    
    required init?(coder aDecoder: NSCoder) {
        //NOP
        fatalError("init(coder:) has not been implemented")
    }
    
    func spawnPipes() {
        //土管が移動して消える
        let distanceToMove = CGFloat(deviceWidth + 2.0 * pipeUpTexture.size().width)
        let movePipes = SKAction.moveBy(x: -distanceToMove, y:0.0, duration:TimeInterval(0.01 * distanceToMove))
        let removePipes = SKAction.removeFromParent()
        let movePipesAndRemove = SKAction.sequence([movePipes, removePipes])
        
        let pipePair = SKNode()
        pipePair.position = CGPoint( x: deviceWidth + pipeUpTexture.size().width * 2, y:  0)
        pipePair.zPosition = -10
        
        //上から伸びるパイプ
        let pipeDown = SKSpriteNode(texture: pipeDownTexture)
        pipeDown.setScale(3.5)
        pipeDown.position = CGPoint(x: 0.0, y: deviceHeight - pipeDown.size.height / 2)

        //下から伸びるパイプ
        let pipeUp = SKSpriteNode(texture: pipeUpTexture)
        pipeUp.setScale(3.5)
        pipeUp.position = CGPoint(x: 0.0, y: pipeUp.size.height / 2 - groundHeight)
        
        pipePair.addChild(pipeDown)
        pipePair.addChild(pipeUp)
        pipePair.run(movePipesAndRemove)
        
        //画面からはみ出している分だけ親のpipePairNodeのY座標を動かしてパイプの位置を変更
        pipePair.position.y = pipePair.position.y + CGFloat.random(in: 0...pipeUp.frame.height / 2)

        self.addChild(pipePair)
    }
    
}

ここまででフィールドが完成しました!

鳥が位置がずれていてどこかに行ってしまってますが、次回当たり判定を加えるのと一緒に修正しようと思います。

その4に続く: いまさらFlappy Birdを作ってみる④