目前我参与的几个Unity项目的集成方都通过AAR集成UnityPlayer到Android工程,截止2022.3.0f1 Unity暂未提供直接导出AAR包功能,需要编写Editor脚本实现一键导出AAR
本方式基于Unity Editor 2021.3.14f1 Windows 版本,其他Unity版本表现可能会有所不同,暂未支持其他平台(macOS、Linux)的Unity Editor
脚本大体流程为:
- 修改BuildSettings
- 导出Android工程
- 复制/处理Gradle相关文件
- 编辑AndroidManifest.xml文件
- 调用Gradle编译
- 复制编译结果
先贴代码,我的代码技术很糟糕仅供参考:
using System;
using UnityEditor;
using UnityEngine;
using UnityEditor.Build.Reporting;
using System.IO;
using System.Xml;
public class EditorUtils
{
//Development版本
private static bool _isDevelopment = true;
//编译选项-仅适用Debug版本
private static BuildOptions _buildOptions = BuildOptions.Development
| BuildOptions.CompressWithLz4
| BuildOptions.ConnectWithProfiler
// | BuildOptions.AllowDebugging
// | BuildOptions.EnableDeepProfilingSupport
// | BuildOptions.WaitForPlayerConnection
;
//需要编译的场景
private static string[] _scenesToBuild = { "Assets/Scenes/SampleScene.unity" };
//要编译的Android项目模块
private static string[] _modulesToBuild = { "unityLibrary"};
//用于编译的临时文件夹
private static string _buildDir = "AndroidProjects/ExportedUnityProject/";
[MenuItem("Tools/Build/Build AAR")]
public static void ExportAar()
{
string fileExt = _isDevelopment ? "-debug.aar" : "-release.aar";
string targetPath = EditorUtility.SaveFilePanel("Save as", "AndroidProjects/IntegrationDemo/app/libs",
_modulesToBuild[0] + fileExt, "aar");
if (targetPath.Length == 0) return;
//清理旧编译输出
foreach (var module in _modulesToBuild)
{
string path = _buildDir + module + "/build/outputs/aar/" + module + fileExt;
if (File.Exists(path))
File.Delete(path);
}
//Export Project
if (!BuildAndroidPlayer(_scenesToBuild, _buildDir))
{
Debug.LogError("导出Android项目失败");
return;
}
//Copy Gradle cmd file
if (!CopyGradleFile(_buildDir))
{
Debug.LogError("复制Gradle相关依赖文件失败");
return;
}
//防止媒体文件被压缩
ReplaceFileContent("'.unityexp'", "'.unityexp', '.webm' , '.mov'",
_buildDir + "unityLibrary/build.gradle");
//防止com.unity3d.player.UnityPlayerActivity成为主Activity
DisableMainActivity(_buildDir + "unityLibrary/src/main/AndroidManifest.xml");
//Build AAR
RunGradleBuild(Path.Combine(Environment.CurrentDirectory, "AndroidProjects/ExportedUnityProject/"),
_modulesToBuild);
if (!File.Exists(_buildDir + _modulesToBuild[0] + "/build/outputs/aar/" + _modulesToBuild[0] + fileExt))
{
Debug.LogError("Failed to compile aar");
return;
}
//复制编译成果到目标目录
string targetDir = Path.GetDirectoryName(targetPath);
foreach (var module in _modulesToBuild)
{
string fileName = module + fileExt;
File.Copy(_buildDir + module + "/build/outputs/aar/" + fileName, Path.Combine(targetDir, fileName),
true);
}
//Success!
Debug.Log("Exported aar to " + targetPath);
}
/// <summary>
/// 从UnityEditor目录中复制GradleBuild相关的文件
/// </summary>
/// <param name="buildDir">目标项目路径</param>
/// <returns>是否复制成功</returns>
private static bool CopyGradleFile(string buildDir)
{
try
{
//Copy gradle files to project
File.Copy(
GetPathInUnityEditor(
"Data/PlaybackEngines/AndroidPlayer/Tools/VisualStudioGradleTemplates/gradlew.bat"),
buildDir + "gradlew.bat", true);
File.Copy(
GetPathInUnityEditor(
"Data/PlaybackEngines/AndroidPlayer/Tools/VisualStudioGradleTemplates/gradle-wrapper.jar"),
buildDir + "gradle/wrapper/gradle-wrapper.jar", true);
}
catch (Exception e)
{
return false;
}
return true;
}
/// <summary>
/// 调用Gradle编译项目
/// </summary>
/// <param name="pathToProject">项目路径</param>
/// <param name="moduleToBuild">需要编译的模块,例如unityLibrary</param>
/// <param name="isDebug">是否是Debug包</param>
static void RunGradleBuild(string pathToProject, string[] moduleToBuild)
{
string arguments = "/c gradlew.bat";
foreach (var moduleName in moduleToBuild)
{
//Spaaaaaaaaaaace
arguments += $" {moduleName}:assemble" + (_isDevelopment ? "Debug" : "Release");
}
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo("cmd.exe")
{
Arguments = arguments,
CreateNoWindow = true,
UseShellExecute = false,
WorkingDirectory = pathToProject
};
startInfo.EnvironmentVariables["JAVA_HOME"] =
GetPathInUnityEditor(@"Data\PlaybackEngines\AndroidPlayer\OpenJDK\");
var process = System.Diagnostics.Process.Start(startInfo);
if (process == null)
{
Debug.LogError("Failed to start gradle.");
return;
}
process.WaitForExit();
}
/// <summary>
/// 获取UnityEditor中的路径
/// </summary>
/// <param name="pathInEditor">相对UnityEditor的路径</param>
/// <returns></returns>
private static string GetPathInUnityEditor(string pathInEditor) =>
Path.Combine(Path.GetDirectoryName(EditorApplication.applicationPath), pathInEditor);
/// <summary>
/// 导出安卓工程
/// </summary>
/// <param name="targetScenesPath">要导出的Scene</param>
/// <param name="targetPath">目标路径</param>
/// <returns>是否成功</returns>
private static bool BuildAndroidPlayer(string[] targetScenesPath, string targetPath)
{
BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions();
buildPlayerOptions.scenes = targetScenesPath;
buildPlayerOptions.locationPathName = targetPath;
buildPlayerOptions.target = BuildTarget.Android;
EditorUserBuildSettings.development = _isDevelopment;
EditorUserBuildSettings.exportAsGoogleAndroidProject = true;
if (_isDevelopment)
buildPlayerOptions.options = _buildOptions;
else
buildPlayerOptions.options = BuildOptions.CompressWithLz4;
BuildReport report = BuildPipeline.BuildPlayer(buildPlayerOptions);
BuildSummary summary = report.summary;
return summary.result == BuildResult.Succeeded;
}
/// <summary>
/// 以很土质的方式替换文件中的一段内容,并会以同样很土质的方式跳过已被替换的文件
/// </summary>
/// <param name="from">要替换的文字</param>
/// <param name="to">替换为</param>
/// <param name="filePath">文件位置</param>
private static void ReplaceFileContent(string from, string to, string filePath)
{
string text = File.ReadAllText(filePath);
if (!text.Contains(to))
File.WriteAllText(filePath, text.Replace(from, to));
}
/// <summary>
/// 禁用AndroidManifest的MainActivity,用于导出aar
/// </summary>
/// <param name="path">AndroidManifest.xml的路径</param>
private static void DisableMainActivity(string path)
{
XmlDocument document = new XmlDocument();
document.Load(path);
var node = document.SelectSingleNode("manifest/application/activity");
var nodeToDelete = node.SelectSingleNode("intent-filter");
if (nodeToDelete != null)
node.RemoveChild(nodeToDelete);
document.Save(path);
}
}
1.修改BuildSettings:
首先需要找到Build Settings窗口的各个选项在代码中对应的位置:
EditorUserBuildSettings
EditorUserBuildSettings.development // Development Build
EditorUserBuildSettings.exportAsGoogleAndroidProject // Export Project
详情可到Unity官方文档查看
BuildPlayerOptions
Android工程导出路径,Compression Method,以及Scenes In Build需要使用BuildPlayerOptions(文档链接)来控制,需要用到的有:
//Scenes In Build,需要传入场景文件的路径,例如:“Assets/Scenes/SampleScene.unity”
string[] BuildPlayerOptions.scenes
//导出的目标路径,需要是已经存在的文件夹
string BuildPlayerOptions.locationPathName
//设置目标平台为Android,此处应传入BuildTarget.Android
BuildTarget BuildPlayerOptions.target
2. 导出Android工程
BuildReport BuildPlayer(BuildPlayerOptions buildPlayerOptions)
需要传入上一部分介绍的BuildPlayerOptions来启动导出过程
3. 复制/处理Gradle相关文件
Unity直接导出的安卓工程缺少一些用于启动Gradle的文件 gradlew.bat、gradle-wrapper.jar,这些文件已经包含在Unity Android Build Support中路径如下:
gradlew.bat: <Unity安装路径>/Data/PlaybackEngines/AndroidPlayer/Tools/VisualStudioGradleTemplates/gradlew.bat
gradle-wrapper.jar: <Unity安装路径>/Data/PlaybackEngines/AndroidPlayer/Tools/VisualStudioGradleTemplates/gradle-wrapper.jar
在Editor脚本中Unity安装路径可以通过Unity.exe的所在文件夹来获取
Path.GetDirectoryName(EditorApplication.applicationPath)
在Editor脚本中需要将gradlew.bat复制到上一节导出的Android工程的跟目录,gradle-wrapper.jar复制到导出的Android工程的gradle/wrapper/目录下。示例代码如下:
private static void CopyGradleFile(string buildDir)
{
//Copy gradle files to project
File.Copy(
GetPathInUnityEditor(
"Data/PlaybackEngines/AndroidPlayer/Tools/VisualStudioGradleTemplates/gradlew.bat"),
buildDir + "gradlew.bat", true);
File.Copy(
GetPathInUnityEditor(
"Data/PlaybackEngines/AndroidPlayer/Tools/VisualStudioGradleTemplates/gradle-wrapper.jar"),
buildDir + "gradle/wrapper/gradle-wrapper.jar", true);
}
如果Unity工程里面包含不在StreamingAssets里的视频、音乐等媒体文件,最好在这一步在unityLibrary的build.gradle文件中要求不压缩这类媒体,最简单粗暴的办法是在aaptOptions->noCompress中加入对应的扩展名。示例代码如下:
/// <summary>
/// 以很土质的方式替换文件中的一段内容,并会以同样很土质的方式跳过已被替换的文件
/// </summary>
/// <param name="from">要替换的文字</param>
/// <param name="to">替换为</param>
/// <param name="filePath">文件位置</param>
private static void ReplaceFileContent(string from, string to, string filePath)
{
string text = File.ReadAllText(filePath);
if (!text.Contains(to))
File.WriteAllText(filePath, text.Replace(from, to));
}
4. 编辑AndroidManifest.xml文件
Unity导出的项目自带一个 Activity: com.unity3d.player.UnityPlayerActivity,并在AndroidManifest.xml中标记为默认Activity(Android 文档),会在集成时出现冲突,因此需要在脚本中删除这个Activity的intent-filter
Unity导出的项目的AndroidManifest.xml相对固定,可以使用System.Xml删掉这个node,示例代码如下:
/// <summary>
/// 禁用AndroidManifest的MainActivity,用于导出aar
/// </summary>
/// <param name="path">AndroidManifest.xml的路径</param>
private static void DisableMainActivity(string path)
{
XmlDocument document = new XmlDocument();
document.Load(path);
var node = document.SelectSingleNode("manifest/application/activity");
var nodeToDelete = node.SelectSingleNode("intent-filter");
if (nodeToDelete != null)
node.RemoveChild(nodeToDelete);
document.Save(path);
}
com.unity3d.player.UnityPlayerActivity如果不需要的话也可以直接删除(要顺带在manifest里删除activity这个node),实际项目都是直接用UnityPlayer,但这个Activity是个很好的如何启动Unity的Sample,一块发给甲方也是不错的选择(
5.启动Gradle编译
在这个方法中,Gradle的命令格式大概是这样的
gradlew.bat <module1>:<assembleDebug or assembleRelease> <module2>:<assembleDebug or assembleRelease> <module3>:<assembleDebug or assembleRelease> ....
所以直接整一个Process,运行gradlew.bat,然后用通过设置JAVA_HOME让Gradle用Unity自带的JDK1.8(非常重要!)来编译这个项目,示例代码如下:
/// <summary>
/// 调用Gradle编译项目
/// </summary>
/// <param name="pathToProject">项目路径</param>
/// <param name="moduleToBuild">需要编译的模块,例如unityLibrary</param>
/// <param name="isDebug">是否是Debug包</param>
static void RunGradleBuild(string pathToProject, string[] moduleToBuild)
{
string arguments = "/c gradlew.bat";
foreach (var moduleName in moduleToBuild)
{
//Spaaaaaaaaaaace
arguments += $" {moduleName}:assemble" + (_isDevelopment ? "Debug" : "Release");
}
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo("cmd.exe")
{
Arguments = arguments,
CreateNoWindow = true,
UseShellExecute = false,
WorkingDirectory = pathToProject
};
startInfo.EnvironmentVariables["JAVA_HOME"] =
GetPathInUnityEditor(@"Data\PlaybackEngines\AndroidPlayer\OpenJDK\");
var process = System.Diagnostics.Process.Start(startInfo);
if (process == null)
{
Debug.LogError("Failed to start gradle.");
return;
}
process.WaitForExit();
}
至此,编译部分结束了,如果没有报错就可以把aar复制出来了
6. 复制编译结果
上一步编译好的aar会出现在导出的安卓工程的unityLibrary/build/outputs/unityLibrary-<debug or release>.aar
使用File.Copy复制出来即可,示例代码如下:
//复制编译成果到目标目录
string targetDir = Path.GetDirectoryName(targetPath);
foreach (var module in _modulesToBuild)
{
string fileName = module + fileExt;
File.Copy(_buildDir + module + "/build/outputs/aar/" + fileName, Path.Combine(targetDir, fileName),
true);
}