排行榜指南
环境要求
- Unity
- Android
- iOS
- UE4
- Unity 2019.4 或更高版本
- iOS 11 或更高版本,Xcode 版本 14.1 或更高版本
- Android 5.0(API level 21)或更高版本
Android 5.0(API level 21)或更高版本
iOS 11 或更高版本,Xcode 版本 14.1 或更高版本
- 安装 UE 4.26 及以上版本
- iOS 12 或更高版本
- Android 5.0(API level 21)或更高版本
支持平台:Android / iOS
权限说明
- Unity
- Android
- iOS
- UE4
该模块需要如下权限:
权限 | 使用目的 | 权限申请时机 |
---|---|---|
网络权限 | 用于访问网络数据 | 用户首次使用该功能时会申请权限 |
该模块将在应用中添加如下权限:
<uses-permission android:name="android.permission.INTERNET" />
集成前准备
- 参考 准备工作 创建应用、开启内建账户、排行榜服务、绑定 API 域名;
SDK 获取
排行榜功能是数据存储 SDK 的一部分,排行榜 SDK 配置与数据存储 SDK 配置一致;
- Unity
- Android
- iOS
- UE4
SDK 可以通过 Unity Package Manager 导入或手动导入,二者任选其一。请根据项目需要选择。
方法一:使用 Unity Package Manager
在项目的 Packages/manifest.json
文件中添加以下依赖:
"dependencies":{
"com.leancloud.realtime": "https://github.com/leancloud/csharp-sdk-upm.git#realtime-2.3.0",
"com.leancloud.storage": "https://github.com/leancloud/csharp-sdk-upm.git#storage-2.3.0",
}
在 Unity 顶部菜单中选择 Window > Package Manager 可查看已经安装在项目中的包。
方法二:手动导入
-
在 下载页 找到 LeanCloud C# SDK 下载地址,下载
LeanCloud-SDK-Realtime-Unity.zip
。 -
解压后的
LeanCloud-SDK-Realtime-Unity.zip
为 Plugins 文件夹,拖拽至 Unity 即可。
- 打开项目的
project/app/build.gradle
文件,添加 gradle 配置如下:
dependencies {
...
implementation 'com.taptap:lc-storage-android:8.2.24'
}
- 下载 TapSDK UE4,TapSDK-UE4-xxx.zip 解压后将
LeanCloud
文件夹 Copy 到项目的Plugins
目录中 - 重启 Unreal Editor
- 打开 编辑 > 插件 > 项目 > LeanCloud,开启
LeanCloud
模块
添加依赖
在 Project.Build.cs 中添加所需模块:
PublicDependencyModuleNames.AddRange(new string[] { "Core",
"CoreUObject",
"Engine",
"Json",
"InputCore",
"JsonUtilities",
"SlateCore",
});
if (Target.Platform == UnrealTargetPlatform.IOS || Target.Platform == UnrealTargetPlatform.Android)
{
PublicDependencyModuleNames.AddRange(
new string[]
{
"LeanCloudMobile"
}
);
}
else
{
PublicDependencyModuleNames.AddRange(
new string[]
{
"LeanCloud"
}
);
}
导入头文件
#include "LCLeaderboard.h"
SDK 初始化
内建账户 SDK 初始化完成后会同步初始化排行榜功能,因此无需重复初始化;
快速入门
创建排行榜
创建排行榜有 3 种方式:
- 在 TapTap 开发者中心的排行榜控制台创建。
- 在服务端等受信任的环境下调用 REST API 创建。
- 在服务端等受信任的环境下调用 SDK 的管理接口创建。
比如,可以在控制台创建一个名称为 world
,成员类型为「内建账户」,排序为 descending
,更新策略为 better
,重置周期为「每月」的世界排行榜。
提交成绩
假设玩家已登录(currentUser
),通过如下代码即可更新成绩:
- Unity
- Android
- iOS
- UE4
var statistic = new Dictionary<string, double>();
statistic["world"] = 20.0;
await LCLeaderboard.UpdateStatistics(currentUser, statistic);
Map<String, Double> statistic = new HashMap<>();
statistic.put("world", 20.0);
LCLeaderboard.updateStatistic(currentUser, statistic).subscribe(new Observer<LCStatisticResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull LCStatisticResult jsonObject) {
// scores saved
}
@Override
public void onError(@NotNull Throwable throwable) {
// handle error
}
@Override
public void onComplete() {}
});
NSDictionary *statistic = @{
@"world" : 20.0,
};
[LCLeaderboard updateCurrentUserStatistics:statistic, callback:^(NSArray *statistics, NSError *error) {
if (statistics) {
// statistics 是更新后你的最好/最新成绩
} else if (error) {
// 处理错误
}
}];
TMap<FString, double> Statistic;
Statistic.Add("world", 20.0);
FLCLeaderboard::FStatisticsDelegate OnSuccess = FLCLeaderboard::FStatisticsDelegate::CreateLambda([=](TArray<FLCLeaderboardStatistic> Statistics) {
// statistics 是更新后你的最好/最新成绩
});
FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) {
// 处理错误
});
FLCLeaderboard::UpdateCurrentUserStatistics(Statistic, OnSuccess, OnError);
获取名次
接下来我们获取排行榜中的前十,由于之前我们只提交了一个玩家(当前玩家)的成绩,所以结果中只包含一个成绩。
- Unity
- Android
- iOS
- UE4
var leaderboard = LCLeaderboard.CreateWithoutData("world");
var rankings = await leaderboard.GetResults(limit: 10);
LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world");
leaderboard.getResults(0, 10, null, null).subscribe(new Observer<LCLeaderboardResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull LCLeaderboardResult leaderboardResult) {
List<LCRanking> rankings = leaderboardResult.getResults();
// process rankings
}
@Override
public void onError(@NotNull Throwable throwable) {
// handle error
}
@Override
public void onComplete() {}
});
LCLeaderboard *leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"world"];
leaderboard.limit = 10;
[leaderboard getUserResultsWithOption:nil callback:^(NSArray *rankings, NSInteger count, NSError *error) {
// rankings 是排行榜前十的排名信息
}];
FLCLeaderboard Leaderboard("world", FLCLeaderboard::User);
Leaderboard.Limit = 10;
FLCLeaderboard::FRankingsDelegate OnSuccess = FLCLeaderboard::FRankingsDelegate::CreateLambda([=](TArray<FLCLeaderboardRanking> Rankings, int64 Count) {
// process rankings
});
FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) {
// handle error
});
Leaderboard.GetResults(OnSuccess, OnError);
看过上面这个最简单的接入排行榜的例子后,下面就详细介绍排行榜的各个接口。
成绩管理
更新用户成绩
当用户完成了一局游戏后,你可以在客户端使用 SDK 的 updateStatistic
方法更新该用户的成绩。
不过,从反作弊的角度出发,我们建议在控制台勾选「只允许使用 Master Key 更新分数」,然后在服务端更新成绩。
- Unity
- Android
- iOS
- UE4
var statistic = new Dictionary<string, double> {
{ "score", 3458.0 },
{ "kills", 28.0 }
};
await LCLeaderboard.UpdateStatistics(currentUser, statistic);
Map<String, Double> statistic = new HashMap<>();
statistic.put("score", 3458.0);
statistic.put("kills", 28.0);
LCLeaderboard.updateStatistic(currentUser, statistic).subscribe(new Observer<LCStatisticResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull LCStatisticResult jsonObject) {
// saved
}
@Override
public void onError(@NotNull Throwable throwable) {
// handle error
}
@Override
public void onComplete() {}
});
NSDictionary *statistic = @{
@"score" : 3458.0,
@"kills" : 28.0,
};
[LCLeaderboard updateCurrentUserStatistics:statistic callback:^(NSArray *statistics, NSError *error) {
if (!error) {
// saved
}
}];
TMap<FString, double> Statistic;
Statistic.Add("score", 3458.0);
Statistic.Add("kills", 28.0);
FLCLeaderboard::FStatisticsDelegate OnSuccess = FLCLeaderboard::FStatisticsDelegate::CreateLambda([=](TArray<FLCLeaderboardStatistic> Statistics) {
// saved
});
FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) {
// handle error
});
FLCLeaderboard::UpdateCurrentUserStatistics(Statistic, OnSuccess, OnError);
更新用户成绩需要玩家登录,玩家只能更新自己的成绩。
你可以一次性更新多个排行榜的成绩,比如上面的示例中同时更新了得分(score
)和击杀(kills
)两个排行榜的成绩。
客户端无法更新 object 和 entity 的成绩。 如需更新其他用户、object、entity 的成绩,需要使用 REST API 或 SDK 提供的管理接口。
删除用户成绩
玩家也可以删除自己的成绩:
- Unity
- Android
- iOS
- UE4
await LCLeaderboard.DeleteStatistics(currentUser, new List<string> { "world" });
// 暂未支持
[LCLeaderboard deleteCurrentUserStatistics:@[@"world"], callback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// 删除成功
} else if (error) {
// 处理错误
}
}];
// 暂未支持
和更新成绩一样,玩家只能删除自己的成绩。 如需删除其他用户、object、entity 的成绩,需要使用 REST API 或 SDK 提供的管理接口。
查询排行榜成员成绩
已登录用户可以通过 GetStatistics
方法查询其他用户在所有排行榜中的成绩:
- Unity
- Android
- iOS
- UE4
var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b");
var statistics = await LCLeaderboard.GetStatistics(otherUser);
foreach(var statistic in statistics) {
Debug.Log(statistic.Name);
Debug.Log(statistic.Value);
}
// 查询排行榜成员成绩
LCUser otherUser = null;
try {
otherUser = LCUser.createWithoutData(LCUser.class, "5c76107144d90400536fc88b");
} catch (LCException e) {
e.printStackTrace();
}
LCLeaderboard.getUserStatistics(otherUser).subscribe(new Observer<LCStatisticResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull LCStatisticResult lcStatisticResult) {
List<LCStatistic> statistics = lcStatisticResult.getResults();
for (LCStatistic statistic : statistics) {
Log.d(TAG, statistic.getName());
Log.d(TAG, String.valueOf(statistic.getValue()));
}
}
@Override
public void onError(@NotNull Throwable throwable) {
// handle error
Toast.makeText(MainActivity.this, "查询失败: " + throwable.getMessage(), Toast.LENGTH_SHORT).show();
}
@Override
public void onComplete() {}
});
NSString *otherUserObjectId = @"5c76107144d90400536fc88b";
[LCLeaderboard getStatisticsWithUserId:otherUserObjectId, statisticNames:nil, callback:^(NSArray<LCLeaderboardStatistic *> * _Nullable *statistics, NSError _Nullable *error) {
if (statistics) {
for (LCLeaderboardStatistic *statistic in statistics) {
NSLog(@"排行榜名称:%@", statistic.name);
NSLog(@"成绩:%f", statistic.value);
}
} else if (error) {
// 处理错误
}
}];
FLCLeaderboard::FStatisticsDelegate OnSuccess = FLCLeaderboard::FStatisticsDelegate::CreateLambda([=](TArray<FLCLeaderboardStatistic> Statistics) {
for (FLCLeaderboardStatistic Statistic : Statistics) {
// 排行榜名称:Statistic.Name;
// 成绩:Statistic.Value;
}
});
FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) {
// 处理错误
});
FString OtherUserObjectId = "5c76107144d90400536fc88b";
FLCLeaderboard::GetStatistics(OtherUserObjectId, OnSuccess, OnError, {}, FLCLeaderboard::User);
更多的场景下,只关心用户在某个或某几个排行榜中的成绩。 查询时指定相应排行榜的名称即可:
- Unity
- Android
- iOS
- UE4
var statistics = await LCLeaderboard.GetStatistics(otherUser, new List<string> { "world" });
LCLeaderboard.getUserStatistics(otherUser, Arrays.asList("world")).subscribe(/** 略 **/);
[LCLeaderboard getStatisticsWithUserId:otherUserObjectId, statisticNames:@[@"world"], callback:^(NSArray<LCLeaderboardStatistic *> * _Nullable statistics, NSError * _Nullable error) {
// 略
}];
FLCLeaderboard::GetStatistics(OtherUserObjectId, OnSuccess, OnError, {"world"}, FLCLeaderboard::User);
类似地,可以查询 object 和 entity 的成绩。
- Unity
- Android
- iOS
- UE4
例如,假定武器排行榜是 object 排行榜:
var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b");
var statistics = await LCLeaderboard.GetStatistics(excalibur, new List<string> { "weapons" });
如果武器排行榜是 entity 排行榜:
var statistics = await LCLeaderboard.GetStatistics("excalibur", new List<string> { "weapons" });
例如,假定武器排行榜是 object 排行榜:
String excaliburObjectId = "582570f38ac247004f39c24b";
LCLeaderboard.getMemberStatistics("Weapon", excaliburObjectId,
Arrays.asList("weapons")).subscribe(/** 略 **/);
如果武器排行榜是 entity 排行榜:
LCLeaderboard.getMemberStatistics(LCLeaderboard.MEMBER_TYPE_ENTITY, "excalibur",
Arrays.asList("weapons")).subscribe(/** 略 **/);
顺便提下,之前提到的 getUserStatistics
方法
LCLeaderboard.getUserStatistics(otherUser, Arrays.asList("weapons")).subscribe(/** 略 **/);
等价于:
LCLeaderboard.getMemberStatistics(LCLeaderboard.LCLeaderboard.MEMBER_TYPE_USER,
otherUser.getObjectId(),
Arrays.asList("weapons")).subscribe(/** 略 **/);
例如,假定武器排行榜是 object 排行榜:
NSString *excaliburObjectId = @"582570f38ac247004f39c24b";
[LCLeaderboard getStatisticsWithObjectId:excaliburObjectId, statisticNames:@[@"weapons"],
option:nil
callback:^(NSArray *statistics, NSError *error) {
// 略
}];
如果武器排行榜是 entity 排行榜:
[LCLeaderboard getStatisticsWithEntity:@"excalibur", statisticNames:@[@"weapons"],
callback:^(NSArray<LCLeaderboardStatistic *> * _Nullable *statistics, NSError * _Nullable error) {
// 略
}];
例如,假定武器排行榜是 object 排行榜:
FString ExcaliburObjectId = "582570f38ac247004f39c24b";
FLCLeaderboard::GetStatistics(ExcaliburObjectId, OnSuccess, OnError, {}, FLCLeaderboard::Object);
如果武器排行榜是 entity 排行榜:
FLCLeaderboard::GetStatistics("excalibur", OnSuccess, OnError, {}, FLCLeaderboard::Entity);
最后,还可以查询一组成员的成绩:
- Unity
- Android
- iOS
- UE4
var otherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "5c76107144d90400536fc88b");
var anotherUser = LCObject.CreateWithoutData(TDSUser.CLASS_NAME, "672a127144a90d00536f3456");
var statistics = await LCLeaderboard.GetStatistics({otherUser, anotherUser}, new List<string> { "world" });
var oneObject = LCObject.CreateWithoutData<LCObject>("abccb27133a90ddd536ffffa");
var anotherUser = LCObject.CreateWithoutData<LCObject>("672a1279345777005a2b2444");
var statistics = await LCLeaderboard.GetStatistics({oneObject, anotherObject}, new List<string> { "weapons" });
var statistics = await LCLeaderboard.GetStatistics({"Sylgr", "Leiptr"}, new List<string> { "rivers" });
// 暂未支持
NSString *otherUserObjectId = @"5c76107144d90400536fc88b";
NSString *anotherUserObjectId = @"672a127144a90d00536f3456";
[leaderboard getStatisticsWithUserIds:@[otherUserObjectId, anotherUserObjectId]
callback:^(NSArray<LCLeaderboardStatistic *> * _Nullable statistics, NSError * _Nullable error) {
// 略
}];
NSString *oneObjectId = @"abccb27133a90ddd536ffffa";
NSString *anotherObjectId = @"672a1279345777005a2b2444";
[leaderboard getStatisticsWithObjectIds:@[oneObjectId, anotherObjectId]
option:nil
callback:^(NSArray<LCLeaderboardStatistic *> * _Nullable statistics, NSError * _Nullable error) {
// 略
}];
[leaderboard getStatisticsWithEntities:@[@"Sylgr", "Leiptr"]
callback:^(NSArray<LCLeaderboardStatistic *> * _Nullable statistics, NSError * _Nullable error) {
// 略
}];
// 暂未支持
获取排行榜结果
通过 Leaderboard#getResults
方法可以获取排行榜结果。
最常见的使用场景是获取排名前 N 的用户成绩和获取当前登录用户附近的排名。
首先,我们构造一个排行榜实例:
- Unity
- Android
- iOS
- UE4
var leaderboard = LCLeaderboard.CreateWithoutData("world");
LCLeaderboard.CreateWithoutData
方法接受两个参数:
public static LCLeaderboard CreateWithoutData(string statisticName, string memberType = LCLeaderboard.USER_MEMBER_TYPE)
statisticName
为排行榜名称,这里需要传入云端已经存在的排行榜名称。上例中是world
。memberType
为排行榜成员类型,传入LCLeaderboard.USER_MEMBER_TYPE
表示成员类型为用户,传入LCLeaderboard.ENTITY_MEMBER_TYPE
表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。上例中省略了这一参数,表示使用默认值,成员类型为用户。
LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world");
LCLeaderboard.createWithoutData
方法接受两个参数:
public static LCLeaderboard createWithoutData(String name, String memberType)
name
为排行榜名称,这里需要传入云端已经存在的排行榜名称。上例中是world
。memberType
为排行榜成员类型,传入LCLeaderboard.MEMBER_TYPE_USER
表示成员类型为用户,传入LCLeaderboard.MEMBER_TYPE_ENTITY
表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。用户是最常用的排行榜类型,因此createWithoutData
还提供了一个单参数的重载方法,上例中就只传入了排行榜名称,此时成员类型为用户。
LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"world"];
FLCLeaderboard Leaderboard("world", FLCLeaderboard::User);
FLCLeaderboard
初始化方法接受两个参数:
Name
为排行榜名称,这里需要传入云端已经存在的排行榜名称。上例中是world
。InMemberType
为排行榜成员类型,传入FLCLeaderboard::User
表示成员类型为用户,传入FLCLeaderboard::Entity
表示成员类型为 entity,传入FLCLeaderboard::Object
表示成员类型为 object
构造排行榜实例后,调用该实例上的相应方法即可获取排名。
获取指定区间的排名
获取排行榜的 Top 10:
- Unity
- Android
- iOS
- UE4
var rankings = await leaderboard.GetResults(limit: 10);
GetResults
方法可以指定以下参数限制查询范围:
参数名 | 类型 | 说明 |
---|---|---|
aroundUser | LCUser | 获取某用户附近的排名,详见下节。 |
aroundObject | LCObject | 获取某 object 附近的排名,详见下节 |
aroundEntity | string | 获取某 entity 附近的排名,详见下节。 |
limit | number | 限制返回的结果数量,默认 10。 |
skip | number | 指定从某个位置开始获取,与 limit 一起可以实现翻页,默认 0。 |
selectKeys | string[] | 指定返回的 Ranking 中的 user 需要包含的属性,详见下文。 |
includeKeys | string[] | 指定返回的 Ranking 中的 user 需要包含的 Pointer 属性,详见下文。 |
includeStatistics | string[] | 指定返回的 Ranking 中需要包含的其他成绩,详见下文。 |
version | number | 指定返回某个历史版本的成绩。 |
返回的结果是一个排名数组(Ranking[]
),Ranking
上有如下属性:
属性 | 类型 | 说明 |
---|---|---|
Rank | int | 排名,从 0 开始 |
User | LCUser | 该成绩的用户(user 排行榜) |
Object | LCObject | 该成绩的 object(object 排行榜) |
Entity | string | 该成绩的 entity(entity 排行榜) |
Value | double | 成绩 |
IncludedStatistics | List<Statistic> | 该成员在其他排行榜的成绩 |
leaderboard.getResults(0, 10, null, null).subscribe(new Observer<LCLeaderboardResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull LCLeaderboardResult leaderboardResult) {
List<LCRanking> rankings = leaderboardResult.getResults();
}
@Override
public void onError(@NotNull Throwable throwable) {
// handle error
}
@Override
public void onComplete() {}
});
Leaderboard#getResults
方法可以指定以下参数限制查询范围:
Observable<LCLeaderboardResult> getResults(
int skip, int limit,
List<String> selectMemberKeys, List<String> includeStatistics)
skip
指定从某个位置开始获取,与limit
一起可以实现翻页,默认 0limit
限制返回的结果数量,默认 20selectMemberKeys
指定返回的 LCRanking 中排行榜成员需要包含的属性,详见下文includeStatistics
指定返回的 LCRanking 中需要包含的其他成绩,详见下文
如需获取历史版本的排行榜信息,可以在排行榜实例上设置版本号:
int previousVersion = currentVersion - 1;
leaderboard.setVersion(previousVersion);
LCRanking 提供了以下方法供获取排名信息:
// 排名,从 0 开始
int getRank()
// 该成绩的用户(user 排行榜)
LCUser getUser()
// 该成绩的 object(object 排行榜)
LCObject getObject()
// 该成绩的 entity(entity 排行榜)
String getEntityId()
// 成绩
double getStatisticValue()
// 该成员在其他排行榜的成绩
List<LCStatistic> getIncludedStatistics()
leaderboard.limit = 10;
[leaderboard getUserResultsWithOption:nil,
callback:^(NSArray <LCLeaderboardRanking *> * _Nullable *rankings, NSInteger count, NSError * _Nullable error) {
// rankings 是排行榜前十的排名信息
}];
Leaderboard 实例上可以设置如下属性限制查询范围:
/// 指定从某个位置开始获取,与 limit 一起可以实现翻页,默认 0
@property (nonatomic) NSInteger skip;
/// 限制返回的结果数量,默认 20
@property (nonatomic) NSInteger limit;
/// 指定返回的 LCLeaderboardRanking 中需要包含的其他成绩
@property (nonatomic, nullable) NSArray<NSString *> *includeStatistics;
/// 指定排行榜版本号,默认 0
@property (nonatomic) NSInteger version;
getUserResultsWithOption
的第一个参数是 LCLeaderboardQueryOption
,可以指定返回的 LCLeaderboardRanking
中排行榜成员需要包含的属性,详见下文。
回调中的 rankings
是一个 LCLeaderboardRanking 数组。
LCLeaderboardRanking 包含如下属性:
// 排行榜名称
@property (nonatomic, readonly, nullable) NSString *statisticName;
/// 排名,从 0 开始
@property (nonatomic, readonly) NSInteger rank;
/// 成绩
@property (nonatomic, readonly) double value;
/// 该成员在其他排行榜的成绩
@property (nonatomic, readonly, nullable) NSArray<LCLeaderboardStatistic *> *includedStatistics;
/// 该成绩的用户(user 排行榜)
@property (nonatomic, readonly, nullable) LCUser *user;
/// 该成绩的 object(object 排行榜)
@property (nonatomic, readonly, nullable) LCObject *object;
/// 该成绩的 entity(entity 排行榜)
@property (nonatomic, readonly, nullable) NSString *entity;
上例我们调用的是 getUserResultsWithOption
方法,所以 user
属性不为空,object
、entity
属性均为空。
如果排行榜是 object 排行榜或 entity 排行榜,那么相应地需要调用 getObjectResultsWithOption
及 getEntityResultsWithCallback
方法。
getObjectResultsWithOption
的参数和 getUserResultsWithOption
相同。
因为 entity 排行榜的成员为字符串,所以 getEntityResultsWithCallback
不支持 LCLeaderboardQueryOption
,第一个参数就是回调,回调的参数和 getUserResultsWithOption
、getObjectResultsWithOption
相同:
- (void)getEntityResultsWithCallback:(void (^)(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error))callback;
Leaderboard.Limit = 10;
FLCLeaderboard::FRankingsDelegate OnSuccess = FLCLeaderboard::FRankingsDelegate::CreateLambda([=](TArray<FLCLeaderboardRanking> Rankings, int64 Count) {
// 排行榜前十的排名信息
});
FLCError::FDelegate OnError = FLCError::FDelegate::CreateLambda([=](const FLCError& Error) {
// handle error
});
Leaderboard.GetResults(OnSuccess, OnError);
Leaderboard 实例上可以设置如下属性限制查询范围:
/// 指定从某个位置开始获取,与 limit 一起可以实现翻页,默认 0
int64 Skip = 0;
/// 限制返回的结果数量,默认 20
int64 Limit = 20;
/// 是否返回排行榜的数量
bool WithCount = false;
/// 指定排行榜版本号,默认 0
int64 Version = 0;
/// 指定返回的 LCLeaderboardRanking 中需要包含的其他成绩
TArray<FString> IncludeStatistics;
/// 指定返回的 LCRanking 中排行榜成员需要包含的属性
TArray<FString> SelectMemberKeys;
FLCLeaderboardRanking 提供了以下方法供获取排名信息:
// 排名,从 0 开始
int64 Rank;
// 该成绩的用户(user 排行榜)
TSharedPtr<FLCUser> User;
// 该成绩的 object(object 排行榜)
TSharedPtr<FLCObject> Object;
// 该成绩的 entity(entity 排行榜)
FString EntityId;
// 成绩
double Value;
// 该成员在其他排行榜的成绩
TArray<FLCLeaderboardStatistic> Statistics;
默认情况下返回的排行榜结果中的 user
是一个 指向 LCUser
的 Pointer。
如果想要像下面这个例子一样,在排行榜结果中显示用户名或者其他的用户属性(对应 _User
表中的属性),那么需要使用 selectKeys
指定需要包含的属性。
排名 | Username | Score↓ |
---|---|---|
0 | Genji | 3458 |
1 | Lúcio | 3252 |
2 | D.Va | 3140 |
- Unity
- Android
- iOS
- UE4
var rankings = await leaderboard.GetResults(limit: 10,
selectKeys: new List<string> { "nickname" });
List<String> selectKeys = new ArrayList<>();
selectKeys.add("nickname");
leaderboard.getResults(0, 10, selectKeys, null).subscribe(/* 略 */);
leaderboard.limit = 10;
LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init];
option.selectKeys = @[@"nickname"];
[leaderboard getUserResultsWithOption:option,
callback:^(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error) {
// 略
}];
Leaderboard.Limit = 10;
Leaderboard.SelectMemberKeys = {"nickname"};
Leaderboard.GetResults(OnSuccess, OnError);
如果想要在排行榜结果中包含用户的其他成绩,可以使用 includeStatistics
。
例如,查询得分排行榜时同时返回相应用户的击杀数:
排名 | Username | Score↓ | Kills |
---|---|---|---|
0 | Genji | 3458 | 28 |
1 | Lúcio | 3252 | 2 |
2 | D.Va | 3140 | 31 |
- Unity
- Android
- iOS
- UE4
var rankings = await leaderboard.GetResults(limit: 10, selectKeys: new List<string> { "username" }
includeStatistics: new List<string> { "kills" });
List<String> selectKeys = new ArrayList<>();
selectKeys.add("username");
List<String> includeStatistics = new ArrayList<>();
includeStatistics.add("kills");
leaderboard.getResults(0, 10, selectKeys, includeStatistics).subscribe(/* 略 */);
leaderboard.limit = 10;
leaderboard.includeStatistics = @[@"kills"];
LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init];
option.selectKeys = @[@"username"];
[leaderboard getUserResultsWithOption:option,
callback:^(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error) {
// 略
}];
Leaderboard.Limit = 10;
Leaderboard.SelectMemberKeys = {"username"};
Leaderboard.IncludeStatistics = {"kills"};
Leaderboard.GetResults(OnSuccess, OnError);
如果想要包含的属性是 Pointer 类型或文件类型,那么 selectKeys
只会返回 Pointer 本身。
想要一并获取 Pointer 指向的另一个 class 的数据的话,需要额外使用 includeKeys
指定。
例如,假设 club
是一个指向 Club
类的 Pointer:
- Unity
- Android
- iOS
- UE4
var leaderboard = LCLeaderboard.CreateWithoutData("weapons", "Weapon");
var rankings = await leaderboard.GetResults(limit: 10,
selectKeys: new List<string> { "name", "club" },
includeKeys: new List<string> { "club" });
// 暂未支持,如有此类需求可在 onNext 方法中额外发起一次查询
LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"];
leaderboard.limit = 10;
LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init];
option.selectKeys = @[@"name", @"club"];
option.includeKeys = @[@"club"];
[leaderboard getUserResultsWithOption:option,
callback:^(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error) {
// 略
}];
// 暂未支持
注意,排行榜 不保证成绩并列的成员之间的先后顺序。 例如,假设 A、B、C 的成绩为 42、32、32,且排行榜的排序策略为降序,那么查询排行榜的返回结果可能是 A、B、C,也可能是 A、C、B。
当成绩相同时,如果需要其他因素去设置排名可以参考如下示例**(示例是通过成绩上传的时间戳去判断排名,当成绩相同时,越早/晚提交,排名越靠前/后)**:
Android 请求示例:
public class RankingActivity extends AppCompatActivity{
// ...
/**
* 上传/更新成绩
*
* */
private void submitScore() {
double score = 324.45; // 实际的分数
long ts = System.currentTimeMillis() / 1000; // 时间戳
double last_score = toEncode( score, ts); // 将实际分数与时间戳组合生成一个新的数据上传到服务器
Map<String, Double> statistic = new HashMap<>();
statistic.put("word", last_score);
LCLeaderboard.updateStatistic(LCUser.currentUser(), statistic).subscribe(new Observer<LCStatisticResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull LCStatisticResult jsonObject) {
Log.e(TAG, "onNext: "+jsonObject.getResults().get(0).toString());
}
@Override
public void onError(@NotNull Throwable throwable) {
ToastUtil.showCus(throwable.getMessage(), ToastUtil.Type.ERROR);
}
@Override
public void onComplete() {}
});
}
/**
* 查询排行榜列表
* */
private void searchRankList() {
// 获取排行榜的实例
LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("word");
leaderboard.getResults(0, 10, null, null).subscribe(new Observer<LCLeaderboardResult>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onNext(@NotNull LCLeaderboardResult leaderboardResult) {
List<LCRanking> rankings = leaderboardResult.getResults();
for(int i=0; i<rankings.size(); i++){
/**
* 将查询的方法进行解密
*/
double score = rankings.get(i).getStatisticValue(); // 获取查询到的分数
String formatScore = String.format("%.3f", score); // 将 double 类型的分数系列化为保留小数点后3位的字符串
BigDecimal decimalValue = new BigDecimal(formatScore);
long longPart = decimalValue.longValue(); // 获取查询到的 StatisticValue 的整数部分
float floatPart = decimalValue.subtract(BigDecimal.valueOf(longPart)).floatValue(); // 获取查询到的 StatisticValue 的小数部分
int[] decryptedScoreTs = toDecode(longPart); // 将整数部分进行解密操作
int decryptedScore = decryptedScoreTs[0]; // 获取实际分数的整数部分
long decryptedTs = decryptedScoreTs[1]; // 获取提交成绩时的时间戳
BigDecimal bigDecimalValue = new BigDecimal(Integer.toString(decryptedScore))
.add(new BigDecimal(Float.toString(floatPart))); // 将实际分数的整数部分拼接上小数部分得到实际的分数
double doubleValue = bigDecimalValue.doubleValue(); // 将实际分数转化为 double 类型
Log.e(TAG, "解析后的score:"+doubleValue + "解析后的时间戳:"+ decryptedTs );
}
}
@Override
public void onError(@NotNull Throwable throwable) {
}
@Override
public void onComplete() {}
});
}
/**
* score 实际的分数
* ts 时间戳
* 将实际分数与时间戳组合生成一个新的数据上传到服务器
*/
public static double toEncode(double score, long ts) {
int int_score = (int) score; // 实际分数的整数部分
float float_score = (float) (score - int_score); // 实际分数的小数部分
/**
* 当排行榜使用降序排列,分数相同时,时间越近,排名靠前,使用这行代码
*/
// long encryptedScoreTs = ((long) int_score << 32) | (ts & 0xFFFFFFFFL); // 将整数部分分数与时间戳进行加密处理合并成一个新的 score
/**
* 当排行榜使用降序排列,分数相同时,时间越近,排名靠后,使用这行代码
*/
long encryptedScoreTs = ((long) int_score << 32) - (ts & 0xFFFFFFFFL); // 将整数部分分数与时间戳进行加密处理合并成一个新的 score
double newScore = (double)encryptedScoreTs + float_score; // 然后将加密生成的数据拼接上小数部分的数据,上传到服务器
return newScore;
}
/**
* 将加密的数据进行解析出实际分数和当时提交成绩时的时间戳
*
*/
public static int[] toDecode(long encryptedNewScore) {
/**
* 当排行榜使用降序排列,分数相同时,时间越近,排名靠前,使用这行代码
*/
// int score = (int) (encryptedNewScore >> 32);
/**
* 当排行榜使用降序排列,分数相同时,时间越近,排名靠后,使用这行代码
*/
int score = (int) (encryptedNewScore >> 32) + 1;
long ts = encryptedNewScore & 0xFFFFFFFFL;
return new int[]{score, (int) ts};
}
}
获取当前用户附近的排名
排名 | Username | Score↓ |
---|---|---|
… | ||
24 | Bastion | 716 |
25 (You) | Widowmaker | 698 |
26 | Hanzo | 23 |
… |
- Unity
- Android
- iOS
- UE4
如果想达到类似上表的效果,在调用 GetResults
方法时额外传入当前用户即可:
var rankings = await leaderboard.GetResults(aroundUser: currentUser, limit: 3, selectKeys: new List<string> { "username" });
如果想达到类似上表的效果,可以调用 getAroundResults
方法:
List<String> selectKeys = new ArrayList<>();
selectKeys.add("username");
leaderboard.getAroundResults(currentUser.getObjectId(), 0, 3, selectKeys, null).subscribe(/* 略 */);
getAroundResults
方法的第一个参数为排行榜成员的 ID(user 或 object 为 objectId
、entity 为字符串),其他参数和 getResults
相同。
如果想达到类似上表的效果,可以调用 getUserResultsAroundUser
方法:
leaderboard.limit = 3;
[leaderboard getUserResultsAroundUser:currentUser.objectId,
option:nil,
callback:^(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error) {
// 略
}];
如果想达到类似上表的效果,可以调用 GetAroundResults
方法:
Leaderboard.Limit = 3;
Leaderboard.GetAroundResults(CurrentUser->GetObjectId();, OnSuccess, OnError);
GetAroundResults
方法的第一个参数为排行榜成员的 ID(user 或 object 为 objectId
、entity 为字符串),其他参数和 GetResults
相同。
上面的代码示例中,limit
的值为 3,表示获取与当前玩家相邻的前后两个玩家的排名,当前玩家会在结果的 中间位置。
如仅需获取当前玩家排名,可指定 limit
为 1。
类似地,可以获取某个 object 或 entity 附近的排名。 例如,获取武器榜上和某件武器排名相近的武器,同时获取这些武器的名称、攻击力、等级:
- Unity
- Android
- iOS
- UE4
var leaderboard = LCLeaderboard.CreateWithoutData("weapons", "Weapon");
var excalibur = LCObject.createWithoutData("Weapon", "582570f38ac247004f39c24b");
var rankings = await leaderboard.GetResults(aroundObject: excalibur, limit: 3, selectKeys: new List<string> { "name", "attack", "level" });
LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world", "Weapon");
String excaliburObjectId = "582570f38ac247004f39c24b";
List<String> selectKeys = new ArrayList<>();
selectKeys.add("name");
selectKeys.add("attack");
selectKeys.add("level");
leaderboard.getAroundResults(excaliburObjectId, 0, 3, selectKeys, null).subscribe(/* 略 */);
LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"];
leaderboard.limit = 3;
LCLeaderboardQueryOption *option = [[LCLeaderboardQueryOption alloc] init];
option.selectKeys = @[@"name", @"attack", @"level"];
NSString *excaliburObjectId = @"582570f38ac247004f39c24b";
[leaderboard getObjectResultsAroundObject:excaliburObjectId,
option:option,
callback:^(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error) {
// 略
}];
FLCLeaderboard Leaderboard("weapons", FLCLeaderboard::Object);
Leaderboard.Limit = 3;
Leaderboard.SelectMemberKeys = {"name", "attack", "level"};
FString ExcaliburObjectId = "582570f38ac247004f39c24b";
Leaderboard.GetAroundResults(ExcaliburObjectId, OnSuccess, OnError);
上面的例子中假定武器排行榜是通过 object 排行榜实现的,存储武器信息的 Class 为 Weapon
。
如果武器排行榜是通过 entity 排行榜实现的,且只需获取武器的名称,那么相应的代码为:
- Unity
- Android
- iOS
- UE4
var leaderboard = LCLeaderboard.CreateWithoutData("weapons", LCLeaderboard.ENTITY_MEMBER_TYPE);
var rankings = await leaderboard.GetResults(aroundEntity: "excalibur", limit: 3);
LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("world", LCLeaderboard.ENTITY_MEMBER_TYPE);
leaderboard.getAroundResults("excalibur", 0, 3, null, null).subscribe(/* 略 */);
LCLeaderboard leaderboard = [[LCLeaderboard alloc] initWithStatisticName:@"weapons"];
leaderboard.limit = 3;
[leaderboard getEntityResultsAroundEntity:@"excalibur",
callback:^(NSArray^(NSArray<LCLeaderboardRanking *> * _Nullable rankings, NSInteger count, NSError * _Nullable error) {
// 略
}];
FLCLeaderboard Leaderboard("weapons", FLCLeaderboard::Entity);
Leaderboard.Limit = 3;
Leaderboard.GetAroundResults("excalibur", OnSuccess, OnError);
控制台
在排行榜控制台(游戏服务 > 云服务 > 排行榜),你可以:
- 新建、重置、编辑、删除排行榜。
- 查看排行榜当前版本的数据、删除成绩、下载排行榜的历史数据归档文件。
- 设置是否允许客户端查询前一版本,是否只允许使用 Master Key 更新分数。
SDK 管理接口
除了通过控制台管理排行榜外,C# SDK 和 Java SDK 也提供了管理接口,可以在服务端等受信任的环境下使用。 另外,也可以直接调用 REST API 提供的管理接口。
下面我们先介绍 SDK 提供的管理接口。
请注意,调用管理接口 API 需要在 SDK 初始化时使用 masterKey,因此只能在服务端等可信任的环境中调用,不得在客户端使用。
- Unity
- Android
- iOS
- UE4
LCApplication.Initialize({{appid}}, {{appkey}}, "https://xxx.example.com", {{masterkey}});
LCApplication.UseMasterKey = true;
LeanCloud.setMasterKey({{masterkey}});
// 不支持
// 不支持
创建排行榜
- Unity
- Android
- iOS
- UE4
var leaderboard = await LCLeaderboard.CreateLeaderboard("time", order: LCLeaderboardOrder.ASCENDING);
可以指定的参数类型和默认值如下:
public static async Task<LCLeaderboard> CreateLeaderboard(string statisticName,
LCLeaderboardOrder order = LCLeaderboardOrder.Descending,
LCLeaderboardUpdateStrategy updateStrategy = LCLeaderboardUpdateStrategy.Better,
LCLeaderboardVersionChangeInterval versionChangeInterval = LCLeaderboardVersionChangeInterval.Week,
string memberType = LCLeaderboard.USER_MEMBER_TYPE)
statisticName
所排名的成绩名字。order
排序,可以传入LCLeaderboardOrder.Descending
或LCLeaderboardOrder.Ascending
。updateStrategy
成绩更新策略,可以传入LCLeaderboardUpdateStrategy.Better
、LCLeaderboardUpdateStrategy.Last
、LCLeaderboardUpdateStrategy.Sum
。versionChangeInterval
自动重置周期,可以传入LCLeaderboardVersionChangeInterval.Never
、LCLeaderboardVersionChangeInterval.Day
、LCLeaderboardVersionChangeInterval.Week
、LCLeaderboardVersionChangeInterval.Month
。memberType
成员类型,传入LCLeaderboard.USER_MEMBER_TYPE
表示成员类型为用户,传入LCLeaderboard.ENTITY_MEMBER_TYPE
表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。
LCLeaderboard.createWithMemberType(LCLeaderboard.MEMBER_TYPE_USER, "time",
LCLeaderboard.LCLeaderboardOrder.Ascending,
LCLeaderboard.LCLeaderboardUpdateStrategy.Last,
LCLeaderboard.LCLeaderboardVersionChangeInterval.Day).subscribe(new Observer<LCLeaderboard>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {}
@Override
public void onNext(@NotNull final LCLeaderboard lcLeaderboard) {
System.out.println("leaderboard created");
}
@Override
public void onError(@NotNull Throwable throwable) {
System.out.println("failed to create leaderboard. Cause " + throwable);
}
@Override
public void onComplete() {}
});
你可以指定以下参数:
public static Observable<LCLeaderboard> createWithMemberType(String memberType, String name,
LCLeaderboardOrder order,
LCLeaderboardUpdateStrategy updateStrategy,
LCLeaderboardVersionChangeInterval versionChangeInterval)
memberType
成员类型,传入LCLeaderboard.MEMBER_TYPE_USER
表示成员类型为用户,传入LCLeaderboard.MEMBER_TYPE_ENTITY
表示成员类型为 entity,成员类型为 object 时请传入相应 Class 名称。name
所排名的成绩名字。order
排序,可以传入LCLeaderboard.LCLeaderboardOrder.Descending
(默认值)或LCLeaderboard.LCLeaderboardOrder.Ascending
。updateStrategy
成绩更新策略,可以传入LCLeaderboard.LCLeaderboardUpdateStrategy.Better
(默认值)、LCLeaderboard.LCLeaderboardUpdateStrategy.Last
、LCLeaderboard.LCLeaderboardUpdateStrategy.Sum
。versionChangeInterval
自动重置周期,可以传入LCLeaderboard.LCLeaderboardVersionChangeInterval.Never
、LCLeaderboard.LCLeaderboardVersionChangeInterval.Day
、LCLeaderboard.LCLeaderboardVersionChangeInterval.Week
(默认值)、LCLeaderboard.LCLeaderboardVersionChangeInterval.Month
。
默认值指相应参数传入 null
时对应的取值。
不支持
不支持
手动重置排行榜
- Unity
- Android
- iOS
- UE4
var leaderboard = LCLeaderboard.CreateWithoutData("score");
await leaderboard.Reset();
LCLeaderboard leaderboard = LCLeaderboard.createWithoutData("score");
leaderboard.reset().subscribe(new Observer<Boolean>() {
@Override
public void onSubscribe(@NotNull Disposable disposable) {
}
@Override
public void onNext(@NotNull Boolean aBoolean) {
if (aBoolean) { // aBoolean should always be true
System.out.println("leaderboard reset");
}
}
@override
public void onerror(@notnull throwable throwable) {
system.out.println("Failed to reset leaderboard. Cause " + throwable);
}
@override
public void oncomplete() {}
});
// 不支持
// 不支持
获取排行榜属性
通过以下接口可以获取当前排行榜的属性,例如:重置周期、版本号、更新策略等。
- Unity
- Android
- iOS
- UE4
var leaderboardData = await LCLeaderboard.GetLeaderboard("world");
LCLeaderboard.fetchByName("world").subscribe(new Observer<LCLeaderboard>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(LCLeaderboard lcLeaderboard) {
int v =lcLeaderboard.getVersion();
String statisticName = lcLeaderboard.getStatisticName();
Log.d(TAG, String.valueOf(v));
Log.d(TAG, statisticName);
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
// 不支持
// 不支持