Class: TonMenu

RPGなどのゲームで見られる「ウィンドウシステム」「コマンドメニュー」を実装するためのクラスです。
単純なテキスト選択肢から、アイテムリスト、設定画面のような複雑なUIまで幅広く対応します。
かなり難しいですが、自作するより遥かに楽なので、サンプルを触って理解してみましょう。

アーキテクチャと概念

TonMenu システムは、主に 「メニュー本体 (TonMenu)」「管理クラス (TonMenuManager)」 の2つで構成されています。

flowchart TD
    Game[ゲームシーン] -->|Update/Draw| Manager[TonMenuManager]
    
    subgraph StackStruct ["TonMenuManager (スタック構造)"]
        Manager -->|管理| Stack
        Stack -->|前面| MenuA["TonMenu (Active)"]
        Stack -->|背面| MenuB["TonMenu (Inactive/Paused)"]
    end
    
    MenuA -->|描画| ItemsA[アイテムとレイアウト]
    MenuB -->|描画| ItemsB[アイテムとレイアウト]
TonMenuManagerとTonMenuの関係図

実際のゲーム画面におけるTonMenuManager、TonMenu、TonMenuItem、TonMenuElementの関係性

この図は、TonMenuシステムの全体像を表しています。なぜ「Manager」と「Menu」が分かれているのでしょうか?
それは、複数のメニュー(アイテム画面、装備画面など)が重なり合う状況を管理するためです。「Stack(スタック)」という構造を使うことで、新しいメニューを開いたときに古いメニューを「下」に保存し、閉じれば元に戻るという、RPGでよくある挙動を自動的に処理してくれます。Game SceneはManagerだけに指示を出せばよく、個々のメニューの状態を細かく気にする必要がありません。

Quick Start

Step 1: マネージャーの初期化

classDiagram
    class ゲームシーン {
        TonMenuManager _menuManager
    }
    ゲームシーン --> TonMenuManager : 所有

このクラス図は「所有(Ownership)」の関係を示しています。ゲームシーンが TonMenuManagerを持っている、つまり「マネージャーの生存期間はシーンと同じ」であることを意味します。シーンが始まるときにマネージャーを作り、シーンが終われば一緒に消える、という管理の責任範囲を表しています。

private TonMenuManager _menuManager;

public void Initialize()
{
    // マネージャーの生成
    _menuManager = new TonMenuManager();
}

Step 2: メニューの作成

graph LR
    Create[new TonMenu] --> Config[設定]
    Config --> |SetContentScale| Size[コンテンツ(画像・テキスト)サイズ調整]
    Config --> |SetCursorIcon| Icon[カーソル画像]
    Config --> |SetLoopable| Loop[カーソルループ設定]

メニューを作る際の流れです。いきなり完成品を作るのではなく、まず「空のメニュー」を生成し、そこへ「コンテンツ(画像・テキスト)サイズ」「カーソル画像」「カーフルループ設定」といったオプションを後付けで設定していく様子を表しています。これにより、必要な機能だけを選んでカスタマイズできる柔軟な設計になっています。

// 位置(50,50), ウィンドウサイズ(240x150), 1列4行, 各アイテムサイズ(200x50), 空白選択不可
var menu = new TonMenu(new Rectangle(50, 50, 240, 150), 1, 4, 200, 50, false);

// 外観設定
menu.SetContentScale(0.8f);           // コンテンツ(画像・テキスト)サイズ
menu.SetCursorIcon("finger");         // カーソル画像
menu.SetLoopable(true);               // カーソル上下ループ有効

Step 3: アイテムの追加

sequenceDiagram
    participant Code
    participant Menu
    participant Item
    
    Code->>Item: new TonMenuItem()
    Code->>Item: SetLayout(Text/Icon...)
    Code->>Item: OnDecided = () => { ... }
    Code->>Menu: AddItem(Item)

プログラムコードがどのようにアイテム(選択肢)を作ってメニューに渡すかを示しています。重要なのは、「決定時の動作(OnDecided)」をアイテム作成時に決めている点です。「このアイテムが選ばれたら何をするか」というロジックをアイテム自身に持たせることで、メニュー側は中身を気にせず「選ばれたら実行するだけ」というシンプルな構造を保てます。

// アイテム追加
menu.AddItem(CreateTextItem("たたかう", () => {
    Console.WriteLine("攻撃!");
}));

// テキストのみのアイテム作成ヘルパー
private TonMenuItem CreateTextItem(string text, Action action)
{
    // メニューアイテムを作成
    var item = new TonMenuItem();
    // 空のレイアウトパネルを作成してテキストを追加
    var layout = new TonMenuPanel(TonMenuPanel.LayoutType.Free);
    // レイアウトパネルにテキスト要素を追加
    layout.AddChild(new TonMenuText(text));
    // メニューアイテムにレイアウトを設定
    item.SetLayout(layout);
    // 決定時の動作を設定
    item.OnDecided = action;

    return item;
}

Step 4: メニューの表示 (Push)

graph TD
    TonMenuManager -->|Push| Stack
    Stack -->|Active!| New_TonMenu
    Stack -->|Pause...| Old_TonMenu

「Push(プッシュ)」という操作の概念図です。お皿を積み重ねるように、新しいメニュー(New TonMenu)を既存のメニュー(Old TonMenu)の上に載せます。
これにより、下のメニューは削除されずに「一時停止(Paused)」状態で待機できます。キャンセルボタンで上のメニューをどかせば、下のメニューがそのままの状態で復帰する。これがスタック管理の利点です。

// メニューを開く
_menuManager.Push(menu);

Step 5: ループ処理 (Update/Draw)

graph TD
    GameUpdate[Game.Update] --> ManagerUpdate[Manager.Update]
    ManagerUpdate -->|Input| ActiveMenu[ActiveMenu.Control]
    
    GameDraw[Game.Draw] --> ManagerDraw[Manager.Draw]
    ManagerDraw -->|1. Callback| DrawWindow[ウィンドウ・スクロールカーソル描画]
    ManagerDraw -->|2. Draw| DrawContents[TonMenuItem描画]

ゲームループ(毎フレーム繰り返される処理)の中で、メニューがどう動くかを表しています。
Update(更新): 操作を受け付けるのは「一番上のアクティブなメニュー」だけです。下のメニューが勝手に動かないように制御されています。
Draw(描画): 逆に描画は、ウィンドウの背景を描いてから中身を描く、という順序で丁寧に行われます。これにより、ウィンドウ枠の上に文字が綺麗に乗ることになります。ユーザはウィンドウとスクロールカーソルの描画を自分で行う必要があります。これはある程度デザイン上の汎用性をもたせるためです。逆にTonMenuItem内に画像を配置した場合、標準機能ではこれをアニメーションさせたりすることはできません。カーソルもです。

public void Update(GameTime gameTime)
{
    // 入力制御などを一任
    _menuManager.Update();
}

public void Draw()
{
    // メニュー描画
    // 引数のコールバックで「メニューごとのウィンドウ背景」を描画します
    _menuManager.Draw((menu) => 
    {
        if (menu.IsActive)
            Ton.Gra.FillRoundedRect("window_bg_active", menu.WindowRect...);
        else
            Ton.Gra.FillRoundedRect("window_bg_inactive", menu.WindowRect...);
    });
}

Information

ライフサイクルイベント

状態変化のタイミングで様々なコールバックが発火します。

stateDiagram-v2
    [*] --> Inactive : 生成
    Inactive --> Active : Push / Resume (OnEnter/OnResume)
    Active --> Paused : Submenu Push (OnPause)
    Paused --> Active : Submenu Pop (OnResume)
    Active --> Inactive : Pop (OnExit)
    
    Active --> Active : カーソル移動 (OnSelectionChanged)
    Active --> Active : 決定操作 (OnDecided)

メニューが生まれてから消えるまでの「一生」の状態変化(ステートマシン)です。
単に開閉するだけでなく、「サブメニューが開かれて裏に回ったとき(Paused)」や「再び表に出てきたとき(Active)」といった細かい状態があります。これらのタイミングでイベント(OnPause, OnResume)が呼ばれるため、細やかな演出が可能になります。

イベント タイミング 主な用途
OnEnter メニューが表示され、操作可能になった瞬間 ヘルプ表示、開始SE
OnExit メニューが閉じられる(Pop)瞬間 後始末、終了SE
OnPause サブメニューが開かれ、背後に回る時 装飾の解除
OnResume サブメニューが閉じられ、最前面に戻った時 ヘルプ復帰
OnPostDraw 描画完了後 スクロール矢印などのカスタム描画

Methods

public TonMenu(Rectangle rect, int column, int row, int width, int height, bool bAllowBlankSelect, bool bAllowMultiSelect = false)

TonMenuのコンストラクタ。

void SetFont(string fontId)

使用するフォントを設定します。

void SetTextColor(Color defaultColor, Color? disabledColor = null)

テキストの基本色を設定します。

void SetCursorColor(Color selectedColor, Color? frameColor = null)

カーソルの背景色と枠色を設定します。

void SetCursorIcon(string iconName, Vector2? offset = null)

カーソル用の画像アイコンを設定します。

void SetContentScale(float scale)

内部のテキストやアイコンの描画倍率を設定します。

void SetLoopable(bool isLoop)

カーソルが端でループするか設定します。

void SetTextOffset(int offset)

パネルレイアウト内のテキスト描画オフセットを設定します。

void AddItem(TonMenuItem item)

メニューにアイテムを追加します。

void Clear()

メニューの内容をクリアし、カーソル位置やスクロールをリセットします。

TonMenuItem GetCurrentItem()

現在選択中のアイテムを取得します。空白選択時はnullを返すことがあります。