文档

支持

Cloud Code

任务系统

Implement a quest system that issues players with quests using Remote Config and Cloud Save.
阅读时间7 分钟最后更新于 1 个月前

任务系统示例演示了如何使用 Cloud Code 通过 Remote ConfigCloud Save 服务实现任务示例。此示例涵盖服务器授权反作弊、其他 UGS 服务响应的缓存以及玩家 Cloud Save 数据的修改。此示例使用推送消息在玩家完成任务时通知玩家。

概览

任务系统允许玩家调用 Cloud Code 模块来发布任务。为了向玩家提供任务,Cloud Code 可以执行以下操作:
  • 将任务存储在 Remote Config 中。
  • 从 Remote Config 中获取活动任务列表,并随机选择一个任务。
  • 将任务分配给玩家,并将数据存储在 Cloud Save 中。
玩家可以执行计入任务进度的操作。 Cloud Code 会检查玩家上次取得进度的时间,并根据以下限制允许或拒绝该操作对进度的影响:
  • 每分钟的进度被限制在一定的速率以内。
  • 玩家每分钟只能执行任务配置所允许的操作数量。
这是一种简单的反作弊方法,通过限制玩家获取任务进度点的速度来防止作弊。 一旦玩家完成任务,Cloud Save 便会删除任务数据并通过推送消息通知玩家。

设置任务

您需要在 Remote Config 服务中定义您的任务:
  1. 导航到 Unity Cloud Dashboard
  2. 选择 Products(产品)> Remote Config
  3. 选择 Config(配置) 选项卡。
  4. 选择 Add a key(添加键)
  5. 输入
    QUESTS
    作为键名称。
  6. 选择 JSON 作为类型。
  7. 粘贴以下内容作为值:
    [ { "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 }]
  8. 发布该配置。
该配置充当活动任务数据库。 任务结构如下:
  • id
    :任务的唯一标识符。
  • name
    :任务的名称。
  • progress_required
    :完成任务所需的进度点数。
  • progress_per_minute
    :玩家每分钟可以获得的进度点数。
  • reward
    :玩家完成任务后获得的奖励。
例如,假设玩家在每一分钟冷却时间结束时执行操作,Chop Trees(砍树)任务只能在五分钟内完成。

设置 Cloud Code

设置一个名为
QuestSystem
的 Cloud Code 模块来处理任务系统。

限制对 Cloud Save 的访问

要为您的 Cloud Save 数据增加一层安全保护,您可以使用访问控制来创建资源策略。 为了防止用户通过修改自己的 Cloud Save 数据来作弊,您需要为 UGS 创建资源策略。 创建一个名为
cloud-save-resource-policy.json
的文件并粘贴以下内容:
{ "statements": [ { "Sid": "deny-cloud-save-data-write-access", "Effect": "Deny", "Action": ["Write"], "Principal": "Player", "Resource": "urn:ugs:cloud-save:/v1/data/projects/*/players/*/items**" } ]}
您可以使用 UGS CLI 来部署该文件:
ugs access upsert-project-policy cloud-save-resource-policy.json
要了解有关资源策略的更多信息,请参阅访问控制

设置任务服务

创建一个名为
QuestService.cs
的文件。
任务服务从 Remote Config 获取任务并将任务缓存一段时间。
using Microsoft.Extensions.Logging;using Newtonsoft.Json;using Unity.Services.CloudCode.Apis;using Unity.Services.CloudCode.Core;using Unity.Services.CloudCode.Shared;namespace QuestSystem;public interface IQuestService{ List<Quest> GetAvailableQuests(IExecutionContext context, IGameApiClient gameApiClient);}public class QuestService : IQuestService{ private const string QuestKey = "QUESTS"; private readonly ILogger<QuestService> _logger; public QuestService(ILogger<QuestService> logger) { _logger = logger; } private DateTime? CacheExpiryTime { get; set; } // Reminder: cache cannot be guaranteed to be consistent across all requests private List<Quest>? QuestCache { get; set; } public List<Quest> GetAvailableQuests(IExecutionContext context, IGameApiClient gameApiClient) { if (QuestCache == null || DateTime.Now > CacheExpiryTime) { var quests = FetchQuestsFromConfig(context, gameApiClient); QuestCache = quests; CacheExpiryTime = DateTime.Now.AddMinutes(5); // data in cache expires after 5 mins } return QuestCache; } private List<Quest> FetchQuestsFromConfig(IExecutionContext ctx, IGameApiClient gameApiClient) { try { var result = gameApiClient.RemoteConfigSettings.AssignSettingsGetAsync(ctx, ctx.AccessToken, ctx.ProjectId, ctx.EnvironmentId, null, new List<string> { "QUESTS" }); var settings = result.Result.Data.Configs.Settings; return JsonConvert.DeserializeObject<List<Quest>>(settings[QuestKey].ToString()); } catch (ApiException e) { _logger.LogError($"Failed to assign Remote Config settings. Error: {e.Message}"); throw new Exception($"Failed to assign Remote Config settings. Error: {e.Message}"); } }}

数据类

创建一个名为
DataClasses.cs
的文件来存储数据类。数据类可用于对 Cloud Code 从 Remote Config 和 Cloud Save 接收的响应进行反序列化。创建一个名为
DataClasses.cs
的文件来存储数据类。
using Newtonsoft.Json;namespace QuestSystem;public class Quest{ [JsonProperty("id")] public int ID { get; set; } [JsonProperty("name")] public string? Name { get; set; } [JsonProperty("reward")] public int Reward { get; set; } [JsonProperty("progress_required")] public int ProgressRequired { get; set; } [JsonProperty("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(); } [JsonProperty("quest-name")] public string? QuestName { get; set; } [JsonProperty("reward")] public long Reward { get; set; } [JsonProperty("progress-left")] public long ProgressLeft { get; set; } [JsonProperty("progress-per-minute")] public long ProgressPerMinute { get; set; } [JsonProperty("quest-start-time")] public DateTime QuestStartTime { get; set; } [JsonProperty("last-progress-time")] public DateTime LastProgressTime { get; set; }}
要了解有关自定义序列化的更多信息,请参阅自定义序列化

依赖项设置

要连接 Cloud Code 模块,您需要设置
QuestService
GameApiClient
PushClient
  • QuestService
    管理和缓存任务。
  • 需要
    GameApiClient
    来调用 Remote Config 和 Cloud Save 服务。
  • 需要
    PushClient
    来向玩家发送推送通知。
ICloudCodeSetup
接口可用于管理模块的依赖项。请在依赖项注入文档中了解更多相关信息。
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(GameApiClient.Create()); config.Dependencies.AddSingleton(PushClient.Create()); }}

任务控制器

创建
QuestController
类作为服务的主类和用户流量入口点。
using Microsoft.Extensions.Logging;using Newtonsoft.Json;using Unity.Services.CloudCode.Apis;using Unity.Services.CloudCode.Core;using Unity.Services.CloudCode.Shared;using Unity.Services.CloudSave.Model;namespace QuestSystem;public class QuestController{ private const string QuestDataKey = "quest-data"; private const string PlayerHasQuestInProgress = "Player already has a quest in progress!"; private const string PlayerHasNoQuestInProgress = "Player does not have a quest in progress!"; private const string PlayerHasCompletedTheQuest = "Player has already completed their quest!"; private const string PlayerCannotProgress = "Player cannot make quest progress yet!"; private const string PlayerProgressed = "Player made quest progress!"; private const string PlayerHasFinishedTheQuest = "Player has finished the quest!"; private ILogger<QuestController> _logger; public QuestController(ILogger<QuestController> logger) { _logger = logger; } [CloudCodeFunction("AssignQuest")] public async Task<string> AssignQuest(IExecutionContext context, IQuestService questService, IGameApiClient gameApiClient) { var questData = await GetQuestData(context, gameApiClient); if (questData?.QuestName != null) return PlayerHasQuestInProgress; var availableQuests = questService.GetAvailableQuests(context, gameApiClient); 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(context, gameApiClient, QuestDataKey, JsonConvert.SerializeObject(questData)); return $"Player was assigned quest: {quest.Name}!"; } [CloudCodeFunction("PerformAction")] public async Task<string> PerformAction(IExecutionContext context, IGameApiClient gameApiClient, PushClient pushClient) { var questData = await GetQuestData(context, gameApiClient); if (questData?.QuestName == null) return PlayerHasNoQuestInProgress; if (questData.ProgressLeft == 0) return PlayerHasCompletedTheQuest; if (DateTime.Now < questData.LastProgressTime.AddSeconds(60 / questData.ProgressPerMinute)) return PlayerCannotProgress; questData.LastProgressTime = DateTime.Now; questData.ProgressLeft--; await SetQuestData(context, gameApiClient, QuestDataKey, JsonConvert.SerializeObject(questData)); if (questData.ProgressLeft <= 0) { await HandleQuestCompletion(context, gameApiClient, pushClient); return PlayerHasFinishedTheQuest; } return PlayerProgressed; } public async Task HandleQuestCompletion(IExecutionContext context, IGameApiClient gameApiClient, PushClient pushClient) { await NotifyPlayer(context, pushClient); try { await gameApiClient.CloudSaveData.DeleteItemAsync(context, context.AccessToken, QuestDataKey, context.ProjectId, context.PlayerId); } catch (ApiException e) { _logger.LogError("Failed to delete a quest for player. Error: {Error}", e.Message); throw new Exception($"Failed to delete a quest for player. Error. Error: {e.Message}"); } } private async Task NotifyPlayer(IExecutionContext context, PushClient pushClient) { const string message = "Quest completed!"; const string messageType = "Announcement"; try { await pushClient.SendPlayerMessageAsync(context, message, messageType, context.PlayerId); } catch (ApiException e) { _logger.LogError("Failed to send player message. Error: {Error}", e.Message); throw new Exception($"Failed to send player message. Error: {e.Message}"); } } private async Task<QuestData?> GetQuestData(IExecutionContext context, IGameApiClient gameApiClient) { try { var result = await gameApiClient.CloudSaveData.GetItemsAsync( context, context.AccessToken, context.ProjectId, context.PlayerId, new List<string> { QuestDataKey }); if (result.Data.Results.Count == 0) return null; return JsonConvert.DeserializeObject<QuestData>(result.Data.Results.First().Value.ToString()); } catch (ApiException e) { _logger.LogError($"Failed to retrieve data from Cloud Save. Error: {e.Message}"); throw new Exception($"Failed to retrieve data from Cloud Save. Error: {e.Message}"); } } private async Task SetQuestData(IExecutionContext context, IGameApiClient gameApiClient, string key, string value) { try { await gameApiClient.CloudSaveData .SetItemAsync(context, context.ServiceToken, context.ProjectId, context.PlayerId, new SetItemBody(key, value)); } catch (ApiException e) { _logger.LogError($"Failed to save data in Cloud Save. Error: {e.Message}"); throw new Exception($"Failed to save data in Cloud Save. Error: {e.Message}"); } }}

为玩家分配任务

您可以调用
AssignQuest
函数为玩家分配任务。此函数:
  • 为玩家分配 Remote Config 服务中的任务。
  • 为玩家创建一个
    QuestData
    对象并将其存储在 Cloud Save 中。
如果玩家已经在进行某个任务,此函数会做出相应的响应。

推进任务

您可以调用
PerformAction
函数来推进任务。要更新进度,请在玩家每次执行推进任务进度的操作时调用此函数。
  • 如果冷却时间已过期,或者这是玩家的第一次进度函数调用,那么玩家将获得一个进度点,并且他们在 Cloud Save 中的
    progress-left
    值会下降。
  • 如果在冷却期间调用此函数,Cloud Code 将做出响应:
    Player cannot make quest progress yet!
  • 如果调用成功,Cloud Code 将做出响应:
    Player made quest progress!
    。在 Cloud Save 中,Cloud Code 会减小玩家的
    progress-left
    值并更新他们的
    last-progress-time
  • 如果最后一次成功调用使
    progress-left
    为零,Cloud Code 会通知玩家他们已完成任务。任务数据会从 Cloud Save 中删除。

验证模块

要验证对 Cloud Save 数据的更改,您可以在 Unity Cloud Dashboard 中检查玩家的 Cloud Save 数据。
  1. 导航到 Unity Cloud Dashboard
  2. 选择 Products(产品)> Player Management(玩家管理)
  3. 选择 Player Management(玩家管理)
  4. 在搜索字段中,输入玩家 ID,然后选择 Find Player(查找玩家)
  5. 导航到 Cloud Save > Data(数据) 部分。
  6. quest-data
    键中追踪任务的进度。
要确认您的推送通知有效,请参阅推送消息