メインコンテンツまでスキップ

メニューシステム

メニューシステムは MenuSelector コンポーネントの上に構築されています -- これは、任意のボタンのリストやグリッドに対してカーソルナビゲーション、入力、オーディオ、セレクタ矢印を処理する汎用的で再利用可能なコントローラーです。すべてのゲーム内メニュー(ポーズメニュー、バトルメニュー、ショップ、図鑑、オプションなど)はこれを継承します。

場所: Assets/OpenMon/Core/Runtime/UI/MenuSelector.cs 名前空間: OpenMon.MCE.UI

MenuSelector は HidableUiElement<MenuSelector> を拡張し、IInputReceiver を実装します。MenuItem の子のリストを管理し、次を提供します:

  • 垂直または水平のナビゲーション
  • ラップアラウンドのカーソル移動
  • アニメーション付きのセレクタ矢印(MCE 同梱のトゥイーン API — サードパーティ製アセットは不要)
  • 高速ナビゲーションのためのホールドスクロール
  • 移動と選択時のオーディオフィードバック
  • 選択、ホバー、戻るのイベント

主なシリアライズフィールド​

[SerializeField] protected int DefaultSelected;          // Starting cursor position
[SerializeField] private bool RequestInputOnShow = true; // Auto-request input focus
[SerializeField] protected Navigation MenuNavigation; // Vertical or Horizontal
[SerializeField] protected bool InvertedControls = true; // Invert nav direction
[SerializeField] private bool HideWhenChosen = true; // Auto-hide on select
[SerializeField] private bool NoChoosing; // Browse-only mode
[SerializeField] public bool NoGoingBack; // Disable back button
[SerializeField] public bool ShowSelectorArrow = true; // Show animated arrow
[SerializeField] public bool CanHoldToScrollFaster; // Hold-to-repeat
[SerializeField] public float HoldNavigationInterval; // Repeat rate (seconds)
public List<MenuItem> MenuOptions = new(); // The button list

イベント​

イベント型発火タイミング
OnButtonSelectedAction<int>プレイヤーが選択を確定したとき(選ばれた項目のインデックス)
OnHoveredAction<int>カーソルが新しい項目に移動したとき
OnBackSelectedActionプレイヤーが戻る/キャンセルボタンを押したとき

ライフサイクル​

Show(true)
└─ RequestInput() ──► IInputManager pushes this onto the input stack
└─ OnStateEnter()
├─ Instantiates selector arrow prefab
├─ Starts HoldNavigation coroutine
└─ Selects DefaultSelected after one frame

Show(false)
└─ ReleaseInput() ──► IInputManager pops this from the input stack
└─ OnStateExit()
├─ Stops HoldNavigation coroutine
├─ Deselects current item
└─ Destroys selector arrow

ナビゲーションフロー​

プレイヤーが方向を押すと:

  1. OnNavigation() が入力ベクトルを読み取り、関連する軸(垂直は Y、水平は X)を抽出します。
  2. Navigate() が選択インデックスを増減し、境界でラップアラウンドします。
  3. 操作不可のボタンは自動的にスキップされます。
  4. 新しいインデックスで Select() が呼ばれ、次を行います:
    • 前の項目の選択を解除
    • ナビゲーションオーディオを再生
    • CurrentSelection を更新
    • OpenMon.Foundation.Tweening API でセレクタ矢印を移動
    • OnHovered を発火
    • 新しいボタンで MenuItem.OnSelect() を呼び出し
// Simplified navigation logic
protected virtual void Navigate(float input)
{
if (InvertedControls) input *= -1;

int newSelection = CurrentSelection;

// Find next interactable item, wrapping around
switch (input)
{
case > 0: newSelection = (newSelection + 1) % MenuOptions.Count; break;
case < 0: newSelection = (newSelection - 1 + MenuOptions.Count) % MenuOptions.Count; break;
}

Select(newSelection);
}

ホールドスクロール​

CanHoldToScrollFaster が true のとき、ナビゲーション入力を押し続けると、HoldNavigationInterval のレートで Navigate() の呼び出しが繰り返されます。これは永続的なコルーチンを使用します:

protected IEnumerator HoldNavigation()
{
while (true)
{
if (Holding) Navigate(LastInput);
yield return new WaitForSeconds(HoldNavigationInterval);
}
}

場所: Assets/OpenMon/Core/Runtime/UI/MenuItem.cs

MenuSelector 内の各ボタンは MenuItem です -- 次を備えた HidableAndSubscribableButton です:

  • ArrowSelectorPosition -- セレクタ矢印が着地すべき位置を定義する Transform の子。
  • ToggleIgnoreLayout -- true のとき、項目を隠すと LayoutElement.ignoreLayout を通じてレイアウト計算からも除外されます。
  • OnSelect() / OnDeselect() -- Unity の EventSystem の選択状態をトリガーするために、親の MenuSelector から呼ばれます。
public class MenuItem : HidableAndSubscribableButton
{
public Transform ArrowSelectorPosition;

public virtual void OnSelect()
{
EventSystem.current.SetSelectedGameObject(null);
Button.Select();
}

public virtual void OnDeselect() => Button.OnDeselect(null);
}

ScrollableMenuSelector​

場所: Assets/OpenMon/Core/Runtime/UI/ScrollableMenuSelector.cs

ビューポートより背の高いメニュー(例:技やアイテムの長いリスト)では、ScrollableMenuSelector が Select() をオーバーライドして親の ScrollRect を自動スクロールします:

public override void Select(int index, bool playAudio = true, bool updateArrow = true, bool force = false)
{
base.Select(index, playAudio, false, force);
UpdateScroll(); // Scroll to keep selection visible
UpdateSelectorArrowPosition(); // Then reposition the arrow
}

private void UpdateScroll()
{
if (MenuOptions.Count < 2) return;
Scroll.verticalNormalizedPosition = 1 - (float)CurrentSelection / (MenuOptions.Count - 1);
}

VirtualizedMenuSelector​

場所: Assets/OpenMon/Core/Runtime/UI/VirtualizedMenuSelector.cs

非常に大きなリスト(数百項目、図鑑、PC ボックス)では、VirtualizedMenuSelector<TData, TButtonController, TButtonFactory> がオブジェクトプーリングを使用し、プレイヤーのスクロールに合わせて少数のボタンインスタンスをリサイクルします:

  • 固定数の TButtonController インスタンスが作成されます(ビューポートと小さなバッファを埋めるのに十分な数)。
  • ScrollRect がスクロールすると、画面外の項目が再配置され、新しいデータで再設定されます。
  • 抽象メソッド PopulateChildData(TButtonController child, TData data) は、リサイクルされた各ボタンにデータをバインドするために、サブクラスで実装する必要があります。
// Setting data on a virtualized menu
public void SetButtons(List<TData> newData, bool clearPrevious = true)
{
Data = newData;
RowCount = Data.Count;
Refresh(clearPrevious);
}

ボタンインスタンスは Zenject の GameObjectFactory<TButtonController> を通じて作成されます:

[Inject] private TButtonFactory buttonFactory;

// Inside CheckChildItems():
childItems[i] = buttonFactory.CreateUiGameObject(Content);

例:SavegameButton​

public class SavegameButton : VirtualizedMenuItem
{
[SerializeField] private TMP_Text Text;

public void SetSaveName(string nameToSet) => Text.SetText(nameToSet);

public class Factory : GameObjectFactory<SavegameButton> { }
}

ゲーム画面​

メインメニュー(タイトル画面)​

場所: Assets/OpenMon/Core/Runtime/UI/MainMenu/

タイトル画面は、New Game、Continue(ロード)、Credits を提供します。ロード画面は、保存されたゲームを表示するために SavegameButton 項目を持つ VirtualizedMenuSelector を使用します。

ゲーム内ポーズメニュー​

場所: Assets/OpenMon/Core/Runtime/UI/GameMenu/GameMenuScreen.cs

GameMenuScreen は MenuSelector を拡張し、トゥイーンアニメーションでスライドイン/アウトします。選択を ButtonSelected(int index) の switch で振り分け、11個のエントリを持ちます — 元の8個に加え、3つの構築可能コンテンツエントリ(case 9〜11)。インデックスはそのままで、スロット 8 は非表示です:

インデックスエントリアクション
0DexDialogManager.ShowDexScreen(...)
1MonstersDialogManager.ShowPlayerRosterMenu(...)
2BagDialogManager.ShowBag(...)
3MapMapSceneLauncher.ShowMap(...)
4QuestsDialogManager.ShowQuestsScreen(...)
5ProfileDialogManager.ShowProfileScreen(...)
6OptionsDialogManager.ShowOptionsMenu()
7Exitセーブを促し、終了またはタイトルへ戻る
9Battle TowerEphemeralTowerLauncher.Launch(diContainer, playerCharacter) — 無限で一時的な挑戦
10My BaseEnterOrDecorateSecretBase() — 内部に入る、または内部にいるとき装飾パレットを開く
11My GymEnterOrManageGym() — プレイヤーのジムに入る、または内部にいるとき装飾/管理する

3つの新規エントリはローカライズされ(ソース内ではスペイン語ラベル Torre de Batalla / Mi Base / Mi Gimnasio)、利用可能性でゲートされます — メニュープレハブでオーサリングスロットが接続されるまで、それぞれ安全な no-op です:

エントリ利用可能性ゲート(GameMenuScreen にシリアライズ)
Battle Tower(case 9)プレイヤーが空でない PlayerRoster を持つ
My Base(case 10)secretBaseInteriorScene(SceneInfoAsset)が割り当て済み。そうでなければ入場はスキップ
My Gym(case 11)gymInteriorScene(SceneInfoAsset)が割り当て済み。そうでなければ入場はスキップ

case 9〜11 は依存性注入されます:diContainer が一時的なタワーをコンストラクタ注入し、teleporter(PlayerTeleporter)がプレイヤーをベース/ジムの内部へワープさせます。画面を占有するエントリの前には、CloseMenuForOverworldAction() が OnBack のティアダウンをミラーリングし、バトル/シーンの背後にフィールドがフリーズしたまま残らないようにします。

メニューは OnExtra2 によるクイックセーブに対応し、戻るボタンと専用のメニューキー(OnExtra1)の両方で切り替えられます。

// Opening the game menu
DialogManager.ShowGameMenu(onButtons, playerCharacter, onBack);

ボタンの表示は UpdateLayout(List<bool>) で制御されます -- ブール値のリストを渡して特定のエントリを表示/非表示にします(例:プレイヤーが入手する前に Dex を隠す)。

3つの新規エントリの背後にあるゲームプレイについては、シークレットベースとプレイヤージムガイドを参照してください。

オプション画面​

場所: Assets/OpenMon/Core/Runtime/UI/Options/

ゲーム設定を MenuSelector ベースのコントロールとして公開します:

  • ボリュームスライダー(Global、Music、FX)(VolumeSlider / TweenableSlider 経由)
  • 解像度、フルスクリーンモード、モニター選択
  • 言語選択
  • ゲームプレイ:バトル速度、バトルスタイル、難易度、捕獲難易度、走行モード
  • オートセーブのトグル、セーブ形式、ニックネーム表示

バトル UI​

場所: Assets/OpenMon/Core/Runtime/UI/Battle/

バトル HUD は、(DialogManager を経由しない)スタンドアロンの MenuSelector サブクラスを使用します:

  • MainBattleMenu -- たたかう / バッグ / モンスター / にげる、最後のボールのクイック投げ(OnExtra2)付き。
  • BattleMoveMenuSelector -- 4技グリッド。
  • TargetMonstersMenuSelector -- マルチバトルの対象選択。
  • BattleInfoPanel -- 詳細なステータス/タイプ情報のオーバーレイ。

カスタムメニューの追加​

  1. ルートに MenuSelector(またはサブクラス)を、各ボタン用に MenuItem の子を持つプレハブを作成します。
  2. 各 MenuItem の ArrowSelectorPosition を、矢印が表示されるべき子 Transform に設定します。
  3. インスペクターで MenuNavigation(Vertical/Horizontal)、オーディオ参照、矢印プレハブを設定します。
  4. コントローラーでイベントをサブスクライブします:
public class MyCustomMenu : MonoBehaviour
{
[SerializeField] private MenuSelector menu;

private void OnEnable()
{
menu.OnButtonSelected += OnItemChosen;
menu.OnBackSelected += OnBack;
}

private void OnItemChosen(int index)
{
// Handle selection
}

private void OnBack()
{
// Close menu
}
}
  1. 大きなリストの場合は、VirtualizedMenuSelector<TData, TButton, TFactory> を継承し、PopulateChildData() を実装し、UIElementsInstaller にボタンファクトリを登録します。