【Unity2D】Flappy Bird を作るチュートリアル (2/3)

# 今回やること
1. [x] プロジェクトの作成
2. [x] プレイヤーの作成
3. [ ] ブロック（障害物）の作成 ←②
4. [ ] ブロック管理の作成 ←②
5. [ ] スコアの実装
6. [ ] ゲームオーバーの実装
7. [ ] リトライの実装

プレイヤーの作成まで終わったので、②ブロック（障害物）に関連する実装を進めていきます。

# ページのリンク

* [Flappy Bird を作るチュートリアル (1/3)](https://qiita.com/2dgames_jp/items/59b361cf430d3dc6584b)
* Flappy Bird を作るチュートリアル (2/3) ←今のページ
* [Flappy Bird を作るチュートリアル (3/3)](https://qiita.com/2dgames_jp/items/77d6a09e65b37ea09f58)

# ブロックの作成
Flappy Bird はジャンプで障害物をタイミング良く回避するゲームです。今回は障害物として、ブロックが右から流れてくるようにします。

## ブロック画像の追加
ダウンロードした素材フォルダの「5box.png」を Projectビューの Assetsフォルダにドラッグ＆ドロップします。
<img width="327" alt="スクリーンショット_2019_11_22_22_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/94779500-d26e-8ace-db91-fc7a3443944d.png">
以下のように "5box" のスプライトが作られればOKです。
<img width="243" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/0887d059-fab1-e49a-2b1d-002bba47290b.png">
作成した `5box` スプライトを Hierarchyビューにドラッグ＆ドロップします。
<img width="435" alt="スクリーンショット_2019_11_22_22_15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/f0c805db-6aee-dae7-dc10-4b26600f437c.png">
作成した「5box」ゲームオブジェクトを Inspectorビューから「Block」にリネームします。
![021.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/0208e048-f0c6-0a01-519d-aab8e1f07367.gif)

## 動きをつける
まずは Block オブジェクトを右端に移動させます。
Hierarchyビューで Blockオブジェクトをダブルクリックすると、Sceneビューで中心に表示されます。そうしたら Sceneビューで Blockオブジェクトを右の方へドラッグすると右側に移動できます。
![022.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/942b4772-03a8-72a1-6d57-2f38f730b871.gif)

ゲームオブジェクトに動きをつけるには `Rigidbody` が必要となります。Blockゲームオブジェクトを選択して、Inspectorビューの「Add Component」から `Rigidbody 2D`を追加します。
<img width="284" alt="UnityEditor_AdvancedDropdown_AddComponentWindow_と_Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/69cf097e-2f04-edcc-b7a9-71894890c6b6.png">
Blockオブジェクトは重力を無視したいので、 `Gravity Scale` を `0` にして重力を無効にします。
<img width="270" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/98693e1a-864a-ef4c-c9db-f00e5a9a62c3.png">
念のため実行して Blockオブジェクトが動かないことを確認しておきます。

続けて、Scriptコンポーネントを追加します。
![023.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/fa375ddc-0a5f-91e5-4ee5-0ba1c8e7874a.gif)

スクリプトの名前は「Block」とし、以下のように記述します。

```Cs:Block.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// ブロック
public class Block : MonoBehaviour {
  Rigidbody2D _rigidbody;
  float _speed = -100; // 移動速度

  void Start() {
    // 物理挙動コンポーネントを取得
    _rigidbody = GetComponent<Rigidbody2D>();
    // 力を加える
    _rigidbody.AddForce(new Vector2(_speed, 0));  
  }

  void Update() {
  }
}
```

メンバ変数に `_rigidbody` / `_speed` を追加し、`Start()` で Rigidbody2D コンポーネントを取得して、左向きに力を加えています。

では実行してブロックが動くことを確認します。

![024.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/6a60743b-9106-fca2-a2f5-dc6a9c9795f3.gif)

## プレイヤーとの衝突判定を行う
次にプレイヤーとの衝突判定を実装します。プレイヤーがブロックにぶつかったら衝突するようにします。

まずは Blockオブジェクトを選び、Add Component > Circle Collider 2D を追加します。
![025.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/2cbc2cb5-b0e1-52d8-d941-f74077d88fd6.gif)
Blockオブジェクトをダブルクリックすると、Sceneビューで周りに丸い線（当たり判定）があるのが確認できます。
<img width="578" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/58ad698f-3580-4ccb-cdc0-3a8cd0cb68a8.png">
そして Inspectorビュー から、Circle Collider 2Dコンポーネントの `Is Trigger` にチェックを入れておきます。
<img width="297" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/e8cb7811-abe7-8397-e377-a10697f3b895.png">
これにより衝突した際に物理挙動（跳ね返ったり、転がったりする）を行わずに、「当たったかどうか」だけを判定できるようになります。
例えば、プレイヤーがブロックにぶつかったときに、ブロックが跳ね返ってふっとんだりしなくなります。
例えば `Is Trigger` にチェックを入れないと以下のような挙動となります。
![026.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/a959f263-049d-95ba-ee0d-ebd052d4bd12.gif)
これはこれで面白い挙動ですが、FlappyBirdを作るには不適切なので、Is Triggerで衝突を扱います。

Playerオブジェクトにも「Circle Collider 2D」を追加して、`Is Trigger`にチェックを入れておき、`Radius` の値を `0.4` に減らしておきます。
<img width="296" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/ed3a5271-b8fc-665a-b763-9271cffd0ac5.png">
`Radius` とは円の当たり判定の半径で、この値が小さくなると当たり判定が小さくなります。プレイヤーの当たり判定は小さめにすることでゲームを遊びやすくするためです。

では衝突したら Playerオブジェクトが消えるようにします。
Playerスクリプトを開いて、`OnTriggerEnter2D` 関数を追加します。

```Cs:Player.cs
  // 衝突判定
  private void OnTriggerEnter2D(Collider2D collision) {
    // 衝突したので消滅
    Destroy(gameObject);
  }
```

これは `Is Trigger` が有効なコリジョンに衝突したときに発生する処理となります。`Destroy` に `gameObject` を渡すと削除処理が呼び出されます。

では実行してプレイヤーがブロックにぶつかると消滅することを確認します。
![027.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/b08130fa-9d69-3340-6915-5edac59c9874.gif)

## Blockオブジェクトをプレハブ化する
ブロックが1つだけでは簡単すぎるので、たくさん発生するようにします。たくさん発生させるには、Blockオブジェクトをプレハブ化すると、複製しやすくなります。

プレハブを使う理由を簡単に説明すると、ここまでのプレイヤーやブロックは実体化という処理が行われており、ゲーム画面（シーン）で利用可能な状態になっています。
<img width="511" alt="名称未設定.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/9e68a576-2d73-4ace-9666-b6900923ba6c.png">
それに対して、プレハブ化を行うと、ゲーム外にオブジェクトが配置されます。
<img width="777" alt="名称未設定.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/95188ab3-5e5a-19cb-6b52-657a6133274a.png">
そして「実体化」を後から行うことで、ゲーム内にたくさん配置することが可能になります。これにより間違って複製したいオブジェクトをゲーム中に消してしまう心配はなくなります。また、Unityがプレハブを使って複製する仕組みを提供しているのでそれに従ったほうが楽、ということもあります。

では、Blockオブジェクトをプレハブ化します。
プレハブ化は簡単で、HierarchyビューにあるBlockオブジェクトを、Projectビューにドラッグ＆ドロップするだけです。
![028.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/05156924-1841-79a0-9cfe-5f3b3fee81e1.gif)
プレハブを作ったら、Hierarchyビューにある Blockオブジェクトは不要なので消しておきます。

## ブロック生成管理オブジェクトを作成する
プレハブ化したブロックの生成を行うオブジェクトを作成します。
素材画像の「xbox.png」を Projectビューにドラッグ＆ドロップします。
<img width="591" alt="スクリーンショット_2019_11_25_10_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/82d9a2d8-03bb-dfa8-53f9-24362a21694a.png">
追加された `xbox` スプライトを Hierarchyビューにドラッグ＆ドロップします。
<img width="463" alt="スクリーンショット_2019_11_25_10_12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/8c57cef9-44f8-f858-0df2-02eacdf4e58c.png">
そして xboxオブジェクトを「BlockMgr」にリネームします。
Windows環境であれば`F2`、MacOSX環境であれば`Enter`でリネームできます。Inspectorビューからリネームしてもよいです。
![030.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/93e70b2b-76ea-c4b9-a905-50ba04e21ba5.gif)
ちなみに「Mgr」とは「Manager」の省略語です。人によっては「Mng」と省略するケースもあります。省略語が好きでない場合は「BlockManager」としてもよいです。

## ブロック生成スクリプトの作成
BlockMgrオブジェクトを選択したまま、Inspectorビューから `Add Component > New Script > Name に "BlockMgr" と入力 > Create and Add` を選び BlockMgrスクリプトを追加します。
![031.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/e0a96772-85b7-eba7-e391-1c7910b7c063.gif)

BlockMgrスクリプトは以下のように記述します。

```cs:BlockMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlockMgr : MonoBehaviour {
  // 生成するBlockオブジェクト
  public GameObject block;

  // 0になったらBlockオブジェクトを生成
  float _timer = 0;

  void Start() {
  }

  void Update() {
    // ①経過時間を差し引く
    _timer -= Time.deltaTime;
    if(_timer < 0) {
      // ②0になったのでBlock生成
      // ③BlockMgrの場所から生成
      Vector3 position = transform.position;
      // ④プレハブをもとにBlock生成
      GameObject obj = Instantiate(block, position, Quaternion.identity);
      // ⑤1秒後にまた生成する
      _timer += 1;
    }
  }
}
```

スクリプトの説明です。
メンバ変数には `block` を用意し、Blockオブジェクトのプレハブをここに入れておきます。また `_timer` は生成を繰り返すためのタイマーです。この値が `0` 以下になったとき Blockオブジェクトを生成します。

生成処理は `Update` 関数に記述しています。

```cs:BlockMgr.cs
    // ①経過時間を差し引く
    _timer -= Time.deltaTime;
```

`Time.deltaTime` は前回の `Update` からの経過時間です。この値で `_timer` から引き算をすることで一定時間ごとに何らかの処理を行う、という判定が可能となります。

```cs:BlockMgr.cs
    if(_timer < 0) {
      // ②0になったのでBlock生成
```

`_timer` は時間が経過するごとに減少する値となるため、これが `0` になったら何らかの処理を行う、という判定をしています。

```cs:BlockMgr.cs
      // ③BlockMgrの場所から生成
      Vector3 position = transform.position;
```

③ではまず BlockMgrオブジェクトの位置を取得しています。オブジェクトを生成するには座標が必要となるため、ひとまずBlockMgrの位置を基準に生成するようにします。

```cs:BlockMgr.cs
      // ④プレハブをもとにBlock生成
      GameObject obj = Instantiate(block, position, Quaternion.identity);
```

④が実際にBlockオブジェクトを生成している処理となります。 `Instantiate` という関数にプレハブ(ここでは `block`)を渡すことで、Blockオブジェクトを生成できます。Instantiate() の二番目の引数 `Quaternion.identity` は回転値です。今回は回転を加えないので identity (初期値＝ゼロ）の値としています。

```cs:BlockMgr.cs
      // ⑤1秒後にまた生成する
      _timer += 1;
```

⑤ではタイマーを再設定しています。`1` を足し込むことで、「1秒後」にもう一度生成を行います。

## Blockプレハブの設定
このまま実行すると、エラーとなりブロックの生成は行われません。`block` に Blockプレハブが設定されていないためです。
そのためプレハブの設定を行います。
設定方法は簡単で、Unityエディタに戻って、
1. Hierarchyビューから「BlockMgr」オブジェクトを選択
2. Projectビューから「Blockプレハブ」を Inspectorビューの BlockMgr(Script) の `Block` にドラッグ＆ドロップ

これで設定ができます。
![032.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/467fe61b-a877-30cd-1204-869ae6715701.gif)

では実行して、Blockオブジェクトが次々と生成されることを確認します。
![033.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/442d17ef-7740-4378-555b-39f4b1641355.gif)

## Blockの破棄処理と移動速度の設定
一見うまくいってそうですが、実は大きな問題があります。実行中の Hierarchyビューを見てみます。
<img width="213" alt="Unity_2018_4_12f1_Personal_-_SampleScene_unity_-_TestFlappyBird_-_PC__Mac___Linux_Standalone__Personal___Metal_.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/e0af490e-7d27-67e9-a6da-674cbccbeb42.png">
すると、このように大量の `Block(Clone)` というオブジェクトが生成されています。今回のようなシンプルなゲームではあまり問題にはならないのですが、これが 何千何万と生成されると、実行時のメモリが不足してゲームがクラッシュする可能性があります。
そのため、不要になった Blockオブジェクトを削除するようにします。

Blockスクリプトを開いて、画面の左端に出たら消します。

```cs:Block.cs
  void Update() {
    Vector2 position = transform.position;
    if(position.x < GetLeft()) {
      Destroy(gameObject); // 画面外に出たので消す.
    }
  }

  float GetLeft() {
    // 画面の左下のワールド座標を取得する
    Vector2 min = Camera.main.ViewportToWorldPoint(Vector2.zero);
    return min.x;
  }
```

画面の左端を取得する `GetLeft` 関数を追加して、`Update()` で画面外に出たかどうかチェックし、画面外に出たら `Destroy()` で消滅させます。

では実行して、画面外に出た Blockオブジェクトが消えるのを確認します。

## Blockオブジェクトの移動速度を変化させる
時間が経過するにつれて、Blockオブジェクトの移動が速くなるようにして、少しずつ難易度を上げるようにします。

まずは Blockスクリプトを開いて速度を設定する関数 `SetSpeed()` を追加します。

```cs:Block.cs
  // 速度を設定
  public void SetSpeed(float speed) {
    _speed = speed;
  }
```

Blockスクリプトであればどこに定義しても問題ありません。`SetSpeed()` は公開関数にするので、
`public` キーワードを忘れずにつけます。

続けて、BlockMgrスクリプトを以下のように修正します。

```cs:BlockMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlockMgr : MonoBehaviour {
  // 生成するBlockオブジェクト
  public GameObject block;

  // 0になったらBlockオブジェクトを生成
  float _timer = 0;
  // ①トータルの経過時間を保持
  float _totalTime = 0;

  void Start() {
  }

  void Update() {
    // 経過時間を差し引く
    _timer -= Time.deltaTime;
    // ②トータル時間を加算
    _totalTime += Time.deltaTime;

    if(_timer < 0) {
      // 0になったのでBlock生成
      // BlockMgrの場所から生成
      Vector3 position = transform.position;
      // プレハブをもとにBlock生成
      GameObject obj = Instantiate(block, position, Quaternion.identity);
      // ③Blockオブジェクトの「Block」スクリプトを取得する
      Block blockScript = obj.GetComponent<Block>();
      // ④速度を計算して設定
      // 基本速度100に、経過時間x10を加える
      float speed = 100 + (_totalTime * 10);
      blockScript.SetSpeed(-speed); // 左方向なのでマイナス
      // 1秒後にまた生成する
      _timer += 1;
    }
  }
}
```

メンバ変数に `_totalTime` を追加し、生成したBlockオブジェクトにトータル時間を加えた速度を設定するようにしています。

①は `_totalTime` の定義です。
②では `_totalTime` に経過時間の差分である `Time.deltaTime` を「加算」しています。これにより経過したトータルの時間が入ることになります。

③で生成したBlockオブジェクトから、Blockスクリプトコンポーネントを取得しています。
そして④で速度を計算し、`SetSpeed()` で速度を設定しています。
速度の計算式は経過時間を変数としたシンプルな一次式となっています。

$速度＝100 + (10 * 経過時間[秒])$

| 経過時間 | 式 | 速度 |
|:-:|:-:|:-:|
| 0.0秒  | 100+(10*0)  | 100  |
| 1.0秒  | 100+(10*1)  | 110  |
| 2.0秒  | 100+(10*2)  | 120  |
| ...  | ...  | ...  |

では実行して動きを確認します。
![034.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/7cb46c89-0a15-8ebb-98dc-905f109ab441.gif)

時間経過によりブロックの移動速度が少しずつですが上昇しています。

## ブロックを生成する高さをランダムにする
ブロックの生成位置が常に中央では単調なゲームとなってしまうので、ばらつきを出すようにします。
その前に、ブロック生成位置を右にずらしたいので、`BlockMgr` オブジェクトを右にずらします。

Hierarchyビューで `BlockMgr` オブジェクトをダブルクリックして、Sceneビューの中央に表示し、それをドラッグ＆ドロップすることで動かすことができます。
![035.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/f1a5919c-ecff-6255-2500-08164a9c6507.gif)

BlockMgr スクリプトを開いて、`Update()` を修正します。

```cs:BlockMgr.cs
  void Update() {
    // 経過時間を差し引く
    _timer -= Time.deltaTime;
    // トータル時間を加算
    _totalTime += Time.deltaTime;

    if(_timer < 0) {
      // 0になったのでBlock生成
      // BlockMgrの場所から生成
      Vector3 position = transform.position;
      // ※上下(±3)のランダムな位置に出現させる
      position.y = Random.Range(-3, 3);
      // プレハブをもとにBlock生成
      GameObject obj = Instantiate(block, position, Quaternion.identity);
      // Blockオブジェクトの「Block」スクリプトを取得する
      Block blockScript = obj.GetComponent<Block>();
      // 速度を計算して設定
      // 基本速度100に、経過時間x10を加える
      float speed = 100 + (_totalTime * 10);
      blockScript.SetSpeed(-speed); // 左方向なのでマイナス
      // 1秒後にまた生成する
      _timer += 1;
    }
  }
```

修正箇所は※がついている一行だけです。

```cs:BlockMgr.cs
      // ※上下(±4)のランダムな位置に出現させる
      position.y = Random.Range(-4, 4);
```

`Random.Range()` 指定した数値の範囲でのランダムな値を返す便利な関数です。ここでは「-4」と「4」を指定しているので、`-4〜4` の範囲のランダムな値を返します。

では実行して動作を確認します。
![036.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/7b39a021-3223-cc93-5755-62e5ed680908.gif)
ランダムな位置からブロックが出現するようになりました。

## ブロックの生成に偏りを入れる
現状でもゲームとして成立していますが、発生タイミングが均一で単調さを感じてしまうので、少し偏りを入れます。
偏りのルールは「10のうち3つは連続で出現する」とします。

BlockMgrスクリプトを開いて以下のように修正します。

```cs:BlockMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlockMgr : MonoBehaviour {
  // 生成するBlockオブジェクト
  public GameObject block;

  // 0になったらBlockオブジェクトを生成
  float _timer = 0;
  // トータルの経過時間を保持
  float _totalTime = 0;
  // ①ブロック生成回数
  int _cnt = 0;

  void Start() {
  }

  void Update() {
    // 経過時間を差し引く
    _timer -= Time.deltaTime;
    // トータル時間を加算
    _totalTime += Time.deltaTime;

    if(_timer < 0) {
      // 0になったのでBlock生成
      // BlockMgrの場所から生成
      Vector3 position = transform.position;
      // ※上下(±3)のランダムな位置に出現させる
      position.y = Random.Range(-4, 4);
      // プレハブをもとにBlock生成
      GameObject obj = Instantiate(block, position, Quaternion.identity);
      // Blockオブジェクトの「Block」スクリプトを取得する
      Block blockScript = obj.GetComponent<Block>();
      // 速度を計算して設定
      // 基本速度100に、経過時間x10を加える
      float speed = 100 + (_totalTime * 10);
      blockScript.SetSpeed(-speed); // 左方向なのでマイナス

      // ②生成回数をカウントアップ
      _cnt++;
      if(_cnt%10 < 3) {
        // 0.1秒後にまた生成する
        _timer += 0.1f;
      } else {
        // 1秒後にまた生成する
        _timer += 1;
      }
    }
  }
}
```

変更箇所は２つです。
①でブロック生成回数を保持する変数 `_cnt` を定義しています。

```cs:BlockMgr.cs
  // ①ブロック生成回数
  int _cnt = 0;
```

`Update()` 内に記述した②で生成回数をカウントアップし、下一桁が「0〜２」の場合は「0.1秒後」に生成するようにしました。

```cs:BlockMgr.cs
      // ②生成回数をカウントアップ
      _cnt++;
      if(_cnt%10 < 3) {
        // 0.1秒後にまた生成する
        _timer += 0.1f;
      } else {
        // 1秒後にまた生成する
        _timer += 1;
      }
```

`_cnt%10` は _cnt を 10で割った時の余りの値を求める記述です。これにより下一桁を求め、3より小さい（＝0〜2）の場合は、「0.1秒後」にブロックを生成するようにしています。

実行すると、ブロックの生成に少し偏りがあることが確認できます。
![037.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/53ad75dd-88ea-cffb-0fc7-6784aac40bfd.gif)

これでプレイヤーとブロックについての処理の実装は終わりです。
次では、スコアと、ゲームオーバー、リトライ処理を実装して完成とします。

# 次回

→[Flappy Bird を作るチュートリアル (3/3)](https://qiita.com/2dgames_jp/items/77d6a09e65b37ea09f58)