初心者が作る物としての「ウィザードリィっぽいDRPG」の良いところのひとつは,街の場面が単なる選択肢だってことだ.
{宿屋,店,酒場,迷宮,…}のどこに行くのか? というのを選ぶためのGUIがあるだけで,「町のマップ」だとか「そこに人が歩いている」みたいなのが要らない分だけ易しいと思うんだよね.
とはいえ,そういう選択肢のGUI(これを本稿内では「メニュー」と呼ぶことにする)の実装が易しいかというと……果たしてどうかな?
とりあえずここでは「メニュー」というのは,
という程度の物だということにする.
街の場面の他にも,どのアイテムを使うだとか,攻撃対象は誰かとか,とにかく操作のほとんどはメニューになるから,何はともあれメニューは真っ先に作る必要がありそうだ.
前記したような限定的なものだけを想定するなら,入力の処理はそんなに難しくないと思う.
「カーソルの位置」という情報は長期的に保持しておく必要がありそうだから,まずは「それを保持している(であろう)やつ」というのを用意しよう:
//何かメニューの状態(?)を保持しているであろうやつ
struct IMenuContent
{
virtual int CursorPos() const = 0; //現在のカーソル位置を返す
//カーソル位置を変更するメソッド.結果としてカーソル位置が変わったか否かを戻り値で返す
virtual bool IncCursorPos() = 0; //カーソルを「ひとつ次の位置」に移動させる
virtual bool DecCursorPos() = 0; //カーソルを「ひとつ前の位置」に移動させる
};
そしたら,入力の処理は
//戻り値用.何が成されたかを示す.
enum class HandleInputResult
{
, //何も変化無し
None, //カーソル位置が変化した
CursorMoved, //項目選択操作が成された
Selected//キャンセル操作が成された
Canceled };
//メニューに対する入力処理
(
HandleInputResult HandleInput&Content, //操作対象
IMenuContent const IController &Controller //何か,操作入力の状態を得る手段
)
{
//カーソル移動
if( Controller.カーソルをひとつ前に動かす操作が成された )
{
if( TgtContent.DecCursorPos() ){ return HandleInputResult::CursorMoved; }
}
if( Controller.カーソルをひとつ次に動かす操作が成された )
{
if( TgtContent.IncCursorPos() ){ return HandleInputResult::CursorMoved; }
}
//選択とキャンセル
if( Controller.選択操作が成された ){ return HandleInputResult::Selected; }
if( Controller.キャンセル操作が成された ){ return HandleInputResult::Canceled; }
return HandleInputResult::None;
}
くらいか.
このコードを使う側は HandleInput()
が Selected
を返してきたなら Content
の CursorPos()
の値を見て「2つ目の項目が選ばれたか……そしたら……」って感じで対応する処理を起動できる.
キャンセル不能なメニューに対して Canceled
が返ってきた場合は単に無視すればいい.
(そういう意味では Canceled
という名称はイマイチだな.「キャンセル操作が成された」みたいな意味の名称の方が良いだろう)
操作はできたので,あとは表示の側があればとりあえず使えるハズ.
メニューの個々の項目を描画することができるならば,メニューの描画とは全項目を先頭側から順に(描画位置を適切にずらしながら)描画すれば良いって話になる.
ってことで,まずは項目から:
//メニューの1項目を描画する手段
struct IMenuItem
{
//この項目の描画色を設定
virtual void SetColor( /*描画色*/ ) = 0;
//描画処理
virtual void Draw(
const Rect &ItemDrawReg, //描画する範囲 (※ Rect型とは矩形を示すやつなのだとして.GDIならば RECT かな)
bool IsAtCursorPos, //カーソルがこの項目を指しているか否か
bool IsMenuFocused //この項目が所属するメニューに現在入力フォーカスがあるか否か
) const = 0;
};
状況に応じて描画具合を変えたい気がするから Draw()
にはそのために必要そうな引数を並べる.
色の事前設定手段がこんな感じで存在するのが良いのか,それとも描画色も
Draw()
の引数にするのが良いのか,そこは実際に便利な側を選べばいいかな.
(しかしこの手のメソッドの名前は Draw
なのか
Paint
なのか? まぁどっちでもいいが)
単に std::vector< IMenuItem* >
とかそれに類する物があればそれで済むような気もするけども,
IMenuContent
あたりに混ぜ込んじゃってもいいんじゃないかな.
//何かメニューの状態(?)を保持しているであろうやつ
struct IMenuContent
{
//カーソル位置関係
virtual int CursorPos() const = 0;
virtual bool IncCursorPos() = 0;
virtual bool DecCursorPos() = 0;
//とりあえずこの2つさえあれば,項目群の描画が可能だ
virtual int nItems() const = 0; //項目の個数を得る
virtual const IMenuItem &Item( int index ) const = 0; //項目へのconst参照を得る
//あと,本件では話を簡単にするため「同一のメニュー内の項目は全部同一のサイズ[pixel]」ってことにする.
//そしたらそのサイズを取得する手段もここにあればいいかな.
virtual Vec2i ItemDrawSize() const = 0; //(※ Vec2i 型は2次元のベクトルみたいなのだとして)
//その他,項目の並びが縦なのか横なのかもメニューごとに決まるだろうからそういうの
virtual bool IsVerticalMenu() const; //縦並びならtrue, 横並びならfalse
};
こんなのがあるなら,const IMenuContent &
を引数にとる描画関数を書くことには問題はひとつも無いハズ.
void DrawMenu(
const IMenuContent &TgtMenuContent, //描画対象
bool IsMenuFocused, //メニューが入力フォーカスを持っている状態か否か
const Vec2i &TopLeft, //描画位置(※ Vec2i 型は2次元のベクトルなのだとして)
bool DrawFrame=true, //Option. 全体を囲む枠を描画するかどうか
int Margin=0 //Option. 隣接項目間に設ける余白[pixel]
)
{
//項目群の描画
= (左上がTopLeftで,サイズが TgtMenuContent.ItemDrawSize() な矩形)
Rect ItemDrawRect const Vec2i DrawPosOffset =
.IsVertialMenu() ?
TgtMenuContent{ 0, ItemDrawRect.Height() + Margin } :
{ ItemDrwRect.Width() + Margin, 0 };
for( int iItem=0; iItem<TgtMenuContent.nItems(); ++i )
{
.Item( iItem ).Draw( ItemDrawRect, (iItem==TgtMenuContent.CursorPos()), IsMenuFocused );
TgtMenuContent.TopLeft += DrawPosOffset;
ItemDrawRect}
//枠の描画
if( DrawFrame ){ /*略.↑の処理中にサイズを算出しとく必要がありそう*/ }
}
みたいな. Option
としている引数も
IMenuContent
の方に持たせちゃってもいいかも.
とりあえずはインタフェースクラスの実装を必要なだけ頑張って作りさえすれば,ゲームで必要になるいろんな見た目のメニュー群を作ることができるハズ.
「頑張って作る」のところでは,いくつかのメニューに関して何かしらの共通コードを作れたりだとか,そういう具体実装の話はまぁいろいろとあるかもだけど.