基于Python和Jenkins的Unity自动化打包方案总结(3)

282 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第26天,点击查看活动详情

C#核心打包逻辑实现要点

本篇中,讲介绍核心打包逻辑的实现要点,由于这部分是和业务相关的,因此不会给出非常详细的代码,但会说明一些重要的实现方法。

切换C#预处理定义

接上篇,在我们的核心打包类MainBuilder中,包含了切换编译环境的入口方法:

public static void Build_SwitchEnv()
{
    var targetGroup = getBuildTargetGroup();
    SwitchScriptDefines(targetGroup);
    
    string version = $"{Config.VersionBase}.{Config.SvnReversion}";
    PlayerSettings.bundleVersion = version; 
}

该方法中,首先调用getBuildTargetGroup获取当前的BuildTargetGroup。

static BuildTargetGroup getBuildTargetGroup()
{
    BuildTargetGroup group = BuildTargetGroup.Standalone;
    switch (Config.OS)
    {
        case BuildOS.iOS:
            group = BuildTargetGroup.iOS;
            break;
        case BuildOS.Android:
            group = BuildTargetGroup.Android;
            break;       
    }
    return group;
}

这是根据我们在Config设置的OS来决定的。 获取到这BuildTargetGroup后,就可以调用SwitchScriptDefines来切换c#的预处理定义了。我们在代码注释中详细说明:

public static void SwitchScriptDefines(BuildTargetGroup targetGroup)
{
    //根据targetGroup获取当前平台已有的symbols,即预处理定义
    string symbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(targetGroup);
    //获取到的symbols是分号分割的字符串,因此将字符串split成数组
    var defines = symbols.Split(new char[] { ';' });
    //定义一个HashSet方便我们操作    
    HashSet<string> hashset = new HashSet<string>(defines);

    //根据Config的参数,设置或者取消设置相应的预处理定义,比如这儿根据是否选择了DevelopmentMode来决定是否启用DEBUG_MODE宏
    if (Config.DevelopmentMode)
    {
        hashset.Add("DEBUG_MODE");
    }
    else if (hashset.Contains("DEBUG_MODE"))
    {
        hashset.Remove("DEBUG_MODE");
    }

    //根据Config的OS参数进行设置,例如对于移动平台,添加MOBILE_UI宏。
    switch (Config.OS)
    {
        case BuildOS.iOS:
        case BuildOS.Android:
            hashset.Add("MOBILE_UI"); 
            break;
        default:                    
            break;
    }            

    //根据Config的Store参数,针对不同的商店定义不同的宏,获取去除一些无关的宏
    var store = Config.Store;                           
    if (store == BuildStore.Steam)
    {
        if (hashset.Contains("PLATFORM_GOOGLE"))
        {
            hashset.Remove("PLATFORM_GOOGLE");
        }

        if (!hashset.Contains("PLATFORM_STEAM"))
        {
            hashset.Add("PLATFORM_STEAM");
        }
    }
    else if (store == BuildStore.Google)
    {
        if (hashset.Contains("PLATFORM_STEAM"))
        {
            hashset.Remove("PLATFORM_STEAM");
        }

        if (!hashset.Contains("PLATFORM_GOOGLE"))
        {
            hashset.Add("PLATFORM_GOOGLE");
        }
    }
    else if (store == BuildStore.Default)
    {
        if (hashset.Contains("PLATFORM_STEAM"))
        {
            hashset.Remove("PLATFORM_STEAM");
        }
        if (hashset.Contains("PLATFORM_GOOGLE"))
        {
            hashset.Remove("PLATFORM_GOOGLE");
        }
    }                                
    
    //将hashset导出到string数组
    defines = new string[hashset.Count];
    hashset.CopyTo(defines);

    //核心方法:针对targetGroup,设置修改后的 defines
    PlayerSettings.SetScriptingDefineSymbolsForGroup(targetGroup, defines);    
}

修改Assets以及编译Addressable

如果不同的打包参数下,需要使用不同的资源,我们可以使用AssetDatabase相关的函数对Assets进行操作。

  • 例如: AssetDatabase.DeleteAsset("Assets/SDK/SteamWorks.Net"); 如果当前不是打包steam版本,则删除Steam相关SDK。
  • 修改Assets后,需要使用AssetDatabase.SaveAssets();进行保存。

编译 Addressable

  • 清除已build的缓存内容: AddressableAssetSettings.CleanPlayerContent();
  • 执行Addressable的编译:AddressableAssetSettings.BuildPlayerContent(out var result);

具体的打包操作

static bool BuildPlayer()
{
    PrepareBuildPath();           

    BuildPlayerOptions playerOptions = new BuildPlayerOptions();
    playerOptions.locationPathName = _outputPathExe;
    playerOptions.options = getBuildOptions();
    playerOptions.scenes = getBuildScenes();
    playerOptions.target = getBuildTarget();
    playerOptions.targetGroup = getBuildTargetGroup();

    BuildReport report = BuildPipeline.BuildPlayer(playerOptions);
    BuildResult result = report.summary.result;            
    BuilderUtils.Log($"Build Player Complete, result: {result}");
    return result == BuildResult.Succeeded;
}

打包目录准备

首先,PrepareBuildPath中准备版本包保存的目录,主要是根据参数进行目录命名,以及exe或apk的命名。如果目录不存在则创建目录,如果存在则清空目录。

static void PrepareBuildPath()
{    
    _outputFolder = Config.OutputFolder;
    
    if (!System.IO.Directory.Exists(_outputFolder))
    {
        System.IO.Directory.CreateDirectory(_outputFolder);                
    }            
    
    string folderName = $"{Config.AppName}_{Config.VersionType}_{Config.BuildDateTime}_{Config.SvnReversion}";
    if(Config.Store != BuildStore.Default)
    { 
        folderName = $"{folderName}_{Config.Store}";
    }    

    _outputPath = System.IO.Path.Combine(_outputFolder, folderName);

    switch (Config.OS)
    {
        case BuildOS.Windows:
            if (System.IO.Directory.Exists(_outputPath))
            {
                System.IO.Directory.Delete(_outputPath, true);                        
            }
            else
            {
                System.IO.Directory.CreateDirectory(_outputPath);
            }
            _outputPathExe = System.IO.Path.Combine(_outputPath, $"{Config.AppName}.exe");
            break;
        case BuildOS.Android:                    
            _outputPathExe = _outputPath + ".apk";                    
            break;
    }            
}

填充BuildPlayerOptions结构

  • 具体见上面的代码,其中 playerOptions.options 通过 getBuildOptions 获取:
static BuildOptions getBuildOptions()
{
    BuildOptions option = BuildOptions.None;
    if (Config.DevelopmentMode)
    {
        option |= BuildOptions.Development;
    }

    if (Config.OS == BuildOS.iOS && Config.DevelopmentMode)
    {
        option |= BuildOptions.SymlinkSources;
    }
    return option;
}

这主要是设置打包测试版需要的option。

  • getBuildScenes是为了获取打包的Unity场景:
static string[] getBuildScenes()
{                
    List<string> scenes = new List<string>();

    if(Config.BuildScenes != null && Config.BuildScenes.Length > 0)
    {
        foreach(var sceneName in Config.BuildScenes)
        {
            if(!string.IsNullOrEmpty(sceneName) && sceneName.EndsWith(".unity"))
            {                
                scenes.Add(sceneName);
            }                    
        }
    }

    if(scenes.Count == 0)
    {
        EditorBuildSettingsScene[] scene = EditorBuildSettings.scenes;
        for (int i = 0; i < scene.Length; i++)
        {
            scenes.Add(scene[i].path);
        }
    }            
    
    return scenes.ToArray();                                   
}

如果Config.BuildScenes中填写了要打包的场景,则优先使用该设置。否则会从Unity编辑器Editor Settings中的Scenes in Build中获取。

执行Build并获取结果

很简单,只要执行BuildPlayer即可。

BuildReport report = BuildPipeline.BuildPlayer(playerOptions);
BuildResult result = report.summary.result;        

打包后操作

打包之后,如果是Windows平台,我们获得了一个目录,其中包含exe文件和游戏资源。但我们也许想将该目录压缩成一个zip文件以方便保存和传递版本。因此,在调用BuildPlayer之后,我们执行以下方法打zip包:

if (Config.OS == BuildOS.Windows)
{
    string zipFilePath = _outputPath + ".zip";
    if (System.IO.File.Exists(zipFilePath))
    {
        System.IO.File.Delete(zipFilePath);
    }
    var fastZip = new FastZip();
    fastZip.CreateZip(zipFilePath, _outputPath, true, null);                   
}

这儿使用了SharpZipLib这个库,需要在Editor目录中添加相应的dll文件。另外需要using ICSharpCode.SharpZipLib.Zip;

小结

至此我们完成了C#端的打包系统的代码,下一步我们去配置Jenkins项目,准备相关的参数。再之后我们完成python脚本去将整个流程串起来。