目前我参与的几个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窗口的各个选项在代码中对应的位置:

我的Windows机器寄了,先用mac上的代替~

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);
}