如何利用UGUI在Unity中实现一个本地排行榜

write by 书封影

前置知识:Unity的基本操作,C#基础,List泛型集合基本操作,GameManager,string的基本使用方法。注意,本文默认你已经掌握上述知识,并在后文中不会对上述知识进行详细阐述。本文的代码包含大量书封影写的塑料英文注释,还请谅解。

前言

菜菜的书封影在Unity Junior Programmer的学习过程中,结课作业有一项是制作一个像这样的乒乓游戏。

游戏中的大部分内容已经做好了,而我们需要实现的是使游戏能够在本地存储玩家的分数信息,同时显示所有玩家中,最高分的玩家的名字及其分数。这里需要用到TMP中的Input Filed来允许玩家输入其姓名。以上内容倒是不难,在此不作阐述。

然而在实现以上功能后,闲得慌的书封影突然灵机一动,想像街机一样制作一个可以查看所有玩家排名的排行榜。书封影思考了一会儿后,发现这个事情对于久经沙场的程序员来说不难,但是对于书封影这种菜旺来说,还是有一点难度。在书封影不停的思考和尝试后,他成功实现了如下图所示的排行榜。

每个玩家在结束游戏后,都需要输入其姓名,在确认姓名之后,游戏会向玩家展示整个游戏的排行榜,这个排行榜上会显示包括玩家在内的,所有玩家的排名,姓名,和分数数据。

也就是,系统不但会储存玩家的分数数据,还会根据玩家的分数进行排名,并且按照排名顺序显示在系统UI上。

这是如何实现的呢?

功能需求清单

书封影是一个策划,所以他在正式开始施工之前,先列了一份功能需求清单:

  1. 排行榜的信息来源于玩家Game Over 后的结算分数和玩家自行输入的姓名。

  1. 排行榜将显示包括:排名(rank),姓名(name),分数(score)三个元素。

    扫描二维码关注公众号,回复: 14661903 查看本文章
  1. 排行榜的排序基于玩家的分数元素。

  1. 排行榜默认显示最上方的信息。

  1. 排行榜最好能够拖动或滑动,以便于查看下方排名信息。

  1. 可以的话,希望能够根据策划需求,限制排行榜最多个数。比如,排行榜只包含10个人的信息。如果未进前十则不显示在排行榜内。(limit)

书封影列好了这份清单之后,发现第一项已经完成了,并开始思考如何逐步实现其他功能。

物件化

书封影在思考了一会儿后,发现每一个排名的内容在种类上是相同的,即含有排名(rank),姓名(name),分数(score)三个变量。

书封影认为,可以共用一个相同的排名物件(RankItem),然后根据不同的玩家数据,改变其变量内容,而后对应生成该物件,从而获得不同的玩家排名。

想到这,书封影开始行动了起来。

首先,书封影创建了一个空的GameObject,将其命名为RankItem,而后在其下添加了三个子物体,子物体的种类均为Text-TextMeshPro,名字分别为:Rank_RankItem、Name_RankItem、Score_RankItem,得到如图物件结构

对子物件经过稍微摆放后,获得了如图效果。

与此同时,书封影创建了一个Script,命名为RankItem,并将其附着在RankItem物件下。而后开始着手对脚本进行编辑。

首先,他定义了三个TextMeshProUGUI(需要命名空间TMPro),并在Editor内将每个子物件拖入了Inspector。

    [Header("Child")]
    public TextMeshProUGUI rankText;
    public TextMeshProUGUI nameText;
    public TextMeshProUGUI scoreText;

而后定义了一个初始化函数,其他脚本文件可以调用该函数完成传参。

    /// <summary>
    /// Pass the parameters in will change the element of RankItem.
    /// </summary>
    /// <param name="rank"></param>
    /// <param name="name"></param>
    /// <param name="score"></param>
    public void Initiate(int rank, string name, int score)
    {
        rankText.text = rank.ToString();
        nameText.text = name;
        scoreText.text = score.ToString();
    }

至此,书封影完成了排名内容的物件化。需求清单的第二项完成!

接下来,它只需要完成传参、自动生成、文件保存三大内容就可以了。

Grid

现在已经完成了单个RankItem的工作了,可是我要如何才能让生成的RankItem自动成为一个序列呢?

所幸书封影曾经见过一个叫grid的东西,但他忘了这个东西的参数配置了,于是他上网搜了一下,重新复习了grid的使用方法。

Unity - Manual: Grid Layout Group (unity3d.com)

于是书封影手动创建了一个Empty GameObject,并将其命名为Grid,在Inspector中为其增加了名为“Grid Layout Group”的组件,并参考手册的内容,将其调参如下。

将Grid扔进事先准备好的RankCanvas里后,书封影试着往里面不断克隆同一个RankItem,这些RankItem非常规矩的一列列排了下来。

“非常好!排行榜有个基本的样子了!”书封影暗暗想到。

Scroll View

虽然这些RankItem在屏幕上成功自动序列化,表格化了,但是书封影却对他很不满意,因为当他不断克隆RankItem时,整个Grid居然无限衍生了,呈现了下图中的效果

这实在是太丑陋了!

书封影不希望这些RankItem无限制的出现在屏幕上,而是希望能有一个遮罩,所有的RankItem都只显示在这个遮罩内,且可以拖动遮罩中的内容。

如何实现呢?

书封影相信Unity提供了这样的插件,于是他在Hirerachy中点击了鼠标右键,在UI一栏中发现了一个叫做Scroll View的UI,他点击添加后,得到了这样的东西

书封影试了试运行,发现两边的bar都可以被拖动。于是他对这个东西产生了好奇,并开始了对他的研究。

他翻了好久才发现,这东西在手册中标注为Scroll Rect,而非Scroll view,不过这并不重要,重要的是上面的各个API代表着什么

Unity - Manual: Scroll Rect (unity3d.com)

书封影点开了这个Scroll view的文件层级,并删除了两个的Scrollbar,

与此同时,为了于黑色的背景相适应,在Inspector窗口中,书封影将Scroll view的Image组件中的Color改成了纯黑色,当然,也可以改成完全透明。

最后在Scroll Rect组件下,取消了Horizontal的勾选,只保留了Vertical。

而后在Scene窗口中,对Scroll view的大小稍作了适应性调整

完成后,书封影将Grid拖入了scroll view下的Content物件里,再不断克隆RankItem,得到了下图结果

点击运行,书封影试着在游戏内拖动排行榜,完全没问题。

嗯!完美!需求清单的第四、五项完成!

接下来只需要完成根据分数排名和限制最多显示即可!

(此时的Scroll View在拖动后会有回弹效果,如果你想变成完全可拖动的,可以试着对Scroll View下的Content的大小进行自适应调整,比如先锚定Grid和Content的相对位置,然后在每增加一个RankItem的情况下增加Content的长度……建议在完成排行榜的内容后再回过头来动手试一试)

信息存储读取模块框架构建

完成了UI的部署之后,一个新的问题摆在了书封影面前:我们每次游戏结束都会产生新的玩家姓名和分数信息,我们该如何把这些所有的玩家信息都存储起来呢?

最开始,书封影尝试着用一个类定义玩家的信息,并用一个List存储所有类,而后转为json存储。但是书封影太笨了,当他试着输出json时发现里面什么都没有,所以他很沮丧。这个问题卡了他很久,直到他转变了存储方法,改为利用注册表文件配合字符串特性进行存储。

(如果看完本文之后,在你的尝试之下,你成功运用json进行了存储,请务必教一教书封影这个傻瓜)

书封影在UI场景(简称UI)和游戏场景(简称Main)之间做了一个不会销毁的GameManager(默认你知道他的原理),这个GameManager负责在UI和Main之间传递玩家的分数信息和名字信息。其变量定义如下所示(省略号略去部分必须代码和已展现过的代码):

public class GameManager : MonoBehaviour
{

    //Call a GameManager instance
    public static GameManager instance;

    [Header("Parameters")]
    public int m_Points = 0;
    public bool m_GameOver = false;
    public string m_PlayerName = null;

    // Start is called before the first frame update
    void Awake()
    {...}
}

而后,书封影在GameManager下新定义了一个公共类,用于存储单个玩家的信息。

public class GameManager : MonoBehaviour
{
    ......

    [System.Serializable]
    //Save partition, using registry to save the rank information.
    public class Save
    {
        public string name;
        public int score;
    }
}

然后他在Save这个类的后面,定义了一个List,用于存储所有的Save实例

public class GameManager : MonoBehaviour
{
    ......
    [System.Serializable]
    //Save partition, using registry to save the rank information.
    public class Save
    {...}
    //All information of rank will stored in this list.
    private List<Save> saves = new List<Save>();
}

考虑到可能需要限制排名显示个数(例如只显示前十个),于是他又定义了一个rankLimit

    /// <summary>
    /// To limit the visible number of RankItem.
    /// </summary>
    private int rankLimit = 10;

OK!现在书封影在[System.Serializable]下完成了信息存储读取模块的框架构建!接下来要做的,就是完成具体的存储读取功能了!

PlayerPrefs

我们已经知道,一个排名信息由三个元素组成,分别是排名rank,名字name和分数score。正如上文所说,书封影之前试着将List转为json,但他失败了,于是他又思考了一伙儿,查询资料,发现了另一种存储方式——注册表。

Unity3D提供了一个用于本地持久化保存与读取的类——PlayerPrefs。工作原理非常简单,以键值对的形式将数据保存在文件中,然后程序可以根据这个名称取出上次保存的数值。这个文件就是本地注册表文件,可以存储数据和字符串。

关于PlayerPrefs,可以前往手册中详细查看

Unity - Scripting API: PlayerPrefs (unity3d.com)

书封影的思路是这样的:可以利用C#的string的语法糖,名字、分数两个个元素封装成一个长字符串,格式为

name+score|name+score

例如,我们的书封影叫做ShuFengYingt、分数为100,则其字符串化为

“ShuFengYingt+100”

如果又加入了一个叫SFY的,分数为80,则为

“ShuFengYingt+100|SFY+80”

欸,等等,排名信息呢?

书封影觉得,由于每一次加入新的数据都需要依据玩家分数对排行榜进行重构,所以不妨在每次新加入数据时,对排名信息进行计算,就没必要存储排名信息了。

接下来,书封影写下了三个方法,分别是ParseInfo解析,SaveInfo存储,以及GetInfo读取。

ParseInfo

由于书封影将排名信息用了特定格式进行存储,即加密,那么相应的,我们首先需要有一个方法来对“加密”了的信息进行解析。这便是ParseInfo的作用。

书封影将ParseInfo方法写在了rankLimit下。

    [System.Serializable]
    //Save partition, using registry to save the rank information.
    public class Save
    {...}
    //...
    private List<Save> saves = new List<Save>();
    private int rankLimit = 10;

    /// <summary>
    /// Parse the information about rank from locating strings.
    /// </summary>
    public void ParseInfo()
    {
	//To be completed.
    }

书封影首先构想了存储信息的注册表的名字,为“PONGPI_SaveInfo”,也就是说,游戏的排名信息将存储在一个名为PONGPI_SaveInfo的注册表文件里。

而后,在ParseInfo方法中,书封影利用PlayerPrefs的GetString方法,获取注册表中名为PONGPI_SaveInfo的文件的字符串信息,并存储于自定义的字符串变量saveString当中。

    public void ParseInfo()
    {
        saves = new List<Save>();
        string saveString = PlayerPrefs.GetString("PONGPI_SaveInfo");
	//To be completed.
    }

考虑到第一次游玩游戏时,PONGPI_SaveInfo不存在,saveString为空,所以,书封影对saveString的内容进行了一个判断,保证执行解析的前提时saveString不为空。

    public void ParseInfo()
    {
	string saveString =  PlayerPrefs.GetString("PONGPI_SaveInfo");

	//If there are informations in the saving string, then Parse it.
	if (!string.IsNullOrEmpty(saveString))
	{
	    //To be completed.
	}
	    //To be completed.
    }

而后就是解析方法的主体部分了。

如上文所言,我们的信息存储结构是"name"+"score"|"name"+"score”,所以,我们第一步需要将连在一起的信息依据“|”元素拆开。

    //If there are informations in the saving string, then Parse it.
    if (!string.IsNullOrEmpty(saveString))
    {
	//Split each item by '|'.And save each item in a array of string.
        string[] rankItemArry = saveString.Split('|');
    }

这样,每个不同的排名信息都会以单个name+score的形式存储于rankItemArray当中。

接下来,我们需要依据“+”号,拆开每个name+score。变成分开的name和score,存储于一个名为rankItem的字符串数组当中。

    //Split each item by '|'.And save each item in an array of string.
    string[] rankItemArry = saveString.Split('|');
    //Split each item by '+' and save it in an array named rankItem each for loop.
    for (int ident = 0;ident < rankItemArry.Length;ident++)
    {
	string[] rankItem = rankItemArry[ident].Split('+');
    }

而后在之前定义的Save类下创建一个实例化对象,并修改其数据成员的参数。

    //Split each item by '+' and save it in an array named rankItem each for loop.
    for (int ident = 0;ident < rankItemArry.Length;ident++)
    {
	string[] rankItem = rankItemArry[ident].Split('+');
	Save save = new Save();

	//The first elment is its name
	save.name = rankItem[0];
	//And the second is the score
	//Use int.Parse() method to transform the string to integer.
	save.score = int.Parse(rankItem[1]);
    }

最后利用List的Add方法,将该对象传入saves列表当中。

    //Split each item by '+' and save it in an array named rankItem each for loop.
    for (int ident = 0;ident < rankItemArry.Length;ident++)
    {
	......
	//Add it to the list.
	saves.Add(save);
    }

这样,ParseInfo的主体部分就完成了。不过ParseInfo方法并没有写完,别忘了,我们只做了名字和分数的解析,而排名位次信息,则并没有完成。

List冒泡排序模板

接下来要做的就是排位了。

书封影希望每当解析完注册表文件中的信息并存储在saves列表中之后,能够依据saves中每一个元素的分数信息,对saves列表进行重新排序。而这就需要利用List的Sort方法,并自己写下排序依据的方法。这段话可能有点绕,我们不妨结合代码来看一下。

书封影首先在ParseInfo内,于主体代码下增加了一个条件判断:即当saves非空时,对saves进行排序

public void ParseInfo()
{
    ......
    //If the saves list isn't in null, sort the rank list by score.
    if(saves.Count != 0)
    {
	saves.Sort();
    }
}

但这样做是不能完成排序的——我们还需要写一个CompareScore方法传入Sort方法当中,才能让List的Sort方法依据分数排序。

于是书封影在ParseInfo方法下,新增了一个返回值为整型的CompareScore方法

public void ParseInfo()
{...}
private int CompareScore()
{
    //To be completed.
}

该方法将作用于Save类的实例化对象,所以需要传入两个Save对象进去进行比较

/// <summary>
/// Compare two objects based on score in the Sort() method.
/// </summary>
/// <param name="player_1"></param>
/// <param name="player_2"></param>
/// <returns></returns>
int CompareScore(Save player_1, Save player_2)
{
    //To be completed.
}

而后获取两个对象的分数,并比较,再返回值。

/// <summary>
/// Compare two objects based on score in the Sort() method.
/// </summary>
/// <param name="player_1"></param>
/// <param name="player_2"></param>
/// <returns></returns>
int CompareScore(Save player_1, Save player_2)
{
    int score_1 = player_1.score;
    int score_2 = player_2.score;
    if (score_1 == score_2)
    {
        return 0;
    }
    else if (score_1 > score_2)
    {
        return -1;
    }
    else
    {
        return 1;
    }
}

分数相同则返回0,传入参数的前者大于后者则返回-1,小于后者则返回1。这样就能让List中的对象在Sort方法下依据分数大小由大至小进行排名。

(如果分数相同返回0,前大于后返回1,小于后返回-1,则由小至大排名)

本方法是List下冒泡排序方法的基本模板。

而后,书封影将这个方法重新传入Sort当中,依据分数排名的功能就大功告成了!

public void ParseInfo()
{
    ......
    //If the saves list isn't in null, sort the rank list by score.
    if(saves.Count != 0)
    {
	//Simply pass in the name of comparision method.
	saves.Sort(CompareScore);
    }
}
///...
int CompareScore(Save player_1, Save player_2)
{...}

SaveInfo

完成了解析方法的书写之后,接下来,书封影则开始了对存储方法的施工。

书封影的存储方法将完成saves列表向注册表文件的转化。

public void ParseInfo()
{...}
/// <summary>
/// Using "name"+"score"|"name"+"score" structure to store it in a long string.
/// </summary>
public void SaveInfo()
{
    //To be completed.
}

由于存储排名时需要对排名的所有信息进行修改并排序,所以方法的第一步便是调用刚刚写好的ParseInfo。

public void SaveInfo()
{
    //Parse the rank every save time.Or we can say, refresh it every game over time.
    ParseInfo();

    //To be completed.
}

而后,书封影创建了一个Save类的实例化对象,并依据GameManager中的数据,修改了改对象的数据成员,而后将其添加到saves的List当中(书封影温馨提示:如果忘了Save类和GameManager的定义内容,可以上翻至题为“信息存储读取模块框架构建”的区域进行重新浏览)

public void SaveInfo()
{
    //Parse the rank every save time.Or we can say, refresh it every game over time.
    ParseInfo();

    //Add the newest information of rank to the rank list.
    Save save = new Save();
    save.name = m_PlayerName;
    save.score = m_Points;
    saves.Add(save);

    //To be completed.
}

存入数据后,新的saves列表可能并不是按照分数由大到小排列的(比如新加入的玩家信息可能是分数最高的),所以我们需要再次进行排序.

public void SaveInfo()
{
    ......
    //Sort the list after add a new rank.
    saves.Sort(CompareScore);
}

书封影突然想到,在我们最开始的需求列表里,有一项需求为“可以的话,希望能够根据策划需求,限制排行榜最多个数”,于是书封影依据之前定义好的rankLimit,对超出rankLimit的数据进行移除操作。

public void SaveInfo()
{
    ......
    //Sort the list after add a new rank.
    saves.Sort(CompareScore);

    //Limit the number of rank item.
    //If the number of the rank item overflows the ranklimit, then remove the last one of the rank list.
    if (saves.Count > rankLimit)
    {
        saves.RemoveAt(saves.Count - 1);
    }
}

完成了这一步之后,剩下的便是简单的将saves内的数据,依据“name+score|name+score”的格式,转为字符串,并以“PONGPI_SaveInfo”为文件名,储存于注册表了。(注意,文件名一定要与ParseInfo中的一致)

public void SaveInfo()
{
    ......
    //Store all the information of the rank in this string.
    string saveString = "";

    for (int ident = 0;ident < saves.Count;ident++)
    {
        //Transform every rank information to a string.
        string temp = "";
        temp += saves[ident].name;
        temp += "+";
        temp += saves[ident].score;

        //If it is the last one of the list, it not need to add the "|"
        if (ident != saves.Count - 1)
        {
            temp += "|";
        }
        saveString += temp;
    }

    //Put it into the registry. 
    PlayerPrefs.SetString("PONGPI_SaveInfo", saveString);
}

至此,SaveInfo的方法就写完了。

GetInfo

很好!书封影已经完成了ParseInfo,SaveInfo,也就是完成了数据的“解密”与“加密”,或者说,解析与存储,那么读取方法呢?

书封影想了想,SaveInfo会用于每次游戏结束,玩家输入自己的姓名并确认后,自动存储玩家的信息。

ParseInfo用于解析已有的信息,并存储于名为“saves”的List列表当中。

那么GetInfo就应该用于获取List当中的信息,并以此为基础创建排行榜。

所以GetInfo非常简单,直接调用ParseInfo并返回一个List即可。

/// <summary>
/// Using to get the information on the rank list which had been saved in the registry.
/// </summary>
/// <returns>Save List</returns>
public List<Save> GetInfo()
{
    ParseInfo();
    return saves;
}

生成排行榜-管理器初始化

书封影终于终于终于在不会被销毁的GameManager中完成了三个重要方法的书写。但是,这些方法目前没有受到调用,也就起不到任何作用。可是问题来了,书封影该如何调用这些方法,才能让排行榜正确产生呢?

SaveInfo方法的使用非常简单,只需要在玩家游戏结束,输入名字并确认后调用即可。所以,书封影的重点放在了排行榜的自动生成上。

为了更好的管理排行榜,书封影在排行榜所在场景的Hierarchy中格外新增了一个GameObject,名为UIManager。并创建了与其同名的脚本文件,附着于其上。并为其添加了未激活的RankItem的Prefab作为子物件。

在Script中,书封影声明了生成排行榜所要用到的RankItem模板和Grid的Transform,以及一个囊括排行榜脚本的List泛型集合

using ...;
public class UIManager : MonoBehaviour
{
    [Header("RankList")]
    private RankItem rankItemTemplate;
    //The parent of all the rankItem.
    public Transform Grid;
    //Store all of the rankItem in a list.
    private List<RankItem> rankItem_List = new();

}

将Grid拖入Inspector后,书封影在Script的Start方法中,初始化了rankItemTemplate

using ...;
public class UIManager : MonoBehaviour
{
    [Header("RankList")]
    private RankItem rankItemTemplate;
    //The parent of all the rankItem.
    public Transform Grid;
    //Store all of the rankItem in a list.
    private List<RankItem> rankItem_List = new();

    private void Start()
    {
	rankItemTemplate = transform.Find("RankItem").GetComponent<RankItem>();
    }

}

完成了排行榜管理器的各项基础参数初始化后,接下来要做的便是调用方法,生成排行榜了。

生成排行榜-CreateNewRankItem方法

书封影的思路是先在Grid下生成足够多的空白rankItem,而后再依据其Script分别改变其姓名和分数参数。所以书封影先写了返回RankItem脚本的CreateNewRankItem方法用于克隆RankItem。

private void Start()
{...}

/// <summary>
/// Creat a new rankItem and set it in Grid.
/// </summary>
private RankItem CreateNewRankItem()
{
    //Duplicate a rank item by rankItemTemplate.
    GameObject gameObject = Instantiate(rankItemTemplate.gameObject); 
    //Get its scripts.
    RankItem newRankItem = gameObject.GetComponent<RankItem>();
}

由于到此为止,当调用此方法后新建的RankItem位于UIManager下且为未激活状态,所以书封影需要在获得文章中名为“物件化”的一步中创建的RankItemScript后,调整其父级为Grid且为激活状态。

/// <summary>
/// Creat a new rankItem and set it in Grid.
/// </summary>
private RankItem CreateNewRankItem()
{
    //Duplicate a rank item by rankItemTemplate.
    GameObject gameObject = Instantiate(rankItemTemplate.gameObject); 
    //Get its scripts.
    RankItem newRankItem = gameObject.GetComponent<RankItem>();

    //Set parent and to be active in the Canvas.
    newRankItem.transform.SetParent(Grid);
    newRankItem.gameObject.SetActive(true);
}

而后书封影还手动设置了其大小为初始大小,即比例为1

/// <summary>
/// Creat a new rankItem and set it in Grid.
/// </summary>
private RankItem CreateNewRankItem()
{
    //Duplicate a rank item by rankItemTemplate.
    GameObject gameObject = Instantiate(rankItemTemplate.gameObject); 
    //Get its scripts.
    RankItem newRankItem = gameObject.GetComponent<RankItem>();

    //Set parent and to be active in the Canvas.
    newRankItem.transform.SetParent(Grid);
    newRankItem.gameObject.SetActive(true);

    newRankItem.transform.localScale = Vector3.one;
}

这是由于受到Canvas下的名为“Canvas Scaler”的影响,当屏幕比例或者像素比例发生变化时,新增的排名物件在Canvas下可能也会改变大小比例,虽然这样在一定程度上可以帮助UI适应不同的分辨率,但是会严重影响排行榜的美观。

所以书封影才在Scroll View所在的Canvas下,将Canvas Scaler的模式设置为了“Scale With Screen Size”,确保屏幕比例不影响UI物件比例,而后在生成新RankIten物件的CreateNewRankItem方法中,手动设置其比例恒为初始值。

再接着,书封影将此脚本文件添加进了rankItem_List当中,并return

/// <summary>
/// Creat a new rankItem and set it in Grid.
/// </summary>
private RankItem CreateNewRankItem()
{
    ......

    rankItem_List.Add(newRankItem);
    return newRankItem;
}

于是,CreateRankItem方法便写完了,当然,和之前的那些方法一样,这个方法的作用只是增加代码的抽象性,使其不至于显得很混乱,要真正使用这个方法,我们还需要进一步完善整个排行榜的生成架构。

生成排行榜-Initiate方法

完了CreateRankItem之后,书封影需要做的就是正确调用之前的方法并进行排行榜的生成了。

/// <summary>
/// Get the rank at the registry.Or load the rank.
/// </summary>
public void Initiate()
{
    //To be completed.
}
//...
private RankItem CreateNewRankItem()
{...}

由于书封影可能需要设置排行榜的可见范围(rankLimit),所以,每当玩家打开排汗榜时,系统应当将先将原有的所有RankItem物件设为非激活状态(inactive),而后再对排行榜进行排序,并重新激活显示rankLimt内的RankItem

考虑到此,书封影继续写下了后面的代码,将rankItem_List下的所有物件设为inactive

/// <summary>
/// Get the rank at the registry.Or load the rank.
/// </summary>
public void Initiate()
{
    //First , set all of the items to be inactive.

    //To definite the change of the rank information at last open time.
    //It is not must needed if you not need to limit the visible rank.
    if (rankItem_List.Count > 0)
    {
        foreach (RankItem temp in rankItem_List)
        {
            temp.gameObject.SetActive(false);
        }

    }
    //To be completed.
}

而后,书封影调用GameManager下定义的Save类,在此创建了一个List泛型集合,用来获取注册表中的信息。

/// <summary>
/// Get the rank at the registry.Or load the rank.
/// </summary>
public void Initiate()
{
    //First , set all of the items to be inactive.

    //To definite the change of the rank information at last open time.
    //It is not must needed if you not need to limit the visible rank.
    if (rankItem_List.Count > 0)
    {
        foreach (RankItem temp in rankItem_List)
        {
            temp.gameObject.SetActive(false);
        }
    }
    //Get the newest rank information from GameManager.
    List<GameManager.Save> saves = GameManager.instance.GetInfo();
  
    //To be completed.
}

此时,GameManager的GetInfo方法得到了调用。于是,Initate下创建的saves中获取了注册表内已经完成排序的信息(还记得GetInfo方法是什么吗?可以自己去看看)

而后,利用代码,在Grid下创建RankItem并修改信息。

/// <summary>
/// Get the rank at the registry.Or load the rank.
/// </summary>
public void Initiate()
{
    ...
    List<GameManager.Save> saves = GameManager.instance.GetInfo();
    //Foreach the rank list to create a visible UI for every rank item.
    for (int ident = 0;ident < saves.Count;ident++)
    {
        RankItem rankItem = CreateNewRankItem();
        rankItem.Initiate(ident + 1, saves[ident].name, saves[ident].score);
    }
  
  
}

这里调用了rankItem的Initiate方法,此方法定义在RankItem的脚本下,用于修改RankItem的各项信息,如果忘了,可以去本文中的“物件化”板块重新温习。

其实到此为止,排行榜已经基本完成了,但是书封影在尝试使用的时候发现这样做不是很严谨,会导致Untiy报错,这是怎么回事呢?

原来,在书封影于UIManager下的Initiate方法中调用GetInfo创建saves列表时,他没有考虑到游戏初次启动,GetInfo会返回null,导致之后的循环找不到saves的存在,进而不存在saves.Count,于是导致报错。

所以书封影对上述代码进行了轻微修正。

/// <summary>
/// Get the rank at the registry.Or load the rank.
/// </summary>
public void Initiate()
{
    ...
    List<GameManager.Save> saves = GameManager.instance.GetInfo();
    //Foreach the rank list to create a visible UI for every rank item.
    //If the saves is null(at first play), break out.
    if (saves.Count <= 0)
    {
        return;
    }
    //Else 
    else
    {
        //Foreach the rank list to create a visible UI for every rank item.
        for (int ident = 0;ident < saves.Count;ident++)
        {
            RankItem rankItem = CreateNewRankItem();
            rankItem.Initiate(ident + 1, saves[ident].name, saves[ident].score);
        }
    }
}

当游戏初次启动时,不进行下面的循环即可。

生成排行榜-GetRankItem方法

排行榜成功了!书封影刚刚才高兴得以为自己完成了,结果又发现了一个不可忽视的新问题。

书封影试着将Initiate方法在打开排行榜时进行调用,当他对排行榜进行多次开合时,他在Hierarchy窗口中看到了这一幕。

原来,受到Inititate中设置inactive的影响,尽管这些RankItem的内容都相同,他们却都没有得到复用,系统忠诚地依据代码重复地生产着相同的物件。在排行榜物件数量较小时,这样的现象影响不大,但当排行榜有成千上万个物件时,游戏的性能在多次开合后就会达到好几十万甚至上百万个。

显然,这会严重迫害游戏的运作性能。

为了完成对这一现象的优化,书封影在UIManager下新建了一个名为GetRankItem的方法。

private RankItem CreateNewRankItem()
{...}
/// <summary>
/// If an item of the list is not active, then it can be used to save system resources, 
/// if there not, then create a new one.
/// </summary>
/// <returns>A rankItem</returns>
private RankItem GetRankItem()
{
    //To be completed.
}

而后,书封影执行了一个用于检测RankItem列表(rankItem_List)中是否有可以复用覆写的rankitem资源。

/// <summary>
/// If an item of the list is not active, then it can be used to save system resources, 
/// if there not, then create a new one.
/// </summary>
/// <returns>A rankItem</returns>
private RankItem GetRankItem()
{
    foreach (RankItem temp in rankItem_List)
    {
        //If an item of the list is not active, then it can be used.
        if (!temp.gameObject.activeInHierarchy)
        {
            temp.gameObject.SetActive(true);
            return temp;
        }
    }
    //Else create a new one.
    RankItem rankItem = CreateNewRankItem();
    return rankItem;
}

完成了该方法后,书封影再次优化了Initiate代码,将其中的CreateNewRankItem改为了GetRankItem

///...
public void Initiate()
{
    ...
 
    if{...}
    else
    {
        //...
        for (int ident = 0;ident < saves.Count;ident++)
        {
            RankItem rankItem = GetRankItem();
            rankItem.Initiate(ident + 1, saves[ident].name, saves[ident].score);
        }
    }
}

修改完后,书封影再次回到游戏进行尝试,发现问题完美解决了。

复习回顾

OK!至此,书封影已经完全成功地实现了这个基于UGUI,利用注册表进行存储的排行榜系统,虽然路途艰辛但他收获了极强的成就感!激动之下,他决定重新梳理一遍整个系统的实现思路和代码架构,重现排行榜系统设计的思维脉络。

书封影做排行榜的想法纯粹来源于好玩,但是他还是很有策划精神地列下了一个排行榜需求清单,明确了排行榜要实现哪些功能,以及哪些可能的拓展功能。

而后,书封影想到了可以将排行榜分装为单一物件的集合,因为每条排行榜元素的信息种类都是相同的。

之后,书封影的目标是让这些单一的排行榜物件能够自动形成列表的样式,在此目标下,他发现了Grid Layout Group组件的使用方法。

再然后,书封影希望Grid是一个可以拖动浏览的表单,于是他发现了UGUI的Scrow View,并学会了如何使用这一UI框架。

然而,仅仅是UI层面的部署还远远不够,书封影不得不使用代码来完成进一步的构造工作。

于是,他首先利用GameManager实现数据传递,而后思考了如何将排行榜信息进行存储。思考后,书封影选择了利用PlayerPrefs将信息存储于注册表当中。

可是,注册表内的信息形式只能是数字字符串一类,所以书封影决定将排行榜元素的name和score信息以name+score|name+score的格式存储于注册表字符串当中。

这种形成格式和拆解元素的过程类似于数据加密和解密,于是,书封影在GameManager下创建了ParseInfo的方法用于解密,一个SaveInfo的方法用于存储与加密,一个GetInfo的方法用于获取解密后获得的排行榜物件列表,其调用关系如下:

不过这可远远不够,于是书封影又创建了一个UIManager,进一步利用注册表中的数据创建排行榜,在UIManager下,书封影为排行榜系统主要书写了CreateNewRankItem,GetRankItem和Initiate三个方法。调用关系如下:

书封影在玩家结束游戏时调用SaveInfo,打开排行榜时调用Initiate方法,成功实现了排行榜效果。

结语

希望你也成功实现了你自己的排行榜,如果有什么好的建议或者遇到了什么问题,可以联系书封影,与他一起探究Unity的UI系统。

(完)

猜你喜欢

转载自blog.csdn.net/qq_36486755/article/details/128974874
今日推荐