Google Play Store – 인앱 결제(IAP) 구현

서비스 활성화

IAP 서비스 설치

  1. 서비스 구현에 앞서, Unity 에서, IAP (In App Purchase) 기능을 활성화 한다.
  2. Windows > General > Services 메뉴로 접근
  3. Game Economy > In App Purchasing 을 선택하여 Install 을 진행한다.

Adaptive Performance 설치

IAP를 설치하고, 서비스를 활성화 하게 되면, 필수적으로 Adaptive Performance 모듈을 필요로 하게 되며 해당 모듈이 설치되어 있지 않으면, 빌드 과정에서 에러를 발생하게 된다.

아래 과정을 거쳐 해당 모듈도 설치를 진행한다.

  1. IAP 와 동일하게, Windows > General > Services 메뉴에서.
  2. All 탭을 선택한 후, Packages > Adaptive Performance 메뉴로 이동하여
  3. 해당 모듈을 설치하거나, Unlock 을 하여 활성화 해 준다.

Adaptive Performance – Samsung Android Provider 활성화

Adaptive Performance 가 활성화 되면, Samsung Android Provider도 활성화 시켜 줘야 한다.

  1. Edit > Project Settings 로 이동
  2. Adaptive Performance 메뉴로 이동하여, Android 탭에서, Samsung Android Provider 를 체크하여 활성화 함

Android 환경 설정

Adaptive Performance 를 사용하기 위해서는,

  1. Android OS: 7.0 이상 (Project Settings > Players >
  2. Scripting Backend: IL2CPP 로 설정되어 있어야 함

Allow downloads over HTTP 설정

Purchase 라이브러리 활성화 하는 과정 중에 HTTP로 라이브러리를 다운로드 하는 경우가 발생하므로, 에러를 방지하기 위해 해당 옵션을 Always allowed로 설정

프로젝트 서비스 설정

  1. 서비스를 활성화 하고, Edit > Project Settings 창을 연다.
  2. Services > In-App Purchasing 메뉴로 접근한다. (만약 해당 메뉴가 보이지 않는다면, 서비스 활성화를 통해 IAP 모듈을 설치를 다시 진행한다.)

Purchases 활성화

In-App Purchases 메뉴 우측의 활성화 Toggle Button을 클릭하여 모듈을 활성화 한다.

License key 입력

In-App Purchases 활성화 창에서, 하단 Receipt Obfuscator 부분에 라이선스 키를 입력

라이선스 키는 Play Store Console 의 Play를 통한 수익 창출 > 수익 창출 설정 메뉴 하단의 라이선스 에서 확인할 수 있다.

만약 해당 컨트롤을 통해 등록되지 않을 경우, Unity Cloud 를 통해 등록을 시도하고, 다시 시도함

구매 처리 클래스 구현

구매 처리 클래스 구현

구매 처리 클래스는 IStoreListener를 상속받는 클래스를 생성함

  1. IStoreController와 IExtensionProvider를 내부 변수로 생성

생성자

생성자 에서는, 구매될 제품의 정보를 구현한다.

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

builder.AddProduct(“제품명”, ProductType.Consumable, new IDs

{

        {“제품명_구글버전”, GooglePlay.Name},

        {“제품명_맥버전”, MacAppStore.Name}

}

UnityPurchsing.Initialize(this, builder);

OnInitialized 메소드 (IStoreListener)

해당 메소드에서는, 내부 변수들을 초기화 한다.

public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {

    this.controller = controller;

    this.extensions = extensions;

}

OnIntializeFailed 메소드 구현 (IStoreListener)

초기화 실패에 대한 처리 메소드 구현

public void OnInitializeFailed(InitializationFailureReason error) {

    … 에러 처리

}

public void OnInitializeFailed(InitializationFailureReason error, string message) {

    … 에러 처리

}

PrucessPurchase 메소드 구현 (IStoreListener)

구매 처리 메소드로 구매 결과를 리턴

public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs e) {

    // product Id

    string productId = e.purchasedProduct.definition.id;

    return PurchaseProcessingResult.Complete;

}

OnPurchaseFailed 메소드 구현 (IStoreListener)

구매 실패에 대한 처리 메소드 구현

public void OnPurchaseFailed(Product I, PurchaseFailureReason p) {

    … 에러 처리

}

Store 에 상품 등록

각 Store 상품 등록 절차에 맞춰 상품을 등록함

또한 등록된 아이디는 생성자 부분에 등록해 주어야 함

구매 처리 메소드 구현

public void Purchase(string productId) {

    Product product = controller.products.WithID(productId);

    if (product != null && product.availableToPurchase) {

        controller.InitiatePurchase(product);

    }

    else {

        … 구매 상품이 없음에 대한 에러 처리

    }

}

Unity – Plugin (Game Object) 구성

Game Object로 등록되는 클래스 생성 법

직접 적으로 Game Object로 등록할 수 있는 클래스 생성 법

MonoBehavior 생성

MonoBehavior를 상속받는 클래스를 생성하여, 해당 Object의 동작을 구현한다.

public class MySpecialObject : MonoBehavior
{
void Start() { … }
}

 

GameObject 메뉴 생성

MenuItem Attribute를 통해 클래스를 생성하여 등록 가능한 클래스를 생성한다.

[MenuItem(“메뉴 위치”, bool isValidateFunction, int priority)]

  1. 메뉴 위치 : 메뉴에서 표출될 위치로 GameObject를 가장 최 상위로 두고, 서브 메뉴로 접근 (GameObject/Sub Menu)
  2. isValidationFunction : 해당 메뉴가 호출되기 전에 해당 함수를 검증함
  3. priority : 메뉴의 우선순위를 설정함

public class MySpecialObjectCreator
{
[MenuItem(“GameObject/My Special Object”, false, 0)]
public static void CreateMySpecialObject()
{
// 새로운 게임 오브젝트 생성
GameObject newObject = new GameObject(“MySpecialObject”);

// MySpecialObject 컴포넌트 추가
newObject.AddComponent<MySpecialObject>();

// 새로 생성한 게임 오브젝트를 선택
Selection.activeGameObject = newObject;
}
}

 

하위 GameObject 추가

GameObject 하위에 자식 GameObject를 생성하려면, transform.parent를 통해 설정하면 된다. 코드는 아래와 같다.

GameObject parent = new GameObject(“TITLE”);
GameObject subObject = new GameObject(“TITLE”);
subObject.transform.parent = parent.transform;

Unity – Plugin (Menu & Setting Windows) 구성

개요

해당 방법을 통해 Unity 에서, 사용자 정의 메뉴를 추가하고, 데이터를 관리할 수 있는 인터페이스를 생성할 수 있음.

구성은 크게,

  1. 메뉴를 추가,
  2. 메뉴 선택 시, 띄울 화면 구성
  3. 메뉴에서 관리할 데이터의 관리 (저장하기, 불러오기)

로 구성된다.

 

메뉴 구성 방법

메뉴는 Static Function 에 MenuItem Attribute 를 통해 구성 가능하다.

  1. Static Method를 생성함

    public static void MyMenu() { … }

  2. Menu Item Annotation을 통해 해당 메뉴를 구성한다.

    [MenuItem(“MyMenu/ My Settings”)]

  3. 화면 표출 코드

    화면을 노출 시키기 위해 아래 코드를 추가함

     

    EditorWindow EditorWindow.GetWindow(System.Type windowType, bool utility, string title) (+ 12 overloads)

     

    MySettingWindow window = (MySettingWindow)GetWindow(typeof(MySettingWindow), false, “My Settings”);

[전체 코드]

[MenuItem(“MyMenu/My Settings”)]

public static void MyMenu() {

    MySettingWindow window = (MySettingWindow)GetWindow(typeof(MySettingWindow), false, “My Settings”);

}

 

화면 구성

띄워질 화면에서 구성할 요소들을 설정한다.

OnGUI() 메소드

해당 메소드는, 매 프레임 마다, 호출되는 메소드로, GUI 핸들링 기능을 제공하는 메소드

데이터 변수

데이터를 관리할 변수들을 추가한다.

private bool toggleValue;

 

화면에 요소를 노출하고, 변수에 대입

EditorGUILayout 메소드 들을 통해, 화면에 UI를 배치하고, 데이터를 대입한다. 해당 메소드는, 이름으로 지정된 인스턴스가 생성되어 있는지 확인하고, 생성되지 않았으면, 2번째 인자의 초기값을 통해 인스턴스를 생성하고 초기값을 그대로 리턴 한다. 만약 인스턴스가 존재하면, 해당 인스턴스의 현재 값을 리턴 한다.

toggle = EditorGUILayout.Toggle(“Enable Feature”, toggleValue);

 

Toggle

EditorGUILayout.Toggle(string title, bool value);

Silder

EditorGUILayout.Slider(string title, float value);

Button

버튼의 경우는, 클릭 되었을 경우 true를 리턴 하고, 그렇지 않으면, false를 리턴 한다.

if (GUILayout.Button(“Button Title”)) { … }

 

데이터 관리

설정된 데이터는 Unity가 재 시작 되면, 기존에 입력했던 데이터가 모두 초기화 된다. 해당 데이터는 다음과 같은 방법으로 저장 및 로드 할 수 있다.

데이터 저장

EditorPrefs.SetBool(“TITLE”, toggleValue);

EditorPrefs.SetFloat(“TITLE”, sliderValue);

데이터가 저장되는 TITLE 구분자는 유니크한 값이어야 한다. 그렇지 않아 중복될 경우, 이전 값이 덮어쓰여짐에 주의해야 함

데이터 로딩

toggleValue = EditorPrefs.GetBool(“TITLE”);

silderValue = EditorPrefs.GetFloat(“TITLE”);

구현 위치

데이터 로딩은, OnGUI() 에서, Window를 구성한 이후, 바로 호출해 주면, 화면이 구성되면서, 이전 데이터를 로딩할 수 있으며,

데이터 저장은, Apply 버튼을 구성하여 저장하는 방법(위에서 설명한 Button)이나, 아래와 같이 실시간으로 저장되도록 구성함

private void OnGUI() {

    …

    bool newToggleValue = EditorGUILayout.Toggle(“TITLE”, toggleValue);

    if (newToggleValue != toggleValue) {

        toggleValue = newToggleValue;

        ApplySettings();

    }

    …

}

다른 클래스나 컴포넌트에서 데이터 가져오기

Singleton Pattern을 통한 데이터 공유

Singleton Pattern의 클래스를 생성하여 데이터를 입력한 후, 다른 클래스나 컴포넌트에서 데이터를 호출할 수 있는 인터페이스를 제공한다.

public class SettingsManager : MonoBehavior
{
    public static SettingsManager Instance { get; set; }
    // … 관리될 변수 정의

    private void Awake() {
        if (Instance == null) {
            Instance = this;
            DontDestroyOnLoad(gameObject); // 씬 간 데이터 유지
        }
        else {
            Destroy(gameObject);
        }
    }
}

 

ScriptableObject

ScriptableObject 를 통해 데이터에 접근

아래와 같은 ScriptableObject 클래스를 생성하면, 해당 데이터는 Create Assets 메뉴를 통해 컴포넌트를 생성할 수 있고, 해당 Assets 은 단순히 Project 에 Asset 을 생성해 놓는 것 만으로도 인스턴스 화 되어 데이터에 접근할 수 있다. 또한 Assets으로 생성된 객체에 접근하여 데이터를 수정하면, 수정된 데이터가 파일로 저장되어 지속적으로 영속화 되어 프로젝트 재 시작 시에도 이전 설정된 데이터 값이 계속 유지된다.

이는 Object에 Component로 등록 되어야만 인스턴스 화 되는 MonoBehavior와는 다른 형태로 동작하며, Settings 데이터등을 관리하기에 최적화 되어 있는 클래스 타입으로 볼 수 있다.

[CreateAssetMenu(fileName = “SettingsData”, menuName = “Settings/SettingsData”, order = 1)]
public class SettingsData : ScriptableObject
{
public bool toggleValue = false;
public float sliderValue = 1.0f;
}

이 경우는 간혹 Windows 에서 변경한 데이터의 변경 사항을 감지하지 못하여 데이터가 저장되지 않는 경우가 발생한다. 이를 방지하기 위하여 GUI 에서, Changed를 감지하여 저장하는 로직을 추가함

if (GUI.changed) {
EditorUtility.SetDirty(settinsData);     // 데이터 변경을 강제로 설정
AssetDatabase.SaveAssets();        // Asset Database 에 데이터 저장 명령을 직접 호출
}

 

Static Object

전역 데이터로 변수를 생성하여 관리하는 방식으로, 추천하지 않는다.

Firebase Notification 연동

Firebase 활성화

Firebase 계정 및 프로젝트 생성

Firebase 계정 등록 절차에 따라 계정 및 프로젝트를 생성한다.

Firebase 구성 파일 추가

Firebase Console 을 통해 플랫폼별 Firebase 구성 파일을 가져와 Unity 프로젝트에 추가한다.

Android 의 경우

  1. google-services.json 다운로드를 클릭하여 파일을 다운로드 함
  2. 다운로드 한 파일을 Assets 폴더로 이동한다.

Android SDK Version

Android 의 경우, 필수적으로 SDK 24 이상을 사용해야 함.

Firebase Unity SDK 추가

  1. 아래 파일을 다운로드 함

    https://firebase.google.com/download/unity?hl=ko&_gl=1*1god004*_up*MQ..*_ga*MTk2MjQxMTE5My4xNzI4NTQwMDA0*_ga_CW55HF8NVT*MTcyODU0MDAwNC4xLjAuMTcyODU0MDAwNC4wLjAuMA..

  2. 압축 해제 후, 다음 패키지를 설치한다.
    1. Assets > Import Package > Custom Package 를 선택하거나,

      해당 패키지를 더블클릭하여 Import 창을 띄움

    2. FirebaseMessaging.unitypackage (클라우드 메시징용 패키지)

코드 작성

Handler 추가

Token Receive Handler 설정 코드 추가

FirebaseMessaging.TokenReceived += OnTokenReceived;

private void OnTokenReceived(object sender, TokenReceivedEventArgs e) { … }

 

Message Receive Handler 설정 코드 추가

FirebaseMessaging.MessageReceived += OnMessageReceived;

private void OnMessageReceived(object sender, MessageReceivedEventArgs e) { … }

 

메시지 초기화 설정

FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task => {

    var dependencyStatus = task.Result;

    if (dependencyStatus == DependencyStatus.Available) {

        // 설정 코드 추가

}

else {

    …

}

});

 

문제 해결

Crash Error 관련

특이하게, Android 35 버전으로 구동된 에뮬레이터에서는 Crash Error 가 발생한다. 해당 문제는 다른 버전을 사용하면 해결되는 것으로 보인다. 구체적인 문제 원인은 확인되지 않고 있다.

2024/10/16 14:34:18.174 1725 1725 Error CRASH *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

2024/10/16 14:34:18.175 1725 1725 Error CRASH Version ‘2022.3.50f1 (c3db7f8bf9b1)’, Build type ‘Development’, Scripting Backend ‘il2cpp’, CPU ‘arm64-v8a’

2024/10/16 14:34:18.175 1725 1725 Error CRASH Build fingerprint: ‘google/sdk_gphone16k_x86_64/emu64xa16k:15/AP31.240617.003/12088229:user/dev-keys’

2024/10/16 14:34:18.176 1725 1725 Error CRASH Revision: ‘0’

2024/10/16 14:34:18.176 1725 1725 Error CRASH ABI: ‘arm64’

2024/10/16 14:34:18.182 1725 1725 Error CRASH Timestamp: 2024-10-16 14:34:18.176553076+0900

2024/10/16 14:34:18.182 1725 1725 Error CRASH pid: 1725, tid: 1725, name: .gamesdk_client >>> com.gravity.gamesdk.client.gamesdk_client <<<

2024/10/16 14:34:18.182 1725 1725 Error CRASH uid: 10201

2024/10/16 14:34:18.183 1725 1725 Error CRASH signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr ——–

2024/10/16 14:34:18.185 1725 1725 Error CRASH x0 0000000000000001 x1 0000000000000000 x2 0000000000010006 x3 0000745433259e30

2024/10/16 14:34:18.186 1725 1725 Error CRASH x4 0000000000000008 x5 0000000000000000 x6 0000000000000010 x7 7f7f7f7f7f7f7f7f

2024/10/16 14:34:18.186 1725 1725 Error CRASH x8 61b222bc3e0efb77 x9 61b222bc3e0efb77 x10 0000000000000000 x11 0000000000000000

2024/10/16 14:34:18.186 1725 1725 Error CRASH x12 00000000ffffffbf x13 0000000000000020 x14 0000000000000400 x15 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x16 000074542deb7d88 x17 0000745434d6e968 x18 0000745431a5c000 x19 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x20 0000000000000000 x21 0000000000000000 x22 0000000000000000 x23 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x24 0000000000000000 x25 0000000000000000 x26 0000000000000000 x27 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH x28 0000000000000000 x29 0000000000000000

2024/10/16 14:34:18.187 1725 1725 Error CRASH lr 0000000000000000 sp 0000745433259ff0 pc 0000000000000000 pst 0000000000000000

2024/10/16 14:34:21.044 1725 1725 Error CRASH Forwarding signal 11

0001/01/01 00:00:00.000 -1 -1 Info ——— beginning of crash

2024/10/16 14:34:21.045 1725 1725 Fatal libc Fatal signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7454a0ac38ec in tid 1725 (.gamesdk_client), pid 1725 (.gamesdk_client)