Unity與Android的數(shù)據(jù)通信解決方案
簡述
Android 與 Unity 的交互有兩種方式:Android 作為 Unity 的一部分或者把 Unity 作為 Android 的一部分骚灸。至于使用哪種方式,就要根據(jù)具體情況來決定了慌植。
如果你的項目是以 Unity 為主( Unity 的部分需要經(jīng)常改動甚牲,而 Android 的部分比較固定),就把 Android 作為 Unity 的一部分來實現(xiàn)交互涤浇。
如果你的項目是以 Android 為主( Android 的部分需要經(jīng)常改動鳖藕, Unity 部分比較固定),這時就把 Unity 作為 Android 的插件來使用只锭。
目前我們虛擬形象的app所采用的技術(shù)方案是:Unity作為Android的一部分著恩,這樣能很好的把一些雜七雜八的業(yè)務(wù)需求,如登錄注冊,開屏廣告喉誊,版本更新邀摆,素材維護等等交給Android端負責(zé),Unity端只專注負責(zé)渲染形象即可伍茄。
對相機的維護是由Android端來完成的栋盹,Android端使用人臉識別sdk來對相機采集到的每一幀數(shù)據(jù)做人臉識別,會產(chǎn)生幾種類型的數(shù)組敷矫,通過連續(xù)不斷的人臉識別例获,然后連續(xù)不斷的傳遞人臉數(shù)據(jù)(其實本質(zhì)上就是數(shù)組),Unity就能實現(xiàn)是模型跟著人臉晃動的功能曹仗。
所以我們首先要解決的問題是:如何把人臉識別的出來的數(shù)組信息榨汤,傳遞給Unity?推到更加普遍的場景就是怎茫,Unity和android是如何進行數(shù)據(jù)通信的收壕?
Android向Unity傳遞數(shù)據(jù)
sendMessage
UnityPlayer.UnitySendMessage("GameManager", "ZoomIn", "");
這段代碼的意思是,調(diào)用GameManager這個游戲?qū)ο蟮哪_本中的ZoomIn方法轨蛤,由于這個ZoomIn方法不需要傳入?yún)?shù)蜜宪,所以這里我們寫兩個冒號代表空,但是絕不能寫null祥山,否則會遇到崩潰圃验。
這種調(diào)用方式的缺陷也是非常明顯,參數(shù)值只能傳遞一個缝呕,而且只支持傳遞String類型的參數(shù)损谦,如果我們確實需要傳遞多個參數(shù),可以把這個參數(shù)封裝在一個自定義對象中岳颇,把自定義對象序列化成String來進行傳輸,但是由于涉及到序列化和反序列化颅湘,耗時肯定是難免的话侧,下面的這種方式就能很好解決這個問題,他支持傳遞多種參數(shù)闯参。
AndroidJavaProxy
第一步:在安卓端定義接口
public interface ExActivityListener
{
public void onRestart();
public void onStart();
public void onResume();
public void onPause();
public void onStop();
public void onActivityResult(int requestCode, int resultCode, Intent data);
}
第二步:然后在UnityActivity中添加一個方法瞻鹏,這個方法用于接收Unity關(guān)于上面接口的實現(xiàn), 核心函數(shù)就是setListener和傳遞的參數(shù)鹿寨,多態(tài)形式新博,這個會在Unity端傳入
public class MainActivity extends UnityPlayerActivity {
private ExActivityListener listener;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
}
public void setListener(ExActivityListener listener)
{
Log.v("Unity", "setListener(1)!------------");
this.listener = listener;
}
@Override
public void onRestart()
{
Log.v("Unity", "onRestart!------------");
super.onRestart();
if(listener != null) listener.onRestart();
}
@Override
public void onStart()
{
super.onStart();
if(listener != null) listener.onStart();
}
@Override
public void onResume()
{
super.onResume();
if(listener != null) listener.onResume();
}
@Override
public void onPause()
{
super.onPause();
if(listener != null) listener.onPause();
}
@Override
public void onStop()
{
if(listener != null) listener.onStop();
super.onStop();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data)
{
if(listener != null) listener.onActivityResult(requestCode, resultCode, data);
}
剩下就是看Unity端,需要先實現(xiàn)這個接口脚草,這樣才可能產(chǎn)生回調(diào)
using UnityEngine;
using System.Collections;
public class Hoge : MonoBehaviour
{
public class ActivityListener : AndroidJavaProxy
{
public ActivityListener() : base("com.baofeng.test.ExActivityListener")
{
}
public void onRestart()
{
UnityEngine.Debug.LogError("Back to Unity onRestart");
}
public void onStart()
{
UnityEngine.Debug.LogError("Back to Unity onStart");
}
public void onResume()
{
UnityEngine.Debug.LogError("Back to Unity onResume");
}
public void onPause()
{
UnityEngine.Debug.LogError("Back to Unity onPause");
}
public void onStop()
{
UnityEngine.Debug.LogError("Back to Unity onStop");
}
public void onActivityResult(int requestCode, int resultCode, AndroidJavaObject data)
{
UnityEngine.Debug.LogError("onActivityResult");
}
}
void Awake()
{
AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity");
activity.Call("setListener", new ActivityListener());
UnityEngine.Debug.LogError("Awake");
}
}
通過AndroidJavaProxy方式來傳遞數(shù)據(jù)給Unity赫悄,非常類似Android上層的接口設(shè)計,即接口的定義和調(diào)用交給自己來做,接口的實現(xiàn)交給外部實現(xiàn)埂淮,二者通過setListener關(guān)聯(lián)在一起姑隅,通過這種方式,可以傳遞多個參數(shù)給Unity端倔撞,但是這種方式有一個缺點就是比較負責(zé)讲仰,但是更具有擴展性和維護性。
如何才能傳遞數(shù)組
由于通過AnroidJavaProxy的方式痪蝇,參數(shù)支持的類型是基本數(shù)據(jù)類型和自定義對象鄙陡,并不支持數(shù)組類型,所以我們?nèi)匀粺o法傳遞
我們識別出來的包含人臉信息的數(shù)組躏啰,強行傳輸會出現(xiàn)以下異常
AndroidJavaException: java.lang.NoSuchMethodError: no method with name='getLength' signature='(L[BI' in class Ljava/lang/reflect/Array;
at UnityEngine.AndroidJNISafe.CheckException () [0x00000] in <filename unknown>:0
at UnityEngine.AndroidJNISafe.GetMethodID (IntPtr obj, System.String name, System.String sig) [0x00000] in <filename unknown>:0
at UnityEngine._AndroidJNIHelper.GetMethodID (IntPtr jclass, System.String methodName, System.String signature, Boolean isStatic) [0x00000] in <filename unknown>:0
at UnityEngine.AndroidJNIHelper.GetMethodID (IntPtr javaClass, System.String methodName, System.String signature, Boolean isStatic) [0x00000] in <filename unknown>:0
at UnityEngine._AndroidJNIHelper.GetMethodID[Int32] (IntPtr jclass, System.String methodName, System.Object[] args, Boolean isStatic) [0x00000] in <filename unknown>:0
at UnityEngine.AndroidJNIHelper.GetMethodID[Int32] (IntPtr jclass, System.String methodName, System.Object[] args, Boolean isStatic) [0x00000] in <filename unknown
即Android端定義的方法和Unity實現(xiàn)的方法趁矾,他們的參數(shù)沒有匹配上,所以會出現(xiàn)NoSuchMethodError的錯誤丙唧。要解決這個問題愈魏,我們需要做的就是將byte數(shù)組封裝起來,可以使用如下的JavaBean
public class BytesWrapper{
private byte[] bytes;
public void setBytes(byte[] bytes){
this.bytes = bytes;
}
public byte[] getBytes(){
return this.bytes;
}
}
這樣我們就可以沒有錯誤的使用AndroidJavaProxy進行反射了想际,同樣有問題的還有l(wèi)ong類型的數(shù)組培漏。
Unity向Android傳遞數(shù)據(jù)
剛才我們提到,我們把人臉識別的byte數(shù)組胡本,通過自定義對象包裝起來傳遞給了Unity牌柄,那么Unity如何才能把這個自定義對象轉(zhuǎn)換成byte數(shù)組呢,細心的同學(xué)已經(jīng)發(fā)現(xiàn)了侧甫,我剛才在自定義對象中增加了getBytes方法珊佣,其實我們獲取到這個自定義對象后,調(diào)用此對象的getBytes方法就能去除byte數(shù)組了披粟。
那么問題現(xiàn)在就轉(zhuǎn)換成咒锻,Unity如何調(diào)用Android的方法,其實非常簡單守屉,直接找到此類惑艇,然后反射調(diào)用此類的方法即可,Unity調(diào)用Android的方法拇泛,都是采用反射的方式滨巴。但是這里要特別注意,如果你反射的類不是一個靜態(tài)的類存在俺叭,即全局只有一個恭取,每次反射都相當于new一個新的類,然后調(diào)用其中的方法熄守,可能會造成一些奇怪的問題蜈垮,所以最好你反射的類是一個靜態(tài)的類耗跛。由于這里我能確保調(diào)用的BytesWrapper類中的getBytes方法,實現(xiàn)比較簡單窃款,不會影響到Android端的其他類的運行课兄,所以才采用反射方式。
AndroidJavaObject jo = new AndroidJavaObject("com.wenming.demo.BytesWrapper");
jo.Call<byte>("getBytes");
AndroidJavaObject 有多個調(diào)用方法晨继,你可以根據(jù)需要調(diào)用需要的方法
方法 | 返回值 | 說明 |
---|---|---|
Call | void | 調(diào)用類的普通方法烟阐,不返回任何對象 |
Call(T) | T | 調(diào)用類的普通方法,返回對象T |
CallStatic | void | 調(diào)用類的靜態(tài)方法紊扬,不返回任何對象 |
CallStatic(T) | T | 調(diào)用類的靜態(tài)方法蜒茄,此方法返回對象T |
Get(T) | T | 獲取成員變量 |
GetStatic(T) | T | 獲取類的靜態(tài)成員變量 |
Set(T) | void | 設(shè)置成員變量 |
SetStatic(T) | void | 設(shè)置類的成員變量 |
接口回調(diào)的設(shè)計
如果這篇教程止步在這里,那么和其他網(wǎng)絡(luò)上的教程也沒什么區(qū)別了餐屎。所以這里要再進一步探討一個問題是:假設(shè)存在這樣的業(yè)務(wù)流程檀葛,Android端調(diào)用Untiy 的a方法成功后,再調(diào)用Unity的b方法腹缩,ab的方法的成功失敗都需要告知android端屿聋,讓android端做相應(yīng)的業(yè)務(wù)處理。
讀者可能會問藏鹊,調(diào)用是在android端的润讥,那么a方法想必是運行在主線程的,b也是同樣的運行在主線程盘寡,b方法直接寫在a方法下面不就好了嗎楚殿?其實不然,Unity有可能在a方法中做了一些異步操作竿痰,這樣就不能保證b在a成功之后執(zhí)行了脆粥,另外,如果a如果在執(zhí)行過程中出錯影涉,也需要把相應(yīng)的錯誤碼返回給Android端变隔,Android端再做相應(yīng)的業(yè)務(wù)處理(失敗重試,Toast提示等等)蟹倾,所以成功失敗的回調(diào)是必要的弟胀。我們可以通過剛才介紹的數(shù)據(jù)通信的方式,來優(yōu)雅的實現(xiàn)這樣的回調(diào)方式喊式。
下面這個時序圖,描述了接口回調(diào)方案的整體調(diào)用流程萧朝,我們會用文字來詳細整個流程的經(jīng)過
第一步: 定義接口岔留,其中有2個方法,方法a與方法b检柬,functionKey的作用我們后面再談
public interface UnityAndroidProxy {
void a(String functionKey, String str);
void b(String functionKey);
}
第二步: 創(chuàng)建接口管理類献联,通過UnityManager這個類竖配,我們對a方法做了進一層包裝,后續(xù)如果Android需要調(diào)用a方法里逆,只需要傳入?yún)?shù)进胯,以及監(jiān)聽此方法的CallBack,然后直接調(diào)用UnityManager.getInstance().methodA()方法即可原押。Unity在執(zhí)行完a方法之后胁镐,可以去反射找到UnityManager這個單例,進而調(diào)用到notifyMethodInvokeCallback這個方法诸衔,通過這個方法盯漂,我們接受到了接受Unity傳送過來的errCode,errMessage笨农,args等參數(shù)就缆,通過判斷errCode我們可以知道A方法調(diào)用成功失敗。但是這里有一個問題谒亦,如果我的定義的接口有很多個竭宰,那么對應(yīng)的監(jiān)聽這個接口的回調(diào)也會有很多個,我們怎么樣才能確保a方法執(zhí)行完成之后份招,會調(diào)用到a方法的CallBack呢切揭?接下來我們就要創(chuàng)建一個管理這個CallBack的類
public class UnityManager {
private static UnityManager sInstance;
private UnityAndroidProxy mUnityAndroidProxy;
private UnityCallbackManager mCallbackManager = new UnityCallbackManager();
private UnityManager() {}
public static UnityManager getInstance() {
if (sInstance == null) {
synchronized (UnityManager.class) {
if (sInstance == null) {
sInstance = new UnityManager();
}
}
}
return sInstance;
}
public void methodA(UnityMethodInvokeCallback callback, String str) {
String funcKey = mCallbackManager.addCallback(callback);
mUnityAndroidProxy.a(funcKey, str);
}
private void notifyMethodInvokeCallback(int errCode, String errMessage, String args) {
String callbackUniqueId = UnityJsonUtils.getStringValue(args, UNITY_CALLBACK_FUNCTION_KEY);
IUnityMethodCallback unityCallback = mCallbackManager.popCallback(callbackUniqueId);
if (unityCallback != null && unityCallback instanceof UnityMethodInvokeCallback) {
UnityMethodInvokeCallback methodInvokeCallback = (UnityMethodInvokeCallback) unityCallback;
if (errCode == 0) {
methodInvokeCallback.onInvokeSuccess(args);
} else {
methodInvokeCallback.onInvokeFailure(errCode, errMessage);
}
}
}
}
第三步:我們通過UnityCallBackManager這個管理類,對每一個CallBack通過hashCode的形式生成出一個唯一的md5值脾还,然后使用HashMap把MD5值和CallBack這個對象按照key-Value的形式存儲起來伴箩。在調(diào)用a方法的時候,我們多傳遞了一個參數(shù)funtionKey鄙漏,這個funtionKey就是對應(yīng)CallBack的MD5值嗤谚。如果A方法執(zhí)行完,Unity會調(diào)用notifyMethodInvokeCallback方法怔蚌,并且會把這個MD5值包含在args這個參數(shù)中巩步,Android端通過解析這個args這個json字符串,得到對應(yīng)的md5桦踊,然后通過MD5在HashMap中找到了相應(yīng)的CallBack椅野,然后我們就能根據(jù)errCode調(diào)用callBack.onSuccess或者callBack。onFailed了
public class UnityCallbackManager {
private Map<String, IUnityMethodCallback> mUnityCallbacks = new HashMap<>();
private String getCallbackUniqueId(IUnityMethodCallback callback) {
if (callback == null) {
return "";
}
return String.valueOf(callback.hashCode());
}
public String addCallback(IUnityMethodCallback callback) {
if (callback == null) {
return "";
}
String callbackUniqueId = getCallbackUniqueId(callback);
mUnityCallbacks.put(callbackUniqueId, callback);
return callbackUniqueId;
}
public @Nullable
IUnityMethodCallback popCallback(String callbackUniqueId) {
if (TextUtils.isEmpty(callbackUniqueId)) {
return null;
}
return mUnityCallbacks.remove(callbackUniqueId);
}
public @Nullable
IUnityMethodCallback peekCallback(String callbackUniqueId) {
if (TextUtils.isEmpty(callbackUniqueId)) {
return null;
}
return mUnityCallbacks.get(callbackUniqueId);
}
}
結(jié)尾
在這一章中籍胯,我們詳細討論了Android與Unity的通信方式竟闪,并且給出了一個Unity接口回調(diào)的解決方案,在下一章中我們會去討論unity 與 android 的布局管理杖狼,進一步闡述我們虛擬形象的app是如何解決Unity與Android的布局管理問題的炼蛤。