何はともあれメニューが必要(1)

もくじへ戻る


初心者が作る物としての「ウィザードリィっぽい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(
  IMenuContent &Content,  //操作対象
  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 を返してきたなら ContentCursorPos() の値を見て「2つ目の項目が選ばれたか……そしたら……」って感じで対応する処理を起動できる.
キャンセル不能なメニューに対して Canceled が返ってきた場合は単に無視すればいい.
(そういう意味では Canceled という名称はイマイチだな.「キャンセル操作が成された」みたいな意味の名称の方が良いだろう)


描画処理

操作はできたので,あとは表示の側があればとりあえず使えるハズ.

メニューの個々の項目を描画することができるならば,メニューの描画とは全項目を先頭側から順に(描画位置を適切にずらしながら)描画すれば良いって話になる.
ってことで,まずは項目から:

1項目の描画手段

//メニューの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 なのか? まぁどっちでもいいが)

「N個の項目」は?

単に 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]
)
{
  //項目群の描画
  Rect ItemDrawRect = (左上がTopLeftで,サイズが TgtMenuContent.ItemDrawSize() な矩形)
  const Vec2i DrawPosOffset =
    TgtMenuContent.IsVertialMenu() ?
    { 0, ItemDrawRect.Height() + Margin } :
    { ItemDrwRect.Width() + Margin, 0 };
  
  for( int iItem=0; iItem<TgtMenuContent.nItems(); ++i )
  {
    TgtMenuContent.Item( iItem ).Draw( ItemDrawRect, (iItem==TgtMenuContent.CursorPos()), IsMenuFocused );
    ItemDrawRect.TopLeft += DrawPosOffset;
  }
  
  //枠の描画
  if( DrawFrame ){ /*略.↑の処理中にサイズを算出しとく必要がありそう*/ }
}

みたいな. Option としている引数も IMenuContent の方に持たせちゃってもいいかも.

とりあえずはインタフェースクラスの実装を必要なだけ頑張って作りさえすれば,ゲームで必要になるいろんな見た目のメニュー群を作ることができるハズ.
「頑張って作る」のところでは,いくつかのメニューに関して何かしらの共通コードを作れたりだとか,そういう具体実装の話はまぁいろいろとあるかもだけど.


つづく