目標
シングルトンでアプリ(ゲーム)を構築し,ゲームをコンポーネント形式で管理できるようにしていきます.
動作環境
- 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
を実行し,引数は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
従って,手動でロック操作をしなくてもよい形になっています.
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);
}
}
};
ゲームオブジェクトは何らかのコンポーネントを複数持っており,Update
とRender
を繰り返し実行します.コンポーネントは,同一キーを持つことができず,順序関係が定義されないunordered_map
コンテナに格納し,キー型はtypeindex
です(typeid(T)
で取得可能).AddComponent
メソッドでクラスのテンプレートとしてT
,そのコンストラクタに使う引数としてArgs
を用意しており,抽象クラスであるComponent
の子クラスを複数格納するため,コンテナの値はポインタを使っています.また,解放や所有権の設定が適切になされるよう,スマートポインタのshared_ptr
とmake_shared
を使います.GetComponent
メソッド自体に引数はありませんが,テンプレート関数の書き方を活かし,型指定がそのまま検索する型となるようにしています.返す型は参照渡しにすると,未登録のコンポーネント処理に困るのでポインタで返します.
抽象クラスComponent
の定義
コンポーネントのベースを作ります.
class Component {
public:
virtual ~Component() = default;
virtual void Update(GameObject& obj) {}
virtual void Render(GameObject& obj) {}
};
子が持つデフォルトのメソッドとしてUpdate
とRender
を定義しておきます.
派生コンポーネントの定義
位置コンポーネント
簡易的なメソッドとして,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ライブラリ あたり?)でゲームを本格的に作る方面にもっていきたいですね.