【C++】Singletonなアプリでコンポーネント形式のゲームを作る

## 目標

シングルトンでアプリ(ゲーム)を構築し，ゲームをコンポーネント形式で管理できるようにしていきます．

## 動作環境

- C++11 以降
- Win11, Ubuntu(WSL)22.04
- gcc v13

## Singletonとは

デザインパターンの1つで，インスタンスの生成を1度までに制限し，以降は生成済みのインスタンスを参照させます．

コンストラクタを公開せず(private or protected)，`GetInstace`メソッド(public)でインスタンスの有無を確認し，無いなら生成し，有るならポインタ渡しや参照渡しで返します．

## Singletonをまずは作ってみる

今回は，最初からマルチスレッドでインスタンスが同時に作成されないように排他制御を行います．

まずは全体から，

```c++
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>

class Singleton {
   private:
    static std::unique_ptr<Singleton> pinstance_;
    static std::mutex mutex_;

   protected:
    Singleton(const int value) : value_(value) {}

    int value_;

   public:
    ~Singleton() {}

    // 複製禁止
    Singleton(Singleton& singleton) = delete;
    // 代入禁止
    void operator=(const Singleton&) = delete;

    static Singleton& GetInstance(const int& value = 1) {
        // 排他制御を実現する
        std::lock_guard<std::mutex> lock(mutex_);

        if (!pinstance_) {
            pinstance_ = std::unique_ptr<Singleton>(new Singleton(value));
        }

        return *pinstance_;
    }

    int GetValue() const { return value_; }
};

// 静的メンバの宣言
std::unique_ptr<Singleton> Singleton::pinstance_ = nullptr;
std::mutex Singleton::mutex_;

// プロトタイプ
void ThreadFunc1();
void ThreadFunc2();

int main() {
    std::thread t1(ThreadFunc1);
    std::thread t2(ThreadFunc2);

    t1.join();
    t2.join();

    return 0;
}

void ThreadFunc1() {
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    Singleton& singleton = Singleton::GetInstance(3);
    std::cout << singleton.GetValue() << std::endl;
}

void ThreadFunc2() {
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    Singleton& singleton = Singleton::GetInstance(7);
    std::cout << singleton.GetValue() << std::endl;
}
```

:::note warn
静的メンバの宣言を忘れずに！
```cpp
std::unique_ptr<Singleton> Singleton::pinstance_ = nullptr;
std::mutex Singleton::mutex_;
```
:::


### `GetInstance`メソッド

`GetInstance`メソッドに注目します．

```c++
static Singleton& GetInstance(const int& value = 1) {
    // 排他制御を実現する
    std::lock_guard<std::mutex> lock(mutex_);

    if (!pinstance_) {
        pinstance_ = std::unique_ptr<Singleton>(new Singleton(value));
    }

    return *pinstance_;
}
```

`<mutex>`を使って，プライベートメンバにmutexを作成し，ロック操作を行います．よって，1つのスレッドだけがリソースを読み書きできるようになります．`pinstance_`にポインタが割り当てられているか確認したのち，空の場合はスマートポインタの1つである`unique_ptr`で生成します．今回は`make_unique`を使っていません．また`*pinstance_`を返していますが，中身の型は`T&`と参照渡しになります．メリットとしては未定義なものを受け取る側で想定しなくていいことでしょうか．

### 動作の詳細

`main`関数で2つ並行処理，ThreadFunc1とThreadFunc2を実現しています．それぞれで，`GetInstance`を実行し，引数は`3`・`7`と別物にしています．もし，ロック操作をしない場合，出力が揃わないことも多々あります．折角のシングルトンパターンの設計が意味を成さなくなってしまうので必要なんですね
(`lock()`をコメントアウトしてみると同じようになります)．

#### 成功例

```
3
3
```

もしくは

```
7
7
```

#### 失敗例(排他制御しない場合に発生)

```
3
7
```

### 追記('25/2/12)

C++11では，ブロックスコープ内のstatic(ローカル)変数はスレッドセーフであることが規定されています．

<https://cpprefjp.github.io/lang/cpp11/static_initialization_thread_safely.html>

従って，手動でロック操作をしなくてもよい形になっています．

```cpp:GetInstance関数
static Singleton& GetInstance(const int& value = 1) {
    static Singleton instance(value);

    return instance;
}
```

サイトによると，追記前のようなロック操作をユーザが手動で行っていたことが背景にあったそうです．

コメントで教えてくださりありがとうございます．

## ゲーム(アプリ)のベースを構築してみる

今回の本題であるゲームのベースを作っていきます．イメージはUnityのヒエラルキーとコンポーネント．

まずは，Singletonのアプリを構築します．

```cpp
class SingletonApplication {
   private:
    static std::unique_ptr<SingletonApplication> pinstace_;
    static std::mutex mutex_;

    bool running;
    std::vector<std::shared_ptr<GameObject>> objects;

   protected:
    SingletonApplication() : running(true) {
        std::cout << "Application Initialized" << std::endl;
    }

   public:
    SingletonApplication(const SingletonApplication&) = delete;
    SingletonApplication& operator=(const SingletonApplication&) = delete;

    static SingletonApplication& GetInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (!pinstace_) {
            pinstace_ = std::unique_ptr<SingletonApplication>(
                new SingletonApplication());
        }

        return *pinstace_;

        /** '25/2/12 追記: この場合もstatic変数をブロックスコープ内で
                                定義するだけでスレッドセーフになります **/
        
        // static SingletonApplication instance;
        //
        // return instance;
    }

    /* ゲーム用記述 */

    void AddObject(std::shared_ptr<GameObject> obj) { objects.push_back(obj); }

    void Update() {
        for (auto& obj : objects) {
            obj->Update();
        }
    }

    void Render() {
        std::cout << "\033[2J\033[H"; // 出力文をクリア
        for (auto& obj : objects) {
            obj->Render();
        }
    }

    void Run() {
        while (running) {
            Update();
            Render();
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }
    }

    void Stop() { running = false; }
};

std::unique_ptr<SingletonApplication> SingletonApplication::pinstace_ = nullptr;
std::mutex SingletonApplication::mutex_;
```

基本的な構造は変わっていません．`GetInstance`メソッドでインスタンスを得た後に，`Run`メソッドが実行できるようにしているのみです．

### ゲームのオブジェクトの定義

```cpp
class GameObject {
   private:
    std::unordered_map<std::type_index, std::shared_ptr<Component>> components_;

   public:
    std::string name;

    GameObject(std::string _name) : name(_name) {}

    template <typename T, typename... Args>
    void AddComponent(Args&&... args) {
        components_[typeid(T)] =
            std::make_shared<T>(std::forward<Args>(args)...);
    }

    template <typename T>
    T* GetComponent() {
        auto it = components_.find(typeid(T));
        return (it != components_.end()) ? dynamic_cast<T*>(it->second.get())
                                        : nullptr;
    }

    void Update() {
        for (auto& [type, component] : components_) {
            component->Update(*this);
        }
    }

    void Render() {
        for (auto& [type, component] : components_) {
            component->Render(*this);
        }
    }
};
```

ゲームオブジェクトは何らかのコンポーネントを複数持っており，`Update`と`Render`を繰り返し実行します．コンポーネントは，同一キーを持つことができず，順序関係が定義されない`unordered_map`コンテナに格納し，キー型は`typeindex`です(`typeid(T)`で取得可能)．`AddComponent`メソッドでクラスのテンプレートとして`T`，そのコンストラクタに使う引数として`Args`を用意しており，抽象クラスである`Component`の子クラスを複数格納するため，コンテナの値はポインタを使っています．また，解放や所有権の設定が適切になされるよう，スマートポインタの`shared_ptr`と`make_shared`を使います．`GetComponent`メソッド自体に引数はありませんが，テンプレート関数の書き方を活かし，型指定がそのまま検索する型となるようにしています．返す型は参照渡しにすると，未登録のコンポーネント処理に困るのでポインタで返します．

### 抽象クラス`Component`の定義

コンポーネントのベースを作ります．

```cpp
class Component {
   public:
    virtual ~Component() = default;
    virtual void Update(GameObject& obj) {}
    virtual void Render(GameObject& obj) {}
};
```

子が持つデフォルトのメソッドとして`Update`と`Render`を定義しておきます．

### 派生コンポーネントの定義

#### 位置コンポーネント

簡易的なメソッドとして，`move`を追加します．また，座標に関するメンバは公開しておきます．

```cpp
class TransformComponent : public Component {
   public:
    // 公開
    int x, y;

    TransformComponent(int x0, int y0) : x(x0), y(y0) {}

    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
};
```

#### 自動直線移動コンポーネント

オブジェクトが毎フレーム直線移動することを仮定したものです．

```cpp
class AutoLinearMovingComponent : public Component {
   public:
    int dx, dy;

    AutoLinearMovingComponent(int _dx, int _dy) : dx(_dx), dy(_dy) {}

    void Update(GameObject& obj) override {
        TransformComponent* transform = obj.GetComponent<TransformComponent>();
        if (transform) {
            transform->move(dx, dy);
        }
    }
};
```

`Update`の定義で自身のゲームオブジェクトが投げられるようにしていたので`GetComponent`が利用できます．型指定に`<TransformComponent>`を入れたので関数内で`components_.find(typeid(TransformComponent))`が実行されポインタを返してきます．

#### 表示コンポーネント

`Render`をオーバーライドしたコンポーネントを作ります．構造は自動直線移動コンポーネントと大差ありません．

```cpp
class RendererComponent : public Component {
    void Render(GameObject& obj) override {
        TransformComponent* transform = obj.GetComponent<TransformComponent>();
        if (transform) {
            std::cout << "[" << obj.name << "] (" << transform->x
                      << ", " << transform->y << ")" << std::endl;
        }
    }
};
```

### `main`関数

最後に`main`関数ですね．今回はシーン機能を作っていませんが，ゲームオブジェクトと同じ要領で作ることができれば実現可能だと思います．
セットでライブラリのインクルードも記述します．

```cpp
#include <iostream>
#include <memory>
#include <mutex>
#include <typeindex>
#include <unordered_map>
#include <vector>

int main() {
    SingletonApplication& app = SingletonApplication::GetInstance();

    // シーンマネージャーを作ればシーン内のイニシャライズで定義できる

    // オブジェクト作成
    auto player = std::make_shared<GameObject>("Player");
    player->AddComponent<TransformComponent>(5, 5);
    player->AddComponent<RendererComponent>();

    auto enemy = std::make_shared<GameObject>("Enemy");
    enemy->AddComponent<TransformComponent>(10, 2);
    enemy->AddComponent<AutoLinearMovingComponent>(1, 2);
    enemy->AddComponent<RendererComponent>();

    // オブジェクト登録
    app.AddObject(player);
    app.AddObject(enemy);

    // メインループ
    app.Run();
}
```

今回は単一シーンなので，この場でオブジェクトとコンポーネントを登録しています．共に位置コンポーネントと表示コンポーネントは追加し，`enemy`のみ自動直線移動を追加しました．

### 実行結果

実行すると，0.5秒毎に出力がクリアされ，enemy の位置が更新されつつ表示されることがわかります．

```
[Player] (5, 5)
[Enemy] (13, 8)
```

## おわりに

次はグラフィックライブラリ(GLFW や DXライブラリ あたり？)でゲームを本格的に作る方面にもっていきたいですね．