はじめに
ゲームオブジェクトのアップデート関数をコンポーネント化して一括で更新できるようにします。
#1.アップデート関数
よくあるアップデート関数として描画関数draw()を作ることがあるかと思います。
struct PLAYER { void draw(){} };
struct ENEMY { void draw(){} };
void main(){
PLAYER player;
ENEMY enemy;
player.draw();
enemy.draw();
}
このやり方は直感的で初期は作成が容易ですが、作成が進むにつれて下記のような問題が出てきます。
1.アップデート関数の種類が増えるたびに呼び出しの記述を追加する必要がある
struct PLAYER {
void draw(){}
void secall(){} // secall を追加、main()でplayer.secall()を忘れずに追加しないと...
};
これは下記のようにUpdate()のようなメソッドを用意することで多少緩和できますが、追加そのものはコードのどこかで必ず発生します。
struct PLAYER {
void draw(){}
void secall(){}
void Update(){ // main()では player.Update() だけでよくなり修正は不要になる
draw();
secall(); // Update()内への追加はやっぱり必要
}
};
2.関連するメンバ変数が増えていくと変数名の管理が大変になってくる
struct PLAYER {
int draw_timer; // どのアップデート関数で使うか名前を区別しないと...
int secall_timer;
void draw(){}
void secall(){}
};
変数名のコーディングルールで解決する方法もありますが、変数名が長くなりがちですし、PLAYERクラス直下の変数自体は増えていくので、だんだんと見通しが悪くなっていきます。
#2.コンポーネント化
drawやsecallをコンポーネント化することでこれらの問題を解決していきます。
まず、それぞれのアップデート関数をクラスにします。
struct ACTION_DRAW {
private:
timer; // 衝突しない!
public:
void Update(){}
};
struct ACTION_SECALL {
private:
timer; // 衝突しない!
public:
void Update(){}
};
クラスにすることで関連するメンバ変数のスコープが小さくなり、名前の管理が容易になります。
そして、これらのクラスを継承して、それぞれのUpdate()をまとめて呼び出しします。
struct PLAYER : ACTION_DRAW,ACTION_SECALL {
void Update(){
ACTION_DRAW::Update();
ACTION_SECALL::Update();
}
};
ただ、このままではアクションを追加するたびにUpdate()の修正が必要なことに変わりはありません。
#3.パラメータパックの展開
パラメータパック展開を利用することで、追加の記述を自動化することができます。
template<typename... TYPE>struct ACTION : TYPE... {
public:
ACTION() : TYPE{}...{}
template<class TUPLE, std::size_t...INDEX>void updater( const TUPLE*, std::index_sequence<INDEX...>){
using EXPANSION = int[];
( void )EXPANSION{ ( std::tuple_element_t<INDEX,TUPLE>::Updater() , 0 )... };
};
public:
virtual void Updater( void ){
using TYPE_PACK = std::tuple<TYPE...>;
updater( ( TYPE_PACK* )nullptr, std::make_index_sequence<std::tuple_size_v<TYPE_PACK>>{});
}
};
使い方は上記のtemplateにアクションクラスをパラメータで渡して継承させます。
struct PLAYER : ACTION<ACTION_DRAW,ACTION_SECALL>{};
struct ENEMY : ACTION<ACTION_DRAW>{}; // ENEMY はSECALL不要
void main(){
PLAYER player;
ENEMY enemy;
player.Update(); // ACTION_DRAW::Update()とACTION_SECALL::Update()が呼ばれる
enemy.Update(); // ACTION_DRAW::Update()だけ呼ばれる
}
各アクションのUpdate()の呼び出しの記述はコンパイラがパラメータパックで行ってくれます。修正箇所が1ヶ所になるので、修正漏れによるUpdate()の呼び忘れがなくなります。
#4.使い方
ゲーム的な使い方としては下記のような感じで使うと便利かと思います。
struct ACTION_MOVE{ void Update(){} };
struct ACTION_JUMP{ virtual void Update(){} };
struct ACTION_ATTACK{ void Update(){} };
struct ACTION_MAGIC{ void Update(){} };
struct ACTION_GUARD{ void Update(){} };
struct ACTION_JUMP2 : ACTION_JUMP { void Update() override {} }; // 二段ジャンプ
// キャラの職業で使用可能なアクションをパラメータで渡す
struct WARRIOR : ACTION<ACTION_MOVE,ACTION_JUMP, ACTION_ATTACK,ACTION_GUARD>{};
struct WIZARD : ACTION<ACTION_MOVE,ACTION_JUMP2,ACTION_MAGIC>{};
持たせるアクションの種類はコンパイル時に静的に決まるので、アクションの付け替えのようなことはできませんが、アクション毎にメンバ変数を持てるので柔軟はそこそこあるかと思います。
#5.おわりに
各アクションのUpdate()を引数にオブジェクトのポインタを渡せるように改良すれば、オブジェクトや他のアクションのメンバを参照することも可能になります。