■ 新・ゲーム開発講座




■ へっぽこプログラミング入門♪


■第48夜:SAVEデータのパッケージ

ファイル入出力の基礎、画像ファイル名のキャッシュ処理ができたところで、今回はSAVEデータのパッケージ部分をインプリメントしましょう。ソースはこちらです(ようやく動くソースだ…^^;)。

■仕様

スクリプト上で明示的にSAVEポイントを表すコマンドを定めます。正規表現(笑)と短縮表記をそれぞれ以下のように定めます。パラメータはありません。

■書式: #save_point
#sp


#save_point コマンドは、コマンド実行時の以下の内容を、SAVEファイル構造体にコピーします。後日インプリメントするシステムメニューのSAVEが選択されたとき、この構造体の中身をDISKに書き出します。

■保存する内容

・現在実行しているスクリプトファイル名
・テキストポインタが指し示しているスクリプトファイルの位置
・現在メモリ内に読み込んでいる画像ファイル名
・現在演奏しているBGM
・コンフィグ情報(フォント、テキスト表示エリア、テキスト表示座標など)
・フラグ構造体 Mode_stat


■Mode_stat

#save_point は一発完了型のコマンドなので、フラグ構造体 Mode_stat に新規メンバを加える必要はありません。


■コマンド解釈部

もういい加減ルーチンワークですが、一応解説しておきましょう(^^;)
TexitEngine.cpp の コマンド解釈部 void Command_call() に、コマンド名と実行部の関数名を追記します。実行関数の名前は Com_save_point() と致しましょう。


void Command_call()
//「#」を見つけたところでこの関数を呼ぶと、「#」に
//続くコマンド名を解析してその処理関数を呼びます
{


中略

//コマンド名の評価 → 処理関数呼び出し
if( strcmp(com_name,"delay" )==0 ){ Com_delay(); flag=ON;} //遅延
if( strcmp(com_name,"wait" )==0 ){ Com_cursor_blink(); flag=ON;} //カーソルブリンク
if( strcmp(com_name,"w" )==0 ){ Com_cursor_blink(); flag=ON;} //カーソルブリンク
if( strcmp(com_name,"halt" )==0 ){ Com_halt(); flag=ON;} //終了
if( strcmp(com_name,"page" )==0 ){ Com_page(); flag=ON;} //改ページ
if( strcmp(com_name,"g_load" )==0 ){ Com_g_load(); flag=ON;} //BMPのLOAD
if( strcmp(com_name,"g_copy" )==0 ){ Com_g_copy(); flag=ON;} //BMPのCOPY
if( strcmp(com_name,"g_change")==0 ){ Com_g_change(); flag=ON;} //画面切替
if( strcmp(com_name,"flag_on" )==0 ){ Com_flag_on(); flag=ON;} //イベントフラグON
if( strcmp(com_name,"flag_off")==0 ){ Com_flag_off(); flag=ON;} //イベントフラグOFF
if( strcmp(com_name,"disp_flag")==0 ){ Com_disp_flag(); flag=ON;} //イベントフラグ表示
if( strcmp(com_name,"if_flag" )==0 ){ Com_if_flag(); flag=ON;} //フラグによる分岐
if( strcmp(com_name,"jump" )==0 ){ Com_jump(); flag=ON;} //ラベルジャンプ
if( strcmp(com_name,"file_change")==0 ){Com_file_change(); flag=ON;} //ファイル間ジャンプ
if( strcmp(com_name,"select3" )==0 ){ Com_select3(); flag=ON;} //3択
if( strcmp(com_name,"play_wav")==0 ){ Com_play_wav(); flag=ON;} //WAV演奏開始
if( strcmp(com_name,"stop_wav")==0 ){ Com_stop_wav(); flag=ON;} //WAV演奏停止 if( strcmp(com_name,"play_midi")==0 ){ Com_play_midi(); flag=ON;} //MIDI演奏開始
if( strcmp(com_name,"stop_midi")==0 ){ Com_stop_midi(); flag=ON;} //MIDI演奏停止
if( strcmp(com_name,"play_mp3")==0 ){ Com_play_mp3(); flag=ON;} //MP3演奏開始
if( strcmp(com_name,"stop_mp3")==0 ){ Com_stop_mp3(); flag=ON;} //MP3演奏停止
if( strcmp(com_name,"set_font_size")==0 ){ Com_set_font_size(); flag=ON;} //フォントサイズ変更
if( strcmp(com_name,"set_text_area")==0 ){ Com_set_text_area(); flag=ON;} //テキストエリア変更
if( strcmp(com_name,"set_text_wait")==0 ){ Com_set_text_wait(); flag=ON;} //テキストウェイト変更
if( strcmp(com_name,"set_text_color")==0 ){ Com_set_text_color(); flag=ON;} //テキストカラー変更
if( strcmp(com_name,"set_str")==0 ){ Com_set_str(); flag=ON;} //文字列変数に値をセット
if( strcmp(com_name,"text_input")==0 ){ Com_text_input(); flag=ON;} //ダイアログを開いてテキスト入力
if( strcmp(com_name,"save_point")==0 ){ Com_save_point(); flag=ON;} //セーブポイント設定
if( strcmp(com_name,"sp")==0 ) { Com_save_point(); flag=ON;} //セーブポイント設定(短縮表記)

// ↑
//そのうちここに他のコマンドも追加していきましょう
// ↓

//フラグを見て、切り出されたコマンド名が有効なコマンドだったかどうか確認する
if( flag == OFF ) {

char str[256];

sprintf( str,"警告:無効なコマンド [#%s] が記述されています",com_name);
MessageBox(NULL,str,"Command_call()",MB_OK);

}

}


■コマンド実行部

Text_Com_04.cpp に 実行部を追加します。make_save_info() というのが処理の本体ですが、後日解説するSAVEポイントの自動生成で流用したいので処理本体を分割記述しました。ちょっとわかりにくくてすみません(汗)


int Com_save_point()
{

TEXT++; //コマンド名終端の1バイト先にテキストポインタを進めておく

make_save_info();

return 0;

}


で、SAVEポイント生成(=データのパッケージ)の本体部分は SystemMenu_01.cpp に加えることにしました。記述が分散してしまって申し訳ないのですが、くににん的脳内ではスクリプトコマンドは Text_Comなんたら.cpp、システムメニュー絡みの処理は SystemMenu_01.cpp に記述するというローカルルールがあるのでこんなことになっています。わかりにくかったらソースの切り貼り等みなさんで手を加えちゃってください (汗 ^^;)

ではいよいよデータのパッケージ部分です。SystemMenu_01.h にSAVE構造体を以下のように宣言します。ファイル名1個あたり512byteとゆーのはちょっと大袈裟かもしれませんが、理論上はファイル名は256byteまで許されていますし、スクリプターがサブフォルダを掘ってスクリプトを格納する可能性もあるので多少の余裕をみておくことにします。



//SAVEファイル
typedef struct _Save_stat {

//スクリプトの実行位置
char script_fname[512]; //現在読み込んでいるスクリプトファイルの名称
int TEXT_offset; //テキストポインタのオフセット

//画面情報
char BK_bmp_fname[512]; //裏画面に読み込んであるBMPファイル名
char BG_bmp_fname[512]; //背景画面に読み込んであるBMPファイル名
char PT_bmp_fname[512]; //パーツ画面に読み込んであるBMPファイル名

//BGM関係
char MIDI_fname[512]; //現在演奏中のMIDIファイル名
char MP3_fname[512]; //現在演奏中のMP3ファイル名
char WAV_fname[512]; //現在演奏中のWAVファイル名
int WAV_loop_flag; //SND_FILENAME | SND_ASYNC | SND_LOOP or SND_FILENAME | SND_ASYNC;
// ※MIDIとMP3はループ演奏のみ → ループフラグは無い
//CONFIG情報
int Font_Size; //フォントサイズ
int TEXT_WAIT; //1文字表示のウェイト
RECT TEXT_AREA; //テキストを展開する画面領域
int TEXT_X; //現在の表示位置X
int TEXT_Y; //現在の表示位置Y
int TEXT_X_PITCH; //文字送りピッチX
int TEXT_Y_PITCH; //文字送りピッチY
DWORD TEXT_COLOR1; //文字色(表)
DWORD TEXT_COLOR2; //文字色(影)

//SavePoint Mode
int save_point_mode;

int dummy[512]; //将来の拡張用(あんまり意味ないかも?^^;)

//フラグ類
_Mode_stat Mode_stat_buf; //Mode_statをそっくり取り込むための器

} _Save_stat;
extern _Save_stat Save_stat;


ところでSAVEデータパッケージを作るにあたって、プログラマが絶対に守るべき掟がひとつあります。

ポインタは保存しない


↑これは絶対に守らなければなりません。ポインタはメモリの特定のアドレスを指し示していますが、一度SAVEしたデータを再ロードしたとき、以前のメモリ内アドレスが完全に再現する保証はありません (…といいますか、まずそんなことは無い)。ゲーム実行中にポインタで管理していたデータは、基準アドレスからのオフセットの形で保存しなければ危険です。

本稿のソースの中で、この制限に引っ掛かるのはテキストインタプリタの実行位置を示す *TEXT です。ところでテキストの実行位置は、スクリプトを格納するバッファ TEXT_BUF からの距離でも表すことができるため、int offset = TEXT - TEXT_BUF のようにポインタ同士の引き算でオフセット値に置き換えることができます。この基準アドレスからのオフセット量を保存するようにすれば、不都合はありません。いざデータを再LOADしたとき、メモリ内に確保されたバッファ TEXT_BUF の先頭アドレスはSAVE時と異なるかもしれませんが、TEXT = TEXT_BUF + offset とすれば *TEXT はバッファに読み込まれたスクリプトファイルの正しい位置を指し示します。

では、そのデータをパッケージする部分を見てみましょう (SystemMenu_01.cpp)。基本的にパラメータの単純コピーで、ポインタの部分だけ Save_stat.TEXT_offset = TEXT - TEXT_BUF とポインタ同士を引き算してメモリ上の距離=オフセット値を求めています。



_Save_stat Save_stat; //SAVEファイル構造体

void make_save_info()
// SAVEするための情報をパッケージする。
// 注意:SAVE時ではなく、スクリプトがSAVEポイントを通過したときに
// この関数を呼んで必要情報をSAVE構造体にパックしておく。
{

//スクリプトの実行位置
Save_stat.TEXT_offset = TEXT - TEXT_BUF;
strcpy( Save_stat.script_fname, Text_fname );

//画面に読み込んであるファイル名
strcpy(Save_stat.BK_bmp_fname,BK_bmp_fname);
strcpy(Save_stat.BG_bmp_fname,BG_bmp_fname);
strcpy(Save_stat.PT_bmp_fname,PT_bmp_fname);

//BGM系
if( MIDI_stat.type == _MIDI && MIDI_stat.MIDI_play_stat==ON ){
strcpy(Save_stat.MIDI_fname,MIDI_stat.MIDI_fname); //現在演奏中のMIDIファイル名

}else{
strcpy(Save_stat.MIDI_fname,"");
}

if( MIDI_stat.type == _MP3 && MIDI_stat.MIDI_play_stat==ON ){
strcpy(Save_stat.MP3_fname ,MIDI_stat.MIDI_fname); //現在演奏中のMP3ファイル名
}else{
strcpy(Save_stat.MP3_fname ,"");
}

if( WAV_stat.WAV_play_stat==ON ){
strcpy(Save_stat.WAV_fname,WAV_stat.WAV_fname); //現在演奏中のWAVファイル名
}else{
strcpy(Save_stat.WAV_fname,"");
}

Save_stat.WAV_loop_flag = WAV_stat.WAV_play_flag; //MIDIがループ演奏=ON それ以外=OFF

//コンフィグ項目
Save_stat.Font_Size = Font_Size;
Save_stat.TEXT_AREA = TEXT_AREA;
Save_stat.TEXT_COLOR1 = TEXT_COLOR1;
Save_stat.TEXT_COLOR2 = TEXT_COLOR2;
Save_stat.TEXT_WAIT = TEXT_WAIT;
Save_stat.TEXT_X = TEXT_X;
Save_stat.TEXT_X_PITCH = TEXT_X_PITCH;
Save_stat.TEXT_Y = TEXT_Y;
Save_stat.TEXT_Y_PITCH = TEXT_Y_PITCH;

//Mode_stat(フラグ構造体)
memcpy( &Save_stat.Mode_stat_buf,&Mode_stat,sizeof(Mode_stat) );

//--------------------------------------------------
// 動作確認用:強制的にDISKに書き出し
//--------------------------------------------------

HLS_bsave( ".\\save\\test.svd",&Save_stat,sizeof(Save_stat));

}



■動作を確認しよう

さて、今回は動作確認のために関数の最後にDISKへの強制書き出しを記述しています。
前々回に作成したバイナリSAVE関数 HLS_bsave() を呼んでいる訳ですが、このように
SAVEデータをパッケージするのと同時にDISKに書き出すと、いわゆる
自動SAVE
になります。適当なカウンタを一個用意して、ファイル名を 001、002、003…としていけば
無限に途中経過を保存できますが…やりすぎるとウザイだろうなぁ(爆)

さて冗談はともかく、さっそく実行して動作を確認してみましょう。今回から、動作確認のため必要に応じてMIDIを鳴らしています。MIDIファイルは鷹月ぐみな情報局のフリーサウンドライブラリ(曲数が尋常ではないです ^^;)から、みくしぎまさき氏の曲を使わせて頂きました。素敵な曲をありがとうございます。







表面上は何も起こったようにみえませんが、フォルダ SAVE の中に test.svd という記念すべきSAVEデータ第一号ができています。さっそく、バイナリエディタで中身を覗いてみましょう。バイナリエディタはフリーの優れたものがネット上でも多く出回っています。くににんは DDS2氏作成の Stirling を愛用していますのでそれを使って↓確認を行いました(^^)

まずデータの最初はスクリプトファイルの名称ですが、これは問題ないようです。512byteの余白がちょっともったいないような気がしますが、まあそこはそれ(^^;)






そして512byte = 200h byte目がテキストポインタ *TEXT をオフセット化した値になります。オフセットは 32bit int で格納したので 4 byte ぶんのサイズがあります。
エディタでみるとアドレス200h からの 4 byte
F1 01 00 00 となっていますが、インテル系CPUはリトルエンディアン(メモリの下位から値が格納される形式)なので、実際の値は 00 00 01 F1 となります。この値を覚えておいて、次にスクリプトファイル default.txt をメモ帳で開きましょう




ここではSAVEポイントは短縮表記で #sp と表記してあります。この #sp の直後の位置がきちんとファイルに保存されていれば成功なのですが…どうでしょう?

それをたしかめるには、このスクリプトファイルをメモ帳ではなくバイナリエディタで開くと検証が容易です。さっそく開いてみると…





ファイル先頭から 00 00 01 F1 バイト目は、#sp の直後の CR(0D)-LF(0A) つまり改行コードのところを指しています。これで、ポインタの引き算で得られたオフセット値が正しくスクリプトファイル上のSAVE位置を表していることが確認できました。とりあえず成功のようです(^0^)