いまさらFlappyBirdを作ってみる④

前回までで、フィールドにいろんなアイテムを配置していきました。

今回は当たり判定をつけて完成させます。

当たり判定については以前SpriteKit当たり判定という記事を書いているのでわかりにくかったらこちらも参考にしてみてください。

鳥を表示させる

当たり判定に入る前に鳥の配置を適当にしていたので鳥を表示させます。

didMoveの中でaddChildしている順番を以下のように変更して鳥が最前面に表示されるように修正します。

override func didMove(to view: SKView) {
    setupFieldProperty()
    
    self.addChild(movingBackground)
    movingBackground.addChild(pipes)
    pipes.startAnimation()

    self.addChild(bird)
}

BirdSpriteのinitializerでポジションの代入を忘れていたので代入すれば表示されるようになるはずです。

self.position = position

当たり判定

鳥と地面

以下のように当たり判定を追加します。

private let birdCategory: UInt32 = 1 << 0
private let worldCategory: UInt32 = 1 << 1

override func didMove(to view: SKView) {
  ...省略〜    
 
    //鳥の当たり判定
    bird.physicsBody?.categoryBitMask = birdCategory
    bird.physicsBody?.collisionBitMask = worldCategory
    
    //床部分にNodeを配置して当たり判定をつける
    let groundTextureHeight = SKTexture(imageNamed: "land").size().height
    let ground = SKNode()
    ground.position = CGPoint(x: 0, y: groundTextureHeight)
    ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.frame.size.width, height: groundTextureHeight * 2.0))
    ground.physicsBody?.isDynamic = false
    ground.physicsBody?.categoryBitMask = worldCategory
    self.addChild(ground)
    
}

カテゴリーを定義しているところがわかりにくいと思うのですが、これは16進数の0x0000110進数の1です。

let birdCategory: UInt32 = 1 << 0

categoryBitMaskは自分の属する世界の値のようなイメージです。

NARUTOという漫画の中で攻撃がすり抜けてしまうトビというキャラクタがいますが、categoryBitMaskが別物なので別世界にいて見えているけど攻撃が当たらない!というのが個人的にはイメージしやすいのかなと思います。

https://bibi-star.jp/posts/1720

collisionBitMaskは自分(Node)どの世界にいるNodeとぶつかるのかという判定を行うものです。

鳥のcollisionBitMaskにworldCategoryを設定していますが、categoryBitMaskにworldCategoryを設定しているのは地面なので、地面にぶつかった時に鳥の衝突が発生しています。

bird.physicsBody?.collisionBitMask = worldCategory

鳥と土管

spawnPipesメソッドの中で以下のように設定します。

pipeDown.physicsBody = SKPhysicsBody(rectangleOf: pipeDown.size)
pipeDown.physicsBody?.isDynamic = false
pipeDown.physicsBody?.categoryBitMask = pipeCategory

あとは鳥のほうにも衝突判定をつけてあげれば完成です。

bird.physicsBody?.collisionBitMask = worldCategory | pipeCategory

土管にぶつかって鳥が回転してますが、これで一通り作ることができました。

残り土管を超えた時にスコアの部分を追加していく処理とかが残ってますが、私が飽きてきてしまったのでいったんここで終了とします。

コード

GameScene

class GameScene: SKScene {
    lazy var bird: BirdSprite = {
        let position = CGPoint(x: self.frame.midX / 2, 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,
                              pipeCategory: self.pipeCategory,
                              birdCategory: self.birdCategory)
    }()
    
    private let birdCategory: UInt32 = 1 << 0
    private let worldCategory: UInt32 = 1 << 1
    private let pipeCategory: UInt32 = 1 << 2
    
    override func didMove(to view: SKView) {
        setupFieldProperty()
        
        self.addChild(movingBackground)
        movingBackground.addChild(pipes)
        pipes.startAnimation()

        self.addChild(bird)
        
        //鳥の当たり判定
        bird.physicsBody?.categoryBitMask = birdCategory
        bird.physicsBody?.collisionBitMask = worldCategory | pipeCategory
        
        //床の当たり判定
        let groundTextureHeight = SKTexture(imageNamed: "land").size().height
        let ground = SKNode()
        ground.position = CGPoint(x: 0, y: groundTextureHeight)
        ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: self.frame.size.width, height: groundTextureHeight * 2.0))
        ground.physicsBody?.isDynamic = false
        ground.physicsBody?.categoryBitMask = worldCategory
        self.addChild(ground)
        
    }
    
    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
    let pipeCategory: UInt32
    let birdCategory: UInt32
    
    init(pipeUpTexture: SKTexture, pipeDownTexture:SKTexture, groundHeight: CGFloat, deviceWidth: CGFloat, deviceHeight: CGFloat, pipeCategory: UInt32, birdCategory: UInt32){
        self.pipeUpTexture = pipeUpTexture
        self.pipeDownTexture = pipeDownTexture
        self.groundHeight = groundHeight
        self.deviceHeight  = deviceHeight
        self.deviceWidth = deviceWidth
        self.pipeCategory = pipeCategory
        self.birdCategory = birdCategory
        
        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)
        pipeDown.physicsBody = SKPhysicsBody(rectangleOf: pipeDown.size)
        pipeDown.physicsBody?.isDynamic = false
        pipeDown.physicsBody?.categoryBitMask = pipeCategory

        //下から伸びるパイプ
        let pipeUp = SKSpriteNode(texture: pipeUpTexture)
        pipeUp.setScale(3.5)
        pipeUp.position = CGPoint(x: 0.0, y: pipeUp.size.height / 2 - groundHeight)
        pipeUp.physicsBody = SKPhysicsBody(rectangleOf: pipeDown.size)
        pipeUp.physicsBody?.isDynamic = false
        pipeUp.physicsBody?.categoryBitMask = pipeCategory
        
        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)
    }
    
}

BirdSprite

class BirdSprite: SKSpriteNode {
    
    init(texture:[SKTexture], animateDuration: Double, position: CGPoint){
        super.init(texture: texture[0], color: .clear, size: texture[0].size())
        physicsBody = SKPhysicsBody(rectangleOf: frame.size)
        setScale(2.0)
        self.position = position
        let anim = SKAction.animate(with: texture, timePerFrame: animateDuration)
        let flap = SKAction.repeatForever(anim)
        run(flap)
    }

    required init?(coder aDecoder: NSCoder) {
        //NOP
        fatalError("init(coder:) has not been implemented")
    }
    
    func fly(){
        //速度の更新,これがないとジャンプが一定ではなくなる
        physicsBody?.velocity = CGVector(dx: 0, dy: 0)
        //質量を無視して力を加える
        physicsBody?.applyImpulse(CGVector(dx: 0, dy: 20))
    }
}