見出し画像

メニューを作る(3)・ウィンドウ表示(Unityメモ)

操作の実装のあたりをつけます。


今回実装したウィンドウ表示。よくある開発中の画面

操作の呼び出し

実装中のGUIでは、ユースケース(シーン)→スロット(ユースケースの引数)→スロットごとの操作とたどっていき、操作を指定してGoを押下すると操作用のウィンドウが開きます。パーティ編成などデータの操作は個々の操作ウィンドウの中に実装する、という方針です。Unityの場合はデータ操作などの動作をコンポーネントに分けて実装できるので、機能追加も比較的行いやすいです。

メニューからウィンドウを開く

メニューのデータ構造にはボタン選択時に実行されるメソッドを登録できます。そのメソッドにウィンドウを開く処理を書けば十分です。

var party_schema = new UISlot {
    //略
    child = new List<UISlot> {
        //略
        new UISlot{asset="unit",
        ope= new List<UIOpe>{
            new UIOpe{ asset="list", proc= ()=>{ UnitListWindow.SetActive(true); } },
            //略
            }
        },
        //略
    }
};

ユニット一覧表示の実装

まずはユニット一覧表示の実装をしました。データ操作と表示を一通り行う操作なので、操作の実装の叩き台としてちょうど良いです。
・データ1件の表示:データ構造の定義、GUIへの引き渡し
・ユニットデータのリスト表示:スクロール領域の実装
・ユニット情報ウィンドウの表示:リストからのデータ1件の取得・ユーザ選択との連携

実装自体は難しくないのですが、問題となるのは「誰が生成の責任を持つのか」という現実世界でもよくあるやつです。
またUnityの場合、「動的に生成する」と一口に言っても、プレハブに仕込んでおく場合とスクリプト上で一から生成する場合を区別する必要があります。
結論としては以下の通りにしました。

  • ユニットのデータをどの段階で取得するか

    • リスト表示ウィンドウが全件を取得

    • DBへのアクセスはリスト表示ウィンドウだけで済ませる

  • ユニット1件のデータをどう渡すか(データベースのIDかインスタンスか)

    • インスタンスを渡す

    • 画像などアセットを変更したい場合はアセットIDもメンバに含めておく

  • データを表示するGUI要素(特にテキスト)をいつ生成するか

    • リスト表示するGUIについては、件数分のGUIをスクリプト上で生成する。件数が不定のため

    • 1件を表示するGUIについては、表示用のGUI要素はプレハブに仕込んでおく。能力値の種類やレイアウトが固定のため

DBのデータ定義も上記を考慮して設計することにします。

ユニットデータの定義

テスト用にユニットデータを定義します。

//本番ではmodelのUnitクラスを使う
public class TestUnitData
{

    private readonly Dictionary<int, string> AssignLabelText = new Dictionary<int, string>()
    {
        {0, "未設定" },
        {1, "待機" },
        {2, "修復" },
        {3, "生産" },
        {4, "戦闘" },
    };

    public int UnitId = 0;
    public string Name = "";
    public string Image = ""; //顔画像のAsset
    public int Exp = 0;
    public int AssignID = 0; //未設定
    public int Attack = 0;
    public int Fanout = 0;
    public int Level { get { return (int)System.Math.Floor(System.Math.Log10(Exp));  } }
    public string Assign{ get { return AssignLabelText[AssignID]; } }
}

ユニットデータのリスト表示

ユニットリストウィンドウはUnityシーン内のゲームオブジェクトとして作成しました。

ユニットリスト表示ウィンドウ

リストから1件だけ選択(排他的選択)できる場合と複数件を選択できるようにする場合を切り替えたかったので、リスト選択をToggleGroupとToggleを使って実装しました。リストの親の方にToggleGroupをつけて、リスト1件のGUIにToggleコンポーネントをつけて親が持つToggleGroupを設定します。親のToggleGroupを有効にすると、子のToggleを1個だけオンに出来るようになります。

ユニットリストの親。ToggleGroupをつけてリストを排他的選択できるようにしている

リストウィンドウ表示時に、リスト項目の追加と子となる項目に対する処理を行います。子のGUIが持つユニット表示用のコンポーネントのリストを親が持っておくと、反復処理がしやすいです。

public class UnitListViewer : MonoBehaviour
{
    [SerializeField] private Transform ListAreaTransform; //一覧の表示先となるGameObject
    [SerializeField] private ToggleGroup ListAreaToggleGroup; //一覧のToggleGroup(排他的選択のためのグループ)
    [SerializeField] private UnitInfoViewer UnitInfoView; //ユニット情報を表示するウィンドウ

    private List<GameObject> UnitLineList;
    private List<UnitLineSetter> UnitLineSetterList;


    async void OnEnable()
    {
        //ユニットデータを取得
        //本番ではDBから取得
        var unitdata = new List<TestUnitData>() { /*略*/ }

        //データをGUIにセット
        UnitLineList = new List<GameObject>();
        UnitLineSetterList = new List<UnitLineSetter>();
        var prefab = await Addressables.LoadAssetAsync<GameObject>("UnitLine").Task;
        for(int i=0; i<unitdata.Count; i++)
        {
            //GUIを配置
            var line = Instantiate(prefab, ListAreaTransform, false);
            UnitLineList.Add(line);
            var lineRect = line.GetComponent<RectTransform>();
            var yPos = -1.0f * (float)i; //1Uずつ下にずらす
            lineRect.anchoredPosition = new Vector3(0.0f, yPos, 0.0f);

            //UnitInfoで表示するデータをセット
            var setter = line.GetComponent<UnitLineSetter>();
            UnitLineSetterList.Add(setter);
            setter.Unit = unitdata[i];
            setter.SetText();            

            //排他的に選択する場合のためにToggleGroupを設定
            // ※ToggleGroupのAllowSwitchOffをONにしておくと、一つも選択されていない状態を許す
            if (ListAreaToggleGroup is not null)
            {
                setter.SetToggleGroup(ListAreaToggleGroup);
            }

            //ハンドラをセット
            setter.SetHandler((arg) => { OnGo(); });

        }
    }

    void OnDisable() { /*略*/ }

    void OnGo()
    {
        //選択された行を表示。最初に見つかった行を得る
        UnitInfoView.Hide();
        foreach (var unit in UnitLineSetterList)
        {
            if(unit.IsOn())
            {
                UnitInfoView.Unit = unit.Unit;
                break;
            }
        }
        UnitInfoView.Show();
    }
}

子のハンドラのセットでは、選択された行のユニット情報を表示するメソッドを登録しています。このメソッドは親の持つメソッドです。というのも、ユニット情報ウィンドウの開閉は親が一元管理するため、親側でウィンドウの開閉を制御しています。このあたりの実装は人によって考え方が違うと思います。子にユニット情報ウィンドウを関連付けて子が開閉しても良いです。

ユニットデータ1件の表示

ユニットデータ1行分のGUIのプレハブ
public class UnitLineSetter : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI NameLabel;
    [SerializeField] private TextMeshProUGUI LevelLabel;
    [SerializeField] private TextMeshProUGUI ExpLabel;
    [SerializeField] private TextMeshProUGUI AssignLabel;
    [SerializeField] private Toggle Toggle;
    public TestUnitData Unit; //1件のデータ

    public void SetText()
    {
        NameLabel.SetText(Unit.Name);
        LevelLabel.SetText($"{Unit.Level}");
        ExpLabel.SetText($"{Unit.Exp}");
        AssignLabel.SetText(Unit.Assign);
    }
    public void SetHandler(UnityAction<bool> action)
    {
        //以前のハンドラとリソースを開放
        Toggle.onValueChanged.RemoveAllListeners();
        //ハンドラを登録
        Toggle.onValueChanged.AddListener(action);
    }

    public bool IsOn()
    {
        return Toggle.isOn;
    }
    public void SetToggleGroup(ToggleGroup group)
    {
        Toggle.group = group;
    }
}

主な処理は以下のふたつです。
・ユニット1体のデータの抜粋をGUIのテキストに書き込む
・イベントハンドラを登録。Toggleの場合はオンオフの値が変化したときのハンドラ

ちなみに、Unityのエディタ画面からToggleを追加するとチェックマーク☑が付いてきますが、Toggleの本体はGUIではなくクリック領域だそうです。実際にチェックボックスの画像を消してもオンオフが動作しました。

ToggleのTarget Graphicプロパティはチェックボックスの背景(□の方)、Graphicプロパティはオンのときの画像(✓の方)の指定です。Graphicに強調色の画像を入れておくと、オンになっている=情報表示中の行を強調表示できます。
ただ、色が半透明の場合、Toggleオンとカーソル選択が重なると色が変わってしまうのをどうしたものか。色を選ぶときに重なることを想定してませんでした…

Toggleの設定。強調色=カーソルがある行の色を赤橙、グラフィックス=オンになっている行の色を黄緑に設定
カーソル行とオンの行が重なった時。黄色…

ユニット情報ウィンドウの表示

ユニット情報ウィンドウ

ユニット情報ウィンドウもUnityシーン内のゲームオブジェクトとして作成しました。ユニットデータ1行のGUIと同様に、実装する処理はデータを表示するだけです。


public class UnitInfoViewer : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI NameLabel;
    [SerializeField] private TextMeshProUGUI LevelLabel;
    /* 略 */
    //顔
    [SerializeField] private Image FaceImage;
    //能力値
    [SerializeField] private TextMeshProUGUI AttackLabel;
    /* 略 */
    [SerializeField] private TextMeshProUGUI FanoutLabel;
    //行動
    [SerializeField] private List<TextMeshProUGUI> ActionLabelList;

    public TestUnitData Unit;

    private async void OnEnable()
    {
        if (Unit is null) return;

        //名前などをセット
        NameLabel.SetText(Unit.Name);
        /* 略 */

        //能力値をセット
        AttackLabel.SetText($"{Unit.Attack}");
        /* 略 */
        FanoutLabel.SetText($"{Unit.Fanout}");

        //行動をセット、略

        //顔画像をセット
        var asset = await Addressables.LoadAssetAsync<Sprite>(Unit.Image).Task;
        FaceImage.sprite = asset;
    }

    public void Hide()
    {
        gameObject.SetActive(false);
    }
    public void Show()
    {
        gameObject.SetActive(true);
    }
}

ユニット情報ウィンドウをクリックすると閉じるようにしています。ユニット情報ウィンドウにEventTriggerのコンポーネントをつけて設定しました。

すりガラス風のウィンドウ表示

ユニット一覧のウィンドウですが、よく見ると後ろ側の背景がぼけていると思います。元々はウィンドウをそのまま半透明表示していたのですが、そのままだと文字が重なって読みづらかったため、ぼかした背景を重ねて表示するようにしました。
本当はウィンドウの後ろにある領域を文字ごとぼかしたかったのですが、半透明のオブジェクトをぼかす処理をシェーダーグラフで書けそうになかったので断念しました。折衷案として、背景をカメラのポストプロセスでぼかしてレンダーテクスチャに書き込み、レンダーテクスチャを切り抜いてウィンドウの背にはめ込んでいます。

調べた方法

シェーダをコードで実装。本質的な対処はこれ。ただし面倒くさそう

シェーダーグラフで畳み込みを実装する方法も見かけたのですが、周辺サンプルを1個ずつ取得して演算する処理を1ステップずつノードでつなぐのは保守が大変そう。

シェーダーグラフで_CameraOpaqueTexture変数を使って直前の描画結果を得る方法もあるのですが、半透明オブジェクトをぼかしたい場合には使えません。あとURPではGrabPassは使えません。その代わり、_CameraSortingLayerTexture を使うと、描画するUIのソーティングレイヤーに対して、その下にあるソーティングレイヤーの描画結果を得られるようです。

すりガラスもどきをノンコードで実装

背景をぼかすだけなら、シェーダ以外にカメラのポストプロセスで出来ることが分かったのが転換点でした。

ポストプロセス後の出力をレンダーテクスチャに出すと、ゲームオブジェクトとして表示できるようになります。

記事にないポイントとしては、レンダーテクスチャのアスペクト比に合わせてカメラが取得する画像のアスペクト比も決まるので、画面全体を撮りたい場合はレンダーテクスチャのアスペクト比を画面のアスペクト比に合わせる必要があります。

・背景をカメラを通してレンダーテクスチャに転送できる
・RawImageコンポーネントでレンダーテクスチャをゲームオブジェクトとして表示できる
・背景をカメラに通してポストプロセスでぼかせる

これらを組み合わせると、背景をカメラに通す→ポストプロセスでぼかす→レンダーテクスチャに転送する→RawImageを使ってゲームオブジェクトとして表示、という流れを作れます。これによりぼかした背景をウィンドウの背に表示できます。
カメラとゲームオブジェクトとのデータの出入りがややこしいので、配線図のような書き方でデータの流れをまとめると、Unityでの設定を組みやすくなりました。
データの流れはカメラレイヤとレンダーターゲット(カメラとレンダーテクスチャ)でまとめました。カメラレイヤを信号のバスに、レンダーターゲットをモジュールに見立てています。

カメラレイヤとレンダーターゲットとのデータの流れ

カメラを別に用意するので負荷が高そうですが、コードを書かなくてよいのは良いこと。

RawImageのUV矩形に指定するビューポートはスクリプトでウィンドウの位置から計算しています。

ユニットリストウィンドウの背のRawImage

次にやる事

アイコンを作って白い四角の部分を置き替えます。

この記事が気に入ったらサポートをしてみませんか?