# ユースケースサンプル: 個々のプレイヤーに合わせてゲームの難易度を調整する

> 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 Admin 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 設定編集者) および **Triggers Configuration Viewer** (Triggers 設定閲覧者) を選択します。
   * Admin (管理者) ドロップダウンから **Unity Environments Viewer** (Unity 環境閲覧者) を選択します。
3. **Save** (保存) を選択します。
4. **Add Key** (キーの追加) を選択します。
5. base64 エンコードを使用して **Key ID** (キー ID) と **Secret key** (秘密鍵) をエンコードします。形式は "key\_id:secret\_key" です。この値をメモしておきます。

詳細については、[Authentication](../../authentication) を参照してください。

### Leaderboards の設定##configure-the-ugs-cli

スコアを送信するには、リーダーボードを設定する必要があります。Leaderboards をまだ使用したことがない場合は、[Leaderboards の使用の準備](/leaderboards/get-started.md) を参照してください。

[Unity Dashboard](https://cloud.unity.com) を使用してリーダーボードを設定できます。

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) は、スコアがリーダーボードに送信されたときに `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

プレイヤーのデータを Cloud Save に保存する `Config` クラスを作成します。

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

新しいプレイヤーの Cloud Save データを初期化する `TuneGameDifficulty` クラスを作成します。クラスには、プレイヤーがサインアップしたときにトリガーされる関数 `InitializeNewPlayer` が含まれます。クラスは、Cloud Save でプレイヤーのデータを初期化するために `Config` クラスに依存します。

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

プレイヤーがサインアップしたときに `InitializeNewPlayer` 関数をトリガーするために、トリガー設定を `trigger_configurations.json` ファイルに追加します。

```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 Admin API](../define-triggers/rest-api) を使用してトリガーを作成します。

> **Note:**
>
> **ノート:** [Triggers Admin API に個別のリクエストを送信](../define-triggers/rest-api) してトリガーを作成することもできます。API では、1 つのリクエストで複数のトリガーを作成することはできません。

> **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;
    }

}
```

これで、モジュールにトリガーごとにゲーム難易度を上げるすべての関数が追加されました。

## プレイヤー設定管理の設定##configure-player-config-management

ヘルパーメソッドを追加するクラス `PlayerConfigManagement` を定義します。

ヘルパーメソッドを使用して、Cloud Save 内のプレイヤーのデータを操作し、イベントを起動できます。ヘルパーメソッドを使用して、スコアをリーダーボードに送信することもできます。

プレイヤーが手動の難易度変更をリクエストしたときに、`ChangeDifficulty` メソッドを使用して、Cloud Save 内のプレイヤーのデータを初期値にリセットできます。このメソッドはトリガーに関連付けられていないことに注意してください。このメソッドはゲームクライアントから呼び出すことができます。

> **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) を参照してください。受信したトークンを Bearer トークンとしてリクエストヘッダーで HTTP 認証に使用します。

例えば、以下の 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. イベントをトリガーしながら、データ変更の進行状況を追跡します。
