並ゲームプログラマの書き捨て場

ゲームプログラマやってる私の日記、雑記、備忘録、公開場所

目的に合った静的領域を確保しておく方法 for C++

2014/09/14の記事 - この間の記事はアルコールに醸造されました。


安定のお久しぶりです、djannです。
大きく間が空いてしまったが、たいていいつものことなので、特に気にせず進めていく。


今回の記事だが、タイトルの通り「目的に合った静的領域を確保しておく方法」を書いてみる。

静的領域とは何かというと、operator newやstd::malloc等を用いず、あらかじめコンパイル時に確保されている領域のことだ。
どのように確保するかというと、つまりはグローバル変数を作ればいい。そうすればプログラム中で扱える領域が静的に確保される。


なお、こちらの記事の内容は通常のPC向けにアプリケーションを作る場合は特に必要ない。いつでも自由に使いたい時にoperator newを用い、動的にメモリを確保し解放すればいい。そちらの方がはるかに生産性は良いはずだ。

ではどういう時に考えるかというと、例えばゲームなどを作る場合だ。
ゲームは1秒間に60回または30回辺りのループで成り立っている。ループ1回にゲーム中の処理を行い、画面への表示を作成することの繰り返しが行われているわけだ。

つまり、非常に短い時間に様々な処理を終えなくてはならないため、メモリを動的に確保するという(それなりに)重い処理は相性が悪いということになる。
なので、ゲームプログラミングではメモリの動的確保は出来るだけ行わないようにする。という通説がある。

※まあ流石に、外部リソースを用いるのに一切の動的確保を行わないというのは、逆にハードウェアリソースの無駄が大きいため、ようは適材適所ということになるが。


ちなみに私は昔、どれだけクリティカルになるかを知る為に1つの3Dモデルを読み込むつたないコードを書いた。
std::vectorを用い、手に入るにも関わらず全体のサイズを求めず、1要素を読み込む度にpush_back()で追加するといった、はぢめてのC++プログラミングのようなコードだ。

データ自体がASCII文字そのままでstd::ifstreamで読み込みつつ解析を行っていたというのもあるのだろうが、1つのモデルの読み込みが完了するまでに、市販ゲームのロード時間を包み込める程度の時間がかかり、1人で笑っていた。


さて、では本題に入ろう。
主な用途としてはモデルやテクスチャ等の外部リソースではなく、例えばゲーム中の配置物、キャラクタ等を表現するクラス用の領域確保だ。

さて、静的領域を確保しておくわけだが、あまり適当に取っておくわけにもいかない。自分が作ったゲームをプレイしようとしてくれる人が、常にメモリを8GB載せているとは限らない。
なので大抵は大方の稼働スペックを想定し、そのスペック上では(上記の例でいえば)キャラクタを何体出せるか、フィールド上に何個のオブジェクトを置けるかを計算し、それに見合う数だけを確保する。

一番シンプルな形は以下のようになるだろう。

char buf[ 1024 ];

きっと単精度浮動小数点数で座標x, yを持ったキャラクタが128体出るのだろう。
これでも問題ないのだが、いかんせんよく分からない。数が足りなくなった場合を、どうやって見付ければいいのだろうか?

ではどうするか?
次の段階はsizeof演算子を使うことだ。例えばclass Enemyがあると思って見てほしい。
※sizeof演算子は、その型がどれだけのサイズになるかを戻してくれる。と書くと、どの層をターゲットにしているのかよくわからなくなってきた。ので、あまりに一般的なものは、次からは説明を省く事にする。

char buf[ sizeof( Enemy ) * 128 ];

随分わかりやすくなった。Enemyが最大128個作成される、その為のバッファなのだろうと分かる。
が、もし単純なEnemyではなくなったらどうしようか。

敵の中でも爆発するBlastEnemy、転がってくるRollingEnemyが追加された。必要な情報量も増えている。
それぞれが何体ずつ出てくるかは自由に、ただし敵自体の最大登場数は128体だ。

一番サイズの大きいクラス * 128体分のバッファを取りたい。さてこんな時はどうしよう。
脳内コンパイラでサイズを計算したり、mainでsizeof演算子を使うコードだけ書いてそれぞれの大きさを調べるのも良いが、種類が増えていく度に毎回行うのは手間だ。では、条件演算子を用いよう。

char buf[ (sizeof(BlastEnemy) < sizeof(RollingEnemy) ? sizeof(RollingEnemy): sizeof(BlastEnemy)) * 128 ];



sizeof演算子と条件演算子の合わせ技は、コンパイル時に計算を行ってくれる。結果、bufはBlastEnemyとRollingEnemyのうち、よりサイズの大きい方 * 128体分のサイズとなる。

解決かと思ったが、いかんせん見辛いという問題がある。
例えば更に派生型が増えたらどうするか?条件演算子は入れ子に出来るが、入れ子にした際の可読性の悪さはお墨付きだ(個人的に)。

出来ればリスト的にただ型一覧を並べて、その中で最大公約数的サイズを求めたい。
型の扱いといえば、C++にはとても強力な機能がある。そうtemplateだ。複数の型を受け取り、その中で一番サイズの大きな型をtypeとして戻すメタ関数(コンパイル時に働く関数)を作成しよう。

一気に作るよりまず、if文のようにtrue or falseで判別する二択のメタ関数を用意する。型に対するifということでtype_ifとでも名付けよう。


template <bool comd, class vType1, class vType2>
struct type_if {
    typedef vType1 type;
};
template <class vType1, class vType2>
struct type_if<false, vType1, vType2> {
    typedef vType2 type;
};

以上がその実装となる。
bool, なんらかの型その1, なんらかの型その2を受け取るテンプレート構造体だ。
下にほとんど同じのが書いてあるのは、その特殊バージョンとなる。テンプレート第一引数がfalseの時はこうなるという実装を書いている。
type_if< true, int, float >の時は下側とは一致しない。なぜなら第一引数はfalseではないから。
つまりtypename type_if< true, int, float >::typeはintのtypedefとなる。

type_if< false, int, float >の時は下側と一致する。なぜなら第一引数はfalseだから。
つまりtypename type_if< false, int, float >::typeはfloatのtypedefとなる。


さて、次はtype_ifメタ関数を用いて、目的の複数型を渡したらその中で一番サイズの大きな型がtypeとして戻ってくるメタ関数を書こう。

template <class vType, class ...Types>
struct is_biggest_type {
private: typedef typename is_biggest_type<Types...>::type inner_type;
public:
    typedef typename type_if< sizeof(T) < sizeof(inner_type), inner_type, vType >::type type;
};
template <class vType>
struct is_biggest_type<vType> {
    typedef vType type;
};

class ...TypesはC++11以降の記法で、printfでおなじみの可変長引数をテンプレート引数に適用したものだ。
inner_typeのtypedefのところで、再帰的にis_biggest_typeをインスタンス化(実体化)している。

今回の下のis_biggest_type<T>は、テンプレート引数が複数あるis_biggest_typeとは一致しない場合のオーバーロードを表している。

順を追って見ていくと、複数引数を渡されたis_biggest_typeはinner_typeのtypedefを行う際に、先頭の引数を一つ減らした状態で再帰的にis_biggest_typeをインスタンス化する。
繰り返していくうちに、テンプレート引数が1つだけになった時に、下のis_biggest_typeがインスタンス化され、そこで渡されている型がtypeとしてtypedefされる。
そこから呼び出し元の最初の引数とtype_ifで比較し続けながら再帰を巻き戻っていく。
そして最後に残っていた型が、最初の使用場所のis_biggest_typeのtypeとして咲き誇るわけだ。


これで、リスト中で最大サイズの型を取得することが可能となった。ではこれを用いて、継承関係にあるクラス群の128個分のバッファを確保してみよう。

is_biggest_type< Enemy, BlastEnemy, RollingEnemy, ParalyseEnemy, DrownEnemy >::type buf[ 128 ];


一覧表記で、とても見やすくなった。
これ以降は、例えば各種バッファを必要とされたクラスに配置newをして戻したり等、自分が欲しい機能を持ったクラスのメンバ変数として用意してもいいし、好きなように自身の作るゲームに合うように作ればいい。




P.S
もし静的領域にそのままバッファを確保することで、実行ファイルのサイズが大きくなる事が気になるようであれば、そのバッファはプログラム開始時に動的確保されるようにすればいい。例えば以下のようにだ。

foo *make_static_foo(){
    return new foo[ 128 ];
}
foo *foo_ = make_static_foo();

Cでは違法だが、C++ではこれは合法となる。初期化の順番が不定なのが問題であり、気を付ける必要があるが。