いまさら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進数の0x00001、10進数の1です。
let birdCategory: UInt32 = 1 << 0
categoryBitMaskは自分の属する世界の値のようなイメージです。
NARUTOという漫画の中で攻撃がすり抜けてしまうトビというキャラクタがいますが、categoryBitMaskが別物なので別世界にいて見えているけど攻撃が当たらない!というのが個人的にはイメージしやすいのかなと思います。
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))
}
}