任务系统

本示例介绍如何将 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 函数时,它都能理解 QuestServiceGameAPIClient 的含义。您可以在此处进一步了解 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!”响应。达到此状态后,您可以通知玩家返回到向他们发出任务以获取奖励的位置或屏幕。