最近有幸接触到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端的接入复杂度,那我们就一个一个来解决吧。
为了降低android端代码和unity端代码的耦合度,我分了两个方面来考虑:一个是android端提供给unity调用的入口统一化,unity所发来的所有消息都经过一个单一对象分发出去,这样的话,两端就不会再出现直接调用彼此具体的某个方法之类的了;另一个是两端通信的消息内容标准化,无论是unity3d调用android还是android调用unity3d,我都让他们传递一个ICallInfo来通信,至于具体要调用哪个类的那个方法,这些信息都封装在ICallInfo里。
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场景而设计的扩展接口,后面会讲到。
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字符串。
上面已经说明了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
很简单的封装,把反射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);
}
}
}
到了这里,两端代码封装基本告一段落,接下来看看两端在封装后如何通信。
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看看整个工程的源码,比在这里看这些片段要容易理解得多。
本来这里要继续讲(吹)解(bi)android端对unity sdk封装的相关问题的,但是我感觉写到这里,篇幅似乎有点过长(贴代码贴得多_),我怕很难有人愿意坚持看下去,所在这里就不在继续讲(吹)解(bi)了。其实大家去看了源码的话,看不看我我接下来的文章也没有多大意义了,因为实在是太简单了。迫使我想继续写下去的唯一理由就是我想把当时封装unity sdk时的一些思考分享给大家,让大家真正的理解我为什么会那样去做,而不是直接引用了这个库或者只是翻了翻源码,然后就import到你们的工程开始使用。我希望的是大家可以散发思路,写出更优秀的unity与android通信的中间件,因为就目前来说,这方面的开源资料还是比较少的,希望大家一起来完善它,谢谢。