使用示例:为各个玩家调整游戏难度级别

此示例展示了如何使用触发器根据 Cloud Save 中的玩家数据和 Leaderboards 中的分数来调整游戏难度级别。

**注意:**由于用例的复杂性,此示例使用了 Cloud Code 模块。

此示例使用了 Cloud Save 服务发出的 key-saved 事件、Leaderboards 服务发出的 score-submitted 事件以及 Authentication 服务发出的 signed-up 事件。

此示例使用 Cloud Save 中的玩家数据来追踪玩家的进度,并使用 Leaderboards 中的分数来追踪玩家的技能水平。此示例使用玩家的进度和技能水平来调整玩家个人的游戏难度级别。

此示例包含多个触发器,这些触发器以不同的方式影响游戏难度级别:

  • 当玩家提交高分时,增加游戏难度级别。
  • 当玩家完成一定数量的游戏会话时,增加游戏难度级别。
  • 当玩家解锁一定数量的成就时,增加游戏难度级别。
  • 当玩家完成一定数量的遭遇战时,增加游戏难度级别。
  • 当玩家成功完成一定次数的挑战时,增加游戏难度级别。
  • 当玩家获得一定数量的经验值时,增加游戏难度级别。
  • 当玩家挑战失败达到一定次数时,降低游戏难度级别。

触发器使用过滤器来评估事件有效负载,以便仅在某些 Cloud Save 键更新时触发 Cloud Code 逻辑。

**注意:**您不能通过 UGS CLI 创建带有过滤器的触发器。此示例使用 Triggers Admin API 来创建触发器。

先决条件

首先需要创建具有所需访问角色的服务帐户。

使用服务帐户进行身份验证

在调用 Triggers 服务之前,必须使用服务帐户进行身份验证。

  1. 导航到 Unity Dashboard
  2. 选择 Administration(管理)> Service Accounts(服务帐户)
  3. 选择 **New(新建)**按钮并输入服务帐户的名称和描述。
  4. 选择 Create(创建)

添加产品角色并创建密钥:

  1. 选择 Manage product roles(管理产品角色)
  2. 将以下角色添加到服务帐户:
    • 从 LiveOps 下拉选单中,选择 **Triggers Configuration Editor(Triggers 配置编辑者)**和 Triggers Configuration Viewer(Triggers 配置查看者)
    • 从 Admin(管理)下拉选单中,选择 Unity Environments Viewer(Unity 环境查看者)
  3. 选择 Save(保存)
  4. 选择 Add Key(添加密钥)
  5. 使用 base64 编码方式对 **Key ID(密钥 ID)**和 **Secret key(密钥)**进行编码。格式为“key_id:secret_key”。请记下此值。

如需了解更多信息,请参阅身份验证

设置 Leaderboards

要提交分数,您需要设置 Leaderboards。如果您以前没有使用过 Leaderboards,请参阅开始使用 Leaderboards

您可以使用 Unity Dashboard 设置 Leaderboards。

  1. 导航到 Unity Dashboard
  2. 选择 Leaderboards
  3. 选择 Add Leaderboard(添加排行榜)
  4. 输入排行榜的名称。
  5. 配置排行榜设置。
  6. 选择 Finish(完成)

记下排行榜 ID。

检查事件

此用例使用以下事件:

这些事件会将事件有效负载作为参数传递给 Cloud Code。

Cloud Save:保存键

在 Cloud Save 中更新键时,Cloud Save 服务会发出 key-saved 事件。

事件有效负载包含以下信息:

{
  "id": "7LpyhpsIvEczGkDI1r8J6gHhHezL",
  "idType": "player",
  "key": "LEVEL",
  "value": 1,
  "valueIncluded": true,
  "writeLock": "7b8920a57912509f6b5cbb183eb7fcb0",
  "accessClass": "default",
  "modifiedDate": "2021-03-04T09:00:00Z"
}

如需了解更多信息,请参阅 Cloud Save:保存键

Leaderboards:提交分数

在将分数提交到排行榜时,Leaderboards 服务会发出 score-submitted 事件。

事件有效负载包含以下信息:

{
  "leaderboardId": "leaderboard",
  "updatedTime": "2019-08-24T14:15:22Z",
  "playerId": "5drhidte8XgD4658j2eHtSljIAzd",
  "playerName": "Jane Doe",
  "rank": 42,
  "score": 120.3,
  "tier": "gold"
}

如需了解更多信息,请参阅 Leaderboards:提交分数

Authentication:注册

在玩家注册时,Authentication 服务会发出 signed-up 事件。

事件有效负载包含以下信息:

{
  "playerId": "string",
  "providerId": "string",
  "createdAt": "string"
}

如需了解更多信息,请参阅 Authentication:注册

在新玩家注册时初始化 Cloud Save 数据

为了追踪玩家进度,您需要为新玩家初始化 Cloud Save 数据。要初始化数据,您可以使用 Authentication:注册事件并触发 Cloud Code 模块终端。

创建一个 Cloud Code 模块,其中包含为新玩家初始化 Cloud Save 数据的函数:

定义一个类来存储玩家数据

创建一个 Config 类以将玩家的数据存储在 Cloud Save 中。

C#

using Newtonsoft.Json;

namespace TuneGameDifficulty;

/**
 * Config class is used to define the data structure of the player's data in Cloud Save.
 * The PlayerConfig class contains all the player's data. Any new player data should be added to this class.
 * The DifficultySettings class defines predefined difficulty levels and their corresponding settings for global game difficulty.
 *
 * The player is initialized with player data and default difficulty settings on Authentication.
 * However, the player can manually change the difficulty level at any time, and the settings will be updated accordingly, with the player's data reset.
 *
 * When the player's difficulty settings are modified by the use of Triggers, the values will not be accurate to the base settings defined in this class.
 * The default values for each difficulty levels are used as a base to calculate the new values.
 */
public abstract class Config
{

    // The DifficultySettings class defines the settings for each difficulty level.
    // You can use this class to define your own difficulty levels and their corresponding settings.
    public class DifficultySettings
    {
        [JsonProperty("EnemySpawnRate")] public double? EnemySpawnRate { get; set; }

        [JsonProperty("EnemyHealthMultiplier")]
        public double? EnemyHealthMultiplier { get; set; }

        [JsonProperty("ChallengeRewardsMultiplier")]
        public double? ChallengeRewardsMultiplier { get; set; }

        [JsonProperty("CurrencyRewardsMultiplier")]
        public double? CurrencyRewardsMultiplier { get; set; }

        [JsonProperty("TimeLimitMultiplier")] public double? TimeLimitMultiplier { get; set; }
    }

    // The DifficultyLevel enum defines the available difficulty levels for the game.
    public enum DifficultyLevel
    {
        VeryEasy,
        Easy,
        Medium,
        Hard,
        VeryHard,
        Custom
    }

    // The GlobalSettings dictionary defines the default settings for each difficulty level in the game.
    // The modifiers allow to increase or decrease the difficulty of the game.
    private static readonly Dictionary<DifficultyLevel, DifficultySettings> GlobalSettings = new()
    {
        {
            DifficultyLevel.VeryEasy, new DifficultySettings
            {
                EnemySpawnRate = 0.3,
                EnemyHealthMultiplier = 0.6,
                ChallengeRewardsMultiplier = 0.6,
                CurrencyRewardsMultiplier = 1.5,
                TimeLimitMultiplier = 2
            }
        },
        {
            DifficultyLevel.Easy, new DifficultySettings
            {
                EnemySpawnRate = 0.5,
                EnemyHealthMultiplier = 0.8,
                ChallengeRewardsMultiplier = 0.8,
                CurrencyRewardsMultiplier = 1.2,
                TimeLimitMultiplier = 1.5
            }
        },
        {
            DifficultyLevel.Medium, new DifficultySettings
            {
                EnemySpawnRate = 1,
                EnemyHealthMultiplier = 1,
                ChallengeRewardsMultiplier = 1,
                CurrencyRewardsMultiplier = 1,
                TimeLimitMultiplier = 1
            }
        },
        {
            DifficultyLevel.Hard, new DifficultySettings
            {
                EnemySpawnRate = 1.5,
                EnemyHealthMultiplier = 1.2,
                ChallengeRewardsMultiplier = 1.2,
                CurrencyRewardsMultiplier = 0.8,
                TimeLimitMultiplier = 0.7
            }
        },
        {
            DifficultyLevel.VeryHard, new DifficultySettings()
            {
                EnemySpawnRate = 2,
                EnemyHealthMultiplier = 1.5,
                ChallengeRewardsMultiplier = 1.5,
                CurrencyRewardsMultiplier = 0.5,
                TimeLimitMultiplier = 0.5
            }
        }
    };

    private const DifficultyLevel DefaultDifficulty = DifficultyLevel.VeryEasy;
    private const int DefaultValue = 0;

    // The PlayerConfig class defines the player's data structure in Cloud Save.
    // Any new player data should be added to this class.
    public class PlayerConfig
    {
        public long SessionsPlayed { get; set; } = DefaultValue;
        public long UnlockedAchievements { get; set; } = DefaultValue;
        public long EncounterFrequency { get; set; } = DefaultValue;
        public long CompletedChallenges { get; set; } = DefaultValue;
        public long FailedChallenges { get; set; } = DefaultValue;
        public long ExperiencePoints { get; set; } = DefaultValue;


        [JsonProperty("GameDifficulty")]
        public DifficultySettings GameDifficulty { get; set; }

        // Default constructor with optional parameter
        public PlayerConfig(DifficultyLevel difficulty = DefaultDifficulty)
        {
            GameDifficulty = GlobalSettings[difficulty];
        }

        public PlayerConfig(DifficultySettings difficultySettings)
        {
            GameDifficulty = difficultySettings;
        }
    }

}

该类包含一个 PlayerConfig 类,定义玩家在 Cloud Save 中的数据结构。

定义模块终端来初始化 Cloud Save 数据

创建一个 TuneGameDifficulty 类,为新玩家初始化 Cloud Save 数据。该类包含一个函数 InitializeNewPlayer,当玩家注册时触发。该类依赖 Config 类来初始化玩家在 Cloud Save 中的数据。

C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Shared;
using Unity.Services.CloudSave.Model;

namespace TuneGameDifficulty
{
    public class TuneGameDifficulty
    {
        private readonly ILogger<TuneGameDifficulty> _logger;

        public TuneGameDifficulty(ILogger<TuneGameDifficulty> logger)
        {
            _logger = logger;
        }

        /**
         * InitializeNewPlayer is called when a new player is created.
         * It initializes the player's data in Cloud Save with default values and is called on Authentication sign-up event.
         */
        [CloudCodeFunction("InitializeNewPlayer")]
        public async Task InitializeNewPlayer(IExecutionContext ctx, IGameApiClient gameApiClient, string playerId)
        {
            try
            {
                var response = await gameApiClient.CloudSaveData.GetItemsAsync(ctx, ctx.ServiceToken, ctx.ProjectId, playerId);
                if (response.Data.Results.Count == 0)
                {
                    var dataToSave = new Config.PlayerConfig();
                    var items = dataToSave.GetType().GetProperties().Select(property => new SetItemBody(property.Name, property.GetValue(dataToSave)!)).ToList();
                    await gameApiClient.CloudSaveData.SetItemBatchAsync(ctx, ctx.ServiceToken, ctx.ProjectId, playerId, new SetItemBatchBody(items));

                    _logger.LogInformation("The player {id} has been initialized with default values.", playerId);
                }
            }
            catch (ApiException e)
            {
                _logger.LogError("Failed to initialize player data for player {playerId} in Cloud Save. Error: {Error}", playerId, e);
                throw new Exception($"Failed to initialize {playerId} data in Cloud Save. Error: {e.Message}");
            }
        }

        public class ModuleConfig : ICloudCodeSetup
        {
            public void Setup(ICloudCodeConfig config)
            {
                config.Dependencies.AddSingleton(GameApiClient.Create());
            }
        }
    }
}

现在您有了一个可以在玩家注册时触发的模块。

Now you have a module that can trigger when a player signs up.

配置触发器

现在有了为新玩家初始化 Cloud Save 数据的模块后,可以配置触发器。

创建一个 trigger_configurations.json 文件来存储用于部署的触发器配置。

在新玩家注册时初始化 Cloud Save 数据

trigger_configurations.json 文件中添加触发器配置以在玩家注册时触发 InitializeNewPlayer 函数:

[
  {
    "name": "initialize-player-data-on-signup",
    "eventType": "com.unity.services.player-auth.signed-up.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/InitializeNewPlayer"
  }
]

此配置使用 Authentication 服务发出的 Authentication:注册事件。

请参阅使用示例:在新玩家注册时初始化 Cloud Save 数据以了解更多信息。

调整游戏难度级别

为了调整游戏难度级别,您需要创建由不同事件触发的多个触发器。这些触发器使用过滤器来评估事件有效负载,以便仅在定义的过滤条件满足时才触发 Cloud Code 逻辑。

您可以根据自己的游戏设计自定义过滤条件。例如,您只能在特定的值范围内触发 Cloud Save 事件。

trigger_congigurations.json 文件中添加以下触发器配置:

[
  {
    "name": "increase-difficulty-on-high-skill",
    "eventType": "com.unity.services.leaderboards.score-submitted.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnHighSkill",
    "filter": "data['score'] > 1000 && data['score'] < 5000"
  },
  {
    "name": "increase-difficulty-on-sessions-played",
    "eventType": "com.unity.services.cloud-save.key-saved.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnSessionsPlayed",
    "filter": "data['key']=='SessionsPlayed' && data['value'] > 0"
  },
  {
    "name": "increase-difficulty-on-achievement-unlocks",
    "eventType": "com.unity.services.cloud-save.key-saved.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnAchievements",
    "filter": "data['key']=='UnlockedAchievements' && data['value'] > 0"
  },
  {
    "name": "increase-difficulty-on-encounter-frequency",
    "eventType": "com.unity.services.cloud-save.key-saved.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnEncounterFrequency",
    "filter": "data['key']=='EncounterFrequency' && data['value'] > 0"
  },
  {
    "name": "increase-difficulty-on-winning-challenges",
    "eventType": "com.unity.services.cloud-save.key-saved.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnChallengeCompletion",
    "filter": "data['key'] == 'CompletedChallenges' && data['value'] > 0"
  },
  {
    "name": "increase-difficulty-on-experience-points",
    "eventType": "com.unity.services.cloud-save.key-saved.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnExperiencePoints",
    "filter": "data['key'] == 'ExperiencePoints' && data['value'] > 0"
  },
  {
    "name": "decrease-difficulty-on-failed-challenges",
    "eventType": "com.unity.services.cloud-save.key-saved.v1",
    "actionType": "cloud-code",
    "actionUrn": "urn:ugs:cloud-code:TuneGameDifficulty/TriggerOnChallengeFailure",
    "filter": "data['key'] == 'FailedChallenges' && data['value'] > 0"
  }
]

部署触发器

运行以下 bash 脚本来创建触发器。确保 trigger_configurations.json 文件与脚本位于同一目录中。

**注意:**您不能通过 UGS CLI 创建带有过滤器的触发器。此示例使用 Triggers Admin API 来创建触发器。

**注意:**您还可以向 Triggers Admin API 发送单独的请求来创建触发器。此 API 不允许您在单个请求中创建多个触发器。

Note: You can also send individual requests to the Triggers Admin API to create the triggers. The API doesn't allow you to create multiple triggers in a single request.

使用密钥 ID 和密钥的 base64 编码值作为 Authorization 标头的值。

#!/bin/bash

API_URL='https://services.api.unity.com/triggers/v1/projects/<PROJECT_ID>/environments/<ENVIRONMENT_ID>/configs'
HEADERS=(-H "Content-Type: application/json" -H "Authorization: Basic YOUR_KEY_ID:YOUR_SECRET_KEY")

jq -c '.[]' < trigger_configurations.json | while IFS= read -r line; do
  curl -X POST "$API_URL" "${HEADERS[@]}" --data-raw "$line"
done

**注意:**该脚本使用 jq 命令行 JSON 处理器来迭代 trigger_configurations.json 文件中的触发器配置。在运行该脚本之前,请确保已安装 jq

定义逻辑来调整游戏难度级别

创建一个 Cloud Code 模块,其中包含增加每个触发器的游戏难度级别的所有函数。

向该模块添加一个 MultplierConfig 类。此类定义难度修改器的默认值。以下示例使用乘数来计算触发器上每个难度级别的新值。您可以使用此类根据您的游戏设计自定义难度修改器。

C#

namespace TuneGameDifficulty;

/**
 * MultiplierConfig class is used to store the default values for the difficulty modifiers.
 * The multipliers are used to calculate the new values for each difficulty level on triggers.
 */
public static class MultiplierConfig
{
    public enum ConfigurationSetting
    {
        SessionsPlayed,
        UnlockedAchievements,
        EncounterFrequency,
        CompletedChallenges,
        FailedChallenges,
        ExperiencePoints,
    }

    public static readonly Dictionary<ConfigurationSetting, double> DefaultModifiers = new()
    {
        { ConfigurationSetting.SessionsPlayed, 0.1 },
        { ConfigurationSetting.UnlockedAchievements, 0.2 },
        { ConfigurationSetting.EncounterFrequency, 0.2 },
        { ConfigurationSetting.CompletedChallenges, 0.4 },
        { ConfigurationSetting.FailedChallenges, 0.5 },
        { ConfigurationSetting.ExperiencePoints, 0.6 }
    };

}

向该模块添加一个类 DifficultyTriggerHandler,定义每个触发器的逻辑。此类包含每个触发器的函数。

  • 该函数与触发该函数的事件相关。例如,TriggerOnHighSkill 函数与 score-submitted 事件相关。
  • 该函数将事件有效负载作为参数。例如,TriggerOnHighSkill 函数从 score-submitted 事件有效负载中获取 scoreplayerId 作为参数。
  • 该函数使用 MultiplierConfig 类来计算难度设置的默认修改器。例如,TriggerOnHighSkill 函数使用 SessionsPlayed 键的默认修改器来计算 EnemySpawnRate 难度设置的新值。
  • 该函数使用 GenerateNewValue 辅助方法来计算难度设置的新值。这可以根据您的游戏设计进行自定义。

C#

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 TuneGameDifficulty;

/**
 * This class contains the Cloud Code functions that are tied to Trigger events.
 * The functions are associated with the following events:
 * - Leaderboards Score Submitted
 * - Cloud Save Key Saved
 *
 * The functions are associated with the following triggers:
 * - TriggerOnHighSkill is associated with the Leaderboards Score Submitted event.
 * - TriggerOnSessionsPlayed is associated with the Cloud Save Key Saved event.
 * - TriggerOnAchievements is associated with the Cloud Save Key Saved event.
 * - TriggerOnEncounterFrequency is associated with the Cloud Save Key Saved event.
 * - TriggerOnChallengeCompletion is associated with the Cloud Save Key Saved event.
 * - TriggerOnChallengeFailure is associated with the Cloud Save Key Saved event.
 * - TriggerOnExperiencePoints is associated with the Cloud Save Key Saved event.
 */
public class DifficultyTriggerHandler
{
    private const string GameDifficultyKey = "GameDifficulty";
    private readonly ILogger<DifficultyTriggerHandler> _logger;
    public DifficultyTriggerHandler(ILogger<DifficultyTriggerHandler> logger)
    {
        _logger = logger;
    }

    /**
     * Generates a new value for particular difficulty setting based on the default modifier and the new value of the affecting key.
     * - newAffecting value is the new value of the affecting key, for example the new value of SessionsPlayed key when the player plays a new session.
     * - defaultModifierForAffectingValue is the default modifier for the affecting value, for example the default modifier for SessionsPlayed key found in MultiplierConfig.
     * - lastDifficultyValue is the last value of the difficulty setting, for example the last value of EnemySpawnRate.
     * - increaseValue is a boolean that indicates whether the new value should be added or subtracted from the last value.
     *
     * The new value is calculated as follows:
     * - if newAffectingValue for the SessionsPlayed key is 2, the new value for EnemySpawnRate is calculated by adding 2 * 0.1 to the last value of EnemySpawnRate.
     */
    private static Task<double> GenerateNewValue(double newAffectingValue, double defaultModifierForAffectingValue, double lastDifficultyValue, bool increaseValue = true)
    {
        // Ensure the new value is not less than 0.1
        const double minValue = 0.1;
        double scalingFactor = 0.1;  // Adjust this value to control the rate of reduction

        double newValue = increaseValue
            ? lastDifficultyValue + newAffectingValue * defaultModifierForAffectingValue
            : lastDifficultyValue - scalingFactor * newAffectingValue * Math.Abs(defaultModifierForAffectingValue);

        return Task.FromResult(Math.Max(minValue, newValue));
    }

    /**
     * TriggerOnHighSkill function is associated with the Leaderboards Score Submitted event.
     * The default filter for the trigger is set to trigger the function when the player's score 1000 > x > 5000.
     */
    [CloudCodeFunction("TriggerOnHighSkill")]
    public async Task TriggerOnHighSkill(IExecutionContext ctx, IGameApiClient gameApiClient, double score, string playerId)
    {
        var scoreDifficultyMapping = new Dictionary<double, int>()
        {
            {1000, 1},
            {2000, 2},
            {3000, 3},
            {4000, 4},
            {5000, 5}
        };

        var scoreLevel = scoreDifficultyMapping.First(x => x.Key > score).Value;
        var currentDifficultyForPlayer = await ReadCurrentPlayerData(ctx, gameApiClient, playerId);

        var newEnemySpawnRate = await GenerateNewValue(scoreLevel, 0.3, (double)currentDifficultyForPlayer.EnemySpawnRate!);
        var newEnemyHealthMultiplier = await GenerateNewValue(scoreLevel, 0.2, (double)currentDifficultyForPlayer.EnemyHealthMultiplier!);

        await UpdatePlayerGameConfiguration(ctx, gameApiClient, currentDifficultyForPlayer, new Config.DifficultySettings
        {
            EnemySpawnRate = newEnemySpawnRate,
            EnemyHealthMultiplier = newEnemyHealthMultiplier
        }, playerId);
    }


    /**
     * TriggerOnSessionsPlayed function is associated with Cloud Save Key Saved event.
     * The default filter for the trigger is set to trigger the function when the player's SessionsPlayed key is updated.
     */
    [CloudCodeFunction("TriggerOnSessionsPlayed")]
    public async Task TriggerOnSessionsPlayed(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id)
    {
        await TriggerOnConfiguration(ctx, gameApiClient, value, id, new List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)>
        {
            (MultiplierConfig.ConfigurationSetting.SessionsPlayed, ds => ds.EnemySpawnRate, (ds, newMultiplier) => ds.EnemySpawnRate = newMultiplier, true)
        });
    }



    /**
     * TriggerOnAchievements function is associated with the Cloud Save Key Saved event.
     * The default filter for the trigger is set to trigger the function when the player's UnlockedAchievements key is updated.
    */
    [CloudCodeFunction("TriggerOnAchievements")]
    public async Task TriggerOnAchievements(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id)
    {
        await TriggerOnConfiguration(ctx, gameApiClient, value, id, new List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)>
        {
            (MultiplierConfig.ConfigurationSetting.UnlockedAchievements, ds => ds.ChallengeRewardsMultiplier, (ds, newMultiplier) => ds.ChallengeRewardsMultiplier = newMultiplier, true)
        });
    }

    /**
    * TriggerOnEncounterFrequency function is associated with the Cloud Save Key Saved event.
    * The default filter for the trigger is set to trigger the function when the player's EncounterFrequency key is updated.
    */
    [CloudCodeFunction("TriggerOnEncounterFrequency")]
    public async Task TriggerOnEncounterFrequency(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id)
    {
        await TriggerOnConfiguration(ctx, gameApiClient, value, id, new List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)>
        {
            (MultiplierConfig.ConfigurationSetting.EncounterFrequency, ds => ds.EnemySpawnRate, (ds, newMultiplier) => ds.EnemySpawnRate = newMultiplier, true),
            (MultiplierConfig.ConfigurationSetting.EncounterFrequency, ds => ds.TimeLimitMultiplier, (ds, newMultiplier) => ds.TimeLimitMultiplier = newMultiplier, false)
        });
    }

    /**
     * TriggerOnChallengeCompletion function is associated with the Cloud Save Key Saved event.
     * The default filter for the trigger is set to trigger the function when the player's CompletedChallenges key is updated.
     */
    [CloudCodeFunction("TriggerOnChallengeCompletion")]
    public async Task TriggerOnChallengeCompletion(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id)
    {
        await TriggerOnConfiguration(ctx, gameApiClient, value, id, new List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)>
        {
            (MultiplierConfig.ConfigurationSetting.CompletedChallenges, ds => ds.CurrencyRewardsMultiplier, (ds, newMultiplier) => ds.CurrencyRewardsMultiplier = newMultiplier, true),
            (MultiplierConfig.ConfigurationSetting.CompletedChallenges, ds => ds.ChallengeRewardsMultiplier, (ds, newMultiplier) => ds.ChallengeRewardsMultiplier = newMultiplier, true)
        });
    }

    /**
     * TriggerOnChallengeFailure function is associated with the Cloud Save Key Saved event.
     * The default filter for the trigger is set to trigger the function when the player's FailedChallenges key is updated.
     */
    [CloudCodeFunction("TriggerOnChallengeFailure")]
    public async Task TriggerOnChallengeFailure(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id)
    {
        await TriggerOnConfiguration(ctx, gameApiClient, value, id, new List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)>
        {
            (MultiplierConfig.ConfigurationSetting.FailedChallenges, ds => ds.EnemyHealthMultiplier, (ds, newMultiplier) => ds.EnemyHealthMultiplier = newMultiplier, false),
            (MultiplierConfig.ConfigurationSetting.FailedChallenges, ds => ds.EnemySpawnRate, (ds, newMultiplier) => ds.EnemySpawnRate = newMultiplier, false),
            (MultiplierConfig.ConfigurationSetting.FailedChallenges, ds => ds.TimeLimitMultiplier, (ds, newMultiplier) => ds.TimeLimitMultiplier = newMultiplier, true),
        });
    }


    /**
     * TriggerOnExperiencePoints function is associated with the Cloud Save Key Saved event.
     * The default filter for the trigger is set to trigger the function when the player's ExperiencePoints key is updated.
     */
    [CloudCodeFunction("TriggerOnExperiencePoints")]
    public async Task TriggerOnExperiencePoints(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id)
    {
        await TriggerOnConfiguration(ctx, gameApiClient, value, id, new List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)>
        {
            (MultiplierConfig.ConfigurationSetting.ExperiencePoints, ds => ds.TimeLimitMultiplier, (ds, newMultiplier) => ds.TimeLimitMultiplier = newMultiplier, false),
            (MultiplierConfig.ConfigurationSetting.ExperiencePoints, ds => ds.EnemyHealthMultiplier, (ds, newMultiplier) => ds.EnemyHealthMultiplier = newMultiplier, true),
        });
    }

    /**
     * TriggerOnConfiguration is a helper method that updates the player's game difficulty settings in Cloud Save.
     * The method takes a list of tuples that contain the following:
     * - The configuration setting that is being updated
     * - A function that returns the last value of the configuration setting
     * - An action that sets the new value of the configuration setting
     * - A boolean that indicates whether the new value should be added or subtracted from the last value
     * The method loops through the list and updates the configuration settings accordingly.
     */
    private async Task TriggerOnConfiguration(IExecutionContext ctx, IGameApiClient gameApiClient, double value, string id, List<(MultiplierConfig.ConfigurationSetting, Func<Config.DifficultySettings, double?>, Action<Config.DifficultySettings, double>, bool)> settings)
    {
        var currentDifficultyForPlayer = await ReadCurrentPlayerData(ctx, gameApiClient, id);
        var newConfigSettings = new Config.DifficultySettings();

        // for each, append the new value to the new config settings object
        foreach (var (configurationSetting, getLastValue, setNewValue, increaseValue) in settings)
        {
            var multiplier = MultiplierConfig.DefaultModifiers[configurationSetting];
            var lastValue = getLastValue(currentDifficultyForPlayer);

            var newValue = await GenerateNewValue(value, multiplier, (double)lastValue!, increaseValue);
            setNewValue(newConfigSettings, newValue);
        }

        await UpdatePlayerGameConfiguration(ctx, gameApiClient, currentDifficultyForPlayer, newConfigSettings, id);
    }


    // ReadCurrentPlayerData is a method that reads the player's configuration settings from Cloud Save, including the game difficulty settings as a PlayerConfig object.
    private async Task<Config.DifficultySettings> ReadCurrentPlayerData(IExecutionContext ctx, IGameApiClient gameApiClient,
        string playerId)
    {
        try
        {
            var response = await gameApiClient.CloudSaveData.GetItemsAsync(ctx, ctx.ServiceToken, ctx.ProjectId, playerId);
            var gameDifficulty = JsonConvert.DeserializeObject<Config.DifficultySettings>(response.Data.Results.First(x => x.Key == GameDifficultyKey).Value.ToString());
            return gameDifficulty!;
        }
        catch (ApiException e)
        {
            _logger.LogError("Failed to read player {playerId} game difficulty settings in Cloud Save. Error: {Error}", playerId, e.Message);
            throw new Exception($"Failed to initialize {playerId} data in Cloud Save. Error: {e.Message}");
        }
    }

    // UpdatePlayerGameConfiguration is a method that updates the player's game difficulty settings in Cloud Save.
    private async Task UpdatePlayerGameConfiguration(IExecutionContext ctx, IGameApiClient gameApiClient, Config.DifficultySettings currentSettings, Config.DifficultySettings newSettings,
        string playerId)
    {
        try
        {
            var mergedSettings = MergeSettings(currentSettings, newSettings);
            await gameApiClient.CloudSaveData.SetItemAsync(ctx, ctx.ServiceToken, ctx.ProjectId, playerId, new SetItemBody(GameDifficultyKey, mergedSettings));
        }
        catch (ApiException e)
        {
            _logger.LogError("Failed to update player {playerId} game difficulty settings in Cloud Save. Error: {Error}", playerId, e.Message);
            throw new Exception($"Failed to initialize {playerId} data in Cloud Save. Error: {e.Message}");
        }
    }

    // MergeSettings is a helper method that merges the current settings with the new settings. Only the new settings that are not null are updated.
    private static Config.DifficultySettings MergeSettings(Config.DifficultySettings currentSettings, Config.DifficultySettings newSettings)
    {
        var mergedSettings = new Config.DifficultySettings();
        var properties = typeof(Config.DifficultySettings).GetProperties();

        foreach (var property in properties)
        {
            var currentValue = (double?)property.GetValue(currentSettings);
            var newValue = (double?)property.GetValue(newSettings);

            // Update only if the new value is not null
            property.SetValue(mergedSettings, newValue ??
                                              // If new value is null, retain the current value
                                              currentValue);
        }

        return mergedSettings;
    }

}

现在该模块包含了增加每个触发器的游戏难度级别的所有函数。

配置玩家配置管理

定义一个类 PlayerConfigManagement 来添加辅助方法。

您可以使用辅助方法来操作 Cloud Save 中的玩家数据并触发事件。此外,还可以使用辅助方法向排行榜提交分数。

当玩家请求手动更改难度时,您可以使用 ChangeDifficulty 方法将 Cloud Save 中的玩家数据重置为默认值。请注意,此方法与触发器无关。您可以从游戏客户端调用此方法。

注意:您可以使用访问控制来限制对 ChangeDifficulty 的访问,并确保只有经过授权的用户才能调用此方法。

C#

using Microsoft.Extensions.Logging;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode.Shared;
using Unity.Services.CloudSave.Model;
using Unity.Services.Leaderboards.Model;

namespace TuneGameDifficulty;

/**
 * PlayerConfigManagement class is used to manipulate the player's data in Cloud Save and fire Triggers.
 * It contains helper methods to update the player's data and submit scores to leaderboards to activate Triggers.
 */
public class PlayerConfigManagement
{
    private readonly ILogger<PlayerConfigManagement> _logger;
    public PlayerConfigManagement(ILogger<PlayerConfigManagement> logger)
    {
        _logger = logger;
    }


    // This method is called when the player requests a manual difficulty change.
    // All the current configuration is  to default values alongside the player data and the new difficulty is applied.
    [CloudCodeFunction("ChangeDifficulty")]
    public async Task ChangeDifficulty(IExecutionContext ctx, IGameApiClient gameApiClient, Config.DifficultyLevel newDifficulty)
    {
        try
        {
            var defaultPlayerConfig = new Config.PlayerConfig(newDifficulty);

            var items = defaultPlayerConfig.GetType().GetProperties().Select(property => new SetItemBody(property.Name, property.GetValue(defaultPlayerConfig)!)).ToList();
            await gameApiClient.CloudSaveData.SetItemBatchAsync(ctx, ctx.ServiceToken, ctx.ProjectId, ctx.PlayerId, new SetItemBatchBody(items));

            _logger.LogInformation("The player has reset their difficulty to {newDifficulty}. They progress has been reset.", newDifficulty);

        }
        catch (ApiException e)
        {
            _logger.LogError("Failed to reset player data in Cloud Save. Error: {Error}", e.Message);
            throw new Exception($"Failed to reset player data in Cloud Save. Error: {e.Message}");
        }
    }

    // Use this helper method to manipulate the player's data in Cloud Save and fire Triggers.
    [CloudCodeFunction("UpdatePlayerStats")]
    public async Task<string> UpdatePlayerStats(IExecutionContext ctx, IGameApiClient gameApiClient, string playerId, string key,
        double newValue)
    {
        try
        {
            await gameApiClient.CloudSaveData.SetItemAsync(ctx, ctx.ServiceToken, ctx.ProjectId, playerId, new SetItemBody(key, newValue));
            _logger.LogInformation("The player's {key} has been updated to {newValue}", key, newValue);
            return $"The player's {playerId} {key} has been updated to {newValue}";

        }
        catch (ApiException e)
        {
            _logger.LogError("Failed to update player data for player {playerId} in Cloud Save. Error: {Error}", playerId, e.Message);
            throw new Exception($"Failed to update player data for player {playerId} in Cloud Save. Error: {e.Message}");
        }
    }

    // Use this helper method to submit a score to the leaderboard.
    // Note: requires a leaderboard to be created first.
    [CloudCodeFunction("SubmitScore")]
    public async Task<string> SubmitScore(IExecutionContext ctx, IGameApiClient gameApiClient, string playerId, string leaderboardId, double score)
    {
        try
        {
            await gameApiClient.Leaderboards.AddLeaderboardPlayerScoreAsync(ctx, ctx.ServiceToken, new Guid(ctx.ProjectId), leaderboardId, playerId, new LeaderboardScore(score));
            _logger.LogInformation("The player's {playerId} score of {score} has been submitted to {leaderboardId}", playerId, score, leaderboardId);
            return $"The player's {playerId} score of {score} has been submitted to {leaderboardId}";

        }
        catch (ApiException e)
        {
            _logger.LogError("Failed to submit score to leaderboard {leaderboardId}. Error: {Error}", e.Message, leaderboardId);
            throw new Exception($"Failed to submit score to leaderboard {leaderboardId}. Error: {e.Message}");
        }
    }
}

部署 Cloud Code 模块

部署 Cloud Code 模块,其中包含增加每个触发器的游戏难度级别的函数。请参阅部署 Hello World 以了解如何部署模块。

**注意:**如果要使用相同的服务帐户部署该模块,请不要忘记添加额外的服务帐户角色 Cloud Code Editor

测试用例

为了测试用例,您可以触发与触发器相关的事件。

对于 API 请求,您需要使用 Bearer 令牌作为 Authorization 标头的值。请参阅身份验证,使用服务帐户或无状态令牌以玩家或受信任客户端的身份进行身份验证。请在请求标头中使用收到的令牌作为 HTTP 身份验证的持有者令牌。

例如,可以使用下面的 cURL 命令向排行榜提交分数以触发 TriggerOnHighSkill 函数:

curl -XPOST -H 'Authorization: Bearer <BEARER_TOKEN>' -H "Content-type: application/json" --data-raw '{
  "params": {
    "playerId": "<PLAYER-ID>",
    "leaderboardId": "<LEADERBOARD_ID>",
    "score": 2000
   }
}' 'https://cloud-code.services.api.unity.com/v1/projects/<PROJECT_ID>/modules/TuneGameDifficulty/SubmitScore'

还可以更新 Cloud Save 中的玩家数据以触发其他函数。例如,要触发 TriggerOnSessionsPlayed 函数,请使用 SessionsPlayed 键调用 UpdatePlayerStats 函数:

curl -XPOST -H 'Authorization: Bearer <BEARER_TOKEN>' -H "Content-type: application/json" --data-raw '{
  "params": {
      "key": "SessionsPlayed",
       "newValue": 10
   }
}' 'https://cloud-code.services.api.unity.com/v1/projects/<PROJECT_ID>/modules/TuneGameDifficulty/UpdatePlayerStats'

**注意:**如果您通过 Cloud Code 脚本测试 Cloud Save 事件,必须将参数类型定义为 number。如果没有定义特定类型,则参数将作为字符串传递,过滤器评估将失败。

**注意:**在真实的游戏场景中,要触发这些函数,您可以根据玩家的操作从游戏客户端调用终端。如需详细了解如何运行这些函数,请参阅运行模块

要验证对 Cloud Save 数据的更改,您可以在 Unity Cloud Dashboard 中检查玩家的 Cloud Save 数据。

  1. 导航到 Unity Cloud Dashboard
  2. 选择 Player Management(玩家管理)
  3. 在搜索字段中,输入玩家 ID,然后选择 Find Player(查找玩家)
  4. 导航到 Cloud Save > **Data(数据)**部分。
  5. 在触发事件时追踪数据变化进度。