20230825更新

因为谷歌新政策要求,目前需要支持V5 V6,更新也很简单,将下面的改成5.0或6.0的aar即可

implementation("com.android.billingclient:billing:4.0.0")

改成

implementation("com.android.billingclient:billing:5.0.0")

implementation("com.android.billingclient:billing:6.0.0")

代码都是兼容的,亲测有效

-------------------------------------------------------------------

Unity接GooglePlay In-App Billing坑还是蛮多的,各种坑。

接的方式目前来看有三种:

  1. 采用Unity IAP插件,开启Unity的IAP Service
  2. 采用Android源生接入,在Android Studio接入,然后打包出jar或aar放到Unity项目,使用Unity调用
  3. 打包出Android工程,在Android工程中接入

这次介绍的是第二种,使用安卓源生方式接入,因为该方式一劳永逸,新项目可以很快就完成接入。

为什么不用第一种呢?直接导入IAP插件,然后设置参数,就可以很快实现了,该方式可以直接参考google文档

第一种IAP插件的缺点:

虽然该方式只需要导入插件,然后进行一些参数的设置,但是此方式特别麻烦的一点是需要在Unity中开启Service,但是开启后又得填一大堆信息,巨麻烦无比,而且Unity的网络简直不能看,要么是打不开,要么是卡半死,而且网站老变,以及没有完整的文档。这些都是很恶心人的事情,甚至还要创建组织啥的,反正谁用谁恶心。

----------------------------------分割线------------------------------------------------------------------------

正式接入

接入之前需要的储备知识是:Unity如何与Android交互

准备jar和aar

我们的目标是导出自己封装的jar(里面封装了接口供Unity调用,也就是桥接层),以及找到谷歌官方提供的Billing V4插件(aar)。

因为从2021.8.2起,谷歌要求必须接入V3版以上的插件。所以我们这次干脆接了最新的V4.

接入文档可以参考指南:从 AIDL 迁移到 Google Play 结算库的迁移指南

获取aar文件

  • 打开Android Studio,创建一个工程
  • 创建一个Module,创建完如下(Module要选择Library)
  • 打开build.gradle,修改导出为Library,在dependencies中添加依赖,并同步。
    implementation("com.android.billingclient:billing:4.0.0")
    注意:plugins这边要改为'com.android.library',导出才会是aar,否则是apk.
    
    plugins {
        id 'com.android.library'
    }
    
    android {
        compileSdkVersion 30
    
        defaultConfig {
            //applicationId "com.egogame.iapforgoogleplay"
            minSdkVersion 19
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    
    dependencies {
    
        implementation 'androidx.appcompat:appcompat:1.3.0'
        implementation 'com.google.android.material:material:1.3.0'
        implementation 'org.jetbrains:annotations:15.0'
        testImplementation 'junit:junit:4.+'
        androidTestImplementation 'androidx.test.ext:junit:1.1.2'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    
        implementation("com.android.billingclient:billing:4.0.0")
    }
  • 同步完成后,在gradle缓存路径下找到谷歌官方提供的.aar文件,复制到Unity工程Assets/Plugins/Android目录下,待用
    文件路径:C:\Users\xxx\.gradle\caches\modules-2\files-2.1\com.android.billingclient\billing\4.0.0\31aa58e2d4286f6b96480764e7a84d5de9935f02

打包jar 

  • 创建BaseMainActivity.java类,该类创建了一些供Unity调用的接口格式。
    当前类还未开始接入GP插件接口。
    package com.egogame;
    
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.util.ArrayList;
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    
    import android.app.Activity;
    import android.content.Context;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Looper;
    import android.widget.Toast;
    
    
    public class BaseMainActivity{
    	public static String UNITY_GO_NAME="IAP";
    	public static final String LOG_TAG = "EgoGameLog";
    
    	protected Handler uiHandler = new Handler(Looper.getMainLooper());
    
    	//unity项目启动时的上下文
    	private Activity unityActivity;
    	private Context context;
        
        public void Init(final String goName,final String googlePlayPublicKey){
    		PrintLog("Init:"+goName+"===="+googlePlayPublicKey);
    		uiHandler.post(new Runnable() {
    
    			@Override
    			public void run() {
    				UNITY_GO_NAME=goName;
    				OnInitHandle(googlePlayPublicKey);
    			}
    		});
    	}
        
        public void PrintLog(final String message,final Boolean toast){
    		android.util.Log.d(LOG_TAG, message);
        	uiHandler.post(new Runnable() {
    			@Override
    			public void run() {
    				if(toast) Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
    			}
    		});
    	}
    	
        public boolean IsIAPSupported(){
    		return false;
    	}
    
    	final public void RequstProduct(final String idsJson){
    		uiHandler.post(new Runnable() {
    
    			@Override
    			public void run() {
    				String[] realProducts=null;
    				try {
    					JSONObject jObject=new JSONObject(idsJson);
    					JSONArray jArray=jObject.getJSONArray("productIds");
    					realProducts=new String[jArray.length()];
    					for (int i = 0; i < jArray.length(); i++) {
    						realProducts[i]=jArray.getString(i);
    					}
    				} catch (Exception e) {
    					PrintLog("RequstProduct数据传输错误:"+e.getMessage());
    				}
    				if(realProducts!=null){
    					OnRequstProduct(realProducts);
    				}else{
    					RequestProductsFail("数据解析错误:"+idsJson);
    				}
    			}
    		});
    	}
    
    	final public void BuyProduct(final String productJson){
    		uiHandler.post(new Runnable() {
    
    			@Override
    			public void run() {
    				try {
    					JSONObject jObject=new JSONObject(productJson);
    					String productId=jObject.getString("productId");
    					boolean isConsumable=jObject.getBoolean("isConsumable");
    					
    					OnBuyProduct(productId,isConsumable);
    				} catch (Exception e) {
    					PrintLog("BuyProduct数据传输错误:"+e.getMessage());
    				}
    			}
    		});
    	}
    	
    	protected void OnInitHandle(String googlePlayPublicKey){
    	}
    
    	protected void OnRequstProduct(String[] productId){
    	}
    	protected void OnBuyProduct(String productId,boolean isConsumable){
    	}
    	
    	protected void BuyComplete(String productId){
    		PrintLog("购买成功:"+productId);
    		SendUnityMessage("ProductBuyComplete", productId);
    	}
    	
    	protected void BuyCancle(String productId){
    		PrintLog("购买取消:"+productId);
    		SendUnityMessage("ProductBuyCancled", productId);
    	}
    	
    	protected void BuyFail(String productId,String error){
    		PrintLog("购买失败:"+productId+"原因:"+error);
    		try {
    			JSONObject jObject=new JSONObject();
    			jObject.put("productId", productId);
    			jObject.put("error", error);
    			SendUnityMessage("ProductBuyFailed", jObject.toString());
    		} catch (JSONException e) {
    			PrintLog("BuyFail数据错误:"+e.getMessage());
    		}
    	}
    	
    	protected void RequestProductsFail(String message){
    		SendUnityMessage("ProductRequestFail", message);
    	}
    	
    	protected void RecieveProductInfo(ArrayList<SkuItem> skuItems,ArrayList<String> invalidProductIds){
    		JSONObject jsonObject=new JSONObject();
    		try {
    			JSONArray skuArray=new JSONArray();
    			JSONObject tmpObj = null;
    			for (int i = 0; i < skuItems.size(); i++) {
    				SkuItem skuItem=skuItems.get(i);
    				tmpObj = new JSONObject();
    			    tmpObj.put("productId" , skuItem.productId);
    			    tmpObj.put("title" , skuItem.title);
    			    tmpObj.put("desc" , skuItem.desc);
    			    tmpObj.put("price" , skuItem.price);
    			    tmpObj.put("formatPrice" , skuItem.formatPrice);
    			    tmpObj.put("priceCurrencyCode" , skuItem.priceCurrencyCode);
    			    tmpObj.put("skuType" , skuItem.skuType);
    			    skuArray.put(tmpObj);
    			}
    			
    			JSONArray invalidArray=new JSONArray();
    			for (int i = 0; i < invalidProductIds.size(); i++) {
    				invalidArray.put(invalidProductIds.get(i));
    			}
    			jsonObject.put("skuItems", skuArray);
    			jsonObject.put("invalidIds", invalidArray);
    		} catch (JSONException e) {
    			PrintLog("Json数据错误:"+e.getMessage());
    		}
    		String info=jsonObject.toString();
    		PrintLog("当前产品信息:"+info);
    		SendUnityMessage("RecieveProductInfos", info);
    	}
        
    	public void PrintLog(String message){
    		PrintLog(message,false);
    	}
    	
    	public void SendUnityMessage(String func,String value){
    		CallUnity(UNITY_GO_NAME, func, value);
    	}
    
    	public Activity CurrentActivity(){
    		return getActivity();
    	}
    
    	/**
    	 * Android调用Unity的方法
    	 * @param gameObjectName    调用的GameObject的名称
    	 * @param functionName      方法名
    	 * @param args              参数
    	 * @return                  调用是否成功
    	 */
    	boolean CallUnity(String gameObjectName, String functionName, String args){
    		try {
    			Class<?> classtype = Class.forName("com.unity3d.player.UnityPlayer");
    			Method method =classtype.getMethod("UnitySendMessage", String.class,String.class,String.class);
    			method.invoke(classtype,gameObjectName,functionName,args);
    			return true;
    		} catch (ClassNotFoundException e) {
    			System.out.println(e.getMessage());
    		} catch (NoSuchMethodException e) {
    			System.out.println(e.getMessage());
    		} catch (IllegalAccessException e) {
    			System.out.println(e.getMessage());
    		} catch (InvocationTargetException e) {
    
    		}
    		return false;
    	}
    
    	/**
    	 * 利用反射机制获取unity项目的上下文
    	 * @return
    	 */
    	Activity getActivity(){
    		if(null == unityActivity) {
    			try {
    				Class<?> classtype = Class.forName("com.unity3d.player.UnityPlayer");
    				Activity activity = (Activity) classtype.getDeclaredField("currentActivity").get(classtype);
    				unityActivity = activity;
    				context = activity;
    			} catch (ClassNotFoundException e) {
    				System.out.println(e.getMessage());
    			} catch (IllegalAccessException e) {
    				System.out.println(e.getMessage());
    			} catch (NoSuchFieldException e) {
    				System.out.println(e.getMessage());
    			}
    		}
    		return unityActivity;
    	}
    }
    
    
    package com.egogame;
    
    public class SkuItem {
    	public String productId;
    	public String title;
    	public String desc;
    	public String price;
    	public String formatPrice;//格式化价格,包括其货币符号
    	public String priceCurrencyCode;//货币代码
    	public String skuType;//内购还是订阅
    }
    
  • 创建MainActivity类,开始接入billing插件(该命名为Activity其实没有继承Activity,因为继承Activity后,需要Unity那边AndroidManfiest文件指定为Activity才能生效,所以这里不采用继承Activity)
    package com.egogame;
    
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Looper;
    import android.text.TextUtils;
    
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    
    import com.android.billingclient.api.BillingClient;
    import com.android.billingclient.api.BillingClientStateListener;
    import com.android.billingclient.api.BillingFlowParams;
    import com.android.billingclient.api.BillingResult;
    import com.android.billingclient.api.ConsumeParams;
    import com.android.billingclient.api.Purchase;
    import com.android.billingclient.api.PurchasesUpdatedListener;
    import com.android.billingclient.api.SkuDetails;
    import com.android.billingclient.api.SkuDetailsParams;
    import com.android.billingclient.api.SkuDetailsResponseListener;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class MainActivity extends BaseMainActivity implements PurchasesUpdatedListener,BillingClientStateListener {
        private static final long RECONNECT_TIMER_START_MILLISECONDS = 1L * 1000L;
        private static final long RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L; // 15 mins
    
        private static final Handler handler = new Handler(Looper.getMainLooper());
        private BillingClient billingClient;
        private String[] cacheRequestList;
        private Map<String, SkuDetails> skuDetailsLiveDataMap=new HashMap<>();
        private boolean isConsumable;
        private String buyProductId;
        private boolean billingSetupComplete = false;
        // how long before the data source tries to reconnect to Google play
        private long reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS;
    
        @Override
        protected void OnInitHandle(String googlePlayPublicKey) {
            super.OnInitHandle(googlePlayPublicKey);
    
            if (googlePlayPublicKey.contains("CONSTRUCT_YOUR")) {
                throw new RuntimeException("Please put your app's public key in MainActivity.java. See README.");
            }
            billingClient = BillingClient.newBuilder(CurrentActivity()).setListener(this).enablePendingPurchases().build();
            billingClient.startConnection(this);
        }
    
        private void retryBillingServiceConnectionWithExponentialBackoff() {
            handler.postDelayed(() ->
                            billingClient.startConnection(this),
                    reconnectMilliseconds);
            reconnectMilliseconds = Math.min(reconnectMilliseconds * 2,
                    RECONNECT_TIMER_MAX_TIME_MILLISECONDS);
        }
    
        @Override
        protected void OnRequstProduct(String[] productId) {
            super.OnRequstProduct(productId);
    
            List<String> skuList = new ArrayList<>();
            skuList.addAll(Arrays.asList(productId));
            cacheRequestList=productId;
            SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
            params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
            billingClient.querySkuDetailsAsync(params.build(),
                    new SkuDetailsResponseListener() {
                        @Override
                        public void onSkuDetailsResponse(BillingResult billingResult,
                                                         List<SkuDetails> skuDetailsList) {
                            int responseCode = billingResult.getResponseCode();
                            PrintLog("onSkuDetailsResponse:"+billingResult+" code:"+GetResponseText(responseCode));
    
                            switch (responseCode){
                                case BillingClient.BillingResponseCode.OK:
                                    RecieveProducts(skuDetailsList);
                                    break;
                                default:
                                    RequestProductsFail("Failed to query inventory: " + billingResult.getDebugMessage());
                                    PrintLog("Failed to query inventory: "+billingResult.getDebugMessage());
                                    break;
                            }
                        }
                    });
        }
    
        private String GetResponseText(int responseCode){
            switch (responseCode){
                case BillingClient.BillingResponseCode.OK:
                    return "OK";
                case BillingClient.BillingResponseCode.SERVICE_TIMEOUT:
                    return "SERVICE_TIMEOUT";
                case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
                    return "FEATURE_NOT_SUPPORTED";
                case BillingClient.BillingResponseCode.USER_CANCELED:
                    return "USER_CANCELED";
                case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
                    return "SERVICE_DISCONNECTED";
                case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
                    return "SERVICE_UNAVAILABLE";
                case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
                    return "BILLING_UNAVAILABLE";
                case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
                    return "ITEM_UNAVAILABLE";
                case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
                    return "DEVELOPER_ERROR";
                case BillingClient.BillingResponseCode.ERROR:
                    return "ERROR";
                case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
                    return "ITEM_ALREADY_OWNED";
                case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
                    return "ITEM_NOT_OWNED";
                default:
                    return "UnKnown";
            }
        }
    
        private void RecieveProducts(List<SkuDetails> skuDetailsList){
            ArrayList<SkuItem> skuItems=new ArrayList<SkuItem>();
            ArrayList<String> invaildIds=new ArrayList<String>();
            PrintLog("cacheRequestList:"+cacheRequestList);
            int length=cacheRequestList.length;
            if(cacheRequestList!=null && length>0){
                for(int i=0;i<length;i++){
                    String productId=cacheRequestList[i];
                    if(!TextUtils.isEmpty(productId)){
                        SkuDetails detail=null;
                        for (SkuDetails skuDetails : skuDetailsList) {
                            if(skuDetails.getSku().equals(productId)){
                                detail=skuDetails;
                                break;
                            }
                        }
    
                        if(detail==null){
                            PrintLog("未找到该产品信息:"+productId);
                            invaildIds.add(productId);
                            continue;
                        }
                        skuDetailsLiveDataMap.put(productId,detail);
    
                        String price=detail.getPrice();
                        String formatPrice=price;
    
                        SkuItem skuItem=new SkuItem();
                        skuItem.productId=productId;
                        skuItem.title=detail.getTitle();
                        skuItem.desc=detail.getDescription();
                        skuItem.price=price;
                        skuItem.formatPrice=formatPrice;
                        skuItem.priceCurrencyCode=detail.getPriceCurrencyCode();
                        skuItem.skuType=detail.getType();
    
                        skuItems.add(skuItem);
                    }
                }
            }
            RecieveProductInfo(skuItems,invaildIds);
        }
    
        @Override
        public boolean IsIAPSupported() {
            return true;
        }
    
        @Override
        protected void OnBuyProduct(String productId, boolean isConsumable) {
            super.OnBuyProduct(productId, isConsumable);
    
            SkuDetails skuDetails=skuDetailsLiveDataMap.get(productId);
            if(null!=skuDetails){
                buyProductId=productId;
                this.isConsumable=isConsumable;
    
                BillingFlowParams purchaseParams =
                        BillingFlowParams.newBuilder()
                                .setSkuDetails(skuDetails)
                                .build();
    
                billingClient.launchBillingFlow(CurrentActivity(), purchaseParams);
            }else{
                BuyFail(productId,"Can not find SkuDetails:"+productId);
                PrintLog("未请求商品数据,请先请求:"+productId);
            }
        }
    
        @Override
        public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
            int responseCode = billingResult.getResponseCode();
    
            PrintLog("BillingResult [" + GetResponseText(responseCode) + "]: "
                    + billingResult.getDebugMessage());
            switch (responseCode) {
                case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
                case BillingClient.BillingResponseCode.OK:
                    FlowFinish(true,null,list);
                    break;
                case BillingClient.BillingResponseCode.USER_CANCELED:
                    String productId=buyProductId;
                    buyProductId=null;
                    BuyCancle(productId);
                    break;
                default:
                    FlowFinish(false,billingResult.getDebugMessage(),list);
                    break;
            }
        }
    
        private void FlowFinish(Boolean isSuccess,String message,List<Purchase> purchases){
            if(isSuccess){
                if(buyProductId!=null){
                    String productId=buyProductId;
                    buyProductId=null;
                    String purchaseToken=null;
                    for (Purchase purchase : purchases) {
                        for (String skus : purchase.getSkus()) {
                            //需要校验付款状态
                            if(skus.contains(productId) &&
                                    purchase.getPurchaseState()==Purchase.PurchaseState.PURCHASED){
                                purchaseToken=purchase.getPurchaseToken();
                                break;
                            }
                            if(purchaseToken!=null) break;
                        }
                    }
    
                    if(isConsumable){
                        if(purchaseToken==null){
                            CallBackBuyFail("unknown purchaseToken:"+productId);
                        }else {
                            ConsumeParams consumeParams =
                                    ConsumeParams.newBuilder()
                                            .setPurchaseToken(purchaseToken)
                                            .build();
                            billingClient.consumeAsync(consumeParams,(billingResult, token) -> {
                                if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
                                    BuyComplete(productId);
                                }else{
                                    CallBackBuyFail(billingResult.getDebugMessage());
                                }
                            });
                        }
                    }else{
                        BuyComplete(productId);
                    }
                }
            }else{
                if(buyProductId!=null){
                    CallBackBuyFail(message);
                }
            }
        }
    
        private void CallBackBuyFail(String message){
            String productId=buyProductId;
            buyProductId=null;
            BuyFail(productId, message);
            PrintLog("Error purchasing: " + message);
        }
    
        @Override
        public void onBillingServiceDisconnected() {
            PrintLog("onBillingServiceDisconnected");
            billingSetupComplete = false;
            retryBillingServiceConnectionWithExponentialBackoff();
        }
    
        @Override
        public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
            int responseCode = billingResult.getResponseCode();
            String debugMessage = billingResult.getDebugMessage();
            PrintLog("onBillingSetupFinished: " + debugMessage+"("+ GetResponseText(responseCode)+")",true);
            switch (responseCode) {
                case BillingClient.BillingResponseCode.OK:
                    // The billing client is ready. You can query purchases here.
                    // This doesn't mean that your app is set up correctly in the console -- it just
                    // means that you have a connection to the Billing service.
                    reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS;
                    billingSetupComplete = true;
                    break;
                case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
                case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
                    PrintLog("Billing Service Unavailable:"+debugMessage,true);
                    break;
                default:
                    retryBillingServiceConnectionWithExponentialBackoff();
                    break;
            }
        }
    }
    
  • 选择Module,并且点击Build Module,将会导出aar文件
  • 我们将aar改为rar格式,然后用压缩软件打开,取出里面的classes.jar,我们只需要该jar即可,该jar是我们封装好的接口,后续有用,为了方便认,我将名字改为IAPForGooglePlay.jar
  • 将jar放置到Unity工程下,Assets/Plugins/Android目录下,该目录下文件如下:
    注意:jar要放在Android目录下,而不是Android/libs目录下,经试验放libs下会识别不到
  • Android这边的工作就结束了,后面我们来写Unity这边的代码

 编写Unity接口

  • 创建类GooglePlay_IAPBridge,编写调用Android我们封装好的jar里的接口的桥接类
    #if UNITY_ANDROID
    using System;
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    using Newtonsoft.Json;
    
    public class GooglePlay_IAPBridge{
    	class BuyProductData
    	{
    		public string productId;
    		public bool isConsumable;
    	}
    	private AndroidJavaObject javaObject;
    
    	private GooglePlay_IAPBridge() {
    		if (Application.platform != RuntimePlatform.Android)
    			return;
    		javaObject = new AndroidJavaObject("com.egogame.MainActivity");
    	}
    
    	private volatile static GooglePlay_IAPBridge _instance = null;
    	private static readonly object lockHelper = new object();
    	public static GooglePlay_IAPBridge getInstance()
    	{
    		if(_instance == null)
    		{
    			lock(lockHelper)
    			{
    				if(_instance == null)
    					_instance = new GooglePlay_IAPBridge();
    			}
    		}
    		return _instance;
    	}
    	public void Init(string goName,string publicKey){
    		Debug.Log ("[GooglePlay_IAPBridge]Init:" + goName + "=====" + publicKey);
    		if (Application.platform != RuntimePlatform.Android)
    			return;
    		javaObject.Call ("Init", goName, publicKey);
    	}
    
    	public bool IsIAPSupported(){
    		if (Application.platform != RuntimePlatform.Android)
    			return false;
    		return javaObject.Call<bool> ("IsIAPSupported");
    	}
    
    	public void RequestProducts(string jsonData)
    	{
    		Debug.Log ("[GooglePlay_IAPBridge]RequstProduct:" + jsonData);
    		if (Application.platform != RuntimePlatform.Android)
    			return;
    		javaObject.Call ("RequstProduct", jsonData);
    	}
    
    	public void BuyProduct(string productId,bool isConsumable){
    		BuyProductData buyProductData=new BuyProductData();
    		buyProductData.productId = productId;
    		buyProductData.isConsumable = isConsumable;
    		string jsonData = JsonConvert.SerializeObject(buyProductData);
    		Debug.Log ("[GooglePlay_IAPBridge]BuyProduct:" + jsonData);
    		if (Application.platform != RuntimePlatform.Android)
    			return;
    		javaObject.Call ("BuyProduct", jsonData);
    	}
    }
    #endif
    
    这里面我们直接 javaObject = new AndroidJavaObject("com.egogame.MainActivity");实例化我们jar封装好的类,即可直接调用public方法。请注意:因为Android和Unity线程不一样,所以jar处理时需要规避线程的同步性。
  • 再封装一个IAPBridge类,用来分流不同渠道,转接不同接口文件
    using UnityEngine;
    using System.Collections.Generic;
    using Newtonsoft.Json;
    
    public class IAPBridge{
    	class RequestSkuData
    	{
    		public string[] productIds;
    	}
    	
    	public static void InitIAp(string goName,string publicKey=""){
    		Debug.Log("[IAPBridge]InitIAp:" + goName + "==" + publicKey);
    #if UNITY_IPHONE
    		if(Application.platform==RuntimePlatform.IPhonePlayer){
    			iOS_IAPBridge.InitIAPManager(goName);
    		}
    #elif UNITY_ANDROID
    		if(Application.platform==RuntimePlatform.Android){
    			GooglePlay_IAPBridge.getInstance().Init(goName,publicKey);
    		}
    #endif
    	}
    
    	public static bool IAPEnabeld(){
    #if UNITY_IPHONE
    		if(Application.platform==RuntimePlatform.IPhonePlayer){
    			return iOS_IAPBridge.IsProductAvailable();
    		}
    #elif UNITY_ANDROID
    		if(Application.platform==RuntimePlatform.Android){
    			return GooglePlay_IAPBridge.getInstance().IsIAPSupported();
    		}
    #endif
    		return false;
    	}
    
    	public static void RequstProducts(List<string> productIds){
    		RequestSkuData data=new RequestSkuData();
    		data.productIds = productIds.ToArray();
    		string jsonData = JsonConvert.SerializeObject(data);
    		Debug.Log("[IAPBridge]RequstProducts:"+jsonData);
    #if UNITY_IPHONE
    		iOS_IAPBridge.RequstProductInfo(jsonData);
    #elif UNITY_ANDROID
    		if (Application.platform == RuntimePlatform.Android){
    			GooglePlay_IAPBridge.getInstance().RequestProducts(jsonData);
    		}
    #endif
    	}
    
    	public static void SendBuyProduct(string productId,bool isConsumable){
    		Debug.Log(string.Format("[IAPBridge]SendBuyProduct:{0} isConsumable:{1}",productId,isConsumable));
    #if UNITY_IPHONE
    		if(Application.platform==RuntimePlatform.IPhonePlayer){
    			iOS_IAPBridge.BuyProduct(productId);
    		}
    #elif UNITY_ANDROID
    		if (Application.platform == RuntimePlatform.Android){
    			GooglePlay_IAPBridge.getInstance().BuyProduct(productId, isConsumable);
    		}
    #endif
    	}
    
    	public static void RestoreProduct(){
    		Debug.Log("[IAPBridge]Restore!");
    #if UNITY_IPHONE
    		if(Application.platform==RuntimePlatform.IPhonePlayer){
    			iOS_IAPBridge.Restore();
    		}
    #endif
    	}
    }
    

  • 然后Unity这边要接收下Android那边传过来的接口,将这个类挂载到某个GameObject下,如GameObject名为IAPObject,则上面初始化时调用IAPBridge.InitIAP将该gameObject名作为goName参数传过去即可。
    using Newtonsoft.Json;
    using UnityEngine;
    #pragma warning disable 0649
    
    /// <summary>
    /// 该类主要用于接收iOS和Android回调,做一个桥接用途
    /// </summary>
    public class IAPMessage : MonoBehaviour {
    	class BuyFailData
    	{
    		public string productId;
    		public string error;
    	}
    	
    	#region callback from Objective-c/JAR
    	//获取到产品列表回调
    	void RecieveProductInfos(string jsonData)
    	{
    		if(string.IsNullOrEmpty(jsonData)) return;
    		var infoData = JsonConvert.DeserializeObject<IAPProductInfoData>(jsonData);
    		OnProductInfoReceived (infoData);
    	}
    
    	//产品列表请求失败
    	void ProductRequestFail(string message)
    	{
    		OnProductInfoFail(message);
    	}
    
    	//购买成功回调
    	void ProductBuyComplete(string productId)
    	{
    		OnProductBuyComplete(productId);
    	}
    	
    	//购买失败回调
    	void ProductBuyFailed(string jsonData)
    	{
    		var infoData = JsonConvert.DeserializeObject<BuyFailData>(jsonData);
    		OnBuyProductFail (infoData.productId, infoData.error);
    	}
    	
    	//获取商品回执回调
    	void ProvideContent(string msg){}
    
    	//购买取消回调
    	void ProductBuyCancled(string productId)
    	{
    		OnBuyProductCancled(productId);
    	}
    	
    	/// <summary>
    	/// 恢复购买成功
    	/// </summary>
    	/// <param name="productId"></param>
    	void RestoreComplete(string productId){
    		OnRestoreCompleted (productId);
    	}
    	#endregion
    	
    	//接收到产品信息
    	void OnProductInfoReceived(IAPProductInfoData info){
    		Debug.Log("[IAPMessage]Unity接收到商品信息:" + info.ToString());
    		IAPManager.internal_CallBySDK_ProductInfosReceive(info);
    	}
    	
    	//接收到产品信息
    	void OnProductInfoFail(string error){
    		Debug.Log("[IAPMessage]Unity商品信息请求失败:" + error);
    		IAPManager.internal_RequestProductInfoFail(error);
    	}
    	
    	//购买完成
    	void OnProductBuyComplete(string productId){
    		Debug.Log ("[IAPMessage]购买完成" + productId);
    		IAPManager.internal_CallBySDK_BuyComplete(productId);
    	}
    
    	//购买失败
    	void OnBuyProductFail(string productId,string error){
    		Debug.Log(string.Format("[IAPMessage]购买失败:{0} 错误信息{1}", productId, error));
    		IAPManager.internal_CallBySDK_BuyFail(productId, error);
    	}
    	
    	//购买取消
    	void OnBuyProductCancled(string productId){
    		Debug.Log ("[IAPMessage]购买取消" + productId);
    		IAPManager.internal_CallBySDK_BuyCanceled(productId);
    	}
    	
    	//恢复完成
    	void OnRestoreCompleted(string productId){
    		Debug.Log ("[IAPMessage]恢复购买完成:"+productId);
    		IAPManager.internal_CallBySDK_RestoreCompleted(productId);
    	}
    }
    
    #pragma warning restore 0649
  • using System.Collections.Generic;
    
    public class IAPProductInfoData
    {
        public List<IAPSkuItem> skuItems;//请求到的产品列表
        public string[] invalidIds;//无效产品id
    }
    
    public struct IAPSkuItem{
        public string productId;//后台产品id
        public string title;//后台标题
        public string desc;//后台描述
        public string price;//格式化价格,显示请用formatPrirce
        public string formatPrice;//格式化价格,包括其货币符号
        public string priceCurrencyCode;//货币代码
        public string skuType;//内购还是订阅 subscription/inapp
    
        public override string ToString ()
        {
            return string.Format(
                "[productId]:{0} [title]:{1} [desc]:{2} [price]:{3} [formatPrice]:{4} [priceCurrencyCode]:{5} [skuType:]{6}",
                productId, title, desc, price, formatPrice, priceCurrencyCode, skuType);
        }
    }
    
    public struct IAPProvideData
    {
        public string cfgId;
        public string title;
        public string desc;
        public string formatPrice;
    }

  • 到这里就接入完成了,调用对应接口即可实现IAP的接入。

 请注意:

*打包的apk不要对安卓代码进行混淆,否则代码会被裁剪导致调用不到java代码,如下

Minify和Custom Proguard File不要勾选

 如果接入有问题,可以加Q群进行提问

Android部分源码可以在这里下载到:https://download.csdn.net/download/egostudio/20463417 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐