いまさら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を作ってみる④