学習記事一覧 · Unity

【学習】Unity:アイテム所持とセーブ入門(第6回)― 1 マス:ItemButton とスロットの見た目

第5回 までで、OwnedItemsData.Instance追加・保存 までつなげました。

次はそれを 画面に並べて見せる ための、「インベントリの枠・1つ分」の部品です。

書籍準拠のサンプル(章 8-4-3 に相当する教材の流れ)では ItemButton.cs1 アイテム分の見た目(画像・個数・アクティブ切り替え)を担当します。

シリーズ


今日のゴール

  • OwnedItem をセットしたら ImageText が自動で変わる(プロパティの set
  • 空きスロットのときは 画像も個数も隠すButton.interactablefalse
  • ItemType と Sprite[Serializable] なクラス配列でインスペクタから結びつけられる

シーン側のひな形(Prefab 想定)

構成 役割
Button(ルート、ItemButton 付与) クリックの受け皿(※今回は中身 TODO)
子:Image アイテムアイコン
子:Text(Legacy の UnityEngine.UI.Text 想定) 所持数

[RequireComponent(typeof(Button))] により、このスクリプトは ボタン必須です。


コードのブロックごとの説明

教材と 同じ ItemButton を、処理のかたまりごとに分けて読みます。すべてを一度にほしいときは、後半の 完成コード(コピー用) のコードブロックを使ってください。

ブロック①:名前空間と Button 必須

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Button))]
public class ItemButton : MonoBehaviour
  • System / System.Linq … 後ほど [Serializable] クラス記述や First で使う。
  • UnityEngine.UI … Legacy UI の ImageText
  • [RequireComponent(typeof(Button))]このオブジェクトには必ず Button がつく。付け忘れをコンパイル時〜追加時に防ぎます。

ブロック②:OwnedItem プロパティ(心臓部)

インベントリ 1マスぶんのデータ代入すると同時にアイコン・個数・押せるかどうかまで更新します。

    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;
            }
        }
    }
場所 何をしているか
get 裏側の _ownedItem を読むだけ
_ownedItem = value 渡された所持情報(または null)を保持
isEmpty null なら空きスロット。空きなら見た目を消したいので起点にする
image / numberSetActive 空き:アイコンと数字を消す。割当あり:両方見せる。親の Button だけ残し「枠だけのマス」にしやすい
_button.interactable ButtonSetActive とは別に クリック可否がある。空きでは false にして OnClick を防ぐ
First(...).sprite 所持には ItemType(種類の列挙)だけ。Sprite はインスペクタの itemSprites で対応を引く。該当が無いと First は例外。全種並べておく前提
number.text = "" + Number Text.text は文字列。数値は "" +ToString() 相当。読み書き優先なら Number.ToString() でよい

第7回 の一覧側は OwnedItem = ... と代入するだけでよく、個々の Image/Button をさわらなくて済む 役割分担になっています。


ブロック③:インスペクタ用フィールドと非公開フィールド

    [SerializeField] private ItemTypeSpriteMap[] itemSprites; // 各アイテム用の画像を指定するフィールド
    [SerializeField] private Image image;
    [SerializeField] private Text number;
    private Button _button;
    private OwnedItemsData.OwnedItem _ownedItem;
  • [SerializeField] private … コード外からは読めず、Inspector だけ割り当てる定番パターン。
  • itemSprites … 種類ごとの Sprite 対応表。割当が揃っているか確認がポイント。
  • _button_ownedItemAwake/プロパティからのみ触るため private

ブロック④:Awake でボタン取得とクリック登録

    private void Awake()
    {
        _button = GetComponent<Button>();
        _button.onClick.AddListener(OnClick);
    }
  • Awake … オブジェクト初期化。子の参照は Inspector、コンポーネント取得はコード。
  • onClick.AddListener(OnClick) … 実行時に メソッドを購読(UnityEvent)。今回 OnClick は TODO で中身なしでも登録だけは済ませられる。

ブロック⑤:OnClick(この教材では未完)

    private void OnClick()
    {
        // TODO ボタンを押した時の処理はここに書く
    }
  • 後から「使用」「詳細ウィンドウ」などをここにつなぐ想定。空きでは interactable が false のため発火しないようにしてある。

ブロック⑥:ItemTypeSpriteMap

    /// <summary>
    /// アイテムの種類とSpriteをインスペクタで紐付けられるようにするためのクラス
    /// </summary>
    [Serializable]
    public class ItemTypeSpriteMap
    {
        public Item.ItemType itemType;
        public Sprite sprite;
    }
  • [Serializable] + public フィールドItemTypeSpriteMap[][SerializeField] すると、**インスペクタに「要素ごとの行」**が出やすくなる。書籍側の Item.ItemTypeSprite を 1対1 で並べるためのデータ形です。

完成コード(コピー用)

すべてを 1ファイルにしたいときは、このまま差し替えてよい一覧です。

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Button))]
public class ItemButton : MonoBehaviour
{
    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;
            }
        }
    }
    [SerializeField] private ItemTypeSpriteMap[] itemSprites; // 各アイテム用の画像を指定するフィールド
    [SerializeField] private Image image;
    [SerializeField] private Text number;
    private Button _button;
    private OwnedItemsData.OwnedItem _ownedItem;
    private void Awake()
    {
        _button = GetComponent<Button>();
        _button.onClick.AddListener(OnClick);
    }
    private void OnClick()
    {
        // TODO ボタンを押した時の処理はここに書く
    }
    /// <summary>
    /// アイテムの種類とSpriteをインスペクタで紐付けられるようにするためのクラス
    /// </summary>
    [Serializable]
    public class ItemTypeSpriteMap
    {
        public Item.ItemType itemType;
        public Sprite sprite;
    }
}

プログラムの流れ

プログラムの流れ

外部が OwnedItem に値をセット

null
        ↓ Yes → 見た目を消す・押せなくする
        ↓ No
対応 Sprite を検索して Image に反映/個数テキスト更新・押せる

重要ポイント

  • UI の更新ロジックを「どこ?」に置くか … この教材では OwnedItem のプロパティに集約し、呼び出し側は 代入するだけにしています
  • First(x => …) … 種類ごとの画像が itemSprites に並んでいないと ランタイムで例外になります。授業では 並び・抜けをチェックする習慣を付ける
  • OnClick はあえて未完成 … 使う・アイテム説明などは 次の機能として課題化しやすいです

発展アイデア

  • FirstOrDefault + null チェックで、Sprite 未定義でも落ちないようにする
  • UI Toolkit 版に読み替え(VisualElementLabel
  • 押したときOwnedItemsData.Instance.Use とつなぐ(※例外設計との付き合い方は第4〜5回の前提と整合させる)

次に進む

1 個分ができたら、それを Instantiate で増やして **リスト全体を作ります。

第7回:ItemsDialog で一覧と反映