Unity本格入門:いきのこバトルで学ぶ UI とメニュー
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)
対象読者:第4回まで読み、メインシーンの戦闘まわりを把握したあと、画面上のメニュー・ダイアログ・ゲージを追いたい方
Menu.csを軸に、ポーズ・アイテム/レシピ窓・ライフゲージのつながりを読み解きます。
前提:第3回:プレイヤーと新 Input System。OwnedItemsData の保存形式は 第6回:所持品データとオーディオ で扱います(本記事では Instance の参照が中心です)。
記事の目次
- この記事で扱う範囲
- コードを全部見てみよう
- クラス関係(この記事で登場する範囲)
- UMLで整理する(状態・シーケンス)
- ポイント解説(①〜⑧)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
ポイント一覧
- ①
Menuのシングルトンとボタン登録 - ②
Time.timeScaleでポーズと再開 - ③
Updateでスコア表示と投げオノ数を同期 - ④ Legacy
Input.GetButtonDown("Cancel")と ESC の処理順 - ⑤
ItemsDialogとスロットの量産 - ⑥
ToggleとOwnedItemsのバインド - ⑦
LifeGaugeとfillAmount・座標変換 - ⑧
RecipeDialogの開閉
この記事で扱う範囲
- 主に:
Menu.cs、ItemsDialog.cs、RecipeDialog.cs、LifeGauge.cs(抜粋) - 触れるだけ:
ItemButton.cs(きのこの使用は第6回で詳述)

説明(学習のヒント):Time.timeScale でポーズしつつ、ESC/Cancel の入口から メニュー・アイテム・レシピを切り替えるイメージです。LifeGauge で体力表示を追う流れも含みます。
コードを全部見てみよう
Menu.cs(全文)
using UnityEngine;
using UnityEngine.UI;
public class Menu : SingletonMonoBehaviourInSceneBase<Menu>
{
[SerializeField] private Button pauseButton;
[SerializeField] private GameObject pausePanel;
[SerializeField] private Button resumeButton;
[SerializeField] private Text scoreText;
[SerializeField] private Button itemsButton;
[SerializeField] private Button recipeButton;
[SerializeField] private Button throwAxeButton;
[SerializeField] private Text throwAxeNumberText;
[SerializeField] private GameObject throwAxeCursor;
[SerializeField] private ItemsDialog itemsDialog;
[SerializeField] private RecipeDialog recipeDialog;
private void Start()
{
pausePanel.SetActive(false);
pauseButton.onClick.AddListener(Pause);
resumeButton.onClick.AddListener(Resume);
itemsButton.onClick.AddListener(ToggleItemsDialog);
recipeButton.onClick.AddListener(ToggleRecipeDialog);
throwAxeButton.onClick.AddListener(() => { PlayerController.Instance.ToggleReadyToThrow(); });
}
private void Update()
{
scoreText.text = "" + MainSceneController.Instance.MinutesInGame;
var throwAxeItem = OwnedItemsData.Instance.GetItem(Item.ItemType.ThrowAxe);
var throwAxeNumber = throwAxeItem?.Number ?? 0;
throwAxeNumberText.text = "" + throwAxeNumber;
throwAxeButton.interactable = throwAxeNumber > 0;
if (!Input.GetButtonDown("Cancel")) return;
if (PlayerController.Instance.IsReadyToThrow)
{
PlayerController.Instance.IsReadyToThrow = false;
return;
}
if (itemsDialog.gameObject.activeSelf)
{
itemsDialog.Toggle();
return;
}
if (recipeDialog.gameObject.activeSelf)
{
recipeDialog.Toggle();
return;
}
if (pausePanel.activeSelf)
{
Resume();
}
}
public bool IsThrowAxeActive
{
set
{
throwAxeButton.image.color = value ? Color.red : Color.white;
throwAxeCursor.SetActive(value);
}
}
private void Pause()
{
Time.timeScale = 0;
pausePanel.SetActive(true);
}
private void Resume()
{
Time.timeScale = 1;
pausePanel.SetActive(false);
}
private void ToggleItemsDialog()
{
itemsDialog.Toggle();
}
private void ToggleRecipeDialog()
{
recipeDialog.Toggle();
}
}ItemsDialog.cs(全文)
using UnityEngine;
public class ItemsDialog : MonoBehaviour
{
[SerializeField] private int buttonNumber = 15;
[SerializeField] private Transform panel;
[SerializeField] private ItemButton itemButton;
private ItemButton[] _itemButtons;
private void Start()
{
gameObject.SetActive(false);
for (var i = 0; i < buttonNumber - 1; i++)
{
Instantiate(itemButton, panel);
}
_itemButtons = GetComponentsInChildren<ItemButton>();
}
public void Toggle()
{
gameObject.SetActive(!gameObject.activeSelf);
if (!gameObject.activeSelf) return;
for (var i = 0; i < buttonNumber; i++)
{
_itemButtons[i].OwnedItem = OwnedItemsData.Instance.OwnedItems.Length > i
? OwnedItemsData.Instance.OwnedItems[i]
: null;
}
}
}RecipeDialog.cs(全文)
using UnityEngine;
public class RecipeDialog : MonoBehaviour
{
private void Start()
{
gameObject.SetActive(false);
}
public void Toggle()
{
gameObject.SetActive(!gameObject.activeSelf);
}
}LifeGauge.cs(全文)
using UnityEngine;
using UnityEngine.UI;
public class LifeGauge : MonoBehaviour
{
[SerializeField] private Image fillImage;
private RectTransform _parentRectTransform;
private Camera _camera;
private MobStatus _status;
private void Update()
{
Refresh();
}
public void Initialize(RectTransform parentRectTransform, Camera targetCamera, MobStatus status)
{
_parentRectTransform = parentRectTransform;
_camera = targetCamera;
_status = status;
Refresh();
}
private void Refresh()
{
fillImage.fillAmount = _status.Life / _status.LifeMax;
var screenPoint = _camera.WorldToScreenPoint(_status.transform.position);
RectTransformUtility.ScreenPointToLocalPointInRectangle(_parentRectTransform, screenPoint, null,
out var localPoint);
transform.localPosition = localPoint + new Vector2(0, 80);
}
}クラス関係(この記事で登場する範囲)
説明(学習のヒント):Menu だけがシーン内シングルトン基底を継承し、アイテム窓・レシピ窓は Menu から開閉を指示される関係です。
UMLで整理する(状態・シーケンス)
状態(ポーズとダイアログのイメージ)
説明(学習のヒント):プレイ中・ポーズ・アイテム・レシピの「いまのモード」を丸で表し、矢印は切り替わりのイメージです(実装の if 分岐と対応)。
シーケンス(Cancel 処理の入口)
Update で Cancel を検知したあと、上から順に if で分岐します(先にマッチした処理だけが実行されます)。
説明(学習のヒント):短い図ですが、Cancel を検知したあと Menu 内で上から順に どう振り分けるか、という入口だけを示しています。
ポイント①:Menu のシングルトンとボタン登録
Menu も SingletonMonoBehaviourInSceneBase<Menu> です。Start で各 Button の onClick にリスナーを登録し、ポーズ・アイテム・レシピ・投擲モード切替を UI から呼び出します。投擲は PlayerController.Instance に直接触れています。
ポイント②:Time.timeScale でポーズと再開
Time.timeScale = 0; // Pause
Time.timeScale = 1; // ResumetimeScale が 0 のとき、ゲーム内時間に依存する処理(物理やアニメの多く)は止まります。ポーズ UI を出すときの定番です。再開し忘れるとずっと止まったままになるので、Resume の対になる操作を必ず用意します。
ポイント③:Update でスコア表示と投げオノ数を同期
MainSceneController.Instance.MinutesInGame を Text に表示し、OwnedItemsData から投げオノの個数を取ってボタンと連動させています。interactable で「0個なら押せない」UI にしています。
ポイント④:Legacy Input.GetButtonDown("Cancel") と ESC の処理順
このプロジェクトでは Legacy Input Manager の Cancel ボタン(通常 ESC)で、
- 投擲モードならオフ
- アイテム窓が開いていれば閉じる
- レシピ窓が開いていれば閉じる
- ポーズ中なら再開
の優先順位で処理しています。第3回の新 Input System(移動・攻撃)と併存している点に注意してください。
ポイント⑤:ItemsDialog とスロットの量産
プレハブ itemButton を buttonNumber - 1 回 Instantiate し、スロット数を確保しています。GetComponentsInChildren<ItemButton>() で配列化し、開いたときに OwnedItems を差し込みます。
ポイント⑥:Toggle と OwnedItems のバインド
gameObject.SetActive(!gameObject.activeSelf);表示に切り替わったタイミングだけ、OwnedItemsData.Instance.OwnedItems の要素を各 ItemButton に渡します。足りないスロットは null で「空き」を表現しています。
ポイント⑦:LifeGauge と fillAmount・座標変換
Image.fillAmountで HP 比率を表示(教科書の HP ゲージと同じ考え方)WorldToScreenPoint→ScreenPointToLocalPointInRectangleで、3D キャラの頭上に UI を追従
Canvas が Screen Space - Overlay のとき、コメントのとおり第3引数に null を渡す書き方になっています。
ポイント⑧:RecipeDialog の開閉
レシピ側は Toggle で表示を反転するだけの最小実装です。中身のリスト表示などは別コンポーネントやプレハブ側の責務になります。
コードの流れを整理しよう
説明(学習のヒント):Update 側(上)とボタン経由(下)の2系統に分けています。Cancel のひし形は「押されたときだけ右の処理へ」というイメージです。
自分でカスタマイズしてみよう!
挑戦①:ポーズ中だけ別のテキストを出す
pausePanel 内に説明文を足し、Pause で表示を切り替えてみましょう。
挑戦②:スロット数
buttonNumber を変えると、Instantiate の回数と GetComponentsInChildren の結果が変わります。多すぎると負荷になるので、必要数に合わせましょう。
まとめ
Menuがシーン内シングルトンとして、ボタン・スコア表示・ESC 優先順位をまとめるTime.timeScaleでポーズし、ダイアログはSetActiveのトグルで開閉するItemsDialogはスロットを量産し、開いたときにOwnedItemsをバインドするLifeGaugeはfillAmountと 座標変換でキャラに追従する
所持データの保存形式と SE 再生は 第6回:所持品データとオーディオ へ続きます。
最終更新:2026年4月