[Unity]ランキングの表示

2023年10月17日

得点順にUIに表示される機能を実装する学習をしましょう

このチュートリアルは次の内容に続きになります

実行イメージ

起動直後にランキングリストに登録されている情報をUIに表示しています
スペースキーを押下すると更新されたランキングが表示されます

登録された10人のランカーの人たちをイラスト付きでランキング表示しています
もちろんランキングに変動があると入れ替わります

プロジェクトの作成

サンプルでは、RankingSampleとして新しくUnityプロジェクトを作成しています

シーンの全体構成

マネージャーオブジェクト

ランキングを更新するためのマネージャーオブジェクトです
仮にランカー(ユーザー)ごとのイラストを入れています

ランキング表示用オブジェクト構成イメージ(後述に登録方法)

ランキング表示用オブジェクト詳細

RankingBoardオブジェクト(Canvas):UIのキャンバス

ランキングを表示するCanvasです
画面がサイズの変更の影響を受けないように設定を調整しています

RankingDialogオブジェクト(Panal):ランキングを表示するためのエリア

Canvas内にランカーを表示するためのエリアを設定しています
UIのPanalをシーンを子オブジェクトとして追加します
これはランキング表示の背景にもなっています

エリアの大きさの変更

縦のレイアウトを整えるグループの作成

縦(垂直)のオブジェクトが均等に自動配置されるようにVertical Layaut Groupコンポーネントを追加します
Paddingで上下左右の隙間(間隔)を調整します

Ranking Dialogスクリプトをアタッチして、RankingButtonオブジェクトをインスタンス上にアウトレット接続しています

RankingButtonオブジェクト(Button):ランカー表示用

個別のランキング情報を表示しています
将来、クリックすると詳細表示もできるようにButtonにしています

ランカーの枠の大きさを指定します

このオブジェクトはランカー1人あたり1つ作成されます
ここで、大きさを設定します
全てのランカーに一致するサイズになります

ランカーの情報表示欄を登録します

RankingButtonゲームオブジェクトの子オブジェクトにUIのImageを追加します
RankingButtonゲームオブジェクトの子オブジェクト内のイメージは横方向に均等に配置したいので、今度はHorizontal Layout Groupコンポーネントを追加します

Imageオブジェクト(Image):ランカーイメージアイコン(イラスト)

ランカーのイメージアイコンを表示するエリアです
Imageを貼り付けています

参考)サイズのイメージ

Nameオブジェクト(Text):ランカーの名前

ランカーの名前表示エリアです
Textになっています

参考)サイズのイメージ

Score(Text):ランカーのスコア

ランカーのスコア表示エリアです
Textになっています

参考)サイズのイメージ

コード

Rankingスクリプト

ランキング一覧を管理するスクリプトです
シングルトンで構成され複数のインスタンスの作成はされません

内部クラスに個別のランカー情報を持っています

外部からはリストを取得することができます

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class Ranking
{
    Ranking()
    {
    }

    static Ranking instance;

    public static Ranking GetInstance
    {
        get
        {
            if (instance == null)
            {
                instance = new Ranking();
            }
            return instance;
        }
    }

    public class Ranker
    {
        public string name;
        public Sprite sprite;
        public int score;

        public Ranker(string name, Sprite sprite, int score)
        {
            this.name = name;
            this.sprite = sprite;
            this.score = score;
        }
    }

    List<Ranker> rankers = new List<Ranker>();

    public List<Ranker> Rankers => rankers.OrderByDescending(ranker => ranker.score).ToList();

    public void Add(string name, Sprite sprite, int score)
    {
        Ranker ranker = new Ranker(name, sprite, score);
        rankers.Add(ranker);
    }

    public void Remove(string name)
    {
        rankers.Remove(rankers.Find(score => score.name == name));
    }

    public void Clear()
    {
        rankers.Clear();
    }
}

RankingDialogスクリプト

ランキング表示用のエリアにランカーボタンを作成する役割があります

各ランカーは、インスタンスの作成で構成されています
単に追加していますが、LayoutGroupのコンポーネントにより自動的に均等表示されます

using System.Collections.Generic;
using UnityEngine;

public class RankingDialog : MonoBehaviour
{
    [SerializeField]
    int buttonNumber = 10;

    [SerializeField]
    RankingButton rankingButton;

    RankingButton[] rankingButtons;

    private void Awake()
    {
        CreateButton();
    }

    private void CreateButton()
    {
        for (int i = 0; i < buttonNumber - 1; i++)
        {
            Instantiate(rankingButton, transform);
        }

        rankingButtons = GetComponentsInChildren<RankingButton>();
    }

    public void ShowRanking()
    {
        List<Ranking.Ranker> rankers = Ranking.GetInstance.Rankers;

        for (int i = 0; i < rankers.Count; i++)
        {
            rankingButtons[i].Ranker = rankers[i];
        }
    }
}

説明

ランカーごとのボタンの生成します
登録されたボタンの数(buttonNumber)分まで繰り返されます

private void CreateButton()
{
    for (int i = 0; i < buttonNumber - 1; i++)
    {
        Instantiate(rankingButton, transform);
    }
}

子オブジェクトにアタッチされているRankingButtonスクリプトを配列で取得します
この先使いたいのはこのスクリプトのメンバーなのでオブジェクト自体を取得しなくても良いです

rankingButtons = GetComponentsInChildren<RankingButton>();

ランキングを表示します(実際はランカーのボタンを表示します)
ランカー一覧は、RankingクラスのRankersプロパティに保存されています
このクラスから作られるインスタンスは1つで十分なのでシングルトンの呼び出しを使って取得しています

その後、所得した一覧情報を順番に子オブジェクトから取得したスクリプトのRankerプロパティに代入(セット)して行っています

public void ShowRanking()
{
    List<Ranking.Ranker> rankers = Ranking.GetInstance.Rankers;

    for (int i = 0; i < rankers.Count; i++)
    {
        rankingButtons[i].Ranker = rankers[i];
    }
}

RankingButton

ランカー個人の情報を表示するボタンを管理しています
オブジェクトがボタンなので、将来クリックすることで何かを実行できるようにすることもできます
ボタンに表示される情報は、ランカー個人アイコン・名前・得点になります
それぞれに設定するようにします

サンプルでは、Text型(旧来)にしていますが、TextMeshProを使う場合は次を参考に変更してください

次のコードを参考に調整してください
ただし、このままでのコピーでは動作しないので注意してください

using TMPro;

public class Sample : MonoBehaviour 
{
    [SerializeField]
    private TextMeshProUGUI sampleText;

    void Start() 
    {
        sampleText.text = "表示させたい文字列";
    }
}
using UnityEngine;
using UnityEngine.UI;

public class RankingButton : MonoBehaviour
{
    [SerializeField]
    Image icon;

    [SerializeField]
    Text nameText;

    [SerializeField]
    Text scoreText;

    Ranking.Ranker ranker;

    public Ranking.Ranker Ranker
    {
        get
        {
            return ranker;
        }
        set
        {
            ranker = value;
            icon.sprite = ranker.sprite;
            nameText.text = ranker.name;
            scoreText.text = ranker.score.ToString();
        }
    }
}

説明

Ranking.Ranker ranker;

Rankerクラスの情報は、Rankingクラスのインナークラス(クラス内部に記述されたクラス)なので、.(ピリオド、点)で繋いで表現します

内部クラスでない場合だと単に次のようになりますね

Ranker ranker;

UIの各情報(画像イメージ・名前・スコア)に表示する処理を担っています
Rankerプロパティに代入(セット)すること(setが呼ばれる)で、表示されるようになります

Rankerプロパティは、Ranking.Ranker型なので、valueには3つの情報が渡されます

public Ranking.Ranker Ranker
{
    set
    {
        ranker = value;
        icon.sprite = ranker.sprite;
        nameText.text = ranker.name;
        scoreText.text = ranker.score.ToString();
    }
}

このプロパティに代入するのは、RankingDialogクラスのShowRankingメソッドになります
for文で繰り返し、順番に代入されます

rankingButtons[i].Ranker = rankers[i];

マネージャー

マネージャーは、ゲームを進行をコントロールする役割があるとします
ゲームオーバーになった際、プレイヤーの得点をもとにランキング管理のためのデータを作成します
プレイヤーの情報としては、クラスで、名前・アイコン・得点をメンバーとして管理しています

using UnityEngine;

public class Sample : MonoBehaviour
{
    [SerializeField]
    Sprite[] sprites;

    [SerializeField]
    RankingDialog rankingDialog;

    void Start()
    {
        Ranking ranking = Ranking.GetInstance;
        ranking.Add("太郎", sprites[0], 70);
        ranking.Add("次郎", sprites[1], 60);
        ranking.Add("三郎", sprites[2], 50);

        ranking.Remove("次郎");

        ranking.Add("四郎", sprites[4], 65);

        foreach (var ranker in ranking.Rankers)
        {
            Debug.Log($"名前:{ranker.name}  ポイント:{ranker.score}");
        }

        rankingDialog.ShowRanking();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            UpdateRanking();

            rankingDialog.ShowRanking();
        }
    }

    private void UpdateRanking()
    {
        Ranking ranking = Ranking.GetInstance;
        ranking.Clear();
        ranking.Add("五郎", sprites[0], 75);
        ranking.Add("六郎", sprites[1], 88);
        ranking.Add("七郎", sprites[2], 20);

        ranking.Remove("七郎");

        ranking.Add("八郎", sprites[3], 10);

        foreach (var ranker in ranking.Rankers)
        {
            Debug.Log($"名前:{ranker.name}  ポイント:{ranker.score}");
        }
    }
}

発展(10人固定表示を参加人数分表示に変更とリファクタリング)

今は、10人固定のため、ランキングに入っている人数分だけ表示したい
それに伴いコードを整理するために現在サンプル的にHierarchyに表示されているオブジェクトをPrefabにします

実行結果

上位3人のみ表示できるようになりました
また、上寄せにもなっていますね

変更点

上寄せになるように変更します

RankingDialogオブジェクトにアタッチしているVertical Layout Groupのプロパティを変更します

ランキングボタンのPrefab化

直接、仕様更新には関係しませんが、リファクタリングしてわかりやすくするため、Prefab化します
Prefabにした後は、Hierarchyビューから削除しておきます

RankingDialogのスクリプトを更新

ランキングボタンのPrefab化に伴い、コードを整理します

登録ボタン数の変数は削除します
Awakeイベント関数でのボタン事前作成も削除します
ShowRankingメソッドが呼び出されると、一旦、子オブジェクトのランキングボタンを全て削除して再度作り直します

using System.Collections.Generic;
using UnityEngine;

public class RankingDialog : MonoBehaviour
{
    [SerializeField]
    RankingButton rankingButton;

    public void ShowRanking()
    {
        // 子オブジェクトを全て削除
        foreach (Transform n in transform)
        {
            Destroy(n.gameObject);
        }

        List<Ranking.Ranker> rankers = Ranking.GetInstance.Rankers;

        for (int i = 0; i < rankers.Count; i++)
        {
            RankingButton rb = Instantiate(rankingButton, transform);
            rb.Ranker = rankers[i];
        }
    }
}

インスタンス作成用のPrefabをアウトレット接続

今後、インスタンスはPrefabから作るため、置き換えます

Unity

Posted by hidepon