본문 바로가기

유니티/인앱결제

[ 인앱결제 ] 03 . IAP Manager 구현

[ IAP  Manager ]

[ IAP ]

해당 방식으로 멀티 플랫폼 인앱결제를 진행한다. 유니티 IAP Serveice를 통해 구현 할 것이다.

인앱결제를 유니티 인앱결제 Purchasing namespace에 있는 라이브러리를 활용할것이다.

[  IAP Manager 생성 ]

[ IAP Manager ]

using UnityEngine.Purchasing

=>인앱 관련한 것들을 담당할것이다 .  인앱결제 관련 라이브러리를 다음의 선언으로 가져온다

public class IAPManager : MonoBehavior , IStoreListener

=>또한 ISotreListenr 인터페이스의 상속으로 강제하는 다음의 퍼블릭 함수들을 구현한다 .

이는 매니저가 인앱결제 과정에서 일어나는 이벤트를 알아차려야 하기 때문이다 .

static IAPManager _instance;

public static IAPManager Instance
{
    get
    {
        if (_instance != null) return _instance;
        _instance = FindObjectOfType<IAPManager>();
        if (_instance == null) _instance = new GameObject("IAP Manager").AddComponent<IAPManager>();
        return _instance;
    }
}

=>IAPManager는 싱글턴패턴으로 구현한다 .

 

[ 상품 식별자 구현 ]

public const string ProductGold = "gold";	//Consumable

=>골드라는 상품의 식별자 . 이는 무한정 구매 , 소비가 가능하다

 

public const string ProductCharacterSkin = "Character_skin;	//UnConsumable

=>캐릭터의 스킨 . 한번 구매하면 계속 등록된다 . (재구매 불가)

 

public const string ProductSubscription ="premium_subscription"	//Subscription

=>구독 서비스 아이템 (매달 돈을 낸다 . 이로 인해 남들과 다른 경험치 증가 등의 효과 )

 

 

여기서의 아이디는 유니티 에디터 내부에서 식별을 위한 아이디이다 .

앱스토어 ,구글플레이에 실제 사용하는 아이디는 아니다 .

private const string _ios_GoldID = "com.studio.app.gold";
private const string _android_GoldID = "com.studio.app.gold";

private const string _ios_SkinID = "com.studio.app.skin";
private const string _android_SkinID = "com.studio.app.skin";

private const string _ios_PremiumSubID = "com.studio.app.sub";
private const string _android_SkinID = "com.studio.app.sub";

=>구글 / 앱스토어 개발자 센터에서 해당 상품에 대한 식별자를 직접 설정해야 한다 . 만약 유니티에서 gold를 사용한다면 ,

각 플랫폼 별의 아이디를 사용하는 것이다 .

 

[ 초기화 ]

스토어 컨트롤러 / 스토어 익스텐션을 받을 수 있는 변수에 할당이 필요 .

이 두가지가 변수는 구매의 통제와 다른 플랫폼을 위한 특수 구매과정을 실행하게 해준다 .

중요한 점은 현재 IAP Manager는 지연생성 싱글턴이기에 초기화 과정이 빠르게 끝나지 않는다 .

즉, 사용시점에 생성되어 버리면 초기화 과정이 끝나지 않은 상태일수 있기에 사용전 미리 생성함이 좋다 .

    IStoreController storeController;   //구매 과정을 제어하는 함수 제공
    IExtensionProvider storeExtensionProvider;  //여러 플랫폼을 위한 확장처리 제공
  • IStoreController는 인앱결제 모듈을 활성화 시키는 순간에 생성되는 오브젝트이다 .인앱결제에 대한 처리과정을       직접 실행가능한 함수를 제공 , 이를 통해 구매등의 처리를 발생시킨다 .발생시킨 처리는 IAP Manager가 Listen           하여 처리를 하는 것이다 .
  •  IExtensionProvider는 이와 별개로 멀티 플랫폼 구매 구현시 ,대부분의 플랫폼에 적용되는 과정이 지원안되는         독특한 플랫폼을 위한 확장 기능을 제공한다 .

 

=>해당 변수에 대한 할당은 자동이 아니라 직접 처리가 필요하다 .  또한 해당 메서드는 두번 실행되면 안되니 ,

 bool 프로퍼티로 중복을 방지한다 . 

//get 만 있다면 Ramda로 표현 가능
public bool IsInitialized => _storeController != null && _storeExtensionProvider != null;

void Awake()
{
	//자신 이외의 인스턴스가 있다면
    if (_instance != null & _instance != this)
    {
        Destroy(gameObject);
       return;
   }

    DontDestroyOnLoad(gameObject);
    InitUnityIAP();
}

//초기화를 진행한다
void InitUnityIAP()
{
	if(IsInitialized)
    return;
    
	var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    
    //상품 등록 : 골드
    builder.AddProduct(
    id:ProductGold,ProductType.Consumable ,new IDs()
    {
    		{_ios_GoldID,AppleAppStore.Name},
        	{_android_GoldID,GooglePlay.Name}
    	}
    );
    
     //상품 등록 : 스킨
    builder.AddProduct(
    id:ProductCharacterSkin,ProductType.NonConsumable ,new IDs()
    {
    		{_ios_SkinID,AppleAppStore.Name},
        	{_android_SkinID,GooglePlay.Name}
    	}
    );
    
     //상품 등록 : 구독
    builder.AddProduct(
    id:ProductSubscription,ProductType.Subscription ,new IDs()
    {
    		{_ios_PremiumSubID,AppleAppStore.Name},
        	{_android_PremiumSubID,GooglePlay.Name}
    	}
    );
}

=> ConfiguratuibBuilder는 인앱결제와 관련된 Builder를 생성 할 수 있는 클래스 .

StandardPurchasingModule.Instance()의 인스턴스를 생성한다. 이는 유니티 기본제공 스토어 설정들이다 .

=>해당 builder에 상품을 등록한다 .

이때 , 범용적으로 쓸 아이디 등록 / 상품의 타입 / 앱스토어에 특화된 아이디를 newIDS로 등록

 

UnityPurchasing.Initialize(this , builder);

=>이제 , Builder의 설정을 통해 UnityPurchasing인앱결제 모듈을 활성화 (초기화)를 시작한다 .인자는 다음과 같다 .

  • Listener 유니티 Init시 그것을 들을 Listenr : 유니티 인앱결제 모듈이 활성화시 그 타이밍에 뭔가를 실행할 리스너
  • Builder 의 설정을 넣어줄것이다 .

=>UnityPurchasing.Initialize가 끝나면 this(IStoreListener)에게 메시지를 던질 것이다 . IAP Manager는 이벤트를 듣고

그 타이밍에 뭔가를 실행할것이다 . 완료시 storeController과 extension이 생성되는데 , 이를 등록하는 동작이 필요하다 .

 

++추가

버전 4.7의 IAP Package를 사용하니 다음의 에러가 발생하였다 .

유니티 IAP Manual을 확인해본 결과 Unity Gaming Services에 대한 Initiaizing이 추가로 이행되어야 함을 알 수 있었다 .

유니티 IAP에 연결되던 Legacy Analytics는 여전히 켜져 있어야 한다고 하면서도 새로 서비스되는 유니티 게이밍 서비스와도 연결이 되어 있는지 유니티 IAP를 사용하려면 유니티 게이밍 서비스 초기화 해야한다

해당 내용은 메뉴얼에 잘 나와있다

 

=>코드리스 유니티 IAP를 사용하는 경우에는 IAP Catalog에서 Automatically initialize Unity Gaming Services 체크해주면 되고, 코드리스를 사용하지 않는 일반적인 환경에서는 초기화 코드를 작성해야한다 .

 

using System;
using Unity.Services.Core;
using Unity.Services.Core.Environments;
using UnityEngine;

public class InitializeUnityServices : MonoBehaviour
{
    public string environment = "production";

    async void Start()
    {
        try
        {
            var options = new InitializationOptions()
                .SetEnvironmentName(environment);

            await UnityServices.InitializeAsync(options);
        }
        catch (Exception exception)
        {
            // An error occurred during services initialization.
        }
    }
}

해당 부분을 IAPManager에서 따로 구현 (InitUnityGamingServices())의 초기화 전에 넣어주어 동작하게 하였다 .

 

 

 

[ IStoreListener 메서드 구현 ]

public void OnInitialzied(IStoreController controller , IExtensionProvider extension)
{
	Debug.Log("유니티 IAP 초기화 성공");
	storeController = controller;
    storeExtensionProvider = extensions
}

=>IstroreListner가 구현하게 강제하는 메서드이다 . 인자로 스토어 컨트롤러 / 스토어 익스텐션을 받음. 이를 할당해준다 .

 

public void OnInitialziedFailed(InitializationFailuerReason error)
{
	Debug.Log($"유니티 IAP 초기화 실패 {error}");
}

=>실패시 왜 Init 실패했는지에 대한 정보가 들어온다 .

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
	Debug.Log(message : $:구매 성공 - ID : {args.purchaseProduct.definition.id}");
    
    if(args.purchasedProduct.definition.id == ProductGold)
    {
    	Debug.Log(message :" 골드 상승 처리 ");
    }else  if(args.purchasedProduct.definition.id == ProductSkin)
    {
    	Debug.Log(message :" 스킨 등록 ");
    }else  if(args.purchasedProduct.definition.id == ProductSubscription)
    {
    	Debug.Log(message :" 구독 서비스 시작 ");
    }
    
    return PurchaseProcessingResult.Complete;
}

=>인앱 상품 구매시도 , 완료 직전에 실행한다 .구매 완료에 대한 정보 생성 리턴한다.

해당 상품에 따라 다른 처리를 하고 예외가 없다 가정하고 Complete를 보낸다 .

 

public void OnPurchaseFailed(Product product , PurchaseFailureReason reason)
{
	Debug.LogWarning("구매 실패 - {product.definition.id},{reason}");
}

=>실패시상품과 원인을 알려준다

 

여기까지 구현하면 인터페이스가 강제하는 부분에 대한 처리는 모두 끝났다 .

여기까지의 메서드는 구매의 시도 즉, 구매의 시도 자체에 대한 메서드가 필요하다 .

 

 [ 구매 매서드 구현 ]

//구매
public void Purchase(string productID)
{
	if(!IsInitialized)return;
    
    //해당 아이디의 상품을 반환
    var product =storeController.peouducts.WithID(productId);
    //Consumable은 영수증이 없다 ..!등록 아이템은 영수증 있음 .
    
    //상품이 있고 , 구매가 가능하다면
    if(product !=null && product.availableToPurchase)
    {
    	Debug.Lg(message:$"구매시도 - {product.definition.id}");
        //구매 시동
        storeController.InitiatePurchase(product);
    }
    else
    {
    	Debug.Lg(message:$"구매시도불가 - {productid}");
    }
     
}

//이전 구매를 복구한다 . 안드로이드는 자동 복구지만 아이폰이라면 필수로 구현해야 한다 .
public void RestorePurchase()
{
	if(!IsInitialize)return;
    //앱스토어라면
    if(Application.platform == RuntimePlatform.IPhonePlayer || Application.platform.OSXPlayer)
    {
    	Debug.Log("구매 복구 시도");
        //확장 처리이기에 extension에서 처리 즉 ,여러 플랫폼 특유의 기능을 제공한다
        //인터페이스 타입으로 다뤄지는 익스텐션 오브젝트를 받음
        var appleExt = storeExtensionProvider.GetExtension<IAppleExtension>()
        //실행되는 순간 기존의 내역 복구 , 입력으로 복구 결과를 받는 콜백함수를 받음
        appleExt.RestoreTransitions(
        callback:result=>Debug.Log(message : $"구매 복구 시도 결과 -{result}"));
    }
}

//구매 완료 여부
public bool HadPurchased(string productId)
{
	if(!IsInitialized)return false;
    //Consumerable 상품은 레시피가 안가져와짐 (NonComsumable / Subscription만 나옴)
    var product = storeController.product.WirhID();
    if(product!=null)
    {
    	return product.hasReceipt;
    }
    return false;
}

 [ 버튼 이벤트로 구매하기 ]

//구매할 상품에 대한 ID
public string targetProductID;	

//구매 버튼에 등록할 이벤트
public void HandleClick()
{
	//만약 상품이 비소모품 / 구독형이라면 구매여부가 체크가 필요하다
    if(targetProductId == IAPManager.ProductCharacterSkin||
    targetProductID == IAPManager.ProductSubscriptioin)
    {
    	IAPManager.Instance.HadPurchased(targetProductID)
        {
        	Debug.Log("이미 구매함");
            return;
        }
    }
    
    //구매를 한다
    IAPManager.Instance.Purchase(targetProductID);
}

 

=>ios , android 라면 팝업이 뜨고 , 유니티의 동작이 정지 , 이후 구매 성공 혹은 실패에 대한 처리가 실행될것 .

 

출처

유니티 메뉴얼 IAP (더 디테일한듯?)

유니티 메뉴얼 - UnityIAP

유니티 Scripting API - IAP

 

고라니  TV - 인앱결제 _6분_

레트로 유튜브 - 유니티 인앱결제 (IAP) 한방에 개발하기 - iOS/Android (외부 플러그인 없이)

LlamAcademy - Cross-Platform IAP Store Page with Unity IAP in 2023 | Unity Tutorial

 

Boxwi 블로그 - Unity IAP: Unity In-App Purchasing requires Unity Gaming Services to have been initialized before use.