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<Activity> 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 {
    /// <summary>
    /// Android caller.封裝AndroidCall
    /// </summary>
    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<AndroidJavaObject>("currentActivity");
            //自定義Android和Unity3D交互的一個類
            androidCall = new AndroidJavaObject("com.ykbjson.lib.unity3dplugin.AndroidCall",
                                                new System.Object[] { currentActivity });
        }

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

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

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

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

很簡單的封裝,把反射android端的AndroidCall的代碼封裝起來,不用再每個script文件裏去寫重複代碼;把調用android端的方法封裝一下,統一入口。

再看看CallInfo,和android端CallInfo大同小異,這邊裝載數據是用的是Dictionary,對應java的Map結構,因爲android端用的是fastjson,他的JSONObject是實現了Map接口的

namespace AndroidCall {
    /// <summary>
    /// CallInfo.調用Android代碼時的消息封裝
    /// </summary>
    [Serializable]
    public class CallInfo {
        public String callModelName;
        public String callMethodName;
        public Dictionary<string, object> callMethodParams = new Dictionary<string, object>();
        public CallInfo child;
        public bool needCallMethodParams = true;

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

到了這裏,兩端代碼封裝基本告一段落,接下來看看兩端在封裝後如何通信。

2.1.4 兩端通信的代碼片段

unity掛載的一個腳本

/// <summary>
/// Ball controller.遊戲中小球的腳本
/// </summary>
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<Rigidbody>();
        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調用的方法------------------------------

    /// <summary>
    /// Sets the pause.
    /// </summary>
    /// <param name="param">Parameter.</param>
    public void SetPause(string param) {
        CallInfo callInfo = JsonMapper.ToObject<CallInfo>(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通信的中間件,因爲就目前來說,這方面的開源資料還是比較少的,希望大家一起來完善它,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章