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("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 :0

at UnityEngine.AndroidJNISafe.GetMethodID (IntPtr obj, System.String name, System.String sig) [0x00000] in :0

at UnityEngine._AndroidJNIHelper.GetMethodID (IntPtr jclass, System.String methodName, System.String signature, Boolean isStatic) [0x00000] in :0

at UnityEngine.AndroidJNIHelper.GetMethodID (IntPtr javaClass, System.String methodName, System.String signature, Boolean isStatic) [0x00000] in :0

at UnityEngine._AndroidJNIHelper.GetMethodID[Int32] (IntPtr jclass, System.String methodName, System.Object[] args, Boolean isStatic) [0x00000] in :0

at UnityEngine.AndroidJNIHelper.GetMethodID[Int32] (IntPtr jclass, System.String methodName, System.Object[] args, Boolean isStatic) [0x00000] in

即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("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提示等等),所以成功失败的回调是必要的。我们可以通过刚才介绍的数据通信的方式,来优雅的实现这样的回调方式。

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

229470ca4473

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 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的布局管理问题的。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐