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");
// 第一个参数是 Java 类的完整包名,剩下的其他参数会传递给构造方法。
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string");
- 调用对象方法:使用
AndroidJavaObject
类的 Call 方法,有泛型与非泛型的两个版本。
// 泛型版本,目的是指定返回值的类型
int hash = jo.Call<int>("hashCode"); // 非泛型版本,处理返回值是void的情况。 jo.Call("aMethodReturnVoid"); // String中没有返回void的简单方法。。。
// 泛型版本,目的是指定返回值的类型
int hash = jo.Call<int>("hashCode"); // 非泛型版本,处理返回值是void的情况。 jo.Call("aMethodReturnVoid"); // String中没有返回void的简单方法。。。
- 获取类,主要用于获取静态字段或调用静态方法,常用来获取 UnityPlayer。
// 传入类的完整包名
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.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# 类型 |
基本类型,比如 | 对应的值类型 |
|
|
数组类型 | 数组类型 (不能是多维数组) |
其他继承自 |
|
Android 调用 Unity
从 Android 端并不能直接调用 Unity 脚本,而是通过消息发送或者接口回调的方式。
消息发送方式
消息发送是一个非常简单的调用机制,建立在一个发消息的接口之上:
// objectName: Unity 对象的名称
// methodName: Unity 对象绑定的脚本方法名
// message: 自定义消息
UnityPlayer.UnitySendMessage(String objectName, String methodName, String message);
// 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; this.methodName = methodName; } public void invoke(String message) { UnityPlayer.UnitySendMessage(objectName, methodName, 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; this.methodName = methodName; } 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/" + file.getName()); } } }
// 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/" + file.getName()); } } }
// C# 脚本:
void OnStart() { AndroidJavaObject downloader = new AndroidJavaObject("my.package.Downloader"); downloader.Call("setDownloadCallback", gameObject.name, "OnJavaMessage"); } void OnJavaMessage(string message) { // 这里解析 message,例:"download/filename.txt" if (message.StartsWith("downloaded/") { // 处理下载完成的逻辑... } }
// C# 脚本:
void OnStart() { AndroidJavaObject downloader = new AndroidJavaObject("my.package.Downloader"); downloader.Call("setDownloadCallback", gameObject.name, "OnJavaMessage"); } void OnJavaMessage(string message) { // 这里解析 message,例:"download/filename.txt" 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) {...} }
// 回调接口
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
,可以参考这篇文章 Unity Loom 插件使用
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); } }
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 } }
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个步骤:
- 实现
UnityBroadcastHelper.IBroadcastListener
接口。 - 定义一个
UnityBroadcastHelper
对象并初始化。 - 在方法
void OnReceive(string action, Dictionary<string, string> dictionary)
中自定义广播处理代码。 - 在合适的时机调用 helper.Stop() 停止监听广播。
可以看出与 Java 代码中自定义 BroadcastReceiver
几乎是相同的步骤,下面分析一下原理。
- 先使用一个 Java 对象
UnityBroadcastHelper
来持有BroadcastReceiver
,再通过 Java 代码注册到 Context 中。 - 再使用上文提到的接口方式将
UnityBroadcastHelper.BroadcastListener
映射为 C# 中的UnityBroadcastHelper.IBroadcastListener
。这样在 Java 端接收到广播时调用 C# 端的接口,就可以通知 C# 广播已经接收到。 - 最后使用数据获取接口将广播中的数据,也就是保存 Extra 的 Bundle,映射为 C# 中的 Dictionary,传递给
OnReceive
方法,方便 C# 使用。这里为了简单把所有类型的数据都映射为了 string 类型,这个映射比较繁琐,有需要可以再写详细一些。
Java 代码:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.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; 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; } context.registerReceiver(broadcastReceiver, intentFilter); } 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 = intent.getAction(); MyLog.d(TAG, "UnityBroadcastHelper: action: " + action); Bundle bundle = intent.getExtras(); 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 = bundle.get(key); values[i] = value != null ? value.toString() : null; MyLog.d(TAG, "UnityBroadcastHelper: key[" + i + "]: " + key); MyLog.d(TAG, "UnityBroadcastHelper: value[" + i + "]: " + value); i++; } keysQueue.offer(keys); valuesQueue.offer(values); listener.onReceive(action); } }; }
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.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; 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; } context.registerReceiver(broadcastReceiver, intentFilter); } 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 = intent.getAction(); MyLog.d(TAG, "UnityBroadcastHelper: action: " + action); Bundle bundle = intent.getExtras(); 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 = bundle.get(key); values[i] = value != null ? value.toString() : null; MyLog.d(TAG, "UnityBroadcastHelper: key[" + i + "]: " + key); MyLog.d(TAG, "UnityBroadcastHelper: value[" + i + "]: " + value); i++; } keysQueue.offer(keys); valuesQueue.offer(values); listener.onReceive(action); } }; }
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; if (!javaObject.Call<bool>("hasKeyValue")) { return; } string[] keys = javaObject.Call<string[]>("getKeys"); string[] values = javaObject.Call<string[]>("getValues"); javaObject.Call("pop"); Dictionary<string, string> dictionary = new Dictionary<string, string>(); Debug.Log("onReceive: dictionary: " + dictionary); int n = keys.Length; for (int i = 0; i < n; i++) { dictionary[keys[i]] = values[i]; } listener.OnReceive(action, dictionary); } } private readonly AndroidJavaObject javaObject; private UnityBroadcastHelper(string[] actions, IBroadcastListener listener) { ListenerAdapter adapter = new ListenerAdapter(listener, this); javaObject = new AndroidJavaObject("UnityBroadcastHelper", actions, adapter); } public static UnityBroadcastHelper Register(string[] actions, IBroadcastListener listener) { return new UnityBroadcastHelper(actions, listener); } public void Stop() { javaObject.Call("stop"); } }