【UniRx】ReactivePropertyでクールダウン実装


# はじめに
ここで言うクールダウンとは、よくアクションゲームにあるスキル再発動可能になるまでの時間のことを指します。

<font color="red">
追記
クールダウンやゲーム制限時間などの時間を使った処理はUniTaskを使用した方がいいです!!
なのでここで紹介している実装方法は非推奨である可能性があります。この記事はログとして残しておきます。
<font /color="red">

# 実装したいもの
1. プレイヤーが矢を撃つスキルがある
2. 矢を撃つスキル使用後は5秒間のクールダウンがある
3. クールダウン解消直前に矢を準備し始めるアニメーションを行う

## 1. 矢を打つスキル実装
実装には`UniRx`を使用します。

```Player.cs
// 矢のプレハブ
[SerializeField] GameObject arrow;

void Start()
{
    // 1. 矢を打つスキル実装
    this.UpdateAsObservable()
        .Subscribe(_ =>
        {
            // 攻撃ボタンを押したとき処理に入る
            if (Input.GetButtonDown("Attack1"))
            {
                // 矢を発射する(スキルを使用する)
                Instantiate(arrow, transform.position, transform.rotation);
            }
        });
}
```

攻撃ボタンを押したとき、矢が発射される機能を簡単に実装しました。
`arrow`の移動制御は今回考えないものとします（プレイヤーの向いている方向に真っ直ぐ飛んでいくものとイメージして頂ければOKです）。

## 2. クールダウン実装
```Player.cs
// 矢のプレハブ
[SerializeField] GameObject arrow;
// クールダウン
ReactiveProperty<int> cooldown = new ReactiveProperty<int>(0);

void Start()
{
    // 1. 矢を打つスキル実装
    this.UpdateAsObservable()
        .Subscribe(_ =>
        {
            // 攻撃ボタンを押した時&クールダウンが解消されてる時、処理に入る
            if (Input.GetButtonDown("Attack1") && cooldown.Value == 0)
            {
                // 矢を発射する(スキルを使用する)
                Instantiate(arrow, transform.position, transform.rotation);
                cooldown.Value = 300; // スキル使用後なので、クールダウンにする
            }
        });

    // 2. クールダウン実装
    this.UpdateAsObservable()
        .Where(_ => cooldown.Value > 0) // クールダウン中の時のみ
        .Subscribe(_ =>
        {
            // 毎フレーム、クールダウンを消費していく
            cooldown.Value--;
        });
}
```
`ReactiveProperty<int>`型の`cooldown`を作成して、0を初期値として代入します。
スキルを使用すると、`cooldown`に300の数値が設定されます。いわゆるクールダウンに入ったというやつですね(60fps想定なので300フレームは5秒の設定)。

そして、`UpdateAsObservable()`によって毎フレーム`cooldown`を減算していき、クールダウンが消費されていきます。`Where()`で`cooldown`の値が0より大きい時のみという条件をお忘れなく。

**1.矢を打つスキル実装**で実装したスキル使用の`if`に`cooldown==0`という条件を追加します。これならクールダウンが解消されていなければ、スキル使用はできませんね。

# 3. クールダウン解消直前にアニメーション開始の実装
以下の仕様で実装します。

- クールダウン解消まで残り1秒になった時、アニメーション実行
- クールダウンが解消された時、アニメーション実行

```Player.cs
// 矢のプレハブ
[SerializeField] GameObject arrow;
// クールダウン
ReactiveProperty<int> cooldown = new ReactiveProperty<int>(0);

void Start()
{
    // 1. 矢を打つスキル実装
    this.UpdateAsObservable()
        .Subscribe(_ =>
        {
            // 攻撃ボタンを押した時&クールダウンが解消されてる時、処理に入る
            if (Input.GetButtonDown("Attack1") && cooldown.Value == 0)
            {
                // 矢を発射する(スキルを使用する)
                Instantiate(arrow, transform.position, transform.rotation);
                cooldown.Value = 300; // スキル使用後なので、クールダウンにする
            }
        });

    // 2. クールダウン実装
    this.UpdateAsObservable()
        .Where(_ => cooldown.Value > 0) // クールダウン中の時のみ
        .Subscribe(_ =>
        {
            // 毎フレーム、クールダウンを消費していく
            cooldown.Value--;
        });

    // 3. 残りクールダウンに対応した処理実装
    cooldown
        .SkipLatestValueOnSubscribe() // 初期値は通知しない
        .Subscribe(x =>
        {
            // クールダウンが残り1秒になった時
            if (x == 60) { 矢を準備するアニメーション開始 }
            // クールダウン終了時
            if (x == 0) { クールダウン終了エフェクト表示 }
        });
}
```

`cooldown`の値を監視し、値に変化があった場合に通知され、`Subscribe`内の処理を行います。

**2.クールダウン実装**で実装した通り、`cooldown`の値は毎フレーム1づつ減っていきますが、クールダウン解消まで残り1秒になった時と、解消された時だけアニメーションを行いたいので`Subscribe`内に`if`を2つ設置し、残りクールダウンが60フレーム(1秒)の時と、0フレームの時で処理を行えるようにします。

これなら、好きなクールダウンのタイミングで処理を行うことができますね。

# ちなみに、コルーチンで実装する場合
```Player.cs
// 矢のプレハブ
[SerializeField] GameObject arrow;
// クールダウン
int cooldown = 0;

void Start()
{
    // 1.矢を撃つスキル実装
    this.UpdateAsObservable()
        .Where(_ => Input.GetButtonDown("Attack1"))  //攻撃ボタンを押した時のみ 
        .Subscribe(_ =>
        {
            // クールダウンが解消されている時、処理に入る
            if (cooldown <= 0)
            {
                // 矢を発射する(スキルを使用する)
                Debug.Log("矢を発射");
                Instantiate(arrow, transform.position, transform.rotation);
                cooldown = 300;  // スキル使用後なので、クールダウンに入る(5秒)
            }
            else
            {
                Debug.Log("スキルはクールダウン中");
            }
        });

    // 2.クールダウン実装
    Observable
        .FromMicroCoroutine<string>(observer => UseSkillCoroutine(observer))
        .Subscribe(x =>
        {
            // 3. 残りクールダウンに対応した処理実装
            // OnNextされた値を確認
            if (x == "ArrowSet") { 矢を準備するアニメーション開始 }
            else if (x == "CoolDownEnd") { クールダウン終了エフェクト表示 }
        })
        .AddTo(this);
}

IEnumerator UseSkillCoroutine(IObserver<string> observer)
{
    while (true)
    {
        // クールダウン中の時のみ
        if (cooldown > 0)
        {
            // 毎フレーム、クールダウンを消費していく
            cooldown--;
            // クールダウンが残り1秒になった時、通知する
            if (cooldown == 0) { observer.OnNext("CoolDownEnd"); }
            // クールダウン終了時、通知する
            if (cooldown == 60) { observer.OnNext("ArrowSet"); }
        }
        yield return null; // 1フレーム待機
    }
}
```
ロジックはあまり変わりませんね。
`ReactiveProperty`とコルーチン、どちらの実装方法が合っているかは未調査です。
<font color="DarkGray">こちらは少々実装が荒いかもしれないです。</font>

# おわりに
以上が`ReactiveProperty`を使用したクールダウンの実装方法です。
この実装方法ならちょっと修正を加えて、残りクールダウンの半分解消する(あと3秒のクールダウンを1.5秒にする)ということも可能です。クールダウンゲージといったパッと見で分かりやすいUIにも組み込みやすいと思います。

また、今回紹介したロジックで複数のスキルを管理したい場合は`ReactiveCollection`を使用すればOKです。
<font color="DarkGray">(コルーチン実装の方で複数スキル管理する場合は上記の実装方法では厳しく、スキル使用のifにcooldownを直接使わず、flagを使用するといった工夫が必要になります)</font>

この実装方法がアンチパターンではないこと祈ります...