因为本文内容比较多,整理花费时间比较长,故分几篇完成,以下为本文目录结构,方便查阅:
Unity3d接入googleplay内购详细说明(一)
引言
一、准备条件:
二、谷歌开发者后台应用创建说明:
Unity3d接入googleplay内购详细说明(二)
三、Unity3d向安卓通信以及接受通信
四、Unity导出安卓Apk正式签名说明
五、使用Eclipse运行unity导出的工程
六、Java代码接入谷歌内购:
七、谷歌内购Java代码
Unity3d接入googleplay内购详细说明(三)
八、Apk上传谷歌商店测试版以及添加测试者
九、Zipalign处理APK文件
十、添加google+群组并邀请其成为测试者
十一、测试机googleplay安装以及配置:
Unity3d接入googleplay内购详细说明(四)
十二、真机测试中出现的常见错误以及解决方式:
十三、成功测试购买以及正式版发布
————————————————————————————————————————————————————————————
1、作为测试的是临时写的unity3ddemo,只具有最基本的支付功能。首先解决unity安卓通信,这个基本上都是固定的代码。
例子里面分别添加了2种有效商品,后台中sku分别为jb_1,lb_1;
2、其中主要就几句代码,基本通用,需要改的仅是“Pay”方法,以及注意传入的string参数(用来区分不同sku):
private void UnityToAndroid(string buykey)
{
AndroidJavaClass m_unityPlayer = newAndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject m_curActivity = m_unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
m_curActivity.Call("Pay",buykey);
}
3、所有代码如下:
using UnityEngine;
usingSystem.Collections;
public classGUIpay : MonoBehaviour
{
private int i = 0;
private int j = 0;
private string key;//suk
private string MessageFromAndroid;//从安卓端接受的消息
void OnGUI(){
if(GUILayout.Button("金币1",GUILayout.Height(100),GUILayout.Width(150))){
i++;
key="jb_1";
UnityToAndroid(key);
}
if(GUILayout.Button("礼包1",GUILayout.Height(100),GUILayout.Width(150))){
j++;
key ="lb_1";
UnityToAndroid(key);
}
GUILayout.Button("购买:"+key+"分别:"+i.ToString()+"次/"+j.ToString()+"次",GUILayout.Height(100),GUILayout.Width(200));
GUILayout.Button("AndroidMessage:"+MessageFromAndroid,GUILayout.Height(100),GUILayout.Width(200));
}
//安卓支付通信
private void UnityToAndroid(stringbuykey){
AndroidJavaClass m_unityPlayer =new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject m_curActivity =m_unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
if(m_curActivity == null)
{
Debug.LogError("获得不到JAVA对象");
return;
}
m_curActivity.Call("Pay",buykey);
}
//从安卓端接受消息,因为本脚本挂在一直存在的MainCamera对象上,将从其上获取消息,对应java代码要将消息发送对象指定为MainCamera
void Messgae(string message)
{
MessageFromAndroid = message;
}
}
4、运行效果如下,点击会报错,所以只能在真机上测试:
5、接下来,打开设置,勾选导出安卓工程,icon,应用名字,起始页等如果仅做测试可以不必正式。就是说如果不是正式发布的apk,仅需要正式的包名,正式的签名,正式发布记得修改其他icon、起始页等信息:
6、上传到谷歌商店需要正式的签名,不能为debug模式签名,否则上传失败。签名制作方式如下:
一、签名的意义
为了保证每个应用程序开发商合法ID,防止部分开放商可能通过使用相同的Package Name来混淆替换已经安装的程序,我们需要对我们发布的APK文件进行唯一签名,保证我们每次发布的版本的一致性(如自动更新不会因为版本不一致而无法安装)。
这里详细解释一下:
安装包apk的名字,在世界范围内,有众多开发者,很可能命名重复,当命名重复的时候就需要有一个唯一的识别符加以区分。这就是安卓中的签名。这个签名不用向谷歌申请,而是自己在本机签名即可。
这个签名唯一,相当重要,务必保管好,是制作后续版本更新的重要依据。如果更换,即使是同一个应用,也不会覆盖安装,而是单独当做另外一个新的应用安装。而且当你提交应用时,如果更换签名,就不会当成之前应用的后续更新版本。
二、.签名的步骤(仅安卓需要签名)
一般来说,安卓的apk安装包都是由eclipse编译而成,而他自带了签名插件。包括正式签名和dubug测试签名。
而我们这里并不用eclipse,而是利用unity自带签名功能。
1、工程确认无bug后,需要倒出apk文件时:
2、打开导出设置选项:
3、这publishing settings下面默认是debug签名,如果仅仅自己测试,就不需要修改,上传谷歌商店,需要制作签名。
4、选择 create new keystore,下面选择保存位置,
5、点击key
6、填写详细签名信息,务必记住密码,因为在eclipes里面要使用。存储到可靠位置,丢失后无法找回:
7、证书如下,命令行工具能打开,需要安装jdk:
验证签名信息:
命令行输入 jarsigner -verify -verbose -certs XXX.apk(apk 完整路径) 可以看到 比对签名信息(需要安装了jdk)
8、签名栏默认是unsigned,不签名,debug模式
以后再次导出apk时,必须先选中该签名,还需要在编译前输入原始密码
9、如果为debug签名,那么在上传到谷歌应用商店时会遇到这样情况:
10、签名正确即可正确上传
11、当然也可以利用eclipes来签名,具体请百度。接下来我们要在eclipes里面接入谷歌内购api:
1、打开eclipes,创建工作空间。
2、新建安卓工程:
3、填写跟unity一样的包名,其他默认即可,下一步·····下一步
5、这是刚创建的空工程
6、接下来将unity导出的工程,里面的文件夹直接拖动到hnn工程下,选择全部覆盖,我们的工程内容将被替换为unity内容:
7、为了试验一下是否能够在真机上运行,可以编译一下工程:
8、找到编译出来的apk安装到真机上测试,能够启动即可,按钮什么的当然现在还没有作用:
以下是开发者中心对内购的详细解释,英文文档,可以谷歌翻译一下。链接地址:
PreparingYour In-app Billing Application
1、首先我们需要下载内购sdk。在eclipes里面可以下载到,目前因为禁止链接外网,下载时可能需要vpn,内购demo文件并不大。
这个是他的官方demo,介绍了如何在安卓工程中接入内购,这就是为什么我们选择unity导出安卓工程,而不选择eclipes打jar文件,放到unity中编译apk:
我们需要的就是:IInAppBillingService.aidl这个文件。
2、将该文件拖入到我们的工程src目录下、路径请自己设定好。
3、此外还需要加入必备的工具类。
也就是util文件夹下面的所有代码。同样拖动到我们的工程目录下,拖进来后可能会报引入路径错,注意修改成符合你自己工程的正常路径。例如我的工程导入完毕后结构大约如此。
从上至下依次为内购必须文件、unity自带类、内购工具类。文件目录结构:
4、AndroidMainfest文件添加权限,如果你还做了其他接入功能,例如分享等,权限做合并。
<uses-permission android:name="com.android.vending.BILLING"/>
5、接着我们在java代码中写入被unity调用以及向unity回传消息的代码。
前文说道:
6、向uinty中发送消息,一般用来传送是否购买成功等等。
7、Unity中接受内容继续做处理:
8、内购方面,需要写入base64 ras公共密钥。就是前文说的再谷歌开发者后台申请的key
9、再写入sku,这个sku就是内购项目的唯一id,可以从unity中传过来,也可以写在java中,我们这里做传入。
10、其他添加内购时的各种监听,是否登录,是否符合测试条件,是否绑定银行卡等等,主要借鉴其demo中的代码。
以下为unity导出的默认类中所有代码:
packagecom.taojinzhe.hnn;
importcom.unity3d.player.*;
importandroid.app.NativeActivity;
importandroid.content.res.Configuration;
importandroid.graphics.PixelFormat;
importandroid.os.Bundle;
importandroid.view.KeyEvent;
import android.view.MotionEvent;
importandroid.view.View;
importandroid.view.Window;
importandroid.view.WindowManager;
importandroid.app.Activity;
importandroid.app.AlertDialog;
importandroid.content.Intent;
importandroid.content.SharedPreferences;
import android.util.Log;
importandroid.widget.ImageView;
importandroid.widget.Toast;
importcom.util.IabHelper;
importcom.util.IabResult;
importcom.util.Inventory;
importcom.util.Purchase;
public classUnityPlayerNativeActivity extends NativeActivity
{
protected UnityPlayer mUnityPlayer; // don't change the name of thisvariable; referenced from native code
//___________________________________________________________
//The helper object
IabHelper mHelper;
// Debug tag, for logging
static final String TAG ="hongneinei";
// Does the user have the premium upgrade?
boolean mIsPremium = false;
// Does the user have an activesubscription to the infinite gas plan?
boolean mSubscribedToInfiniteGas = false;
// SKUs for our products: the premiumupgrade (non-consumable) and gas (consumable)
static String SKU_consume ="";
static String SKU_noconsume ="";
//static final String SKU_GAS="";
//SKU for our subscription (infinite gas)
//static final String SKU_INFINITE_GAS ="infinite_gas";
// (arbitrary) request code for the purchaseflow
static final int RC_REQUEST = 10001;
//___________________________________________________________
// Setup activity layout
@Override protected void onCreate (BundlesavedInstanceState)
{
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
getWindow().takeSurface(null);
setTheme(android.R.style.Theme_NoTitleBar_Fullscreen);
getWindow().setFormat(PixelFormat.RGB_565);
mUnityPlayer = newUnityPlayer(this);
if (mUnityPlayer.getSettings().getBoolean ("hide_status_bar", true))
getWindow ().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(mUnityPlayer);
mUnityPlayer.requestFocus();
//___________________________________________________________
/* base64EncodedPublicKey should be YOURAPPLICATION'S PUBLIC KEY
* (that you got from the Google Playdeveloper console). This is not your
* developer public key, it's the*app-specific* public key.
*
* Instead of just storing the entireliteral string here embedded in the
* program, construct the key at runtime from pieces or
* use bit manipulation (for example,XOR with some other string) to hide
* the actual key. The key itself is not secret information, butwe don't
* want to make it easy for an attackerto replace the public key with one
* of their own and then fake messagesfrom the server.
*/
String base64EncodedPublicKey ="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtBVdoRrdD/oCWHYgzhT4TBIh0AZ80n0Sf1uXD8gWQ1H9LdpOB4MX4QG9RP9pBbS0e6W8f7E91bjKyicEa6LetTxpg1Gf+3N0L0c9E2G3RwIO9SXaRUNfzjHN2lzaspLQ52Kj5+SLpT8JD6ISVuro7OS4nxmi7xQT0lx/dPOAOs8mQ/1qmlgwsJRybqWQ+hAvu1fchMggT50TAyG1RyqKTJNErlNYTvog7ZQjjvCZXW5aDBnGeEjFoI79Lnt5XAoaUpuObmbkoCOkJeSiUTQqD+mqAQCvXnBhUso2klLDlOhTz0FUT7X19KZ68OU1Q+lXqNlJ6GurXzhFguvhdo+3DQIDAQAB";
// Create the helper, passing it our contextand the public key to verify signatures with
Log.d(TAG, "Creating IABhelper.");
mHelper = new IabHelper(this,base64EncodedPublicKey);
// enable debug logging (for aproduction application, you should set this to false).
mHelper.enableDebugLogging(true);
// Start setup. This is asynchronousand the specified listener
// will be called once setup completes.
Log.d(TAG, "Startingsetup.");
mHelper.startSetup(newIabHelper.OnIabSetupFinishedListener() {
public voidonIabSetupFinished(IabResult result) {
Log.d(TAG, "Setupfinished.");
if (!result.isSuccess()) {
// Oh noes, there was a problem.
complain("Problemsetting up in-app billing: " + result);
return;
}
// Have we been disposed of inthe meantime? If so, quit.
if (mHelper == null) return;
// IAB is fully set up. Now, let'sget an inventory of stuff we own.
Log.d(TAG, "Setupsuccessful. Querying inventory.");
mHelper.queryInventoryAsync(mGotInventoryListener);
}
});
//___________________________________________________________
}
//___________________________________________________________
protected void Pay(final String buykey)
{
/* TODO: for security, generate your payloadhere for verification. See the comments on
* verifyDeveloperPayload() for more info.Since this is a SAMPLE, we just use
* an empty string, but on a productionapp you should carefully generate this. */
if(buykey.contains("jb_"))
{
SKU_consume = buykey;
}
if(buykey.contains("lb_"))
{
SKU_noconsume = buykey;
}
runOnUiThread(new Runnable()
{
public void run()
{
Toast.makeText(getApplicationContext(),buykey,Toast.LENGTH_SHORT).show();
SendToUnityMessage(buykey);
}
});
String payload = "";
mHelper.launchPurchaseFlow(this,buykey, RC_REQUEST,
mPurchaseFinishedListener,payload);
}
//Listener that's called when we finish querying the items and subscriptions weown 购买侦听器完成
IabHelper.OnIabPurchaseFinishedListenermPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener(){
public void onIabPurchaseFinished(IabResult result, Purchase purchase){
Log.d(TAG, "Purchase finished: " + result + ", purchase:" + purchase);
if (result.isFailure()) {
//complain("Error purchasing: " +result);
//setWaitScreen(false);
return;
}
if (!verifyDeveloperPayload(purchase)) {
complain("Error purchasing.Authenticity verification failed.");
// setWaitScreen(false);
return;
}
Log.d(TAG, "Purchase successful.");
if (purchase.getSku().equals(SKU_consume)) {
Log.d(TAG, "Purchase is gas.Starting gas consumption.");
mHelper.consumeAsync(purchase,mConsumeFinishedListener);
}
else if (purchase.getSku().equals(SKU_noconsume)) {
Log.d(TAG, "Purchase ispremium upgrade. Congratulating user.");
alert("Thank you forupgrading to premium!");
mIsPremium = true;
}
}
};
// Listener that's called when we finishquerying the items and subscriptions we own
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = newIabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventoryinventory) {
Log.d(TAG, "Query inventoryfinished.");
// Have we been disposed of in themeantime? If so, quit.
if (mHelper == null) return;
// Is it a failure?
if (result.isFailure()) {
complain("Failed to queryinventory: " + result);
return;
}
Log.d(TAG, "Query inventorywas successful.");
/*
* Check for items we own. Noticethat for each purchase, we check
* the developer payload to see ifit's correct! See
* verifyDeveloperPayload().
*/
// Do we have the premium upgrade?
Purchase premiumPurchase =inventory.getPurchase(SKU_noconsume);
mIsPremium = (premiumPurchase !=null && verifyDeveloperPayload(premiumPurchase));
Log.d(TAG, "User is " +(mIsPremium ? "PREMIUM" : "NOT PREMIUM"));
// Check for gas delivery -- if weown gas, we should fill up the tank immediately
Purchase gasPurchase =inventory.getPurchase(SKU_consume);
if (gasPurchase != null &&verifyDeveloperPayload(gasPurchase)) {
Log.d(TAG, "We have gas.Consuming it.");
mHelper.consumeAsync(inventory.getPurchase(SKU_consume),mConsumeFinishedListener);
return;
}
Log.d(TAG, "Initial inventoryquery finished; enabling main UI.");
}
};
//Called when consumption is complete
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = newIabHelper.OnConsumeFinishedListener() {
public voidonConsumeFinished(Purchase purchase, IabResult result) {
Log.d(TAG, "Consumptionfinished. Purchase: " + purchase + ", result: " + result);
// if we were disposed of in themeantime, quit.
if (mHelper == null) return;
// We know this is the"gas" sku because it's the only one we consume,
// so we don't check which sku wasconsumed. If you have more than one
// sku, you probably shouldcheck...
if (result.isSuccess()) {
// successfully consumed, so weapply the effects of the item in our
// game world's logic, which inour case means filling the gas tank a bit
Log.d(TAG, "Consumptionsuccessful. Provisioning.");
}
else {
complain("Error whileconsuming: " + result);
}
Log.d(TAG, "End consumptionflow.");
}
};
/** Verifies the developer payload of apurchase. */
boolean verifyDeveloperPayload(Purchase p){
String payload =p.getDeveloperPayload();
/*
* TODO: verify that the developerpayload of the purchase is correct. It will be
* the same one that you sent wheninitiating the purchase.
*
* WARNING: Locally generating a randomstring when starting a purchase and
* verifying it here might seem like agood approach, but this will fail in the
* case where the user purchases anitem on one device and then uses your app on
* a different device, because on theother device you will not have access to the
* random string you originallygenerated.
*
* So a good developer payload hasthese characteristics:
*
* 1. If two different users purchasean item, the payload is different between them,
* so that one user's purchase can't be replayed to another user.
*
* 2. The payload must be such that youcan verify it even when the app wasn't the
* one who initiated the purchase flow (so that items purchased by the useron
* one device work on other devices owned by the user).
*
* Using your own server to store andverify developer payloads across app
* installations is recommended.
*/
return true;
}
void complain(String message) {
Log.e(TAG, "**** TrivialDrive Error: " + message);
alert("Error: " + message);
}
void alert(String message) {
AlertDialog.Builder bld = new AlertDialog.Builder(this);
bld.setMessage(message);
bld.setNeutralButton("OK", null);
Log.d(TAG, "Showing alert dialog: " + message);
bld.create().show();
}
//___________________________________________________________
//向unity发送消息
void SendToUnityMessage(String Sendmessage)
{
UnityPlayer.UnitySendMessage("MainCamera","Messgae",Sendmessage);
}
// Quit Unity
@Override protected void onDestroy ()
{
mUnityPlayer.quit();
super.onDestroy();
}
// Pause Unity
@Override protected void onPause()
{
super.onPause();
mUnityPlayer.pause();
}
// Resume Unity
@Override protected void onResume()
{
super.onResume();
mUnityPlayer.resume();
}
// This ensures the layout will becorrect.
@Override public voidonConfigurationChanged(Configuration newConfig)
{
super.onConfigurationChanged(newConfig);
mUnityPlayer.configurationChanged(newConfig);
}
// Notify Unity of the focus change.
@Override public voidonWindowFocusChanged(boolean hasFocus)
{
super.onWindowFocusChanged(hasFocus);
mUnityPlayer.windowFocusChanged(hasFocus);
}
// For some reason the multiple keyeventtype is not supported by the ndk.
// Force event injection by overridingdispatchKeyEvent().
@Override public booleandispatchKeyEvent(KeyEvent event)
{
if (event.getAction() ==KeyEvent.ACTION_MULTIPLE)
return mUnityPlayer.injectEvent(event);
returnsuper.dispatchKeyEvent(event);
}
// Pass any events not handled by(unfocused) views straight to UnityPlayer
@Override public boolean onKeyUp(intkeyCode, KeyEvent event) { returnmUnityPlayer.injectEvent(event); }
@Override public boolean onKeyDown(intkeyCode, KeyEvent event) { returnmUnityPlayer.injectEvent(event); }
@Override public booleanonTouchEvent(MotionEvent event) { return mUnityPlayer.injectEvent(event); }
/*API12*/ public boolean onGenericMotionEvent(MotionEventevent) { returnmUnityPlayer.injectEvent(event); }
}本片结语:
至此,unity,eclipes代码部分基本上完结,接下来我们将编译出apk进行测试。
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。