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

> Tune game difficulty level for individual players based on their progress and skill level.

이 샘플에서는 Triggers를 사용하여 [Cloud Save](/cloud-save.md)의 플레이어 데이터와 [Leaderboards](/leaderboards.md)의 점수를 기반으로 게임 난이도를 조정하는 방법을 보여 줍니다.

> **Note:**
>
> **참고:** 이 샘플에서는 사용 사례의 복잡도를 고려해 Cloud Code 모듈을 사용합니다.

[Cloud Save](/cloud-save.md) 서비스에서 발생하는 `key-saved` 이벤트, [Leaderboards](/leaderboards.md)에서 발생하는 `score-submitted` 이벤트, [Authentication](/authentication.md)에서 발생하는 `signed-up` 이벤트가 샘플에서 사용됩니다.

샘플은 Cloud Save의 플레이어 데이터를 사용하여 플레이어의 진행도를 트래킹하고 Leaderboards의 점수를 사용하여 플레이어의 스킬 레벨을 트래킹합니다. 샘플에서는 플레이어의 진행도와 스킬 레벨로 각 플레이어의 게임 난이도를 조정합니다.

이 샘플에는 다양한 방식으로 게임 난이도에 영향을 주는 여러 트리거가 포함되어 있습니다.

* 플레이어가 높은 점수를 제출하면 게임 난도가 높아집니다.
* 플레이어가 특정 수만큼 세션을 플레이하면 게임 난도가 높아집니다.
* 플레이어가 특정 수만큼 목표를 잠금 해제하면 게임 난도가 높아집니다.
* 플레이어가 특정 수만큼 매치를 치르면 게임 난도가 높아집니다.
* 플레이어가 특정 수만큼 과제를 성공적으로 완수하면 게임 난도가 높아집니다.
* 플레이어가 특정 수만큼 경험 포인트를 얻으면 게임 난도가 높아집니다.
* 플레이어가 특정 수만큼 과제를 완수하지 못하면 게임 난도가 낮아집니다.

트리거는 필터를 통해 이벤트 페이로드를 평가하여 특정 Cloud Save 키가 업데이트될 때만 Cloud Code 로직을 트리거합니다.

> **Note:**
>
> **참고:** UGS CLI를 통해 필터로 트리거를 생성할 수 없습니다. 이 샘플에서는 [Triggers 관리자 API](../define-triggers/rest-api)를 사용하여 트리거를 생성합니다.

## 필수 조건##prerequisites

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

### 서비스 계정을 사용하여 인증##authenticate-using-a-service-account

Triggers 서비스를 호출하려면 먼저 서비스 계정을 사용하여 인증해야 합니다.

1. [Unity Dashboard](https://cloud.unity.com)로 이동합니다.
2. **Administration** > **Service Accounts**를 선택합니다.
3. **New** 버튼을 선택하고 서비스 계정의 이름과 설명을 입력합니다.
4. **Create**를 선택합니다.

다음과 같이 제품 역할을 추가하고 키를 만듭니다.

1. **Manage product roles**를 선택합니다.
2. 다음 역할을 서비스 계정에 추가합니다.
   * LiveOps 드롭다운에서 **Triggers Configuration Editor**와 **Triggers Configuration Viewer**를 선택합니다.
   * Admin 드롭다운에서 **Unity Environments Viewer**를 선택합니다.
3. **Save**를 선택합니다.
4. **Add Key**를 선택합니다.
5. base64 인코딩을 사용하여 **Key ID**와 **Secret key**를 인코딩합니다. 포맷은 ‘key\_id:secret\_key’입니다. 이 값을 기록해 둡니다.

자세한 내용은 [인증](../../authentication)을 참고하십시오.

### Leaderboards 설정##configure-the-ugs-cli

점수를 제출하려면 Leaderboards를 설정해야 합니다. Leaderboards를 처음 사용하는 것이라면 [Leaderboards 시작하기](/leaderboards/get-started.md)를 참고하십시오.

[Unity Dashboard](https://cloud.unity.com)를 사용하여 Leaderboards를 설정할 수 있습니다.

1. [Unity Dashboard](https://cloud.unity.com)로 이동합니다.
2. **Leaderboards**를 선택합니다.
3. **Add Leaderboard**를 선택합니다.
4. 리더보드의 이름을 입력합니다.
5. 리더보드 설정을 구성합니다.
6. **Finish**를 선택합니다.

리더보드 ID를 기록해 둡니다.

## 이벤트 확인##examine-events

사용 사례에서 다음 이벤트가 사용됩니다.

* [Cloud Save: 키 저장](/triggers/manage-events/supported-ugs-events.md#key-saved)
* [Leaderboards: 점수 제출](/triggers/manage-events/supported-ugs-events.md#score-submitted)
* [Authentication: 가입](/triggers/manage-events/supported-ugs-events.md#signed-up)

이벤트는 Cloud Code에 이벤트 페이로드를 파라미터로 전달합니다.

### Cloud Save: 키 저장##set-up-leaderboards

[Cloud Save](/cloud-save.md) 서비스는 키가 Cloud Save에서 업데이트될 때 `key-saved` 이벤트를 발생시킵니다.

이벤트 페이로드에는 다음 정보가 포함됩니다.

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

자세한 내용은 [Cloud Save: 키 저장](/triggers/manage-events/supported-ugs-events.md#key-saved)을 참고하십시오.

### Leaderboards: 점수 제출##cloud-save-key-saved

[Leaderboards](/leaderboards.md) 서비스는 점수가 Leaderboards에 제출될 때 `score-submitted` 이벤트를 발생시킵니다.

이벤트 페이로드에는 다음 정보가 포함됩니다.

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

자세한 내용은 [Leaderboards: 점수 제출](/triggers/manage-events/supported-ugs-events.md#score-submitted)을 참고하십시오.

### Authentication: 가입##leaderboards-score-submitted

[Authentication](/authentication.md) 서비스는 플레이어가 가입할 때 `signed-up` 이벤트를 발생시킵니다.

이벤트 페이로드에는 다음 정보가 포함됩니다.

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

자세한 내용은 [Authentication: 가입](/triggers/manage-events/supported-ugs-events.md#signed-up)을 참고하십시오.

## 신규 플레이어 가입 시 Cloud Save 데이터 초기화##initialize-cloud-save-data-for-new-players-on-sign-up

플레이어 진행도를 트래킹하려면 신규 플레이어의 Cloud Save 데이터를 초기화해야 합니다. 데이터를 초기화하려면 [Authentication: 가입](/triggers/manage-events/supported-ugs-events.md#signed-up) 이벤트를 사용하여 Cloud Code 모듈 엔드포인트를 트리거합니다.

신규 플레이어의 Cloud Save 데이터를 초기화하는 함수가 포함되어 있는 Cloud Code 모듈을 생성합니다.

### 클래스를 정의하여 플레이어 데이터 저장##authentication-signed-up

`Config` 클래스를 생성하여 Cloud Save에 플레이어 데이터를 저장합니다.

```csharp
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 데이터 초기화##define-a-class-to-store-player-data

`TuneGameDifficulty` 클래스를 생성하여 신규 플레이어의 Cloud Save 데이터를 초기화합니다. 클래스에는 플레이어가 가입할 때 트리거 역할을 하는 `InitializeNewPlayer` 함수가 포함되어 있습니다. 클래스는 `Config` 클래스를 사용하여 Cloud Save의 플레이어 데이터를 초기화합니다.

```csharp
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.

## 트리거 구성##configure-triggers

이제 신규 플레이어의 Cloud Save 데이터를 초기화할 수 있는 모듈을 확보했으니 트리거를 구성할 수 있습니다.

`trigger_configurations.json` 파일을 만들어 배포용 트리거 구성을 저장합니다.

### 신규 플레이어 가입 시 Cloud Save 데이터 초기화##define-a-module-endpoint-to-initialize-cloud-save-data

`trigger_configurations.json` 파일에 트리거 구성을 추가하여 플레이어가 가입할 때 `InitializeNewPlayer` 함수를 트리거합니다.

```JSON
[
  {
    "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: 가입](/triggers/manage-events/supported-ugs-events.md#signed-up) 이벤트를 사용합니다.

[사용 사례 샘플: 신규 플레이어 가입 시 Cloud Save 데이터 초기화](./initialize-new-players.md)에서 자세히 알아 보십시오.

### 게임 난이도 조정##initialize-cloud-save-data-for-new-players-on-sign-up

게임 난이도를 조정하려면 다양한 이벤트에서 트리거되는 여러 트리거를 만들어야 합니다. 트리거는 필터를 사용해 이벤트 페이로드를 평가하여 정의된 필터 기준이 충족될 때만 Cloud Code 로직을 트리거합니다.

게임 디자인에 따라 필터 기준을 커스터마이즈할 수 있습니다. 예를 들어 특정 값 범위에 해당할 경우에만 Cloud Save 이벤트를 트리거할 수 있습니다.

`trigger_congigurations.json` 파일에 다음 트리거 구성을 추가합니다.

```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"
  }
]
```

### 트리거 배포##tune-game-difficulty-level

다음 `bash` 스크립트를 실행하여 트리거를 생성합니다. `trigger_configurations.json` 파일이 스크립트와 동일한 디렉토리에 있어야 합니다.

> **Note:**
>
> **참고:** UGS CLI를 통해 필터로 트리거를 생성할 수 없습니다. 이 샘플에서는 [Triggers 관리자 API](../define-triggers/rest-api)를 사용하여 트리거를 생성합니다.

> **Note:**
>
> **참고:** [Triggers 관리자 API에 개별 요청을 전송](../define-triggers/rest-api)하여 트리거를 생성할 수도 있습니다. API를 사용하여 개별 요청에서 여러 트리거를 생성할 수 없습니다.

> **Note:**
>
> You can also [send individual requests to the Triggers Admin API](../define-triggers/rest-api) to create the triggers. The API doesn't allow you to create multiple triggers in a single request.

키 ID와 비밀 키의 base64 인코딩 값을 `Authorization` 헤더의 값으로 사용합니다.

```bash
#!/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
```

> **Note:**
>
> **참고:** 스크립트는 [jq](https://stedolan.github.io/jq/) 커맨드 줄 JSON 프로세서를 사용하여 `trigger_configurations.json` 파일의 트리거 구성을 통해 반복됩니다. 스크립트 실행 전에 먼저 `jq`가 설치되어 있어야 합니다.

## 로직을 정의하여 게임 난이도 조정##define-logic-to-tune-game-difficulty-level

각 트리거에 따라 게임 난도를 높일 수 있는 모든 함수가 포함된 Cloud Code 모듈을 생성합니다.

모듈에 `MultplierConfig` 클래스를 추가합니다. 클래스는 난이도 조정값의 기본값을 정의합니다. 샘플에서는 멀티플라이어를 사용하여 트리거의 각 난이도에 따라 새 값을 계산합니다. 이 클래스를 사용하여 게임 디자인을 기반으로 난이도 조정값을 커스터마이즈할 수 있습니다.

### 트리거 배포##deploy-triggers

```csharp
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` 헬퍼 메서드를 사용하여 난이도 설정에서 새 값을 계산합니다. 이는 게임 디자인을 기반으로 커스터마이즈할 수 있습니다.

```csharp
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 관리 구성##configure-player-config-management

`PlayerConfigManagement` 클래스를 정의하여 헬퍼 메서드를 추가합니다.

헬퍼 메서드를 사용하여 Cloud Save의 플레이어 데이터를 조작하고 이벤트를 실행할 수 있습니다. 헬퍼 메서드로 리더보드에 점수를 제출할 수도 있습니다.

플레이어가 수동으로 난이도 변경을 요청할 경우 Cloud Save의 플레이어 데이터를 기본값으로 초기화하려면 `ChangeDifficulty` 메서드를 사용합니다. 이 메서드는 트리거와 연결되어 있지 않으니 참고하시기 바랍니다. 해당 메서드는 게임 클라이언트에서 호출할 수 있습니다.

> **Note:**
>
> **참고**: [액세스 제어](/triggers/use-cloud-code/access-control.md)를 사용하여 `ChangeDifficulty`에 대한 액세스를 제한하고 권한이 있는 사용자만 메서드를 호출하도록 할 수 있습니다.

```csharp
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 모듈 배포##deploy-the-cloud-code-module

각 트리거에 따라 게임 난도를 높일 수 있는 모든 함수가 포함된 Cloud Code 모듈을 배포합니다. 모듈을 배포하는 방법을 알아보려면 [Hello World 배포](/cloud-code/modules/getting-started.md)를 참고하십시오.

> **Note:**
>
> **참고:** 동일한 서비스 계정으로 모듈을 배포하려면 서비스 계정에 `Cloud Code Editor` 역할을 추가하는 것을 잊지 말아야 합니다.

## 사용 사례 테스트##test-the-use-case

사용 사례를 테스트하려면 트리거와 연결된 이벤트를 트리거합니다.

API 요청의 경우 `Authorization` 헤더의 값으로 `Bearer` 토큰을 사용해야 합니다. 서비스 계정이나 상태 비보존 토큰을 사용하여 플레이어 또는 신뢰할 수 있는 클라이언트로 인증하려면 [인증](/cloud-code/modules/how-to-guides/authentication.md)을 참고하십시오. 받은 토큰을 요청 헤더에서 HTTP 인증을 위한 bearer 토큰으로 사용합니다.

예를 들어 아래 cURL 커맨드를 사용하여 리더보드에 점수를 제출해 `TriggerOnHighSkill` 함수를 트리거할 수 있습니다.

```bash
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` 함수를 호출합니다.

```bash
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'
```

> **Note:**
>
> **참고:** Cloud Code 스크립트를 통해 Cloud Save 이벤트를 테스트하는 경우 파라미터 유형을 `number`로 정의해야 합니다. 특정 유형이 정의되어 있지 않으면 파라미터가 문자열로 전달되어 필터 평가가 완료되지 않습니다.

> **Note:**
>
> **참고:** 실제 게임 시나리오에서 함수를 트리거하려면 플레이어 액션을 기반으로 게임 클라이언트에서 엔드포인트를 호출합니다. 함수 실행 방법에 관한 자세한 내용은 [모듈 실행](/cloud-code/modules/how-to-guides/run-modules.md)을 참고하십시오.

Cloud Save 데이터에 대한 변경 사항을 확인하려면 Unity Dashboard에서 플레이어의 Cloud Save 데이터를 검사하면 됩니다.

1. [Unity Dashboard](https://cloud.unity.com)로 이동합니다.
2. **Player Management**를 선택합니다.
3. 검색 필드에 플레이어 ID를 입력하고 **Find Player**를 선택합니다.
4. **Cloud Save** > **Data** 섹션으로 이동합니다.
5. 이벤트를 트리거할 때 데이터 변경 사항의 진행도를 트래킹합니다.
