任务系统
Implement a quest system that issues players with quests using Remote Config and Cloud Save.
阅读时间7 分钟最后更新于 1 个月前
任务系统示例演示了如何使用 Cloud Code 通过 Remote Config 和 Cloud Save 服务实现任务示例。此示例涵盖服务器授权反作弊、其他 UGS 服务响应的缓存以及玩家 Cloud Save 数据的修改。此示例使用推送消息在玩家完成任务时通知玩家。
概览
任务系统允许玩家调用 Cloud Code 模块来发布任务。为了向玩家提供任务,Cloud Code 可以执行以下操作:- 将任务存储在 Remote Config 中。
- 从 Remote Config 中获取活动任务列表,并随机选择一个任务。
- 将任务分配给玩家,并将数据存储在 Cloud Save 中。
- 每分钟的进度被限制在一定的速率以内。
- 玩家每分钟只能执行任务配置所允许的操作数量。
设置任务
您需要在 Remote Config 服务中定义您的任务:- 导航到 Unity Cloud Dashboard。
- 选择 Products(产品)> Remote Config。
- 选择 Config(配置) 选项卡。
- 选择 Add a key(添加键)。
- 输入 作为键名称。
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 }]
- 发布该配置。
- :任务的唯一标识符。
id - :任务的名称。
name - :完成任务所需的进度点数。
progress_required - :玩家每分钟可以获得的进度点数。
progress_per_minute - :玩家完成任务后获得的奖励。
reward
设置 Cloud Code
设置一个名为QuestSystem限制对 Cloud Save 的访问
要为您的 Cloud Save 数据增加一层安全保护,您可以使用访问控制来创建资源策略。 为了防止用户通过修改自己的 Cloud Save 数据来作弊,您需要为 UGS 创建资源策略。 创建一个名为cloud-save-resource-policy.json您可以使用 UGS CLI 来部署该文件:{ "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 access upsert-project-policy cloud-save-resource-policy.json
设置任务服务
创建一个名为QuestService.csusing 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.csDataClasses.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 模块,您需要设置QuestServiceGameApiClientPushClient- 管理和缓存任务。
QuestService - 需要 来调用 Remote Config 和 Cloud Save 服务。
GameApiClient - 需要 来向玩家发送推送通知。
PushClient
ICloudCodeSetupusing 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()); }}
任务控制器
创建QuestControllerusing 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 服务中的任务。
- 为玩家创建一个 对象并将其存储在 Cloud Save 中。
QuestData
推进任务
您可以调用PerformAction- 如果冷却时间已过期,或者这是玩家的第一次进度函数调用,那么玩家将获得一个进度点,并且他们在 Cloud Save 中的 值会下降。
progress-left - 如果在冷却期间调用此函数,Cloud Code 将做出响应:。
Player cannot make quest progress yet! - 如果调用成功,Cloud Code 将做出响应:。在 Cloud Save 中,Cloud Code 会减小玩家的
Player made quest progress!值并更新他们的progress-left。last-progress-time - 如果最后一次成功调用使 为零,Cloud Code 会通知玩家他们已完成任务。任务数据会从 Cloud Save 中删除。
progress-left
验证模块
要验证对 Cloud Save 数据的更改,您可以在 Unity Cloud Dashboard 中检查玩家的 Cloud Save 数据。- 导航到 Unity Cloud Dashboard。
- 选择 Products(产品)> Player Management(玩家管理)。
- 选择 Player Management(玩家管理)。
- 在搜索字段中,输入玩家 ID,然后选择 Find Player(查找玩家)。
- 导航到 Cloud Save > Data(数据) 部分。
- 在 键中追踪任务的进度。
quest-data