퀘스트 시스템
Implement a quest system that issues players with quests using Remote Config and Cloud Save.
읽는 시간 4분최근 업데이트: 12시간 전
퀘스트 시스템 샘플에서는 Cloud Code를 사용하여 Remote Config와 Cloud Save 서비스에서 퀘스트 샘플을 구현하는 방법을 보여 줍니다. 샘플은 서버 권한이 있는 부정 행위 방지, 다른 UGS 서비스 응답 캐시, 플레이어의 Cloud Save 데이터 수정에 관한 내용을 다룹니다. 샘플에서는 푸시 메시지를 사용하여 플레이어가 퀘스트를 완료할 때 플레이어에게 알림을 전달합니다.
개요
퀘스트 시스템을 사용하면 플레이어가 Cloud Code 모듈을 호출하여 퀘스트를 요청할 수 있습니다. 플레이어에게 퀘스트를 제공하기 위해 Cloud Code는 다음 내용을 수행합니다.- Remote Config에 퀘스트를 저장합니다.
- Remote Config에서 활성 퀘스트 목록을 검색하여 임의로 하나를 선택합니다.
- 플레이어에게 퀘스트를 할당하고 Cloud Save에 데이터를 저장합니다.
- 진행도는 분당 특정 속도로 제한됩니다.
- 플레이어는 1분마다 퀘스트 구성에서 허용되는 만큼의 액션만 수행할 수 있습니다.
퀘스트 설정
Remote Config 서비스에서 퀘스트를 정의해야 합니다.- Unity Cloud 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입니다.
id - : 퀘스트 이름입니다.
name - : 퀘스트를 완수하는 데 필요한 진행도 포인트 양입니다.
progress_required - : 플레이어가 분당 얻을 수 있는 진행도 포인트 양입니다.
progress_per_minute - : 퀘스트 완수에 따라 플레이어가 받을 수 있는 보상입니다.
reward
Cloud Code 설정
QuestSystemCloud Save에 대한 액세스 제한
Cloud Save 데이터에 대한 보안을 강화하려면 액세스 제어를 사용하여 리소스 정책을 만들면 됩니다. 사용자가 자체 Cloud Save 데이터를 수정하여 속임수를 사용하지 못하도록 방지하려면 UGS에 대해 리소스 정책을 만들어야 합니다.cloud-save-resource-policy.jsonUGS CLI를 사용하여 배포할 수 있습니다.{ "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 access upsert-project-policy cloud-save-resource-policy.json
퀘스트 서비스 설정
QuestService.csusing 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.cs커스텀 직렬화에 관해 자세히 알아보려면 커스텀 직렬화를 참고하십시오.using 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 - 마지막에 성공한 호출로 가 0이 되면 Cloud Code는 플레이어가 퀘스트를 완수했음을 플레이어에게 알립니다. 퀘스트 데이터는 Cloud Save에서 삭제됩니다.
progress-left
모듈 확인
Cloud Save 데이터에 대한 변경 사항을 확인하려면 Unity Cloud Dashboard에서 플레이어의 Cloud Save 데이터를 검사하면 됩니다.- Unity Cloud Dashboard로 이동합니다.
- Products > Player Management를 선택합니다.
- Player Management를 선택합니다.
- 검색 필드에 플레이어 ID를 입력하고 Find Player를 선택합니다.
- Cloud Save > Data 섹션으로 이동합니다.
- 키에서 퀘스트 진행도를 트래킹합니다.
quest-data