当前位置:   article > 正文

Unity接入GooglePlay内购V4 V5 V6(源生Android方式)_unity接入google内购

unity接入google内购

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中添加依赖,并同步。
    1. implementation("com.android.billingclient:billing:4.0.0")
    2. 注意:plugins这边要改为'com.android.library',导出才会是aar,否则是apk.
    1. plugins {
    2. id 'com.android.library'
    3. }
    4. android {
    5. compileSdkVersion 30
    6. defaultConfig {
    7. //applicationId "com.egogame.iapforgoogleplay"
    8. minSdkVersion 19
    9. targetSdkVersion 30
    10. versionCode 1
    11. versionName "1.0"
    12. testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    13. }
    14. buildTypes {
    15. release {
    16. minifyEnabled false
    17. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    18. }
    19. }
    20. compileOptions {
    21. sourceCompatibility JavaVersion.VERSION_1_8
    22. targetCompatibility JavaVersion.VERSION_1_8
    23. }
    24. }
    25. dependencies {
    26. implementation 'androidx.appcompat:appcompat:1.3.0'
    27. implementation 'com.google.android.material:material:1.3.0'
    28. implementation 'org.jetbrains:annotations:15.0'
    29. testImplementation 'junit:junit:4.+'
    30. androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    31. androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    32. implementation("com.android.billingclient:billing:4.0.0")
    33. }
  • 同步完成后,在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插件接口。
    1. package com.egogame;
    2. import java.lang.reflect.InvocationTargetException;
    3. import java.lang.reflect.Method;
    4. import java.util.ArrayList;
    5. import org.json.JSONArray;
    6. import org.json.JSONException;
    7. import org.json.JSONObject;
    8. import android.app.Activity;
    9. import android.content.Context;
    10. import android.os.Bundle;
    11. import android.os.Handler;
    12. import android.os.Looper;
    13. import android.widget.Toast;
    14. public class BaseMainActivity{
    15. public static String UNITY_GO_NAME="IAP";
    16. public static final String LOG_TAG = "EgoGameLog";
    17. protected Handler uiHandler = new Handler(Looper.getMainLooper());
    18. //unity项目启动时的上下文
    19. private Activity unityActivity;
    20. private Context context;
    21. public void Init(final String goName,final String googlePlayPublicKey){
    22. PrintLog("Init:"+goName+"===="+googlePlayPublicKey);
    23. uiHandler.post(new Runnable() {
    24. @Override
    25. public void run() {
    26. UNITY_GO_NAME=goName;
    27. OnInitHandle(googlePlayPublicKey);
    28. }
    29. });
    30. }
    31. public void PrintLog(final String message,final Boolean toast){
    32. android.util.Log.d(LOG_TAG, message);
    33. uiHandler.post(new Runnable() {
    34. @Override
    35. public void run() {
    36. if(toast) Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
    37. }
    38. });
    39. }
    40. public boolean IsIAPSupported(){
    41. return false;
    42. }
    43. final public void RequstProduct(final String idsJson){
    44. uiHandler.post(new Runnable() {
    45. @Override
    46. public void run() {
    47. String[] realProducts=null;
    48. try {
    49. JSONObject jObject=new JSONObject(idsJson);
    50. JSONArray jArray=jObject.getJSONArray("productIds");
    51. realProducts=new String[jArray.length()];
    52. for (int i = 0; i < jArray.length(); i++) {
    53. realProducts[i]=jArray.getString(i);
    54. }
    55. } catch (Exception e) {
    56. PrintLog("RequstProduct数据传输错误:"+e.getMessage());
    57. }
    58. if(realProducts!=null){
    59. OnRequstProduct(realProducts);
    60. }else{
    61. RequestProductsFail("数据解析错误:"+idsJson);
    62. }
    63. }
    64. });
    65. }
    66. final public void BuyProduct(final String productJson){
    67. uiHandler.post(new Runnable() {
    68. @Override
    69. public void run() {
    70. try {
    71. JSONObject jObject=new JSONObject(productJson);
    72. String productId=jObject.getString("productId");
    73. boolean isConsumable=jObject.getBoolean("isConsumable");
    74. OnBuyProduct(productId,isConsumable);
    75. } catch (Exception e) {
    76. PrintLog("BuyProduct数据传输错误:"+e.getMessage());
    77. }
    78. }
    79. });
    80. }
    81. protected void OnInitHandle(String googlePlayPublicKey){
    82. }
    83. protected void OnRequstProduct(String[] productId){
    84. }
    85. protected void OnBuyProduct(String productId,boolean isConsumable){
    86. }
    87. protected void BuyComplete(String productId){
    88. PrintLog("购买成功:"+productId);
    89. SendUnityMessage("ProductBuyComplete", productId);
    90. }
    91. protected void BuyCancle(String productId){
    92. PrintLog("购买取消:"+productId);
    93. SendUnityMessage("ProductBuyCancled", productId);
    94. }
    95. protected void BuyFail(String productId,String error){
    96. PrintLog("购买失败:"+productId+"原因:"+error);
    97. try {
    98. JSONObject jObject=new JSONObject();
    99. jObject.put("productId", productId);
    100. jObject.put("error", error);
    101. SendUnityMessage("ProductBuyFailed", jObject.toString());
    102. } catch (JSONException e) {
    103. PrintLog("BuyFail数据错误:"+e.getMessage());
    104. }
    105. }
    106. protected void RequestProductsFail(String message){
    107. SendUnityMessage("ProductRequestFail", message);
    108. }
    109. protected void RecieveProductInfo(ArrayList<SkuItem> skuItems,ArrayList<String> invalidProductIds){
    110. JSONObject jsonObject=new JSONObject();
    111. try {
    112. JSONArray skuArray=new JSONArray();
    113. JSONObject tmpObj = null;
    114. for (int i = 0; i < skuItems.size(); i++) {
    115. SkuItem skuItem=skuItems.get(i);
    116. tmpObj = new JSONObject();
    117. tmpObj.put("productId" , skuItem.productId);
    118. tmpObj.put("title" , skuItem.title);
    119. tmpObj.put("desc" , skuItem.desc);
    120. tmpObj.put("price" , skuItem.price);
    121. tmpObj.put("formatPrice" , skuItem.formatPrice);
    122. tmpObj.put("priceCurrencyCode" , skuItem.priceCurrencyCode);
    123. tmpObj.put("skuType" , skuItem.skuType);
    124. skuArray.put(tmpObj);
    125. }
    126. JSONArray invalidArray=new JSONArray();
    127. for (int i = 0; i < invalidProductIds.size(); i++) {
    128. invalidArray.put(invalidProductIds.get(i));
    129. }
    130. jsonObject.put("skuItems", skuArray);
    131. jsonObject.put("invalidIds", invalidArray);
    132. } catch (JSONException e) {
    133. PrintLog("Json数据错误:"+e.getMessage());
    134. }
    135. String info=jsonObject.toString();
    136. PrintLog("当前产品信息:"+info);
    137. SendUnityMessage("RecieveProductInfos", info);
    138. }
    139. public void PrintLog(String message){
    140. PrintLog(message,false);
    141. }
    142. public void SendUnityMessage(String func,String value){
    143. CallUnity(UNITY_GO_NAME, func, value);
    144. }
    145. public Activity CurrentActivity(){
    146. return getActivity();
    147. }
    148. /**
    149. * Android调用Unity的方法
    150. * @param gameObjectName 调用的GameObject的名称
    151. * @param functionName 方法名
    152. * @param args 参数
    153. * @return 调用是否成功
    154. */
    155. boolean CallUnity(String gameObjectName, String functionName, String args){
    156. try {
    157. Class<?> classtype = Class.forName("com.unity3d.player.UnityPlayer");
    158. Method method =classtype.getMethod("UnitySendMessage", String.class,String.class,String.class);
    159. method.invoke(classtype,gameObjectName,functionName,args);
    160. return true;
    161. } catch (ClassNotFoundException e) {
    162. System.out.println(e.getMessage());
    163. } catch (NoSuchMethodException e) {
    164. System.out.println(e.getMessage());
    165. } catch (IllegalAccessException e) {
    166. System.out.println(e.getMessage());
    167. } catch (InvocationTargetException e) {
    168. }
    169. return false;
    170. }
    171. /**
    172. * 利用反射机制获取unity项目的上下文
    173. * @return
    174. */
    175. Activity getActivity(){
    176. if(null == unityActivity) {
    177. try {
    178. Class<?> classtype = Class.forName("com.unity3d.player.UnityPlayer");
    179. Activity activity = (Activity) classtype.getDeclaredField("currentActivity").get(classtype);
    180. unityActivity = activity;
    181. context = activity;
    182. } catch (ClassNotFoundException e) {
    183. System.out.println(e.getMessage());
    184. } catch (IllegalAccessException e) {
    185. System.out.println(e.getMessage());
    186. } catch (NoSuchFieldException e) {
    187. System.out.println(e.getMessage());
    188. }
    189. }
    190. return unityActivity;
    191. }
    192. }
    1. package com.egogame;
    2. public class SkuItem {
    3. public String productId;
    4. public String title;
    5. public String desc;
    6. public String price;
    7. public String formatPrice;//格式化价格,包括其货币符号
    8. public String priceCurrencyCode;//货币代码
    9. public String skuType;//内购还是订阅
    10. }
  • 创建MainActivity类,开始接入billing插件(该命名为Activity其实没有继承Activity,因为继承Activity后,需要Unity那边AndroidManfiest文件指定为Activity才能生效,所以这里不采用继承Activity)
    1. package com.egogame;
    2. import android.os.Bundle;
    3. import android.os.Handler;
    4. import android.os.Looper;
    5. import android.text.TextUtils;
    6. import androidx.annotation.NonNull;
    7. import androidx.annotation.Nullable;
    8. import com.android.billingclient.api.BillingClient;
    9. import com.android.billingclient.api.BillingClientStateListener;
    10. import com.android.billingclient.api.BillingFlowParams;
    11. import com.android.billingclient.api.BillingResult;
    12. import com.android.billingclient.api.ConsumeParams;
    13. import com.android.billingclient.api.Purchase;
    14. import com.android.billingclient.api.PurchasesUpdatedListener;
    15. import com.android.billingclient.api.SkuDetails;
    16. import com.android.billingclient.api.SkuDetailsParams;
    17. import com.android.billingclient.api.SkuDetailsResponseListener;
    18. import java.util.ArrayList;
    19. import java.util.Arrays;
    20. import java.util.HashMap;
    21. import java.util.List;
    22. import java.util.Map;
    23. public class MainActivity extends BaseMainActivity implements PurchasesUpdatedListener,BillingClientStateListener {
    24. private static final long RECONNECT_TIMER_START_MILLISECONDS = 1L * 1000L;
    25. private static final long RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L; // 15 mins
    26. private static final Handler handler = new Handler(Looper.getMainLooper());
    27. private BillingClient billingClient;
    28. private String[] cacheRequestList;
    29. private Map<String, SkuDetails> skuDetailsLiveDataMap=new HashMap<>();
    30. private boolean isConsumable;
    31. private String buyProductId;
    32. private boolean billingSetupComplete = false;
    33. // how long before the data source tries to reconnect to Google play
    34. private long reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS;
    35. @Override
    36. protected void OnInitHandle(String googlePlayPublicKey) {
    37. super.OnInitHandle(googlePlayPublicKey);
    38. if (googlePlayPublicKey.contains("CONSTRUCT_YOUR")) {
    39. throw new RuntimeException("Please put your app's public key in MainActivity.java. See README.");
    40. }
    41. billingClient = BillingClient.newBuilder(CurrentActivity()).setListener(this).enablePendingPurchases().build();
    42. billingClient.startConnection(this);
    43. }
    44. private void retryBillingServiceConnectionWithExponentialBackoff() {
    45. handler.postDelayed(() ->
    46. billingClient.startConnection(this),
    47. reconnectMilliseconds);
    48. reconnectMilliseconds = Math.min(reconnectMilliseconds * 2,
    49. RECONNECT_TIMER_MAX_TIME_MILLISECONDS);
    50. }
    51. @Override
    52. protected void OnRequstProduct(String[] productId) {
    53. super.OnRequstProduct(productId);
    54. List<String> skuList = new ArrayList<>();
    55. skuList.addAll(Arrays.asList(productId));
    56. cacheRequestList=productId;
    57. SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
    58. params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
    59. billingClient.querySkuDetailsAsync(params.build(),
    60. new SkuDetailsResponseListener() {
    61. @Override
    62. public void onSkuDetailsResponse(BillingResult billingResult,
    63. List<SkuDetails> skuDetailsList) {
    64. int responseCode = billingResult.getResponseCode();
    65. PrintLog("onSkuDetailsResponse:"+billingResult+" code:"+GetResponseText(responseCode));
    66. switch (responseCode){
    67. case BillingClient.BillingResponseCode.OK:
    68. RecieveProducts(skuDetailsList);
    69. break;
    70. default:
    71. RequestProductsFail("Failed to query inventory: " + billingResult.getDebugMessage());
    72. PrintLog("Failed to query inventory: "+billingResult.getDebugMessage());
    73. break;
    74. }
    75. }
    76. });
    77. }
    78. private String GetResponseText(int responseCode){
    79. switch (responseCode){
    80. case BillingClient.BillingResponseCode.OK:
    81. return "OK";
    82. case BillingClient.BillingResponseCode.SERVICE_TIMEOUT:
    83. return "SERVICE_TIMEOUT";
    84. case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED:
    85. return "FEATURE_NOT_SUPPORTED";
    86. case BillingClient.BillingResponseCode.USER_CANCELED:
    87. return "USER_CANCELED";
    88. case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED:
    89. return "SERVICE_DISCONNECTED";
    90. case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
    91. return "SERVICE_UNAVAILABLE";
    92. case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
    93. return "BILLING_UNAVAILABLE";
    94. case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE:
    95. return "ITEM_UNAVAILABLE";
    96. case BillingClient.BillingResponseCode.DEVELOPER_ERROR:
    97. return "DEVELOPER_ERROR";
    98. case BillingClient.BillingResponseCode.ERROR:
    99. return "ERROR";
    100. case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
    101. return "ITEM_ALREADY_OWNED";
    102. case BillingClient.BillingResponseCode.ITEM_NOT_OWNED:
    103. return "ITEM_NOT_OWNED";
    104. default:
    105. return "UnKnown";
    106. }
    107. }
    108. private void RecieveProducts(List<SkuDetails> skuDetailsList){
    109. ArrayList<SkuItem> skuItems=new ArrayList<SkuItem>();
    110. ArrayList<String> invaildIds=new ArrayList<String>();
    111. PrintLog("cacheRequestList:"+cacheRequestList);
    112. int length=cacheRequestList.length;
    113. if(cacheRequestList!=null && length>0){
    114. for(int i=0;i<length;i++){
    115. String productId=cacheRequestList[i];
    116. if(!TextUtils.isEmpty(productId)){
    117. SkuDetails detail=null;
    118. for (SkuDetails skuDetails : skuDetailsList) {
    119. if(skuDetails.getSku().equals(productId)){
    120. detail=skuDetails;
    121. break;
    122. }
    123. }
    124. if(detail==null){
    125. PrintLog("未找到该产品信息:"+productId);
    126. invaildIds.add(productId);
    127. continue;
    128. }
    129. skuDetailsLiveDataMap.put(productId,detail);
    130. String price=detail.getPrice();
    131. String formatPrice=price;
    132. SkuItem skuItem=new SkuItem();
    133. skuItem.productId=productId;
    134. skuItem.title=detail.getTitle();
    135. skuItem.desc=detail.getDescription();
    136. skuItem.price=price;
    137. skuItem.formatPrice=formatPrice;
    138. skuItem.priceCurrencyCode=detail.getPriceCurrencyCode();
    139. skuItem.skuType=detail.getType();
    140. skuItems.add(skuItem);
    141. }
    142. }
    143. }
    144. RecieveProductInfo(skuItems,invaildIds);
    145. }
    146. @Override
    147. public boolean IsIAPSupported() {
    148. return true;
    149. }
    150. @Override
    151. protected void OnBuyProduct(String productId, boolean isConsumable) {
    152. super.OnBuyProduct(productId, isConsumable);
    153. SkuDetails skuDetails=skuDetailsLiveDataMap.get(productId);
    154. if(null!=skuDetails){
    155. buyProductId=productId;
    156. this.isConsumable=isConsumable;
    157. BillingFlowParams purchaseParams =
    158. BillingFlowParams.newBuilder()
    159. .setSkuDetails(skuDetails)
    160. .build();
    161. billingClient.launchBillingFlow(CurrentActivity(), purchaseParams);
    162. }else{
    163. BuyFail(productId,"Can not find SkuDetails:"+productId);
    164. PrintLog("未请求商品数据,请先请求:"+productId);
    165. }
    166. }
    167. @Override
    168. public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> list) {
    169. int responseCode = billingResult.getResponseCode();
    170. PrintLog("BillingResult [" + GetResponseText(responseCode) + "]: "
    171. + billingResult.getDebugMessage());
    172. switch (responseCode) {
    173. case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
    174. case BillingClient.BillingResponseCode.OK:
    175. FlowFinish(true,null,list);
    176. break;
    177. case BillingClient.BillingResponseCode.USER_CANCELED:
    178. String productId=buyProductId;
    179. buyProductId=null;
    180. BuyCancle(productId);
    181. break;
    182. default:
    183. FlowFinish(false,billingResult.getDebugMessage(),list);
    184. break;
    185. }
    186. }
    187. private void FlowFinish(Boolean isSuccess,String message,List<Purchase> purchases){
    188. if(isSuccess){
    189. if(buyProductId!=null){
    190. String productId=buyProductId;
    191. buyProductId=null;
    192. String purchaseToken=null;
    193. for (Purchase purchase : purchases) {
    194. for (String skus : purchase.getSkus()) {
    195. //需要校验付款状态
    196. if(skus.contains(productId) &&
    197. purchase.getPurchaseState()==Purchase.PurchaseState.PURCHASED){
    198. purchaseToken=purchase.getPurchaseToken();
    199. break;
    200. }
    201. if(purchaseToken!=null) break;
    202. }
    203. }
    204. if(isConsumable){
    205. if(purchaseToken==null){
    206. CallBackBuyFail("unknown purchaseToken:"+productId);
    207. }else {
    208. ConsumeParams consumeParams =
    209. ConsumeParams.newBuilder()
    210. .setPurchaseToken(purchaseToken)
    211. .build();
    212. billingClient.consumeAsync(consumeParams,(billingResult, token) -> {
    213. if(billingResult.getResponseCode()==BillingClient.BillingResponseCode.OK){
    214. BuyComplete(productId);
    215. }else{
    216. CallBackBuyFail(billingResult.getDebugMessage());
    217. }
    218. });
    219. }
    220. }else{
    221. BuyComplete(productId);
    222. }
    223. }
    224. }else{
    225. if(buyProductId!=null){
    226. CallBackBuyFail(message);
    227. }
    228. }
    229. }
    230. private void CallBackBuyFail(String message){
    231. String productId=buyProductId;
    232. buyProductId=null;
    233. BuyFail(productId, message);
    234. PrintLog("Error purchasing: " + message);
    235. }
    236. @Override
    237. public void onBillingServiceDisconnected() {
    238. PrintLog("onBillingServiceDisconnected");
    239. billingSetupComplete = false;
    240. retryBillingServiceConnectionWithExponentialBackoff();
    241. }
    242. @Override
    243. public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
    244. int responseCode = billingResult.getResponseCode();
    245. String debugMessage = billingResult.getDebugMessage();
    246. PrintLog("onBillingSetupFinished: " + debugMessage+"("+ GetResponseText(responseCode)+")",true);
    247. switch (responseCode) {
    248. case BillingClient.BillingResponseCode.OK:
    249. // The billing client is ready. You can query purchases here.
    250. // This doesn't mean that your app is set up correctly in the console -- it just
    251. // means that you have a connection to the Billing service.
    252. reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS;
    253. billingSetupComplete = true;
    254. break;
    255. case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE:
    256. case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE:
    257. PrintLog("Billing Service Unavailable:"+debugMessage,true);
    258. break;
    259. default:
    260. retryBillingServiceConnectionWithExponentialBackoff();
    261. break;
    262. }
    263. }
    264. }
  • 选择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里的接口的桥接类
    1. #if UNITY_ANDROID
    2. using System;
    3. using UnityEngine;
    4. using System.Collections;
    5. using System.Collections.Generic;
    6. using Newtonsoft.Json;
    7. public class GooglePlay_IAPBridge{
    8. class BuyProductData
    9. {
    10. public string productId;
    11. public bool isConsumable;
    12. }
    13. private AndroidJavaObject javaObject;
    14. private GooglePlay_IAPBridge() {
    15. if (Application.platform != RuntimePlatform.Android)
    16. return;
    17. javaObject = new AndroidJavaObject("com.egogame.MainActivity");
    18. }
    19. private volatile static GooglePlay_IAPBridge _instance = null;
    20. private static readonly object lockHelper = new object();
    21. public static GooglePlay_IAPBridge getInstance()
    22. {
    23. if(_instance == null)
    24. {
    25. lock(lockHelper)
    26. {
    27. if(_instance == null)
    28. _instance = new GooglePlay_IAPBridge();
    29. }
    30. }
    31. return _instance;
    32. }
    33. public void Init(string goName,string publicKey){
    34. Debug.Log ("[GooglePlay_IAPBridge]Init:" + goName + "=====" + publicKey);
    35. if (Application.platform != RuntimePlatform.Android)
    36. return;
    37. javaObject.Call ("Init", goName, publicKey);
    38. }
    39. public bool IsIAPSupported(){
    40. if (Application.platform != RuntimePlatform.Android)
    41. return false;
    42. return javaObject.Call<bool> ("IsIAPSupported");
    43. }
    44. public void RequestProducts(string jsonData)
    45. {
    46. Debug.Log ("[GooglePlay_IAPBridge]RequstProduct:" + jsonData);
    47. if (Application.platform != RuntimePlatform.Android)
    48. return;
    49. javaObject.Call ("RequstProduct", jsonData);
    50. }
    51. public void BuyProduct(string productId,bool isConsumable){
    52. BuyProductData buyProductData=new BuyProductData();
    53. buyProductData.productId = productId;
    54. buyProductData.isConsumable = isConsumable;
    55. string jsonData = JsonConvert.SerializeObject(buyProductData);
    56. Debug.Log ("[GooglePlay_IAPBridge]BuyProduct:" + jsonData);
    57. if (Application.platform != RuntimePlatform.Android)
    58. return;
    59. javaObject.Call ("BuyProduct", jsonData);
    60. }
    61. }
    62. #endif
    这里面我们直接 javaObject = new AndroidJavaObject("com.egogame.MainActivity");实例化我们jar封装好的类,即可直接调用public方法。请注意:因为Android和Unity线程不一样,所以jar处理时需要规避线程的同步性。
  • 再封装一个IAPBridge类,用来分流不同渠道,转接不同接口文件
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using Newtonsoft.Json;
    4. public class IAPBridge{
    5. class RequestSkuData
    6. {
    7. public string[] productIds;
    8. }
    9. public static void InitIAp(string goName,string publicKey=""){
    10. Debug.Log("[IAPBridge]InitIAp:" + goName + "==" + publicKey);
    11. #if UNITY_IPHONE
    12. if(Application.platform==RuntimePlatform.IPhonePlayer){
    13. iOS_IAPBridge.InitIAPManager(goName);
    14. }
    15. #elif UNITY_ANDROID
    16. if(Application.platform==RuntimePlatform.Android){
    17. GooglePlay_IAPBridge.getInstance().Init(goName,publicKey);
    18. }
    19. #endif
    20. }
    21. public static bool IAPEnabeld(){
    22. #if UNITY_IPHONE
    23. if(Application.platform==RuntimePlatform.IPhonePlayer){
    24. return iOS_IAPBridge.IsProductAvailable();
    25. }
    26. #elif UNITY_ANDROID
    27. if(Application.platform==RuntimePlatform.Android){
    28. return GooglePlay_IAPBridge.getInstance().IsIAPSupported();
    29. }
    30. #endif
    31. return false;
    32. }
    33. public static void RequstProducts(List<string> productIds){
    34. RequestSkuData data=new RequestSkuData();
    35. data.productIds = productIds.ToArray();
    36. string jsonData = JsonConvert.SerializeObject(data);
    37. Debug.Log("[IAPBridge]RequstProducts:"+jsonData);
    38. #if UNITY_IPHONE
    39. iOS_IAPBridge.RequstProductInfo(jsonData);
    40. #elif UNITY_ANDROID
    41. if (Application.platform == RuntimePlatform.Android){
    42. GooglePlay_IAPBridge.getInstance().RequestProducts(jsonData);
    43. }
    44. #endif
    45. }
    46. public static void SendBuyProduct(string productId,bool isConsumable){
    47. Debug.Log(string.Format("[IAPBridge]SendBuyProduct:{0} isConsumable:{1}",productId,isConsumable));
    48. #if UNITY_IPHONE
    49. if(Application.platform==RuntimePlatform.IPhonePlayer){
    50. iOS_IAPBridge.BuyProduct(productId);
    51. }
    52. #elif UNITY_ANDROID
    53. if (Application.platform == RuntimePlatform.Android){
    54. GooglePlay_IAPBridge.getInstance().BuyProduct(productId, isConsumable);
    55. }
    56. #endif
    57. }
    58. public static void RestoreProduct(){
    59. Debug.Log("[IAPBridge]Restore!");
    60. #if UNITY_IPHONE
    61. if(Application.platform==RuntimePlatform.IPhonePlayer){
    62. iOS_IAPBridge.Restore();
    63. }
    64. #endif
    65. }
    66. }

  • 然后Unity这边要接收下Android那边传过来的接口,将这个类挂载到某个GameObject下,如GameObject名为IAPObject,则上面初始化时调用IAPBridge.InitIAP将该gameObject名作为goName参数传过去即可。
    1. using Newtonsoft.Json;
    2. using UnityEngine;
    3. #pragma warning disable 0649
    4. /// <summary>
    5. /// 该类主要用于接收iOS和Android回调,做一个桥接用途
    6. /// </summary>
    7. public class IAPMessage : MonoBehaviour {
    8. class BuyFailData
    9. {
    10. public string productId;
    11. public string error;
    12. }
    13. #region callback from Objective-c/JAR
    14. //获取到产品列表回调
    15. void RecieveProductInfos(string jsonData)
    16. {
    17. if(string.IsNullOrEmpty(jsonData)) return;
    18. var infoData = JsonConvert.DeserializeObject<IAPProductInfoData>(jsonData);
    19. OnProductInfoReceived (infoData);
    20. }
    21. //产品列表请求失败
    22. void ProductRequestFail(string message)
    23. {
    24. OnProductInfoFail(message);
    25. }
    26. //购买成功回调
    27. void ProductBuyComplete(string productId)
    28. {
    29. OnProductBuyComplete(productId);
    30. }
    31. //购买失败回调
    32. void ProductBuyFailed(string jsonData)
    33. {
    34. var infoData = JsonConvert.DeserializeObject<BuyFailData>(jsonData);
    35. OnBuyProductFail (infoData.productId, infoData.error);
    36. }
    37. //获取商品回执回调
    38. void ProvideContent(string msg){}
    39. //购买取消回调
    40. void ProductBuyCancled(string productId)
    41. {
    42. OnBuyProductCancled(productId);
    43. }
    44. /// <summary>
    45. /// 恢复购买成功
    46. /// </summary>
    47. /// <param name="productId"></param>
    48. void RestoreComplete(string productId){
    49. OnRestoreCompleted (productId);
    50. }
    51. #endregion
    52. //接收到产品信息
    53. void OnProductInfoReceived(IAPProductInfoData info){
    54. Debug.Log("[IAPMessage]Unity接收到商品信息:" + info.ToString());
    55. IAPManager.internal_CallBySDK_ProductInfosReceive(info);
    56. }
    57. //接收到产品信息
    58. void OnProductInfoFail(string error){
    59. Debug.Log("[IAPMessage]Unity商品信息请求失败:" + error);
    60. IAPManager.internal_RequestProductInfoFail(error);
    61. }
    62. //购买完成
    63. void OnProductBuyComplete(string productId){
    64. Debug.Log ("[IAPMessage]购买完成" + productId);
    65. IAPManager.internal_CallBySDK_BuyComplete(productId);
    66. }
    67. //购买失败
    68. void OnBuyProductFail(string productId,string error){
    69. Debug.Log(string.Format("[IAPMessage]购买失败:{0} 错误信息{1}", productId, error));
    70. IAPManager.internal_CallBySDK_BuyFail(productId, error);
    71. }
    72. //购买取消
    73. void OnBuyProductCancled(string productId){
    74. Debug.Log ("[IAPMessage]购买取消" + productId);
    75. IAPManager.internal_CallBySDK_BuyCanceled(productId);
    76. }
    77. //恢复完成
    78. void OnRestoreCompleted(string productId){
    79. Debug.Log ("[IAPMessage]恢复购买完成:"+productId);
    80. IAPManager.internal_CallBySDK_RestoreCompleted(productId);
    81. }
    82. }
    83. #pragma warning restore 0649
    1. using System.Collections.Generic;
    2. public class IAPProductInfoData
    3. {
    4. public List<IAPSkuItem> skuItems;//请求到的产品列表
    5. public string[] invalidIds;//无效产品id
    6. }
    7. public struct IAPSkuItem{
    8. public string productId;//后台产品id
    9. public string title;//后台标题
    10. public string desc;//后台描述
    11. public string price;//格式化价格,显示请用formatPrirce
    12. public string formatPrice;//格式化价格,包括其货币符号
    13. public string priceCurrencyCode;//货币代码
    14. public string skuType;//内购还是订阅 subscription/inapp
    15. public override string ToString ()
    16. {
    17. return string.Format(
    18. "[productId]:{0} [title]:{1} [desc]:{2} [price]:{3} [formatPrice]:{4} [priceCurrencyCode]:{5} [skuType:]{6}",
    19. productId, title, desc, price, formatPrice, priceCurrencyCode, skuType);
    20. }
    21. }
    22. public struct IAPProvideData
    23. {
    24. public string cfgId;
    25. public string title;
    26. public string desc;
    27. public string formatPrice;
    28. }

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

 请注意:

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

Minify和Custom Proguard File不要勾选

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/844677
推荐阅读
相关标签
  

闽ICP备14008679号

        
cppcmd=keepalive&