TIL

[멋쟁이사자처럼 부트캠프 TIL회고] Unity 게임 개발 2.28일 : 출시 설정

Cadi 2025. 2. 28. 22:19

오늘 배운 것 

오늘은 전체적으로 구글 플레이 콘솔을 활용하여, 앱(게임)을 등록하고, 

리더보드 업적 등 GPGS(Google Play Game Service)을 활용하고,

인앱 결제까지 완전하게는 아니지만 이렇게 하면 된다 ~ 를 배웠다. 

 

 

 

01. Google Play Console 설정

 

Google Play Console

처음 가입 시 : 25불 + 등본등으로 제출해서 살고 있는지 확인(신원인증)

 

강사님께서는 유니티로 게임을 만들기 전에, 우선 여기서 앱을 만들고 시작하신다. 

연동하는 부분에 있어 오류가 생길 수 있고, 대부분의 게임마다 반복적으로 사용하는 기능들을 미리 베이스로 두고

(예를 들면 광고, Google Play Game Service 연동 등) 게임 만들기를 시작하신다. 

 

 

 

앱 만들기 화면

한 번 만들어서 테스트용이라도 올리면 삭제가 되지 않으므로 , 유의해야 한다.(숨김은 있다)

 

게임과 앱은 쉽게 변경되지 않는다.잘 체크하자.

 

유료 또는 무료

: 유료로 설정하면 각 플랫폼 별 기준으로 자연스럽게 환율이 적용되어 모든 국가에 배포되게 된다.

  다만 이 때 , 플랫폼마다 환율 가격이 다르다는 점에 유의해야 한다. 만일 플레이스토어와 앱스토어에서 가격이 다르다면
  유저들 사이에서 불필요한 불만이 생길 수 있다. 

 

* 팁 : 무료 게임은 너무나도 많이 나와서 오히려 유료 게임이 메리트가 있을 수 있다.

* 팁2 :  애플은 가격 책정할 때 티어로 결정하는 느낌이다.

 

 

이렇게 앱을 하나 만들고 나면, 기본적이 틀이 생긴다. 이 틀들을 하나씩 체크해보자.

 

설명중에서 테스트 및 출시 - 출시 개요

 

프로덕션이 실제로 올라가는 것을 의미한다. 

 

한 번에 가기도 하지만 내부테스트 비공개 테스트 공개 테스트를 거쳐 가게 된다.

  • 내부 테스트 : 팀 내에서 테스트
  • 비공개 테스트 : 외부인이지만 우리 회사에서 지정한 사람들 (클로즈 베타)
  • 공개 테스트 : 테스트 상태인 것을 인지하고 오픈하는 것

공개 테스트 - 얼리 엑세스 등 사람들의 평가가 달라질 수 있다. 정식 오픈에는 욕이 달리지만, 테스트란 것을 알면 다를 수도.. ? (조금 더 봐주는 경향이 있다, 다만 요즘 얼리로 사기치는 게임이 많아서... 예전보다는 덜 봐준다)

 

내부 테스트 : 새 버전 만들기(오른쪽 상단)

 

앱 번들 빌드 해서 올려놓기 !! (aab로 빌드한다 밑의 과정)

 

 

항상 안드로이드로 빌드할때 체크해야 하는 것들

 

 

플랫폼 설정을 제대로 해 놓아야 전처리기가 작동함.

(우리가 #if Android ~~  로 해 놓은 것들)

 

아이콘 관련

 

디폴트 아이콘을 설정해 놓으면 대부분 되지만, 

안드로이드는 버전에 따라 아이콘을 표시하는 형태가 조금씩 다르다.

 

옛날보다 표현할 수 있는 픽셀 양이 늘었다.  dpi에 맞게 아이콘의 사이즈를 조절할 수 있다.

그러면 해상 해상도에 최적화 되어 있는 이미지가 나오게 된다. 그렇지 않고 제일 낮은 dpi만 넣는다면

이미지가 늘려지면서 이상하게 보일 수 있고, 반대로 큰 이미지를 넣으면 리사이징 과정에서 왜곡되기도 한다.

앱의 아이콘은 앱의 얼굴이기 때문에 (한 이미지로 표현하는) 맞춰 넣어주는 것이 좋다.

 

 

Resoultion and Presentation

 

기본적으로모바일의 경우, 한 창에 하나만 뜨기 때문에 크게 상관 없지만,  PC 게임의 경우 FullScreen Mode를 적절히 조정해 주어야 한다. 

Allowed Orientations for Auto Rotation 

화면이 돌아갈 지 말지, 저번에도 말했지만 설정하는 것이 생각보다 힘들다.

그냥 한 가지 화면으로 보통 하는 것이 편하다.

 

Splash Screen

 

게임을 처음에 구동했을 때 나오는 이미지

개발사, 퍼블리셔, 유통사, 그래픽 전문 등 .. .. 을 설정하곤 한다.

설정을 해 두지 않으면 Unity 로고를 홍보한다 ㅎ.ㅎ

 

Other Settings

여러가지 세팅들, 

auto Graphics API 기본적으로는 오토로 설정한다.

 

내려가다보면 Identification이 있다. 식별한다는 것.

보통 만들고자 하는 게임의 이름은 이미  있을것이다, 그래서 구글은 저 패키지 네임으로 게임을 식별한다.

보통 역 도메인 이름 표기법을 따른다. com.<회사 이름>.게임 이름  형식으로 정한다.

 

* 팁 : 상표 등록(이름)은 할 수 있으면 하는 것이 좋다. 개인이 할 수 있고 어렵지 않고 연장도 가능하기 때문이다.

 ( 기간은 약 1 ~ 2년, 비용은 약 20만원)

만일 스토어에 올렸는데 동일한 이름이 있다면, 보통 올리지 못하게 한다. 이 경우 상표권이 있으면 내 이름이다 ! 

를 주장할 수 있다. 

 

바로 밑에 있는 MinimumAPi Level와 Target API Level 은 앱이 권장하는 최소 Android 버전과

가장 최적화된 Android 버전을 뜻한다. 

기본적으로는 위와 같지만, 강사님 추천으로는 최소는 24, 타겟은 34라고 하셨다.

너무 낮게 하면 GPGS 연동이 어려울 수 있기 때문이다. (후에 직접 보여주셨다, 밑으로 가면 오류의 향연 스샷이 있다)

 

 

 

Configuration

IL2CPP로 설정. (64비트를 위해서, 현재 마켓에서는 무조건 64비트)

ARM64도 선택해야 한다 (스마트폰은 ARM 기반의 CPU를사용하고 있기 때문이다)

x86은 PC 기반으로 동작한다. 

 

 

 

 

Publising Settings

KeyStore   - 요것도 없으면 스토어에 안올라가짐

새로 만들어야 하는데 이걸 Git에 올라가게 되면, 다른 개발자들도 이 파일을 갖고 있게 됨.

그래서 다른 파일에 저장하는 것이 좋다.

나는 상위 폴더에 저장했다.

키스토어 하나에 여러가지의 키를 비밀번호를 달리해서 저장할 수 있다.

 

*참고 : 

ADMob 빌드할 떄도 요구되는 것

 

 

스토어에 올릴 떄 이 내용들은 반드시 챙겨야 한다. 

 

 

이제 빌드해보자, 

 

 

과거에는 APK 파일을 올릴 수 있었다.

Builde App Bundle을 체크하면 된다 (지금은 안드로이드에 올릴 것으므로) aab 파일이 생성되고

 

 

 

아까 테스트 버전 만들기에 이렇게 올리게 되면 올라간다. (Google Play Console에)

출시 노트를 꺽쇠 안에 넣어서 내용을 넣을 수 있다. 

 

 

현재 오류가 있어서(계정 승인이 아직 안됨) 출시가 안되지만, 저장 및 출시 버튼이 뜨게 된다. 

 

 

 

사용자 늘리기 :

스토어 등록 정보에서 자세한 스크린 샷, 동영상, 소개 등을 넣을 수 있다.

 

 

PC용 모바일 도 만들수 있다. (PC 버전에서도 실행되는)

 

* 참고 :  구글 ADs의 60만원으로 ... . .. 성장시키기 광고가 여기서 뜬다 1+1 광고 혜택이다. 잘 만들었다고 생각한다면 ..

 

스토어 실적 등은 게임을 올리면 나온다. 

 

 


이제, 리더보드, 업적 등을 설정하기 위해  Play 게임즈 서비스 설정을 해 보자. 

 

 

 

Play 게임즈 서비스 설정

우선 연동하기 위해 다음과 같은 플러그인을 설치해 주어야 한다.

 Google play games service unity를 검색한다.

 

여기서 관련 플러그인을 받아서 설치할 수 있다. 

안에 들어가서 가이드를 들어가면 다음과 같은 가이드가 있다.

https://developer.android.com/games/pgs/unity/unity-start?hl=ko

 

Unity용 Google Play 게임즈 설정 및 로그인  |  Android game development  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Unity용 Google Play 게임즈 설정 및 로그인 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 문서에서는

developer.android.com

 

(실제로 업데이트가 될 때 가이드가 업데이트 되지 않을 수도 있기 떄문에 깃허브 쪽을 확인하는 것이 좋다)

 

 

leaderBoard & 업적

 

게임을 지속적으로 즐길 수 있게 하기 위한 동기부여 수단으로 활용할 수 있는 리더보드와 업적이다. 

리더보드 창 자체를 GGPS에서 제공하고 있기에 따로 꾸밀 필요는 없다.

(유니티도 자체적으로 제공하고 있고, 물론 커스텀 할 수도 있다.)

 

코드 자체는 이렇게 두 줄 띡 하면 생성되지만, 설정이 힘들다 ㅎ..

이렇게 서버를 선택해서 리더보드 정보를 저장할 서버를 만들어주어야 한다.  

 

 

다음과 같이 만들어진다. 

만들어진 프로젝트를 선택해주고 다음과 같은 과정을 진행하면 된다.

 

1. 방금 만들었다 .

5. 테스터 (이메일 ) 추가해주면 된다.

 

2. 마찬가지로 눌러서 해준다. (밑은 OAuth 검색한 것)

 

3. 사용자 인증 정보

OAuth 클라이언트 ID를 만들어야 한다. 애플리케이션 유형은 안드로이드,

 

 

Keytool - KeyStore를 가져오기 위해 필요하다. 

기본적으로 유니티에서 제공해주고 있지만, 다음에 서술할 내용 때문에 다운 받아놓는 것이 좋다.

JDK(Java Development Kit) 설치 필요. 

https://www.oracle.com/kr/java/technologies/downloads/#jdk23-windows

 

Download the Latest Java LTS Free

Subscribe to Java SE and get the most comprehensive Java support available, with 24/7 global access to the experts.

www.oracle.com

 

 

과거에는 매번 설치해서 유니티에 연동을 시키다 보니까 ,연동을 어렵게 느끼는 사람이 많았다

유니티 자체에서 연동을 시켜버림. 그래서 매 버전마다 자체적으로 연동되어 있는 것이 있다.(기본 설정)

사실 위에서 다운 받은 것도 이미 설치되어 있다.

그러나 위 JDK는 유니티 버전을 삭제하면 동시에 삭제되기 때문에 기본으로 두기는 좀 그렇다.

그래서 나중에는 우리가 설치해 둔 것을 기본으로 설정해서 쓸 수 있다.

그러면 다른 유니티 버전이라고 할 지라도 동일한 환경에서 이용할 수 있다. 

 

 

*팁 : 서버도 엔진 형태 소규모로 서비스 되고 있는 것들이 있다. (채팅, 방 만들기, 매칭 등 기본적인 기능 제공)

서버도 엔진 형태로 서비스 되고 있는 것들이 있음, 규모가 작은 게임에서는 뒤끝서버 등이 있다. 

 

* 자투리 지식 : 결국 게임 엔진이란 것이 비슷한 게임들을 만들다가 어 ? 이건 계속 쓰겠는데 ? 에서 만들어진 것이다.

                         유니티는 그 시작이 조금 다르긴 한다. 

 

 

 

이건 안드로이드만 한 거고, 애플은 보안이 빡세서 조금 더 어렵다.

 

 

이제 진짜 진짜 유니티 설정을 해 보자. 

 

 

 

이 패키지를 다운로드 받은 후 임포트한다. 

임포트 할 때 주의해줘야 한다. 네이티브 코드를 건들기 때문에 빌드가 되다가 안될 수 있다. 

(항상 커밋을 하거나 바뀌는 걸 염두에 두어야 한다)

주로 변경되는 것들은 ExternalDepenaencyManger라는 종속성을 해결해주는 것이다. 

 

깔고 나서는 Resolve !

 

 

그 다음 셋업을 해줘야 한다.  셋업 창이 뜰 텐데, 그 안에 들어갈 내용은 밑에서 가져온다. 

 

리더보드를 생성하고 싶다면 리더보드 창으로 들어가서 설정해주면 된다. 

 

업적, 리더보드 등을설정하고 보면 '리소스 보기' 가 있는데 그 리소스를 복사해서

셋업 창을 열면 열리는

다음과 같은 창에 붙여넣어주면 된다.

 

이제 빌드를 해 주면 잘 될텐데, 그것은 지금 타겟 레벨 , 미니멈 레벨을 잘 설정해서 그런다.

기본값으로 하게 되면

무수한 오류의 악수 요청을 받게 된다. 

심지어 정확히 오류가 어떤 것인지 표시되지 않아서 찾는데 상당한 시간이 걸린다.

강사님께서는 이번 버전에서는 타겟 레벨과 미니멈 레벨을 설정함으로써 이 문제를 해결하셨지만, 

다음 버전에서는 이 해결 방법이 먹히지 않을 수도 있다. 

유료 에셋 등을 사용해 오류를 '조금은' 줄일 수 있지만 ,결국 언젠가는 겪게 될 문제이다. 

 

 

 

이제 아이콘과 이미지 (설정 및 관리 -- 설정) 에서 알맞게 설정해주자. 

 

 

 


스크립트 영역으로 갈 채례이다.

 

GPGSManager라는 클래스를 만들고 관리해 줄 것이다.

우선 네임스페이스 GooglePlayGames를 선언한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using GooglePlayGames;
using GooglePlayGames.BasicApi;

public class GPGSManager : Singleton<GPGSManager>
{
    private void Start()
    {
        PlayGamesPlatform.Instance.Authenticate(ProcessAuthication);
    }

    private void ProcessAuthication(SignInStatus status)
    {
        if (status == SignInStatus.Success)
        {
            Debug.Log("GPGS Authentication Success");
            string name = PlayGamesPlatform.Instance.GetUserDisplayName();
            string id = PlayGamesPlatform.Instance.GetUserId();
            string porfileImage = PlayGamesPlatform.Instance.GetUserImageUrl();
            
            
        }
        else
        {
            Debug.Log("GPGS Authentication Failed");
        }
    }
    protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
    }
}

 

다음과 같이 id, name 이미지를 가져오고 있다. 

이런 식으로도 가능하다,

Social은 플레이스토어와 애플을 둘 다 하기 위한 노력이다. UnityEngine. SocialPlatforms 네임스페이스에 속하며

유니티의 일반적인 소셜 기능 인터페이스를 제공한다. (다양한 소셜 플렛폼에 대한 공통된 접근 방식)

PlayGamesPlatform 클래스는 GPGS에 특화된 기능을 제공한다.

 

구글과 애플의 배포는 여러번 해야 익혀지기 때문에 오늘은 설정의 과정들을 한 편 살펴보고, 다음에는 직접 눈으로 보면서  실행까지 시켜보는 강의를 준비해 오시겠다고 하셨다.

 

public class GPGSManager : Singleton<GPGSManager>
{
    #if UNITY_ANDROID
    private const string readerBoardId = "";

 

readrBoard 값은 밑에 있는 값이다. (우리가 이미 알고 있다.)

 


인앱 결제

 

 

구름 키를 누르면 클라우드 관련 서비스가 다 나온다.

이런 서비스 들을 하나하나씩 배워나가면 게임 적용 기능들이 많은 것을 알 수 있다. 

 

좌상단 구름

 

이걸 인스톨하면 프로젝트 세팅에

다음과 같이 뜨게 된다. 

 

 

이런 클라우드 서비스는 처음에 프로젝트 만들 때 Cloud 기능을 체크해야 한다.

하지만, 연습용으로 할때는 클라우드를 만들지 않기에, 크리에이트 뉴 클라우드 프로젝트를 해야 한다. 

 

이런걸 설정하고 나면 

이런 식으로 구글 플레이 스토어에 등록하고 나면, 상품마다 ID가 생기게 된다. 

 

이 아이디를 등록해주면 되는 것이다.

 

오늘은 상품 등록 당장 어려우니, 일단 코드 쪽에 집중해보자. 

 

public class IAPManager : MonoBehaviour, IStoreListener
{
    public void OnInitializeFailed(InitializationFailureReason error)
    {
        throw new System.NotImplementedException();
    }

    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        throw new System.NotImplementedException();
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        throw new System.NotImplementedException();
    }

    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        throw new System.NotImplementedException();
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        throw new System.NotImplementedException();
    }
}

 

 

IstoreListener를 추가하면, 다음과 같이 다섯 개의 필수로 구현해야 하는 함수가 있다.

각각의 역할은 다음과 같다.

 

  • OnInitialized() : IAP 시스템이 성공적으로 초기화 되었을 때 호출 
  • OnInitializeFailed(): IAP 시스템이 초기화 했을 때 호출, 매개변수에 따라 자세한 메세지를 전달해준다.
  • ProcessPurchase() : 상품 구매가 완료되었을 때 호출된다. 
  • OnPurchaseFailed() : 상품 구매가 실패했을 때 호출된다. 

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;


public class IAPManager : MonoBehaviour, IStoreListener
{
    public enum Type
    {
        NOADS,
        NOADS_Heart_60,
        Heart_20,
        Heart_60,
        Heart_150,
        Heart_300,
        Heart_450
    }

    //상품들의 ID
    // 구글플레이스토어와 소통할 수 있는 ID
    public const string kPID_Heart20 = "heart_20";
    public const string kPID_Heart60 = "heart_60";
    public const string kPID_Heart150 = "heart_150";
    public const string kPID_Heart300 = "heart_300";
    public const string kPID_Heart450 = "heart_450";
    public const string kPID_NoAds = "no_ads";
    public const string kPID_NoAds_Heart60 = "no_ads_heart_60";

    private IStoreController _storeController;
    private IExtensionProvider _storeExtensionProvider;


    private void Start()
    {
        if (_storeController == null)
        {
            InitializePurchasing();
        }
    }

    private void InitializePurchasing()
    {
        if (isInitialized())
        {
            return;
        }

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
        builder.AddProduct(kPID_Heart20, ProductType.Consumable);
        builder.AddProduct(kPID_Heart60, ProductType.Consumable);
        builder.AddProduct(kPID_Heart150, ProductType.Consumable);
        builder.AddProduct(kPID_Heart300, ProductType.Consumable);
        builder.AddProduct(kPID_Heart450, ProductType.Consumable);
        builder.AddProduct(kPID_NoAds, ProductType.NonConsumable);
        builder.AddProduct(kPID_NoAds_Heart60, ProductType.NonConsumable);
        UnityPurchasing.Initialize(this, builder);
    }

    public void BuyProduct(Type type)
    {
        switch (type)
        {
            case Type.Heart_20:
                BuyProductID(kPID_Heart20);
                break;
            case Type.Heart_60:
                BuyProductID(kPID_Heart60);
                break;
            case Type.Heart_150:
                BuyProductID(kPID_Heart150);
                break;
            case Type.Heart_300:
                BuyProductID(kPID_Heart300);
                break;
            case Type.Heart_450:
                BuyProductID(kPID_Heart450);
                break;
            case Type.NOADS:
                BuyProductID(kPID_NoAds);
                break;
            case Type.NOADS_Heart_60:
                BuyProductID(kPID_NoAds_Heart60);
                break;
        }
    }

    private void BuyProductID(string productId)
    {
        if (isInitialized())
        {
            Product product = _storeController.products.WithID(productId);
            if (product != null && product.availableToPurchase)
            {
                Debug.Log(string.Format("Purchaing product asychronously : '{0}'", product.definition.id));
                _storeController.InitiatePurchase(product);
            }
            else
            {
                Debug.Log(
                    "BuyProductID : Not purchasing product, ethier is not found or is not available for purchase");
            }
        }
        else
        {
            Debug.Log("BuyProductID : Not initialized");
        }
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        _storeController = controller;
        _storeExtensionProvider = extensions;
    }

    private bool isInitialized()
    {
        return _storeController != null && _storeExtensionProvider != null;
    }

    public void OnInitializeFailed(InitializationFailureReason error)
    {
        Debug.Log("OnInitializeFailed : " + error);
    }

    public void OnInitializeFailed(InitializationFailureReason error, string message)
    {
        Debug.Log("OnInitializeFailed : " + message);
    }

    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart20, StringComparison.Ordinal))
        {
            Debug.Log("ProcessPurchase: PASS.Product : '" +purchaseEvent.purchasedProduct.definition.id + "'");
            //TODO: 하트 20개 줌.
            UserInformations.HeartCount += 20;
            
        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart60, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 60;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart150, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 150;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart300, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 300;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart450, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 450;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_NoAds, StringComparison.Ordinal))
        {
UserInformations.IsNOAds = true;
        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_NoAds_Heart60, StringComparison.Ordinal))
        {
            UserInformations.IsNOAds = true;
            UserInformations.HeartCount += 60;
        }
        else
        {
            Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", purchaseEvent.purchasedProduct.definition.id));
        }
        return PurchaseProcessingResult.Complete;
    }

    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        Debug.Log("OnPurchaseFailed : " + failureReason);
    }
}

 

 

전체적으로 해석해보자면  IAManager 는 IStoreListener를 인터페이스를 구현하여 In - AppPurchase를 처리한다.

유저가, 광고 제거나 인앱 상품 등을 결제할 수 있게 도와주는 클래스이다. 

 

Type은 열거형으로 구매 가능한 아이템을 정의해 두었다. 가독성을 높이고, 구매할 아이템을 쉽게 참조할 수 있게 한다.

 

다음과 같이 const로 선언한 string 타입은 각 아이템의 ID로 구글 플레이 스토어나 앱스토어에서 설정한

상품 ID가 아래의 문자열과 정확히 일치해야 정상적으로 작동한다. 

public const string kPID_Heart20 = "heart_20";

 

 

IStoreController _storeController는 IAP 시스템의 핵심 컨트롤러이다. (구매 요청 및 결과 처리 담당)

IExtensionProvider _storeExtensionProvider는 추가 기능을 제공하는 확장 프로바이더다. 

private IStoreController _storeController;
private IExtensionProvider _storeExtensionProvider;

 

인앱 결제 시스템을 초기화하는 함수이다.

UnityPurchasing.Initialize(this, builder)를 호출하여 이 클래스와 스토어를 연결하고, 설정된 상품 정보를 가져온다. 

 

그럼연결이 문자열로 되는건가 ? 만들어둔 builder랑 유니티 스토에서 연결된거랑 ?

private void InitializePurchasing()
{
    if (isInitialized())
    {
        return;
    }

    var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    builder.AddProduct(kPID_Heart20, ProductType.Consumable);
    builder.AddProduct(kPID_Heart60, ProductType.Consumable);
    builder.AddProduct(kPID_Heart150, ProductType.Consumable);
    builder.AddProduct(kPID_Heart300, ProductType.Consumable);
    builder.AddProduct(kPID_Heart450, ProductType.Consumable);
    builder.AddProduct(kPID_NoAds, ProductType.NonConsumable);
    builder.AddProduct(kPID_NoAds_Heart60, ProductType.NonConsumable);
    UnityPurchasing.Initialize(this, builder);
}

 

 

Start() 함수에서 이 함수를 불러 초기화 해 준다. (초기화가 되지 않았다면)

private void Start()
{
    if (_storeController == null)
    {
        InitializePurchasing();
    }
}

 

 

 

bool 타입을 리턴하는 isInitialized()는 정상적으로  위의 두 변수가 초기화 되었는지를 체크한다. 

private bool isInitialized()
{
    return _storeController != null && _storeExtensionProvider != null;
}

 

 

상품 구매 요청을 처리하는 함수이다.

정상적으로 초기화 되었다면, 매개변수로 받아온 string 변수를 통해 product를 설정하고,

product가 구매 가능한 상태이면 구매를 진행한다. 

private void BuyProductID(string productId)
{
    if (isInitialized())
    {
        Product product = _storeController.products.WithID(productId);
        if (product != null && product.availableToPurchase)
        {
            Debug.Log(string.Format("Purchaing product asychronously : '{0}'", product.definition.id));
            _storeController.InitiatePurchase(product);
        }
        else
        {
            Debug.Log(
                "BuyProductID : Not purchasing product, ethier is not found or is not available for purchase");
        }
    }
    else
    {
        Debug.Log("BuyProductID : Not initialized");
    }
}

 

 

이렇게 하나의 상품을 구입하는 함수는 아까 설정한 타입에 따라 호출된다.

public void BuyProduct(Type type)
{
    switch (type)
    {
        case Type.Heart_20:
            BuyProductID(kPID_Heart20);
            break;
        case Type.Heart_60:
            BuyProductID(kPID_Heart60);
            break;
        case Type.Heart_150:
            BuyProductID(kPID_Heart150);
            break;
        case Type.Heart_300:
            BuyProductID(kPID_Heart300);
            break;
        case Type.Heart_450:
            BuyProductID(kPID_Heart450);
            break;
        case Type.NOADS:
            BuyProductID(kPID_NoAds);
            break;
        case Type.NOADS_Heart_60:
            BuyProductID(kPID_NoAds_Heart60);
            break;
    }
}

 

 

다음 ProcessPurchase() 함수는 결제가 끝나면 자동으로 호출되는 함수이다.

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart20, StringComparison.Ordinal))
        {
            Debug.Log("ProcessPurchase: PASS.Product : '" +purchaseEvent.purchasedProduct.definition.id + "'");
            //TODO: 하트 20개 줌.
            UserInformations.HeartCount += 20;
            
        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart60, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 60;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart150, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 150;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart300, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 300;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_Heart450, StringComparison.Ordinal))
        {
            UserInformations.HeartCount += 450;

        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_NoAds, StringComparison.Ordinal))
        {
UserInformations.IsNOAds = true;
        }
        else if (String.Equals(purchaseEvent.purchasedProduct.definition.id, kPID_NoAds_Heart60, StringComparison.Ordinal))
        {
            UserInformations.IsNOAds = true;
            UserInformations.HeartCount += 60;
        }
        else
        {
            Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", purchaseEvent.purchasedProduct.definition.id));
        }
        return PurchaseProcessingResult.Complete;
    }

    public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    {
        Debug.Log("OnPurchaseFailed : " + failureReason);
    }
}

(밑의 OnPurchaseFailed()는 실패시 자동으로 호출되는 함수)

 


추가 

1. 굳이 변수를 사용하는 이유
가독성, 오타방지, 유지보수성(수정 시 하나만 수정하면됨)

2. 스토어에서 ID (heart_20)과 같이 설정하면 된다. 주의) 동일해야 함 !

3. IExtensionProvider의 정확한 역할

  • 추가적인 스토어 관련 기능을 제공하는 인터페이스
  • IStoreController가 기본적인 결제 처리(구매 , 검증) 등을 담당한다면 IExtensionProvider는 플랫폼별 추가 기능 담당
    (Ex : 구독 상품 관리, 비소모품 복원, 플랫폼별 추가 기능 지원 등)

4. OnInitialized  함수 관련

OnInitialized는 Unity의 IAP 시스템이 초기화 될 때 자동으로 호출되는 함수

UnityPurchasing.Initialize(this, builder)를 실행하면, 자동으로 호출된다. 

 

5. StringComparison.Ordinal 이란 ?

문자열을 비교할 때 사용되는 옵션, Ordinal은 바이트 값을 기준으로 비교한다는 것.

기본적인 문자열 비교 == 연산자는 대소문자를 구분하지 않을 수 있다.

 

 

 

 

 

 

 

오늘의 목표

1. 코테 재귀 2 문제

2. 주말에 도전해볼게임 구상