■ 新・ゲーム開発講座




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


■第9夜:テキストインタプリタの基礎

前回簡単に解説した文字コードの話をもとに、テキストファイルを読み込んでウェイトをかけながら表示をする実行ファイルを作ってみましょう。なお、今後はいちいちリストの中身を全部解説しているとあまりに大変なので、アルゴリズムの話をメインにして具体的な部分はソースを読んで下さい、というスタンスで行きたいと思います。まずはこのソースをダウンロードしてコンパイル → 実行してみてください。(スケルトンをちょっとだけ進化させています)

さて如何でしょうか。これはディスクから default.txt というテキストファイルを読み込んで、ウェイトをかけながらたらたらと表示するプログラムです。制御文字に対する処理を何も行っていないので、文字列を表示し終わってもメモリ内のゴミ(画面上では・になっている筈です)を延々と表示してしまいますが、まあそこはプロトタイプですからご勘弁願いたいところです(^^)。今回以降、このソースを発展させながらノベルゲームの実行ファイルを作っていくと致しましょう。





■各ソースの内容
内容解説の前に、各ソースの内容について簡単に説明しておきます。

g_tool.cpp 画面表示系関数がいろいろ
BasicTips.cpp 小物関数いろいろ
TextEngine.cpp テキストエンジン本体(という程のシロモノでは ^^;)
main.cpp メインループ周辺


テキストを1文字ずつ表示している部分を、ここではテキストインタプリタ、またはテキストエンジンと呼んでおこうと思います。今回は文字表示を行っているだけですが、ノベルゲームはこの部分を拡張して様々な機能を盛り込むことで完成します。この部分は、テキスト表示枠の範囲を変更することでADVの基本形にもなりますし、RPGの会話モジュールにもなります。事実、この講座で出てくる処理の多くは拙作 Demon's Eye (RPG)の会話モジュールを継承しています(コマンド名もそっくりですし ^^;)。

テキストインタプリタは、スクリプトファイル(ここでは普通のテキストファイル)を読み込んでその先頭から順次文字を表示していきます。制御コマンドにぶつかったときはその処理関数を呼んだりもしますが、ここではまだそこまでは作りこんでいません。なお、
本講座ではスケルトンから始まって順次機能を拡張しながらゲームを作っていきます。突然ソースの内容が全然変わってしまう…ということはありませんので(ファイル分割くらいは時々やりますけど)、安心して読み進んでください♪

小物関数は g_tool.cppBasicTips.cpp に雑多に放り込んでありますで、TextEngine.cpp で妙な関数が出てきたら適宜参照してください(^^)。では main.cpp から解説です。

■main.cppでやっていること

ソースを見れば、もうこれ以上ないくらい単純明解ですね。ゲームシステムの初期化を行う init_game() 関数を設け、その中でテキストエンジンの初期化関数 Init_Text_engine() を呼び、メインループではひたすらテキストエンジンの実行部 Text_engine() を呼びまくっています。この Mainloop() はプログラムが終了するまで無限に回り続けます。従ってテキストエンジン実行部 Text_engine() も延々と繰り返し呼ばれ続けている訳です。この繰り返しの中で、文字表示やコマンド解釈が順次進行していくという次第です。



void init_game()
{
//テキストエンジン初期化
Init_Text_engine();
}

void Mainloop(void)
{
//テキストエンジンを呼ぶ
Text_engine();

}

APIENTRY WinMain(HINSTANCE hIns,HINSTANCE hPI,LPSTR lpArg,int nCmdShow)
{

MSG msg; //Windowsシステムのメッセージ構造体(あんまり気にしなくていいです)
WNDCLASS wc; //Windowクラス(窓を開くときにだけ使用します)

hinst=hIns; //グローバル変数 hinst に、起動時に与えられるインスタンス値を保持しておく

//--------------------------------------------------------------
//窓を開くための所要パラメータをWindowクラス wc のメンバ変数に与える
//--------------------------------------------------------------
wc.hInstance=hIns; //インスタンス値
wc.lpszClassName="窓1号"; //クラス名(適当でかまわない)
wc.lpfnWndProc=(WNDPROC)WndProc; //イベントハンドラを登録
wc.style=0; //スタイルはとりあえず0でいいや
wc.hIcon=LoadIcon((HINSTANCE)NULL,IDI_APPLICATION); //タイトルバーに表示するアイコンはデフォルトでいいや
wc.hCursor=LoadCursor((HINSTANCE)NULL,IDC_ARROW); //マウスカーソルも標準形式でいいや
wc.lpszMenuName=0; //
wc.cbClsExtra=0; //
wc.cbWndExtra=0; //
wc.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
if(RegisterClass(&wc)==0)return 0;

hwnd=CreateWindowEx(
0, //拡張スタイルは特に考えない
wc.lpszClassName, //クラス名(↑で決めたものを与えておけばOK)
"たいとる", //Windowの左肩に表示される窓の名前
WS_OVERLAPPEDWINDOW, //ウィンドウスタイル(←ここでは縁取り指定のみ)
20,20,640,480, //窓の配置ポジション、大きさ(左肩のx,y及び幅、高さ)
(HWND)NULL, //親Windowのハンドル(ここでは無視)
(HMENU)NULL, //メニューも子ウィンドウも無いので気にしない
hIns, //インスタンス値
(LPVOID)NULL //他に参照すべきデータは無いので気にしない

);

if(!hwnd)return 0; //Windowを開くのに失敗したら終了する

ShowWindow(hwnd,nCmdShow); //外枠の描画
UpdateWindow(hwnd); //クライアント領域の描画

win_hdc=GetDC(hwnd); //今後の画面操作に備えて DC を取得しておく

init_game(); //ゲームシステム初期化

//窓が開いたら、ここで無限ループに入ってイベント監視を行う
while(1){
//メッセージキューになにか入って来たか?
if(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
//メッセージがあれば処理する
if(!GetMessage(&msg,(HWND)NULL,0,0))break; //終了メッセージならループ抜け
TranslateMessage(&msg); //キーメッセージを文字メッセージに変換
DispatchMessage(&msg); //ウィンドウプロシージャにメッセージを送付

}else{
//メッセージが無ければ Mainloop()を実行
Mainloop();

}

}

//↓終了時には wParam パラメータが持つ終了コードを返す規定になっている
return msg.wParam;


}




■TextEngine.cppでやっていること

@初期化
まずは main.cppInit_game() から起動直後に呼び出されていた初期化関数から見ていきましょう。。void Init_Text_engine() です。ここではまずバッファにテキストファイル default.txt を読み込んだ後、デフォルトパラメータを決める _Set_Default_params() を呼んでいます。その後、
タイマーをセットして戻っています。

デフォルトパラメータについては特に解説は必要ないと思います。単にフォントのサイズやテキスト展開エリア、文字送りピッチなどを決めているだけです。テキスト表示関係の変数はプログラムのあちこちから参照されることになるので、ここでは安直にグローバル変数で持つことにします(笑) フォントサイズや表示ウェイトは #define してシンボルで持てばいいじゃないか、という声も聞こえて来そうですが、将来スクリプト上からコマンド指示で変更できるよう拡張する予定がありますので変数のカタチで持っておきます。

ここで一番重要なのはグローバル変数の unsigned char *TEXT でしょう。このポインタがテキスト処理の要(かなめ)です。1文字表示する毎にインクリメントされてスクリプトファイルの中を移動していく水先案内人であり、このポインタに加減算することで前後のサーチやジャンプなどが出来るようになります。…まあ、それが出来るようになるのは今後の話なんですが(爆死 ^0^)

なおグローバル変数の中に RECT型の変数 TEXT_AREA というのが出てきますが、RECT型というのは矩形領域(要するに四角形)の上下左右(top, bottom, left, right) をメンバにもつ構造体です。画像転送などで良く使われますので覚えておきましょう。



#define SIZE_OF_TEXT_BUF 0xffff //64KBもあればバッファとしては充分でしょう
unsigned char TEXT_BUF[SIZE_OF_TEXT_BUF]; //スクリプトを読み込むエリア
unsigned char *TEXT; //Text pointer(テキスト解釈位置をポイントします)
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_TIMER; //タイマー

//-------------------------------------
// TEXTエンジン初期化
//-------------------------------------

void _Set_Default_params()
{

TEXT_WAIT = 100; //1文字表示のウェイト(ms)
Font_Size = 20; //Font size

TEXT_AREA.left = 20; //テキスト表示エリア(左)
TEXT_AREA.top = 20; //テキスト表示エリア(上)
TEXT_AREA.right = 640-20; //テキスト表示エリア(右)
TEXT_AREA.bottom= 480-20; //テキスト表示エリア(下)

TEXT_X=TEXT_AREA.left; //文字位置カウンタ初期化(表示エリア左肩から始まる)
TEXT_Y=TEXT_AREA.top;

TEXT_X_PITCH = Font_Size/2; //文字送りピッチ(X)
TEXT_Y_PITCH = Font_Size*130/100; //文字送りピッチ(Y)


}

void Init_Text_engine()
{

TEXT=TEXT_BUF; //テキストポインタTEXTをバッファ先頭にセット

//デフォルトのスクリプトファイルを読む
HLS_bload("default.txt",(char *)TEXT_BUF);

_Set_Default_params(); //デフォルトパラメータの設定

HLS_timer_start(&TEXT_TIMER); //タイマースタート


}



ところで、イニシャライスの最後にタイマーを撃っていますが、これが 「1文字ずつ表示」 を実現するカギになります。タイマーについては、ここでは BasicTips.cpp 内の HLS_timer_start( ) 、HLS_timer_check() という2つの小物関数を使うことにします。やっていることといえば GetTickCount() で現在の時間を取得して保存し、チェック時には再び現在時刻を取得してその差分から経過時間を調べているだけですが、この程度でも充分実用性があります。この仕掛けを利用して、1文字表示毎のウェイトを取ってやろうという算段な訳ですね。


int HLS_timer_start( DWORD *timer )
{

*timer = GetTickCount(); //現在時刻を記録

return 0;


}

int HLS_timer_check( DWORD timer , DWORD wait_time )
{

//前回記録した時刻から wait_time ミリ秒経過していたら true を返す
if( timer+wait_time <= GetTickCount() )return true;

return false; //時間が経っていなければ false を返す


}

※良く見たら、本来 bool 型にするべきところですね(爆 ^^;) int でもエラーにはなりませんが♪



Aループ

次に、Mainloop()からひたすら呼ばれる Text_engine() を見てみましょう。関数の最初で経過時間をチェックしています。初期化のときに定めた経過時間が過ぎるまで、関数の中身は実行されずに入り口段階で Mainloop() に戻される仕組みになってる訳です。ここが文字表示のウェイト部分に相当します。

指定時間が経過すると、とりあえずタイマーの関所は通過可能になります。関数の内部に入ってきたら、すかさず次のタイマーショットを放ちます。文字を表示して戻ってくるまでの間にも時間は過ぎてしまうので、なるべく近い位置でネクストショットを放っておこうという意図です(少しでも正確なウェイトをとるために ^^)。

さてその次が重要です。テキストポインタ TEXT の指す1バイトが 0x80 以上かどうかを調べています。2バイト文字か1バイト文字かの簡易判定ですね(ちょっと乱暴だけど ^^;)。それに応じて、2バイト文字用/1バイト文字用の表示を切り替えています。文字表示関数 ChrPut3D()g_tool.cpp にありますので参照してください(第5夜で扱った StrPut3D() の変形版です)



//-------------------------------------
// 文字位置のインクリメント
//-------------------------------------

void Increment_textp_pos(int inc)
//文字表示位置のインクリメント
//int inc:何byteインクリメントするか→1or2

{
//テキスト描画エリアからはみ出してしまう場合には改行操作をする
if( (TEXT_X+TEXT_X_PITCH*inc) < (TEXT_AREA.right-TEXT_X_PITCH*2) ){
TEXT_X += TEXT_X_PITCH*inc;
}else{

TEXT_X = TEXT_AREA.left;

if( (TEXT_Y+TEXT_Y_PITCH) < (TEXT_AREA.bottom-TEXT_Y_PITCH) ){
TEXT_Y += TEXT_Y_PITCH;

}else{

//1ページの表示制限を越えてしまう場合は、警告を表示して強制終了
//※とりあえず TEST version なので改ページ動作は未実装なのよーん
MessageBox(hwnd,"画面の終端に達しました","ERROR",MB_OK );

//強制終了
PostQuitMessage(0);

}


}

}

//-------------------------------------
// ここがエンジン部本体ね♪
//-------------------------------------
int Text_engine()
{

//------------------------------------------
// ウェイトを取りながらテキストを表示する
//------------------------------------------
int color1=RGB(100,200,200); //文字色
int color2=RGB( 10, 10, 10); //影色

//指定時間経過していない場合→Mainloopに戻る
if( HLS_timer_check(TEXT_TIMER,TEXT_WAIT)==false )return 0;

HLS_timer_start(&TEXT_TIMER); //タイマー再スタート

//■1文字を表示する

if( (unsigned char)(*TEXT)<(unsigned char)0x80 ){
ChrPut3D(win_hdc,TEXT_X ,TEXT_Y ,Font_Size,color1,color2,(char *)TEXT,1);

Increment_textp_pos(1); //文字表示位置のインクリメント=1byte

TEXT++; //テキストポインタを1バイト進める
}else{

//2バイト文字の場合は2バイトずつ出力
ChrPut3D(win_hdc,TEXT_X ,TEXT_Y ,Font_Size,color1,color2,(char *)TEXT,2);

Increment_textp_pos(2); //文字表示位置のインクリメント=2byte

TEXT+=2; //テキストポインタを2バイト進める


}

return 0;


}



なお ChrPut3D() で文字表示を行った後、次回のために Incriment_textp_pos()次の文字の表示座標を計算しておきます。計算と言っても、現在座標に文字ピッチを加えるだけの単純なものです ( 注:TEXT_X_PITCH は半角1文字分の数値なので、全角2バイト文字では2倍して加算しています)。加算をしたらテキスト表示エリアをはみ出していないかのチェックを行い、文字がエリア右端まで行っていたら改行操作 ( Yを1段下げてXを0にする) を行います。

・・・これを繰り返していくことで、英数字/漢字かな混じりの文章でも綺麗に表示していくことが可能になるのですね♪(^0^) ちょっと面倒かもしれませんが、一度心臓部が出来てしまえば後に追加する部分はどんどん定型化処理になって加速度的に楽になります。めげずに頑張りましょう。




【半角カナと文字化けについて】
今回のプログラムでは半角カナは無視して単純に80Hでコードを切る形でテキスト解釈をしています。そのため半角カナが混じった文を表示すると、文字化けを起こすことがあります。では半角カナは絶対に使えないのか、というと実はそうでもありません。2バイト文字の1バイト目の解釈のときにひっかかるだけです。ということは、半角カナを使う場合は偶数個の並びで使えば問題は生じないことになります(^0^) 適当なところにスペース1個入れても良し、とゆーことで逃げる道はいろいろあったりします♪