Sprite KitでFlappy Birdを作る

Flappy Birdのクローンやパロディやオマージュはたくさん作られているが、私もSprite Kitで試してみた。
コードはこちら：https://github.com/oinariman/RMFlappyBall

![demo.gif](https://qiita-image-store.s3.amazonaws.com/0/2503/f0543ea8-0b44-7dd5-0e6e-168be16d5f1f.gif)


# 要件
まず、Flappy Birdゲームを構成する要素を分解してみた。
以下の要件を満たせば、Flappy Birdみたいになるはず。

![ssc.png](https://qiita-image-store.s3.amazonaws.com/0/2503/99177143-42a5-9d81-a1dc-787a15147724.png)

## ①トリ
* 中央からやや左に位置。
* 重力に従って落ちる。
* タップすると少し上に跳ね上がる。

## ②床

* ずっと右から左へ等速でスクロールしている。

## ③壁
* ランダムにすきまの空いた壁が、定期的に画面の右端に現れる。
* 壁は画面の左へ等速で移動する。

## ④点数
* 壁がトリより左側へ行くたびに1点加算される。

## ⑤終了条件
* トリが床や壁にぶつかったらゲームオーバー。

# 実装

## ①トリ
丸いスプライトをトリということにして、中央からやや左に置く。
SKPhysicsBodyのインスタンスを与えて、重力に従って落ちるようにする。

画面を触られたら、トリに上向き速度を与えるようにする：

```objc
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    SKNode *ball = [self childNodeWithName:@"ball"];
    [[ball physicsBody] setVelocity:
     CGVectorMake(0.0, kFlappingVelocityY)];
}
```

## ②床
縞模様が右から左に流れていると、ずっと右に進んでいるように見える。
縞模様テクスチャのスプライトを作り、右から左へ移動してまた元に戻る動作を繰り返させた。

```objc
    // 右から左へ行ってまたもとに戻る動きを永遠に繰り返す。
    [floor runAction:[SKAction repeatActionForever:
                      [SKAction sequence:
  @[[SKAction moveTo:CGPointMake(0.0, size.height/2.)
            duration:interval],
    [SKAction moveTo:CGPointMake(self.size.width, size.height/2.) duration:0.0]]]]];
```

床を静止させておいてテクスチャのアニメーションで済ますことも考えたが、壁の速度と合わせる調整がやりづらいのでやめた。

## ③壁
### 壁の配置
上の壁と下の壁を、それぞれランダムに高さを変えて置く。
画面の高さを15分割して、上の壁・穴・下の壁にランダムに割り振ることにした。
これでゲームのチューニングがしやすかった。

```objc
- (void)putWalls {
    // 画面の高さを15分割
    CGFloat unit = self.size.height / 15.0;
    
    // 上の壁の高さ = 高さの最小値 + 乱数
    CGFloat upperWallHeight =
    unit * (arc4random() % (kUpperWallHeightMax - kUpperWallHeightMin)
            + kUpperWallHeightMin);

    // すきまの高さは固定
    
    // 下の壁の高さ = 画面の高さ - (上の壁の高さ + すきまの高さ)
    CGFloat bottomWallHeight =
    self.size.height - upperWallHeight - unit * kHoleHeight;
    
    [self putWallWithHeight:upperWallHeight
                          y:self.size.height - upperWallHeight / 2.];
    [self putWallWithHeight:bottomWallHeight
                          y:bottomWallHeight / 2.];
}
```

でも、よく考えたらこの実装じゃ、3.5インチと4インチで難易度が変わっちゃう。

### 壁の一生
ゲームの開始から終了まで、定期的に壁が作られ続ける。
それぞれの壁は、画面の右端に生まれ、左向きに等速で画面を通過し、左端で消え去る。
壁の移動速度は、画面を通過するのにかかる時間によって決めることとした。

```objc
- (void)putWallWithHeight:(CGFloat)height y:(CGFloat)y {
    SKSpriteNode *wall =
    [SKSpriteNode spriteNodeWithColor:wallColor_
                                 size:CGSizeMake(kWallWidth, height)];
    [wall setPosition:CGPointMake(self.size.width + kWallWidth / 2., y)];
    
    SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:wall.size];
    [body setAffectedByGravity:NO];
    [body setDynamic:NO];
    [wall setPhysicsBody:body];
    
    // 右から左へ動かした後消す
    [wall runAction:[SKAction sequence:
  @[[SKAction moveTo:
     CGPointMake(-kWallWidth / 2., y) duration:kTimeTakenForWallGoThroughScreen],
    [SKAction removeFromParent]]]];
    
    [self addChild:wall];
}
```

なお、床と壁の移動速度が違うとずれて見えるので、壁の移動速度を基準に計算して、床の移動速度を設定している。

## ④点数
壁がトリより左側に行けば1点入る。
トリは水平方向に動かないので、壁が右端からトリを越えるまでの時間はあらかじめ計算できる。
そこで、壁を1個作ったあと、その壁が右端からトリに到達するまでの時間が経ったら自動的に1点入るようにした。

```objc
// 定期的に壁を作る
- (void)putWallsPeriodically {
    [self runAction:
     [SKAction repeatActionForever:
      [SKAction sequence:
  @[[SKAction waitForDuration:kIntervalBetweenWallProductions],
    [SKAction runBlock:^{
          // 壁を1セット作って置く
          [self putWalls];
          
          // 壁が右端からトリに到達するまでの時間が経ったら自動的に1点入れる
          [self runAction:
           [SKAction sequence:
  @[[SKAction waitForDuration:kTimeTakenForWallGoThroughScreen * 0.75],
    [SKAction runBlock:^{
               SKNode *ball = [self childNodeWithName:@"ball"];
               // 上空を乗り越えるのは禁止
               if ([ball position].y > self.size.height) {
                   [self gameOver];
               }
               // 1点追加
               else {
                   [self incrementPoints];
               }
           }]]]];
      }]]]]];
}
```

## ⑤終了条件
SKPhysicsContactDelegateを使って、トリが床か壁にぶつかったら処理が呼ばれるようにする。

---

以上の実装に若干の調整と演出を加えて、Flappy Birdっぽくなった。

もっとゲームを面白くするためには、壁の速度やすきまの大きさ、重力の強さ等々をチューニングすることが必要。

ゲームが愛されるには、かわいいグラフィックやアニメーション演出も超大事。

作者が嫌いになっても、私はFlappy Birdが大好きです。