Siv3Dで「待つ」処理を簡単に書けるC++20コルーチンライブラリCoTaskLibの紹介

この記事はSiv3D Advent Calendar 2024の20日目の記事です。

# はじめに
C++20で、新しい言語構文であるコルーチンが導入されました。
これをSiv3Dで活用するためのタスク型コルーチンライブラリ「CoTaskLib」を開発したので紹介します。

https://github.com/m4saka/CoTaskLib

CoTaskLibを導入すれば、コルーチンを活用してゲーム内の「待ち」を含む処理を簡単に実装できるようになります。

例えば「アニメーションAを再生し、終わるまで待ってからアニメーションBを再生する」といった複数フレームにまたがる処理を、あたかも通常の関数のように時系列順に記述できます。

```cpp:こんな感じで書けるように！
Co::Task<> Example()
{
    co_await AnimationA(); // アニメーションAを再生
    co_await AnimationB(); // 終わったら、アニメーションBを再生
}
```

本記事では、CoTaskLibの使い方について紹介していきます。

# タスクとは？

CoTaskLibにおける「タスク」とは、コルーチンの言語構文を使って記述できる「**複数フレームをまたぐことができる関数**」です。

タスク内では、C++20コルーチンのキーワード(`co_await`/`co_return`/`co_yield`)のうち、下記の2つを使用します。

- `co_await`: 他のタスクを実行し、その完了を待ちます。
- `co_return`: タスクから結果を返します。

※ CoTaskLibでは`co_yield`は使用しません

# 実際に使ってみよう

CoTaskLibを利用する際は、下記のコードをひな型として利用します。
ここからは、MainTask関数へ処理内容を実装していきます。

```cpp:CoTaskLibを利用する際のひな型
#include <Siv3D.hpp>
#include <CoTaskLib.hpp>

Co::Task<> MainTask()
{
    // ここへ処理内容をコルーチンで記述する
    co_return;
}


void Main()
{
    // CoTaskLibを初期化
    Co::Init();

    // MainTaskを実行する。runner変数が有効な間、実行される
    const auto runner = MainTask().runScoped();
    while (System::Update())
    {
    }
}
```

## 1. 指定時間待つ(`Delay`)

`Co::Delay`関数を使うことで、ゲームのメインループを止めることなく時間待ちできます。

```cpp:1秒間待機する例
co_await Co::Delay(1s);
```

これを使って、時間待ちを含む処理を下記のように時間の流れ通りに記述できます。

```cpp:Delayの利用例
Co::Task<> MainTask()
{
    Print << U"スタート！";
    co_await Co::Delay(1s); // 1秒待つ

    Print << U"1秒経過！";
    co_await Co::Delay(1s); // 1秒待つ

    Print << U"2秒経過！";
    co_await Co::Delay(1s); // 1秒待つ

    Print << U"3秒経過！";
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/7a3de5cb-61de-23de-dcf4-b564c59cd4b1.gif)

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

もし仮にCoTaskLibを使用しないで実装する場合、`Stopwatch`を使って時間経過を自前で判定する必要があります。
時間が経過した瞬間のみ実行するために、下記のように前回フレームでの経過秒数(`prevSec`)もチェックする必要があります。

```cpp:OpenSiv3D単体で実装した場合のコード例
void Main()
{
    Stopwatch stopwatch{ StartImmediately::Yes };

    Print << U"スタート！";

    // 前回フレームでの秒数
    int32 prevSec = 0;
 
    while (System::Update())
    {
        // 今回フレームでの秒数
        const int32 sec = stopwatch.s();
  
        if (sec >= 1 && prevSec < 1)
        {
            Print << U"1秒経過！";
        }
        else if (sec >= 2 && prevSec < 2)
        {
            Print << U"2秒経過！";
        }
        else if (sec >= 3 && prevSec < 3)
        {
            Print << U"3秒経過！";
        }

        prevSec = sec;
    }
}
```

</details>

:::

## 2. タスクから戻り値を返す

`Co::Task<T>`のように戻り値の型`T`を指定すれば、タスクから`co_return`で戻り値を返すことができます。返された戻り値は`co_await`で受け取ることができます。

下記の例では、1秒経過後にタスクから戻り値が返され、それを`co_await`で受け取って出力しています。

```cpp
Co::Task<int32> SubTask()
{
    co_await Co::Delay(1s); // 1秒待つ
    co_return 42; // 結果を返す
}

Co::Task<> MainTask()
{
    // SubTaskを実行し、結果を受け取る
    int32 result = co_await SubTask();

    Print << result; // 42が出力される
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/c426dee9-50a6-c439-06c8-1f94cb4f5b8a.gif)

## 3. 条件を満たすまで待機(`WaitUntil`)

`WaitUntil`関数を使用すれば、指定された条件を満たすまで待機することができます。
指定された関数を毎フレーム実行し、その戻り値がfalseの間、待機します。

```cpp
co_await Co::WaitUntil(/*ここへ条件の関数を指定*/);
```

例えば下記のように、マウスクリックされる(`MouseL.down()`がtrueを返す)まで待機できます。

```cpp:WaitUntil関数の例
Co::Task<> MainTask()
{
    Print << U"クリックされるのを待っています...";
 
    // マウスクリックされるまで待機
    co_await Co::WaitUntil([] { return MouseL.down(); });

    Print << U"クリックされました！";
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/788a3bc0-e924-86a6-90e1-ce5d99abb52a.gif)

マウス入力(`MouseL`/`MouseR`)やキーボード入力(`KeyA`/`KeyB`/...)など、`down`関数が実装された型の値であれば、上記の代わりに`Co::WaitUntilDown`関数を利用して記述することもできます。

```cpp:短く書くためのWaitUntilDown関数も用意されている
Co::Task<> MainTask()
{
    Print << U"クリックされるのを待っています...";
 
    // マウスクリックされるまで待機
    co_await Co::WaitUntilDown(MouseL);

    Print << U"クリックされました！";
}
```

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

もし仮にCoTaskLibを使用しないで実装する場合、下記のように毎フレームif文で判定して実行します。
ただし、2回目以降のクリックは無視する必要があるため、実行済みかどうかを保持する状態変数(`isClicked`)を用意しなければいけません。

```cpp:OpenSiv3D単体で実装した場合のコード例
void Main()
{
    Print << U"クリックされるのを待っています...";

    // クリック済みかどうかを状態として保持
    bool isClicked = false;

    while (System::Update())
    {
        if (!isClicked && MouseL.down())
        {
            Print << U"クリックされました！";
            isClicked = true;
        }
    }
}
```

</details>

:::

## 4. 複数のタスクを同時に実行する(`Any`、`All`)

`Any`関数や`All`関数を利用すれば、複数のタスクを同時に実行できます。

### `Any`: いずれかのタスクが完了するまで待機

複数のタスクを同時に実行し、いずれか1つのタスクが完了するまで待機します。

```cpp
co_await Co::Any(タスク1, タスク2, ...);
```

これを利用して、下記のようにいずれかの入力を待つことができます。

```cpp:Any関数の利用例
Co::Task<> MainTask()
{
    Print << U"クリックまたはEnterキーが押下されるのを待っています...";
 
    // マウスクリックまたはEnterキーが押下されるまで待機
    co_await Co::Any(
        Co::WaitUntilDown(MouseL),
        Co::WaitUntilDown(KeyEnter));

    Print << U"クリックまたはEnterキーが押下されました！";
}
```

さらに、各タスクの戻り値は`Optional`のtupleで受け取ることができます。そのため、どのタスクが完了したかによって処理を分岐することもできます。

```cpp:Any関数から戻り値を受け取って分岐する例
Co::Task<> MainTask()
{
    Print << U"クリックまたはEnterキーが押下されるのを待っています...";
 
    // マウスクリックまたはEnterキーが押下されるまで待機
    const auto [isClicked, isEnter] = co_await Co::Any(
        Co::WaitUntilDown(MouseL),
        Co::WaitUntilDown(KeyEnter));

    if (isClicked)
    {
        // マウスクリックされた場合
        Print << U"クリックされました！";
    }
    else
    {
        // Enterキーが押下された場合
        Print << U"Enterが押下されました！";
    }
}
```

::: note
**補足: 戻り値がないタスクに対する`Any`/`All`の戻り値の型について**
`Any`/`All`関数では、戻り値がないタスク(`Co::Task<>`)に対しては戻り値の型は`Co::VoidResult`型(空の構造体)へ置換されます(void型はtupleとして返せないため)。

上記の例の`Co::WaitUntilDown`も戻り値がないタスクなので、`Co::Any`からは`std::tuple<Optional<Co::VoidResult>, Optional<Co::VoidResult>>`型の値が返されます。`isClicked`変数と`isEnter`変数はそれぞれ`Optional<Co::VoidResult>`型になり、それをif文で利用しています。
:::

### `All`: 全てのタスクが完了するまで待機

複数のタスクを同時に実行し、全てのタスクが完了するまで待機します。

```cpp
co_await Co::All(タスク1, タスク2, ...);
```

複数のタスクを同時実行したい場合に便利です。

```cpp
Co::Task<> SubTask()
{
    Print << U"スタート！";
    co_await Co::Delay(1s);

    Print << U"1秒経過！";
    co_await Co::Delay(1s);

    Print << U"2秒経過！";
    co_await Co::Delay(1s);

    Print << U"3秒経過！";
}

Co::Task<> MainTask()
{
    // 2つのSubTaskを同時に実行する
    co_await Co::All(SubTask(), SubTask());
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/433470a0-97ef-01ec-12b7-ca850835fb4c.gif)

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

もし仮にCoTaskLibを使わずに実装する場合、状態を含む処理を複数使い回すには下記のようにクラスとして実装する必要があるため少し面倒です。

```cpp:OpenSiv3D単体で実装した場合のコード例
class Counter
{
private:
    Stopwatch m_stopwatch{ StartImmediately::Yes };
    int32 m_prevSec = 0;

public:
    Counter()
    {
        Print << U"スタート！";
    }

    void update()
    {
        const int32 sec = m_stopwatch.s();
        if (sec >= 1 && m_prevSec < 1)
        {
            Print << U"1秒経過！";
        }
        else if (sec >= 2 && m_prevSec < 2)
        {
            Print << U"2秒経過！";
        }
        else if (sec >= 3 && m_prevSec < 3)
        {
            Print << U"3秒経過！";
        }
        m_prevSec = sec;
    }
};

void Main()
{
    Counter counter1;
    Counter counter2;

    while (System::Update())
    {
        counter1.update();
        counter2.update();
    }
}
```

</details>

:::


:::note warn

**注意: タスクの同時実行はシングルスレッドで動作します**
ここでの「複数のタスクを同時に実行」とは、メインスレッド内での**疑似的**な並列実行を指します。マルチスレッドを使った並列処理ではありません。

具体的には、CoTaskLibは毎フレーム、**現在実行中のタスクの1フレーム分の処理をメインスレッド内で1タスクずつ順に実行していく**仕組みになっています(これは`System::Update()`内で実行されます)。そのため、同時実行中のタスクが異なるスレッドで並列に実行されてしまうことはなく、スレッド安全性・データ競合などを心配する必要もありません。

:::

## 5. 描画処理を含むタスクを実装する(シーケンス)

**シーケンス**を利用することで、描画処理(draw関数)を含むタスクを実装できます。


シーケンスを実装するには、`SequenceBase<T>`を継承したクラスを作成し、下記のようにstart関数・draw関数をオーバーライドします。

```cpp:シーケンスの骨組み
class ExampleSequence : public Co::SequenceBase<>
{
private:
    Co::Task<> start() override
    {
        // ここへタスクを実装
        co_return;
    }

    void draw() const override
    {
        // 毎フレーム呼ばれる描画処理を実装
    }
}
```

シーケンスは、`Play<T>()`関数で再生できます。
もしシーケンスにコンストラクタ引数がある場合は、`Play`関数の引数として指定できます。

```cpp:シーケンスの再生
co_await Co::Play<ExampleSequence>(/*コンストラクタ引数(あれば)*/);
```

下記は、星形を描画するシーケンスの例です。コンストラクタ引数として描画する星の色を受け取っています。

```cpp:指定された色で星形を描画するシーケンスの例
class StarSequence : public Co::SequenceBase<>
{
public:
    // コンストラクタ
    explicit StarSequence(const ColorF& color)
        : m_color(color)
    {
    }

private:
    ColorF m_color;

    Co::Task<> start() override
    {
        // 1秒待機
        co_await Co::Delay(1s);
    }

    void draw() const override
    {
        // 星形を描画
        Shape2D::Star(200, Scene::Center()).draw(m_color);
    }
};

Co::Task<> MainTask()
{
    // 赤色→青色→黄色の順番で星形を描画
    co_await Co::Play<StarSequence>(Palette::Red);
    co_await Co::Play<StarSequence>(Palette::Blue);
    co_await Co::Play<StarSequence>(Palette::Yellow);
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/2d841741-60dd-3edd-fdf7-4e10a6b5506f.gif)

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

もし仮にCoTaskLibを使わずに実装する場合、下記のように時間をもとに色を分岐する実装になると思います。
コード自体はシンプルですが、全体の経過時間をもとに判定しているので、途中に処理を挟みたくなった場合はそれぞれの秒数を順に後ろにずらしていく変更が必要になります。

```cpp:OpenSiv3D単体で実装した場合のコード例
void DrawStar(const ColorF& color)
{
    Shape2D::Star(200, Scene::Center()).draw(color);
}

void Main()
{
    Stopwatch stopwatch{ StartImmediately::Yes };

    while (System::Update())
    {
        const Duration elapsed = stopwatch.elapsed();

        // 経過時間に応じて色を切り替えて描画
        if (elapsed < 1s)
        {
            DrawStar(Palette::Red);
        }
        else if (elapsed < 2s)
        {
            DrawStar(Palette::Blue);
        }
        else if (elapsed < 3s)
        {
            DrawStar(Palette::Yellow);
        }
    }
}
```

</details>

:::

## 6. イージングで滑らかに値を変化させる(`Ease`)

`Ease`関数を使うことで、時間をかけて滑らかに値を変化させることができます。

```cpp
co_await Co::Ease(変数のポインタ, 時間の長さ).to(目標値).play();
```

### 座標を移動する例

下記は、`Vec2`型の`m_position`を操作し、円の描画位置の座標を滑らかに変化させる例です。

```cpp
class EaseExample : public Co::SequenceBase<>
{
private:
    Vec2 m_position{ 100, 100 };

    Co::Task<> start() override
    {
        // 0.5秒かけて(100, 100)から(400, 500)に移動
        co_await Co::Ease(&m_position, 0.5s).to(400, 500).play();

        // 0.2秒待機
        co_await Co::Delay(0.2s);

        // 0.5秒かけて(400, 500)から(700, 100)に移動
        co_await Co::Ease(&m_position, 0.5s).to(700, 100).play();

        // 0.2秒待機
        co_await Co::Delay(0.2s);
    }

    void draw() const override
    {
        Circle{ m_position, 50 }.draw();
    }
};

Co::Task<> MainTask()
{
    co_await Co::Play<EaseExample>();
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/b3f1f114-89cc-c295-37e4-362729819f15.gif)

:::note info

**補足: イージング関数の種類について**

`Co::Ease`関数では、デフォルトのイージング関数に`EaseOutQuad`(目標値に早めに近づく曲線カーブ)が指定されています。

異なるイージング関数を利用したい場合、`setEase`関数で指定できます。

```cpp:別のイージング関数(EaseInQuad)を指定する例
co_await Co::Ease(&m_value, 1s).to(1.0).setEase(EaseInQuad).play();
```

なお、イージングに線形関数を利用したい(直線的に推移させたい)場合、あらかじめ用意されている`Co::LinearEase`関数も利用できます。

```cpp:線形関数でイージングする例
co_await Co::LinearEase(&m_value, 1s).to(1.0).play();
```

:::

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

もし仮にCoTaskLibを使わずに実装する場合、状態を列挙型で表現する必要があるためソースコードが複雑になりやすいです。

```cpp:OpenSiv3D単体で実装した場合のコード例
class MovingCircle
{
public:
    void update()
    {
        const double elapsedSec = m_stopwatch.sF();
        double rate = 0.0;

        switch (m_state)
        {
        case State::MoveDownRight:
            // 0.5秒かけて(100,100)から(400,500)へ移動
            rate = EaseOutQuad(Min(elapsedSec / 0.5, 1.0));
            m_position.x = Math::Lerp(100.0, 400.0, rate);
            m_position.y = Math::Lerp(100.0, 500.0, rate);

            if (elapsedSec >= 0.5)
            {
                m_state = State::Wait1;
                m_stopwatch.restart();
            }
            break;

        case State::Wait1:
            // 0.2秒待機
            if (elapsedSec >= 0.2)
            {
                m_state = State::MoveUpRight;
                m_stopwatch.restart();
            }
            break;

        case State::MoveUpRight:
            // 0.5秒かけて(400,500)から(700,100)へ移動
            rate = EaseOutQuad(Min(elapsedSec / 0.5, 1.0));
            m_position.x = Math::Lerp(400.0, 700.0, rate);
            m_position.y = Math::Lerp(500.0, 100.0, rate);

            if (elapsedSec >= 0.5)
            {
                m_state = State::Wait2;
                m_stopwatch.restart();
            }
            break;

        case State::Wait2:
            // 0.2秒待機
            if (elapsedSec >= 0.2)
            {
                m_state = State::Finished;
            }
            break;

        default:
            break;
        }
    }

    void draw() const
    {
        Circle{ m_position, 50 }.draw();
    }

    bool isFinished() const
    {
        return m_state == State::Finished;
    }

private:
    enum class State
    {
        MoveDownRight,
        Wait1,
        MoveUpRight,
        Wait2,
        Finished,
    };

    Vec2 m_position{ 100, 100 };
    Stopwatch m_stopwatch{ StartImmediately::Yes };
    State m_state = State::MoveDownRight;
};

void Main()
{
    MovingCircle movingCircle;

    while (System::Update())
    {
        if (!movingCircle.isFinished())
        {
            movingCircle.update();
            movingCircle.draw();
        }
    }
}
```

</details>

:::

### アルファ値を変化させてフェードイン・フェードアウトする例

下記は、`double`型の`m_alpha`を0～1で変化させ、指定したテキストをフェード表示する例です。

```cpp
// テキストをフェード表示・クリック待機するシーケンス
class TextFadeSequence : public Co::SequenceBase<>
{
public:
    explicit TextFadeSequence(StringView text)
        : m_text(text)
    {
    }

private:
    Font m_font{ 60 };
    String m_text;
    double m_alpha = 0.0;
    bool m_isWaiting = false;

    Co::Task<> start() override
    {
        // 1秒かけてアルファ値を1に変化させる
        co_await Co::Ease(&m_alpha, 1s).to(1.0).play();

        // クリックされるまで待機
        m_isWaiting = true;
        co_await Co::WaitUntilDown(MouseL);
        m_isWaiting = false;

        // 1秒かけてアルファ値を0に変化させる
        co_await Co::Ease(&m_alpha, 1s).to(0.0).play();
    }

    void draw() const override
    {
        const ColorF textColor{ Palette::White, m_alpha };
        m_font(m_text).drawAt(Scene::Center(), textColor);

        if (m_isWaiting)
        {
            m_font(U"クリックで次へ").drawAt(40, Scene::Center().movedBy(0, 150));
        }
    }
};

Co::Task<> MainTask()
{
    co_await Co::Play<TextFadeSequence>(U"コルーチンを使えば");
    co_await Co::Play<TextFadeSequence>(U"待つ処理が");
    co_await Co::Play<TextFadeSequence>(U"簡単に実装できます！");
    co_await Co::Play<TextFadeSequence>(U"すごいでしょ？");
}
```

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/f6da4901-2285-d937-0370-50275a63ce5f.gif)

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

こちらの例も、CoTaskLibを使わないで実装すると複雑な状態管理が必要になります。

```cpp:OpenSiv3D単体で実装した場合のコード例
class TextFade
{
public:
    explicit TextFade(StringView text)
        : m_text(text)
    {
    }

    void update()
    {
        const auto elapsedTime = m_stopwatch.elapsed();

        switch (m_state)
        {
        case State::FadeIn: // フェードイン中
            m_alpha = Min(elapsedTime / 1s, 1.0);
            if (elapsedTime >= 1s)
            {
                m_state = State::Waiting;
                m_stopwatch.restart();
            }
            break;

        case State::Waiting: // クリック待ち
            m_isWaiting = true;
            if (MouseL.down())
            {
                m_isWaiting = false;
                m_state = State::FadeOut;
                m_stopwatch.restart();
            }
            break;

        case State::FadeOut: // フェードアウト中
            m_alpha = Max(1.0 - (elapsedTime / 1s), 0.0);
            if (elapsedTime >= 1s)
            {
                m_state = State::Finished; // 完了
            }
            break;

        default:
            break;
        }
    }

    void draw() const
    {
        const ColorF textColor{ Palette::White, m_alpha };
        m_font(m_text).drawAt(Scene::Center(), textColor);

        if (m_isWaiting)
        {
            m_font(U"クリックで次へ").drawAt(40, Scene::Center().movedBy(0, 150));
        }
    }

    bool isFinished() const
    {
        return m_state == State::Finished;
    }

private:
    enum class State
    {
        FadeIn,
        Waiting,
        FadeOut,
        Finished,
    };
    Font m_font{ 60 };
    String m_text;
    double m_alpha = 0.0;
    bool m_isWaiting = false;
    Stopwatch m_stopwatch{ StartImmediately::Yes };
    State m_state = State::FadeIn;
};

void Main()
{
    Array<String> texts = {
        U"コルーチンを使えば",
        U"待つ処理が",
        U"簡単に実装できます！",
        U"すごいでしょ？",
    };

    size_t index = 0;
    Optional<TextFade> current;

    while (System::Update())
    {
        if (index >= texts.size())
        {
            // 全テキストを表示済みのため何もしない
            continue;
        }

        if (!current)
        {
            current = TextFade{ texts[index] };
        }

        current->update();
        current->draw();

        if (current->isFinished())
        {
            // 次のテキストへ
            current.reset();
            ++index;
        }
    }
}
```

</details>

:::

## 7. シーン遷移を実装する(`SceneBase`)

**シーン**は、ゲーム内の大まかな画面（例: タイトル画面、ゲーム画面、リザルト画面など）を表す単位です。
シーケンスと少し似ていますが、シーン同士を互いに遷移できる点が異なります。

シーンを実装するには、下記のように`SceneBase`を継承したクラスを作成し、メンバ関数をオーバーライドします。

```cpp:シーンの骨組み
class ExampleScene1 : public Co::SceneBase
{
private:
    Co::Task<> fadeIn() override
    {
        // フェードイン処理(startと同時に実行開始される)
        co_await Co::ScreenFadeIn(1s);
    }

    Co::Task<> start() override
    {
        // シーン開始時に実行されるタスク

        // 下記で次のシーン遷移先を指定できる
        requestNextScene<ExampleScene2>();
    }

    Co::Task<> fadeOut() override
    {
        // フェードアウト処理(startの後に実行される)
        co_await Co::ScreenFadeOut(1s);
    }

    void draw() const override
    {
        // シーンの毎フレームの描画処理
    }
};
```

シーンを実行するには、下記のように`PlaySceneFrom<T>()`関数に最初のシーンを指定します。

```cpp
// TitleSceneから始まる一連のシーン遷移を実行
co_await Co::PlaySceneFrom<TitleScene>();
```

### シーンを利用したゲームサンプル

シーン機能を実際に活用して、サンプルゲームを実装してみました。
タイトル画面・ゲーム画面・結果画面の3つのシーンを備えた、シンプルなクリック連打ゲームです。

![bandicam2024-12-2103-31-51-033-ezgif.com-video-to-gif-converter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192805/8507a881-53c0-3915-f013-315edec897c1.gif)

<details><summary>ソースコードを見る</summary>

```cpp:シーン機能を利用して実装した連打ゲーム
#include <Siv3D.hpp>
#include <CoTaskLib.hpp>

// 前方宣言
class GameScene;
class ResultScene;

// タイトル画面
class TitleScene : public Co::SceneBase
{
private:
    Font m_fontBold{ 60, Typeface::Bold };
    Font m_font{ 30 };

    Co::Task<> fadeIn() override
    {
        co_await Co::ScreenFadeIn(1s);
    }

    Co::Task<> start() override
    {
        // クリックしたら、ゲーム画面へ遷移
        co_await Co::WaitUntilDown(MouseL);
        requestNextScene<GameScene>();
    }

    Co::Task<> fadeOut() override
    {
        co_await Co::ScreenFadeOut(1s, Palette::White);
    }

    void draw() const override
    {
        m_fontBold(U"連打ゲーム").drawAt(Scene::Center().movedBy(0, -50));

        ColorF messageColor;
        if (isFadingOut())
        {
            // クリック後は点滅
            messageColor = ColorF{ Palette::White, Periodic::Square0_1(0.1s) };
        }
        else
        {
            messageColor = ColorF{ Palette::White, Periodic::Sine0_1(2s) };
        }
        m_font(U"クリックで始めます").drawAt(Scene::Center().movedBy(0, 50), messageColor);
    }
};

// ゲーム画面
class GameScene : public Co::SceneBase
{
private:
    // メッセージ表示シーケンス
    class ShowMessage : public Co::SequenceBase<>
    {
    public:
        explicit ShowMessage(StringView text, Duration duration)
            : m_text(text)
            , m_duration(duration)
        {
        }

    private:
        Font m_font{ 120 };
        String m_text;
        Duration m_duration;
        double m_scale = 1.0;
        double m_alpha = 1.0;

        Co::Task<> start() override
        {
            co_await Co::All(
                Co::Ease(&m_scale, m_duration).to(1.2).play(),
                Co::Ease(&m_alpha, m_duration).to(0.0).setEase(EaseInQuad).play());
        }

        void draw() const override
        {
            m_font(m_text).drawAt(120 * m_scale, Scene::Center(), ColorF{ Palette::Yellow, m_alpha });
        }
    };

    Font m_font{ 80 };
    Timer m_gameTimer{ 5s };
    int32 m_clickCount = 0;

    Co::Task<> fadeIn() override
    {
        co_await Co::ScreenFadeIn(1s, Palette::White);
    }

    Co::Task<> start() override
    {
        // カウントダウン
        co_await Co::Play<ShowMessage>(U"3", 1s);
        co_await Co::Play<ShowMessage>(U"2", 1s);
        co_await Co::Play<ShowMessage>(U"1", 1s);

        // ゲーム処理
        co_await Co::All(
            Co::Play<ShowMessage>(U"START", 1.5s),
            game());
        co_await Co::Play<ShowMessage>(U"終了！", 1.5s);

        // 結果画面へ遷移
        requestNextScene<ResultScene>(m_clickCount);
    }

    Co::Task<> game()
    {
        m_gameTimer.start();

        // ゲームは毎フレームの処理として実装
        co_await Co::UpdaterTask<void>(
            [this] (Co::TaskFinishSource<void>& tfs)
            {
                if (MouseL.down())
                {
                    // 連打回数をカウント
                    ++m_clickCount;
                }

                if (m_gameTimer.reachedZero())
                {
                    // ゲーム終了
                    tfs.requestFinish();
                }
            });
    }

    Co::Task<> fadeOut() override
    {
        co_await Co::ScreenFadeOut(1s);
    }

    void draw() const override
    {
        m_font(U"スコア: {}"_fmt(m_clickCount)).drawAt(Scene::Center().movedBy(0, -50));
        m_font(U"残り{}秒"_fmt(m_gameTimer.s_ceil())).drawAt(40, Scene::Center().movedBy(0, 50));
    }
};

// 結果画面
class ResultScene : public Co::SceneBase
{
public:
    explicit ResultScene(int clickCount)
        : m_clickCount(clickCount)
    {
    }

    Co::Task<> fadeIn() override
    {
        co_await Co::ScreenFadeIn(1s);
    }

    Co::Task<> start() override
    {
        // クリックしたら、タイトル画面へ遷移
        co_await Co::WaitUntilDown(MouseL);
        requestNextScene<TitleScene>();
    }

    Co::Task<> fadeOut() override
    {
        co_await Co::ScreenFadeOut(1s);
    }

    void draw() const override
    {
        m_fontBold(U"結果").drawAt(Scene::Center().movedBy(0, -200));
        m_fontBold(U"スコア: {}"_fmt(m_clickCount)).drawAt(Scene::Center().movedBy(0, -30));
        m_font(U"クリックでタイトルに戻ります").drawAt(Scene::Center().movedBy(0, 100), ColorF{ Palette::White, Periodic::Sine0_1(2s) });
    }

private:
    Font m_fontBold{ 50, Typeface::Bold };
    Font m_font{ 30 };
    int m_clickCount;
};

Co::Task<> MainTask()
{
    // 一連のシーン遷移をタイトル画面から開始
    co_await Co::PlaySceneFrom<TitleScene>();
}

void Main()
{
    Co::Init();

    const auto runner = MainTask().runScoped();
    while (System::Update())
    {
    }
}
```

</details>

:::note info

<details><summary>(参考) 上記の例をCoTaskLibを使わずに実装した場合のコード例</summary>

Siv3Dにも標準のシーン管理機能があり、ある程度似たように実装できます。しかし、毎フレームの処理として実装する必要があるため、TimerやStopwatchを駆使して状態を自前で管理する必要があります。

また、シーン間の値の受け渡しにコンストラクタ引数を利用することができないため、グローバル変数に近い存在である共有データを利用して受け渡さないといけません。

```cpp:OpenSiv3D単体で実装した場合のコード例
#include <Siv3D.hpp>

struct SharedData
{
    int32 resultScore = 0;

    std::function<void(const ColorF&)> setFadeColor;
};

using App = SceneManager<String, SharedData>;

// タイトル画面
class TitleScene : public App::Scene
{
private:
    Font m_fontBold{ 60, Typeface::Bold };
    Font m_font{ 30 };
    double m_messageAlpha = 0.0;
    bool m_fadeOut = false;

public:
    TitleScene(const InitData& init)
        : IScene(init)
    {
    }

    void update() override
    {
        m_messageAlpha = Periodic::Sine0_1(2s);

        if (MouseL.down())
        {
            // クリックでゲーム画面に遷移
            getData().setFadeColor(Palette::White);
            changeScene(U"Game");
        }
    }

    void updateFadeIn(double) override { update(); }

    void updateFadeOut(double) override
    {
        // クリック後は点滅
        m_messageAlpha = Periodic::Square0_1(0.1s);
    }

    void draw() const override
    {
        m_fontBold(U"連打ゲーム").drawAt(Scene::Center().movedBy(0, -50));

        const ColorF messageColor{ Palette::White, m_messageAlpha };
        m_font(U"クリックで始めます").drawAt(Scene::Center().movedBy(0, 50), messageColor);
    }
};

// ゲーム画面
class GameScene : public App::Scene
{
private:
    Font m_font{ 80 };
    Font m_messageFont{ 120 };
    Timer m_countdownTimer{ 3s, StartImmediately::Yes };
    Timer m_startMessageTimer{ 1.5s, StartImmediately::No };
    Timer m_gameTimer{ 5s, StartImmediately::No };
    Timer m_endMessageTimer{ 1.5s, StartImmediately::No };
    int32 m_clickCount = 0;

    void drawMessage(StringView message, double rate) const
    {
        const double scale = Math::Lerp(1.0, 1.2, rate);
        const double alpha = Math::Lerp(1.0, 0.0, rate);
        const ColorF color{ Palette::Yellow, alpha };
        m_messageFont(U"{}"_fmt(message)).drawAt(120 * scale, Scene::Center(), color);
    }

public:
    GameScene(const InitData& init)
        : IScene(init)
    {
    }

    void update() override
    {
        if (m_countdownTimer.isRunning())
        {
            // カウントダウン中(何もしない)
        }
        else if (m_countdownTimer.reachedZero() && !m_gameTimer.isStarted())
        {
            // カウントダウン後、ゲーム開始
            m_gameTimer.start();
            m_startMessageTimer.start();
        }
        else if (m_gameTimer.isRunning())
        {
            // ゲーム中
            if (MouseL.down())
            {
                ++m_clickCount;
            }
        }
        else if (m_gameTimer.reachedZero() && !m_endMessageTimer.isStarted())
        {
            // ゲーム終了後、終了メッセージ表示
            m_endMessageTimer.start();
        }
        else if (m_endMessageTimer.isRunning())
        {
            // 終了メッセージ中(何もしない)
        }
        else if (m_endMessageTimer.reachedZero())
        {
            // 終了メッセージ後、結果画面に遷移
            getData().resultScore = m_clickCount;
            getData().setFadeColor(Palette::Black);
            changeScene(U"Result");
        }
    }

    void draw() const override
    {
        // カウントダウン表示
        if (m_countdownTimer.isRunning())
        {
            const int32 sec = m_countdownTimer.s_ceil();
            const double rate = sec - m_countdownTimer.sF();
            drawMessage(U"{}"_fmt(sec), rate);
        }

        // 終了メッセージ
        if (m_endMessageTimer.isRunning())
        {
            const double rate = m_endMessageTimer.progress0_1();
            drawMessage(U"終了！", rate);
        }

        m_font(U"スコア: {}"_fmt(m_clickCount)).drawAt(Scene::Center().movedBy(0, -50));
        m_font(U"残り{}秒"_fmt(m_gameTimer.s_ceil())).drawAt(40, Scene::Center().movedBy(0, 50));
    }
};

// 結果画面
class ResultScene : public App::Scene
{
private:
    Font m_font{ 50, Typeface::Bold };
    Font m_messageFont{ 30 };
    int32 m_score;

public:
    ResultScene(const InitData& init)
        : IScene(init)
        , m_score(getData().resultScore)
    {
    }

    void update() override
    {
        if (MouseL.down())
        {
            changeScene(U"Title");
        }
    }

    void draw() const override
    {
        m_font(U"結果").drawAt(Scene::Center().movedBy(0, -200));
        m_font(U"スコア: {}"_fmt(m_score)).drawAt(Scene::Center().movedBy(0, -30));
        m_messageFont(U"クリックでタイトルに戻ります").drawAt(Scene::Center().movedBy(0, 100), ColorF{ Palette::White, Periodic::Sine0_1(2s) });
    }
};

void Main()
{
    App manager;
    manager.add<TitleScene>(U"Title");
    manager.add<GameScene>(U"Game");
    manager.add<ResultScene>(U"Result");
    manager.get()->setFadeColor = [&] (const ColorF& color) { manager.setFadeColor(color); };

    while (System::Update())
    {
        if (!manager.update())
        {
            break;
        }
    }
}
```

</details>

:::

# CoTaskLibを上手に利用するコツ

最後に、実際のゲームプログラムでCoTaskLibを上手に利用するためのコツを紹介します。

## ★コツ1: `runScoped`関数によるタスク実行はむやみに増やさない

Taskの`runScoped`関数を利用すれば、通常の関数内からでもタスクを新たに実行することができて便利です。

しかし、タスクがばらばらに同時実行されてしまうことでプログラムの流れの複雑化につながりやすいため、メインとなる1個のタスク以外での`runScoped`関数の利用はなるべく減らすことをお勧めします。
同時実行が必要な場合もなるべく`co_await`と`Any`/`All`でやりくりするのがお勧めです。

下記は失敗例で、`runScoped`関数で実行したことで値の受け渡しにグローバル変数が必要になってしまった例です。

```cpp:❌ 悪い例: runScopedで同時実行したタスク間でグローバル変数で値を受け渡す

// グローバル変数
Optional<int32> value;

Co::Task<> Task1()
{
    // ... (何らかの処理)

    value = 42; // 結果をvalueに代入
}

Co::Task<> Task2()
{
    // valueに結果が入るまで待つ
    co_await Co::WaitUntil([&] { return value.has_value(); });

    // ... (valueを使った何らかの処理)
}

void Main()
{
    Co::Init();

    // 2つのタスクを同時に実行(runner変数が有効な間実行される)
    const auto runner1 = Task1().runScoped();
    const auto runner2 = Task2().runScoped();
    while (System::Update())
    {
    }
}
```

複数のタスクを`runScoped`で別々に実行すると、このように外部の変数(グローバル変数など)を使ってデータをやり取りする状況が生じやすいです。

外部の変数を経由してデータを受け渡すと、現在どのタスクの完了を待っている状態かがコード上からは把握しづらくなり、バグを修正するときの原因究明も難しくなります。

互いに処理が依存し合うタスクがある場合、`runScoped`より`co_await`を優先して使用することで、下記のように処理の流れを明確にできます。

```cpp:✅ 良い例: co_return/co_awaitを使って値を受け渡している
Co::Task<> Task1()
{
    // ... (何らかの処理)

    co_return 42; // 結果は戻り値で返す
}

Co::Task<> Task2(int32 value)
{
    // ... (valueを使った何らかの処理)
}

Co::Task<> MainTask()
{
    // Task1から戻り値を受け取り、それを使って次のTask2を実行
    int32 value = co_await Task1();
    co_await Task2(value);
}
```

## ★コツ2: 無理に全てタスクで実装しようとしない。update関数での実装も検討する

タスクを使うことで「待ち」を含む処理が簡単に書けるようになりました。
しかし、処理内容によってはタスクを使った実装よりも、毎フレームの処理(update関数)として実装する方が向いている場合があります。

- **タスクで実装する方が向いている例**:
    - **ターン制バトル**: ターン終了やユーザーの選択を待つ処理がタスクで楽に書けます
    - **ノベルゲーム**: テキスト表示やクリックを待つ処理がタスクで楽に書けます
- **毎フレームの処理(update関数)として実装する方が向いている例**:
    - **アクションゲーム**: キャラクターを動かすために毎フレーム入力を処理する必要があります
    - **音楽ゲーム**: 入力をリアルタイムにタイミング判定するため毎フレーム入力を処理する必要があります

タスクで実装しにくい処理は無理にタスクとして実装しようとせず、適宜下記の機能を利用してupdate関数を併用して実装するのがコツです。

:::note info

下記3つの機能を使えば、毎フレーム実行されるupdate関数をタスクとして利用することができます。

#### `UpdaterTask`: タスクをupdate関数で記述

<details><summary>UpdaterTaskの使い方</summary>

下記3通りの書き方があります。

```cpp:終了条件がない場合
co_await Co::UpdaterTask(
    [] ()
    {
        // 毎フレーム実行される
    });
```

```cpp:戻り値はないが終了条件がある場合
co_await Co::UpdaterTask<void>(
    [] (Co::TaskFinishSource<void>& tfs)
    {
        // 毎フレーム実行される
        
        if (/*何らかの条件を満たしたら*/)
        {
            // 下記で終了できる
            tfs.requestFinish();
        }
    });
```

```cpp:戻り値がある場合
int32 result = co_await Co::UpdaterTask<int32>(
    [] (Co::TaskFinishSource<int32>& tfs)
    {
        // 毎フレーム実行される
        
        if (/*何らかの条件を満たしたら*/)
        {
            // 下記で戻り値を設定して終了できる
            tfs.requestFinish(42);
        }
    });
```

</details>

#### `UpdaterSequenceBase`: シーケンスをupdate関数で記述

<details><summary>UpdaterSequenceBaseの使い方</summary>

下記を通常のシーケンスと同じように利用できます。

```cpp:UpdaterSequenceBase継承クラスの骨組み
class ExampleSequence : public Co::UpdaterSequenceBase<>
{
private:
    void update() override
    {
        // ここに毎フレームの処理を記述

        if (/*何らかの条件*/)
        {
            // シーケンスを終了させたい場合はrequestFinish関数を呼ぶ
            requestFinish();
        }
    }

    void draw() const override
    {
        // ここに毎フレームの描画処理を記述
    }
}
```

</details>

#### `UpdaterSceneBase`: シーンをupdate関数で記述

<details><summary>UpdaterSceneBaseの使い方</summary>

下記を通常のシーンと同じように利用できます。

```cpp:UpdaterSceneBase継承クラスの骨組み
class ExampleScene1 : public Co::UpdaterSceneBase
{
private:
    Co::Task<> fadeIn() override
    {
        // フェードイン処理(updateと同時に実行される)
        co_await Co::ScreenFadeIn(1s);
    }

    void update() override
    {
        // ここに毎フレームの処理を記述

        if (/*何らかの条件*/)
        {
            // 下記で次のシーンへ遷移できる
            requestNextScene<GameScene>();
        }

        if (/*何らかの条件*/)
        {
            // 下記でシーン遷移を終了できる
            requestSceneFinish();
        }
    }

    Co::Task<> fadeOut() override
    {
        // フェードアウト処理(update内でrequest系の関数を呼んで終了した後に実行される)
        co_await Co::ScreenFadeOut(1s);
    }

    void draw() const override
    {
        // シーンの毎フレームの描画処理
    }
};
```

</details>

:::

# まとめ

Siv3D用コルーチンライブラリCoTaskLibを使えば、「待ち」を含む処理を簡単に書けます。

https://github.com/m4saka/CoTaskLib

- タスクとは、コルーチンの構文を使って書ける「**複数フレームをまたぐことができる関数**」
- シーケンスとは「**タスクとdraw関数をセットにした、描画処理を含むタスク**」
- イージング機能(`Ease`)を使えば、値を滑らかに推移させられる
- シーン機能を利用することで、ゲーム内の大まかな画面ごとの処理もタスクとして実装できる