ゲームプログラムとUI処理の共存検討

# メニュー処理の特殊性

2D STGゲームやちょっとしたミニゲームではすべての機能を定期的な更新処理として記述できる。しかしRPGのようにメニュー操作を多用するゲームではそれが難しい。原因は定期更新処理設計に加えてイベント駆動設計を共存せざるを得なくなるため。この文書では２つの設計を併存、協調動作させる方法について検討する。

# 出発点：ミニゲームプログラム例

UI処理無しのミニゲームのコード例を挙げる。UIの無いアクションまたは2D STGゲームを想定。このコードにUI処理を追加しようとしている。

```csharp
void OnEnterFrame()
{
    // キーボード押下状態読み取り
    byte[] statKeys = ReadKeys();

    // プレイヤーキャラクター行動：上下左右とzキーを判定
    UpdatePlayerCharacter(statKeys);

    // 敵キャラクター行動：近づいて攻撃
    UpdateEnemyCharacters();

    // アニメーション状態更新
    UpdateAnimations();

    // キャラクター描画
    RenderCharacters();
}
```

上記コードの想定仕様はこちら：

- 上下左右のカーソルキーで自キャラクターを移動する
- zキーで攻撃
- 敵キャラクターがプレイヤーキャラクターに近づいてきて一定距離以下になったら攻撃してくる

# 検討１、イベント駆動による入力判定

出発点のミニゲームプログラムではボタン押下状態を読み取ることで入力を受け付けていた。一方で業務アプリやWebページのようなUI処理が主なプログラムではたいていイベント駆動で入力を受け付ける。それぞれの入力方式の特徴を比較してみる。

- ボタン押下状態読み取り：複数ボタンの同時押し判定が簡単。定期的な読み取りが前提。１フレームに最大１状態まで。
- ボタン押下イベント検出：１フレームに２つ以上のイベント受け付けも可。定期読み取り処理が不要。

メニュー操作に向いているのはイベント検出の方に思える。しかし同時押し判定が容易な状態読み取りの方もプレイヤーキャラクタ操作に向いている。
おそらく別系統の入力判定方式２つを併存させ、ゲーム処理系とUI操作系とでそれぞれ適した方を使うのが良い。

# 検討２、複数の入力受付状態とその切り替え

メニュー操作を導入すると様々な入力受付状態が必要になる。複数の入力受付状態を定義して切り替える仕組みが必要。

入力受付状態例：

- プレイヤーキャラクタ操作状態：上下左右のカーソルキー移動、zキーで攻撃。
- メニュー操作状態：上下キーでメニュー項目にカーソルを合わせ、zキーで決定、xキーでキャンセル。
- ステータス画面表示状態：xキーでステータス画面を閉じる。
- アイテム選択状態：上下左右のカーソルキーで所持アイテムのアイコンリストからアイテムを選び、zキーで決定、xキーでキャンセル。
- Yes/Noダイアログ操作状態：左右でYes/Noボタンにカーソルを合わせ、zキーで決定。
- OKダイアログ操作状態：zキーでOKボタンを押す。
- 入力破棄状態：何も入力を受け付けず読み捨てる。※時間経過で状態が切り替わるまでのつなぎに使われる。

# 検討３、ゲーム進行の一時停止

アクションゲームではプレイヤー操作が無くても勝手に敵キャラクターが近づいてきて攻撃してくる。メニュー操作中はモンスターの動きを止めたい場合、ゲーム進行の一時停止機能を実現する必要がある。
もし実時間を使ってゲーム内時間の経過を表現している場合はそのままでは対応が難しい。定期更新関数の呼び出しを停止するだけで時間経過が止まるように時間の取り扱い方法を変更する。

# 検討４、バッファ付きGUI部品クラスの導入

GUIを扱うと文字描画する機会が増える。文字サイズを変える、Boldや色変えに対応する、レイアウトを整えるなど、リッチな表現もしたくなる。しかしそのためにはテクスチャやメッシュの動的生成が欠かせない。これを簡単化するため、バッファ付きのリソースハンドルクラスを導入する。
詳しくは別記事を参照：

https://qiita.com/qiitatosh/items/ae13c7bf737d32a1d290

# 検討５、ゲームループ処理への通知

メニュー操作でアイテム使用を選んだ場合を考える。おそらくキー押下イベントハンドラ処理内ですぐ実行することはできない。一時的な変数に「アイテムを使用する、アイテムIDはこれ」という情報を格納しておき、ゲームループ側でそれを読み取って実行する仕組みが要る。

# 変更後：ゲームプログラム＋UI処理のコード例

最初のミニゲームにUI処理を追加することを考える。下記の仕様に従ったコードを書き出してみて実現できそうか検討する。

仕様：

- 上下左右のカーソルキーで自キャラクターを移動する
- zキーで攻撃
- 敵キャラクターが自キャラクターに近づいてきて一定距離以下になったら攻撃してくる
- sキーでメインメニューを開く
- メインメニュー表示中は、上下カーソルキーで表示項目群のどれか１つを選択、アイテムリストを表示できる。
- メインメニュー表示中は、xキーでメインメニューを閉じる
- メインメニュー表示中は、zキーで選択中メインメニュー表示項目を開く
- メインメニュー表示項目は所持アイテムリスト表示、キャラクタステータス画面表示の２つ。
- 所持アイテムリスト表示中は、上下左右カーソルキーで所持アイテムを選択でき、選択中のアイテム詳細を表示できる。
- 所持アイテムリスト表示中は、xキーで所持アイテムリスト表示を閉じてメインメニュー表示に戻る。
- 所持アイテムリスト表示中は、zキーで選択中のアイテムを使用できる。
- キャラクターステータス画面表示中は、xキーでキャラクターステータス画面表示を閉じてメインメニュー表示に戻る。
- メインメニュー表示中または所持アイテムリスト表示中またはキャラクターステータス画面表示中は、自キャラクターの移動と攻撃および敵キャラクターの移動はできない。ゲームを一時停止する。

コード例：

```csharp
void OnEnterFrame()
{
    // メニュー操作中
    if (State == StateEnum.MenuOperating)
    {
    }
    // メニュー操作完了処理待ち中
    else if (State == StateEnum.CompletedMenuOperation)
    {
        BufferKeyDown.Clear();
        OpenHud();
        CurrentEventHander = OnEventControlCharacter;
        State = StateEnum.CharacterInAction;
    }
    // キャラクター操作中
    else if (State == StateEnum.CharacterInAction)
    {
        if (BufferKeyDown.Contains(Key.s))
        {
            BufferKeyDown.Clear();

            // ゲームループ内から状態を変える。
            // もしイベントハンドラ側から安全に状態切り替えできるならそちらに移動も可。
            CloseHud();
            OpenMainMenu();
            CurrentEventHander = OnEventMainMenu;
            State = StateEnum.MenuOperating;
        }
        else
        {
            BufferKeyDown.Clear();

            // キーボード押下状態読み取り
            byte[] statKeys = ReadKeys();

            // プレイヤーキャラクター行動：上下左右とzキーを判定、アイテム使用
            UpdatePlayerCharacter(statKeys, CommandInputFromUI);
            CommandInputFromUI = null;

            // 敵キャラクター行動：近づいて攻撃
            UpdateEnemyCharacters();

            // アニメーション状態更新
            UpdateAnimations();
        }
    }
    else
    {
        throw new InvalidProgramException();
    }

    // GUI描画部品の更新
    UpdateGUIComponentAnimations();

    // グラフィクスリソースハンドルのフラッシュとレンダリング
    FlushHandleAndRender();
}

// 一番上のイベントハンドラ。
void OnEvent(Event ev)
{
    if (ev.Type == EventType.AppQuit)
    {
        ...
    }
    else
    {
        CurrentEventHandler(ev);
    }
}

// キャラクタ操作中
void OnEventControlCharacter(Event ev)
{
    if (ev.Type == EventType.KeyDown)
    {
        BufferKeyDown.Add(ev);
    }
}

// メインメニュー表示中
void OnEventMainMenu(Event ev)
{
    if (ev.Type == EventType.KeyDown)
    {
        if (ev.KeyDown.KeyCode == Key.x)
        {
            CloseMainMenu();
            CurrentEventHander = OnEventDrop;
            State = StateEnum.CompletedMenuOperation;
        }
        else if (ev.KeyDown.KeyCode == Key.z)
        {
            if (MenuSelectingCursor == 0)
            {
                CloseMainMenu();
                OpenItemList();
                CurrentEventHandler = OnEventItemList;
            }
            else if (MenuSelectingCursor == 1)
            {
                CloseMainMenu();
                OpenCharacterStatusPage();
                CurrentEventHandler = OnEventCharacterStatusPage;
            }
        }
        else
        {
            // 上下カーソルキーで項目選択。
            UpdateMainMenu(ev);
        }
    }
}

// アイテムリスト表示中
void OnEventItemList(Event ev)
{
    if (ev.Type == EventType.KeyDown)
    {
        if (ev.KeyDown.KeyCode == Key.x)
        {
            CloseItemList();
            OpenMainMenu();
            CurrentEventHander = OnEventMainMenu
        }
        else if (ev.KeyDown.KeyCode == Key.z)
        {
            if (CurrentSelectingItemId.HasValue)
            {
                CommandInputFromUI = GameCommand.UseItem(CurrentSelectingItemId.Value);
                CloseItemList();
                CurrentEventHander = OnEventDrop;
                State = StateEnum.CompletedMenuOperation;
            }
        }
        else
        {
            // 上下左右カーソルキーで項目選択。
            UpdateItemList(ev);
        }
    }
}

// キャラクターステータス画面表示中
void OnEventCharacterStatusPage(Event ev)
{
    if (ev.Type == EventType.KeyDown)
    {
        if (ev.KeyDown.KeyCode == Key.x)
        {
            CloseCharacterStatusPage();
            OpenMainMenu();
            CurrentEventHander = OnEventMainMenu;
        }
    }
}

// イベント読み捨て中
void OnEventDrop(Event ev)
{
}
```

実現できそう。

# その他の検討項目

入力デバイスにはキーボードの他にもマウスクリックや画面タップによるGUIボタン押下、ジェスチャー、ジョイパッドなど様々ある。動作プラットフォームに応じて適切なデバイスに対応する。

UI操作にアニメーション効果や効果音再生を導入して見栄えをよくする。どちらもゲーム処理にあまり関わらないため、ゲームループを通さずにイベントハンドラから直接扱うことができるはず。

この文書で触れたUI画面の切り替え方法はかなり原始的。もうすこしスマートなやり方がありそうな気がする。