Unity 項目中一些需要訪問安卓操作系統(tǒng)的功能缓艳,比如獲取電量校摩,wifi 狀態(tài)等,需要 Unity 啟動安卓系統(tǒng)的 BroadcastReceiver
監(jiān)聽狀態(tài)阶淘,并在狀態(tài)更新后通知到 Unity 界面衙吩。這就需要一種 Unity 與 Android 互相調(diào)用的機制,直觀地看就是 C# 與 Java 互相調(diào)用的方法舶治。
有 Unity 與 Android 互相調(diào)用需求的項目需要在兩個開發(fā)環(huán)境中同時進行分井,創(chuàng)建兩個工程车猬,這時就涉及到如何將兩個工程連接起來,有兩種方式來連接:
- Android 工程生成 aar/jar 文件尺锚,復(fù)制到 Unity 工程中珠闰,最終使用 Unity 的 Build 機制生成 apk。
- Unity 工程將所有內(nèi)容和代碼導(dǎo)出為一個 Android gradle 項目瘫辩,然后使用 Android Studio 打開項目進行開發(fā)伏嗜,最終使用 Android Studio 打包 apk。
對比一下兩者的優(yōu)缺點:
Unity 使用 jar/aar 庫 | Unity 導(dǎo)出 gradle 項目 | |
---|---|---|
Unity 與 Android 依賴性 | Unity 只依賴 Android 庫文件伐厌,分割清晰承绸,需要同步的文件只有庫文件 | Android 依賴 Unity 導(dǎo)出的場景數(shù)據(jù),需要同步的文件太多 |
開發(fā)調(diào)試速度 | Android 庫文件比較小挣轨,調(diào)試較快 | Unity 工程較大军熏,同步較慢,調(diào)試周期長 |
Build機制 | Unity 內(nèi)置的 Android Build 機制卷扮,類似于 eclipse 編譯 Android 項目 | Android Studio gradle |
Build靈活性 | 較差荡澎,無法深度定制,庫有依賴時需要將全部依賴顯式拷貝到 Unity 工程中 | 非常自由晤锹,可以使用最新的 Android Build 機制 |
如何打包apk | Unity Build 機制直接打包 | Android Studio 打包 |
本項目使用的是第一種方法摩幔,因為這個項目中 Unity 工程特別大,導(dǎo)出 Unity 工程的代價太大鞭铆。但也遇到了庫文件依賴問題或衡,不過由于依賴項不是很多,可以手動解決车遂。以下是解決思路:
- 運行 gradle task
dependencies
封断,可以在 “Gradle projects” 窗口中目標項目的 help 目錄中找到,這個 task 會打印出樹形結(jié)構(gòu)的依賴關(guān)系艰额。 - 將所有的依賴項單獨下載到本地澄港,放到 Unity 工程中。
從這兩個步驟可以看出柄沮,如果依賴層次比較少回梧、數(shù)量比較少,還是可以接受的祖搓,但如果有大量深層的依賴就會變得特別麻煩狱意。
Unity 調(diào)用 Android
Unity官方文檔說明需要通過Plugin的方式調(diào)用Java代碼,但實際上不需要引入任何Plugin就可以調(diào)用Java代碼拯欧。只是一般情況下需要調(diào)用的都是封裝好的庫详囤,這時才需要將 jar 或者 aar 放到 Unity 項目中,然后通過 C# 來訪問其中的內(nèi)容。
jar 或者 aar 文件可以放在Unity任意目錄下藏姐,為了方便管理隆箩,都放在了 Assets/Plugins/Android
目錄下。
C# 調(diào)用 Java 方法羔杨,獲取 Java 字段
C# 調(diào)用 Java 的底層原理是使用JNI調(diào)用捌臊,Unity已經(jīng)提供了很方便的接口:
- 創(chuàng)建對象:C#中使用
AndroidJavaObject
類封裝 Java 對象,new 一個AndroidJavaObject
對象相當(dāng)于調(diào)用對應(yīng)的 Java 對象的構(gòu)造函數(shù)兜材。借助 C# 可變參數(shù)列表理澎,可以給 Java 對象的構(gòu)造函數(shù)傳遞任意數(shù)量的參數(shù)。
// 第一個參數(shù)是 Java 類的完整包名曙寡,剩下的其他參數(shù)會傳遞給構(gòu)造方法糠爬。
AndroidJavaObject jo = new AndroidJavaObject("java.lang.String", "some_string");
- 調(diào)用對象方法:使用
AndroidJavaObject
類的 Call 方法,有泛型與非泛型的兩個版本举庶。
// 泛型版本执隧,目的是指定返回值的類型
int hash = jo.Call<int>("hashCode");
// 非泛型版本,處理返回值是void的情況灯变。
jo.Call("aMethodReturnVoid"); // String中沒有返回void的簡單方法殴玛。。添祸。
- 獲取類,主要用于獲取靜態(tài)字段或調(diào)用靜態(tài)方法寻仗,常用來獲取 UnityPlayer刃泌。
// 傳入類的完整包名
AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
- 獲取靜態(tài)字段,只有泛型版本署尤,因為不會有void類型的字段耙替。。曹体。
AndroidJavaObject jo = jc.GetStatic<AndroidJavaObject>("currentActivity");
設(shè)置字段俗扇、獲取對象字段、調(diào)用靜態(tài)方法的代碼類似箕别,略铜幽。
類型映射
調(diào)用 Java 方法時,直接將 C# 變量/常量 傳遞給 Java 方法串稀,會自動處理 C# 類型到 Java 類型的轉(zhuǎn)換除抛。通過 C# 泛型,可以指定 Java 方法的返回值類型母截,也就是將 Java 類型轉(zhuǎn)換為了 C# 類型到忽。C# 類型與 Java 類型是可以自動轉(zhuǎn)換的,規(guī)則如下:
Java 類型 | C# 類型 |
---|---|
基本類型清寇,比如 int , boolean
|
對應(yīng)的值類型 int , bool
|
String |
string |
數(shù)組類型 | 數(shù)組類型 (不能是多維數(shù)組) |
其他繼承自 Object 的類型 |
AndroidJavaObject |
Android 調(diào)用 Unity
從 Android 端并不能直接調(diào)用 Unity 腳本喘漏,而是通過消息發(fā)送或者接口回調(diào)的方式护蝶。
消息發(fā)送方式
消息發(fā)送是一個非常簡單的調(diào)用機制,建立在一個發(fā)消息的接口之上:
// 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);
}
}
發(fā)送消息需要知道 Unity 對象的名稱和方法名翩迈,而這些信息在 Android 端是不知道的滓走,也不應(yīng)該寫死在 Java 代碼里。因為 Unity 腳本相對于 Android 代碼是上層客戶代碼帽馋,調(diào)用的是 Android 庫文件提供的功能搅方,庫文件是不應(yīng)該知道使用它的客戶代碼的任何具體信息的。
正確的做法是通過某種方式將這些信息注入到庫中绽族,最簡單地姨涡,使用 C# 調(diào)用 Java 端的代碼將這兩個字符串保存到 Java 對象中。
下面的示例規(guī)定了一個簡單的消息格式:消息=類型/數(shù)據(jù)吧慢。
// 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/")
{
// 處理下載完成的邏輯...
}
}
由于這種方式比較粗糙,而且繞不開消息處理方法检诗,如果有多個回調(diào)方法匈仗、傳遞的數(shù)據(jù)比較復(fù)雜,就需要定義復(fù)雜的消息傳遞格式逢慌。
接口調(diào)用方式
這種方法使用起來比較自然悠轩,按照 Java 的風(fēng)格定義好 Java 的回調(diào)接口,然后在 C# 腳本中通過繼承 AndroidJavaProxy
類來實現(xiàn)這個 Java 的接口攻泼。通過 Java 側(cè)提供的回調(diào)設(shè)置方法將實現(xiàn)了接口的 C# 對象設(shè)置給 Java 代碼火架,就完成了 Java 設(shè)置 C# 回調(diào)的過程。
下面舉例說明這個方法:
Java 代碼定義一個下載工具類忙菠,使用一個下載進度和狀態(tài)接口通知調(diào)用者:
// 回調(diào)接口
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 對象的調(diào)用:
public class DownloadHelper {
// 定義 C# 端的接口,對外隱藏 Java 相關(guān)代碼
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 中的方法簽名
// 寫出對應(yīng)的 C# 方法牛欢,參數(shù)類型遵循上文提到的數(shù)據(jù)類型轉(zhuǎn)換規(guī)則骡男。
// 當(dāng) Java 調(diào)用接口方法時,對應(yīng)的 C# 方法會自動調(diào)用傍睹,非常方便隔盛。
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
即可焰望。
后記:
第二種實現(xiàn)的方式交給寫 Unity 腳本的同事后發(fā)現(xiàn)一個問題:由于下載模塊的回調(diào)是在安卓UI線程執(zhí)行的骚亿,這個線程并不是 Unity 的主線程,回調(diào)到 Unity 環(huán)境中不能執(zhí)行各種對象的操作熊赖。因此需要通知 Unity 主線程并在其中執(zhí)行回調(diào)来屠。
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);
}
}
如何直接獲得安卓廣播
雖然可以在安卓層使用 BroadcastReceiver
接收廣播捆姜,并通過自定義的方法傳遞給 C# 層。但如果能在 C# 端直接接收就更方便了迎膜,于是后來又寫了一個通用的廣播接收層泥技。
先來看一下如何使用這個廣播接收層,設(shè)計這個層的主要目的有兩個:一是能直接在 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個步驟:
- 實現(xiàn)
UnityBroadcastHelper.IBroadcastListener
接口。 - 定義一個
UnityBroadcastHelper
對象并初始化榕订。 - 在方法
void OnReceive(string action, Dictionary<string, string> dictionary)
中自定義廣播處理代碼店茶。 - 在合適的時機調(diào)用 helper.Stop() 停止監(jiān)聽廣播。
可以看出與 Java 代碼中自定義 BroadcastReceiver
幾乎是相同的步驟劫恒,下面分析一下原理贩幻。
- 先使用一個 Java 對象
UnityBroadcastHelper
來持有BroadcastReceiver
,再通過 Java 代碼注冊到 Context 中两嘴。 - 再使用上文提到的接口方式將
UnityBroadcastHelper.BroadcastListener
映射為 C# 中的UnityBroadcastHelper.IBroadcastListener
丛楚。這樣在 Java 端接收到廣播時調(diào)用 C# 端的接口,就可以通知 C# 廣播已經(jīng)接收到憔辫。 - 最后使用數(shù)據(jù)獲取接口將廣播中的數(shù)據(jù)趣些,也就是保存 Extra 的 Bundle,映射為 C# 中的 Dictionary螺垢,傳遞給
OnReceive
方法喧务,方便 C# 使用。這里為了簡單把所有類型的數(shù)據(jù)都映射為了 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);
}
};
}
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");
}
}