0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-09

目標

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

動作環境

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

Singletonとは

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

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

Singletonをまずは作ってみる

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

まずは全体から,

#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;
}

静的メンバの宣言を忘れずに!

std::unique_ptr<Singleton> Singleton::pinstance_ = nullptr;
std::mutex Singleton::mutex_;

GetInstanceメソッド

GetInstanceメソッドに注目します.

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を実行し,引数は37と別物にしています.もし,ロック操作をしない場合,出力が揃わないことも多々あります.折角のシングルトンパターンの設計が意味を成さなくなってしまうので必要なんですね
(lock()をコメントアウトしてみると同じようになります).

成功例

3
3

もしくは

7
7

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

3
7

追記('25/2/12)

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

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

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

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

    return instance;
}

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

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

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

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

まずは,Singletonのアプリを構築します.

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メソッドが実行できるようにしているのみです.

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

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);
        }
    }
};

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

抽象クラスComponentの定義

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

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

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

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

位置コンポーネント

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

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;
    }
};

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

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

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をオーバーライドしたコンポーネントを作ります.構造は自動直線移動コンポーネントと大差ありません.

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関数ですね.今回はシーン機能を作っていませんが,ゲームオブジェクトと同じ要領で作ることができれば実現可能だと思います.
セットでライブラリのインクルードも記述します.

#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ライブラリ あたり?)でゲームを本格的に作る方面にもっていきたいですね.

0
0
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?