文档

支持

Cloud Code

任务系统

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

任务系统示例演示了如何使用 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
    键中追踪任务的进度。
要确认您的推送通知有效,请参阅推送消息