Unity本格入門 実装編:Chapter 8 ユーザーインタフェースを作ってみよう
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲にとどめます。本シリーズ目次
対象読者:Unity エディタの基本操作と C# のコードを追いながら読める方
この記事では、いきのこバトルのタイトル・ゲームオーバー・ゲーム内 UI を題材に、Canvas・シーン遷移・DOTween・データ保存・ライフゲージ・スマホ対応の実装を読み解きます。書籍 Chapter 8(8-1〜8-5) に対応します。
記事の目次
おすすめの読み方
セクション一覧
- 題材の範囲
- プロジェクトの構成(今回のファイル)
- クラス関係(この記事で登場する範囲)
- コードを全部見てみよう
- ポイント解説(①〜⑧)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
ポイント一覧
- ①
Canvasの Render Mode とCanvasScalerで解像度を設定する - ②
Button.onClick.AddListenerとSceneManager.LoadSceneでシーン遷移する - ③ DOTween でゲームオーバーテキストをアニメーションさせる
- ④
JsonUtilityとPlayerPrefsでアイテムデータを端末に保存する - ⑤
Time.timeScale = 0でポーズ、= 1で再開する - ⑥
Instantiateで ItemButton スロットを量産しToggleと連携する - ⑦
fillAmountとWorldToScreenPointでライフゲージを 3D キャラに追従させる - ⑧
MobileOnlyでスマホ専用 UI をSetActiveで切り替える
題材の範囲
Chapter 8 のテーマは「プレイヤーが見て触れる画面すべて」です。いきのこバトルには 3 つのシーンがあり、それぞれに UI が実装されています。
| 書籍の節 | テーマ | IkinokoBattle での実装 |
|---|---|---|
| 8-1 | タイトル画面 | TitleScene + StartButton.cs + Exit.cs |
| 8-2 | ゲームオーバー画面 | GameOverScene + GameOverTextAnimator.cs(DOTween) |
| 8-3 | アイテムを出現させる | Item.cs・OwnedItemsData.cs・PlayerPrefs + JSON |
| 8-4 | ゲーム画面の UI | Menu.cs・ItemsDialog.cs・ItemButton.cs・LifeGauge.cs |
| 8-5 | スマホ向け UI | MobileOnly.cs・バーチャルパッド |
この記事では 8 ポイントを通じて「ボタンのイベント登録から始まり、画面演出・データ保存・3D キャラへの UI 追従・プラットフォーム対応」まで一通りのUI実装パターンを読み解きます。
プロジェクトの構成(今回のファイル)
| ファイル | 所属シーン | 役割 |
|---|---|---|
StartButton.cs |
TitleScene | ボタンクリックで MainScene へ遷移 |
Exit.cs |
TitleScene | Esc キーでアプリ終了 |
GameOverTextAnimator.cs |
GameOverScene | DOTween でテキスト演出・10 秒後に TitleScene へ |
OwnedItemsData.cs |
全シーン共通 | 所持品を JSON + PlayerPrefs に保存するシングルトン |
Menu.cs |
MainScene | ポーズ・アイテム欄・レシピ欄のハブ |
ItemsDialog.cs |
MainScene | アイテムスロット UI の生成と更新 |
ItemButton.cs |
MainScene | 1 スロット分のアイテム表示・インタラクション |
LifeGauge.cs |
MainScene | HP ゲージを 3D キャラの頭上に追従させる |
CancelButton.cs / OKButton.cs |
MainScene | SE 付きの汎用ボタン |
MobileOnly.cs |
MainScene | スマホ以外では非表示にする |
クラス関係(この記事で登場する範囲)
【TitleScene】
StartButton ─── SceneManager.LoadScene("MainScene")
Exit ─── Application.Quit / EditorApplication.isPlaying = false
【GameOverScene】
GameOverTextAnimator
├─ DOTween:落下 → シェイク
└─ DOVirtual.DelayedCall(10) → SceneManager.LoadScene("TitleScene")
【MainScene】
Menu
├─ Pause() → Time.timeScale = 0 / pausePanel.SetActive(true)
├─ Resume() → Time.timeScale = 1 / pausePanel.SetActive(false)
└─ ToggleItemsDialog() → ItemsDialog.Toggle()
└─ ItemButton × 15
└─ OwnedItem プロパティ ← OwnedItemsData.Instance.OwnedItems
LifeGaugeContainer(シングルトン)
└─ LifeGauge × n(MobStatus ごとに 1 つ)
└─ fillAmount / WorldToScreenPoint で追従コードを全部見てみよう
StartButton.cs
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
[RequireComponent(typeof(Button))]
public class StartButton : MonoBehaviour
{
private void Start()
{
var button = GetComponent<Button>();
button.onClick.AddListener(() =>
{
SceneManager.LoadScene("MainScene");
});
}
}Exit.cs
using UnityEngine;
public class Exit : MonoBehaviour
{
[SerializeField] private KeyCode exitKey = KeyCode.Escape;
private void Update()
{
if (Input.GetKeyDown(exitKey))
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#elif UNITY_STANDALONE
Application.Quit();
#endif
}
}
}GameOverTextAnimator.cs
using DG.Tweening;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameOverTextAnimator : MonoBehaviour
{
private void Start()
{
var transformCache = transform;
var defaultPosition = transformCache.localPosition; // 終点を保存
transformCache.localPosition = new Vector3(0, 300f); // 上方に移動
transformCache.DOLocalMove(defaultPosition, 1f) // 1 秒で元の位置へ落下
.SetEase(Ease.Linear)
.OnComplete(() =>
{
transformCache.DOShakePosition(1.5f, 100); // シェイク
});
DOVirtual.DelayedCall(10, () =>
{
SceneManager.LoadScene("TitleScene"); // 10 秒後にタイトルへ
});
}
}Menu.cs
using UnityEngine;
using UnityEngine.UI;
public class Menu : MonoBehaviour
{
[SerializeField] private ItemsDialog itemsDialog;
[SerializeField] private Button pauseButton;
[SerializeField] private GameObject pausePanel;
[SerializeField] private Button resumeButton;
[SerializeField] private Button itemsButton;
[SerializeField] private Button recipeButton;
private void Start()
{
pausePanel.SetActive(false);
pauseButton.onClick.AddListener(Pause);
resumeButton.onClick.AddListener(Resume);
itemsButton.onClick.AddListener(ToggleItemsDialog);
recipeButton.onClick.AddListener(ToggleRecipeDialog);
}
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() { /* TODO */ }
}ItemsDialog.cs
using UnityEngine;
public class ItemsDialog : MonoBehaviour
{
[SerializeField] private int buttonNumber = 15;
[SerializeField] private ItemButton itemButton;
private ItemButton[] _itemButtons;
private void Start()
{
gameObject.SetActive(false);
for (var i = 0; i < buttonNumber - 1; i++)
{
Instantiate(itemButton, transform);
}
_itemButtons = GetComponentsInChildren<ItemButton>();
}
public void Toggle()
{
gameObject.SetActive(!gameObject.activeSelf);
if (gameObject.activeSelf)
{
for (var i = 0; i < buttonNumber; i++)
{
_itemButtons[i].OwnedItem = OwnedItemsData.Instance.OwnedItems.Length > i
? OwnedItemsData.Instance.OwnedItems[i]
: null;
}
}
}
}ItemButton.cs(抜粋)
public OwnedItemsData.OwnedItem OwnedItem
{
get { return _ownedItem; }
set
{
_ownedItem = value;
var isEmpty = null == _ownedItem;
image.gameObject.SetActive(!isEmpty);
number.gameObject.SetActive(!isEmpty);
_button.interactable = !isEmpty;
if (!isEmpty)
{
image.sprite = itemSprites.First(x => x.itemType == _ownedItem.Type).sprite;
number.text = "" + _ownedItem.Number;
}
}
}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 camera, MobStatus status)
{
_parentRectTransform = parentRectTransform;
_camera = camera;
_status = status;
Refresh();
}
private void Refresh()
{
fillImage.fillAmount = _status.Life / _status.LifeMax;
var screenPoint = _camera.WorldToScreenPoint(_status.transform.position);
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_parentRectTransform, screenPoint, null, out localPoint);
transform.localPosition = localPoint + new Vector2(0, 80);
}
}MobileOnly.cs
using UnityEngine;
public class MobileOnly : MonoBehaviour
{
private void Start()
{
if (Application.platform != RuntimePlatform.Android
&& Application.platform != RuntimePlatform.IPhonePlayer)
gameObject.SetActive(false);
}
}① Canvas の Render Mode と CanvasScaler で解像度を設定する
Canvas とは
Unity の uGUI(UI システム)はすべて Canvas というコンポーネントの上に乗ります。Text・Image・Button など UI 要素は Canvas の子として配置します。
Render Mode の 3 種類
| Render Mode | 特徴 | 使いどころ |
|---|---|---|
| Screen Space - Overlay | 常に画面最前面に描画。3D オブジェクトより必ず手前になる | HUD・メニュー・ダイアログ(IkinokoBattle でも使用) |
| Screen Space - Camera | 指定カメラの前に描画。カメラのエフェクトが UI に適用される | カメラ演出と UI を連動させたいとき |
| World Space | 3D 空間内に UI を置く。キャラの頭上に浮かぶ名前ラベルなど | ワールド空間内 UI |
CanvasScaler で解像度を統一する
Canvas に自動でアタッチされる CanvasScaler で「どの解像度を基準に UI を設計するか」を決めます。
| UI Scale Mode | 意味 |
|---|---|
| Constant Pixel Size | ピクセル数固定。解像度が変わると UI の見た目サイズも変わる |
| Scale With Screen Size(推奨) | 基準解像度(例:1920×1080)からの比率でスケーリング。異なる解像度でも同じ見た目を保てる |
| Constant Physical Size | 物理サイズ(cm 等)で固定。スマホの DPI 対応に使う |
IkinokoBattle では Scale With Screen Size で Reference Resolution 1920 × 1080 を設定することで、PC・タブレット・スマホで UI の比率が崩れないようにしています。
② Button.onClick.AddListener と SceneManager.LoadScene でシーン遷移する
onClick.AddListener によるイベント登録
StartButton.cs の Start() を見ます。
var button = GetComponent<Button>();
button.onClick.AddListener(() =>
{
SceneManager.LoadScene("MainScene");
});Button.onClick は UnityEvent 型のフィールドです。AddListener に「ボタンが押されたときに実行するメソッド(またはラムダ式)」を登録します。Inspector 上でボタンイベントを設定する方法もありますが、コードで登録することで「このボタンが何をするか」をスクリプトを読むだけで把握できます。
SceneManager.LoadScene のシーン名指定
SceneManager.LoadScene("MainScene");引数はビルド設定に追加済みのシーン名(または番号)です。File → Build Settings → Scenes In Build に対象シーンをドラッグして追加しておかないとビルド後に動きません。
Exit.cs のプラットフォーム分岐
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#elif UNITY_STANDALONE
Application.Quit();
#endif#if / #elif / #endif はコンパイル時に条件分岐するプリプロセッサディレクティブです。エディタ上での実行(UNITY_EDITOR)とスタンドアロンビルド(UNITY_STANDALONE)で終了処理を切り替えています。Application.Quit() はエディタ上では動作しないため、この分岐が必要です。
③ DOTween でゲームオーバーテキストをアニメーションさせる
GameOverTextAnimator.cs は DOTween(人気のアニメーションライブラリ)を使っています。
アニメーションの流れ
// ① 上方(y=300)に瞬間移動
transformCache.localPosition = new Vector3(0, 300f);
// ② 1 秒かけて元の位置まで落下
transformCache.DOLocalMove(defaultPosition, 1f)
.SetEase(Ease.Linear)
.OnComplete(() =>
{
// ③ 落下完了後にシェイク(1.5 秒・強度 100)
transformCache.DOShakePosition(1.5f, 100);
});
// ④ 10 秒後にタイトルへ遷移(Coroutine 不要)
DOVirtual.DelayedCall(10, () =>
{
SceneManager.LoadScene("TitleScene");
});DOTween の主なメソッド
| メソッド | 内容 |
|---|---|
DOLocalMove(終点, 秒数) |
ローカル座標を指定秒数でアニメーション移動 |
SetEase(Ease.xxx) |
イージング(加減速パターン)を指定 |
OnComplete(callback) |
アニメーション完了時にコールバックを実行 |
DOShakePosition(秒数, 強度) |
指定秒数・強度でランダムに揺らす |
DOVirtual.DelayedCall(秒数, callback) |
指定秒数後にコールバックを実行(Coroutine 代替) |
Coroutine との比較
WaitForSeconds を使う Coroutine でも同じ結果を得られますが、DOTween はメソッドチェーンで記述できるためコードが短くまとまります。また DOShakePosition のような複雑な動きを数値だけで実現できるのも強みです。
注意:DOTween は
Time.timeScale = 0(ポーズ中)の影響を受けます。ポーズ中でも動作させたい場合は.SetUpdate(true)を追加します。
④ JsonUtility と PlayerPrefs でアイテムデータを端末に保存する
所持品データの保存は OwnedItemsData.cs が担当します(詳細は OOP設計パターン編・第1回 でも解説しています)。ここでは Chapter 8 の「JSON でデータを保存する」という文脈で要点を整理します。
保存の流れ
OwnedItemsData.Save()
└─ JsonUtility.ToJson(this)
└─ PlayerPrefs.SetString("OWNED_ITEMS_DATA", jsonString)
└─ PlayerPrefs.Save() ← ディスクに書き込み読み込みの流れ
OwnedItemsData.Instance(初回アクセス時)
└─ PlayerPrefs.HasKey("OWNED_ITEMS_DATA") ?
├─ YES → JsonUtility.FromJson<OwnedItemsData>(jsonString)
└─ NO → new OwnedItemsData()(空データ)[Serializable] と [SerializeField] が必要な理由
[Serializable]
public class OwnedItemsData
{
[SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();
...
[Serializable]
public class OwnedItem
{
[SerializeField] private Item.ItemType type;
[SerializeField] private int number;
}
}JsonUtility は [Serializable] がついたクラスしか JSON 変換できません。また private フィールドは通常シリアライズ対象外ですが、[SerializeField] を付けることで JSON(および Inspector)に含まれます。カプセル化を維持しながらデータを保存できる組み合わせです。
⑤ Time.timeScale = 0 でポーズ、= 1 で再開する
private void Pause()
{
Time.timeScale = 0; // 時間を止める
pausePanel.SetActive(true);
}
private void Resume()
{
Time.timeScale = 1; // 通常速度に戻す
pausePanel.SetActive(false);
}Time.timeScale とは
Time.timeScale はゲーム内の時間の進み具合を制御するグローバルな値です。
| 値 | 効果 |
|---|---|
1 |
通常速度 |
0 |
完全停止(物理・アニメーション・Time.deltaTime がすべて 0 になる) |
0.5 |
スローモーション(子弾時間など) |
2 |
2 倍速 |
Time.timeScale = 0 にすると Time.deltaTime が 0 になるため、deltaTime を使っている移動・重力・アニメーションがすべて止まります。IkinokoBattle の PlayerController・EnemyMove・NavMeshAgent は全て Time.deltaTime に依存しているため、1 行で全員を止められます。
ポーズ中の UI は動く
ポーズパネル(pausePanel)の Button クリックなど uGUI の操作は Time.timeScale の影響を受けません。「ゲームは止まっているがメニューは触れる」という状態が自然に実現します。
注意:DOTween はデフォルトで
timeScaleの影響を受けます。ポーズ中も動かしたい演出(フェードなど)には.SetUpdate(true)を追加してください。
⑥ Instantiate で ItemButton スロットを量産し Toggle と連携する
スロットを Instantiate で量産する
private void Start()
{
gameObject.SetActive(false);
for (var i = 0; i < buttonNumber - 1; i++)
{
Instantiate(itemButton, transform); // Prefab を複製して自分の子に追加
}
_itemButtons = GetComponentsInChildren<ItemButton>();
}buttonNumber = 15 のとき、最初から配置済みの 1 つと合わせて合計 15 個の ItemButton が生成されます。GetComponentsInChildren<ItemButton>() で全子要素の ItemButton をまとめて配列取得してキャッシュします。
開閉時にデータを同期する
public void Toggle()
{
gameObject.SetActive(!gameObject.activeSelf);
if (gameObject.activeSelf)
{
for (var i = 0; i < buttonNumber; i++)
{
_itemButtons[i].OwnedItem = OwnedItemsData.Instance.OwnedItems.Length > i
? OwnedItemsData.Instance.OwnedItems[i]
: null;
}
}
}ダイアログを開いた瞬間だけ OwnedItemsData から最新データを読み込みます。常に Update で同期するより、「開いたとき 1 回だけ更新」の方がパフォーマンス的に効率的です。
ItemButton.OwnedItem プロパティセッターでの自動反映
set
{
_ownedItem = value;
var isEmpty = null == _ownedItem;
image.gameObject.SetActive(!isEmpty);
number.gameObject.SetActive(!isEmpty);
_button.interactable = !isEmpty;
if (!isEmpty)
{
image.sprite = itemSprites.First(x => x.itemType == _ownedItem.Type).sprite;
number.text = "" + _ownedItem.Number;
}
}データを OwnedItem にセットするだけで、アイコン画像・個数テキスト・ボタンの有効/無効がすべて自動更新されます。UI 更新ロジックをプロパティのセッターに集約することで、呼び出し側は「データを渡すだけ」で済みます。
⑦ fillAmount と WorldToScreenPoint でライフゲージを 3D キャラに追従させる
fillAmount で HP バーを表現する
fillImage.fillAmount = _status.Life / _status.LifeMax;Image コンポーネントの Image Type を Filled、Fill Method を Horizontal に設定すると、fillAmount(0.0〜1.0)で横方向の塗りつぶし量を制御できます。HP が半分なら 0.5、全快なら 1.0、ゼロなら 0.0 です。
3D 座標を UI 座標に変換する 2 ステップ
3D キャラクターの頭上に UI ゲージを追従させるには、ワールド座標 → スクリーン座標 → Canvas のローカル座標 の 2 段変換が必要です。
// ステップ 1:ワールド座標 → スクリーン座標
var screenPoint = _camera.WorldToScreenPoint(_status.transform.position);
// ステップ 2:スクリーン座標 → Canvas ローカル座標
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_parentRectTransform, screenPoint, null, out localPoint);
// キャラに被らないよう 80px 上にずらす
transform.localPosition = localPoint + new Vector2(0, 80);| 変換 | メソッド | 結果 |
|---|---|---|
| ワールド → スクリーン | Camera.WorldToScreenPoint |
ピクセル座標(左下原点) |
| スクリーン → Canvas ローカル | RectTransformUtility.ScreenPointToLocalPointInRectangle |
Canvas 内のローカル座標 |
3 番目の引数が null の理由
ScreenPointToLocalPointInRectangle の第 3 引数はカメラです。Canvas の Render Mode が Screen Space - Overlay のときは null(カメラ不要)を渡します。Screen Space - Camera の場合は対象カメラを渡す必要があります。
Update で毎フレーム更新する理由
敵は毎フレーム移動するため、ゲージも毎フレーム位置を再計算する必要があります。LifeGauge.Update() が毎フレーム Refresh() を呼ぶことで、ゲージがキャラクターにぴったり追従します。
⑧ MobileOnly でスマホ専用 UI を SetActive で切り替える
private void Start()
{
if (Application.platform != RuntimePlatform.Android
&& Application.platform != RuntimePlatform.IPhonePlayer)
gameObject.SetActive(false);
}Application.platform は現在の実行環境を返す列挙型です。Android でも iOS でもない場合(PC エディタ・PC ビルド)は自分自身を SetActive(false) で非表示にします。
Application.platform の主な値
| 値 | 実行環境 |
|---|---|
RuntimePlatform.WindowsEditor |
Unity エディタ(Windows) |
RuntimePlatform.WindowsPlayer |
Windows ビルド |
RuntimePlatform.Android |
Android 端末 |
RuntimePlatform.IPhonePlayer |
iOS 端末 |
バーチャルパッド(仮想スティック)はスマホでのみ必要で、PC では不要(キーボードで操作)です。MobileOnly コンポーネントを付けた GameObject は PC では表示されず、Android/iOS では表示されます。
シンプルさの価値
MobileOnly.cs はわずか 6 行ですが「プラットフォーム判定の責任はここに集中している」という設計が重要です。UI の GameObject を増やすたびに条件分岐を書く必要がなく、MobileOnly コンポーネントを付けるだけで済みます。
コードの流れを整理しよう
タイトルシーン〜メインシーンへの遷移
TitleScene 起動
└─ StartButton.Start()
└─ onClick.AddListener( SceneManager.LoadScene("MainScene") )
↓ ボタンクリック
SceneManager.LoadScene("MainScene") → MainScene へゲームオーバーシーンの演出フロー
GameOverScene 起動
└─ GameOverTextAnimator.Start()
├─ localPosition を (0, 300) に瞬間移動
├─ DOLocalMove → 1 秒で落下アニメーション
│ └─ OnComplete → DOShakePosition(1.5 秒シェイク)
└─ DOVirtual.DelayedCall(10) → SceneManager.LoadScene("TitleScene")アイテムダイアログの開閉フロー
Menu.Start()
└─ itemsButton.onClick.AddListener(ToggleItemsDialog)
ユーザーがアイテムボタンをクリック
└─ Menu.ToggleItemsDialog()
└─ ItemsDialog.Toggle()
└─ gameObject.SetActive(!activeSelf)
↑ 開いたとき:
└─ for i in 0..14:
_itemButtons[i].OwnedItem = OwnedItemsData.Instance.OwnedItems[i]
└─ プロパティセッター:
├─ image.sprite 更新
├─ number.text 更新
└─ button.interactable 更新ライフゲージの追従フロー
MobStatus.Start()(MobStatus を持つ全キャラ)
└─ LifeGaugeContainer.Instance.Add(this)
└─ Instantiate(lifeGaugePrefab) → LifeGauge 生成
└─ Initialize(rectTransform, camera, status)
LifeGauge.Update()(毎フレーム)
└─ Refresh()
├─ fillImage.fillAmount = Life / LifeMax
├─ WorldToScreenPoint(status.transform.position)
├─ ScreenPointToLocalPointInRectangle → localPoint
└─ transform.localPosition = localPoint + (0, 80)自分でカスタマイズしてみよう!
課題 A:ポーズ画面にフェードを追加する
現在はポーズパネルがパッと表示/非表示になります。DOTween で透明度をアニメーションさせると演出が自然になります。
using DG.Tweening;
using UnityEngine;
// pausePanel に CanvasGroup コンポーネントを追加してから使う
private void Pause()
{
Time.timeScale = 0;
pausePanel.SetActive(true);
pausePanel.GetComponent<CanvasGroup>()
.DOFade(1f, 0.3f)
.SetUpdate(true); // timeScale = 0 でも動くように
}
private void Resume()
{
pausePanel.GetComponent<CanvasGroup>()
.DOFade(0f, 0.3f)
.SetUpdate(true)
.OnComplete(() =>
{
Time.timeScale = 1;
pausePanel.SetActive(false);
});
}課題 B:ライフゲージの色を HP に応じて変化させる
HP が少なくなると赤くなる警告表現を追加します。
private void Refresh()
{
var ratio = _status.Life / _status.LifeMax;
fillImage.fillAmount = ratio;
// HP 30% 以下で赤、50% 以下で黄色、それ以上は緑
fillImage.color = ratio <= 0.3f ? Color.red
: ratio <= 0.5f ? Color.yellow
: Color.green;
// 座標更新(省略)
}課題 C:スロット数をアイテム種類数に合わせて自動調整する
現在 buttonNumber = 15 は固定値です。Item.ItemType の列挙子の数に合わせて自動化できます。
private void Start()
{
// アイテムの種類数を列挙型から自動取得
buttonNumber = System.Enum.GetValues(typeof(Item.ItemType)).Length;
gameObject.SetActive(false);
for (var i = 0; i < buttonNumber - 1; i++)
{
Instantiate(itemButton, transform);
}
_itemButtons = GetComponentsInChildren<ItemButton>();
}これで Item.ItemType に新しいアイテムを追加するだけで、スロット数が自動的に増えます。
まとめ
| ポイント | キーワード |
|---|---|
① CanvasScaler の Scale With Screen Size で解像度差を吸収する |
Reference Resolution |
② onClick.AddListener でボタンイベントをコードで登録し、LoadScene で遷移する |
UnityEvent・SceneManager |
| ③ DOTween のメソッドチェーンで落下・シェイク・遅延実行を簡潔に記述する | DOLocalMove・DOShakePosition・DOVirtual |
④ [Serializable] + JsonUtility + PlayerPrefs で永続化する |
シリアライズ・ToJson・FromJson |
⑤ Time.timeScale = 0 で全オブジェクトを止め、= 1 で再開する |
グローバルな時間制御 |
⑥ Instantiate でスロットを量産し、開くときだけデータ同期する |
Prefab 複製・GetComponentsInChildren |
⑦ WorldToScreenPoint → ScreenPointToLocalPointInRectangle の 2 段変換でゲージを追従 |
3D→UI 座標変換 |
⑧ Application.platform でスマホ判定し SetActive(false) で非表示 |
プラットフォーム分岐 |
Chapter 8 の UI はシーンごとの役割が明確で、「タイトル(開始)」→「メイン(プレイ)」→「ゲームオーバー(演出)」という流れを複数のシンプルなスクリプトが分業して支えています。次回 Chapter 9 では BGM・SE・パーティクル・ポストプロセスによる演出を読み解きます。
← シリーズ目次に戻る
← 前の記事:Chapter 7 敵キャラクターを作って動きを付けよう
次の記事 → 第5回:Chapter 9 ゲームが楽しくなる効果を付けよう
最終更新:2026年4月