기술 자료

지원

Cloud Code

Cloud Code

사용 사례 샘플: 각 플레이어에 맞게 게임 난이도 조정

Tune game difficulty level for individual players based on their progress and skill level.
읽는 시간 12분최근 업데이트: 12시간 전

이 샘플에서는 Triggers를 사용하여 Cloud Save의 플레이어 데이터와 Leaderboards의 점수를 기반으로 게임 난이도를 조정하는 방법을 보여 줍니다. Cloud Save 서비스에서 발생하는
key-saved
이벤트, Leaderboards에서 발생하는
score-submitted
이벤트, Authentication에서 발생하는
signed-up
이벤트가 샘플에서 사용됩니다.
샘플은 Cloud Save의 플레이어 데이터를 사용하여 플레이어의 진행도를 트래킹하고 Leaderboards의 점수를 사용하여 플레이어의 스킬 레벨을 트래킹합니다. 샘플에서는 플레이어의 진행도와 스킬 레벨로 각 플레이어의 게임 난이도를 조정합니다. 이 샘플에는 다양한 방식으로 게임 난이도에 영향을 주는 여러 트리거가 포함되어 있습니다.
  • 플레이어가 높은 점수를 제출하면 게임 난도가 높아집니다.
  • 플레이어가 특정 수만큼 세션을 플레이하면 게임 난도가 높아집니다.
  • 플레이어가 특정 수만큼 목표를 잠금 해제하면 게임 난도가 높아집니다.
  • 플레이어가 특정 수만큼 매치를 치르면 게임 난도가 높아집니다.
  • 플레이어가 특정 수만큼 과제를 성공적으로 완수하면 게임 난도가 높아집니다.
  • 플레이어가 특정 수만큼 경험 포인트를 얻으면 게임 난도가 높아집니다.
  • 플레이어가 특정 수만큼 과제를 완수하지 못하면 게임 난도가 낮아집니다.
트리거는 필터를 통해 이벤트 페이로드를 평가하여 특정 Cloud Save 키가 업데이트될 때만 Cloud Code 로직을 트리거합니다.

필수 조건

먼저 필수 액세스 역할로 서비스 계정을 만들어야 합니다.

서비스 계정을 사용하여 인증

Triggers 서비스를 호출하려면 먼저 서비스 계정을 사용하여 인증해야 합니다.
  1. Unity Dashboard로 이동합니다.
  2. Administration > Service Accounts를 선택합니다.
  3. New 버튼을 선택하고 서비스 계정의 이름과 설명을 입력합니다.
  4. Create를 선택합니다.
다음과 같이 제품 역할을 추가하고 키를 만듭니다.
  1. Manage product roles를 선택합니다.
  2. 다음 역할을 서비스 계정에 추가합니다.
    • LiveOps 드롭다운에서 Triggers Configuration EditorTriggers Configuration Viewer를 선택합니다.
    • Admin 드롭다운에서 Unity Environments Viewer를 선택합니다.
  3. Save를 선택합니다.
  4. Add Key를 선택합니다.
  5. base64 인코딩을 사용하여 Key IDSecret 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 서비스는 점수가 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 Save 데이터를 초기화하는 함수가 포함되어 있는 Cloud Code 모듈을 생성합니다.

클래스를 정의하여 플레이어 데이터 저장

Config
클래스를 생성하여 Cloud Save에 플레이어 데이터를 저장합니다.
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; } }}
클래스에는 Cloud Save에서 플레이어의 데이터 구조를 정의하는
PlayerConfig
클래스가 포함되어 있습니다.

모듈 엔드포인트를 정의하여 Cloud Save 데이터 초기화

TuneGameDifficulty
클래스를 생성하여 신규 플레이어의 Cloud Save 데이터를 초기화합니다. 클래스에는 플레이어가 가입할 때 트리거 역할을 하는
InitializeNewPlayer
함수가 포함되어 있습니다. 클래스는
Config
클래스를 사용하여 Cloud Save의 플레이어 데이터를 초기화합니다.
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
파일이 스크립트와 동일한 디렉토리에 있어야 합니다.
키 ID와 비밀 키의 base64 인코딩 값을
Authorization
헤더의 값으로 사용합니다.
#!/bin/bashAPI_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

로직을 정의하여 게임 난이도 조정

각 트리거에 따라 게임 난도를 높일 수 있는 모든 함수가 포함된 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
    이벤트 페이로드에서
    score
    playerId
    를 파라미터로 사용합니다.
  • 함수는
    MultiplierConfig
    클래스를 사용하여 난이도 설정에서 기본 조정값을 계산합니다. 예를 들어
    TriggerOnHighSkill
    함수는
    SessionsPlayed
    키의 기본 조정값을 사용하여
    EnemySpawnRate
    난이도 설정에서 새 값을 계산합니다.
  • 함수는
    GenerateNewValue
    헬퍼 메서드를 사용하여 난이도 설정에서 새 값을 계산합니다. 이는 게임 디자인을 기반으로 커스터마이즈할 수 있습니다.
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; }}
이제 모듈에는 각 트리거에 따라 게임 난도를 높일 수 있는 모든 함수가 포함되어 있습니다.

플레이어 config 관리 구성

PlayerConfigManagement
클래스를 정의하여 헬퍼 메서드를 추가합니다.
헬퍼 메서드를 사용하여 Cloud Save의 플레이어 데이터를 조작하고 이벤트를 실행할 수 있습니다. 헬퍼 메서드로 리더보드에 점수를 제출할 수도 있습니다. 플레이어가 수동으로 난이도 변경을 요청할 경우 Cloud Save의 플레이어 데이터를 기본값으로 초기화하려면
ChangeDifficulty
메서드를 사용합니다. 이 메서드는 트리거와 연결되어 있지 않으니 참고하시기 바랍니다. 해당 메서드는 게임 클라이언트에서 호출할 수 있습니다.
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 배포를 참고하십시오.

사용 사례 테스트

사용 사례를 테스트하려면 트리거와 연결된 이벤트를 트리거합니다. API 요청의 경우
Authorization
헤더의 값으로
Bearer
토큰을 사용해야 합니다. 서비스 계정이나 상태 비보존 토큰을 사용하여 플레이어 또는 신뢰할 수 있는 클라이언트로 인증하려면 인증을 참고하십시오. 받은 토큰을 요청 헤더에서 HTTP 인증을 위한 bearer 토큰으로 사용합니다.
예를 들어 아래 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 Save 데이터에 대한 변경 사항을 확인하려면 Unity Cloud Dashboard에서 플레이어의 Cloud Save 데이터를 검사하면 됩니다.
  1. Unity Dashboard로 이동합니다.
  2. Player Management를 선택합니다.
  3. 검색 필드에 플레이어 ID를 입력하고 Find Player를 선택합니다.
  4. Cloud Save > Data 섹션으로 이동합니다.
  5. 이벤트를 트리거할 때 데이터 변경 사항의 진행도를 트래킹합니다.