Use case sample: Tune game difficulty level for individual player
Tune game difficulty level for individual players based on their progress and skill level.
Read time 16 minutesLast updated 18 hours ago
This sample shows how you can use Triggers to tune your game difficulty level based on player data in Cloud Save and scores in Leaderboards.
The sample uses the
key-savedscore-submittedsigned-up- Increase the game difficulty level when the player submits a high score.
- Increase the game difficulty level when the player has played a certain number of sessions.
- Increase the game difficulty level when the player has unlocked a certain number of achievements.
- Increase the game difficulty level when the player has played a certain number of encounters.
- Increase the game difficulty level when the player has successfully completed a certain number of challenges.
- Increase the game difficulty level when the player has earned a certain number of experience points.
- Decrease the game difficulty level when the player has failed a certain number of challenges.
Prerequisites
First, you need to create a service account with the required access roles.Authenticate using a Service Account
Before you can call the Triggers service, you must authenticate using a Service Account.- Navigate to the Unity Dashboard.
- Select Administration > Service Accounts.
- Select the New button and enter a name and description for the Service Account.
- Select Create.
- Select Manage product roles.
- Add the following roles to the Service Account:
- From the LiveOps dropdown, select Triggers Configuration Editor, Triggers Configuration Viewer, and Leaderboards Admin.
- From the Admin dropdown, select Unity Environments Viewer.
- From the LiveOps dropdown, select Triggers Configuration Editor, Triggers Configuration Viewer, and Leaderboards Admin.
- Select Save.
- Select Add Key.
- Encode the Key ID and Secret key using base64 encoding. The format is “key_id:secret_key”. Note this value down.
Configure the UGS CLI
Follow the steps below to get started with the UGS CLI:- Install the UGS CLI.
-
Configure your Project ID and Environment as such:
ugs config set project-id <your-project-id>
ugs config set environment-name <your-environment-name> - Authenticate using the Service account you created earlier. Refer to Get Authenticated.
Set up Leaderboards
To follow this sample, you need to create a leaderboard. If you haven't used Leaderboards before, refer to Get started with Leaderboards. You can use the UGS CLI to deploy the followingleaderboard.lbUse the UGS CLI tool to deploy the file:{ "$schema": "https://ugs-config-schemas.unity3d.com/v1/leaderboards.schema.json", "SortOrder": "asc", "UpdateType": "keepBest", "Name": "leaderboard"}
Now that you have created a leaderboard, you can add scores to it. Note down the Leaderboard ID.ugs deploy leaderboard.lb
Examine events
The use case uses the following events: The events pass the event payload to Cloud Code as parameters.Cloud Save: Key Saved
The Cloud Save service emits thekey-savedFor more information, refer to Cloud Save: Key Saved.{ "id": "7LpyhpsIvEczGkDI1r8J6gHhHezL", "idType": "player", "key": "LEVEL", "value": 1, "valueIncluded": true, "writeLock": "7b8920a57912509f6b5cbb183eb7fcb0", "accessClass": "default", "modifiedDate": "2021-03-04T09:00:00Z"}
Leaderboards: Score Submitted
Leaderboards emits thescore-submittedFor more information, refer to Leaderboards: Score Submitted.{ "leaderboardId": "leaderboard", "updatedTime": "2019-08-24T14:15:22Z", "playerId": "5drhidte8XgD4658j2eHtSljIAzd", "playerName": "Jane Doe", "rank": 42, "score": 120.3, "tier": "gold"}
Authentication: Signed up
The Authentication service emits thesigned-upFor more information, refer to Authentication: Signed up.{ "playerId": "string", "providerId": "string", "createdAt": "string"}
Initialize Cloud Save data for new players on sign-up
To track player progress, you need to initialize the Cloud Save data for new players. To initialize the data, you can use the Authentication: Signed up event and trigger a Cloud Code module endpoint. Create a Cloud Code module containing the function that initializes the Cloud Save data for new players:Define a class to store player data
Create aConfigThe class contains ausing 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; } }}
PlayerConfigDefine a module endpoint to initialize Cloud Save data
Create aTuneGameDifficultyInitializeNewPlayerConfigNow you have a module that can trigger when a player signs up.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()); } } }}
Configure triggers
Now that you have a module to initialize the Cloud Save data for new players, you can configure the triggers.
Run the
new-fileTheugs triggers new-file triggers-config
triggers-config.trInitialize Cloud Save data for new players on sign-up
Add a trigger configuration to thetriggers-config.trInitializeNewPlayerThis configuration consumes the Authentication: Signed up event emitted by the Authentication service. Refer to Use case sample: Initialize Cloud Save data for new players on sign-up for more information.{ "Configs": [ { "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" } ]}
Tune game difficulty level
To tune the game difficulty level, you need to create multiple triggers that are triggered by different events. The triggers use filters to evaluate the event payload to only trigger the Cloud Code logic when the defined filter criteria are met. You can customize the filter criteria based on your game design. For instance, you can trigger Cloud Save events only on certain value ranges. Add the following trigger configurations to thetriggers-config.tr{ "Configs": [ { "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" } ]}
Deploy triggers
Use the UGS CLI tool to deploy the configuration:You should get a response similar to the following:ugs deploy triggers-config.tr
Deployed:triggers-config.tr
Define logic to tune game difficulty level
Create a Cloud Code module containing all the functions that increase the game difficulty level for every trigger. Add aMultplierConfigAdd a classnamespace 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- The function ties to the event that triggers the function. For example, the function ties to the
TriggerOnHighSkillevent.score-submitted - The function takes the event payload as parameters. For example, the function takes the
TriggerOnHighSkillandscoreas parameters from theplayerIdevent payload.score-submitted - The function uses the class to calculate the default modifiers for the difficulty settings. For example, the
MultiplierConfigfunction uses the default modifier for theTriggerOnHighSkillkey to calculate the new value for theSessionsPlayeddifficulty setting.EnemySpawnRate - The function uses the helper method to calculate the new values for the difficulty settings. This can be customized based on your game design.
GenerateNewValue
Now the module contains all the functions that increase the game difficulty level for every trigger.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; }}
Configure player config management
Define a classPlayerConfigManagementChangeDifficultyusing 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}"); } }}
Deploy the Cloud Code module
Deploy the Cloud Code module containing the functions that increase the game difficulty level for every trigger. Refer to Deploying Hello World to learn how to deploy a module.Test the use case
To test the use case you can trigger the events that are associated with the triggers. For API requests, you need to use theBearerAuthorizationTriggerOnHighSkillYou can also update the player's data in Cloud Save to trigger other functions. For instance, to trigger thecurl -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'
TriggerOnSessionsPlayedUpdatePlayerStatsSessionsPlayedcurl -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'
To validate the changes to Cloud Save data, you can inspect the player's Cloud Save data in the Unity Dashboard.
- Navigate to Unity Dashboard.
- Select Player Management.
- In the search field, enter the player ID and select Find Player.
- Navigate to the Cloud Save > Data section.
- Track the progress of data changes as you trigger the events.