宝くじの選択画面のような複数の数字を選べるボタンを実装。
エディタ拡張により、ワンボタンで設置可能。
設置で一時的にGridLayoutGroupを利用するので、設置方向や一列の個数の指定も出来ます。
サンプル動画
- 前半: 通常パターン
- 後半: 角丸が際立つImageType: Slicedパターン
使用例
ドロップダウンと組み合わせ、戦術ゲームでの船団への命令に使用。
(縦画面)
*(FはFlagship)
画像素材
- ライセンス: CC0
インポート手順
- (2Dモードの場合は、そのままでOk)
- 3Dモードの場合は、D&Dしてインポートした画像をProjectウィンドウで選択。
- インスペクター -> Texture Type: Sprite (2D and UI)に変更。
- Apply
コード
NumberSelecterEditor
NumberSelecterのインスペクターにCreateボタンを出す為のエディタ拡張。
- 新規スクリプトを作成、「NumberSelecterEditor」と命名。
(エディタ拡張なので、Editorフォルダに入れておく) - 以下のコードをコピペ。
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(NumberSelecter))]
public class NumberSelecterEditor : Editor
{
NumberSelecter numberSelecter;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (numberSelecter == null)
numberSelecter = target as NumberSelecter;
if (GUILayout.Button("Create")){
numberSelecter.CreateItems();
}
}
}
NumberSelecter
- 先にTextMeshProをインポートしておく。
- 新規スクリプトを作成、「NumberSelecter」と命名。
- 以下のコードをコピペ。
- 適当なCanvas下に空のオブジェクトを作成し、NumberSelecterをアタッチ。
- インポートした画像素材を、インスペクターからbtnSpriteへ紐付け。
- 各種項目を調整し、Createボタンをクリック。
(フォントは黒のアウトラインがある設定を推奨) - selectedItemIndexesから選択された数字ボタンのインデックスが読み取れる。
ImageType: Slicedにする場合 (より角丸になる)
- インポートした画像をProjectウィンドウで選択。
- インスペクター -> Sprite Editorを選択。
(3Dモードの場合は、PackageManagerで2D Spriteをインストールする必要あり) - Border -> L, T, R, B: 25程度
- Apply
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class NumberSelecter : MonoBehaviour
{
[Header("数字ボタン数")]
[Range(1, 100)]
public int itemMax = 10;
[Header("ボタン用の画像をインポートして紐付けしておく")]
[SerializeField]
Sprite btnSprite;
[HideInInspector]
[SerializeField]
Button[] btns;
[HideInInspector]
[SerializeField]
Image[] imgs;
[HideInInspector]
[SerializeField]
TextMeshProUGUI[] tmps;
[Header("数字ボタンが選択されている時の色")]
[SerializeField]
Color32 enabledColor = new Color32(255, 255, 255, 255);
[Header("数字ボタンが選択されていない時(デフォルト状態)の色")]
[SerializeField]
Color32 disabledColor = new Color32(200, 200, 200, 128);
//現在選択している数字ボタンのインデックス集。
[HideInInspector]
public List<int> selectedItemIndexes;
[Header("数字ボタンの文字のフォント")]
[SerializeField]
TMP_FontAsset font;
[Header("数字ボタンの文字の大きさ")]
[SerializeField]
[Range(32, 128)]
int fontSize = 64;
//数字を扱うのみなら、index + 1をStringに変換するパターンにしても良い。
[Header("各数字ボタンの文字")]
[SerializeField]
string[] tmpTexts = new string[] {
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
};
[Header("幅を固定する方向 (Column: 縦, Row: 横)")]
[SerializeField]
Constraint constraint = Constraint.FixedRowCount;
enum Constraint {
FixedColumnCount,
FixedRowCount,
}
[Header("幅を固定する値 (1列の個数)")]
[SerializeField]
[Range(1, 10)]
int constraintCount = 5;
//SpriteEditorでボーダーを適切に設定する必要があるが、Slicedにすれば、より角が丸くなる。
[Header("画像タイプ")]
[SerializeField]
ImageType imageType;
enum ImageType {
Simple,
Sliced,
}
[Header("数字ボタンのサイズ")]
[SerializeField]
Vector2 size = new Vector2(150, 150);
[Header("数字ボタン間のマージン")]
[SerializeField]
Vector2 spacing = new Vector2(10, 10);
//一時的に使用。
GameObject tempGo;
GameObject tempGo2;
TextMeshProUGUI tempTmp;
ContentSizeFitter tempContentSizeFitter;
GridLayoutGroup tempGridLayoutGroup;
RectTransform tempRt;
void Awake()
{
//引数ありのボタンリスナー登録は事前に出来ないので、初期化時に行う。
for (int i = 0; i < itemMax; i++) {
//リスナーの仕様上、ローカル変数を噛まさないとバグる。
int index = i;
btns[index].onClick.AddListener(() => ButtonListenerSelectedItem(index));
}
}
//全選択を解除したい時に呼ぶ。
public void Reset()
{
for (int i = 0; i < selectedItemIndexes.Count; i++) {
imgs[selectedItemIndexes[i]].color = disabledColor;
tmps[selectedItemIndexes[i]].color = disabledColor;
}
selectedItemIndexes.Clear();
}
//ボタンリスナー用メソッド。
void ButtonListenerSelectedItem(int index)
{
if (!selectedItemIndexes.Contains(index)) {
selectedItemIndexes.Add(index);
imgs[index].color = enabledColor;
tmps[index].color = enabledColor;
} else {
selectedItemIndexes.Remove(index);
imgs[index].color = disabledColor;
tmps[index].color = disabledColor;
}
}
//Createボタンを押した時に実行される、数字ボタンの生成。
public void CreateItems()
{
selectedItemIndexes = new List<int>(itemMax);
btns = new Button[itemMax];
tmps = new TextMeshProUGUI[itemMax];
imgs = new Image[itemMax];
tempContentSizeFitter = gameObject.AddComponent<ContentSizeFitter>();
tempContentSizeFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize;
tempContentSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
tempGridLayoutGroup = gameObject.AddComponent<GridLayoutGroup>();
tempGridLayoutGroup.cellSize = size;
tempGridLayoutGroup.spacing = spacing;
if (constraint == Constraint.FixedColumnCount) {
tempGridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
} else {
tempGridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedRowCount;
}
for (int i = 0; i < itemMax; i++) {
tempGo = new GameObject(System.String.Format("NumberSelecterItem{0}", i + 1));
tempGo.transform.parent = transform;
tempGo.transform.localScale = Vector3.one;
tempGo.AddComponent<RectTransform>();
imgs[i] = tempGo.AddComponent<Image>();
imgs[i].color = disabledColor;
imgs[i].sprite = btnSprite;
if (imageType == ImageType.Simple) {
imgs[i].type = Image.Type.Simple;
} else {
imgs[i].type = Image.Type.Sliced;
}
btns[i] = tempGo.AddComponent<Button>();
tempGo2 = new GameObject("Tmp");
tempGo2.transform.parent = tempGo.transform;
tempGo2.transform.localScale = Vector3.one;
tempGo2.AddComponent<RectTransform>();
tmps[i] = tempGo2.AddComponent<TextMeshProUGUI>();
if (font != null)
tmps[i].font = font;
tmps[i].fontSize = fontSize;
tmps[i].enableWordWrapping = false;
tmps[i].alignment = TextAlignmentOptions.Center;
tmps[i].color = disabledColor;
tmps[i].text = tmpTexts[i];
tmps[i].raycastTarget = false;
tmps[i].richText = false;
}
StartCoroutine(DestroyLayoutGroup());
}
//ContentSizeFitterとGridLayoutGroupを削除するタイミングが早過ぎて、sizeDeltaが0になってしまうのを防ぐ為のディレイ。
IEnumerator DestroyLayoutGroup()
{
yield return null;
yield return null;
DestroyImmediate(tempContentSizeFitter);
DestroyImmediate(tempGridLayoutGroup);
}
}