セーブ・ロードの自動化 その4(派生クラスの保存)
「セーブロードの自動化その1〜3」までで、オブジェクトの自動セーブとロードの機構を考えてきました。この節では、さらなる続きとして派生クラスのセーブロードを確立します。
@ 拡張分だけ追加保存
次のクラスをご覧下さい。
class CPlayer : public CSaveObjBase
{
protected:
int m_iLevel;
int m_bFlag;
static DATARECORD m_gDataRecord[];
public:
virtual DATARECORD GetDataRecord();
};
class CPlayerEx : pblic CPlayer
{
protected:
int m_iHitPoint;
static DATARECORD m_gDataRecord[];
public:
virtual DATARECORD GetDataRecord();
};
CPlayerクラスはCSaveObjBaseクラスから派生されており、次のようにDATARECORD構造体の配列を定義することで自動セーブロードが可能になっています。
DATARECORD CPlayer::m_gDataRecord[] =
{
DATA_LOCAL( CPlayer, m_iLevel );
DATA_LOCAL( CPlayer, mbFlag );
DATA_END
};
CPlayerExクラスはCPlayerクラスから派生されていて、同様に自動ロードセーブが可能なのですが、現段階では次のように親クラスのメンバ変数も再定義しなければいけません。
DATARECORD CPlayerEx::m_gDataRecord[] =
{
DATA_LOCAL( CPlayerEx, m_iLevel );
DATA_LOCAL( CPlayerEx, mbFlag );
DATA_LOCAL( CPlayerEx, m_iHitPoint ),
DATA_END
};
これはミスも犯しやすいですし、何よりもえらい面倒です。プログラムとしては、重複宣言はバグを生むだけなので避けるべきことです。そこで、次のように自分の親クラスのデータテーブル名前だけを告げるようにしたらどうでしょう。
DATARECORD CPlayerEx::m_gDataRecord[] =
{
DATA_LOCAL( CPlayerEx, m_iHitPoint ),
DATA_BASE( CPlayer::m_gDataTable )
DATA_END
};
・・・何だか誘導尋問感たっぷりですいません(^-^;。実はこうすることにより、セーブ時に親クラスのメンバ変数も保存でき、またロード時に必要な親クラスのテーブル情報も得る事ができるのです。
DATA_BASEマクロは次のようになります。
#define DATA_BASE( TABLEPTR ) \
{\
TYPE_BASE, \
( (__int64)TABLEPTR ), \
0 \
}
TYPE_BASEという新しいデータタイプ、そして親クラスのデータテーブルを整数値に変換しています。
A セーブ機構
親クラスのセーブに対処するために、実際にセーブを行ってくれるCSaveManager::Write関数の中身は全体的に変更する必要が出てきました。ちょっと長いのですが、次のようになります。
// データ書き込み
int CSaveManager::Write( DATARECORD* list, CSaveObjBase *pobj )
{
while(list->type != TYPE_END)
{
// データタイプとサイズの書き込み
fs.write( (char*)&list->type, sizeof(char) ); // データタイプ
switch(list->type)
{
case TYPE_LOCAL: // 通常変数
char *pos = (char*)pobj + list->offset; // データの位置を算出
fs.write( (char*)&list->size, sizeof(int) ); // データサイズ
fs.write( pos, list->size ); // データ本体
break;
case TYPE_PTR: // ポインタ変数
char *pos = (char*)pobj + list->offset; // データの位置を算出
fs.write( (char*)&list->size, sizeof(int) ); // データサイズ
void* p = (void*)*(__int64*)pos;
int Elem = ElemFromPtr( p ); // ポインタを要素番号に変換
fs.write( (char*)&Elem, list->size ); // データ本体
break;
case TYPE_MEM: // メモリブロック
char *pos = (char*)pobj + list->offset; // データの位置を算出
char *sizepos = (char*)pobj + list->size;
int size = *(int*)(sizepos);
fs.write( (char*)&size, sizeof(int) ); // データサイズ
char *pmem = (char*)(*(int*)(pos));
fs.write( pmem, size); // サイズはポインタ先に存在
break;
case TYPE_BASE: // 親クラス保存
Write( (DATARECORD*)(list->offset), pobj );
break;
}
// 次のリストへ
list++;
}
return SAVE_OK;
}
すいません、後でちゃんとリファクタリングしておきます(>_<)。TYPE_BASEが来た場合、データタイプのみを保存して親クラスのメンバ変数の書き込みに行くために、他の各case文に似たような部分がたくだん出てきてしまいました。
DATARECORD::typeがTYPE_BASEの場合、list->offsetに親クラスのデータテーブルへのポインタが格納されているはずなので、それをキャストをしてWrite関数に渡してしまいます。Write関数を再帰関数として扱うわけです。セーブは、実はこれで終わってしまいます。TYPE_BASEが来る度に再帰的にセーブを行ってくれるので、根っこの親まで保存してくれます。
B ロード機構
ロード時に使うCSaveManager::Read関数もちょっと面倒なことになっています。リファクタリングすれば大分綺麗になるはずです。今は太文字部分だけを気にして下さい(^-^;
// データ読み込み
int CSaveManager::Read( DATARECORD* list, CSaveObjBase *pobj )
{
while(list->type != TYPE_END)
{
// データタイプを取得
int type = 0, size = 0;
fs.read( (char*)&type, sizeof(char) );
switch(type)
{
case TYPE_LOCAL: // 通常変数
char *pos = (char*)pobj + list->offset; // オブジェクト内メンバ変数の保存位置を算出
fs.read( (char*)&size, sizeof(int) ); // データサイズ取得
fs.read( pos, list->size ); // データ本体
break;
case TYPE_PTR: // ポインタ変数
// OBjIDを一端格納 → 後でポインタ再結合
char *pos = (char*)pobj + list->offset; // オブジェクト内メンバ変数の保存位置を算出
fs.read( (char*)&size, sizeof(int) ); // データサイズ取得
fs.read( pos, list->size ); // データ本体
break;
case TYPE_MEM: // メモリブロック
char *pos = (char*)pobj + list->offset; // オブジェクト内メンバ変数の保存位置を算出
fs.read( (char*)&size, sizeof(int) ); // データサイズ取得
// サイズ分のメモリを確保
// posの先は任意のポインタ変数なのでtmpポインタと置き
// tmpが指す先にデータを流し込む
// pos->tmp->[Data]
char *tmp = new char[size];
fs.read( tmp, size );
*(int*)(pos) = (int)tmp;
break;
case TYPE_BASE: // 親クラスのメンバ変数
Read( (DATARECORD*)(list->offset), pobj );
break;
}
// 次のリストへ
list++;
}
return LOAD_OK;
}
太文字部分以外は今は気にしなくて結構です。TYPE_BASEが読み込まれた場合、list->offsetには親クラスのDATARECORD配列へのポインタ変数の位置(ポインタ)が示されています。これによって親クラスのデータテーブルを手繰り寄せることができますので、再帰的にRead関数を呼び出せば、全ての親クラスへの変数を取得してくれます。
今回はCSaveManager::Read関数、Write関数にちょっと変更がありましたが、注目の親クラスの変数の初期かも自動化する部分はちょっとした追加で実装できてしまいました。この基本も「Game
Programming Gems 3」を参照しています。この仕組みを提供してくれたMartin
Brownlow氏に感謝です。
C さらなる拡張と調整とリファクタリングと
さて、これで大体のセーブロードは全部自動化でできるようになりました。ちょっとしたゲームであれば、これだけでも十分です。しかし、大規模なゲームとなると、まだ結構不便があるんです。例えばテンプレートクラスである「スマートポインタ」の扱い。少し考えると、セーブロードに対してこれがえらい厄介者である事が分かるはずです。Comポインタなどはもっと悲劇的で、インターフェイスしか与えられないので、セーブする事自体不可能です。悲しいのは、これらの対策をどうにかしなければならないのです。
「Game Programming Gems 3」には、セーブやロード前後にしておきたい事はオーバーライドされたCSaveObjBase::Load、Save、ConnectPtr関数に書くとあるのですが、どうもあまり得策ではないように感じます(私の読み込みが足りないのかもしれませんが)。オーバーロードされた関数からは親クラスのLoad関数やSave関数が呼ばれないため、親クラスで定義したやるべき事が子クラスで無視されてしまいます。子クラスで親クラスのすべきことをもう一度書くと、いわゆるコピペプログラムになってしまい絶対によくありません。ならば、子クラスのLoad、Save関数で親クラスのそれを呼べば良さそうに思えますが、それだとファイルの2重読み込みが発生します。これらの点から得策に感じないわけです。
これを解決するには、CSaveObjBaseクラスに、セーブ前にすべきことを専用に書くPreSave関数、ポインタの結合後にすべきことを専用に書くReconstructor関数を設けて、CSaveManagerクラスがそれを正しく呼ぶように仕込みます。
そして、最後にはちゃんとリファクタリングをしなければ、このクラスはえらいことになります。考えなければならない事は、まだまだありますねぇ・・・