歷史背景
近期在游戲SDK接入的技術支持中,不斷有游戲反饋希望有現(xiàn)成的unity插件可以直接接入,為了減輕和方便游游戲方接入,對SDK進行unity的插件開發(fā)
用到的兵器
1、Android studio 官方下載地址
2、Unity 2017.4.2f2 官方下載地址
官網(wǎng)有時候無法訪問,這邊提供百度
unity2017中文安裝包:鏈接:https://pan.baidu.com/s/1gW2tDhAiodKf0qRNUNCKWg 提取碼:k8to
unity2017的Android支持的插件:鏈接:https://pan.baidu.com/s/1tTCAYlZnDUtXIizz_70Gpg 提取碼:0mtp
Android方面的操作(這邊提供的是aar的形式)
1、新建一個Android Library的module

2、編譯下對應的module,名稱和包名(包名就是unity的項目的包名)

3、原有的要接入SDK,已經(jīng)達成aar包,將SDK的aar包,導入到我們這個plugin的module的libs下,配置一下gradle

4、個人這邊將SDK的內(nèi)容進行了封裝到一個類
public class SinglePaySDK {
private static final String TAG = "SDK";
private SingleOperateCenter mOpeCenter;
public SinglePaySDK(Activity context, String appKey, String gameName, int orientation,
SingleOperateCenter.SingleRechargeListener singleRechargeListener) {
initSDK(context, appKey, gameName, orientation, singleRechargeListener);
}
private void initSDK(Activity activity, String appKey, String gameName, int orientation,
SingleOperateCenter.SingleRechargeListener singleRechargeListener) {
Log.i(TAG, "appKey = " + appKey + ", orientation = " + orientation);
if (isInitReady()) {
return;
}
mOpeCenter = SingleOperateCenter.getInstance();
new OperateCenterConfig.Builder(activity)
.setDebugEnabled(true) //發(fā)布游戲時,要設為false
.setOrientation(orientation) //設置SDK界面方向,應與游戲設置一直
.setSupportExcess(true) //設置是否支持超出金額充值
.setGameKey(appKey) //換成實際游戲的game key
.setGameName(gameName) //換成實際游戲的名字,原則上與游戲名字匹配
.build();
mOpeCenter.init(activity, singleRechargeListener);
}
public void doPay(Activity activity, final String money, final String productName) {
doPay(activity, money, productName, null);
}
public void doPay(final Activity activity, final String money, final String productName,
final String extra) {
Log.d(TAG, "execute method doPay... money = " + money + ", productName = " + productName);
if (mOpeCenter == null) {
Log.d(TAG, "SDK need init first");
return;
}
//extra是透傳的字段
if (TextUtils.isEmpty(extra)) {
mOpeCenter.recharge(activity, money, productName);
} else {
mOpeCenter.recharge(activity, money, productName, extra);
}
}
public boolean isInitReady() {
return mOpeCenter != null;
}
public void doCheck(Activity activity, Callbacks.OnCheckFinishedListener listener) {
Log.d(TAG, "execute doCheck...");
if (!isInitReady()) {
Log.d(TAG, "SDK need init first");
return;
}
mOpeCenter.doCheck(activity, listener);
}
private void doDownload(Activity activity, Callbacks.OnDownloadListener listener) {
Log.d(TAG, "execute doDownload...");
if (!isInitReady()) {
Log.d(TAG, "SDK need init first");
return;
}
mOpeCenter.doDownload(activity, listener);
}
private void doInstall(Activity activity) {
Log.d(TAG, "execute doInstall...");
if (!isInitReady()) {
Log.d(TAG, "SDK need init first");
return;
}
mOpeCenter.doInstall(activity);
}
}
5、在unity的安裝目錄下:D:\Program Files\Unity\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar,將該文件拷貝到plugin 的module下的libs目錄, 刷新gradle
6、由于當前默認使用原有的UnityPlayerActivity,并沒有繼承該該類 繼承UnityPlayerActivity的寫法
AndroidManifest.xml文件需要配置
<activity android:name="com.unity3d.player.UnityPlayerActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:launchMode="singleTask" android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale|layoutDirection|density" android:hardwareAccelerated="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="unityplayer.UnityActivity" android:value="true" />
</activity>
............其他的代碼.......
7、打包成aar

8、由于我們SDK插件打包成aar的時候,會將libs的unity的classes的jar包一并打包進去,需要手動用壓縮軟件打開aar將里面的classes.jar的文件刪掉
Unity方面的操作
1、新建Unity項目,創(chuàng)建Secne場景,添加了Canvas的畫布,在畫布下添加了,Button和Text

2、在unity的project下的assert目錄下新建plugins目,然后再plugins目錄下,在新建Android目錄,將項目的AndroidManifest.xml文件拷貝進去,然后再新家libs目錄將aar包copy進去,這邊有兩個,一個是SDK的aar包,一個我們編寫創(chuàng)建的aar包,
要注意一個地方是plugin-release的AndroidManifest文件包名和主項目的AndroidManifest不能一樣,在17版本編譯會出現(xiàn)重復包名的錯誤
3、在Assert目錄下創(chuàng)建Scripts目錄(這個主要是這邊用于存放C#的腳本文件,直接放Assert級的目錄下也是可以的)

4、上述的腳本文件,就需要選擇一個主要腳本,將該腳本文件添加到component中,用于綁定到該場景,設置調(diào)用通過add component-->scripts, 然后選擇所開發(fā)的腳本文件

5、編譯這邊有兩個區(qū)分,一個是gradle編譯,一個Internal編譯,主要區(qū)別是在2017版本gradle編譯,可以不進行包名設置,internal是需要設置。


6、注意PlatForm中Android 是需自己安裝的
unity2017的Android 平臺支持的插件:鏈接:https://pan.baidu.com/s/1tTCAYlZnDUtXIizz_70Gpg 提取碼:0mtp
7、生成apk
這邊講講上面C#調(diào)用java的相關代碼
先貼代碼
--SinglePaySDKContext.cs文件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SinglePay{
public sealed class SinglePaySDKContext : MonoBehaviour
{
private AndroidJavaObject currentActivity;
private static readonly SinglePaySDKContext _SinglePaySDKContext = new SinglePaySDKContext();
/*
* 獲取當前實例
*/
public static SinglePaySDKContext GetInstance()
{
return _SinglePaySDKContext;
}
/*
* 獲取當前Activity
*/
public AndroidJavaObject GetActivity()
{
if (null == currentActivity)
{
currentActivity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
.GetStatic<AndroidJavaObject>("currentActivity");
}
return currentActivity;
}
/*
* 運行在主UI線程
*/
public void RunOnUIThread(AndroidJavaRunnable runnable)
{
GetActivity().Call("runOnUiThread", runnable);
}
/*
* 獲取根節(jié)點的布局
*/
public AndroidJavaObject GetRootLayout()
{
AndroidJavaClass R = new AndroidJavaClass("android.R$id");
return currentActivity.Call<AndroidJavaObject>("findViewById", R.GetStatic<int>("content"));
}
}
}
--SinglePaySDK.cs文件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using SinglePay;
public class SinglePaySDK : MonoBehaviour {
private Transform canvasForm;
private Button initButton;
private Button payButton;
private Button checkButton;
private Text ResultText;
//表示橫屏
private const int SCREEN_ORIENTATION_LANDSCAPE = 0;
//表示豎屏
private const int SCREEN_ORIENTATION_PORTRAIT = 1;
//表示反向橫屏
private const int SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8;
//表示反向豎屏
private const int SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9;
//獲取全局的上下文件對象
private SinglePaySDKContext singlePaySDKContext;
//獲取單機支付SDK的對象
private AndroidJavaObject singlePaySDK;
//是否開始計時
private bool IsTiming;
//倒計時
private float CountDown;
// Use this for initialization
void Start () {
singlePaySDKContext = SinglePaySDKContext.GetInstance();
canvasForm = GameObject.Find("Canvas").transform;
//獲取控件對象設置時間監(jiān)聽
initButton = canvasForm.Find("InitButton").GetComponent<Button>();
payButton = canvasForm.Find("PayButton").GetComponent<Button>();
checkButton = canvasForm.Find("CheckButton").GetComponent<Button>();
ResultText = canvasForm.Find("ResultText").GetComponent<Text>();
initButton.onClick.AddListener(InitSDK);
payButton.onClick.AddListener(doPay);
checkButton.onClick.AddListener(doCheck);
ResultText.text = "hello";
//Input.backButtonLeavesApp = true;//設置返回鍵,是否退出程序。(系統(tǒng)默認為 false,所以不自己寫方法是不會退出 App 的)
}
public void InitSDK()
{
//AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
//AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
//調(diào)用成員方法
//傳入appkey
//SingleRechargeListenerProxy listenerProxy = new SingleRechargeListenerProxy(this);
//jo.Call("init","70001", SCREEN_ORIENTATION_PORTRAIT, listenerProxy);
//調(diào)用靜態(tài)方法
//ResultText.text = jo.CallStatic<string>("GetInformation");
singlePaySDKContext.RunOnUIThread(new AndroidJavaRunnable(() =>
{
SingleRechargeListenerProxy listenerProxy = new SingleRechargeListenerProxy(this);
singlePaySDK = new AndroidJavaObject("cn.m4399.plugin.SinglePaySDK", singlePaySDKContext.GetActivity(), "70001", "測試游戲",SCREEN_ORIENTATION_PORTRAIT, listenerProxy);
ResultText.text = "init sdk";
}));
}
public void doPay()
{
//AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
//AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
singlePaySDKContext.RunOnUIThread(new AndroidJavaRunnable(() =>
{
bool isInitReady = singlePaySDK.Call<bool>("isInitReady");
//調(diào)用成員方法
//傳入金額和商品名稱
//對應java的方法 doPay(money, productName, extra) 注:extra是透傳參數(shù)
//jo.Call("doPay","10","磚石");
if (isInitReady)
{
singlePaySDK.Call("doPay", singlePaySDKContext.GetActivity(), "10", "磚石", "測試數(shù)據(jù)");
ResultText.text = "do pay";
}
else
{
ResultText.text = "sdk init is not finished";
}
}));
}
/*
* 充值回調(diào)
*/
internal class SingleRechargeListenerProxy : AndroidJavaProxy
{
private SinglePaySDK sdk;
public SingleRechargeListenerProxy(SinglePaySDK singlepay) : base("cn.m4399.operate.SingleOperateCenter$SingleRechargeListener")
{
sdk = singlepay;
}
/**
* 加載成功
*/
void onRechargeFinished(bool success, string msg)
{
Debug.Log("success = "+ success +",msg = "+msg);
sdk.ResultText.text = "success = " + success + ",msg = " + msg;
}
/**
* 加載失敗
* @param message
*/
bool notifyDeliverGoods(bool shouldDeliver, AndroidJavaObject o)
{
Debug.Log("shouldDeliver = "+ shouldDeliver +", o "+ o.Call<string>("getJe"));
return shouldDeliver;
}
}
public void doCheck()
{
singlePaySDKContext.RunOnUIThread(new AndroidJavaRunnable(() =>
{
bool isInitReady = singlePaySDK.Call<bool>("isInitReady");
//調(diào)用成員方法
//傳入金額和商品名稱
//對應java的方法 doPay(money, productName, extra) 注:extra是透傳參數(shù)
//jo.Call("doPay","10","磚石");
if (isInitReady)
{
OnCheckFinishedListenerProxy listenerProxy = new OnCheckFinishedListenerProxy(this);
singlePaySDK.Call("doCheck", singlePaySDKContext.GetActivity(), listenerProxy);
ResultText.text = "do check update";
}
else
{
ResultText.text = "sdk init is not finished";
}
}));
}
/*
* 充值回調(diào)
*/
internal class OnCheckFinishedListenerProxy : AndroidJavaProxy
{
private SinglePaySDK sdk;
public OnCheckFinishedListenerProxy(SinglePaySDK singlepay) : base("cn.m4399.operate.model.callback.Callbacks$OnCheckFinishedListener")
{
sdk = singlepay;
}
void onCheckResponse(AndroidJavaObject o)
{
//這個回調(diào)參數(shù)對象是java的 UpgradeInfo,查看demo文檔
int code = o.Call<int>("getResultCode");
if(code == o.GetStatic<int>("APK_CHECK_NO_UPDATE"))
{
sdk.ResultText.text = "已經(jīng)是最新版本";
}
else if(code == o.GetStatic<int>("APK_CHECK_NEED_UPDATE"))
{
sdk.ResultText.text = "新版本號:"+o.Call<string>("getVersionCode");
sdk.doDownload();
}
else
{
sdk.ResultText.text = "檢查更新失敗";
}
}
}
public void doDownload()
{
singlePaySDKContext.RunOnUIThread(new AndroidJavaRunnable(() =>
{
OnDownloadListenerProxy listenerProxy = new OnDownloadListenerProxy(this);
singlePaySDK.Call("doDownload", singlePaySDKContext.GetActivity(), listenerProxy);
ResultText.text = "do download";
}));
}
internal class OnDownloadListenerProxy : AndroidJavaProxy
{
private SinglePaySDK sdk;
public OnDownloadListenerProxy(SinglePaySDK singlepay) : base("cn.m4399.operate.model.callback.Callbacks$OnDownloadListener")
{
sdk = singlepay;
}
void onDownloadStart()
{
sdk.ResultText.text = "開始下載...";
}
void onDownloadProgress(long progress, long max)
{
sdk.ResultText.text = "總大小:"+max+", 已下載:"+progress;
}
void onDownloadSuccess()
{
sdk.ResultText.text = "下載成功...";
sdk.doInstall();
}
void onDownloadFail(int resultCode, string message)
{
sdk.ResultText.text = "下載失敗,resultCode = " + resultCode + ", message:" + message;
}
}
public void doInstall()
{
singlePaySDKContext.RunOnUIThread(new AndroidJavaRunnable(() =>
{
singlePaySDK.Call("doInstall", singlePaySDKContext.GetActivity());
ResultText.text = "do install";
}));
}
// Update is called once per frame (每一幀刷新)
void Update () {
if (Application.platform == RuntimePlatform.Android && Input.GetKeyDown(KeyCode.Escape)) // 返回鍵
{
if (CountDown == 0) //當?shù)褂嫊r時間等于0的時候
{
CountDown = Time.time; //把游戲開始時間,賦值給 CountDown
IsTiming = true; //開始計時
ToastUtils.showToast("再按一次退出"); //顯示提示信息 —— 這里的提示方法,需要根據(jù)自己需求來完成(用你自己所需要的方法完成提示)
ResultText.text = "再按一次退出";
}
else
{
Application.Quit(); //退出游戲
}
}
if (IsTiming) //如果 IsTiming 為 true
{
if ((Time.time - CountDown) > 2.0) //如果 兩次點擊時間間隔大于2秒
{
CountDown = 0; //倒計時時間歸零
IsTiming = false; //關閉倒計時
}
}
if (Application.platform == RuntimePlatform.Android && Input.GetKeyDown(KeyCode.Home)) // Home鍵
{
//Code
}
}
}
--ToastUtils.cs文件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SinglePay
{
public class ToastUtils
{
public static void showToast(string text)
{
SinglePaySDKContext.GetInstance().RunOnUIThread(new AndroidJavaRunnable(() =>
{
AndroidJavaClass Toast = new AndroidJavaClass("android.widget.Toast");
Toast.CallStatic<AndroidJavaObject>("makeText", SinglePaySDKContext.GetInstance().GetActivity(), text, Toast.GetStatic<int>("LENGTH_SHORT")).Call("show");
}));
}
}
}
上面主要的語法知識點:
1、AndroidJavaClass 這個就是相當于C#到AndroidJava的類的映射
---- 調(diào)用靜態(tài)方法Android中類的靜態(tài)方法 和獲取對應的靜態(tài)字段
AndroidJavaClass jc = new AndroidJavaClass("完整的路徑類名");
靜態(tài)方法調(diào)用:jc.CallStatic<返回類型>("java對應的方法名",object[] args); //args:參數(shù),類型沒有傳默認就是void
靜態(tài)字段獲取:jc.GetStatic<返回類型>("字段名稱")). //public類型的字段
例子
AndroidJavaClass Toast = new AndroidJavaClass("android.widget.Toast"); Toast.CallStatic<AndroidJavaObject>("makeText", SinglePaySDKContext.GetInstance().GetActivity(), text, Toast.GetStatic<int>("LENGTH_SHORT")).Call("show");
2、AndroidJavaObject 這個就是相當于C#到AndroidJava的對象的映射
---- 調(diào)用Android中對的方法 和獲取對應的字段
AndroidJavaObject jo= new AndroidJavaObject ("完整的路徑類名",object[] args); //args:構成方法的參數(shù)
方法調(diào)用:jo.Call<返回類型>("方法名", object[] args); //args:參數(shù),//類型沒有傳默認就是void
成員變量調(diào)用:jo.Get<int>("字段名稱"));
例子:
AndroidJavaObject singlePaySDK = new AndroidJavaObject("cn.m4399.plugin.SinglePaySDK", singlePaySDKContext.GetActivity(), "70001", "測試游戲",SCREEN_ORIENTATION_PORTRAIT, listenerProxy);
singlePaySDK.Call("doPay", singlePaySDKContext.GetActivity(), "10", "磚石", "測試數(shù)據(jù)");
3、AndroidJavaProxy接口的語法調(diào)用
----unity對應java接口
java的接口代碼:
public interface SDKCallbackListener
{
void OnSDKInited(String msg);
void OnAuthSuccess(String token);
void OnCreatedLive(String url);
void OnDeletedLive(String id);
}
Unity中C#的代碼:
Unity C#代碼實現(xiàn)Android Java 代碼必須要完全一致,但是允許在UnityC#代碼中實現(xiàn)多次
class SDKCallbackListener : AndroidJavaProxy
{
// 這句話很重要!??!C#找到Jar中接口的引用
public SDKCallbackListener() : base("包名.SDKCallbackListener") { }
public void OnSDKInited(string msg)
{
ATrace.Log( "OnSDKInited:" + msg);
}
public void OnAuthSuccess(string token)
{
ATrace.Log("OnAuthSuccess:" + token);
}
public void OnCreatedLive(string url)
{
ATrace.Log("OnCreatedLive:" + url);
}
public void OnDeletedLive(string id)
{
ATrace.Log("OnDeletedLive:" + id);
}
}
4、AndroidJavaRunnable其實就是對應java的 java.lang.Runnable.
直接看Unity Api
總結,Android和Unity的交互就是一方導出插件的形式給一方使用,本文寫法是提供了Android導出aar給Unity使用,以上就本文的全部內(nèi)容,如果有什么錯誤的地方,歡迎大家指出
參考文獻:
https://docs.unity3d.com/ScriptReference
http://www.itdecent.cn/p/ceaac83808f2
http://www.itdecent.cn/p/b5e3cfcdf081