有时候,我们需要在 Unity 里调用一些 Android 的功能,这些功能在 Unity 中可能并没有提供接口,需要在 Android 平台上实现。此时,我们需要有一个方法来让 Android 代码和 Unity 代码互调用。这里记录一下操作方法,并提供一个工具来简化两个工程之间的集成流程。

示例工程

下面的记录中所使用的工程可以参考 UnityAndroidExample。其中,根目录是 Unity 工程,可以直接用 Unity 打开。根目录下的 AndroidSample 子目录是 Android 工程,可以用 Android Studio 打开。

Unity 工程运行后如下图左所示,只有一个文本和一个按钮,点击按钮就会触发 Unity 到 Anrdoid 的调用,在主界面上产生一个 toast,同时,触发一次从 Anrdoid 到 Unity 的调用,主界面上的文本变为「Hello From Android」:

具体操作方式

新建一个 Android 工程

这里随便用 Android Studio 建立空一个工程就行了。建立好工程后,参考官方文档在工程里添加一个自定义的模块:

  • 菜单栏点击 「File」-「New」-「New Module…」
  • 弹出窗口中左侧选「Android Library」
  • 右侧填入相关信息后创建模块

假设创建的模块名为「mod」,那么就会在工程根目录下新增一个名为 mod 的目录。此时可以删除工程根目录中默认创建的 app 目录,并将工程根目录中 settings.gradle 文件里的 include ':app' 这一行删除。

添加 Unity jar 依赖

为了在 Android 中和 Unity 互交互,我们需要引入 Unity 提供的库,这个库以 jar 包的形式提供。以下目录中都有这个 classes.jar 文件,有 mono 和 il2cpp 版本,还区分 Release 和 Development:

  • YOUR_EDITOR_PATH/Data/PlaybackEngines/AndroidPlayer/Variations/il2cpp/Release/Classes
  • YOUR_EDITOR_PATH/Data/PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes
  • YOUR_EDITOR_PATH/Data/PlaybackEngines/AndroidPlayer/Variations/il2cpp/Development/Classes
  • YOUR_EDITOR_PATH/Data/PlaybackEngines/AndroidPlayer/Variations/mono/Development/Classes

这里的「YOUR_EDITOR_PATH」是 Unity editor 程序所在的路径,例如,如果在 Windows 上用 Unity Hub 安装了 2020.3.5fc1 的 Unity,那么这个路径就是 C:\Program Files\Unity\Hub\Editor\2020.3.5f1c1\Editor。

参考Android 官方文档添加依赖,将该 jar 文件复制到 Android 工程中的对应模块的 libs 目录中,具体是复制哪一个 jar 无关紧要,因为后面的流程中并不会实际加入这个 jar 包。在复制的之后可以修改一个名字,例如修改为 unity.jar。然后修改 gradle 构建文件,注意这里是修改模块目录下的 build.gradle 而非根目录下的。在 dependencies 中添加如下内容:

dependencies {
    // ... 前面有一堆默认的

    // 添加 unity 的 jar
    // 注意这里必须是 compileOnly 以避免该 jar 包被打入 aar 包中,否则会在之后发生命名冲突
    compileOnly files('./libs/unity.jar')

    // 如果还有别的自定义的 jar 就用 implementation
    implementation files('./libs/some_other_lib.jar')
}

修改完后,Android Studio 会提示是否要同步,点击「Sync Now」即可。

引入 UnityPlayerActivity

我们在实现自己的 Activity 时不能直接实现,而是需要继承 Unity 的 UnityPlayerActivity,这个类型会按照一定的规则去调用 Unity 的回调函数,以确保程序的正确运行。从前这个类就在刚刚我们引入的 unity.jar 中,而在新版本的 Unity 中这个类却以单独文件的形式存在,需要自己拷贝一下,这个文件所在的路径为:YOUR_EDITOR_PATH/Data/PlaybackEngines/AndroidPlayer/Source/com/unity3d/player/UnityPlayerActivity.java。

我们直接将 YOUR_EDITOR_PATH/Data/PlaybackEngines/AndroidPlayer/Source/com 这个目录直接拷到工程里的 mod/src/main/java 目录下,这样一来,我们的代码就可以看到这个 Activity 了。

新增一个 Activity 继承 UnityPlayerActivity

在这里,我们添加一个 showMessage 函数用于给 Unity 调用,同时,在这个函数里面,我们通过 UnityPlayer.UnitySendMessage 来调用 Unity 中的函数。这个 UnityPlayer 定义于我们引入的 unity.jar 文件中。其中第一个参数是 Unity 场景中的对象名,第二个参数是需要调用的函数名,第三个参数是传递的参数:

public class MainActivity extends UnityPlayerActivity {
    // 被用于 Unity 调用的函数
    public void showMessage(final String message) {
        runOnUiThread(() -> Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show());
        // 调用 Unity 的函数
        UnityPlayer.UnitySendMessage("Canvas", "ChangeText", "Hello from Android!");
    }
}

此时,文件结构如下图所示:

构建模块

菜单栏中选择「Build」-「Make Module ‘mod’」。等待构建完成后,会在 mod/build/output/aar 目录下看到构建出来的包。

和 Unity 集成

接下来,我们需要将这个库和 Unity 集成,并让 Unity 以这个 Activity 为入口启动程序。

将这个 aar 包解压,放入 Unity 工程的 Plugins/Android/mod 目录下,然后在这个目录下建立一个 project.properties 文件,填入如下内容:

android.library=true

再在 Plugins/Android 目录下(和 mod 同级)建立一个 AndroidManifest.xml 文件,填入如下内容,注意其中的 ACTIVITY_NAME 需要换成 main Activity 的完整类名(完整包名加上类名)。如果有什么需要申请的权限,也可在此加入:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0">
    <supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/>

    <application
        android:theme="@style/UnityThemeSelector"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name="ACTIVITY_NAME"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
        </activity>
    </application>
</manifest>

要在 Unity 中调用 Android 的函数,需要用类似这样的方法实现。首先,我们要找到当前的 Android Activity,然后我们通过 Call 方法来调用其中的逻辑。其中第一个参数是方法名,后面的参数是需要传递的参数:

#if UNITY_ANDROID && !UNITY_EDITOR
using (AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
{
    using (AndroidJavaObject activity = jc.GetStatic<AndroidJavaObject>("currentActivity"))
    {
        activity.Call("showMessage", "Hello from Unity");
    }
}
#else
#endif

这一块代码用 #if UNITY_ANDROID && !UNITY_EDITOR 包裹,只在 Android 设备上生效。

另外我们还需要在 Unity 场景中添加刚刚 Android 代码中调用 Unity 时 Unity 侧的接收者(在本示例中为 Canvas),这块具体操作直接参考示例工程即可。

Unity 构建

在 Unity 菜单中点击「File」-「Build Settings…」,在弹出的窗口中选择 Android 平台,然后构建即可。

一个小工具

上面这个流程有些是只用操作一次的(例如新建工程),但也存在一批需要反复操作的(例如编译 Android 工程、删除 Unity 的 Activity 等),这些需要反复操作的流程在每次修改 Android 工程中的代码后,都需要进行一次,非常麻烦还容易出错,因此,这里提供一个简单的小工具来简化这个工作。这个工具的安装需要用到 go,需要先安装一下 go 的环境

这个小工具可以编译指定的 Android 模块,然后将 aar 压缩包解压到 Unity 工程中,删除 Unity 的 Activity class,并生成 project.properties 和 AndroidManifest.xml 文件。在生成 AndroidManifest.xml 的时候,提供了默认的文件模板,允许通过命令行参数指定需要申请的 Android 权限。

例如这样的命令:

upack -a ./AndroidSample -e com.example.mod.MainActivity -m mod -p android.permission.BATTERY_STATS ./Assets/Plugins/Android

可以将 Android 工程 AndroidSample 编译,然后将数据解压到 ./Assets/Plugins/Android 目录下,其中参数 -e 用来指定入口 Activity 的类型全名,参数 -m 用来指定 Android 模块名,-p 用来指定需要申请的权限,如果有多个权限需要申请,则可以增加多个 -p 参数。

在示例工程中也可以体验这个工具,每次修改这个 Android 工程中的代码,都可以执行一下工程根目录下的 update_android.bat 脚本,这个脚本会调用这个工具,重新构建 Android 工程并自动将相关内容解压到 Unity 工程中。