【Unity】検索機能付きアイテムリストの作り方

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unityで検索機能付きアイテムリスト作った <a href="https://t.co/6ZnfTiFiEu">pic.twitter.com/6ZnfTiFiEu</a></p>&mdash; 原口昂弥 (@k_haraguchi55) <a href="https://twitter.com/k_haraguchi55/status/1397865064376410116?ref_src=twsrc%5Etfw">May 27, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
# 開発環境
Unity 2019.4.1f1
Windows 10

# 準備
アイテムリストの子に以下のUIを設置する必要があります。

+ 次ページボタン(Button)
+ 前ページボタン(Button)
+ アイテムボタン複数(Button)
+ ページ付け表示用のテキスト(Text)
+ 検索欄(Input Field)

デザインや配置は自由に決めてしまって大丈夫です。

# スクリプト
## アイテム
``` C#:Item.cs
public class Item
{
    public Item(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }
    public void Use()
    {
        Debug.Log($"{this.Name}を使った！");
    }
}
```
今回はサンプルとしてこのクラスを使用します。
名前とUse()関数が定義されていて使用するとDebug.Logに名前を表示します。

## アイテムリスト
```C#:ItemListView.cs
public class ItemListView : MonoBehaviour
{
    [SerializeField] private Button[] itemButtons = default;
    [SerializeField] private GameObject nextPageButton = default;
    [SerializeField] private GameObject prevPageButton = default;
    [SerializeField] private InputField searchInput = default;
    [SerializeField] private Text pagination = default;
    private IEnumerable<Item> items;
    private List<Item> showItems = new List<Item>();
    private int page = 1;

    private int MaxPage => (showItems.Count / itemButtons.Length) + 1;
    public void SetItems(IEnumerable<Item> items)
    {
        this.items = items;
    }

    public void Open()
    {
        showItems.AddRange(items);
        UpdateDisplay();
        gameObject.SetActive(true);
    }

    public void Close()
    {
        searchInput.text = "";
        this.page = 1;
        showItems.Clear();
        gameObject.SetActive(false);
    }

    public void NextPage()
    {
        this.page++;
        UpdateDisplay();
    }

    public void PrevPage()
    {
        this.page--;
        UpdateDisplay();
    }

    public void Search()
    {
        var keyward = searchInput.text;
        showItems.Clear();
        var compareInfo = CultureInfo.CurrentCulture.CompareInfo;
        var resultItems = items.Where(i => 0 <= compareInfo.IndexOf(i.Name, keyward, CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreCase));
        showItems.AddRange(resultItems);
        this.page = 1;
        UpdateDisplay();
    }

    private void UpdateDisplay()
    {
        UpdateItemButtons();
        UpdatePageButtons();
        UpdatePagination();
    }

    private void UpdateItemButtons()
    {
        int baseIndex = (page - 1) * itemButtons.Length;
        for (int i = 0; i < itemButtons.Length; i++)
        {
            var itemButton = itemButtons[i];
            itemButton.onClick.RemoveAllListeners();
            if (i + baseIndex >= showItems.Count)
            {
                itemButtons[i].gameObject.SetActive(false);
                continue;
            }

            var item = showItems[i + baseIndex];
            itemButton.GetComponentInChildren<Text>().text = item.Name;
            itemButton.onClick.AddListener(item.Use);
            itemButton.gameObject.SetActive(true);
        }
    }

    private void UpdatePageButtons()
    {
        if (page > 1)
        {
            this.prevPageButton.SetActive(true);
        }
        else
        {
            this.prevPageButton.SetActive(false);
        }

        if (page < MaxPage)
        {
            this.nextPageButton.SetActive(true);
        }
        else
        {
            this.nextPageButton.SetActive(false);
        }

    }

    private void UpdatePagination()
    {
        if (showItems.Count == 0)
        {
            this.pagination.text = "0/0";
        }

        this.pagination.text = $"{page}/{MaxPage}";
    }
}
```
今回のメインとなるクラスです。
一つ一つ解説していきます。

### List\<Item\> showItems
表示するアイテムをこのリストに追加します。
最初は全アイテムをこのリストに入れます。
検索時は条件に合ったアイテムだけこのリストに入れます。

### SetItems(IEnumerable\<Item\> items)
アイテムを受け取る用の関数です。
最初にこの関数を呼び出す必要があります。

リストや配列ではなくIEnumerableインターフェイスで受け取ることでコレクションを変更しないことを呼び出し側に明示しています。

### Open()
アイテムリストを表示する用の関数です。
Setitemsの後に呼び出されることを想定しています。

保持しているアイテムを全部showItemsに追加した後、表示を更新しています。

### Close()
アイテムリストを閉じるときに使用する関数です。

ページや検索欄を初期化した後アイテムリストを非表示にしています。

### NextPage()
次ページボタンから呼び出します。

ページを一つ進めています。

### PrevPage()
前ページボタンから呼び出します。

ページを一つ戻しています。

### Search()
検索用InputFieldから呼び出します。

処理の流れ

+ InputFieldからキーワードを取り出す。
+ 名前にキーワードを含むアイテムだけを取り出す。
+ 取り出したアイテムをshowItemsに追加。
+ 表示を更新。

キーワードが含まれているかどうかの判定は半角/全角、カタカナ/ひらがな、大文字/小文字を無視して行っています。
その判定にCompareInfoクラスのIndexOfメソッドを使用しています。

[CompaleInfo](https://docs.microsoft.com/ja-jp/dotnet/api/system.globalization.compareinfo?view=net-5.0)

### UpdateDisplay()
アイテムボタンとページ付けとページ切り替え用のボタンの表示を更新しています。

### UpdateItemButtons()
アイテムボタンを更新します。

処理の流れ

+ ページ-1とアイテムボタンの数をかけて前のページまでに表示したアイテム数を取り出す。
+ ループでアイテムボタンを取り出す。
+ 既に表示したアイテム数+1で表示するべきアイテムを取り出す。
+ 表示していないアイテムがなかった場合はアイテムボタンを非表示にする。
+ アイテムを取り出せた場合はアイテムボタンのテキストをアイテムの名前に変更+クリック時にItem.Use()を実行するように変更

### UpdatePageButtons()
ページ切り替え用ボタンの表示/非表示を切り替えます。

現在のページが1なら前ページを非表示
2ページ移行ならなら表示

現在のページが最大ページより低いなら次ページを表示
そうでないなら非表示

### UpdatePagination()
ページ付けを更新します。

現在のページ/最大ページを表示
もし、表示すべきアイテムがないなら0/0を表示

## テスト用スクリプト
``` C#:Test.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour
{
    [SerializeField] ItemListView itemListView = default;

    void Start()
    {
        var items = new List<Item>()
        {
            new Item("やくそう"),
            new Item("すごいやくそう"),
            new Item("せいすい"),
            new Item("すごいせいすい"),
            new Item("ワープヒモ"),
            new Item("薬")
        };

        itemListView.SetItems(items);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            if (itemListView.gameObject.activeSelf)
                itemListView.Close();
            else
                itemListView.Open();
        }
    }
}
```
# 使い方
## アイテムリストUI
+ アイテムリストUIにItemListViewをアタッチ
+ インスペクターから各種フィールドを設定
+ アクティブをオフ

## 検索欄(InputField)
インスペクターからInputField>OnValueChangedにItemListView.Search()を設定

## 前ページボタン
インスペクターからButton>OnClickにItemListView.PervPage()を設定

## 次ページボタン
インスペクターからButton>OnClickにItemListView.NextPage()を設定

# テスト
Test.csを適当なオブジェクトにアタッチ
※アクティブがオンである必要があります

ゲームを実行してEscでアイテムリストが表示されます。
もう一度Escを押すと閉じます。