클라우드 서비스

개발하는 운동인·2024년 12월 20일

목표 : 만들어져있는 서버를 통해 클라우드 서비스를 구현한다.

환경설정

    1. 내가 방금 만든 프로젝트 선택하고
    1. Player Authentication을 찾아서 클릭
    1. 그 다음, PC 플랫폼 선택하면 끝. 아래 사진은 최종 화면
    1. 추가로, 아래 사진처럼 단축키를 추가한다. 아래는 필수적으로 있어야할 서비스 목록이다.

게스트 로그인

    1. 패키지 다운로드. Sample까지 다운로드 한다.
    1. 코드 작성
using UnityEngine;
using UnityEngine.UI;
using Unity.Services.Core;
using Unity.Services.Authentication;
public class UGSManager : MonoBehaviour
{
    [SerializeField]
    Text userIDInfo;

    async void Start()
    {
        await UnityServices.InitializeAsync();

        await AuthenticationService.Instance.SignInAnonymouslyAsync(); //계정을 따로 안만들고 로그인. 기기에 종속

        if(AuthenticationService.Instance.IsSignedIn)  //로그인 됐을 경우
        {
            userIDInfo.text = "Player ID : " + AuthenticationService.Instance.PlayerId; //PlayerId는 유니티가 게스트 로그인하면 알려줌
        }
        else  //로그인 안됏을 경우
        {
            Debug.Log("로그인 실패 ㅠ");
        }
    }

 
}
    1. 텍스트 할당
  • 실행 시, 정상적으로 ID를 받아올 수 있다.

경제 Economy 환경 설정

  • 1번 설정:

  • 2번 설정 : 재화를 설정

  • 돈의 이름과 돈의 가치 그리고, 돈을 무한(Unlimnit)대로 설정한다.

  • 3번 설정

  • 4번 설정 : 인 게임 재화로 무언가를 하는것

  • 1가차 티켓 = 100쌀

  • 5번 설정은 skip한다.

  • 6번 설정: Publish한다.

  • 7번 설정: skip

  • 최종 결과

경제 Economy 구현

    1. Economy 패키지를 다운로드 한다. 마찬가지로 Samples에 UISample도 다운로드한다.
    1. 재화 이미지와 텍스트 ui를 준비한다.
    1. 현재까지 계층구조와 UGSManger에 재화 텍스트를 할당한다.
    1. 코드 작성한다.
using UnityEngine;
using UnityEngine.UI;
using Unity.Services.Core;
using Unity.Services.Economy;
using Unity.Services.Authentication;
using Unity.Services.Economy.Model;
using System.Linq;
public class UGSManager : MonoBehaviour
{
    [SerializeField]
    Text userIDInfo;

    [SerializeField]
    Text ssalInfo;
    async void Start() //비동기 함수여야만 await(비동기) 사용 가능
    {
        await UnityServices.InitializeAsync(); //전체 초기화

        await AuthenticationService.Instance.SignInAnonymouslyAsync(); //계정을 따로 안만들고 로그인. 기기에 종속

        if(AuthenticationService.Instance.IsSignedIn)  //로그인 됐을 경우
        {
            userIDInfo.text = "Player ID : " + AuthenticationService.Instance.PlayerId; //PlayerId는 유니티가 게스트 로그인하면 알려줌

            GetCurrency();
        }
        else  //로그인 안됏을 경우
        {
            Debug.Log("로그인 실패 ㅠ");
        }
    }

 
    //Economy 관련
    async void GetCurrency()
    {
      GetBalancesResult result =  await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

        foreach(var balance in result.Balances)
        {
            Debug.Log(balance.CurrencyId + ":" + balance.Balance);
        }

        ssalInfo.text = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance.ToString();
    }
}
  • 실행 시, 정상적으로 나온다.

경제 Economy 구현 - 버튼으로 재화 증가시키기

    1. 이전 코드에 코드를 더 추가한다.
using UnityEngine;
using UnityEngine.UI;
using Unity.Services.Core;
using Unity.Services.Economy;
using Unity.Services.Authentication;
using Unity.Services.Economy.Model;
using System.Linq;
public class UGSManager : MonoBehaviour
{
    [SerializeField]
    Text userIDInfo;

    [SerializeField]
    Text ssalInfo;
    async void Start() //비동기 함수여야만 await(비동기) 사용 가능
    {
        await UnityServices.InitializeAsync(); //전체 초기화

        await AuthenticationService.Instance.SignInAnonymouslyAsync(); //계정을 따로 안만들고 로그인. 기기에 종속

        if(AuthenticationService.Instance.IsSignedIn)  //로그인 됐을 경우
        {
            userIDInfo.text = "Player ID : " + AuthenticationService.Instance.PlayerId; //PlayerId는 유니티가 게스트 로그인하면 알려줌

            GetCurrency();
        }
        else  //로그인 안됏을 경우
        {
            Debug.Log("로그인 실패 ㅠ");
        }
    }

 
    //Economy 관련
    async void GetCurrency()
    {
      GetBalancesResult result =  await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

        foreach(var balance in result.Balances)
        {
            Debug.Log(balance.CurrencyId + ":" + balance.Balance);
        }

        ssalInfo.text = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance.ToString();
    }

    public void GetSsalPressed()
    {
        AddSsal(100);
    }
    async void AddSsal(int amount)
    {
        GetBalancesResult result = await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

        long currentSsal = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance;

        await EconomyService.Instance.PlayerBalances.SetBalanceAsync("SSAL", currentSsal + amount);

        GetCurrency();
    }
}
    1. 버튼 생성 후 이벤트 추가한다.
  • 실행 시, 버튼을 누를 때 마다 100씩 증가한다.

경제 Economy 가차티켓 구매하기.

    1. 코드를 작성한다. 단 1줄이면 된다.
using UnityEngine;
using UnityEngine.UI;
using Unity.Services.Core;
using Unity.Services.Economy;
using Unity.Services.Authentication;
using Unity.Services.Economy.Model;
using System.Linq;
public class UGSManager : MonoBehaviour
{
       
	  //..생략

    [SerializeField]
    Text gachatTicketInfo;
    
	  //..생략

 
    //Economy 관련
    async void GetCurrency()
    {
      GetBalancesResult result =  await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

        foreach(var balance in result.Balances)
        {
            Debug.Log(balance.CurrencyId + ":" + balance.Balance);
        }

        ssalInfo.text = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance.ToString();
    }

	//..생략


    public async void PurchaseGachaTicket() //버튼 이벤트로 추가.
    {
        MakeVirtualPurchaseResult result =   await EconomyService.Instance.Purchases.MakeVirtualPurchaseAsync("SSALTOTHETICKET");

        GetCurrency();
    }
}
    1. 버튼 생성 후 PurchaseGachaTicket()을 연결한다.
  • 실행 시, 버튼을 누르면 Crystal을 가져간다.

산 아이템이 무엇인지 가져오기

    1. 코드를 작성한다.
using UnityEngine;
using UnityEngine.UI;
using Unity.Services.Core;
using Unity.Services.Economy;
using Unity.Services.Authentication;
using Unity.Services.Economy.Model;
using System.Linq;
using NUnit.Framework;
using System.Collections.Generic;
public class UGSManager : MonoBehaviour
{
    [SerializeField]
    Text userIDInfo;

    [SerializeField]
    Text ssalInfo;

    [SerializeField]
    Text gachatTicketInfo;

//1번@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    List<PlayersInventoryItem> items = new List<PlayersInventoryItem>(); //PlayersInventoryItem을 리스트로 관리한다.

	//..생략

 
    //Economy 관련
    async void GetCurrency()
    {
       GetBalancesResult result =  await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

     foreach(var balance in result.Balances)
     {
         Debug.Log(balance.CurrencyId + ":" + balance.Balance);
     }

     ssalInfo.text = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance.ToString();



     //아이템을 보여주는 코드
     
     //2번@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
     items.Clear(); //클리어 하지 않으면 계속 쌓인다. 
     var invenResult = await EconomyService.Instance.PlayerInventory.GetInventoryAsync();
     items.AddRange(invenResult.PlayersInventoryItems);

     int counter = 0;

     foreach(var item in items)
     {
         if(item.InventoryItemId == "GACHATICKET")//인벤토리 아이디가 GachaTicket 라면 ,
                                                  //GachaTicket -> 유니티 클라우드에 인벤토리 아이디가 GachaTicket여야함
         {
             counter++;
         }
     }
     gachatTicketInfo.text = counter.ToString();
    }

	//..생략
}
  • item.InventoryItemId == "GACHATICKET" 을 알려면 아래와 같이 설정되어야 한다.
  • item의 타입은 PlayerInventoryItem 이며, 더 자세하게 보면 InventoryItemId가 있다.

20개가 넘어가면 못 가져오는 현상 고치기

using UnityEngine;
using UnityEngine.UI;
using Unity.Services.Core;
using Unity.Services.Economy;
using Unity.Services.Authentication;
using Unity.Services.Economy.Model;
using System.Linq;
using NUnit.Framework;
using System.Collections.Generic;
using System.Threading.Tasks;
public class UGSManager : MonoBehaviour
{
    [SerializeField]
    Text userIDInfo;

    [SerializeField]
    Text ssalInfo;

    [SerializeField]
    Text gachatTicketInfo;

    List<PlayersInventoryItem> items = new List<PlayersInventoryItem>();

    async void Start() //비동기 함수여야만 await(비동기) 사용 가능
    {
     	//..생략
    }

 
    //Economy 관련
    async void GetCurrency()
    {
      	//..생략
        
        //아이템을 보여주는 코드

        await FetchAllInventoryItems(); //1. FetchAllInventoryItems() 호출

        int counter = 0;

        foreach(var item in items)
        {
            if(item.InventoryItemId == "GACHATICKET")//인벤토리 아이디가 GachaTicket 라면 ,
                                                     //GachaTicket -> 유니티 클라우드에 인벤토리 아이디가 GachaTicket여야함
            {
                Debug.Log("ㅇㅁ");
                counter++;
            }
        }
        gachatTicketInfo.text = counter.ToString();
    }


   async Task FetchAllInventoryItems()
    {
        items.Clear();

        GetInventoryOptions options = new GetInventoryOptions
        {
            ItemsPerFetch = 20 //기본은 20개로 가져오도록 하고
        };


        try
        {
            var invenResult = await EconomyService.Instance.PlayerInventory.GetInventoryAsync(options); //GetInventoryAsync () : 유니티가 우리한테 20개씩 줌
            items.AddRange(invenResult.PlayersInventoryItems); 

            //20개를 가져오다가

            while(invenResult.HasNext) //아이템이 더 있다면
            {
                invenResult = await invenResult.GetNextAsync(); //더 가져오기
                items.AddRange(invenResult.PlayersInventoryItems);
            }
        }
        catch(RequestFailedException e)
        {
            Debug.Log(e);
        }
     
    }

    public void GetSsalPressed()
    {
        AddSsal(100);
    }
    async void AddSsal(int amount)
    {
     	//..생략
    }


    public async void PurchaseGachaTicket() //구매하는 코드
    {
      	//..생략
    }
}
  • 이전 화면
  • 상자 버튼 클릭 시 , 크리스탈이 10이 남고 상자가 77로 될 것이다.

만약 크리스탈이 부족한데 상자를 얻었을 때 오류가 난다. 그럴 때는 조건문을 따로 만들어서 버튼을 클릭하지 못하게 따로 설정하면 된다. 즉, ui 버튼을 막으면된다.

⭐ 중간 개념 정리 - 코드 부분

⭐ 핵심 키워드

UnityServices.InitializeAsync(): Unity 게임 서비스를 초기화합니다.

AuthenticationService.Instance.SignInAnonymouslyAsync(): 게스트 계정으로 로그인합니다.

EconomyService.Instance.PlayerBalances.GetBalancesAsync(): 플레이어의 재화 잔액을 조회합니다.

EconomyService.Instance.PlayerBalances.SetBalanceAsync(): 플레이어의 특정 재화의 잔액을 설정합니다.

EconomyService.Instance.PlayerInventory.GetInventoryAsync(): 플레이어의 인벤토리 아이템을 조회합니다.

EconomyService.Instance.Purchases.MakeVirtualPurchaseAsync(): 가상 아이템을 구매합니다.

⭐ 실행 흐름 - 1. 초기화 및 로그인

 [SerializeField]
 Text userIDInfo;
    
   async void Start() //비동기 함수여야만 await(비동기) 사용 가능
   {
       await UnityServices.InitializeAsync(); //전체 초기화

       await AuthenticationService.Instance.SignInAnonymouslyAsync(); //게스트 계정으로 로그인. 기기에 종속

       if(AuthenticationService.Instance.IsSignedIn)  //로그인 됐을 경우
       {
           userIDInfo.text = "Player ID : " + AuthenticationService.Instance.PlayerId; //PlayerId는 유니티가 게스트 로그인하면 알려줌

           GetCurrency();
       }
       else  //로그인 안됏을 경우
       {
           Debug.Log("로그인 실패 ㅠ");
       }
   }
    1. Unity Services를 초기화합니다.
    1. 게스트 계정으로 로그인합니다.
    1. 로그인에 성공하면, 사용자 ID와 재화 정보를 조회합니다.

⭐ 실행 흐름 - 2. 재화 조회

    1. Start문에서 GetCurrency() 호출 하도록 함.
    async void GetCurrency()
    {
      GetBalancesResult result =  await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

        foreach(var balance in result.Balances)
        {
            Debug.Log(balance.CurrencyId + ":" + balance.Balance);
        }

        ssalInfo.text = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance.ToString();

        //아이템을 보여주는 코드

        await FetchAllInventoryItems();

        int counter = 0;

        foreach(var item in items)
        {
            if(item.InventoryItemId == "GACHATICKET")//인벤토리 아이디가 GachaTicket 라면 ,
                                                     //GachaTicket -> 유니티 클라우드에 인벤토리 아이디가 GachaTicket여야함
            {
                Debug.Log("ㅇㅁ");
                counter++;
            }
        }
        gachatTicketInfo.text = counter.ToString();
    }
    1. EconomyService를 통해 플레이어의 모든 재화 잔액을 가져옵니다.
    1. 재화 정보 중 SSAL의 잔액을 ssalInfo에 출력합니다.
    1. 인벤토리 아이템 목록을 조회합니다.
    1. 인벤토리에서 'GACHATICKET'의 개수를 카운트하여 gachatTicketInfo에 출력합니다.

⭐ 실행 흐름 - 3. 인벤토리 아이템 조회

    1. GetCurrency 메서드에서 호출
    async Task FetchAllInventoryItems()
    {
        items.Clear();

        GetInventoryOptions options = new GetInventoryOptions
        {
            ItemsPerFetch = 20
        };


        try
        {
            var invenResult = await EconomyService.Instance.PlayerInventory.GetInventoryAsync(options); //GetInventoryAsync () : 유니티가 우리한테 20개씩 줌
            items.AddRange(invenResult.PlayersInventoryItems);

            //20개를 가져오다가

            while (invenResult.HasNext) //아이템이 더 있다면
            {
                invenResult = await invenResult.GetNextAsync(); //더 가져오기
                items.AddRange(invenResult.PlayersInventoryItems);
            }
        }
        catch (RequestFailedException e)
        {
            Debug.Log(e);
        }


    }
    1. 인벤토리를 20개 단위로 조회하며, 더 조회할 아이템이 있다면 계속해서 추가로 가져옵니다.
    1. 가져온 아이템 목록을 items 리스트에 추가합니다.

⭐ 실행 흐름 - 4. 재화 추가 (버튼 클릭 이벤트 사용)

    1. 버튼 이벤트로 호출
    public void GetSsalPressed() // 재화 얻는 버튼이벤트 
    {
        AddSsal(100);
    }
    async void AddSsal(int amount)
    {
        GetBalancesResult result = await EconomyService.Instance.PlayerBalances.GetBalancesAsync();

        long currentSsal = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance;

        await EconomyService.Instance.PlayerBalances.SetBalanceAsync("SSAL", currentSsal + amount);

        GetCurrency();
    }
    1. AddSsal() 메서드가 호출되어 SSAL 재화를 추가합니다.
    1. 현재 재화 잔액을 불러오고, amount만큼 추가하여 업데이트합니다.
    1. 다시 재화 정보를 불러와 UI에 갱신합니다. => GetCurrency() 호출한다는 것

⭐ 실행 흐름 - 5. 아이템 구매 (버튼 클릭 이벤트 사용)

    1. 버튼 이벤트로 호출
  public async void PurchaseGachaTicket() // 재화 팔고 돈을 얻는 버튼이벤트 
  {
      MakeVirtualPurchaseResult result =   await EconomyService.Instance.Purchases.MakeVirtualPurchaseAsync("SSALTOTHETICKET");

      GetCurrency();
  }
    1. EconomyService를 통해 가상 구매 요청을 보냅니다.
    1. 구매가 완료되면 다시 재화 정보를 불러옵니다. => GetCurrency() 호출한다는 것

📄 위 코드 개념 정리

1️⃣ 재화 (Currency)

  • 재화의 잔액을 조회하거나 추가하는 작업이 GetCurrency, AddSsal 메서드에서 이루어집니다.

  • SetBalanceAsync 메서드는 특정 재화의 잔액을 직접 설정합니다.

2️⃣ 인벤토리 (Inventory)

  • 플레이어의 아이템 목록을 FetchAllInventoryItems 메서드에서 가져옵니다.

  • 20개씩 아이템을 조회하며, 더 조회할 아이템이 있다면 가져옵니다.

3️⃣ 가상 구매 (Virtual Purchase)

  • PurchaseGachaTicket 메서드는 Unity Cloud에 사전 정의된 상품(SSALTOTHETICKET)을 구매합니다.

🤔 중간 Q & A

비동기(Async)를 사용하는 이유.

  • 비동기 작업 : 작업이 끝날 때 까지 기다리지 않고, 다른 작업을 동시에 수행할 수 있는 프로그래밍 방식이다. Unity에서는 async와 await 키워드를 사용하여 비동기 작업을 쉽게 구현할 수 있다.

1. 메인 스레드 블로킹 방지

  • 동기 방식으로 처리하면, 통신이 끝날 때까지 메인 스레드가 멈추게 되어 게임의 프레임이 멈추는 현상이 발생합니다. 이러한 이유로 비동기를 사용하면 , 요청이 완료될 때까지 기다리는 동안에도 게임의 프레임이 정상적으로 유지되며, 애니메이션, UI 및 입력 처리가 계속 진행됩니다.

2. 동시 작업 수행

  • 비동기를 사용하면, 동시에 여러 작업을 실행할 수 있습니다. 예를 들어, 로그인과 동시에 인벤토리 조회를 시작할 수 있습니다. 이를 통해 전체 로딩 시간을 단축할 수 있습니다.

3. 간결한 코드 작성

  • 문제점: 콜백(callback) 방식을 사용하면 코드가 중첩되면서 콜백 지옥(Callback Hell) 이 발생할 수 있습니다. 이로 인해 코드의 가독성이 떨어지고 유지보수가 어려워집니다.
  • 해결 방법: 비동기를 사용하면 await 키워드로 자연스럽게 코드의 흐름을 유지할 수 있습니다. 기존의 콜백 지옥 구조를 피할 수 있어, 코드가 더 직관적으로 보입니다.

AddSsal 메서드에서 long 타입을 사용한 이유

  long currentSsal = result.Balances.Single(balance => balance.CurrencyId == "SSAL").Balance;

1. 큰 숫자 범위 지원

  • 게임의 재화는 시간이 지남에 따라 크게 증가할 수 있습니다. 예를 들어, 유저가 게임을 오래 플레이하면, 수백만, 수십억 단위의 재화를 가지게 될 수 있습니다.
  • 하지만 만약 int 타입을 사용하게 된다면 int(32비트) 타입의 최대값은 2,147,483,647로, 이를 초과하면 오버플로우(overflow)가 발생합니다.

따라서, 더 큰 숫자를 안전하게 다루기 위해 long을 사용

2. 타입 일치를 위해서 사용

  • PlayerBalance 클래스의 Balance 필드의 타입이 long으로 선언되어 있기 때문에 타입을 맞춰준다.

클라우드 세이브

  • 서버에다가 세이브 파일을 저장하는 것. 플레이어의 유저 데이터나 게임 파일을 저장할 때 사용

클라우드 세이브 환경 설정

    1. 패키지 다운로드 및 Samples - UISamples 다운로드

  • 아직 아무런 데이터가 없다.

레벨과 경험치를 서버에 저장하기

    1. 텍스트 ui 추가

    1. 기존 코드에 코드를 추가한다.
  • 데이터를 가져올 때, 즉, 세이브 할 때 딕셔너리 방식으로 가져온다. => data의 타입이 딕셔너리

  • 만약 데이터가 없다면 , 딕셔너리.Count == 0 이라면 데이터를 추가해줘야 할 것이다.

    1. LevelData 스크립트 작성 후 코드를 작성한다.
using System;
using UnityEngine;

[Serializable] //저장 가능하게 만듦
public class LevelData 
{
    public int playerLevel;
    public int exp;
}
  • 놀랍겠지만 이게 끝이다. 저장하고 싶은 것이 있다면 더 추가해도 된다.

    1. 코드 작성한다.
    [SerializeField]
    Text playerDataInfo;
    LevelData levelData;

    async void LoadFromCloud()
    {
        try
        {
            var data = await CloudSaveService.Instance.Data.Player.LoadAllAsync(); //플레이어와 관계된 데이터를 다 가져옴
            //세이브할 때는 딕셔너리 방식으로 저장을 한다. 그러므로 데이터가 없을 때 Count가 0이 된다.

            Debug.Log(data); //아무것도 데이터가 없기 때문에 뜨질 않음.

            if(data.Count == 0) //데이터가 없을 때 데이터를 추가하도록 하자. 
            {
                SaveToCloud();
            }
        }
        catch(Exception e)
        {

        }
    }

    async void SaveToCloud()
    {
        if(levelData == null) //레벨 데이타가 없다면
        {
            levelData = new LevelData
            {
                playerLevel = 1,
                exp = 0
            }; //위 코드처럼 초기화한다. 
        }

        var data = new Dictionary<string, object> //데이터가 없으면 만들어서라도 세이브한다.
        {
            {"Player",levelData }
        };

        try
        {
            await CloudSaveService.Instance.Data.Player.SaveAsync(data);
        }
        catch (CloudSaveException e)
        {
            Debug.LogError(e.ToString());
        }
       
    }
  • 실행 시 , PlayerData에 Data가 들어갔다.
  • ID 클릭 시, exp : 0 , playerLevel = 1; 로 저장되었다.

지금까지 코드 간략한 설명

1. LoadFromCloud() - 클라우드에서 데이터를 불러오고, 데이터가 없다면 추가하는 방식이다.

  • 클라우드에 저장된 모든 플레이어 관련 데이터를 불러온다. 데이터가 없는 경우 SaveToCloud() 메서드를 호출하여 기본 플레이어가 가져야할 데이터를 저장하도록 한다.
  • 추가로, 데이터를 불러오다가 오류가 발생하면 예외 처리 한다. 예를 들어 네트워크 연결 문제나 서버 오류 등이 발생할 수 있다.

2. SaveToCloud() - 클라우드에 데이터 저장하기

  • 레벨 데이터가 null 일 때 (즉, 초기화 되지 않을 때) 기본 값을 설정한다. 이 코드는 클라우드에 데이터가 전혀 없는 경우 새 데이터를 생성하는 기능임.
 if(levelData == null) //레벨 데이타가 없다면
 {
     levelData = new LevelData
     {
         playerLevel = 1,
         exp = 0
     }; //위 코드처럼 초기화한다. 
 }

  • 새 데이터를 생성 할 때 딕셔너리 방식으로 저장해야 한다.
  • key: "Player"에 value: levelData(플레이어의 레벨 정보)를 저장합니다.
  • 클라우드 저장소에서는 {"Player": { "playerLevel": 1, "exp": 0 }} 형태로 보관됩니다.
  • ⭐ 클라우드 저장소에서는 모든 종류의 데이터(숫자, 문자열, 객체 등)를 동일한 구조로 관리해야 한다.
  • object 타입은 모든 C# 타입의 최상위 클래스이므로, 어떤 타입의 데이터도 딕셔너리에 저장할 수 있다.
  var data = new Dictionary<string, object> //데이터가 없으면 만들어서라도 세이브한다.
  {
      {"Player",levelData }
  };
  • 클라우드에 데이터를 비동기적으로 저장한다. 또한, 이 작업이 끝날 때까지 기다리게 된다.
  • 예를 들어, 네트워크가 느릴 경우 일시 중단된 후 다시 작업이 이어집니다.
  try
  {
      await CloudSaveService.Instance.Data.Player.SaveAsync(data);
  }
  • 저장 중 오류가 발생하면 예외 처리 한다. 네트워크 연결 문제나 클라우드 서비스에 문제가 발생할 경우 예외 메시지가 출력한다.
   catch (CloudSaveException e)
   {
       Debug.LogError(e.ToString());
   }

    1. 로드하는 코드를 작성한다.
 [SerializeField]
 Text playerDataInfo;
 LevelData levelData;

 async void LoadFromCloud()
 {
     try
     {
         var data = await CloudSaveService.Instance.Data.Player.LoadAllAsync(); //플레이어와 관계된 데이터를 다 가져옴
         //세이브할 때는 딕셔너리 방식으로 저장을 한다. 그러므로 데이터가 없을 때 Count가 0이 된다.

         Debug.Log(data); //아무것도 데이터가 없기 때문에 뜨질 않음.

         if(data.Count == 0) //데이터가 없을 때 데이터를 추가하도록 하자. 
         {
             SaveToCloud();
         }
         else   //⭐추가  데이터가 있다면.
         {
             data.TryGetValue("Player", out var playerDataJson);

             levelData = playerDataJson.Value.GetAs<LevelData>();

             UpdateLevelData();
         }
     }
     catch(Exception e)
     {
         
     }
 }


 async void SaveToCloud()
 {
     if(levelData == null) //레벨 데이타가 없다면
     {
         levelData = new LevelData
         {
             playerLevel = 1,
             exp = 0
         }; //위 코드처럼 초기화한다. 

         UpdateLevelData();   //⭐추가  
     }

     var data = new Dictionary<string, object> //데이터가 없으면 만들어서라도 세이브한다.
     {
         {"Player",levelData }
     };

     try
     {
         await CloudSaveService.Instance.Data.Player.SaveAsync(data);
     }
     catch (CloudSaveException e)
     {
         Debug.LogError(e.ToString());
     }
    
 }

 void UpdateLevelData()   //⭐추가 
 {
     playerDataInfo.text = "Player Level : " + levelData.playerLevel + "\nExp : " + levelData.exp;
 }
  • 실행 시, 정상적으로 텍스트가 업데이트 되었다.

위 코드 간략한 설명

위 코드에 핵심적인 부분은 만약 데이터가 있다면? 이라는 조건을 작성한 것이다.

  • 먼저, 클라우드에서 가져온 data 딕셔너리에서 Player라는 키에 해당하는 데이터(값)을 가져온다.
data.TryGetValue("Player", out var playerDataJson); //결과: playerDataJson에 JSON 형식의 데이터가 저장됩니다.
  • TryGetValue메서드는 키가 존재하면 playerDataJson에 해당 데이터의 값을 저장하고 true를 반환하고, 키가 존재하지 않으면 playerDataJson은 null이 되고, false를 반환합니다.

  • 불러온 JSON 데이터를 LevelData 타입의 객체로 변환한다.
levelData = playerDataJson.Value.GetAs<LevelData>();


  • 마지막으로 불러온 levelData 객체를 업데이트 한다. (목적은 클라우드에서 불러온 데이터를 게임 내의 UI,변수,게임 상태에 반영하는 작업한다.
    void UpdateLevelData()
    {
        playerDataInfo.text = "Player Level : " + levelData.playerLevel + "\nExp : " + levelData.exp;
    }

짧은 Q & A

✅ data.TryGetValue("Player", out var playerDataJson); 에서 out 키워드를 사용한 이유

  • out var playerDataJson은 C#에서 TryGetValue 메서드를 사용할 때 데이터를 안전하게 가져오기 위한 방식입니다. out 키워드를 사용하면 메서드가 성공적으로 값을 반환할 때만 해당 값을 받아올 수 있습니다.

리더보드 환경 설정 ( 순위표 환경설정 )

    1. 단축키 추가 후 생성한다.
  • 이름 설정

    1. 큰게 좋은걸로 선택, 최고점수가 진짜냐 마지막 점수가 진짜냐 총합이 진짜냐. 여기서는 최고점수로 설정

    1. 시즌 리셋 여부. 여기서는 NO
    1. 점수표에 티어제도 여부 . 여기서는 NO

  • Entries 는 현재 순위를 알려줌. 아직은 아무것도 없음.

    1. 패키지 다운로드

리더보드 구현.

    1. 코드를 작성한다.
 async void GetCurrency()
 {
   //..생략

     AddScore(ssal);

	//..생략
 }
    public async void AddScore(long ssal)
    {
        var scoreResponse = await LeaderboardsService.Instance.AddPlayerScoreAsync("SSAlSUNWI", ssal); //순위 추가하기

        GetScores();
    }

    public async void GetScores()
    {
        var scoreResponse = await LeaderboardsService.Instance.GetScoresAsync("SSAlSUNWI"); //스코어 받아오기 
        Debug.Log(JsonConvert.SerializeObject(scoreResponse));
    }
  • 실행 시, 아래 처럼 로그가 출력된다.

위 코드 간략한 설명

  • 점수를 추가하는 AddScore() 메서드를 호출한다.

  • 리더보드 서비스에서 제공하는 AddPlayerScoreAsync 메서드를 호출한다.
  • AddPlayerScoreAsync 는 SSAlSUNWI 라는 리더보드 키에 플레이어의 점수 ssal을 추가하는 네트워크 작업을 수행하고, 이 작업은 시간이 걸리므로 await 키워드를 통해 기다린다.
 var scoreResponse = await LeaderboardsService.Instance.AddPlayerScoreAsync("SSAlSUNWI", ssal); //순위 추가하기

  • 네트워크 호출이 완료 되면 GetScores()을 호출한다. 호출함으로써 리더보드에서 점수를 가져온다.
  public async void AddScore(long ssal)
  {
      var scoreResponse = await LeaderboardsService.Instance.AddPlayerScoreAsync("SSAlSUNWI", ssal); //순위 추가하기

      GetScores();
  }

  • 리더보드에서 점수를 받아온다.
  • GetScoresAsync("SSAlSUNWI")**는 "SSAlSUNWI"`라는 리더보드의 점수를 가져오는 네트워크 요청입니다.
 var scoreResponse = await LeaderboardsService.Instance.GetScoresAsync("SSAlSUNWI"); //스코어 받아오기
  • 리더보드에서 받은 점수 데이터는 scoreResponse 변수에 저장
  • 점수 데이터를 JSON 형식으로 로그로 출력한다. JsonConvert.SerializeObject는 객체를 JSON 문자열로 변환하여 콘솔에 출력합니다.
Debug.Log(JsonConvert.SerializeObject(scoreResponse));

playerName 부분에서 공백이므로 닉네임을 설정해준다.

    1. 코드 작성한다.
    async void Start() //비동기 함수여야만 await(비동기) 사용 가능
    {
           //..생략
           
        if(AuthenticationService.Instance.IsSignedIn)  //로그인 됐을 경우
        {
            userIDInfo.text = "Player ID : " + AuthenticationService.Instance.PlayerId; //PlayerId는 유니티가 게스트 로그인하면 알려줌

            //⭐ 닉네임 설정
            await AuthenticationService.Instance.UpdatePlayerNameAsync("KimJin");

          //..생략
        }
        else  //로그인 안됏을 경우
        {
            Debug.Log("로그인 실패 ㅠ");
          
        }
    }

서버에 클라우드 추가하기. Cloud Code 환경 설정.

어떤 코드를 서버에 넣는 방법을 알기 위해서는 CloudCode 가 필요하다.

    • Cloud Save - Game Data : 모든 유저에게 적용되는 세이브파일 (Cloud code에서 저장하고 바꿀 수 있다. 월드 세이브 데이터)
    1. 패키지 다운로드, Samples도 다운로드 한다.

  • 추가로, Deployment 패키지도 다운로드 한다.

    1. Service에 아래 처럼 CloudCode와 Deployment창이 생겼다.
  • Preferences에도 생겼다.

    1. .net Path를 설정한다. dotnet.exe가 있는 폴더 경로를 입력


    1. 아래처럼 생성한다. 이름은 CloudeCodeReference로 짓는다.

    1. 처음엔 Open Solution 버튼이 아닌 Generate Solution일 것이다. 그걸 먼저 누르고 나서 Open Solution을 누른다.
    1. Open Solution을 누르게 되면 Example.cs 가 자동으로 생성되고 코드가 적혀있다. 이 프로그램에 특징은 서버에 함수들을 추가하는 것이다.
    1. 5번과정에서 Generate Binding을 누르면 아래처럼 폴더가 생긴다.
  • C# 파일도 있다.

  • 해당 파일을 열면 아래처럼 나타난다.

    1. Service - DeployMent에서 DeployAll한다.
  • 결과

  • SayHello가 생겼다.

    1. Open Solution 을 열고 아래 코드를 작성한다.
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
namespace HelloWorld;

public class MyModule
{
    ILogger<MyModule> logger; // 로그 찍는데 사용

    public MyModule(ILogger<MyModule> logger)
    {
        this.logger = logger;
    }

    // Hello 함수를 서버에서 부를 때는 SayHello 함수로 불러주세요
    [CloudCodeFunction("SayHello")]
    public string Hello(string name)
    {
        return $"Hello, {name}!";
    }

    [CloudCodeFunction("GetGacha")]
    public async Task<string> GetGacha(IExecutionContext context, IGameApiClient apiClient)
    {
        //IGameApiClient apiClient; // UGS 기능들을 쓰는 데 사용
        var result = await apiClient.CloudSaveData.GetItemsAsync(context, context.AccessToken, context.ProjectId, context.PlayerId,
            new List<string> { "Player" });

        string savedData = result.Data.Results.First().Value.ToString();

        logger.LogDebug(savedData);

        return savedData;
    }

    public class ModuleConfig : ICloudCodeSetup
    {
        // apiClient를 사용하려면 이 클래스를 작성해주어야 합니다.
        public void Setup(ICloudCodeConfig config)
        {
            config.Dependencies.AddSingleton(GameApiClient.Create());
        }

    }
}
    1. Deploy해서 서버에 코드를 Update 한다.
    1. 그 결과 새로 만든 Getgacha 메서드와 생성되었다.
    1. 이제 서버에 등록한 함수를 유니티에서 호출하는 코드를 작성한다. UGSManger.cs 수정
using System;
using TMPro;
using UnityEngine;
using Unity.Services.Core;
using Unity.Services.Authentication;
using Unity.Services.Economy;
using System.Linq;
using Unity.Services.Economy.Model;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.CloudSave;
using UnityEngine.SocialPlatforms.Impl;
using Unity.Services.Leaderboards;
using Newtonsoft.Json;
using Unity.Services.CloudCode;
using Unity.Services.CloudCode.GeneratedBindings;
using UnityEngine.UI;

public class UGSManager : MonoBehaviour
{
    [SerializeField]
    Text userIdTmp;
    [SerializeField]
    Text ssalTmp;
    [SerializeField]
    Text gachaTicketTmp;
    [SerializeField]
    Text playerDataTmp;

    List<PlayersInventoryItem> items = new List<PlayersInventoryItem>();


    LevelData levelData;

    async void Start()
    {
        // 유니티 서비스를 이용하기 전 초기화
        await UnityServices.InitializeAsync();
        // 게스트 로그인 구현, 이 기기로 로그인 하면 같은 계정으로 인식
        await AuthenticationService.Instance.SignInAnonymouslyAsync();

        if (AuthenticationService.Instance.IsSignedIn)
        {
            // 로그인 성공시
            userIdTmp.text = "Player ID : " + AuthenticationService.Instance.PlayerId;

            // Player Name 변경
            await AuthenticationService.Instance.UpdatePlayerNameAsync("Nell");


            CallCloudFunction();
        }
        else
        {
            // 로그인 실패시 초기 화면으로 돌아간다.
            Debug.Log("");
        }
    }

    async void CallCloudFunction()
    {
        // 서버에서 함수 호출
        try
        {
            //var module = new CloudCodeReferenceBindings(CloudCodeService.Instance);
            //var result = await module.SayHello(" World");

            var module = new CloudCodeReferenceBindings(CloudCodeService.Instance);
            var result = await module.GetGacha();

            Debug.Log("cloud code result : " + result);
        }
        catch (CloudCodeException e)
        {
            Debug.Log(e.Message);
        }
    }
 
}

Remote Config(계속)

  • 모든 유저에게 적용되는 설정파일
  • 게임 밸런스 및 게임 전체 설정
  • Cloud Code에서 저장하고 수정 가능
  • 월드 세이브 데이터



📄 Remote Config에 Gacha Data 설정하기. (가챠 확률표)

    1. 구성에서 name과 타입을 지정해준다.
    1. 변수의 값을 정해준다.
    1. 원하는대로 float형 1.1에 변수 이름 LevelupFactor가 생겼다. 그리고나서 publish를 한다.
    1. publish를 한다.
  • publish를 한다.

  • 다운로드 버튼을 누르면

    1. 액셀파일이 다운로드 되고 내가 설정한 변수를 볼 수 있다.
    1. Templates 탭에 가서 템플릿 생성을 해준다.
    1. 이런창이 나올텐데 JSon형식으로 가챠 확률표를 어떻게 구성할지 작성한다. 코드는 아래 참고한다.
{
    "title": "Gacha Probability",
    "$id": "#gacha-Probability",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "description": "가챠 확률표",
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "factor": {
                "type": "number"
            }
        },
        "required": ["name", "factor"]
    }
}
    1. 구성 탭에서 다시 Add Key를 해준다.타입은 JSon형식으로 하고, 자동으로 템플릿을 설정한다. 이름은 본인 자유!
    1. 왼쪽 코드 창과, 오른쪽 파라미터 창 둘 다 사용 가능하다
  • 10 . 만들고 나면 아래처럼 내가 설정한 대로 나온다.

    1. 이제 Json 파일에 가챠 목록을 추가한다. 사용할 프리펩 목록은 총 8개이다.

    1. 만들고 나면 위 설정한 대로 나온다.
    1. GachaManager.cs 생성한다. 서버인 CodeCloudReference에서 클래스를 추가한다.
    1. 코드를 작성한다.
using HelloWorld;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;

namespace CloudCodeReference
{
    internal class GachaManager
    {
        // CloudCodeReference 경로에 class를 추가하면
        // 서버의 가챠 데이터와 유니티의 UGSManager 코드를 불러올 수 있다.
        // 이 파일은 Example.cs에서 불러와 사용할 예정
        ILogger<MyModule> logger;
        IGameApiClient apiClient;

        [Serializable]
        internal class GachaItem
        {
            // 서버에 저장된 JSON 데이터를 불러온다.
            public string Name { get; set; }
            public string Factor { get; set; }
        }


        public GachaManager(ILogger<MyModule> logger, IGameApiClient apiClient)
        {
            this.logger = logger;
            this.apiClient = apiClient;
        }

        public async Task<string> DoGacha(IExecutionContext context)
        {
            var result = await apiClient.RemoteConfigSettings.AssignSettingsGetAsync(
                context, 
                context.AccessToken, 
                context.ProjectId, 
                context.EnvironmentId,
                null,
                new List<string> { "GachaProbabilityTable" });

            List<GachaItem> items = 
                JsonConvert.DeserializeObject<List<GachaItem>>(
                    result.Data.Configs.Settings["GachaProbabilityTable"].ToString());

            foreach(GachaItem item in items)
            {
                logger.LogDebug(item.Name + " " + item.Factor);

            }
            
            return "";
        }
    }
}
    1. Example.cs에서도 아래 코드를 추가한다.
    [CloudCodeFunction("GetGacha")]
    public async Task<string> GetGacha(IExecutionContext context, IGameApiClient apiClient)
    {
        //IGameApiClient apiClient; // UGS 기능들을 쓰는 데 사용
        var result = await apiClient.CloudSaveData.GetItemsAsync(context, context.AccessToken, context.ProjectId, context.PlayerId,
            new List<string> { "Player" });

        string savedData = result.Data.Results.First().Value.ToString();

        logger.LogDebug(savedData);

        return savedData;
    }
    1. Deploy한다.

웹에서 코딩하는 방법 - Cloud을 짤 때 자바스크립트로 코딩을 하고 싶다면?

    1. 아래와 같이 스크립트를 만든다.
    1. 서버에서 주사위 굴리는 코드가 적혀있다.
    1. 사이트에서 JS스크립트가 생성되었고, 여기서 코드를 작성하거나 수정할 수 있다.

1

    1. 코드를 작성한다.
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudSave;

namespace HelloWorld
{
    public class MyModule
    {
        ILogger<MyModule> logger;   // 로그 찍는데 쓸거임
        IGameApiClient apiClient;   // UGS 기능들을 쓰는 데 쓸거임

        public MyModule(IGameApiClient apiClient, ILogger<MyModule> logger)
        {
            this.apiClient = apiClient;
            this.logger = logger;
        }

        [CloudCodeFunction("SayHello")]
        public string Hello(string name)
        {
            return $"Hello, {name}!";
        }

        [CloudCodeFunction("GetGacha")]
        public async Task<string> GetGacha(IExecutionContext context)
        {
            var result = await apiClient.CloudSaveData.GetItemsAsync(context, context.AccessToken, context.ProjectId, context.PlayerId,
                new List<string> { "Player" });
            string savedData = result.Data.Results.First().Value.ToString();

            logger.LogDebug(savedData);

            return savedData;

        }
    }
}

json에서 다양한 정보를 담는 방법.

    1. 템플릿 부분ㄴ에서 아래 JSon 코드 처럼 작성한다. 이 과정은 기본값을 어떻게 할지 세팅하는 것이다. 아래 사진을 참고.
    1. 구성에서 Add Key를 하고 타입은 json으로 name은 임의로, 템플릿은 1번과정에서 만들었던 템플릿을 선택
    1. name과 factor 프로퍼티가 생성되었고, 세팅해줘야 한다.
    1. 다 만들고 나면 아래처럼 value에 내가 설정한 값이 들어있다.

    1. 기존 CloudeCodeReference에서 새 클래스를 추가한다.
  • 2 코드를 작성한다.
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Unity.Services.CloudCode.Apis;
using Unity.Services.CloudCode.Core;
using Unity.Services.CloudCode;
namespace CloudeCodeReference
{
    internal class GachaManager
    {
        ILogger<MyModule> logger;   // 로그 찍는데 쓸거임
        IGameApiClient apiClient;

        [Serializable]
        internal class GachaItem
        {
            public string Name
            {
                get;
                set;
            }

            public string Factor
            {
                get;
                set;
            }
        }
        public GachaManager(ILogger<MyModule> logger,IGameApiClient apiClient)
        {
            this.logger = logger;
            this.apiClient = apiClient;
        }

        public async Task<string> DoGacha(IExecutionContext context)
        {
            var result = apiClient.RemoteConfigSettings.AssignSettingsGetAsync(
                context, context.AccessToken, context.ProjectId, context.EnvironmentId, null, new List<string> { "GachaProbabilityTable" });

            List<GachaItem> Items = JsonConvert.DeserializeObject<List<GachaItem>>(result.Result.Data.Configs.Settings["GachaProbabilityTable"].ToString());

            foreach(GachaItem item in Items)
            {
                logger.LogDebug(item.Name + "" + item.Factor);
            }
            return " ";
        }
    }
}
    1. 기존 Example.cs에서 로그 찍는부분을 주석처리하고 새 코드를 작성한다.

0개의 댓글