在《火拼 24》的本篇教程中,我们将带你实现客户端-服务端协同的排行榜更新:玩家对战结束后,客户端按结算结果累加总分,通过 UOS PassportFeature SDK 上报至 Passport 排行榜,并同步刷新本地排名。
教程视频
项目工程获取与学习指引
仓库地址:
https://cnb.cool/unity/uos/Rush24Tutorial/-/tree/lesson14-start
分支名称:lesson14-start 分支
分支说明:请先下载 lesson14-start 分支的项目工程,该分支是本节学习的起点。
仓库地址:
https://cnb.cool/unity/uos/Rush24Tutorial/-/tree/lesson14-end
分支名称:lesson14-end 分支
分支说明:lesson14-end 分支包含本节所有功能的完整实现代码,建议在学习完成后参考或用于调试对照。

例如,输入以下命令即可拉取名为 lesson14-start 的分支对应的项目工程:git clone -b lesson14-start https://cnb.cool/unity/uos/Rush24Tutorial.git

教程学习大纲
1. 项目的准备工作
2. 客户端上报分数至排行榜
3. 刷新排行榜界面的 UI 显示
教程操作步骤
接下来我们要开始本节课的学习啦!
请先通过上面提供的 git 仓库链接,下载初始状态(lesson14-start 分支)的项目工程。然后通过 Unity Hub,打开刚刚下载好的项目工程。
在教程中,我们是使用 Unity 2022.3.42 f1c1 版本来打开项目工程的,该教程同样适用于团结引擎,请大家自行选择想要使用的版本来开始你的学习。

打开项目工程以后,首先确保你的项目已经绑定了前几节教程中设置过的同一个 UOS App。

1.2 安装 PassportFeature SDK
Passport Feature 是一款以玩家为核心的在线游戏功能模块,集成了排行榜、成就系统等各类常用功能。在本节教程中,我们将实现其中的排行榜功能——该功能用于展示玩家的游戏成就与排名,以此激励玩家持续提升实力。
下面开始安装 Passport Feature SDK:
在 UOS Launcher 的下拉菜单服务列表中,找到「Passport Feature」选项,点击「Install SDK」按钮,来安装 Passport Feature SDK。

新下载的项目分支工程,请参照《教程七》的讲解步骤,在 UOSEnvironments 窗口填写好你的 Matchmaking Config ID 信息。

在 UOS Passport 服务的「排行榜」页面,点击「立即创建」:

在弹窗中输入排行榜的基本信息:
名字在这里设置为:「对战总分榜」,唯一标识设置为「BattleTotalScore」(需与后续代码中的常量一致),其它选项保持默认不变。

然后就可以看到已经创建好的排行榜了。

接下来,我们将实现在游戏结束时上报玩家分数到排行榜的功能。
在 Scripts/Features/Leaderboard 文件夹下,新建脚本 LeaderboardHelper.cs。

LeaderboardHelper.cs 脚本是一个排行榜系统的辅助类,用于封装 PassportFeature SDK 的排行榜相关功能。它提供了初始化 SDK、上报分数、查询榜单等核心方法,通过统一的接口简化了排行榜系统的调用,并包含完善的错误处理和日志记录。
using System.Collections;using System.Collections.Generic;using UnityEngine;namespace TwentyFour.Scripts.Features.Leaderboard{public static classLeaderboardHelper{}}
创建 Init 方法,作为排行榜功能的初始化入口:
定义布尔变量 _initialized,用于标记 SDK 是否已初始化,避免重复初始化;
在 Init 方法内先调用 Initialize 来初始化 PassportFeature SDK。在初始化过程中如果发生了错误,我们会通过 try...catch... 捕获异常,并打印记录的错误信息,便于代码调试。
using System;using System.Threading.Tasks;using Unity.Passport.Runtime;using Leaderboard;using TwentyFour.Scripts.Features.Player;using Logger = TwentyFour.Scripts.Utilities.Logger;namespace TwentyFour.Scripts.Features.Leaderboard{///<summary>/// 排行榜辅助类,封装 PassportFeatureSDK 排行榜相关 API///</summary>public static class LeaderboardHelper{private static bool _initialized;///<summary>/// 初始化 PassportFeatureSDK///</summary>publicstaticasync Task Init(){if (_initialized) return;try{await PassportFeatureSDK.Initialize();_initialized = true;Logger.Log("LeaderboardHelper Init Success");}catch (Exception e){Logger.LogError($"LeaderboardHelper Init Error: {e}");}}}}
定义排行榜常量 BattleScore:
创建 LeaderboardSlugNames 类,用来集中管理所有排行榜的唯一标识符(SlugName);
其中 BattleScore 变量的值:要和步骤 1.4 中「对战总分榜」的 SlugName(即「BattleTotalScore」)一致。
定义上报分数的方法 UpdateScore:
传入参数:排行榜唯一标识 leaderboardSlugName、要上报的分数 score,返回值: 包含更新后的分数和排名;
首先,要确保 PassportFeature SDK 已经初始化过,若未初始化则先调用 Init 来完成初始化;
然后,调用 SDK 提供的 UpdateScore 方法,实现上报分数到指定排行榜。
namespace TwentyFour.Scripts.Features.Leaderboard{///<summary>/// 排行榜 SlugName 常量定义///</summary>public static class LeaderboardSlugNames{///<summary>/// 对战模式分数排行榜///</summary>public const string BattleScore = "BattleTotalScore";}///<summary>/// 排行榜辅助类,封装 PassportFeatureSDK 排行榜相关 API///</summary>public static class LeaderboardHelper{//此处省略其它代码行......///<summary>/// 上报分数到指定排行榜///</summary>///<param name="leaderboardSlugName">排行榜唯一标识</param>///<param name="score">分数</param>///<returns>更新后的分数信息,失败返回 null</returns>publicstaticasync Task<UpdateScoreResponse> UpdateScore(string leaderboardSlugName, double score){if (!_initialized){await Init();}try{var response = await PassportFeatureSDK.Leaderboard.UpdateScore(leaderboardSlugName, score);Logger.Log($"LeaderboardHelper UpdateScore Success: {leaderboardSlugName}, Score: {response.Score}, Rank: {response.Rank}");return response;}catch (Exception e){Logger.LogError($"LeaderboardHelper UpdateScore Error: {e}");return null;}}}}
定义方法 GetLeaderboardScores:
传入参数:排行榜唯一标识 leaderboardSlugName,最后的返回值 response 中会包含排行榜分数列表;
同理:该方法也需要确保 PassportFeature SDK 已经初始化;
然后调用 SDK 提供的 ListLeaderboardScores 方法,获取排行榜的分数列表。
public static class LeaderboardHelper{//此处省略其它代码行......///<summary>/// 获取指定排行榜的分数列表///</summary>///<param name="leaderboardSlugName">排行榜唯一标识</param>///<returns>排行榜分数列表,失败返回 null</returns>publicstaticasync Task<ListLeaderboardScoresResponse> GetLeaderboardScores(string leaderboardSlugName){if (!_initialized){await Init();}try{var response = await PassportFeatureSDK.Leaderboard.ListLeaderboardScores(leaderboardSlugName);Logger.Log($"LeaderboardHelper GetLeaderboardScores Success: {leaderboardSlugName}, Total: {response.Total}, Count: {response.Count}");return response;}catch (Exception e){Logger.LogError($"LeaderboardHelper GetLeaderboardScores Error: {e}");return null;}}}
定义方法 GetMyScore:
传入参数:排行榜唯一标识 leaderboardSlugName;返回值:一个 LeaderboardMemberScore 对象,包含分数、排名和显示名称信息;
首先,确保 PassportFeature SDK 已经初始化;
然后,调用 SDK 提供的 GetScore 方法,获取当前玩家在指定排行榜中的分数信息;
接着,处理响应结果:检查 response 中的分数列表,如果有分数则返回第一条记录,则构造包含真实数据的 LeaderboardMemberScore 对象返回。如果玩家没有分数记录或发生错误,则返回默认值(分数为 0、排名为 0、显示名称为当前玩家名称)。
public static class LeaderboardHelper{// 此处省略其它代码行......///<summary>/// 获取当前玩家在指定排行榜中的分数///</summary>///<param name="leaderboardSlugName">排行榜唯一标识</param>///<returns>当前玩家的分数,如果没有分数则返回 0</returns>publicstaticasync Task<LeaderboardMemberScore> GetMyScore(string leaderboardSlugName){if (!_initialized){await Init();}var defaultMemberScore = new LeaderboardMemberScore(){Score = 0,Rank = 0,DisplayName = Identity.CurrentPersona.DisplayName};try{var response = await PassportFeatureSDK.Leaderboard.GetScore(leaderboardSlugName);if (response.Scores.Count > 0){var score = response.Scores[0];return new LeaderboardMemberScore(){Score = score.Score,Rank = score.Rank,DisplayName = score.DisplayName};}return defaultMemberScore;}catch (Exception e){Logger.LogError($"LeaderboardHelper GetMyScore Error: {e}");return defaultMemberScore;}}}
创建角色完成后,会调用回调方法 OnCreatePersonaComplete,然后执行协程 SelectPersonaAndInit,依次初始化远程配置、关卡、存档,最后进入主场景。在此过程中,同样需要初始化排行榜。
打开数据加载脚本 LoadGameData.cs,在协程方法 Init() 中调用初始化排行榜的方法 InitLeaderboard;
然后在 InitLeaderboard 方法中,调用 LeaderboardHelper.cs 中的 Init 方法完成排行榜 SDK 初始化,并使用 WaitUntil 等待异步任务完成;
此外,在初始化其它系统之前,需要先调用 PassportSDK.Identity.SelectPersona() 向服务器绑定当前使用的角色。服务器会为该角色颁发对应的访问令牌(Access Token),确保后续排行榜分数操作与正确的角色关联。
using TwentyFour.Scripts.Features.Leaderboard;namespace TwentyFour.Scripts.Gameplay.HomePage{public class LoadGameData : MonoBehaviour{//此处省略其它代码行......voidOnCreatePersonaComplete(Persona p){Identity.CurrentPersona = p;Logger.LogInfo($"创建角色成功,角色ID:{Identity.CurrentPersona.PersonaID}");StartCoroutine(SelectPersonaAndInit());#if (UNITY_WEIXINMINIGAME || MINIGAME_SUBPLATFORM_WEIXIN) && !UNITY_EDITORGetWechatUserInfo.Hide();#endif}IEnumerator SelectPersonaAndInit(){var selectTask = PassportSDK.Identity.SelectPersona(Identity.CurrentPersona.PersonaID);yieldreturnnewWaitUntil(() => selectTask.IsCompleted);yieldreturnStartCoroutine(Init());}IEnumerator Init(){MuninnManager.InitSync();yieldreturnStartCoroutine(InitRemoteConfig());yieldreturnStartCoroutine(InitLeaderboard());yieldreturnStartCoroutine(InitStage());yieldreturnStartCoroutine(InitSave());GameRouter.LoadHomeSceneFirst();}IEnumerator InitLeaderboard(){var t = LeaderboardHelper.Init();yieldreturnnewWaitUntil(() => t.IsCompleted);}}}
在游戏结束时,MuninnMessageHandler.cs 脚本的 Distribute 方法会响应 EndGame 类型服务器消息,触发排行榜分数更新流程,核心实现通过 UploadMyScore 方法完成。
定义方法 UploadMyScore:
传入参数:包含队伍信息的字典 teamInfo,其中记录了各队伍的玩家 ID 和本局得分;
首先,该方法遍历队伍信息字典(teamInfo),根据当前玩家的 PersonaID 查找对应的队伍;
然后,从所属队伍的 teamInfo 中获取本局游戏分数(currentGameScore),并调用 LeaderboardHelper.GetMyScore() 获取玩家在排行榜中的历史总分(originalScore),将两者相加计算出最新总分数(newScore);
接着,要确保计算后的分数不小于 0;
最后,调用 LeaderboardHelper.UpdateScore 方法将更新后的总分数上传到排行榜,并同步记录操作日志。
using TwentyFour.Scripts.Features.Leaderboard;using TwentyFour.Scripts.Features.Player;namespace TwentyFour.Scripts.Features.OnlineMultiplayer.Sync{public class MuninnMessageHandler{publicstaticvoidDistribute(byte[] data){var serverData = Encoding.UTF8.GetString(data);var serverMessage = JsonConvert.DeserializeObject<MuninnMessageModel>(serverData);var type = MuninnMessageModel.GetMessageType(serverMessage.type);switch (type){//此处省略其它代码行......case MuninnMessageModel.Type.EndGame:Logger.Log("[MuninnMessageHandler] 游戏结束");_latestBattleData.teamInfo = serverMessage.teamInfo;_latestBattleData.progress = serverMessage.progress;_latestBattleData.usedTime = serverMessage.usedTime;UploadMyScore(serverMessage.teamInfo);OnEndGame.Invoke(serverMessage);break;}}privatestaticasyncvoidUploadMyScore(Dictionary<string, TeamInfo> teamInfo){if (teamInfo == null || Identity.CurrentPersona == null) return;foreach (var kvp in teamInfo){if (kvp.Value.teamPlayerID == Identity.CurrentPersona.PersonaID){var currentGameScore = kvp.Value.score;var originalScoreItem = await LeaderboardHelper.GetMyScore(LeaderboardSlugNames.BattleScore);var originalScore = originalScoreItem.Score;var newScore = originalScore + currentGameScore;if (newScore < 0) newScore = 0;Logger.Log($"[MuninnMessageHandler] 上传分数: 原分数={originalScore}, 本局={currentGameScore}, 新分数={newScore}");await LeaderboardHelper.UpdateScore(LeaderboardSlugNames.BattleScore, newScore);break;}}}}}
查看游戏结束时的日志输出:已经计算出了当前玩家的最新得分和排名情况。

同时登录 UOS Passport 服务的「排行榜」页面,进入创建的「对战总分榜」,查看「榜单数据」,看到当前已经登录的角色的得分排名情况和编辑器中的日志输出的信息是一样的,说明分数上报成功了。

但是 Game 窗口的「排行榜」的 UI 界面显示还没有刷新。

接着,我们来实现将排行榜的榜单数据实时更新显示到排行榜的 UI 界面上。
可以查看下 TwentyFour/Prefabs/Features/Leaderboard 路径下的预制物体 LeaderboadItem.prefab,它是排行榜面板中用于显示当前玩家分数的固定条目,且已经挂载好了脚本组件 LeaderboardItem.cs。

在排行榜列表中,每个玩家条目都使用 LeaderboardItem.cs 脚本来显示该玩家的信息。脚本中已定义了三个 Text 组件变量,分别用于显示玩家名称、排名和分数。
通过定义 SetData 方法,实现排行榜数据与 UI 的绑定:
该方法接收 LeaderboardMemberScore 类型的排行榜数据作为参数;
从传入的 scoreData 对象中获取排行榜成员的名称、排名和分数后,将获取的数据分别赋值给对应的 Text 组件的 text 属性,从而在界面上显示玩家信息。
using UnityEngine;using UnityEngine.UI;using Leaderboard;namespace TwentyFour.Scripts.Features.Leaderboard{ public class LeaderboardItem : MonoBehaviour { [SerializeField] private Text displayNameText; [SerializeField] private Text rankText; [SerializeField] private Text scoreText; publicvoidSetData(LeaderboardMemberScore scoreData) { if (displayNameText != null) displayNameText.text = scoreData.DisplayName; if (rankText != null) rankText.text = scoreData.Rank.ToString(); if (scoreText != null) scoreText.text = ((long)scoreData.Score).ToString(); } }}
打开 MainScene 场景,查看排行榜界面上的 LeaderboardPanel 面板,已经挂载了LeaderboardPanel.cs 脚本组件 ,并完成了 UI 变量的赋值。

打开 LeaderboardPanel.cs 脚本,实现 RefreshLeaderboardAsync 方法:
当玩家打开排行榜界面或需要刷新排行榜数据时,会调用 RefreshLeaderboardAsync 方法。
首先,调用排行榜辅助类 LeaderboardHelper.cs 的 GetLeaderboardScores 静态方法,异步从服务器获取对战模式的排行榜数据。通过空值检查处理网络请求失败或排行榜数据不存在等情况,防止空引用异常。
然后,遍历排行榜容器(leaderboardItemContainer)的所有子对象并销毁,清空现有的排行榜列表,避免旧数据残留。
接着,遍历返回的排行榜数据,为每个分数创建一个新的排行榜条目(item),获取其挂载的 LeaderboardItem.cs 组件,调用 SetData 方法将排行榜数据绑定到 UI。
最后,单独更新排行榜前三名的玩家头像和名称,为前三名提供特殊的显示效果。
namespace TwentyFour.Scripts.Features.Leaderboard{public class LeaderboardPanel : MonoBehaviour{[SerializeField] private GameObject leaderboardItemContainer;[SerializeField] private GameObject leaderboardItemPrefab;[SerializeField] private GameObject myLeaderboardItem;[SerializeField] private List<Text> topThreeNames;[SerializeField] private List<GameObject> topThreeAvatars;publicasyncvoidOpenPanel(){await RefreshLeaderboardAsync();await ShowMyScore();gameObject.SetActive(true);}publicasync Task RefreshLeaderboardAsync(){var response = await LeaderboardHelper.GetLeaderboardScores(LeaderboardSlugNames.BattleScore);if (response == null) return;// Clear existing itemsforeach (Transform child in leaderboardItemContainer.transform){Destroy(child.gameObject);}// Create items for each scorefor (var i = 0; i < response.Scores.Count; i += 1){var score = response.Scores[i];var item = Instantiate(leaderboardItemPrefab, leaderboardItemContainer.transform);var leaderboardItem = item.GetComponent<LeaderboardItem>();if (leaderboardItem != null){leaderboardItem.SetData(score);}// set top three scoresif (i < topThreeAvatars.Count){topThreeAvatars[i].GetComponent<PlayerCharatorManager>().InitPlayerAvatar(new Dictionary<string, string>(score.PersonaProperties));}if (i < topThreeNames.Count){topThreeNames[i].text = score.DisplayName;}}}}}
继续实现 ShowMyScore 方法:
当玩家打开排行榜界面时,会调用 ShowMyScore 方法显示当前玩家在排行榜中的分数:
首先,调用排行榜辅助类 LeaderboardHelper.cs 的 GetMyScore 静态方法,异步从服务器获取当前玩家在对战模式排行榜中的分数。
然后,获取"我的排行榜条目"(myLeaderboardItem)上挂载的 LeaderboardItem 组件,通过空值检查确保组件存在后,调用 SetData 方法将当前玩家的分数数据绑定到 UI,在界面上显示玩家的排名和分数信息。
namespace TwentyFour.Scripts.Features.Leaderboard{public class LeaderboardPanel : MonoBehaviour{//此处省略其它代码行......privateasync Task ShowMyScore(){var myScore = await LeaderboardHelper.GetMyScore(LeaderboardSlugNames.BattleScore);var leaderboardItem = myLeaderboardItem.GetComponent<LeaderboardItem>();if (leaderboardItem != null){leaderboardItem.SetData(myScore);}}}}
在游戏主界面点击「排行榜」按钮:

打开排行榜窗口后,可以看到排行榜列表中的排名和分数信息与 UOS App「排行榜」中的数据一致,说明分数上报成功并已同步到 UI 显示。


下节教程预告
教程主题——《火拼24》系列教程十五:服务器端排行榜更新
在《火拼 24》的下一篇教程中,我们将实现通过 Lua Plugin 获取并整理整场对局的关键数据,并在对局结束后将结果提交到云函数;云函数根据对局胜负与得分结果更新排行榜信息,完成服务器端的排行统计与写入流程。
记得锁定更新,别错过每一步关键指南!下一篇教程见哦!

Unity Online Services (UOS) 是一个专为游戏开发者设计的一站式游戏云服务平台,提供覆盖游戏全生命周期的开发、运营和推广支持。





每一个“点赞”、“在看”,都是我们前进的动力

点击“阅读原文”,进入 Git 仓库下载项目工程!
