【SpriteKit】チワワがB'zからのXmasプレゼントを貰うだけのクソゲーRPGを作ってみよう！

## これを作ります
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">チワワがB&#39;zからのXmasプレゼントを貰うだけのクソゲーRPGを作りました笑<br><br>（社内のアドベントカレンダーでの技術記事用）<br>本日これの解説記事を書きます。 <a href="https://t.co/AxcPCudoVI">pic.twitter.com/AxcPCudoVI</a></p>&mdash; ウルトラ深瀬 (@UltraFukase) <a href="https://twitter.com/UltraFukase/status/1473369235984650240?ref_src=twsrc%5Etfw">December 21, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## はじめに
どうも！反町隆史顔真似芸人のウルトラ深瀬です！（やかましい）
今回はSwiftの2Dゲーム用フレームワークであるSpriteKitで遊んでみる記事です。
SpriteKitの基礎については割愛してしまいますが、下記にレポジトリを載せていますのでご参考ください。

## レポジトリは[こちら](https://github.com/Fukase-Takamasa/Vamos-a-jugar-al-SpriteKit)
気になる方はまずクローン＆ビルドして遊んでみると雰囲気がわかるかもしれません。
恐縮ですが詳細説明はソースコードで補完していただけると幸いです。
（クローン後、pod installが必要です）

## 何を説明するか
個人的に情報が少なくて困った下記を抜粋して説明します。
#### - MAPの作成とSwiftプロジェクトへのインポート
　→TiledMapとSKTiledを使います。

#### - 障害物判定
　→座標計算で行います。

#### - その他
　→キャラクター移動時の歩いてる風アニメーション
　→カメラの追従
　→イベントオーラのアニメーション

#### - （おまけ）RPG風字幕
　→SRGNovelGameTextsを使います。

## MAPの作成
私は[TiledMap](https://mmorley.hatenablog.com/entry/2016/05/21/002430#%E3%82%BF%E3%82%A4%E3%83%AB%E3%83%9E%E3%83%83%E3%83%97%E3%82%92%E4%BD%9C%E6%88%90)というゲーム用のMAP作成ツールを利用しました。
（上記リンク先の解説記事はCocosCreator用ですが、前半のマップ.tmxファイルの作成までは大体同じです。）

今回作ったMAP。
![Screen Shot 2021-12-21 at 11.30.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/521005/a47f3a6a-c61e-5ccb-9ef0-b0f54c077c7b.png)

画像をみるとわかる通り、四角形のマス目を並べる＆重ねてMAPを作っていきます。
#### - MAP作りの素材（タイルセット）
[こちら](http://yms.main.jp/dotartworld/page2/tile-office01.html)のサイトからお借りして、それらしい素材を集めて作っています。

#### - レイヤーについて
![Screen Shot 2021-12-21 at 11.38.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/521005/124769f6-505b-8e56-2025-5a984dd4718e.png)
机と椅子の重なりを表現するときにもレイヤーは使いましたが、
今回は特に、障害物判定専用のレイヤーを用意しました。
こちら後で使います。


## MAPのSwiftプロジェクトへのインポート
[SKTiled](https://github.com/mfessenden/SKTiled)という有難いライブラリを使います。
先ほど作成した.tmx拡張子の汎用的なMAPファイルをSpriteKitで使えるようにしてくれます。
#### - MAPのインスタンス化

```swift
    //マップ(.tmxファイル)をインポート
    var tilemap = SKTilemap.load(tmxFile: "testMap1")!
    
    //上記インスタンスから抽出した一番全面のレイヤーを後で格納します
    var topLayer: SKTileLayer?
    
    //上記インスタンスから抽出した障害物判定用のレイヤーを後で格納します
    var obstacleLayer: SKTileLayer?
```
インポートしたMAPから、上記のように最前面レイヤーと障害物用レイヤーを抽出して使用します。

#### - チワワをMAPの最前面レイヤーに配置
```swift
    func setChihuahua() {
        //チワワnodeにカメラを設定
        chihuahuaNode.addChild(chihuahuaCamera)
        
        //MAPの最前面のレイヤーを取得
        topLayer = tilemap.tileLayers().last
        
        //チワワを最前面のレイヤーに設置
        topLayer?.addChild(chihuahuaNode)
        // - アンカーポイントの調整（画像と座標基準点の関係）
        chihuahuaNode.anchorPoint = CGPoint(x: 0.5, y: 0)
        // - 初期座標を設定（今回1マス32pxなので、左上から見て右に20、下に6マスのところ）
        chihuahuaNode.position = CGPoint(x: (32 * 20), y: -(32 * 6))
        
        //カメラをSceneに適用
        self.camera = chihuahuaCamera
    }
```
上記のようにチワワをMAPの最前面レイヤーに配置します。
チワワの初期座標については、今回1マス32pxのMAPなので、左上から見て右に20、下に6マスのところに設置しています。


#### - 障害物判定用の座標リストを取得
```swift
    //障害物判定用の座標リストを取得
    func setObstacleTilePositions() {
        //TiledMapで見ていたときにレイヤーリストの下から3番目に位置していたので、index=2を指定して取得
        let obstacleLayer = tilemap.tileLayer(atIndex: 2)
        
        //座標計算ようにタイルのpxを確認。今回は32×32です。
        print("tileSize: \(String(describing: obstacleLayer?.tileSize))")
        
        //障害物タイルの座標リストを配列に格納
        obstacleLayer?.getTiles().forEach({ tile in
            /*
             16pxずつずらしている理由↓
             tileのアンカーポイント（座標基準点）が正方形の中心（x:0.5, y:0.5）になっているので、
             正方形の左上になるようにずらしています。
            */
            obstacleTilePositions += [CGPoint(x: tile.position.x - 16, y: tile.position.y + 16)]
        })

        print(obstacleTilePositions)

        self.obstacleLayer = obstacleLayer
        
    }
```
障害物タイルの座標リストを配列に格納します。


#### - 移動先の座標が障害物座標リストに含まれているかチェック
```swift
    //移動先の座標が障害物座標リストに含まれているかチェック
    func checkObstacles(targetPos: CGPoint) -> Bool {
        
        let result = obstacleTilePositions.first(where: { item in
            
            let xRange = item.x...(item.x + 32)
            let yRange = (item.y - 32)...item.y
            
            return xRange.contains(targetPos.x) && yRange.contains(targetPos.y)
            
        })
                
        print("異動先のタイル座標が障害物（obstacle）である: \(result == nil)")

        return result == nil
        
    }
```


#### - キャラクター移動メソッド内で先程の障害物チェックを実行
```swift
    @objc func move(timer: Timer) {
        //キャラクターの現在座標を取得
        var targetPos = chihuahuaNode.position
        
        //移動しようとしている方向によって移動先座標を書き換え
        switch direction {
        case .up:
            targetPos.y += 32
            
        case .right:
            targetPos.x += 32
            
        case .left:
            targetPos.x -= 32
            
        case .down:
            targetPos.y -= 32
            
        default: break
        }
                
        //MARK: - 移動先の座標が障害物座標リストに含まれていたら、進ませずに終了する
        guard checkObstacles(targetPos: targetPos) else {
            print("checkObstacles == falseなのでreturn")
            return
        }
        
        //キャラクターの移動を実行
        let action = SKAction.move(to: targetPos, duration: 0.2)
        chihuahuaNode.run(action)

    }
```

## その他
#### - キャラクター移動時の歩いてる風アニメーション
[この記事](https://qiita.com/yamajyn/items/3436d650a6e7b6fc759e)が参考になります。
今回はチワワ画像をそれぞれの角度で数pxずつズラした画像を用意して、交互に表示することで上下にピョコピョコ跳ねる動きを作っています。

#### - キャラクターへのカメラの追従
[この記事](https://hajihaji-lemon.com/swift/spritekitscene_attribute/)が参考になります。

### - イベントオーラのアニメーション
[こちら](https://pipoya.net/sozai/assets/effects/effect-event-2/)から素材をお借りし、10枚の画像をパラパラ漫画のように繰り返しアニメーションさせています。

## （おまけ）RPG風字幕
#### - [SRGNovelGameTexts](https://github.com/kazu0620/SRGNovelGameTexts)を使います。
使い方は[この記事](https://qiita.com/kazu0620/items/33e7459388ed6c9f8de7)をご参照ください。

#### - 1点、ハマったポイントについてだけ補足。
そのままだと文章が8文字で改行されてしまうので、
Podライブラリのヘッダーファイルを1行だけ書き換えて使用しました。
（ViewFrameの横幅に代入する値をsuperview.frame.size.widthに変更）

```swift
- (void) _adjustViewFrame {
    float maxX = 0;
    float maxY = 0;
    
    for( UILabel *label in _stringLabels ){
        CGPoint frameOrigin = label.frame.origin;
        CGSize  frameSize   = label.frame.size;
        float labelsMaxX = frameOrigin.x + frameSize.width;
        float labelsMaxY = frameOrigin.y + frameSize.height;
        if( labelsMaxX > maxX ){
            maxX = labelsMaxX;
        }
        if( labelsMaxY > maxY ){
            maxY = labelsMaxY;
        }
    }
    
    self.frame = CGRectMake(
        self.frame.origin.x,
        self.frame.origin.y,

//        maxX,
        //8文字で改行されてしまうバグの対策で、superviewの横幅を指定するよう書き換え
        self.superview.frame.size.width,

        maxY
    );
}
```

## おわりに
さて、明日のアドベントカレンダーはAndroidエンジニアの@ryuji-odaアニキのご登場です！
お楽しみに！
あ"おっ！