(一)Unity与Android的数据通信解决方案

Unity与Android的数据通信解决方案

简述

Android 与 Unity 的交互有两种方式:Android 作为 Unity 的一部分或者把 Unity 作为 Android 的一部分。至于使用哪种方式,就要根据具体情况来决定了。

如果你的项目是以 Unity 为主( Unity 的部分需要经常改动,而 Android 的部分比较固定),就把 Android 作为 Unity 的一部分来实现交互。

如果你的项目是以 Android 为主( Android 的部分需要经常改动, Unity 部分比较固定),这时就把 Unity 作为 Android 的插件来使用。

目前我们虚拟形象的app所采用的技术方案是:Unity作为Android的一部分,这样能很好的把一些杂七杂八的业务需求,如登录注册,开屏广告,版本更新,素材维护等等交给Android端负责,Unity端只专注负责渲染形象即可。

对相机的维护是由Android端来完成的,Android端使用人脸识别sdk来对相机采集到的每一帧数据做人脸识别,会产生几种类型的数组,通过连续不断的人脸识别,然后连续不断的传递人脸数据(其实本质上就是数组),Unity就能实现是模型跟着人脸晃动的功能。

所以我们首先要解决的问题是:如何把人脸识别的出来的数组信息,传递给Unity?推到更加普遍的场景就是,Unity和android是如何进行数据通信的?

Android向Unity传递数据

sendMessage

UnityPlayer.UnitySendMessage("GameManager", "ZoomIn", "");

这段代码的意思是,调用GameManager这个游戏对象的脚本中的ZoomIn方法,由于这个ZoomIn方法不需要传入参数,所以这里我们写两个冒号代表空,但是绝不能写null,否则会遇到崩溃。

这种调用方式的缺陷也是非常明显,参数值只能传递一个,而且只支持传递String类型的参数,如果我们确实需要传递多个参数,可以把这个参数封装在一个自定义对象中,把自定义对象序列化成String来进行传输,但是由于涉及到序列化和反序列化,耗时肯定是难免的,下面的这种方式就能很好解决这个问题,他支持传递多种参数。

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关于上面接口的实现, 核心函数就是setListener和传递的参数,多态形式,这个会在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端,需要先实现这个接口,这样才可能产生回调

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方式来传递数据给Unity,非常类似Android上层的接口设计,即接口的定义和调用交给自己来做,接口的实现交给外部实现,二者通过setListener关联在一起,通过这种方式,可以传递多个参数给Unity端,但是这种方式有一个缺点就是比较负责,但是更具有扩展性和维护性。

如何才能传递数组

由于通过AnroidJavaProxy的方式,参数支持的类型是基本数据类型和自定义对象,并不支持数组类型,所以我们仍然无法传递
我们识别出来的包含人脸信息的数组,强行传输会出现以下异常

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实现的方法,他们的参数没有匹配上,所以会出现NoSuchMethodError的错误。要解决这个问题,我们需要做的就是将byte数组封装起来,可以使用如下的JavaBean

public class BytesWrapper{
    private byte[] bytes;
    public void setBytes(byte[] bytes){
        this.bytes = bytes;
    }
    public byte[] getBytes(){
        return this.bytes;
    }
}

这样我们就可以没有错误的使用AndroidJavaProxy进行反射了,同样有问题的还有long类型的数组。

Unity向Android传递数据

刚才我们提到,我们把人脸识别的byte数组,通过自定义对象包装起来传递给了Unity,那么Unity如何才能把这个自定义对象转换成byte数组呢,细心的同学已经发现了,我刚才在自定义对象中增加了getBytes方法,其实我们获取到这个自定义对象后,调用此对象的getBytes方法就能去除byte数组了。

那么问题现在就转换成,Unity如何调用Android的方法,其实非常简单,直接找到此类,然后反射调用此类的方法即可,Unity调用Android的方法,都是采用反射的方式。但是这里要特别注意,如果你反射的类不是一个静态的类存在,即全局只有一个,每次反射都相当于new一个新的类,然后调用其中的方法,可能会造成一些奇怪的问题,所以最好你反射的类是一个静态的类。由于这里我能确保调用的BytesWrapper类中的getBytes方法,实现比较简单,不会影响到Android端的其他类的运行,所以才采用反射方式。

AndroidJavaObject jo = new AndroidJavaObject("com.wenming.demo.BytesWrapper");
jo.Call<byte>("getBytes");

AndroidJavaObject 有多个调用方法,你可以根据需要调用需要的方法

方法 返回值 说明
Call void 调用类的普通方法,不返回任何对象
Call(T) T 调用类的普通方法,返回对象T
CallStatic void 调用类的静态方法,不返回任何对象
CallStatic(T) T 调用类的静态方法,此方法返回对象T
Get(T) T 获取成员变量
GetStatic(T) T 获取类的静态成员变量
Set(T) void 设置成员变量
SetStatic(T) void 设置类的成员变量

接口回调的设计

如果这篇教程止步在这里,那么和其他网络上的教程也没什么区别了。所以这里要再进一步探讨一个问题是:假设存在这样的业务流程,Android端调用Untiy 的a方法成功后,再调用Unity的b方法,ab的方法的成功失败都需要告知android端,让android端做相应的业务处理。

读者可能会问,调用是在android端的,那么a方法想必是运行在主线程的,b也是同样的运行在主线程,b方法直接写在a方法下面不就好了吗?其实不然,Unity有可能在a方法中做了一些异步操作,这样就不能保证b在a成功之后执行了,另外,如果a如果在执行过程中出错,也需要把相应的错误码返回给Android端,Android端再做相应的业务处理(失败重试,Toast提示等等),所以成功失败的回调是必要的。我们可以通过刚才介绍的数据通信的方式,来优雅的实现这样的回调方式。

下面这个时序图,描述了接口回调方案的整体调用流程,我们会用文字来详细整个流程的经过


Unity接口交互示意图.png

第一步: 定义接口,其中有2个方法,方法a与方法b,functionKey的作用我们后面再谈

public interface UnityAndroidProxy {
    void a(String functionKey, String str);
    void b(String functionKey);
}

第二步: 创建接口管理类,通过UnityManager这个类,我们对a方法做了进一层包装,后续如果Android需要调用a方法,只需要传入参数,以及监听此方法的CallBack,然后直接调用UnityManager.getInstance().methodA()方法即可。Unity在执行完a方法之后,可以去反射找到UnityManager这个单例,进而调用到notifyMethodInvokeCallback这个方法,通过这个方法,我们接受到了接受Unity传送过来的errCode,errMessage,args等参数,通过判断errCode我们可以知道A方法调用成功失败。但是这里有一个问题,如果我的定义的接口有很多个,那么对应的监听这个接口的回调也会有很多个,我们怎么样才能确保a方法执行完成之后,会调用到a方法的CallBack呢?接下来我们就要创建一个管理这个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的形式存储起来。在调用a方法的时候,我们多传递了一个参数funtionKey,这个funtionKey就是对应CallBack的MD5值。如果A方法执行完,Unity会调用notifyMethodInvokeCallback方法,并且会把这个MD5值包含在args这个参数中,Android端通过解析这个args这个json字符串,得到对应的md5,然后通过MD5在HashMap中找到了相应的CallBack,然后我们就能根据errCode调用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);
    }
}

结尾

在这一章中,我们详细讨论了Android与Unity的通信方式,并且给出了一个Unity接口回调的解决方案,在下一章中我们会去讨论unity 与 android 的布局管理,进一步阐述我们虚拟形象的app是如何解决Unity与Android的布局管理问题的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268