クエストシステム
Implement a quest system that issues players with quests using Remote Config and Cloud Save.
読み終わるまでの所要時間 16 分最終更新 5ヶ月前
クエストシステムサンプルは、Cloud Code を使用して Remote Config サービスと Cloud Save サービスでクエストサンプルを実装する方法を示します。このサンプルには、サーバー主導型チート対策、他の UGS サービスレスポンスのキャッシュ、プレイヤーの Cloud Save データの変更が含まれています。このサンプルは プッシュメッセージ を使用して、クエストの終了時にプレイヤーに通知します。
概要
クエストシステムを使用すると、プレイヤーが Cloud Code モジュールを呼び出して、自らに対してクエストを発行できます。プレイヤーにクエストを提供するために、Cloud Code は以下を行います。
- クエストを Remote Config に格納します。
- アクティブなクエストのリストを Remote Config から取得し、1 つのクエストをランダムに選択します。
- クエストをプレイヤーに割り当て、データを Cloud Save に格納します。
プレイヤーは、クエストの進行状況に加算されるアクションを実行できます。
Cloud Code は、プレイヤーが最後に進行したのはいつかを確認し、以下の制限事項に応じて進行状況に加算されるアクションを許可または拒否します。
- 進行の 1 分あたりの速度は制限されます。
- 毎分、プレイヤーはクエスト設定で許可されたアクション数までしか実行できません。
これは、プレイヤーがクエストの進行ポイントを受け取ることができるレートを制限することで、シンプルなチート対策手法として機能します。
プレイヤーがクエストを完了すると、Cloud Save がクエストデータを検出し、プッシュメッセージ でプレイヤーに通知します。
クエストの設定
Remote Config サービスでクエストを定義する必要があります。
- Unity Dashboard に移動します。
- Products (製品) > Remote Config を選択します。
- Config (設定) タブを選択します。
- Add a key (キーの追加) を選択します。
- キー名として を入力します。
QUESTS - 型として JSON を選択します。
- その値として以下を貼り付けます。
[ { "id": 1, "name": "Chop Trees", "progress_required": 5, "progress_per_minute": 1, "reward": 5 }, { "id": 2, "name": "Mine Gold", "progress_required": 5, "progress_per_minute": 1, "reward": 10 }]
- 設定を公開します。
設定はアクティブなクエストのデータベースとして使用されます。
クエストの構造は以下のとおりです。
- : クエストの一意識別子。
id - : クエストの名前。
name - : クエストを完了するために必要な進行ポイントの数。
progress_required - : プレイヤーが 1 分あたりに実行できる進行ポイントの数。
progress_per_minute - : プレイヤーがクエストを完了したときに受け取る報酬。
reward
例えば、1 分間のクールダウンが終わるたびにプレイヤーがアクションを実行することを想定すると、Chop Trees クエストを完了できるのは 5 分後です。
Cloud Code の設定
クエストシステムを処理する、 という Cloud Code モジュールを設定します。
QuestSystemCloud Save へのアクセスの制限
Cloud Save データにセキュリティレイヤーを追加するには、アクセス制御 を使用してリソースポリシーを作成できます。
ユーザーが自分の Cloud Save データを修正して不正を行うことを防ぐために、UGS のリソースポリシーを作成する必要があります。
cloud-save-resource-policy.json{ "statements": [ { "Sid": "deny-cloud-save-data-write-access", "Effect": "Deny", "Action": ["Write"], "Principal": "Player", "Resource": "urn:ugs:cloud-save:/v1/data/projects/*/players/*/items**" } ]}
UGS CLI を使用してこれをデプロイできます。
ugs access upsert-project-policy cloud-save-resource-policy.json
リソースポリシーの詳細については、アクセス制御 を参照してください。
クエストサービスの設定
QuestService.csクエストサービスにより、Remote Config からクエストがフェッチされ、設定した期間にわたりキャッシュされます。
using Microsoft.Extensions.Logging;using Newtonsoft.Json;using Unity.Services.CloudCode.Apis;using Unity.Services.CloudCode.Core;using Unity.Services.CloudCode.Shared;namespace QuestSystem;public interface IQuestService{ List<Quest> GetAvailableQuests(IExecutionContext context, IGameApiClient gameApiClient);}public class QuestService : IQuestService{ private const string QuestKey = "QUESTS"; private readonly ILogger<QuestService> _logger; public QuestService(ILogger<QuestService> logger) { _logger = logger; } private DateTime? CacheExpiryTime { get; set; } // Reminder: cache cannot be guaranteed to be consistent across all requests private List<Quest>? QuestCache { get; set; } public List<Quest> GetAvailableQuests(IExecutionContext context, IGameApiClient gameApiClient) { if (QuestCache == null || DateTime.Now > CacheExpiryTime) { var quests = FetchQuestsFromConfig(context, gameApiClient); QuestCache = quests; CacheExpiryTime = DateTime.Now.AddMinutes(5); // data in cache expires after 5 mins } return QuestCache; } private List<Quest> FetchQuestsFromConfig(IExecutionContext ctx, IGameApiClient gameApiClient) { try { var result = gameApiClient.RemoteConfigSettings.AssignSettingsGetAsync(ctx, ctx.AccessToken, ctx.ProjectId, ctx.EnvironmentId, null, new List<string> { "QUESTS" }); var settings = result.Result.Data.Configs.Settings; return JsonConvert.DeserializeObject<List<Quest>>(settings[QuestKey].ToString()); } catch (ApiException e) { _logger.LogError($"Failed to assign Remote Config settings. Error: {e.Message}"); throw new Exception($"Failed to assign Remote Config settings. Error: {e.Message}"); } }}
データクラス
DataClasses.csDataClasses.csusing Newtonsoft.Json;namespace QuestSystem;public class Quest{ [JsonProperty("id")] public int ID { get; set; } [JsonProperty("name")] public string? Name { get; set; } [JsonProperty("reward")] public int Reward { get; set; } [JsonProperty("progress_required")] public int ProgressRequired { get; set; } [JsonProperty("progress_per_minute")] public int ProgressPerMinute { get; set; }}public class QuestData{ public QuestData() { } public QuestData(string questName, int reward, int progressLeft, int progressPerMinute, DateTime questStartTime) { QuestName = questName; Reward = reward; ProgressLeft = progressLeft; ProgressPerMinute = progressPerMinute; QuestStartTime = questStartTime; LastProgressTime = new DateTime(); } [JsonProperty("quest-name")] public string? QuestName { get; set; } [JsonProperty("reward")] public long Reward { get; set; } [JsonProperty("progress-left")] public long ProgressLeft { get; set; } [JsonProperty("progress-per-minute")] public long ProgressPerMinute { get; set; } [JsonProperty("quest-start-time")] public DateTime QuestStartTime { get; set; } [JsonProperty("last-progress-time")] public DateTime LastProgressTime { get; set; }}
カスタムシリアル化の詳細については、カスタムシリアル化 を参照してください。
依存関係の設定
Cloud Code モジュールを接続するには、、、 を設定する必要があります。
QuestServiceGameApiClientPushClient- はクエストを管理してキャッシュします。
QuestService - Remote Config サービスと Cloud Save サービスを呼び出すために必要です。
GameApiClient - は、プッシュ通知をプレイヤーに送信するために必要です。
PushClient
ICloudCodeSetupusing Microsoft.Extensions.DependencyInjection;using Unity.Services.CloudCode.Apis;using Unity.Services.CloudCode.Core;namespace QuestSystem;public class CloudCodeSetup : ICloudCodeSetup{ public void Setup(ICloudCodeConfig config) { config.Dependencies.AddSingleton<IQuestService, QuestService>(); config.Dependencies.AddSingleton(GameApiClient.Create()); config.Dependencies.AddSingleton(PushClient.Create()); }}
クエストコントローラー
メインクラスとして動作する クラスと、サービスに対するユーザートラフィックのエントリポイントを作成します。
QuestControllerusing 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 QuestSystem;public class QuestController{ private const string QuestDataKey = "quest-data"; private const string PlayerHasQuestInProgress = "Player already has a quest in progress!"; private const string PlayerHasNoQuestInProgress = "Player does not have a quest in progress!"; private const string PlayerHasCompletedTheQuest = "Player has already completed their quest!"; private const string PlayerCannotProgress = "Player cannot make quest progress yet!"; private const string PlayerProgressed = "Player made quest progress!"; private const string PlayerHasFinishedTheQuest = "Player has finished the quest!"; private ILogger<QuestController> _logger; public QuestController(ILogger<QuestController> logger) { _logger = logger; } [CloudCodeFunction("AssignQuest")] public async Task<string> AssignQuest(IExecutionContext context, IQuestService questService, IGameApiClient gameApiClient) { var questData = await GetQuestData(context, gameApiClient); if (questData?.QuestName != null) return PlayerHasQuestInProgress; var availableQuests = questService.GetAvailableQuests(context, gameApiClient); var random = new Random(); var index = random.Next(availableQuests.Count); var quest = availableQuests[index]; questData = new QuestData(quest.Name, quest.Reward, quest.ProgressRequired, quest.ProgressPerMinute, DateTime.Now); await SetQuestData(context, gameApiClient, QuestDataKey, JsonConvert.SerializeObject(questData)); return $"Player was assigned quest: {quest.Name}!"; } [CloudCodeFunction("PerformAction")] public async Task<string> PerformAction(IExecutionContext context, IGameApiClient gameApiClient, PushClient pushClient) { var questData = await GetQuestData(context, gameApiClient); if (questData?.QuestName == null) return PlayerHasNoQuestInProgress; if (questData.ProgressLeft == 0) return PlayerHasCompletedTheQuest; if (DateTime.Now < questData.LastProgressTime.AddSeconds(60 / questData.ProgressPerMinute)) return PlayerCannotProgress; questData.LastProgressTime = DateTime.Now; questData.ProgressLeft--; await SetQuestData(context, gameApiClient, QuestDataKey, JsonConvert.SerializeObject(questData)); if (questData.ProgressLeft <= 0) { await HandleQuestCompletion(context, gameApiClient, pushClient); return PlayerHasFinishedTheQuest; } return PlayerProgressed; } public async Task HandleQuestCompletion(IExecutionContext context, IGameApiClient gameApiClient, PushClient pushClient) { await NotifyPlayer(context, pushClient); try { await gameApiClient.CloudSaveData.DeleteItemAsync(context, context.AccessToken, QuestDataKey, context.ProjectId, context.PlayerId); } catch (ApiException e) { _logger.LogError("Failed to delete a quest for player. Error: {Error}", e.Message); throw new Exception($"Failed to delete a quest for player. Error. Error: {e.Message}"); } } private async Task NotifyPlayer(IExecutionContext context, PushClient pushClient) { const string message = "Quest completed!"; const string messageType = "Announcement"; try { await pushClient.SendPlayerMessageAsync(context, message, messageType, context.PlayerId); } catch (ApiException e) { _logger.LogError("Failed to send player message. Error: {Error}", e.Message); throw new Exception($"Failed to send player message. Error: {e.Message}"); } } private async Task<QuestData?> GetQuestData(IExecutionContext context, IGameApiClient gameApiClient) { try { var result = await gameApiClient.CloudSaveData.GetItemsAsync( context, context.AccessToken, context.ProjectId, context.PlayerId, new List<string> { QuestDataKey }); if (result.Data.Results.Count == 0) return null; return JsonConvert.DeserializeObject<QuestData>(result.Data.Results.First().Value.ToString()); } catch (ApiException e) { _logger.LogError($"Failed to retrieve data from Cloud Save. Error: {e.Message}"); throw new Exception($"Failed to retrieve data from Cloud Save. Error: {e.Message}"); } } private async Task SetQuestData(IExecutionContext context, IGameApiClient gameApiClient, string key, string value) { try { await gameApiClient.CloudSaveData .SetItemAsync(context, context.ServiceToken, context.ProjectId, context.PlayerId, new SetItemBody(key, value)); } catch (ApiException e) { _logger.LogError($"Failed to save data in Cloud Save. Error: {e.Message}"); throw new Exception($"Failed to save data in Cloud Save. Error: {e.Message}"); } }}
クエストのプレイヤーへの割り当て
AssignQuest- Remote Config サービスからクエストをプレイヤーに割り当てます。
- プレイヤーの オブジェクトを作成し、それを Cloud Save に格納します。
QuestData
プレイヤーがすでにクエストを進行している場合、関数は進行に基づいてレスポンスします。
クエストの進行
PerformAction- クールダウンの期限が切れた場合、またはこれがプレイヤーの最初の進行関数呼び出しである場合、プレイヤーには進行ポイントが授与され、Cloud Save 内でそのプレイヤーの 値が減ります。
progress-left - この関数がクールダウン中に呼び出された場合、Cloud Code のレスポンスは です。
Player cannot make quest progress yet! - 呼び出しが成功した場合、Cloud Code のレスポンスは です。Cloud Save で、Cloud Code はプレイヤーの
Player made quest progress!値を減らし、progress-leftを更新します。last-progress-time - 最後の成功した呼び出しで がゼロになった場合、Cloud Code は、クエストが完了したことをプレイヤーに通知します。クエストデータは Cloud Save から削除されます。
progress-left
モジュールの検証
Cloud Save データへの変更を検証するには、Unity Dashboard でプレイヤーの Cloud Save データを調べます。
- Unity Dashboard に移動します。
- Products (製品) > Player Management (プレイヤー管理) を選択します。
- Player Management (プレイヤー管理) を選択します。
- 検索フィールドにプレイヤー ID を入力し、Find Player (プレイヤーを検索) を選択します。
- Cloud Save > Data (データ) セクションに移動します。
- キーでクエストの進行を追跡します。
quest-data
プッシュ通知が作動することを確認する方法については、プッシュメッセージ を参照してください。