cocos2d-x Android In App Billing Dungeons Sample Optimization and Intergration in External Jar Lib

 제목이 거창하네요. 제가 현재 회사에서 하고 있는 Framework는 살짝 어떤 내용인지 몇번 언급한 적이 있었죠. 대략적으로 현 상황을 정리하자면, iOS딴은 xcode에서 mm으로 해주면 c++이든 Objective-C든 간에 소스가 짬뽕이 되어도 컴파일이 되서 하나의 static lib로 결과물을 만들 수 있습니다.

 반면, Android에서는 예외가 발생하더군요. C++로 되어있는 Framework는 NDK를 통해 이상없이 컴파일이 되지만 최소한의 Java 코딩이 더 들어갑니다. 특히나 cocos2d-x를 활용한 순수 게임 코딩이 아닌 제가 지금것 포스팅했던 여러 외부 모듈들을 붙이다보면 어쩔수 없이 JNI와 Java딴 코딩이 들어가죠. 그동안은 cocos2d-x Android template로 자동 생성된 게임의 java파일 즉 GameMainActivity에 그냥 바로 샘플식으로 작업해왔지만 이렇게 하면 다른 팀원들과의 협업에 있어서 C++ lib와 java code 공유라는 반쪽짜리 Android Framework가 되어버리게 됩니다. C++로 된 것을 NDK로 컴파일한 Framework와 Java딴을 컴파일한 JAR static lib도 만들 수 밖에 없는 상황이 된거죠.

 서론이 길었습니다. 이번 포스팅에서는 지난번 안드로이드 인앱 빌링 연동을 했을 때 언급했던 Dungeons Sample 최적화부분과 게임 App이 아닌 따로 만든 안드로이드 외부 Lib JAR 프로젝트에 인앱 빌링 기능을 통합하는 것을 정리해봅니다. 대부분은 위 링크들을 기준으로 선 작업이 되어 있어야하고, 기타 수정부분만을 언급하도록 하겠습니다.

 먼저 링크를 참고하면서 안드로이드 외부 Lib 프로젝트에 구글 인앱 빌링 Dungeons Sample를 Import해줍니다. 단, 이번에는 play_billing 하위 폴더중 res는 빼고 src만 가져옵니다.

<activity android:name="com.example.dungeons.Dungeons"
               android:configChanges="orientation" >
        </activity>

 개발중인 App의 AndroidManifest.xml에서 Dungeons의 Activity는 삭제합니다. 더이상 Dungeons는 Activity가 아닌 일반 class 객체로 할 것이기 때문이죠.

 역시나 기존에 연동했을 때 가져왔던 Dungeons Sample의 Layout 관련 xml 4개도 모두 삭제합니다.

 아래는 최적화? 한 Dungeons class 입니다. 게임용에 맞게 Activity를 상속하지 않고 일반 class로 만들어봤습니다. Restore관련 db와 onRestoreTransactionsResponse 정도와 불필요해 보이는 dialog등을 삭제했고 구독 아이템 이외 타입의 아이템 정도만 구입할 수 있게 정리해봤습니다. JniMapper는 C++ Framework와 통신하기 위해 만든 Java측 Framework에 있는 class입니다. 별것은 없고 각 게임에 맞게 Thread Safe하게 JNI 통신 처리와 Dungeons class의 init, destroy 해주시면 됩니다.

package com.example.dungeons;


import com.example.dungeons.BillingService.RequestPurchase;
import com.example.dungeons.BillingService.RestoreTransactions;
import com.example.dungeons.Consts.PurchaseState;
import com.example.dungeons.Consts.ResponseCode;

import android.os.Handler;
import android.util.Log;

import xxxxx.framework.JniMapper;

/**
 * A sample application that demonstrates in-app billing.
 */
public class Dungeons {
    private static final String TAG = "Dungeons";

    private DungeonsPurchaseObserver mDungeonsPurchaseObserver;
    private Handler mHandler;
    private BillingService mBillingService;
    private boolean bBillingSupport = false;

    /**
     * A {@link PurchaseObserver} is used to get callbacks when Android Market sends
     * messages to this application so that we can update the UI.
     */
    private class DungeonsPurchaseObserver extends PurchaseObserver {
        public DungeonsPurchaseObserver(Handler handler) {
         super(JniMapper.getOwnerActivity(), handler);
        }

        @Override
        public void onBillingSupported(boolean supported, String type) {
            if (Consts.DEBUG) {
                Log.i(TAG, "supported: " + supported);
            }

            if (supported) {
             bBillingSupport = true;
            }
        }

        @Override
        public void onPurchaseStateChange(PurchaseState purchaseState, String itemId,
                int quantity, long purchaseTime, String developerPayload) {
            if (Consts.DEBUG) {
                Log.i(TAG, "onPurchaseStateChange() itemId: " + itemId + " " + purchaseState);
            }
         
            if (purchaseState == PurchaseState.PURCHASED) {
            }
        }

        @Override
        public void onRequestPurchaseResponse(RequestPurchase request,
                ResponseCode responseCode) {
            if (Consts.DEBUG) {
                Log.d(TAG, request.mProductId + ": " + responseCode);
            }
         
            final String cppResultCode;

            if (responseCode == ResponseCode.RESULT_OK) {
                if (Consts.DEBUG) {
                    Log.i(TAG, "purchase was successfully sent to server");
                }
                cppResultCode = request.mProductId;
            } else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
                if (Consts.DEBUG) {
                    Log.i(TAG, "user canceled purchase");
                }
                cppResultCode = "cancelIAP";
            } else {
                if (Consts.DEBUG) {
                    Log.i(TAG, "purchase failed");
                }
                cppResultCode = "faileIAP";
            }
            ///< JNI 호출로 C++에게 결과를 알려준다.
            JniMapper.callCppPurchaseResponse( cppResultCode );
        }

        @Override
        public void onRestoreTransactionsResponse(RestoreTransactions request,
                ResponseCode responseCode) {
            if (responseCode == ResponseCode.RESULT_OK) {
                if (Consts.DEBUG) {
                    Log.d(TAG, "completed RestoreTransactions request");
                }
                // Update the shared preferences so that we don't perform
                // a RestoreTransactions again.
                /*
                SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
                SharedPreferences.Editor edit = prefs.edit();
                edit.putBoolean(DB_INITIALIZED, true);
                edit.commit();
                */
            } else {
                if (Consts.DEBUG) {
                    Log.d(TAG, "RestoreTransactions error: " + responseCode);
                }
            }
        }
    }

    public void init() {
     mHandler = new Handler();
        mDungeonsPurchaseObserver = new DungeonsPurchaseObserver(mHandler);
        mBillingService = new BillingService();
        mBillingService.setContext(JniMapper.getOwnerActivity());

        // Check if billing is supported.
        ResponseHandler.register(mDungeonsPurchaseObserver);
        if (!mBillingService.checkBillingSupported()) {
         
        }
        /*
        if (!mBillingService.checkBillingSupported(Consts.ITEM_TYPE_SUBSCRIPTION)) {
            showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
        }
        */
    }
 
 public void destroy() {
  ResponseHandler.unregister(mDungeonsPurchaseObserver);
     mHandler = null;
     mDungeonsPurchaseObserver = null;
     mBillingService.unbind();
     mBillingService = null;
    }

    /**
     * 아이템 구매 요청
     * @param strProductId
     */
    public void requestPurchase( String strProductId ) {
     if (Consts.DEBUG) {
            Log.d(TAG, "requestPurchase: " + strProductId);
        }
     
     if( bBillingSupport == false )
     {
      ///< JNI 호출로 C++에게 구매 못한다고 알려준다.
         JniMapper.callCppPurchaseResponse( "faileIAP" );
         if (Consts.DEBUG) {
                Log.d(TAG, "requestPurchase: Bulling Not Support");
            }
         return;
     }
 
      //if( mManagedType != Managed.SUBSCRIPTION ) {
       if( !mBillingService.requestPurchase(strProductId, Consts.ITEM_TYPE_INAPP, null) ) {
        //showDialog(DIALOG_BILLING_NOT_SUPPORTED_ID);
       }
         //}
         ///< 구독
      /*
         else
         {
          if( !mBillingService.requestPurchase(strProductId, Consts.ITEM_TYPE_SUBSCRIPTION, null) ) {
           //showDialog(DIALOG_SUBSCRIPTIONS_NOT_SUPPORTED_ID);
          }
         }
         */
    }
}

 이제 작업했던 cocos2d-x c++ 부분을 NDK로 빌드하고 Java측 JAR Lib를 export하고 Android 게임 프로젝트에 다 적용하시면 이상없이 작동할 것입니다.

 모든 예외 상황을 처리한 것도 아니고 구현 안된 부분도 있으므로 아직 추가 개발에 여지는 남아있습니다.

댓글

이 블로그의 인기 게시물

CMake Windows에 설치하기

'xxx.exe' 프로그램을 시작할 수 없습니다. 지정된 파일을 찾을 수 없습니다.

크로스 스레드 작업이 잘못되었습니다. xxx 컨트롤이 자신이 만들어진 스레드가 아닌 스레드에서 액세스되었습니다