기술 자료

지원

Cloud Code

Cloud Code

퀘스트 시스템

Implement a quest system that issues players with quests using Remote Config and Cloud Save.
읽는 시간 4분최근 업데이트: 12시간 전

퀘스트 시스템 샘플에서는 Cloud Code를 사용하여 Remote ConfigCloud Save 서비스에서 퀘스트 샘플을 구현하는 방법을 보여 줍니다. 샘플은 서버 권한이 있는 부정 행위 방지, 다른 UGS 서비스 응답 캐시, 플레이어의 Cloud Save 데이터 수정에 관한 내용을 다룹니다. 샘플에서는 푸시 메시지를 사용하여 플레이어가 퀘스트를 완료할 때 플레이어에게 알림을 전달합니다.

개요

퀘스트 시스템을 사용하면 플레이어가 Cloud Code 모듈을 호출하여 퀘스트를 요청할 수 있습니다. 플레이어에게 퀘스트를 제공하기 위해 Cloud Code는 다음 내용을 수행합니다.
  • Remote Config에 퀘스트를 저장합니다.
  • Remote Config에서 활성 퀘스트 목록을 검색하여 임의로 하나를 선택합니다.
  • 플레이어에게 퀘스트를 할당하고 Cloud Save에 데이터를 저장합니다.
플레이어는 퀘스트 진행도에 반영되는 액션을 수행할 수 있습니다. Cloud Code는 플레이어가 마지막으로 진행을 수행한 시점을 확인하여 액션이 진행도에 반영되는 것을 허용하거나 거부하며, 이는 다음 제한 사항에 따라 달라집니다.
  • 진행도는 분당 특정 속도로 제한됩니다.
  • 플레이어는 1분마다 퀘스트 구성에서 허용되는 만큼의 액션만 수행할 수 있습니다.
이는 플레이어가 퀘스트 진행 점수를 받을 수 있는 속도를 제한하여 부정 행위를 방지하는 간단한 방법입니다. 플레이어가 퀘스트를 완수하면 Cloud Save는 퀘스트 데이터를 삭제하고 푸시 메시지를 통해 플레이어에게 알림을 전달합니다.

퀘스트 설정

Remote Config 서비스에서 퀘스트를 정의해야 합니다.
  1. Unity Cloud Dashboard로 이동합니다.
  2. Products > Remote Config를 선택합니다.
  3. Config 탭을 선택합니다.
  4. Add a key를 선택합니다.
  5. QUESTS
    를 키 이름으로 입력합니다.
  6. JSON을 유형으로 선택합니다.
  7. 값으로 다음을 붙여 넣습니다.
    [ { "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 }]
  8. 구성을 퍼블리시합니다.
구성은 활성 퀘스트 데이터베이스 역할을 합니다. 퀘스트 구조는 다음과 같습니다.
  • id
    : 퀘스트에 대한 고유 ID입니다.
  • name
    : 퀘스트 이름입니다.
  • progress_required
    : 퀘스트를 완수하는 데 필요한 진행도 포인트 양입니다.
  • progress_per_minute
    : 플레이어가 분당 얻을 수 있는 진행도 포인트 양입니다.
  • reward
    : 퀘스트 완수에 따라 플레이어가 받을 수 있는 보상입니다.
예를 들어 1분의 대기 시간이 끝날 때마다 플레이어가 액션을 수행한다고 가정할 때 5분 후에 Chop Trees 퀘스트를 완수할 수 있습니다.

Cloud Code 설정

QuestSystem
이라는 Cloud Code 모듈을 설정하여 퀘스트 시스템을 처리합니다.

Cloud 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.cs
라는 파일을 만들어 데이터 클래스를 저장합니다. 데이터 클래스를 사용하여 Cloud Code가 Remote Config와 Cloud Save에서 수신하는 응답을 역직렬화할 수 있습니다.
DataClasses.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 모듈을 연결하려면
QuestService
,
GameApiClient
,
PushClient
를 설정해야 합니다.
  • QuestService
    는 퀘스트를 관리하고 캐시합니다.
  • GameApiClient
    는 Remote Config와 Cloud Save 서비스를 호출하는 데 필요합니다.
  • PushClient
    는 플레이어에게 푸시 알림을 전송하는 데 필요합니다.
ICloudCodeSetup
인터페이스를 사용하면 모듈의 종속성을 관리할 수 있습니다. 자세한 내용은 종속성 삽입 기술 자료에서 알아보십시오.
using 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()); }}

퀘스트 컨트롤러

QuestController
클래스를 생성하여 메인 클래스로 삼고, 서비스에 대한 사용자 트래픽의 진입점으로 사용합니다.
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 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 서비스에서 플레이어에게 퀘스트를 할당합니다.
  • 플레이어에 대해
    QuestData
    오브젝트를 생성하고 Cloud Save에 저장합니다.
플레이어에게 이미 진행 중인 퀘스트가 있으면 함수는 그에 따라 응답을 제공합니다.

퀘스트 진행

PerformAction
함수를 호출하여 퀘스트를 진행할 수 있습니다. 진행도를 업데이트하려면 플레이어가 퀘스트 진행도에 포함되는 액션을 수행할 때마다 이 함수를 호출합니다.
  • 대기 시간이 만료되었거나 이 액션이 플레이어의 첫 번째 진행도 함수 호출인 경우 플레이어에게 진행 포인트가 부여되고 Cloud Save의
    progress-left
    가 줄어듭니다.
  • 대기 시간 중에 함수가 호출되는 경우 Cloud Code는
    Player cannot make quest progress yet!
    이라는 응답을 제공합니다.
  • 호출이 성공하면 Cloud Code는
    Player made quest progress!
    라는 응답을 제공합니다. Cloud Save에서 Cloud Code는 플레이어의
    progress-left
    값을 차감하고
    last-progress-time
    을 업데이트합니다.
  • 마지막에 성공한 호출로
    progress-left
    가 0이 되면 Cloud Code는 플레이어가 퀘스트를 완수했음을 플레이어에게 알립니다. 퀘스트 데이터는 Cloud Save에서 삭제됩니다.

모듈 확인

Cloud Save 데이터에 대한 변경 사항을 확인하려면 Unity Cloud Dashboard에서 플레이어의 Cloud Save 데이터를 검사하면 됩니다.
  1. Unity Cloud Dashboard로 이동합니다.
  2. Products > Player Management를 선택합니다.
  3. Player Management를 선택합니다.
  4. 검색 필드에 플레이어 ID를 입력하고 Find Player를 선택합니다.
  5. Cloud Save > Data 섹션으로 이동합니다.
  6. quest-data
    키에서 퀘스트 진행도를 트래킹합니다.
푸시 알림이 작동하는지 확인하려면 푸시 메시지를 참고하십시오.