Unity与Android互调⽤
Unity 项⽬中⼀些需要访问安卓操作系统的功能,⽐如获取电量,wifi 状态等,需要 Unity 启动安卓系统的BroadcastReceiver监听状态,并在状态更新后通知到 Unity 界⾯。这就需要⼀种 Unity 与 Android 互相调⽤的机制,直观地看就是 C# 与 Java 互相调⽤的⽅法。
有 Unity 与 Android 互相调⽤需求的项⽬需要在两个开发环境中同时进⾏,创建两个⼯程,这时就涉及到如何将两个⼯程连接起来,有两种⽅式来连接:
Android ⼯程⽣成 aar/jar ⽂件,复制到 Unity ⼯程中,最终使⽤ Unity 的 Build 机制⽣成 apk。
Unity ⼯程将所有内容和代码导出为⼀个 Android gradle 项⽬,然后使⽤ Android Studio 打开项⽬进⾏开发,最终使⽤ Android Studio 打包 apk。
对⽐⼀下两者的优缺点:
Unity 使⽤ jar/aar 库Unity 导出 gradle 项⽬
Unity 与 Android 依赖性Unity 只依赖 Android 库⽂件,分割清晰,需要同步的⽂件只有库
⽂件
Android 依赖 Unity 导出的场景数据,需要同步的
⽂件太多
开发调试速度Android 库⽂件⽐较⼩,调试较快Unity ⼯程较⼤,同步较慢,调试周期长
Build机制Unity 内置的 Android Build 机制,类似于 eclipse 编译 Android
项⽬
Android Studio gradle
Build灵活性较差,⽆法深度定制,库有依赖时需要将全部依赖显式拷贝到
Unity ⼯程中⾮常⾃由,可以使⽤最新的 Android Build 机制
如何打包apk Unity Build 机制直接打包Android Studio 打包
本项⽬使⽤的是第⼀种⽅法,因为这个项⽬中 Unity ⼯程特别⼤,导出 Unity ⼯程的代价太⼤。但也遇到了库⽂件依赖问题,不过由于依赖项不是很多,可以⼿动解决。以下是解决思路:
运⾏ gradle task dependencies,可以在 “Gradle projects” 窗⼝中⽬标项⽬的 help ⽬录中到,这个 task 会打印出树形结构的依赖关系。
将所有的依赖项单独下载到本地,放到 Unity ⼯程中。
从这两个步骤可以看出,如果依赖层次⽐较少、数量⽐较少,还是可以接受的,但如果有⼤量深层的依赖就会变得特别⿇烦。
查看依赖树
Unity 调⽤ Android
Unity官⽅⽂档说明需要通过Plugin的⽅式调⽤Java代码,但实际上不需要引⼊任何Plugin就可以调⽤Java代码。只是⼀般情况下需要调⽤的都是封装好的库,这时才需要将 jar 或者 aar 放到 Unity 项⽬中,然后通过 C# 来访问其中的内容。
jar 或者 aar ⽂件可以放在Unity任意⽬录下,为了⽅便管理,都放在了Assets/Plugins/Android⽬录下。
C# 调⽤ Java ⽅法,获取 Java 字段
C# 调⽤ Java 的底层原理是使⽤JNI调⽤,Unity已经提供了很⽅便的接⼝:
创建对象:C#中使⽤AndroidJavaObject类封装 Java 对象,new ⼀个AndroidJavaObject对象相当于调⽤对应的 Java 对象的构造函数。借助 C# 可变参数列表,可以给 Java 对象的构造函数传递任意数量的参数。
// 第⼀个参数是 Java 类的完整包名,剩下的其他参数会传递给构造⽅法。
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string");
调⽤对象⽅法:使⽤AndroidJavaObject类的 Call ⽅法,有泛型与⾮泛型的两个版本。
// 泛型版本,⽬的是指定返回值的类型
int hash = jo.Call<int>("hashCode");
// ⾮泛型版本,处理返回值是void的情况。
jo.Call("aMethodReturnVoid"); // String中没有返回void的简单⽅法。。。
获取类,主要⽤于获取静态字段或调⽤静态⽅法,常⽤来获取 UnityPlayer。
// 传⼊类的完整包名
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
获取静态字段,只有泛型版本,因为不会有void类型的字段。。。
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
设置字段、获取对象字段、调⽤静态⽅法的代码类似,略。
类型映射
调⽤ Java ⽅法时,直接将 C# 变量/常量传递给 Java ⽅法,会⾃动处理 C# 类型到 Java 类型的转换。通过 C# 泛型,可以指定 Java ⽅法的返回值类型,也就是将 Java 类型转换为了 C# 类型。C# 类型与 Java 类型是可以⾃动转换的,规则如下:
Java 类型C# 类型
基本类型,⽐如int, boolean对应的值类型int, bool
String string
数组类型数组类型 (不能是多维数组)
其他继承⾃Object的类型AndroidJavaObject
Android 调⽤ Unity
从 Android 端并不能直接调⽤ Unity 脚本,⽽是通过消息发送或者接⼝回调的⽅式。
消息发送⽅式
消息发送是⼀个⾮常简单的调⽤机制,建⽴在⼀个发消息的接⼝之上:
// objectName: Unity 对象的名称
// methodName: Unity 对象绑定的脚本⽅法名
// message: ⾃定义消息
UnityPlayer.UnitySendMessage(String objectName, String methodName, String message);
做⼀下简单的封装:
import com.unity3d.player.UnityPlayer;
public class UnityPlayerCallback {
public final String objectName;
public final String methodName;
public UnityPlayerCallback(String objectName, String methodName) {
this.objectName = objectName;
}
public void invoke(String message) {
UnityPlayer.UnitySendMessage(objectName, methodName, message);
}
}
发送消息需要知道 Unity 对象的名称和⽅法名,⽽这些信息在 Android 端是不知道的,也不应该写死在 Java 代码⾥。因为 Unity 脚本相对于Android 代码是上层客户代码,调⽤的是 Android 库⽂件提供的功能,库⽂件是不应该知道使⽤它的客户代码的任何具体信息的。
正确的做法是通过某种⽅式将这些信息注⼊到库中,最简单地,使⽤ C# 调⽤ Java 端的代码将这两个字符串保存到 Java 对象中。
下⾯的⽰例规定了⼀个简单的消息格式:消息=类型/数据。
// Java 代码
public class Downloader {
private UnityPlayerCallback mUnityCallback;
public void setDownloadCallback(String objectName, String methodName) {
mUnityCallback = new UnityPlayerCallback(objectName, methodName);
}
...
void onDownloaded(File file, String url) {
if (mUnityCallback != null) {
mUnityCallback.invoke("downloaded/" + Name());
}
}
}
// C# 脚本:
void OnStart()
{
AndroidJavaObject downloader = new AndroidJavaObject("my.package.Downloader");
downloader.Call("setDownloadCallback", gameObject.name, "OnJavaMessage");
}
void OnJavaMessage(string message)
{
// 这⾥解析 message,例:""
if (message.StartsWith("downloaded/")
{
// 处理下载完成的逻辑...
}
}
由于这种⽅式⽐较粗糙,⽽且绕不开消息处理⽅法,如果有多个回调⽅法、传递的数据⽐较复杂,就需要定义复杂的消息传递格式。
接⼝调⽤⽅式
这种⽅法使⽤起来⽐较⾃然,按照 Java 的风格定义好 Java 的回调接⼝,然后在 C# 脚本中通过继承AndroidJavaProxy类来实现这个 Java 的接⼝。通过 Java 侧提供的回调设置⽅法将实现了接⼝的 C# 对象设置给 Java 代码,就完成了 Java 设置 C# 回调的过程。
下⾯举例说明这个⽅法:
Java 代码定义⼀个下载⼯具类,使⽤⼀个下载进度和状态接⼝通知调⽤者:
// 回调接⼝
public interface DownloadListener {
void onProgress(String name, int progress);
void onDownloaded(String name);
void onError(String name, String message);
}
// 下载⼯具类
public class DownloadHelper {
public void download(String url, String name) {...}
public void setDownloadListener(DownloadListener listener) {...}
}
C# 代码同样定义⼀个同名的DownloadHelper类,⽤来封装对 Java 对象的调⽤:
public class DownloadHelper {
// 定义 C# 端的接⼝,对外隐藏 Java 相关代码
public interface IDownloadListener {
void OnProgress(string name, int progress);
void OnDownloaded(string name);
void OnError(string name, string message);
}
// 定义个 Adapter 来适配 AndroidJavaProxy 对象和 IDownloadListener
private class ListenerAdapter : AndroidJavaProxy {
private readonly IDownloadListener listener;
public ListenerAdapter(IDownloadListener listener) : base("my.package.DownloadListener") {
this.listener = listener;
}
// 继承⾃ AndroidJavaProxy 的对象可以直接按照 Java 中的⽅法签名
/
/ 写出对应的 C# ⽅法,参数类型遵循上⽂提到的数据类型转换规则。
// 当 Java 调⽤接⼝⽅法时,对应的 C# ⽅法会⾃动调⽤,⾮常⽅便。
void onProgress(string name, int progress) {
listener.OnProgress(name, progress);
}
void onDownloaded(string name) {
listener.OnDownloaded(name);
}
void onError(string name, string message) {
listener.OnError(name, message);
}
}
private readonly AndroidJavaObject javaObject;
private ListenerAdapter listenerAdapter;
public DownloadHelper() {
javaObject = new AndroidJavaObject("my.package.DownloadHelper", DefaultDirectory);
}
public void SetDownloadListener(IDownloadListener listener) {
if (listener != null) {
listenerAdapter = new ListenerAdapter(listener);
javaObject.Call("setDownloadListener", listenerAdapter);
} else {
listenerAdapter = null;
javaObject.Call("setDownloadListener", null);
}
}
public void Download(string url, string name) {
javaObject.Call("download", url, name);
}
// 初始化下载⽬录
private static string DefaultDirectory;
static DownloadHelper() {
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
string path = jo.Call<AndroidJavaObject>("getExternalFilesDir", "videos").Call<string>("getCanonicalPath");
DefaultDirectory = path;
}
}
使⽤的时候,直接使⽤ C# DownloadHelper类配合DownloadHelper.IDownloadListener即可。
后记:
第⼆种实现的⽅式交给写 Unity 脚本的同事后发现⼀个问题:由于下载模块的回调是在安卓UI线程执⾏的,这个线程并不是 Unity 的主线程,回调到 Unity 环境中不能执⾏各种对象的操作。因此需要通知 Unity 主线程并在其中执⾏回调。
C# 代码修改如下,使⽤了Loom类,有点类似于安卓的Handler,可以参考这篇⽂章
private class ListenerAdapter : AndroidJavaProxy {
...
void onProgress(string name, int progress) {
Loom.QueueOnMainThread((param) => {
listener.OnProgress(name, progress);
}, null);
}
void onDownloaded(string name) {
Loom.QueueOnMainThread((param) => {
listener.OnDownloaded(name);
}, null);
}
void onError(string name, string message) {
Loom.QueueOnMainThread((param) => {
listener.OnError(name, message);
}, null);
}
}
如何直接获得安卓⼴播
虽然可以在安卓层使⽤BroadcastReceiver接收⼴播,并通过⾃定义的⽅法传递给 C# 层。但如果能在 C# 端直接接收就更⽅便了,于是后来⼜写了⼀个通⽤的⼴播接收层。
先来看⼀下如何使⽤这个⼴播接收层,设计这个层的主要⽬的有两个:⼀是能直接在 C# 代码中注册安卓⼴播,另⼀个是使⽤的代码要⾜够简单。
先上使⽤的代码:
public class BTHolder : MonoBehaviour, UnityBroadcastHelper.IBroadcastListener {
UnityBroadcastHelper helper;
void Start() {
if (helper == null) {
helper = UnityBroadcastHelper.Register(
new string[] { "some_action_string" }, this);
}
}
void OnReceive(string action, Dictionary<string, string> dictionary) {
// handle the broadcast
}
}
可以看到使⽤⼴播需要4个步骤:
1. 实现UnityBroadcastHelper.IBroadcastListener接⼝。
2. 定义⼀个UnityBroadcastHelper对象并初始化。
3. 在⽅法void OnReceive(string action, Dictionary<string, string> dictionary)中⾃定义⼴播处理代码。
4. 在合适的时机调⽤ helper.Stop() 停⽌监听⼴播。
可以看出与 Java 代码中⾃定义BroadcastReceiver⼏乎是相同的步骤,下⾯分析⼀下原理。
1. 先使⽤⼀个 Java 对象UnityBroadcastHelper来持有BroadcastReceiver,再通过 Java 代码注册到 Context 中。
2. 再使⽤上⽂提到的接⼝⽅式将UnityBroadcastHelper.BroadcastListener映射为 C# 中的UnityBroadcastHelper.IBroadcastListener。这样在 Java 端
接收到⼴播时调⽤ C# 端的接⼝,就可以通知 C# ⼴播已经接收到。
3. 最后使⽤数据获取接⼝将⼴播中的数据,也就是保存 Extra 的 Bundle,映射为 C# 中的 Dictionary,传递给OnReceive⽅法,⽅便 C#
使⽤。这⾥为了简单把所有类型的数据都映射为了 string 类型,这个映射⽐较繁琐,有需要可以再写详细⼀些。
Java 代码:
t.BroadcastReceiver;
t.Context;
t.Intent;
t.IntentFilter;
import android.os.Bundle;
import com.unity3d.player.UnityPlayer;
import java.util.LinkedList;
import java.util.Queue;
public class UnityBroadcastHelper {
private static final String TAG = "UnityBroadcastHelper";
public interface BroadcastListener {
void onReceive(String action);
}
private final BroadcastListener listener;
android最新版private Queue<String[]> keysQueue = new LinkedList<>();
private Queue<String[]> valuesQueue = new LinkedList<>();
public UnityBroadcastHelper(String[] actions, BroadcastListener listener) {
MyLog.d(TAG, "UnityBroadcastHelper: actions: " + actions);
MyLog.d(TAG, "UnityBroadcastHelper: listener: " + listener);
this.listener = listener;
IntentFilter intentFilter = new IntentFilter();
for (String action : actions) {
intentFilter.addAction(action);
}
Context context = UnityPlayer.currentActivity;
if (context == null) {
return;
}
}
public boolean hasKeyValue() {
return !keysQueue.isEmpty();
}
public String[] getKeys() {
return keysQueue.peek();
}
public String[] getValues() {
return valuesQueue.peek();
}
public void pop() {
keysQueue.poll();
valuesQueue.poll();
}
public void stop() {
Context context = UnityPlayer.currentActivity;
if (context == null) {
return;
}
context.unregisterReceiver(broadcastReceiver);
}
private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = Action();
MyLog.d(TAG, "UnityBroadcastHelper: action: " + action);
Bundle bundle = Extras();
if (bundle == null) {
bundle = new Bundle();
}
int n = bundle.size();
String[] keys = new String[n];
String[] values = new String[n];
int i = 0;
for (String key : bundle.keySet()) {
keys[i] = key;
Object value = (key);
values[i] = value != null ? String() : null;
MyLog.d(TAG, "UnityBroadcastHelper: key[" + i + "]: " + key);
MyLog.d(TAG, "UnityBroadcastHelper: value[" + i + "]: " + value);
i++;
}
keysQueue.offer(keys);
valuesQueue.offer(values);
}
};
}
C# 代码:
using System.Collections.Generic;
using UnityEngine;
public class UnityBroadcastHelper {
public interface IBroadcastListener {
void OnReceive(string action, Dictionary<string, string> dictionary);
}
private class ListenerAdapter : AndroidJavaProxy {
readonly IBroadcastListener listener;
readonly UnityBroadcastHelper helper;
public ListenerAdapter(IBroadcastListener listener, UnityBroadcastHelper helper) : base("UnityBroadcastHelper$BroadcastListener") { this.listener = listener;
this.helper = helper;
}
void onReceive(string action) {
AndroidJavaObject javaObject = helper.javaObject;
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论