(一)Unity與Android的數(shù)據(jù)通信解決方案

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)過


Unity接口交互示意圖.png

第一步: 定義接口岔留,其中有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的布局管理問題的炼蛤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蝶涩,隨后出現(xiàn)的幾起案子理朋,更是在濱河造成了極大的恐慌絮识,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗽上,死亡現(xiàn)場離奇詭異次舌,居然都是意外死亡,警方通過查閱死者的電腦和手機兽愤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門彼念,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人烹看,你說我怎么就攤上這事国拇。” “怎么了惯殊?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵酱吝,是天一觀的道長。 經(jīng)常有香客問我土思,道長务热,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任己儒,我火速辦了婚禮崎岂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闪湾。我一直安慰自己冲甘,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布途样。 她就那樣靜靜地躺著江醇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪何暇。 梳的紋絲不亂的頭發(fā)上陶夜,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音裆站,去河邊找鬼条辟。 笑死,一個胖子當著我的面吹牛宏胯,可吹牛的內(nèi)容都是我干的羽嫡。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼肩袍,長吁一口氣:“原來是場噩夢啊……” “哼杭棵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起了牛,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤颜屠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鹰祸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甫窟,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年蛙婴,在試婚紗的時候發(fā)現(xiàn)自己被綠了粗井。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡街图,死狀恐怖浇衬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情餐济,我是刑警寧澤耘擂,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站絮姆,受9級特大地震影響醉冤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜篙悯,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一蚁阳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鸽照,春花似錦螺捐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至漏峰,卻和暖如春糠悼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背浅乔。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工倔喂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人靖苇。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓席噩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親贤壁。 傳聞我的和親對象是個殘疾皇子悼枢,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361