ウィンドウの表示と制御(Desigeonより)

そういえば他のRPG制作者はどうやってるんでしょうねえ。描画と入力まわりというのは、最初に決めたやり方を長くひきずるし相当クリティカルな部分ですよね。

メニューの作り方が分からない

http://d.hatena.ne.jp/Longsword/20090426/1240688073

Longswordさんのところではフレーム単位での処理を前提としているようなので、私とは状況が違うと思いますが、Desigeonはウィンドウシステムでやってます。キャラクタ情報画面、店の画面、街の画面などすべてウィンドウクラスを割り当てています。ウィンドウクラスが描画を管理し、入力はウィンドウクラスがWindowsメッセージで判断してます。全体としてはWindowsのモーダルダイアログボックスがモデルです。

描画はDraw()という仮想関数をウィンドウクラスに作っています。再描画をかけるときはこれを呼んで、OSにWM_PAINTするだけです。

ウィンドウへの入力は、ウィンドウがメッセージ番号を見るんじゃなくて、だいたい使うメッセージが決まっているので仮想関数OnKeyDown()やOnMouseMove()などを作っておいて、メインフレームがWM_KEYDOWNやWM_MOUSEMOVEに応じてこれらを呼ぶようにしています。そのときのトップウィンドウの仮想関数を呼び出します。この感覚もDraw()と同じです。このあたりは古典的なウィンドウシステムですね。

ウィンドウはプログラムのどこからでも開けるようになっていて、ウィンドウの起動機能はグローバルスコープ化されています。ウィンドウ自体はローカル管理です。グローバル関数によってローカルのウィンドウクラスを起動する、という感じです。

//
// ウィンドウを開く
//
//     KGS_CharView   : ゲームキャラクタ情報画面クラス
//     pChar          : ゲームキャラクタへのポインタ
//     this           : 現在のウィンドウ
//     KGC_GetSpace() : グローバルなデータがいろいろ入ってる
//     ShowView()     : ウィンドウを起動
//
KGS_CharView    dCharView( pChar, this );
KGC_GetSpace()->ShowView( &dCharView );

グローバル呼び出しShowView()にはメッセージループがあります。だからキャラクタ情報の閲覧を終えるまでは制御が戻りません。ESC等のキー入力を検出すると、キャラクタ情報画面は自分でシステムから自分を切り離します(共通してKGC_GetSpace()->DeleteView(this)というのを呼んでいます)。するとShowView()がそのことを検出して、メッセージループを終えます。

ShowView()から制御が戻ったら、dCharViewは破棄します。必要ならdCharViewから戻り値を取り出します。そこは描画クラスのインプリメント次第です。

タスクの概念は持っていません。(スレッドを分ける箇所もありますが、その目的は今回の主旨とは無関係)

メッセージループ内でやっている例外的な処理は、アプリケーションのクローズだけです。これが発生したときだけはすべての仕事を中断してプログラムを終えねばなりません。だからウィンドウを開いているときにアプリ終了となればthrowを使って、強引にループを抜けています。呼び出し親も飛び越えてアプリケーション最初のメッセージループに戻ります。setjmp()的なことをしているのはその一箇所のみです。(他、例外スローはファイルオープン失敗等のエラー処理に使っていますが、まず動くことはないです)

ウィンドウをレイヤ分けしています。thisを渡して子のウィンドウを起動するので、ウィンドウが親をDraw()してから自分をDraw()すれば、背景を損ねないでトップの描画ができます。

キーボードやマウスのメッセージ処理は常にトップウィンドウだけです。今のところ、それ以外は必要がないのでやっていません。

再描画が必要なら再描画関数(共通してKGC_GetSpace()->Redraw()というのを呼んでいます)を呼びます。これをやるとトップウィンドウを再描画して、メインフレームの描画領域にAPIメッセージWM_PAINTを送ります。例えばキャラクタが回復呪文を使ったら、ヒットポイントの表示を更新しなければなりません。

よく使うものは関数でラップしてシンプル化します。例えばイエス・ノーを問う画面。

 // 商品を買うかを問う
 if( YesNo( "買う?" ) )
 {
    // イエス
 }
 else
 {
    // ノー
 }

YesNo()にどんな引数を渡すかはいろいろあって良いのですが、とにかく気持ちの上では「買う?」という文字列を渡して返事が欲しいのです。返事があるまでは待ちたいのです。これを一行でやりたい。関数でラップしてシンプル化してます。

YesNo()の中では、やはりイエス・ノーのウィンドウクラスを作って、ShowView()しているので処理的には前と同じです。

他には値入力のInputVolume( int* inputvolume, int max, int min, int default )や、テキストファイルを表示するMessage( LPCTSTR path )などを作ってあって、必要なときにぱっと呼び出すようにしています。


私はいつも、楽なコーディングに落とし込む目的を作ってから仕組みを考えてます(だからあり合わせで作ったっぽい画面になるんでしょうな)。タスクのキューを作ったり、コルーチンを制御したり、スレッド分けをすることで楽ができるならそうすべきと思いますし、楽ができないなら作りはじめてもどうせ終わらない。

市販ゲームだと自社システムのスクリプトやライブラリを使ったりするので、そのお約束に合わせて書くことが多いんだと思います。PCだとNScripter吉里吉里が採用されていたりする。吉里吉里なんかはそれだけで独自の汎用システムを書いてる人もいそうです。アリスソフトは伝統的に社内システム(System3)を作って使っていますが、そのシステムは一般に公開されていて、コミュニティが作られて、移植が行われて。私は今のところ使う予定がありませんが、選択肢は他にもありそうです。

引き出しを増やしたり、多面的にものを見るために教本も時には参考になりますが、教本単体は特定の条件に特化している可能性があることと(例えばSTG向けとか)、個人の経験に基づく自己満足本という見方もしたいです。自由すぎるC++では特に、現場現場で考えたいですね。