Unity 和 Android 混合开发更友好的方案
cfanr Lv4

前言

在网上搜索 Unity 和 Android 的混合开发,一般可以查到,大致分为两种方案:

  1. 以 Unity 主导的开发,Android 提供 JAR 包或 AAR 包,导入到 Unity 作为插件,最后由 Unity 开发打包 APK;(对 Unity 技能要求高点)

  2. 以 Android 主导的开发,Unity 导出 Android 项目,再以 Module 方式或 AAR 方式引入到 Android 工程项目中,最后由 Android 开发打包 APK;(对Android 技能要求高点)

但是这两种方式都有不足的地方:

  • 方式1,对 Android 开发人员不够友好,Android 工程,只能运行 Android 端的代码,不能同步 Unity 端代码,每次 Android 端修改代码都要导出包给 Unity 开发,通过 Unity 来调试;另外,如果有依赖第三方库,每次改完Android 工程的,还得修改 Unity 的 Android 插件配置的;(使用常规的方式,第三方库不支持打包进 aar)

  • 同样,方式2,对 Unity 开发人员不够友好,Unity 端只能运行 Unity 代码,不能同步运行 Android 端代码,每次改完都要导包给 Android 端验证;

这两种方式,都增加了 Android 端和 Unity 端的开发测试成本(只能单方开发测试)和沟通成本(虽然导包给对方的流程可以直接通过 Git 上传包文件到对方的项目工程,但开发阶段,需要太频繁上传包来测试,频繁沟通也挺费时间的)。需要三个Git 工程:Unity 工程、Unity 导出的 Android 工程、Android 工程。

新方案的大致思路

那么,有没有一种更友好的混合开发方式,使两端开发都能够测试对方的功能,又可以减少沟通成本呢?

有!

  1. 以 Unity 导出的 Android 工程结构作为 Android 项目的基本结构,Unity 每次导出工程都是直接覆盖在 Android 工程上;(这样能做到 Unity 或 Android 端都能在各自开发平台上运行对方的代码)
  2. 将 Unity 上需要频繁修改的配置文件索引到 Android 工程中 (后续修改配置只需要改 Android 工程的配置,无需再修改两份配置)
  3. 只需要管理两个工程:Unity 工程和 Android 工程;
  4. 另外,为了减少 Unity 导出的 Android 工程中 unityLibrary module 频繁变化,导致提交 Git 的时候变化的文件太多( asset 中 unity 相关文件),还可以通过脚本将 unityLibrary 改成 AAR 来上传;

这种混合方式特别适合,Unity 端和 Android 端都需要频繁修改代码的情况,能够极大降低调试和沟通成本,而且使双端整个开发流程都比较顺畅。

操作流程

下面介绍大致的操作流程:

PS: 为了减少两个平台使用的 Gradle 和 JDK 版本兼容性问题,Unity 建议使用2019.4 长期稳定版本,Android Gradle 建议使用 4.0.1 (不要太高),并且 JDK 使用 1.8 的,下面的示例都是基于此版本(都是在 Mac 平台,Windows可能有点差异性),其他高版本不一定可行,或需要花更多时间来弄适配。

1.创建一个新的 Unity 工程:在File/Build Settings切换到Android平台后,进入Player Settings,在 Other Settings 修改包名、版本号和版本名、Minimum API和 Target API、Unity编译方式、ABI架构等信息。
Unity-Android-settings.png

再在Publishing Settings上勾选使用自定义模板:
Unity-Android-settings-2.png

2.从 Unity 平台导出 Android 工程:选择 File/Build Settings,然后务必勾选 Export Project,在点击 Export (首次选择会引导选择导出的目录,后续会直接导出到上次选的目录),选择创建好的 Android 项目的目录
Unity-Export-Android.png

3.使用 Android Studio 打开导出的 Android 工程,并修改相关配置:

  • 会提示是否使用 Android Studio (后面简称为 AS)的SDK,可以改为使用 AS 的 SDK;
    Android-Studio-change-SDK.png

  • 打开工程后,会自动添加了 gradlewgradlew.bat脚本;(后续编写的脚本会调用到,如果没有,可以拷贝其他 Android 项目的)

  • gradle/wrapper/gradle-wrapper.perperties修改 gradle 版本为 6.7.1,根目录下的 build.gradle 的插件版本改为 4.0.1 (笔者测试这两个版本的匹配性较好)

  • 编译运行通过后,分别将根目录和 launcher 下的 build.gradle 分别拷贝到 gradle 文件下,并重命名为 base.gradlelauncher.gradle,原来的文件内容改为使用插件引入;
    Android-after-change-build-gradle.png

  • 再次编译运行保证配置无误;

4.修改 Unity 工程 Android 插件配置:

  • 拷贝Android 工程下 settings.gradle,并重命名为 settingsTemplate.gradle 黏贴到 Unity 工程的 Assets/Plugins/Android 目录下(这是一个 Unity 隐藏的模板文件)
  • 进入 Assets/Plugins/Android 下分别将 baseProjectTemplate.gradlelauncherTemplate.gradle 的内容改为 Android 工程根目录和 launcher 下的 build.gradle 的内容,可以查看 UnitySample/HaloUnity/Assets/Plugins/Android/launcherTemplate
  • 后续,如果 Android 工程修改了 settings.gradlegradle.properties,以及 launcher 下的 AndroidManifest.xml,要同时修改 Unity 里面的模板配置
  • 删掉 AndroidManifest.xml 文件启动页 UnityPlayerActivity 的配置(也就是 unityLibrary 里面的,不然会有两个包名的启动页),建议不要使用 Unity SDK 里面提供的 UnityPlayerActivity,使用自己定义的;改完后,如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?xml version="1.0" encoding="utf-8"?>
    <!-- GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN-->
    <manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.traffee.lovetigresse.verse"
    xmlns:tools="http://schemas.android.com/tools">
    <application>

    </application>
    </manifest>
  • 将启动页配置在 LauncherManifest.xml 中,或自己的业务 module;
  • 改完后,再次导出工程到 Android 项目,并保证导出的工程能正常运行;

此时,基本配置就弄完了。但还有些地方需要优化,比如,如果 Unity 每次导出项目,需要同步代码给 Android的时候,基本上 unityLibrary module 下很多文件都会改变(特别是assets目录下,有很多unity的逻辑),为了避免 GIT 提交太多文件的变更,可以考虑将 unityLibrary module 改为 AAR 来提交(只需要提交一个文件)。

动态修改 unityLibrary 编译方式

首先,需要考虑的时,Unity 导出工程时,unityLibrary 是以 module 的形式存在的,但在 Android 开发时,unityLibrary 是以 AAR 形式存在的,所以需要动态修改 settings.gradle 的配置,并判断当前是处于什么状态去编译。

需要实现一个脚本来实现以下功能:

  • 通过调用 gradlew 或 gradlew.bat 脚本将 unityLibrary module 编译生成 AAR;
  • 将 AAR 拷贝到指定目录;
  • 移除 unityLibrary module(最好将 unityLibrary 文件夹添加到忽略文件中):包含修改 settings.gradle 配置,以及删除 unityLibrary 文件夹(防止编译缓存影响到下次 Unity 导出工程时的编译结果);
  • AAR 最好支持根据版本来命名,方便区分;
  • 脚本最好可以传版本名参数作为 AAR 的版本标识;

下面是完成上述功能的 Shell 脚本 build_aar.sh (内部有判断),Windows 脚本看具体工程 build_aar.bat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/bin/bash

#获取Unity AAR 文件名 (必须在调用前声明,否则会找不到)
getUnityAARFileName(){
nameList=`ls -1`
unityAARName="$1" #默认名字
while read line
do
if [[ "$line" == *unityLibrary* ]]; then
# echo "存在unity aar"
unityAARName=$line
fi
done <<< "${nameList}" #注意加{} 是为了避免换行符丢失
echo "$unityAARName"
}

#remove unity module from settings.gradle
removeUnityModule(){
# Mac 系统,sed 命令需要增加一个备份文件
sed -i '.copy' "/unityLibrary/d" "$mydir/settings.gradle"
rm "$mydir/settings.gradle.copy"
rm "$mydir/unityLibrary"
}

mydir=$(cd `dirname $0`; pwd)
#echo "当前脚本路径:$mydir"

localPropFile="$mydir/local.properties"
if [ ! -f "$localPropFile" ];then
echo "error: 当前脚本目录${mydir}不存在local.properties文件"
exit
fi

sdkPath=""
while read line || [[ -n ${line} ]]
do
if [[ "$line" =~ ^sdk.dir* ]];then
sdkPath=$line
break
fi
done < $localPropFile

strSplit=(${sdkPath//=/ })
sdkPath=${strSplit[1]}
if [ ! -d "$sdkPath" ];then
echo "error: ${localPropFile}文件配置的Android sdk路径不对,请配置正确的路径!"
exit
fi

gradlewFile="$mydir/gradlew"
if [ ! -f "$gradlewFile" ];then
echo "error: 当前脚本目录${mydir}不存在gradlew文件"
exit
fi

gradlePath="$mydir/gradle"
if [ ! -d "$gradlePath" ];then
echo "error: 当前脚本目录${mydir}不存在gradle资源"
exit
fi

#manifestFile="$mydir/unity/AndroidManifest.xml"
#if [ ! -f "$manifestFile" ];then
# echo "error: 当前脚本目录${mydir}/unity 不存在AndroidManifest.xml文件"
# exit
#fi

path="$mydir"
#path="$1"
lib_path="$path/unityLibrary"
#echo "path=$path, lib_path=$lib_path"
if [ -d $lib_path ]
then
#echo "是Unity工程目录"
cp -f $localPropFile $path
cp -f $gradlewFile $path
cp -rf $gradlePath $path
# cp -f $manifestFile "$path/unityLibrary/src/main"
else
echo "error: 请输入正确的Unity工程目录"
fi

echo "path: $path"
cd $path
./gradlew unityLibrary:build


if [ $? -ne 0 ];then
echo "error: gradle编译生成arr失败!"
exit 1;
else
arrLibSrcFile="${lib_path}/build/outputs/aar/unityLibrary-release.aar"
arrLibTargetPath="${mydir}/verse/libs"

cd $arrLibTargetPath
#设置 aar 名字
if [ $# -ge 2 ];then
defAARName="unityLibrary-v$2.aar"
else
defAARName="unityLibrary-v1.0.00.aar"
fi
#echo $defAARName
unityAARName=`getUnityAARFileName "$defAARName"`
#echo $unityAARName
# 拷贝、移动和重命名
cp -f $arrLibSrcFile $arrLibTargetPath
mv unityLibrary-release.aar $unityAARName
# Mac 打开指定文件夹
open $arrLibTargetPath
echo "生成aar包成功,并已拷贝到指定项目的对应文件夹上,请检查是否正确~"
removeUnityModule
fi

另外,还需要动态判断 unityLibrary 是以 module 还是 AAR 形式存在。
base.gradle 增加校验当前是否有 unityLibrary module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ext {
hasUnityModule = true
}

checkUnityProjects()

def checkUnityProjects() {
println '---Run on Root Projects---'
def hasUnityModule = false
this.getAllprojects().eachWithIndex { Project project, int i ->
if (i == 0) {
println "Root Projects ':${project.name}'"
} else {
println "Module project ':${project.name}' "
if (project.name == 'unityLibrary') {
hasUnityModule = true
}
}
}
ext.hasUnityModule = hasUnityModule
println "---End: hasUnityModule='${ext.hasUnityModule}'---"
}

在 launcher.gradle 里面配置编译时使用的是 module 还是 AAR(注意:必须保证 AAR的版本命名和应用版本命名一致)

1
2
3
4
5
6
7
8
9
dependencies {
// 务必注释掉,否则会将 unity-classes.jar 打到 aar,仅采用 compileOnly 引入
// implementation fileTree(dir: 'libs', include: ['*.jar'])
if (hasUnityModule) {
implementation project(':unityLibrary')
} else {
implementation(name: "unityLibrary-v${version_name}", ext:'aar')
}
}

另外,settings.gradle 文件必须保证 include ‘:unityLibrary’是单行格式,脚本会在生成完 AAR 后,删掉该行。

使用自定义 UnityPlayerActivity

需要用到一个小技巧,由于要用到 Unity SDK 的 API ,就必须在当前 module 添加,如果是编译 unityLibrary module 时,Unity 的 jar 包是在 unityLibrary module 里面的,是无法引用到,这时需要拷贝一份 unity-classes.jar 放到当前 module 的 libs 下,并使用 compileOnly 的编译方式,上述配置则需改为:

1
2
3
4
5
6
7
8
// 务必注释掉,否则会将 unity-classes.jar 打到 aar,仅采用 compileOnly 引入
// implementation fileTree(dir: 'libs', include: ['*.jar'])
if (hasUnityModule) {
compileOnly files('libs/unity-classes.jar')
implementation project(':unityLibrary')
} else {
implementation(name: "unityLibrary-v${version_name}", ext:'aar')
}

然后拷贝 UnityPlayerActivity 重命名为 GameActivity,再修改 Android 和unity 工程的 Manifest 启动页配置即可。

Unity 平台直接编译运行到手机

如果 Android 端没配置 productFlavors, Unity 平台是可以直接使用 Build And Run导出并运行、安装 APK 到手机上的。

但如果配置了的话,由于编译后的 APK 文件命名被改了,需要在编译之后,拷贝个备份改回原来的默认命名,才能找到 APK 文件,才能正常安装。

1
2
3
4
5
6
7
productFlavors {
publish {
}

develop {
}
}

修改编译后 APK 文件的脚本,添加在 launcher.gradle 里面的 android{} 内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 打包完成复制一份重命名的文件,并改为默认命名(unity开发平台需要用到默认命名的文件)
this.project.afterEvaluate { project ->
project.tasks.each { task ->
if (task.toString().contains("assemble")) {
task.doLast {
android.applicationVariants.all { variant ->
variant.outputs.all {
if (variant.productFlavors[0].name.contains("publish")) {
println("productFlavors is publish type, ignore")
return
}
def defName = "${project.name}-${variant.buildType.name}.apk"
println("productFlavors: ${variant.productFlavors[0].name}")
def workFolder = "${project.getProjectDir().path}/build/outputs/apk/${variant.productFlavors[0].name}/${variant.buildType.name}"
def destFolder = new File("${project.getProjectDir().path}/build/outputs/temp/${variant.buildType.name}")
def unityInstallFolder = "${project.getProjectDir().path}/build/outputs/apk/${variant.buildType.name}"
try {
if (!destFolder.exists()) {
destFolder.mkdir()
}
copy {
from "${workFolder}/${outputFileName}"
into "${destFolder}/"
rename {
defName
}
}
copy {
from "${destFolder}/$defName"
into "${unityInstallFolder}/"
}
if (destFolder.exists()) {
destFolder.deleteDir()
}
} catch (Exception e) {
print e
}
}
}
}
}
}
}

开发流程

  • Unity 开发:在 Unity 开发平台上开发完功能后,通过 File/Build And Run直接导出 Android 工程并编译运行 APK 安装到手机上;测试无误后,进入 Android 工程目录,Mac 执行 ./build_aar.sh versionName ,Windows 执行 build_aar.bat versionName (versionName 名字一定要和 Android 工程的一致,或者只输入 ./build_aar.shbuild_aar.bat会以当前的 AAR 文件名版本名作为版本),编译生成 AAR 后(最好再打开 AS 运行一遍,验证编译安装正常),直接 GIT 提交 unityLibrary-v1.x.xx.aar 文件到 Android 工程即可;
  • Android 开发:正常基于 Android 工程开发,如果改动 settings.gradlegradle.properties,以及 launcher 下的 AndroidManifest.xml,需要同步改动 Unity Asset/Plugin 的模板文件,并提交,同时通知 Unity 更新 Unity 工程和 Android 工程代码即可;

问题&&解决

  1. 如果提示 Project with path ‘:unityLibrary’ could not be found in root project ‘xxx’.
    可以全局搜索 unityLibrary 看看哪里有引用到 unityLibrary module 的配置(脚本里面的不用管),可以检查下 base.gradle 是否有以下配置,有则将它改为放置 AAR 的 module 名字,比如 launcher

    1
    2
    3
    flatDir {
    dirs "${project(':launcher').projectDir}/libs"
    }
  2. 如果提示Could not resolve all files for configuration':launcher:debugRuntimeClasspath'. > Could not find :unityLibrary-v1.0.0 ,是找不到引用的AAR文件,有可能是你的版。本号命名规则和脚本里面默认的不一致,没指定版本的话,AAR 默认名字是 unityLibrary-v1.0.00.aar,你的应用版本命名必须和AAR的版本命名一致;

  3. 在电脑终端上先执行 gradle -version,如果提示找不到该命令,则先配置 gradle 环境

  4. 如果有提示 Unity 使用的 Gradle 版本缺失某个文件或和Android 工程的不一致的话,可以考虑将 Unity 的 Gradle 版本替换成 Android 工程使用的版本;

  5. 如果提示 JDK 11 版本太高,可考虑将 Android Studio 的 JDK 版本降到 1.8 (新版本的 AS用的 JDK 都是11的)

–End–