다음을 통해 공유


Economy v2, Unity 및 Android 시작하기

Important

이제 Economy v2가 일반 공급됩니다. 지원 및 피드백을 받으려면 PlayFab 포럼으로 이동하세요.

이 자습서는 PlayFab, Unity + IAP 서비스 및 Android 과금 API를 사용하여 앱내 구매(IAP)를 설정하는 방법을 보여줍니다.

시작하기 전에

Android 청구 API 및 PlayFab은 함께 작동하여 클라이언트에 대한 IAP 환경을 제공합니다.

PlayFab Economy v2 - 상환 일정

PlayMarket을 통해 귀하의 제품 ID가격을 설정함으로써 시작하세요. 처음에 모든 제품은 플레이어가 구매할 수 있는 디지털 엔터티인 얼굴이 없는 것이지만 PlayFab 플레이어에게는 의미가 없습니다.

그러한 주체를 유용하게 만들려면 PlayFab 품목 카탈로그에서 미러링해야 합니다. PlayFab은 얼굴 없는 엔터티를 번들, 컨테이너 및 개별 항목으로 변환합니다.

각각 다음과 같은 자신의 독특한 얼굴을 갖고있습니다.

  • 타이틀
  • 설명
  • 태그
  • 유형
  • 이미지
  • 특성

모든 상품은 ID 공유를 통해 마켓 상품과 연동됩니다.

구매할 수 있는 실제 현금 항목에 액세스하는 가장 좋은 방법은 GetItems를 사용하는 것입니다.

품목의 ID가 PlayFab과 외부 IAP 시스템 사이의 링크입니다. 그러므로 우리는 품목 ID를 IAP 서비스로 전달합니다.

이 지점에서 구입 프로세스가 시작됩니다. 플레이어가 IAP 인터페이스와 상호 작용하여, 구입에 성공하면 귀하는 영수증을 받습니다.

PlayFab은 영수증을 확인하고 구매를 등록하여 방금 구매한 항목을 PlayFab 플레이어에게 부여합니다.

고객 애플리케이션 설정

이 섹션에서는 PlayFab, UnityIAP 및 Android Billing API를 사용하여 IAP를 테스트하도록 애플리케이션을 구성하는 방법을 보여줍니다.

필수 구성 요소:

  • Unity 프로젝트.
  • 귀하의 타이틀과 협력하도록 구성된 가져온 PlayFab Unity SDK.
  • Visual Studio 같은 편집기가 설치되고 Unity 프로젝트에서 작동하도록 구성되었습니다.

첫 번째 단계는 UnityIAP를 설정하는 것입니다:

  1. 서비스로 이동합니다.
  2. 서비스 탭이 선택되어 있는지 확인합니다.
  3. 귀하의 Unity 서비스 프로필 또는 조직을 선택합니다.
  4. 생성 버튼을 선택합니다.

UnityIAP 서비스 설정

  1. 다음, 앱내 구매(IAP) 서비스로 이동합니다.

UnityIAP 서비스로 이동

  1. 교차 플랫폼 IAP 간소화 토글을 설정하여 서비스를 활성화합니다.

  2. 이어서 계속 버튼을 선택합니다.

UnityIAP 서비스 활성화

플러그인 목록 페이지가 나타날 것입니다.

  1. 가져오기 버튼을 선택합니다.

UnityIAP 서비스 - 플러그인 가져오기

Unity 설치 및 가져오기 절차를 모든 플러그인을 가져온 시점까지 계속하십시오.

  1. 플러그인이 설치되었는지 확인합니다.
  2. 그런 다음 AndroidIAPExample.cs라는 새 스크립트를 만듭니다.

UnityIAP 새 스크립트 만들기

다음 코드를 포함하는 AndroidIAPExample.cs(자세한 설명은 코드 주석 참조)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Extension;

using PlayFab;
using PlayFab.ClientModels;
using PlayFab.EconomyModels;

/// <summary>
/// Unity behavior that implements the the Unity IAP Store interface.
/// Attach as an asset to your Scene.
/// </summary>
public class AndroidIAPExample : MonoBehaviour, IDetailedStoreListener
{
    // Bundles for sale on the Google Play Store.
    private Dictionary<string, PlayFab.EconomyModels.CatalogItem> _googlePlayCatalog;

    // In-game items for sale at the example vendor.
    private Dictionary<string, PlayFab.EconomyModels.CatalogItem> _storefrontCatalog;

    private string _purchaseIdempotencyId = null;

    private PlayFabEconomyAPIAsyncResult _lastAPICallResult = null;

    private static readonly PlayFabEconomyAPIAsync s_economyAPI = new();

    private static IStoreController s_storeController;

    // TODO: This callback is for illustrations purposes, you should create one that fits your needs
    public delegate void PlayFabProcessPurchaseCallback(PurchaseProcessingResult result);

    /// <summary>
    /// Event that is triggered when a purchase is processed.
    /// </summary>
    /// <remarks>
    /// TODO: Subscribe to this event in your game code to handle purchase results.
    /// </remarks>
    public event PlayFabProcessPurchaseCallback PlayFabProcessPurchaseEvent;

    /// <summary>
    /// True if the Store Controller, extensions, and Catalog are set.
    /// </summary>
    public bool IsInitialized => s_storeController != null
                             && _googlePlayCatalog != null
                             && _storefrontCatalog != null;

    // Start is called before the first frame update.
    public void Start()
    {
        Login();
    }

    /// <summary>
    /// Attempts to log the player in via the Android Device ID.
    /// </summary>
    private void Login()
    {
        // TODO: it is better to use LoginWithGooglePlayGamesService or a similar platform-specific login method for final game code.

        // SystemInfo.deviceUniqueIdentifier will prompt for permissions on newer devices.
        // Using a non-device specific GUID and saving to a local file
        // is a better approach. PlayFab does allow you to link multiple
        // Android device IDs to a single PlayFab account.
        PlayFabClientAPI.LoginWithCustomID(new LoginWithCustomIDRequest()
        {
            CreateAccount = true,
            CustomId = SystemInfo.deviceUniqueIdentifier
        }, result => RefreshIAPItems(), PlayFabSampleUtil.OnPlayFabError);
    }

    /// <summary>
    /// Queries the PlayFab Economy Catalog V2 for updated listings
    /// and then fills the local catalog objects.
    /// </summary>
    private async void RefreshIAPItems()
    {
        _googlePlayCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        SearchItemsRequest googlePlayCatalogRequest = new()
        {
            Count = 50,
            Filter = "AlternateIds/any(t: t/type eq 'GooglePlay')"
        };

        SearchItemsResponse googlePlayCatalogResponse;
        do
        {
            googlePlayCatalogResponse = await s_economyAPI.SearchItemsAsync(googlePlayCatalogRequest);
            Debug.Log("Search response: " + JsonUtility.ToJson(googlePlayCatalogResponse));

            foreach (PlayFab.EconomyModels.CatalogItem item in googlePlayCatalogResponse.Items)
            {
                _googlePlayCatalog.Add(item.Id, item);
            }

        } while (!string.IsNullOrEmpty(googlePlayCatalogResponse.ContinuationToken));

        Debug.Log($"Completed pulling from PlayFab Economy v2 googleplay Catalog: {_googlePlayCatalog.Count()} items retrieved");

        _storefrontCatalog = new Dictionary<string, PlayFab.EconomyModels.CatalogItem>();
        GetItemRequest storeCatalogRequest = new()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };

        GetItemResponse storeCatalogResponse;
        storeCatalogResponse = await s_economyAPI.GetItemAsync(storeCatalogRequest);
        List<string> itemIds = new();

        foreach (CatalogItemReference item in storeCatalogResponse.Item.ItemReferences)
        {
            itemIds.Add(item.Id);
        }

        GetItemsRequest itemsCatalogRequest = new()
        {
            Ids = itemIds
        };

        GetItemsResponse itemsCatalogResponse = await s_economyAPI.GetItemsAsync(itemsCatalogRequest);
        foreach (PlayFab.EconomyModels.CatalogItem item in itemsCatalogResponse.Items)
        {
            _storefrontCatalog.Add(item.Id, item);
        }

        Debug.Log($"Completed pulling from PlayFab Economy v2 villagerstore store: {_storefrontCatalog.Count()} items retrieved");

        InitializePurchasing();
    }

    /// <summary>
    /// Initializes the Unity IAP system for the Google Play Store.
    /// </summary>
    private void InitializePurchasing()
    {
        if (IsInitialized) return;

        var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance(AppStore.GooglePlay));

        foreach (PlayFab.EconomyModels.CatalogItem item in _googlePlayCatalog.Values)
        {
            string googlePlayItemId = item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay")?.Value;
            if (!string.IsNullOrWhiteSpace(googlePlayItemId))
            {
                builder.AddProduct(googlePlayItemId, ProductType.Consumable);
            }
        }

        UnityPurchasing.Initialize(this, builder);
    }

    /// <summary>
    /// Draw a debug IMGUI for testing examples.
    /// Use UI Toolkit for your production game runtime UI instead.
    /// </summary>
    public void OnGUI()
    {
        // Support high-res devices.
        GUI.matrix = Matrix4x4.TRS(new Vector3(0, 0, 0), Quaternion.identity, new Vector3(3, 3, 3));

        if (!IsInitialized)
        {
            GUILayout.Label("Initializing IAP and logging in...");
            return;
        }

        if (!string.IsNullOrEmpty(_purchaseIdempotencyId) && (!string.IsNullOrEmpty(_lastAPICallResult?.Message)
                                                           || !string.IsNullOrEmpty(_lastAPICallResult?.Error)))
        {
            GUILayout.Label(_lastAPICallResult?.Message + _lastAPICallResult?.Error);
        }

        GUILayout.Label("Shop for game currency bundles.");
        // Draw a purchase menu for each catalog item.
        foreach (PlayFab.EconomyModels.CatalogItem item in _googlePlayCatalog.Values)
        {
            // Use a dictionary to select the proper language.
            if (GUILayout.Button("Get " + (item.Title.ContainsKey("en-US") ? item.Title["en-US"] : item.Title["NEUTRAL"])))
            {
                BuyProductById(item.AlternateIds.FirstOrDefault(item => item.Type == "GooglePlay").Value);
            }
        }

        GUILayout.Label("Hmmm. (Translation: Welcome to my humble Villager store.)");
        // Draw a purchase menu for each catalog item.
        foreach (PlayFab.EconomyModels.CatalogItem item in _storefrontCatalog.Values)
        {
            // Use a dictionary to select the proper language.
            if (GUILayout.Button("Buy "
                + (item.Title.ContainsKey("en-US") ? item.Title["en-US"] : item.Title["NEUTRAL"]
                + ": "
                + item.PriceOptions.Prices.FirstOrDefault().Amounts.FirstOrDefault().Amount.ToString()
                + " Diamonds"
                )))
            {
                Task.Run(() => PlayFabPurchaseItemById(item.Id));
            }
        }
    }

    /// <summary>
    /// Integrates game purchasing with the Unity IAP API.
    /// </summary>
    public void BuyProductById(string productId)
    {
        if (!IsInitialized)
        {
            Debug.LogError("IAP Service is not initialized!");
            return;
        }

        s_storeController.InitiatePurchase(productId);
    }

    /// <summary>
    /// Purchases a PlayFab inventory item by ID.
    /// See the <see cref="PlayFabEconomyAPIAsync"/> class for details on error handling
    /// and calling patterns.
    /// </summary>
    async public Task<bool> PlayFabPurchaseItemById(string itemId)
    {
        if (!IsInitialized)
        {
            Debug.LogError("IAP Service is not initialized!");
            return false;
        }

        _lastAPICallResult = new();

        Debug.Log("Player buying product " + itemId);

        if (string.IsNullOrEmpty(_purchaseIdempotencyId))
        {
            _purchaseIdempotencyId = Guid.NewGuid().ToString();
        }

        GetItemRequest getVillagerStoreRequest = new()
        {
            AlternateId = new CatalogAlternateId()
            {
                Type = "FriendlyId",
                Value = "villagerstore"
            }
        };

        GetItemResponse getStoreResponse = await s_economyAPI.GetItemAsync(getVillagerStoreRequest);
        if (getStoreResponse == null || string.IsNullOrEmpty(getStoreResponse?.Item?.Id))
        {
            _lastAPICallResult.Error = "Unable to contact the store. Check your internet connection and try again in a few minutes.";
            return false;
        }

        CatalogPriceAmount price = _storefrontCatalog.FirstOrDefault(item => item.Key == itemId).Value.PriceOptions.Prices.FirstOrDefault().Amounts.FirstOrDefault();
        PurchaseInventoryItemsRequest purchaseInventoryItemsRequest = new()
        {
            Amount = 1,
            Item = new InventoryItemReference()
            {
                Id = itemId
            },
            PriceAmounts = new List<PurchasePriceAmount>
            {
                new()
                {
                    Amount = price.Amount,
                    ItemId = price.ItemId
                }
            },
            IdempotencyId = _purchaseIdempotencyId,
            StoreId = getStoreResponse.Item.Id
        };

        PurchaseInventoryItemsResponse purchaseInventoryItemsResponse = await s_economyAPI.PurchaseInventoryItemsAsync(purchaseInventoryItemsRequest);
        if (purchaseInventoryItemsResponse == null || purchaseInventoryItemsResponse?.TransactionIds.Count < 1)
        {
            _lastAPICallResult.Error = "Unable to purchase. Try again in a few minutes.";
            return false;
        }

        _purchaseIdempotencyId = "";
        _lastAPICallResult.Message = "Purchasing!";
        return true;
    }

    private void OnRegistration(LoginResult result)
    {
        PlayFabSettings.staticPlayer.ClientSessionTicket = result.SessionTicket;
    }

    public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    {
        s_storeController = controller;

        extensions.GetExtension<IGooglePlayStoreExtensions>().RestoreTransactions((result, error) => {
            if (result)
            {
                Debug.LogWarning("Restore transactions succeeded.");
            }
            else
            {
                Debug.LogWarning("Restore transactions failed.");
            }
        });
    }

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

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

    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureReason failureReason)
    {
        Debug.Log($"OnPurchaseFailed: FAIL. Product: '{product.definition.storeSpecificId}', PurchaseFailureReason: {failureReason}");
    }

    public void OnPurchaseFailed(UnityEngine.Purchasing.Product product, PurchaseFailureDescription failureDescription)
    {
        Debug.Log($"OnPurchaseFailed: FAIL. Product: '{product.definition.storeSpecificId}', PurchaseFailureReason: {failureDescription}");
    }

    /// <summary>
    /// Callback for Store purchases. Subscribe to PlayFabProcessPurchaseEvent to handle the final PurchaseProcessingResult.
    /// <see href="https://docs.unity3d.com/Packages/com.unity.purchasing@4.8/api/UnityEngine.Purchasing.PurchaseProcessingResult.html"/>
    /// </summary>
    /// <remarks>
    /// This code does not account for purchases that were pending and are
    /// delivered on application start. Production code should account for these cases.
    /// </remarks>
    /// <returns>Complete immediately upon error. Pending if PlayFab Economy is handling final processing and will trigger PlayFabProcessPurchaseEvent with the final result.</returns>
    public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    {
        if (!IsInitialized)
        {
            Debug.LogWarning("Not initialized. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        if (purchaseEvent.purchasedProduct == null)
        {
            Debug.LogWarning("Attempted to process purchase with unknown product. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        if (string.IsNullOrEmpty(purchaseEvent.purchasedProduct.receipt))
        {
            Debug.LogWarning("Attempted to process purchase with no receipt. Ignoring.");
            return PurchaseProcessingResult.Complete;
        }

        Debug.Log("Attempting purchase with receipt " + purchaseEvent.purchasedProduct.receipt);
        GooglePurchase purchasePayload = GooglePurchase.FromJson(purchaseEvent.purchasedProduct.receipt);
        RedeemGooglePlayInventoryItemsRequest request = new()
        {
            Purchases = new List<GooglePlayProductPurchase>
            {
                new()
                {
                    ProductId = purchasePayload.PayloadData?.JsonData?.productId,
                    Token = purchasePayload.PayloadData?.JsonData?.purchaseToken
                }
            }
        };

        PlayFabEconomyAPI.RedeemGooglePlayInventoryItems(request, result =>
        {
            Debug.Log("Processed receipt validation.");

            if (result?.Failed.Count > 0)
            {
                Debug.Log($"Validation failed for {result.Failed.Count} receipts.");
                Debug.Log(JsonUtility.ToJson(result.Failed));
                PlayFabProcessPurchaseEvent?.Invoke(PurchaseProcessingResult.Pending);
            }
            else
            {
                Debug.Log("Validation succeeded!");
                PlayFabProcessPurchaseEvent?.Invoke(PurchaseProcessingResult.Complete);
                s_storeController.ConfirmPendingPurchase(purchaseEvent.purchasedProduct);
                Debug.Log("Confirmed purchase with Google Marketplace.");
            }
        },
        PlayFabSampleUtil.OnPlayFabError);

        return PurchaseProcessingResult.Pending;
    }
}

/// <summary>
/// Utility classes for the sample.
/// </summary>
public class PlayFabEconomyAPIAsyncResult
{
    public string Error { get; set; } = null;

    public string Message { get; set; } = null;
}

public static class PlayFabSampleUtil
{
    public static void OnPlayFabError(PlayFabError error)
    {
        Debug.LogError(error.GenerateErrorReport());
    }
}

/// <summary>
/// Example Async wrapper for PlayFab API's.
/// 
/// This is just a quick sample for example purposes.
/// 
/// Write your own customer Logger implementation to log and handle errors
/// for user-facing scenarios. Use tags and map which PlayFab errors require your
/// game to handle GUI or gameplay updates vs which should be logged to crash and
/// error reporting services.
/// </summary>
public class PlayFabEconomyAPIAsync
{
    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/get-item"/>
    /// </summary>
    public Task<GetItemResponse> GetItemAsync(GetItemRequest request)
    {
        TaskCompletionSource<GetItemResponse> getItemAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItem(request, (response) => getItemAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            getItemAsyncTaskSource.SetResult(default);
        });
        return getItemAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/get-items"/>
    /// </summary>
    public Task<GetItemsResponse> GetItemsAsync(GetItemsRequest request)
    {
        TaskCompletionSource<GetItemsResponse> getItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.GetItems(request, (response) => getItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            getItemsAsyncTaskSource.SetResult(default);
        });
        return getItemsAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/inventory/purchase-inventory-items"/>
    /// </summary>
    public Task<PurchaseInventoryItemsResponse> PurchaseInventoryItemsAsync(PurchaseInventoryItemsRequest request)
    {
        TaskCompletionSource<PurchaseInventoryItemsResponse> purchaseInventoryItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.PurchaseInventoryItems(request, (response) => purchaseInventoryItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            purchaseInventoryItemsAsyncTaskSource.SetResult(default);
        });
        return purchaseInventoryItemsAsyncTaskSource.Task;
    }

    /// <summary>
    /// <see href="https://learn.microsoft.com/rest/api/playfab/economy/catalog/search-items"/>
    /// </summary>
    public Task<SearchItemsResponse> SearchItemsAsync(SearchItemsRequest request)
    {
        TaskCompletionSource<SearchItemsResponse> searchItemsAsyncTaskSource = new();
        PlayFabEconomyAPI.SearchItems(request, (response) => searchItemsAsyncTaskSource.SetResult(response), error => 
        {
            PlayFabSampleUtil.OnPlayFabError(error);
            searchItemsAsyncTaskSource.SetResult(default);
        });
        return searchItemsAsyncTaskSource.Task;
    }
}

[Serializable]
public class PurchaseJsonData
{
    public string orderId;
    public string packageName;
    public string productId;
    public string purchaseToken;
    public long   purchaseTime;
    public int    purchaseState;
}

[Serializable]
public class PurchasePayloadData
{
    public PurchaseJsonData JsonData;

    public string signature;
    public string json;

    public static PurchasePayloadData FromJson(string json)
    {
        var payload = JsonUtility.FromJson<PurchasePayloadData>(json);
        payload.JsonData = JsonUtility.FromJson<PurchaseJsonData>(payload.json);
        return payload;
    }
}

[Serializable]
public class GooglePurchase
{
    public PurchasePayloadData PayloadData;

    public string Store;
    public string TransactionID;
    public string Payload;

    public static GooglePurchase FromJson(string json)
    {
        var purchase = JsonUtility.FromJson<GooglePurchase>(json);

        // Only fake receipts are returned in Editor play.
        if (Application.isEditor)
        {
            return purchase;
        }

        purchase.PayloadData = PurchasePayloadData.FromJson(purchase.Payload);
        return purchase;
    }
}
  1. 코드로 불리는 새 GameObject를 만듭니다.
  2. AndroidIAPExample 구성 요소를 추가합니다(클릭 및 끌기 또는).
  3. 장면을 저장합니다.

UnityIAP 예 게임 개체 만들기

마지막으로 빌드 설정으로 이동합니다.

  1. 장면이 빌드의 장면 영역에 추가되었는지 확인합니다.
  2. Android 플랫폼을 선택합니다.
  3. 플레이어 설정 영역으로 이동합니다.
  4. 패키지 이름을 지정합니다.

참고 항목

PlayMarket 충돌을 피하기 위해 고유한 패키지 이름을 지으십시오.

UnityIAP 예 게임 개체 추가

마지막으로 평소와 같이 애플리케이션을 빌드하고 APK가 있는지 확인합니다.

테스트하려면 PlayMarket 및 PlayFab을 구성해야 합니다.

IAP용 PlayMarket 애플리케이션 설정

이 섹션은 PlayMarket 애플리케이션을 위한 IAP를 활성화하는 방법에 대해 설명합니다.

참고 항목

애플리케이션 자체를 설정하는 것은 이 자습서의 범위 밖입니다. 우리는 귀하가 어떤 애플리케이션을 이미 갖고 있고 그것이 적어도 알파 릴리스를 게시하도록 구성되어 있다고 간주합니다.

PlayMarket 애플리케이션 활성화

유용한 사항:

  • 그 지점에 도달하려면 APK가 업로드되어 있어야 합니다. 이전 섹션에서 구성한 APK를 사용합니다.
  • IAP 샌드박스를 사용하려면 APK를 알파 또는 베타 애플리케이션으로 업로드하세요.
  • 콘텐츠 등급 구성에는 애플리케이션에서 IAP가 어떻게 활성화되는지에 대한 질문이 포함될 것입니다.
  • PlayMarket은 게시자가 IAP를 사용하거나 테스트하는 것을 허용하지 않습니다. 테스트 목적으로 다른 Google 계정을 선택하고 알파/베타 빌드의 테스터로 추가합니다.
  1. 애플리케이션 빌드를 게시합니다.

  2. 메뉴에서 앱내 제품을 선택합니다.

    • 판매자 계정을 요청하면 계정을 연결하거나 만드세요.
  3. 새 제품 추가 버튼을 선택합니다.

    PlayMarket 새 제품 추가

  4. 새 제품 화면에서 관리되는 제품을 선택합니다.

  5. 100diamonds와(과) 같은 설명이 포함된 제품 ID를 지정합니다.

  6. 계속을 선택합니다.

    PlayMarket 제품 ID 추가

  7. PlayMarket을 사용하려면 타이틀(1)설명(2)(예: 100 DiamondsA pack of 100 diamonds to spend in-game)을 입력해야 합니다.

    데이터 항목 데이터는 PlayFab 서비스에서만 제공되며 일치하는 데 ID만 필요합니다.

    PlayMarket 제품 타이틀 설명 추가

  8. 더 멀리 스크롤하여 가격 추가 버튼을 선택합니다.

    PlayMarket 제품 가격 추가

  9. "$0.99"와 같은 유효한 가격을 입력하십시오(가격이 각 국가/지역을 위해 어떻게 독립적으로 변환되는지 주목하세요).

  10. 적용 버튼을 선택합니다.

    PlayMarket 제품 추가 현지 가격 적용

  11. 마지막으로 화면의 맨위로 되돌아가 품목의 상태를 활성으로 변경합니다.

    PlayMarket 제품을 활성으로 만들기

  12. 라이선스 키를 저장하여 PlayFab을 PlayMarket과 연결합니다.

  13. 메뉴에서 서비스 & API로 이동합니다.

  14. 그 다음 Base64 버전의 를 찾아 저장합니다.

PlayMarket 제품 라이선스 키 저장.

그 다음 단계는 IAP 테스트를 활성화하는 것입니다. 알파 및 베타 빌드를 위해 샌드박스가 자동으로 활성화되지만, 우리는 앱을 테스트할 권한이 있는 계정을 설정할 필요가 있습니다:

  1. 으로 이동합니다.
  2. 왼쪽 메뉴에서 계정 세부 정보를 찾아 선택합니다.
  3. 라이선스 테스트 영역을 찾습니다.
  4. 테스트 계정이 목록에 있는지 확인합니다.
  5. 라이선스 테스트 응답RESPOND_NORMALLY로 설정되어 있는지 확인합니다.

설정값 적용을 잊지 마세요!

PlayMarket IAP 테스트 활성화

이 시점에서 Play Market 쪽 통합을 확립해야 합니다.

PlayFab 타이틀 설정

마지막 단계는 우리 제품을 반영하는 PlayFab 타이틀을 구성하고, Google 과금 API와 통합하는 것입니다.

  1. 추가 기능을 선택합니다.
  2. 이어서 Google 추가 기능을 선택합니다.

PlayFab Google 추가 기능 열기

  1. 패키지 ID를 입력합니다.
  2. 이전 섹션에서 얻은 Google 앱 라이선스 키를 입력합니다.
  3. Google 설치 버튼을 선택하여 변경 사항을 적용합니다.

다음 단계는 100개의 다이아몬드 번들을 PlayFab에 반영하는 것입니다.

  1. 새 Economy Catalog(V2) 통화를 만듭니다.

  2. 타이틀을 편집하고 설명을 추가합니다(예: Diamonds, Our in-game currency of choice.).

  3. 사용자의 통화 diamonds을(를) 더 쉽게 찾을 수 있도록 식별 ID를 추가하세요.

  4. 저장 및 게시를 선택하여 변경을 완료합니다.

  5. 통화 목록에서 통화를 관찰합니다.

  6. 다음으로 새 Economy Catalog(V2) 번들을 만듭니다.

  7. 타이틀을 편집하고 설명을 추가합니다(예: 100 Diamonds Bundle, A pack of 100 diamonds to spend in-game.).

    {
        "NEUTRAL": "100 Diamonds Bundle",
        "en-US": "100 Diamonds Bundle",
        "en-GB": "100 Diamonds Bundle",
        "de-DE": "100 Diamantenbüschel"
    }
    

    참고 항목

    이 데이터는 Play 마켓 항목 타이틀설명관련이 없으며 독립적입니다.

  8. 콘텐츠 형식을 사용하여 번들을 구성할 수 있습니다(예: appstorebundles). 콘텐츠 형식은 ⚙️ > 타이틀 설정 > Economy(V2)에서 관리됩니다.

  9. 디스플레이 속성에 지역화된 가격을 추가하여 실제 가격을 추적합니다.

    {
        "prices": [
            "en-us": 0.99,
            "en-gb": 0.85,
            "de-de": 0.45
        ]
    }
    
  10. 번들에 새 항목을 추가합니다. 필터에서 통화를 선택하고 이전 집합에서 만든 통화를 선택합니다. 이 번들에서 판매하려는 통화 금액과 일치하도록 수량을 설정합니다.

  11. "GooglePlay" Marketplace에 대한 새 플랫폼을 추가합니다. GooglePlay Marketplace가 아직 없는 경우 Economy 설정 페이지에서 만들 수 있습니다. 이전 섹션에서 만든 Google Play 콘솔 제품 ID와 일치하도록 Marketplace ID를 설정합니다.

  12. 저장 및 게시를 선택하여 변경을 완료합니다.

  13. 번들 목록에서 번들을 관찰합니다.

다음으로, 플레이어가 인게임 NPC 공급업체를 나타내기 위해 PlayFab 스토어에서 통화를 사용할 수 있도록 인게임 구매를 설정할 수 있습니다.

  1. 새 Economy Catalog(V2) 항목을 만듭니다.
  2. 제목을 편집하고 설명을 추가하세요(예: "황금 검", "황금으로 만든 검").
  3. 지역화된 키워드를 추가하여 플레이어가 스토어에서 아이템을 찾는 데 도움을 줄 수 있습니다. API를 통해 나중에 검색할 수 있도록 항목을 구성하는 데 도움이 되는 태그 및 콘텐츠 형식을 추가합니다. 디스플레이 속성을 사용하여 아머(armor) 값, 예술 자산의 상대 경로 또는 게임에 저장해야 하는 기타 데이터와 같은 게임 데이터를 저장할 수 있습니다.
  4. 새 가격을 추가하고 이전 단계에서 만든 통화를 선택합니다. 금액은 기본으로 설정하려는 가격으로 설정합니다. 나중에 생성한 스토어에서 가격을 재정의할 수 있습니다.
  5. 저장 및 게시를 선택하여 변경을 완료합니다.
  6. 품목 목록의 품목을 확인합니다.
  7. 마지막으로 새 Economy Catalog(V2) 스토어를 만듭니다.
  8. 타이틀을 편집하고 설명을 추가합니다(예: Villager Store, A humble store run by a humble villager.).
  9. 식별 ID를 제공하여 검색을 더 쉽게 만듭니다(예: villagerstore).
  10. 이전 단계에서 만든 항목을 저장소에 추가합니다. 스토어에 여러 항목을 추가하고 필요한 경우 기본 가격을 재정의할 수 있습니다.
  11. 저장 및 게시를 선택하여 변경을 완료합니다.
  12. 스토어 목록에서 스토어를 관찰합니다.

PlayFab 타이틀 설정을 완료했습니다.

테스트

테스트 목적으로 알파/베타 버전을 사용하여 앱을 다운로드하십시오.

  • 테스트 계정과 진짜 Android 기기를 사용하십시오.
  • 앱을 시작하면 IAP가 초기화되고, 품목을 나타내는 한 버튼이 보여야 합니다.
  • 그 버튼을 선택합니다.

테스트 앱 - 다이아몬드 100개 구입 단추

IAP 구매가 시작됩니다. 구매에 성공하는 시점까지 Google Play 지침을 따릅니다.

앱 테스트 - Google Play - 결제 성공

마지막으로, PlayFab 게임 관리자 대시보드에서 귀하의 타이틀로 가서 새 이벤트를 찾습니다.

구매가 제공되고 검증되었으며 PlayFab 에코시스템으로 연결되었는지 확인합니다.

UnityIAP 및 Android Billing API를 PlayFab 애플리케이션에 성공적으로 통합했습니다!

다음 단계

  1. 데모 IMGUI 디스플레이를 대체하는 구매용 Unity UI 도구 키트 인터페이스를 빌드합니다.
  2. PlayFab 오류를 처리하고 사용자에게 표시하는 사용자 지정 Unity 로거를 만듭니다.
  3. Unity UI에 표시할 PlayFab 항목 이미지 필드에 아이콘 이미지를 추가합니다.