본문 바로가기
Unity • C#

[Unity] IAP 구매버튼 Custom Editor로 편하게 만들기

by SAENS 2023. 5. 21.

1. IAP Catalog

Unity IAP 패키지(com.unity.purchasing)에서는 IAP Catalog라는 JSON 형식의 파일을 통해 상품들을 관리합니다. 각 스토어에서 각 상품에 대응되는 id를 비롯한 여러 정보들이 이 IAP Catalog에 담겨져 있습니다.

패키지에서 기본적으로 제공되는 IAP Button 컴포넌트에서 다음과 같이 Title, Description, Price에 해당하는 Text 뷰를 할당해주면, 선택한 Product ID에 맞춰 IAP Catalog에 있는 상품 품목의 정보가 Text 뷰들에 할당됩니다.

그런데 안타깝게도 Image에 대해서는 수동으로 처리해야 하고, 애초에 저는 저 컴포넌트들을 지정해주는 것마저 귀찮았습니다.

그래서 Custom Editor를 이용하여 에디터에서 부모 오브젝트에 컴포넌트를 하나 만들어 IAP Catalog에 등록된 상품 id만 선택하면 자동으로 자식 오브젝트들을 탐색하여 제목, 가격 뿐 아니라 이미지까지 표시해주도록 하고 프리팹으로 만들었습니다. 결과물은 다음과 같습니다.

2. 가격 정책

Apple App store에서는 가격 책정을 할 때 Tier 시스템을 이용합니다. 다음 그림에서 볼 수 있듯이, Tier N은 (N-0.01)$ 입니다.

Google Play Store 에서는 가격 템플릿이라는 시스템을 사용하는(것이 권장되는)데, 개발자(또는 마케터?)가 직접 다음과 같이 가격 템플릿을 설정하고, 템플릿과 상품을 연결할 수 있습니다. 즉, 하나의 템플릿에 여러 상품이 연결하여, 여러 상품의 가격을 한 번에 변경할 수 있는 것이죠.

두 플랫폼에서 가격 정책을 통일시키는 것이 편할 것 같아서, 개인적으로 더 맘에 드는 Apple의 Tier 시스템으로 통일했습니다. Google에서는 아예 가격 템플릿의 이름을 'Tier N'으로 하고 그에 맞게 가격을 조정했습니다.

이걸 먼저 설명드리는 이유는, IAP 상품 구매 버튼 뷰 컴포넌트를 만들 때 가격 텍스트를 어떻게 표시할 것인지에 영향을 주기 때문입니다.

 

3. Apple Screenshot Path

Apple 앱스토어의 경우 각 IAP 상품에 대한 Screenshot이 있어야 합니다. 그리고 이 정보는 IAP Catalog에 해당 이미지 파일에 대한 path로 입력할 수 있습니다. 따라서 이미지에 대한 동적 처리는 이 screenshotPath를 이용해 처리해 줄 것입니다.

 

4. 오브젝트 구조

제가 원하는 레이아웃은 아래 왼쪽 그림과 같습니다. Button이라는 오브젝트에서는 VerticalLayoutGroup 컴포넌트를 통해 이미지, 타이틀, 가격 오브젝트들이 순서대로 알맞은 위치에 있도록 했습니다. IAPButton의 인앱구매 프로세스를 발생시키는 기능은 그대로 사용할 것이기 때문에, 여기에서 IAP 상품 ID에 따라 동적으로 변경이 필요한 컴포넌트의 타입은 IAPButton(Button) RawImage(Image), TMP_Text(Title), TMP_Text(Price) 입니다.

 

5. 코드

IAPItemView라는 클래스와, 해당 타입의 컴포넌트의 Custom Editor 클래스인 IAPItemViewEditor 클래스를 만들어 줄 것입니다.

using UnityEngine;

public class IAPItemView : MonoBehaviour
{
    [HideInInspector] public int productIdIndex = 0;
}

→ 프리팹의 가장 상위 오브젝트에 들어갈 컴포넌트인 IAPItemView는 어떤 인덱스의 상품을 선택했는지에 대한 정보인 productIdIndex를 가지고 있습니다. 아래에서 IAP ID를 선택하는 드롭다운 메뉴를 표시하는 부분에서 접근합니다.

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Purchasing;
using TMPro;

[CustomEditor(typeof(IAPItemView))]
public class IAPItemViewEditor : Editor
{
    IAPItemView view;

    // Getting product ID from IAP Catalog
    public string iapId;
    string[] productIDs;
    Dictionary<string, ProductCatalogItem> products;
    ProductCatalogItem product => products[iapId];

    // Components
    IAPButton iapButton;
    TMP_Text title;
    TMP_Text price;

    // Image Component
    string projectPath = "/Users/sanghunsong/DEV/Unity/MazeR/";
    RawImage image;


    void OnEnable()
    {
        InitComponents();
        InitProductsFromCatalog();
    }

    void InitComponents()
    {
        view = target as IAPItemView;

        iapButton = view.GetComponentInChildren<IAPButton>();
        image = iapButton.GetComponentInChildren<RawImage>();
        title = iapButton.transform.Find("Title").GetComponentInChildren<TMP_Text>();
        price = iapButton.transform.Find("Price").GetComponentInChildren<TMP_Text>();
    }

    void InitProductsFromCatalog()
    {
        ProductCatalog catalog = ProductCatalog.LoadDefaultCatalog();

        products = new Dictionary<string, ProductCatalogItem>();
        List<string> ids = new List<string>();
        foreach (var p in catalog.allProducts)
        {
            products.Add(p.id, p);
            ids.Add(p.id);
        }

        productIDs = ids.ToArray();
    }

    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        view.productIdIndex = EditorGUILayout.Popup("Product ID", view.productIdIndex, productIDs);
        iapId = productIDs[view.productIdIndex];
        SetView();
    }

    void SetView()
    {
        // Set Some Properties
        iapButton.productId = iapId;
        title.text = product.defaultDescription.Title;
        price.text = (product.applePriceTier * 1f - 0.01f).ToString() + "$";

        // Set Image
        int substrIndex = projectPath.Length;
        string screenshotPath = product.screenshotPath;
        string relPath = screenshotPath.Substring(substrIndex);
        Texture t = (Texture)AssetDatabase.LoadAssetAtPath(relPath, typeof(Texture));
        image.texture = t;

        // View Update
        EditorApplication.QueuePlayerLoopUpdate();
    }
}

 

01) InitProductsFromCatalog

가장 먼저, IAP Catalog에 접근한 뒤 productIDs에 모든 상품의 ID(string)을 저장합니다. 이는 인스펙터에서 ID를 선택할 수 있도록 하기 위한 변수입니다. 그리고 catalog의 allProducts에 접근하면 각각의 상품에 대한 정보인 ProductCatalogItem 객체를 얻을 수 있는데, ID로 쉽게 접근할 수 있도록 Dictionary에 저장해 둡니다. ProductCatalogItem 객체를 통해서 상품의 가격, 타이틀, 설명 등의 정보를 얻을 수 있습니다.

02) InitComponent

IAPItemView타입의 view를 포함해서 변경해줄 컴포넌트들을 자식오브젝트에서 찾아 지정해줍니다. MonoBehaviour와 Transform에서 제공되는 함수를 통해 찾는데, 이때 iapButton, image의 경우에는 해당되는 클래스가 자식 중에 하나만 있기 때문에 GetComponentInChildren<T>를 통해 얻었지만, TMP_Text 타입이 두 개 있으므로 title, price의 경우에는 각각 "Title", "Price"를 이름으로 하는 자식 오브젝트를 먼저 찾고 그 오브젝트에서 TMP_Text 컴포넌트를 얻었습니다.

03) EditorGUILayout.Popup(~) in OnInspectorGUI

이 메서드를 통해 01)에서 초기화된 productIDs를 드롭다운 형태로 선택하도록 할 수 있습니다. 선택되는 항목에 대한 인덱스를 반환하므로, view.의 해당 인덱스에 접근하여 iapId 변수에 저장합니다.

04) SetViews(), SetImageView()

01)과 03)에서 얻은 정보들을 바탕으로 뷰에 값을 할당해줍니다. 메서드를 나눈 이유는 Image의 경우 screeshotPath에 대한 추가적인 처리가 필요하기 때문입니다. LoadAssetAtPath(path)의 path에 절대경로(컴퓨터의 루트 폴더로부터 시작)가 아닌 상대경로(프로젝트 폴더로부터 시작)를 넣어줘야 하지만, IAP Catalog의 screenshotPath는 절대경로이기 때문에, screenshotPath에서 프로젝트 폴더의 절대경로에 해당하는 문자열을 제거해줘야 합니다. 상대경로를 통해 Texture 에셋을 불러오고, 해당 텍스처를 할당해줍니다.

 

이제 다음처럼 드롭다운 메뉴를 통해 IAP Catalog 내의 id를 선택하기만 하면, IAP Catalog에 입력된 대로 뷰에 값들이 할당됩니다.

 

댓글