携手创作,共同成长!这是我参与「掘金日新计划 · 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脚本去将整个流程串起来。