任务系统
本示例介绍如何将 Cloud Code 用于以下目的:
- 服务器授权反作弊
- 缓存其他 UGS 服务响应
- 修改玩家的 Cloud Save 数据
问题陈述
本示例中的任务系统允许玩家请求向他们发任务。任务存储在 Remote Config 中。Cloud Code 可用于联系 Remote Config 以获取当前活动任务列表并随机选择一个任务。然后,该任务将存储在 Cloud Save 中。
玩家可以执行计入任务进度的操作。Cloud Code 会检查玩家上次取得进度的时间,并允许或拒绝该操作计入进度。Cloud Code 通过定义每分钟允许的进度来实现这一目的。每分钟,玩家只能执行变量中定义的进度大小。这是一种简单的反作弊方法,通过限制玩家获取任务进度点的速度来防止作弊。
任务设置
在 Remote Config 中,创建一个名为 QUESTS
的变量,并选择其类型为 JSON
。将以下内容粘贴为该变量的值:
[{
"id": 1,
"name": "Chop Trees",
"progress_required": 5,
"progress_per_minute": 1,
"reward": 5
}, {
"id": 2,
"name": "Mine Gold",
"progress_required": 5,
"progress_per_minute": 1,
"reward": 10
}]
这将用作我们的活动任务数据库。本示例使用简单的任务,例如 Chop Trees(砍树)或 Mine Gold(开采金币)。progress_required
决定了每个任务需要达到多少进度才能被视为完成。progress_per_minute
变量限制玩家每分钟可以获得多少进度点。在本例中,假设玩家在每一分钟冷却时间结束时执行操作,Chop Trees(砍树)任务只能在五分钟内完成。
C# 模块设置
为了使本示例正常工作,需要设置 Cloud Code C# 模块。Cloud Code C# 模块是一个旨在部署到 Cloud Code 服务的 .NET 项目。要了解有关如何创建和部署 Cloud Code C# 模块的更多信息,请参阅部署 Hello World。
Cloud Save 设置
为了防止用户通过修改自己的 Cloud Save 数据来作弊,您需要为 UGS 创建资源策略。请参阅如何使用访问控制(Unity Gaming Services(Unity 游戏服务)简介)。
{
"Sid": "deny-cloud-save-data-write-access",
"Effect": "Deny",
"Action": ["Write"],
"Principal": "Player",
"Resource": "urn:ugs:cloud-save:/v1/data/projects/*/players/*/items**"
},
任务服务
创建一个名为 QuestService.cs
的文件。此文件从 Remote Config 获取任务并将任务缓存一段时间。
using System.Text.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
namespace QuestSystem;
public interface IQuestService
{
IList<Quest> GetAvailableQuests(IExecutionContext ctx);
}
public class QuestService : IQuestService
{
private readonly IGameApiClient _apiClient;
public QuestService(IGameApiClient apiClient)
{
_apiClient = apiClient;
}
private DateTime? CacheExpiryTime { get; set; }
// Reminder: cache cannot be guaranteed to be consistent across all requests
private IList<Quest>? QuestCache { get; set; }
public IList<Quest> GetAvailableQuests(IExecutionContext ctx)
{
if (QuestCache == null || DateTime.Now > CacheExpiryTime)
{
var quests = FetchQuestsFromRC(ctx);
QuestCache = quests;
CacheExpiryTime = DateTime.Now.AddMinutes(5); // data in cache expires after 5 mins
}
return QuestCache;
}
private IList<Quest> FetchQuestsFromRC(IExecutionContext ctx)
{
var result = _apiClient.RemoteConfigSettings.AssignSettingsGetAsync(ctx, ctx.AccessToken, ctx.ProjectId,
ctx.ProjectId, null, new List<string> { "QUESTS" });
var settings = result.Result.Data.Configs.Settings;
return JsonSerializer.Deserialize<List<Quest>>(settings["QUESTS"].ToString());
}
}
数据类
需要两个包含数据字段的类。将这些类放在名为 DataClasses.cs
的文件中。这些类用于对 Cloud Code 从 Remote Config 和 Cloud Save 接收的响应进行 JSON 反序列化。
using System.Text.Json.Serialization;
namespace QuestSystem;
public class Quest
{
[JsonPropertyName("id")] public int ID { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("reward")] public int Reward { get; set; }
[JsonPropertyName("progress_required")]
public int ProgressRequired { get; set; }
[JsonPropertyName("progress_per_minute")]
public int ProgressPerMinute { get; set; }
}
public class QuestData
{
public QuestData()
{
}
public QuestData(string questName, int reward, int progressLeft, int progressPerMinute, DateTime questStartTime)
{
QuestName = questName;
Reward = reward;
ProgressLeft = progressLeft;
ProgressPerMinute = progressPerMinute;
QuestStartTime = questStartTime;
LastProgressTime = new DateTime();
}
[JsonPropertyName("quest-name")] public string? QuestName { get; set; }
[JsonPropertyName("reward")] public long Reward { get; set; }
[JsonPropertyName("progress-left")] public long ProgressLeft { get; set; }
[JsonPropertyName("progress-per-minute")]
public long ProgressPerMinute { get; set; }
[JsonPropertyName("quest-start-time")] public DateTime QuestStartTime { get; set; }
[JsonPropertyName("last-progress-time")] public DateTime LastProgressTime { get; set; }
}
Cloud Code 设置
为了使依赖项注入功能正常运行,需要另一个名为 CloudCodeSetup
的类。这样可以确保每当调用 Cloud Code 函数时,它都能理解 QuestService
和 GameAPIClient
的含义。您可以在此处进一步了解 Cloud Code 中的依赖项注入:依赖项注入。
using Microsoft.Extensions.DependencyInjection;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
namespace QuestSystem;
public class CloudCodeSetup : ICloudCodeSetup
{
public void Setup(ICloudCodeConfig config)
{
config.Dependencies.AddSingleton<IQuestService, QuestService>();
config.Dependencies.AddSingleton<IGameApiClient>(s => GameApiClient.Create());
}
}
任务控制器
QuestController
是模块的主类,用作服务的用户流量入口点。
using System.Text.Json;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudSave.Model;
namespace QuestSystem;
public class QuestController
{
[CloudCodeFunction("AssignQuest")]
public async Task<string> AssignQuest(IExecutionContext ctx, IQuestService questService, IGameApiClient apiClient)
{
var questData = await GetQuestData(ctx, apiClient);
if (questData?.QuestName != null) return "Player already has a quest in progress!";
var availableQuests = questService.GetAvailableQuests(ctx);
var random = new Random();
var index = random.Next(availableQuests.Count);
var quest = availableQuests[index];
questData = new QuestData(quest.Name, quest.Reward, quest.ProgressRequired, quest.ProgressPerMinute,
DateTime.Now);
await SetQuestData(ctx, apiClient, "quest-data", JsonSerializer.Serialize(questData));
return $"Player was assigned quest: {quest.Name}!";
}
[CloudCodeFunction("PerformAction")]
public async Task<string> PerformAction(IExecutionContext ctx, IGameApiClient apiClient)
{
var questData = await GetQuestData(ctx, apiClient);
if (questData?.QuestName == null) return "Player does not have a quest in progress!";
if (questData.ProgressLeft == 0) return "Player has already completed their quest!";
if (DateTime.Now < questData.LastProgressTime.AddSeconds(60 / questData.ProgressPerMinute)) return "Player cannot make quest progress yet!";
questData.LastProgressTime = DateTime.Now;
questData.ProgressLeft--;
await SetQuestData(ctx, apiClient, "quest-data", JsonSerializer.Serialize(questData));
return "Player made quest progress!";
}
private async Task<QuestData?> GetQuestData(IExecutionContext ctx, IGameApiClient apiClient)
{
var result = await apiClient.CloudSaveData.GetItemsAsync(
ctx, ctx.AccessToken, ctx.ProjectId, ctx.PlayerId,
new List<string> { "quest-data" });
if (result.Data.Results.Count == 0) return null;
return JsonSerializer.Deserialize<QuestData>(result.Data.Results.First().Value.ToString());
}
private async Task<string> SetQuestData(IExecutionContext ctx, IGameApiClient apiClient, string key, string value)
{
var result = await apiClient.CloudSaveData
.SetItemAsync(ctx, ctx.AccessToken, ctx.ProjectId, ctx.PlayerId, new SetItemBody(key, value));
return result.Data.ToJson();
}
}
Cloud Code 函数 AssignQuest
AssignQuest
函数获取本示例先前发布到 Remote Config 的当前活动任务。此函数还会检查玩家是否已经有正在进行的任务,如果没有,则会向玩家分配任务。
Cloud Code 函数 PerformAction
每次玩家执行可能产生任务进度点的操作时,调用 PerformAction
函数。假设冷却时间已过期,或者这是玩家的第一次进度函数调用,那么玩家将获得一个进度点,并且他们在 Cloud Save 中的 progress-left
会下降。
如果在冷却期间调用此函数,则 Cloud Code 会发出如下所示的消息:Player cannot make quest progress yet!
。如果玩家成功(在冷却期之外调用函数),Cloud Code 会发出“Player made quest progress!
”响应,玩家的 progress-left
会下降,并会更新 last-progress-time
。
如果玩家的 progress-left
为零,则对 PerformAction
函数的任何后续调用都会得到“Player has already completed their quest!
”响应。达到此状态后,您可以通知玩家返回到向他们发出任务以获取奖励的位置或屏幕。