一 、前言
最近有幸接觸到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源碼。
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通信的中間件,因爲就目前來說,這方面的開源資料還是比較少的,希望大家一起來完善它,謝謝。