Android与Unity通信的SDK(一)

一 、前言

最近有幸接触到unity,也刚好有时间,索性就花了点时间来认识和学习unity,学了差不多一个多月吧,算是窥探到了一点点unity的门路,本想再继续往深处研究下的,但是在继续学习的过程中发现unity和Android通信稍微有点不太畅快,就是unity端和Android端要通信的的话,我觉得有两个问题比较麻烦:

1.两端代码的依赖度比较高。怎么说呢,如果你有一点unity3d基础,你就知道unity要调用Android的话,则必须确切的知道Android端具体的的类名、方法名以及方法需要的参数等信息,这样的话,每个unity项目的代码就比较定制化,扩展性不强;Android端调用unity端倒是比较简单,因为unity的sdk里封装好了对应的方法,不过还是有一点麻烦的地方就是,当要调用的unity端的方法太多差异太大的时候,就没有办法进行较好的封装,不便于维护。

2.unity编译生成的android项目里,呈现unity场景的Activity需要加入大量的代码,并且没有支持在fragment里呈现场景的示例,每次集成都需要做很多重复性工作。

为了解决在学习和使用unity的过程中遇到的这些问题,所以我就花了点时间实现了一个可以帮助android开发者快速对接unity工程的插件工程。这个插件工程支持使用Activity、Fragment、View等组件呈现unity场景,虽然谈不上什么高大上,但是接入简单,只需少量代码即可完成与unity工程的对接。具体的可以去看github的demo和unity3dplugin源码。

demo依赖的unity工程

android端demo和plugin源码工程

ps:demo依赖的unity工程有个文件超过了100M,无法上传到github,所以上传到了code.aliyun.com,没账号的童鞋注册后登录了再点击链接就可以了。

想要快速看到效果?

1.clone demo依赖的unity工程到本地,用unity编辑器导入,编译并导出为android project备用。

2.新建一个android项目,并参照android端demo和plugin源码工程里的demo,集成unity3dplugin,然后复制第一步生成的android project里src/main/assets目录里的内容到你工程的src/main/assets目录下,其他代码参照demo里的即可。

或者,直接clone demo编译运行。

关于unity3d入门,这里有一个很好的链接,大家要耐心看完:一个小时内用Unity3D制作一个小游戏

二 、实现思路

不了解unity3d与android通信的请先戳这里:Android与Unity交互研究

上面说到了这个插件主要是为了解决两个问题:一个是降低android端代码和unity端代码的耦合度,还有一个是降低android端的接入复杂度,那我们就一个一个来解决吧。

2.1 解决unity和android两端代码的依赖度比较高的问题

为了降低android端代码和unity端代码的耦合度,我分了两个方面来考虑:一个是android端提供给unity调用的入口统一化,unity所发来的所有消息都经过一个单一对象分发出去,这样的话,两端就不会再出现直接调用彼此具体的某个方法之类的了;另一个是两端通信的消息内容标准化,无论是unity3d调用android还是android调用unity3d,我都让他们传递一个ICallInfo来通信,至于具体要调用哪个类的那个方法,这些信息都封装在ICallInfo里。

2.1.1 android端提供给unity调用的单一入口——AndroidCall

	public class AndroidCall {
    private final String TAG = getClass().getName();
    public static boolean enableLog;

    private IOnUnity3DCall onUnity3DCall;
    private SoftReference hostContext;

    /**
     * In order to further relax the restrictions of OnUnity3DCall, let Fragment implement OnUnity3DCall can also load Unity3D view, so added {@link IGetUnity3DCall}
     *       interface.
     *
     * @param iGetUnity3DCall
     */
    public AndroidCall(@NonNull IGetUnity3DCall iGetUnity3DCall) {
        this.onUnity3DCall = iGetUnity3DCall.getOnUnity3DCall();
        hostContext = new SoftReference<>((Activity) onUnity3DCall.gatContext());
    }

    public void destroy() {
        if (null != hostContext) {
            hostContext.clear();
        }
    }

    protected void checkConfiguration() {
        if (null == hostContext || null == hostContext.get()) {
            throw new RuntimeException(getClass().getSimpleName() + " ,Invalid Context");
        }
    }

    @Nullable
    public Context getApplicationContext() {
        final Context context = getContext();
        return null == context ? null : context.getApplicationContext();
    }

    @Nullable
    public Context getContext() {
        return null == hostContext || null == hostContext.get() ? null : hostContext.get();
    }


    public void onVoidCall(@NonNull String param) {
        if (enableLog) {
            Log.d(TAG, "onVoidCall, param : " + param);
        }
        onAndroidVoidCall(CallInfo.Builder.create().build(param));
    }

    public Object onReturnCall(@NonNull String param) {
        if (enableLog) {
            Log.d(TAG, "onReturnCall, param : " + param);
        }
        return onAndroidReturnCall(CallInfo.Builder.create().build(param));
    }

    public void onAndroidVoidCall(@NonNull ICallInfo param) {
        checkConfiguration();
        if (null == onUnity3DCall) {
            return;
        }
        onUnity3DCall.onVoidCall(param);
    }

    public Object onAndroidReturnCall(@NonNull ICallInfo callInfo) {
        checkConfiguration();
        if (null == onUnity3DCall) {
            return null;
        }
        return onUnity3DCall.onReturnCall(callInfo);
    }
}

91行代码,提供的功能也简单,供unity端反射实例化,然后在需要调用android端方法的时候,直接调用这个类的onVoidCall方法或onReturnCall方法,由于unity端传递过来的消息的内容都是json字符串,这里需要把这些json字符串转换成ICallInfo对象,然后再把ICallInfo对象下发给当前持有的IOnUnity3DCall对象,实现了IOnUnity3DCall接口的对象就可以从ICallInfo里读取数据开始处理了。

至于这里面出现的IGetUnity3DCall、IOnUnity3DCall等对象,是为了android端的Fragment、View等也能显示unity场景而设计的扩展接口,后面会讲到。

2.1.2 android端和unity通信的标准化载体——ICallInfo

public interface ICallInfo extends Serializable {

    @NonNull
    String getCallMethodName();

    @NonNull
    String getCallModelName();

    @Nullable
    JSONObject getCallMethodParams();

    @Nullable
    ICallInfo getParent();

    @Nullable
    ICallInfo getChild();

    boolean isUnityCall();

    boolean isNeedCallMethodParams();

    void send();
}

android调用unity和uinty调用android还是有些不同的。android调用unity时,必需要指定modelName;而unity调用android时,不需要指定modelName,因为在unity端看来,当前呈现场景的对象必定是UnityPlayer里的currentActivity。

这里把ICallInfo定义成一个接口,是为了将来能扩展,每个项目可以根据自己实际的需要去扩展ICallInfo,以实现更适合的通信内容。但是就目前来看,我所实现的CallInfo几乎已经可以实现高度的自定义化了,因为我包含实际参数的对象是一个JSONObject对象。

我们来看看ICallInfo的实现,CallInfo

public class CallInfo implements ICallInfo {
    private String callModelName;
    private String callMethodName;
    private JSONObject callMethodParams = new JSONObject();
    private CallInfo child;
    private CallInfo parent;
    private boolean unityCall = true;
    private boolean needCallMethodParams = true;

    public CallInfo() {
    }

    CallInfo(@Nullable Builder builder) {
        this();
        if (null != builder) {
            setUnityCall(builder.unityCall)
                    .setNeedCallMethodParams(builder.needCallMethodParams)
                    .setCallModelName(builder.callModelName)
                    .setCallMethodName(builder.callMethodName)
                    .setCallMethodParams(builder.callMethodParams)
                    .setChild(builder.child)
                    .setParent(builder.parent);
        }
    }

    public CallInfo setCallModelName(@Nullable String callModelName) {
        this.callModelName = callModelName;
        return this;
    }

    public CallInfo setCallMethodName(@NonNull String callMethodName) {
        this.callMethodName = callMethodName;
        return this;
    }

    public CallInfo setCallMethodParams(@Nullable JSONObject callMethodParams) {
        if (null != callMethodParams) {
            this.callMethodParams.putAll(callMethodParams);
        }
        return this;
    }

    public CallInfo setChild(@Nullable CallInfo child) {
        this.child = child;
        return this;
    }

    public CallInfo setParent(@Nullable CallInfo parent) {
        this.parent = parent;
        return this;
    }

    public CallInfo setUnityCall(boolean unityCall) {
        this.unityCall = unityCall;
        return this;
    }

    public CallInfo setNeedCallMethodParams(boolean needCallMethodParams) {
        this.needCallMethodParams = needCallMethodParams;
        return this;
    }

    public CallInfo addCallMethodParam(@NonNull String key, @Nullable Object value) {
        this.callMethodParams.put(key, value);
        return this;
    }

    @Override
    @Nullable
    public String getCallModelName() {
        return callModelName;
    }

    @Override
    @NonNull
    public String getCallMethodName() {
        return callMethodName;
    }

    @Override
    @Nullable
    public JSONObject getCallMethodParams() {
        return callMethodParams;
    }

    @Override
    @Nullable
    public CallInfo getChild() {
        return child;
    }

    @Override
    @Nullable
    public CallInfo getParent() {
        return parent;
    }

    @Override
    public boolean isUnityCall() {
        return unityCall;
    }

    @Override
    public boolean isNeedCallMethodParams() {
        return needCallMethodParams;
    }

    @NonNull
    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }

    @Override
    public void send() {
        Unity3DCall.doUnity3DVoidCall(this);
    }

    public static class Builder {
        private String callModelName;
        private String callMethodName;
        private JSONObject callMethodParams = new JSONObject();
        private CallInfo child;
        private CallInfo parent;
        private boolean unityCall;
        private boolean needCallMethodParams = true;

        private Builder() {

        }

        public static Builder create() {
            return new Builder();
        }

        public Builder callModelName(@Nullable String callModelName) {
            this.callModelName = callModelName;
            return this;
        }

        public Builder callMethodName(@NonNull String callMethodName) {
            this.callMethodName = callMethodName;
            return this;
        }

        public Builder addCallMethodParam(@NonNull String key, @Nullable Object value) {
            this.callMethodParams.put(key, value);
            return this;
        }

        public Builder child(@Nullable CallInfo child) {
            this.child = child;
            return this;
        }

        public Builder parent(@Nullable CallInfo parent) {
            this.parent = parent;
            return this;
        }

        public Builder unityCall(boolean unityCall) {
            this.unityCall = unityCall;
            return this;
        }

        public Builder needCallMethodParams(boolean needCallMethodParams) {
            this.needCallMethodParams = needCallMethodParams;
            return this;
        }

        public CallInfo build() {
            return new CallInfo(this);
        }

        public CallInfo build(@Nullable String param) {
            return JSONObject.parseObject(param, CallInfo.class);
        }
    }
}

很简单,就是一个数据的封装,便于统一unity端和android端的通信信息,使用json传递数据,可以描述非常复杂的数据信息,轻松应对各种奇葩的数据需求。

有了AndroidCall和CallInfo,现在unity端已经可以和android端用CallInfo传递信息了,unity端的script代码也封装了一点,我们稍后再说,我们先来看android端调用unity端的代码。

public class Unity3DCall {
    /**
     * Call Unity3d
     *
     * @param callInfo Carrier for Android and Unity3D interaction
     */
    public static void doUnity3DVoidCall(@NonNull ICallInfo callInfo) {
        if(enableLog) {
            Log.i("Unity3DCall", callInfo.toString());
        }
        UnityPlayer.UnitySendMessage(callInfo.getCallModelName(), callInfo.getCallMethodName(),
                callInfo.isNeedCallMethodParams() ? callInfo.toString() : "");
    }
}

很简单吧,三个参数,modelName、methodName、param 。这其中的modelName对应的是unity组件挂载的script文件指定的名字,methodName对应的是unity组件挂载的script文件里的方法名字,param及时那个方法需要的参数啦,由于我们用ICallInfo传递数据,所以一般都是ICallInfo的json字符串。

2.1.3 unity端调用android的script的一点封装

上面已经说明了android如何调用unity端,为了unity端也比较容易的调用android端,所以我在unity端也封装了两个script文件:AndroidCaller、CallInfo

我们先看看AndroidCaller

namespace AndroidCall {
    /// 
    /// Android caller.封装AndroidCall
    /// 
    public class AndroidCaller {
        //和Android交互需要的对象
        private AndroidJavaObject androidCall;

        public AndroidCaller() {
            //Android端Activity必须持有的对象,是一个FrameLayout
            AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            //UnityPlayer构造方法取药一个hostActivity
            AndroidJavaObject currentActivity = unityPlayer.GetStatic("currentActivity");
            //自定义Android和Unity3D交互的一个类
            androidCall = new AndroidJavaObject("com.ykbjson.lib.unity3dplugin.AndroidCall",
                                                new System.Object[] { currentActivity });
        }

        public void OnVoidCall(string param) {
            CallInfo callInfo = JsonMapper.ToObject(param);
            OnVoidCall(callInfo);
        }

        public void OnVoidCall(CallInfo callInfo) {
            androidCall.Call("onVoidCall", callInfo.ToString());
        }

        public object OnReturnCall(string param) {
            CallInfo callInfo = JsonMapper.ToObject(param);
            return OnReturnCall(callInfo);
        }

        public object OnReturnCall(CallInfo callInfo) {
            return androidCall.Call("onReturnCall", callInfo.ToString());
        }
    }
}
 
  

很简单的封装,把反射android端的AndroidCall的代码封装起来,不用再每个script文件里去写重复代码;把调用android端的方法封装一下,统一入口。

再看看CallInfo,和android端CallInfo大同小异,这边装载数据是用的是Dictionary,对应java的Map结构,因为android端用的是fastjson,他的JSONObject是实现了Map接口的

namespace AndroidCall {
    /// 
    /// CallInfo.调用Android代码时的消息封装
    /// 
    [Serializable]
    public class CallInfo {
        public String callModelName;
        public String callMethodName;
        public Dictionary callMethodParams = new Dictionary();
        public CallInfo child;
        public bool needCallMethodParams = true;

        public override string ToString() {
            return JsonMapper.ToJson(this);
        }
    }
}

到了这里,两端代码封装基本告一段落,接下来看看两端在封装后如何通信。

2.1.4 两端通信的代码片段

unity挂载的一个脚本

/// 
/// Ball controller.游戏中小球的脚本
/// 
public class BallController : MonoBehaviour {
    public float speed;//可配置的移动速度
    public Text countText;//可配置的得分显示
    public Text winText;//可配置的获胜显示
    private Rigidbody rb;//当前关联的可碰撞主体
    private int count;//当前碰撞的Coin个数
    //和Android交互需要的对象
    private AndroidCaller androidCall;
    //是否暂停的标志
    private bool isPause;

    void Start() {
        //android端调用时指定的modelName
        name = "Ball";
        androidCall = new AndroidCaller();
        rb = GetComponent();
        count = 0;
        countText.text = "Coins collected: " + count;
        winText.text = "You Win!!!";
        winText.gameObject.SetActive(false);

    }

    void Update() {
        //判断是否点击了鼠标左键
        //if (Input.GetMouseButtonDown(0)) {
        //    isPause = !isPause;
        //}
    }

    void FixedUpdate() {
        //float xMov = Input.GetAxis("Horizontal");
        //float zMove = Input.GetAxis("Vertical");
        if (isPause) {
            rb.velocity = Vector3.zero;
            return;
        }
        float xMov = Input.acceleration.x;
        float zMove = Input.acceleration.y;
        Vector3 movemont = new Vector3(xMov, 0, zMove);
        //钳制加速度向量到单位球
        if (movemont.sqrMagnitude > 1) {
            movemont.Normalize();
        }
        rb.AddForce(movemont * speed, ForceMode.VelocityChange);
        //rb.//rb.MovePosition(rb.transform.position + movemont * speed);
    }

    void OnTriggerEnter(Collider other) {
        if (other.gameObject.CompareTag("Coin")) {
            CallInfo callInfo = new CallInfo {
                callMethodName = "showToast"
            };
            other.gameObject.SetActive(false);
            count++;
            countText.text = "Coins collected: " + count;
            if (count == 9) {
                gameObject.SetActive(false);
                winText.gameObject.SetActive(true);
                //通知android端显示toast
                callInfo.callMethodParams.Add("message", "恭喜你,闯关成功!");
            } else {
                //通知android端显示toast
                callInfo.callMethodParams.Add("message", "又得1分,继续加油哦!");
            }
            androidCall.OnVoidCall(callInfo);
        }
    }

    //---------------------------响应android调用的方法------------------------------

    /// 
    /// Sets the pause.
    /// 
    /// Parameter.
    public void SetPause(string param) {
        CallInfo callInfo = JsonMapper.ToObject(param);
        this.isPause = Convert.ToBoolean(callInfo.callMethodParams["isPause"]);
    }
}

在Start方法里指定自己的名字为”Ball“,有一个SetPause方法来控制游戏暂停。

android端控制游戏暂停的代码就如下所示

 //在xml文件里指定的onClick
public void setPause(View view) {
    isPause = !isPause;
    buttonPause.setText(isPause ? "继续" : "暂停");
    CallInfo.Builder
            .create()
            .callModelName("Ball")//对应的unity组件挂在的script文件指定的名字,本demo中对应BallController
            .callMethodName("SetPause")//对应的unity组件挂在的script文件里的方法名字,本demo中对应BallController的SetPause方法
            .addCallMethodParam("isPause", isPause)对应的unity组件挂在的script文件里的方法需要的参数
            .build()
            .send();
}

unity端调用android端的代码,参见上面BallController里的OnTriggerEnter方法

void OnTriggerEnter(Collider other) {
    if (other.gameObject.CompareTag("Coin")) {
        CallInfo callInfo = new CallInfo {
            callMethodName = "showToast"
        };
        other.gameObject.SetActive(false);
        count++;
        countText.text = "Coins collected: " + count;
        if (count == 9) {
            gameObject.SetActive(false);
            winText.gameObject.SetActive(true);
            //通知android端显示toast
            callInfo.callMethodParams.Add("message", "恭喜你,闯关成功!");
        } else {
            //通知android端显示toast
            callInfo.callMethodParams.Add("message", "又得1分,继续加油哦!");
        }
        androidCall.OnVoidCall(callInfo);
    }
}

android端响应unity调用的代码如下,注意switch里的showToast,和BallController里的OnTriggerEnter方法里指定的methodName是一致的

//unity3d发送过来的消息,不需要返回值
@Override
public void onVoidCall(@NonNull ICallInfo callInfo) {
    switch (callInfo.getCallMethodName()) {
        case "showToast":
            showToast(callInfo.getCallMethodParams().getString("message"));
            break;

        default:
            break;
    }
}

/**
 * 显示一个toast
 *
 * @param message
 */
private void showToast(String message) {
    Toast.makeText(this, "来自Unity的消息: " + message, Toast.LENGTH_SHORT).show();
}

至于全部的代码,大家可以去我的github看看整个工程的源码,比在这里看这些片段要容易理解得多。

2.2 适当的偷一下懒

本来这里要继续讲(吹)解(bi)android端对unity sdk封装的相关问题的,但是我感觉写到这里,篇幅似乎有点过长(贴代码贴得多_),我怕很难有人愿意坚持看下去,所在这里就不在继续讲(吹)解(bi)了。其实大家去看了源码的话,看不看我我接下来的文章也没有多大意义了,因为实在是太简单了。迫使我想继续写下去的唯一理由就是我想把当时封装unity sdk时的一些思考分享给大家,让大家真正的理解我为什么会那样去做,而不是直接引用了这个库或者只是翻了翻源码,然后就import到你们的工程开始使用。我希望的是大家可以散发思路,写出更优秀的unity与android通信的中间件,因为就目前来说,这方面的开源资料还是比较少的,希望大家一起来完善它,谢谢。

你可能感兴趣的:(Android开发,unity3d,unity,unity3d,unity与android通信)