■ 新・ゲーム開発講座




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


■第41夜:メニュー窓を開く

さて、永らく休止したり間延びしたり…でちまちまと進行してきた 「へっぽこプログラミング入門」 もいよいよ終盤にさしかかって参りました。基本的な機能はひととおりインプリメントできているので、このままでも短いノベル/ADVなら問題なく作れる筈です。
…が、ちょっと込み入った作品を作ろうとすると、途中でプレイを中断したくなる場合もありそうですよね♪ そのあたりを考慮すると、やはりシステムメニュー(SAVE、LOAD、終了など)があったほうが便利そうです。そこで今回から数回に分けて、このシステムメニューを作っていきたいと思います。とりあえず初回は単純に窓を開いて閉じるだけです。ソースは
こちらです。

■グラフィックな窓もどき

前回作成した 「名前入力窓」 はWindowsのシステムを利用してダイアログを開きました。しかし今回は敢えてグラフィックベースで自力で窓もどきを書くことに致しましょう。理由は単純で、デザインが自由に出来て見栄えがよいからです。もちろん、一個の独立したウィンドウとしての機能を持っていたダイアログと違って、あくまでも「もどき」なのであまり複雑なものは目指しません。内容的には、
第32夜で扱った選択処理の延長線上(窓枠の描画が加わった)という程度です。第32夜と違いがあるとすれば、メニュー起動がマウスの右クリックになることくらいでしょうか。



■書式

今回はマウスの右クリック起動なので、スクリプトの書式指定はありません。

■仕様

書式がなくても、仕様は決めておかなければなりません。まず起動条件ですが、いくら右クリックで起動と言っても時と場合を選ぶことが必要です。テキストの遅延表示中や選択肢を選んでいる最中、あるいは画面切り替えエフェクトの途中でシステムメニューが開いてしまったのでは具合が悪いでしょうし、いらぬバグの元になりそうです。窓を開くには、なによりも安定した状況が欲しいところです。

ではシステム的に最も安定しているタイミングはなにか…という話になりますが、平行して走っているタスクがほとんど無く、テキスト表示も停止している状態といえば、現実的にはカーソルブリンク中が最も安定した状態であるといえるでしょう。そこで、まず

1)メニュー起動はカーソルブリンク中のマウス右クリックとする
2)もういちど右クリックするとメニューウィンドウは閉じる

という単純な決まりごとのみ決めて、ウィンドウ画面を表示してみましょう。またメニュー項目としては

1)SAVE
2)LOAD
3)FPS表示(ON/OFF)
4)EXIT(→プログラムの終了)

の4つがあれば必要充分としましょう。それぞれの処理は、後日別の章にて順次実装していきます。なお、このように同じボタン(キー)を押すごとにONとOFFが交互に切り替わる仕組みを「トグルスイッチ」などと呼びます。

■窓構造体

ウィンドウのBMPは以下のようなものを用意します。ウィンドウ3つぶんを横に並べた構成で、左から順にポジ、ネガ、バッファになります。ポジはウィンドウのデフォルトの状態で、マウスがメニュー項目に重なったときだけネガ部分の赤文字の部分を表示します。バッファ(緑の部分)はウィンドウによって隠れてしまう背景部分の画像を一時的に退避しておくスペースです。



システムメニューの記述は、SystemMenu_01.cpp というファイルに記述することにします。だんだんソースも分量が増えてきたので読むのも大変とは思いますが、そのへんはご愛嬌を(^^;)窓そのものの情報は以下のような構造体で扱うことにしましょう。

//--------------------------------------------------
//窓構造体
//--------------------------------------------------
typedef struct {
HDC
HBITMAP
int
int
int
int
int
int
DWORD
hDC; //BMPのDC
hBitmap; //BMP本体のハンドル
width; //窓の幅
hight; //窓の高さ
wx; //窓の左肩の座標 X
wy; //窓の左肩の座標 Y
esx; //BITMAP上の画面退避位置 X
esy; //BITMAP上の画面退避位置 Y
timer; //タイマ
}_Menu_Win;

↑の構造体を見て 最後のタイマーって何だ?と疑問に思う方もいるかと思いますが、トグルスイッチを実現するにはタイマーは不可欠なのでここに入れています。トグルスイッチは押すたびにONOFFが交互に切り替わるのですが、タイマーを入れない状態で不用意にメニューをクリックすると、北斗神拳もどきの連打として入力されてしまう場合があるからです。仮に50fpsでタスクが回っていると、1回のループは 20msec で完了してしまいますが、人間の指はそんなに早く反応できません。経験的に、再入防止を図るのに適当な入力スキャン間隔は 200〜300msec くらいだろうと思います。100msec を切ると、よほど神経の良い人でないとうまくクリックすることが困難になります。

■フラグ Mode_stat

システムメニューはフラグを多用したタスク処理になりますので、制御フラグ構造体 Mode_stat にメンバを追加しておきます(main.h 参照)。今回は窓が順次重なっていくスタック構造はとらず、単純な1窓毎の切り替え式とする予定なので、この1個のフラグを各窓で使いまわします。使いまわすためにはフラグの値はON/OFFの2値では間に合いませんので、窓毎のフラグの値(ID)を定義しておきます。↓のリストでは101,102…と振っていますが、値は他と区別がつけば何でも結構です。このフラグを手がかりに、main.cppMainloop() 内で各種処理が振り分けられます(詳細は後述)。



struct _Mode_stat {
int
int
int
int
int
int
int
char _Str256
int
flag_text; //テキスト表示:ON=進行 OFF=停止
flag_delay; //遅延処理:ON=遅延あり OFF=遅延なし
flag_cursor_blink; //カーソル点滅:ON=点滅 OFF=点滅なし
flag_halt; //終了:ON=終了 OFF=終了ではない
flag_page2; //改ページ type2:ON=改ページ処理中 OFF=処理中でない
flag_g_change; //画面切替効果:ON=切替中 OFF=処理はない
flag_select3; //3択:ON=選択中 OFF=処理はない
event_flag[EVENT_FLAG_MAX]; //イベントフラグ
str[STR_STR_MAX]; //汎用文字列変数
flag_system_menu; //システムメニュー状態:OFF=処理中ではない/その他=各種メニューID
};
extern _Mode_stat Mode_stat;

//システムメニューのID
#define SYSMENU_MAIN 101
#define SYSMENU_LOAD 102
#define SYSMENU_SAVE 103
#define SYSMENU_FPS 104
#define SYSMENU_EXIT 105


ところで、以前にも書きましたが、グローバル変数は初期値が0になります。今回作成しているプログラムでは

#define ON 1
#define OFF 0

とシンボル定義(BasicTips.h)していますので、特に明示的な初期化をしない場合は構造体のメンバもデフォルトでみな OFF になります。手抜きっぽいですが初期化の手間が省けて結構有効なワザではないかと思います(^^;)

■メニューのトリガー部分

メニューの起動条件は @マウスの右ボタンが押されて、かつ Aカーソルブリンク中 でした。そこでmain.cpp のイベント監視ループ LRESULT WndProc() にこのトリガー処理を仕込みます。何パターンか試してみたところ、くににん的には右ボタンを押した瞬間よりも離した瞬簡にメニュー窓が開くほうがしっくりする感じがしたので、イベントメッセージ=WM_RBUTTONUP のところでメニュー起動関数を呼ぶように記述しました。カーソルブリンク処理中かどうかは Mode_stat のメンバ flag_cursor_blinkON になっているか調べればよいので、ブリンク処理中のみメニュー起動関数 System_menu() を呼ぶようにします。



LRESULT WndProc(HWND hwnd,UINT msg,WPARAM wprm,LPARAM lprm)
{
switch(msg){

〜中略〜

case WM_RBUTTONDOWN: //マウス右ボタンが押された
Mouse_stat.time_of_R_click_old = Mouse_stat.time_of_R_click;
Mouse_stat.time_of_R_click = GetTickCount(); //時刻をメモ
goto SET_MOUSE_STAT;


case WM_RBUTTONUP: //マウス右ボタンが離された
if( Mode_stat.flag_cursor_blink == ON ){
System_menu(); //システムメニューを起動
}
goto SET_MOUSE_STAT;

case WM_MOUSEMOVE: //マウスが動いた

SET_MOUSE_STAT:
//リアルタイムでマウスの状態を保持
//※ゲームの処理はここでセットした変数の値を参照して動作します
Mouse_stat.fwkeys=(int)wprm; //ボタン状態
Mouse_stat.xpos=(int)LOWORD(lprm); //X座標
Mouse_stat.ypos=(int)HIWORD(lprm); //Y座標

break;


〜中略〜

default: //その他のイベントはWindowsのシステムにお任せ(楽ちん、楽ちん)
return DefWindowProc(hwnd,msg,wprm,lprm);

}

return 0;
}


■メニューの起動処理

では、いよいよ窓を開く部分です。メニュー処理の本体部分の記述はソースを分けて SystemMenu_01.cpp に記述していくことにしましょう。まず、窓用構造体を SysMenu_main という名でグローバル変数として宣言します。グローバル変数にしておくのは「撃つ」「回す」「締める」のタスク処理の3要素それぞれの関数で参照する可能性があるためです。

※いきなりここから読み始めた人には3要素といわれてもピンと来ないかもしれません。スクリプトコマンドの基本的な要素については第10夜「遅延」を参照願います(・ω・)ノシ


↓のリストをみると、最初に #define がたくさん並んでいますが、これは記述が冗長になるのを防ぐ為に構造体の各メンバを短縮表記しているものです。あまり気にしないで結構です。(define は直訳すると「定義する」「決める」という意味になります)



//-----------------------------------------------------
// システムメニュー(メイン=親窓)
//-----------------------------------------------------

_Menu_Win SysMenu_main; //メインメニューの窓情報用

//名前が長いと面倒なので窓構造体のメンバを短縮表記(^^;)
#define
#define
#define
#define
#define
#define
#define
#define
#define
_HDC
_HBITMAP
_WIDTH
_HIGHT
_WX
_WY
_ESX
_ESY
_TIMER
SysMenu_main.hDC
SysMenu_main.hBitmap
SysMenu_main.width
SysMenu_main.hight
SysMenu_main.wx
SysMenu_main.wy
SysMenu_main.esx
SysMenu_main.esy
SysMenu_main.timer

int System_menu() //システムメニュー窓を開く
{

_WIDTH = 100; //窓の幅
_HIGHT = 120; //窓の高さ
_WX = 280; //窓の左肩の座標 X
_WY = 100; //窓の左肩の座標 Y
_ESX = 200; //BITMAP上の画面退避位置 X
_ESY = 0; //BITMAP上の画面退避位置 Y

//再入防止
if( Mode_stat.flag_system_menu == SYSMENU_MAIN ) return 0;

//前回のクリックから一定時間経過(ここでは200msec)するまではスキャンは開始しない
if( HLS_timer_check( SysMenu_main.timer,200)==false )return 0;

//BMP読み込み画面を生成する
HDC work_hdc; //作業用のDC
work_hdc=GetDC(hwnd); //主(表)画面のDCの内容を取得
SysMenu_main.hDC=CreateCompatibleDC(work_hdc); //同じ設定でバック画面用のDCを生成
SysMenu_main.hBitmap=CreateCompatibleBitmap(work_hdc,640,480); //主(表)画面と同じ属性で画面生成
SelectObject(_HDC,_HBITMAP); //DCと画面本体を関連付ける
ReleaseDC(hwnd,work_hdc); //作業用DCを開放

//BITMAP読み込み
char f_name[256];
strcpy( f_name,G_PATH ); //グラフィックデータのパスに
strcat( f_name,"menu_window_main.bmp" ); //BITMAPファイル名を追加して
Load_Bmp( _HDC, f_name ); //読み込み

//窓と重なる部分の背景を退避
BitBlt( _HDC,_ESX,_ESY,_WIDTH,_HIGHT, Back_DC,_WX,_WY,SRCCOPY );

//窓描画
BitBlt( Back_DC,_WX,_WY,_WIDTH,_HIGHT,_HDC,0,0,SRCCOPY);

//フラグ操作
Mode_stat.flag_system_menu = SYSMENU_MAIN; //システムのメインメニュー
Mode_stat.flag_cursor_blink = OFF; //カーソル表示は停止しておく
Mode_stat.flag_text = OFF; //テキスト表示は停止しておく

//タイマーを撃つ
HLS_timer_start( &SysMenu_main.timer );

return 0;

}



System_menu() でやっていることはそんなに難しい処理ではありません。窓構造体に必要パタメータを入れて、再入防止とインターバル処理 (前回クリックから一定時間経過するまでは実行を受け付けない) を行い、その後は窓BMPの読み込みと表示、そしてタスク処理にむけたフラグの準備までを行って、最後にタイマーを撃ちます。

ここで重要なのは「再入処理」「前回クリックから一定時間入力を受け付けない」部分です。再入を禁じるのは、メニューが既に開いている状態で再度右クリックを検出した場合、それは「窓を開く」という意味ではなく「閉じる」意味なのでここでは受け付けないというものです。再入かどうかの判断は Mode_stat.flag_system_menu のフラグを見て行います。Mode_stat.flag_system_menu は、System_menu() の最後のところで SYSMENU_MAIN がセットされ、窓が開いていなければ OFF なので、区別をつける材料としては丁度よいわけです。

ところで、メモリに読み込んだ窓BMPと、バックサーフェイスの関係がすこしわかりにくいかもしれないので図にしてみました。如何でしょう?




■Mainloop()

起動処理を記述したら、次に main.cppMainloop() にフラグ処理部分を追加します。Mainloop() は常にぐるぐると処理が回っている部分で、ここから各タスク処理を呼んでいます。System_menu()Mode_statのフラグを立てましたので、フラグをもとにシステムメニューのタスク処理関数 System_menu_task() を呼ぶように追記しておきます。switch文の形式にしているのは、これから実装していくEXIT、FPS、LOAD、SAVEの各処理をフラグで分けるための準備と思ってください。



void Mainloop(void)
{

static DWORD fps_keep=0; //FPSを一定に保つためのタイマー変数
if( GetTickCount() < fps_keep + 1000/60 ) return;
fps_keep = GetTickCount(); //システム時刻を取得

//テキスト表示
if( Mode_stat.flag_text == ON ) Text_engine();

//遅延
if( Mode_stat.flag_delay == ON ) Com_delay_task();

//カーソルブリンク
if( Mode_stat.flag_cursor_blink == ON ) Com_cursor_blink_task();

//終了
if( Mode_stat.flag_halt == ON ) Com_halt_task();

//改ページ type2
if( Mode_stat.flag_page2 == ON ) Com_page2_task();

//画面切替え効果
if( Mode_stat.flag_g_change == ON ) Com_g_change_task();

//3択
if( Mode_stat.flag_select3 == ON ) Com_select3_task();

//システムメニュー
switch( Mode_stat.flag_system_menu ) {
case SYSMENU_MAIN: //メインメニューのスキャン
System_menu_task();
break;
}

//情報表示
Disp_Mouse_info(); //マウス情報(不要ならコメント化しちゃってください)
HLS_stc_FPS(Back_DC); //FPS(不要ならコメント化しちゃってください)

BitBlt(win_hdc,0,0,640,480,Back_DC,0,0,SRCCOPY); //裏画面 → 表画面にコピー

}


■メニューのタスク処理

メニューのタスク処理は System_menu_task() になります。ここではまだ選択支処理は行わず、マウスの右クリックを Mouse_stat (第31夜参照) を参照しながらスキャンし、右クリックを検出したら退避してあったウィンドウの背景を書き戻してフラグ類をリセットして終了しています。ここでも重要なのはタイマーの扱いで、トグル動作を確実にするために前回のクリックから 200msec 以内での再クリックは無視するようにしています。


int System_menu_task() //システムメニューのタスク部分
{

//前回のクリックから一定時間経過(ここでは200msec)するまではスキャンは開始しない
if( HLS_timer_check( SysMenu_main.timer,200)==false )return 0;

//右クリック(メニュー消去)されていれば背景を書き戻して処理を終了する
if( Mouse_stat.fwkeys == MK_RBUTTON ) {

BitBlt(Back_DC,_WX,_WY,_WIDTH,_HIGHT,_HDC,_ESX,_ESY, SRCCOPY);
DeleteDC( _HDC ); //DCの消去

//フラグ操作
Mode_stat.flag_system_menu = OFF; //システムのメインメニュー -> OFF
Mode_stat.flag_cursor_blink = ON; //カーソル表示再開

//タイマーを撃つ(直後にふたたびメニュー窓を開くのを防止するため)
HLS_timer_start( &SysMenu_main.timer );

return 0;

}

return 0;

}

さて、これを実行するとどうなるでしょう。テキストが流れている最中はメニューは反応せず、カーソルブリンクしている時のみ右クリックでメニュー窓が開きます。再度右クリックで窓は閉じます。トグル動作がどういうものか、おわかり頂けますでしょうか。

※おまけ:System_menu の _WX,WY(メニューウィンドウの表示位置)に Mouse_stat のマウス座標を代入してやると、マウスカーソルの位置にウィンドウが開くようになります。固定位置よりこちらのほうがスマートかも…(^^;)