游戏开发,免不了要上多个平台,如googleplay、appstore等,这就涉及到了游戏源码版本管理与分支设计问题。下面我们来探讨其中一种版本管理方式,没有对错,只有适不适合当下,另外更希望能对大家起到抛砖引玉的作用。
笔者认为,版本管理是服务于团队、完善工作流程、解决某些问题、提升团队效率而提出来的一种工作流,千万不要为了"版本管理"而"版本管理",无端的去多做一些无谓的工作而增加团队的负担。这一点特别体现在应用源码版本管理工具上,有git、svn、pscm等,我们要根据项目大小、团队成员数量、应用工具水平等因素选择,而不能一味的追求某种工具,而让团队增加没必要的学习成本和应用成本。切记工具是为人服务的,而不是人服务于工具。下面,以笔者个人最近的一个项目为例,我们一起探讨一下基于 Unity 官方版本管理软件 Plastic SCM下的游戏多平台分支与打包策略。
首先上一张分支图,如下:
如上图,项目工程只有一个main 主分支,所有的源码都是以这个主分支为准。有多个平台的,可以依赖于主分支创建对应的平台分支,就像上面的Amazon\googleplay\ios分支等。主分支的开发和修改原则是,只包含项目与平台无关的代码。平台分支的开发和修改原则是,只包含主分支与平台相关的代码。当主分支有修改时,可以正向合并到其它分去上,这样就能保证一份代码各个平台通用。与平台相关的,可以在对应平台的分支下开发和修改。
我们要遵守一个原则,我们只能从主分支正向合并到其它平台分支,并不能从平台分支逆向合并到主分支。如果能逆向合并,创建平台分支的意义也就不存在了,因为主分支最终会包含所有分支的内容。
我们平时如果需要出包,那就看情况,如果只是普通的测试包,那就主分支直接出包好了。如果需要各个平台分别出包,合并后,那就对应分支下打包就好了,相对来说还是十分简单有效的。
上图只是解决了多平台分支的问题,肯定有小伙伴会问,如果多人协作开发,单一个主分支可以吗?当然不可以啊,思路其实就和多平台分支一样,分别依赖于主分支创建各个开发者所需分支就行了,但开发者分支与平台分支合并的原则是不一样的。开发者分支合并的原则是先拉取,再合并/推送,也就是可以正向/逆向合并。
通过上述操作,基本上解决了多人协作、多个平台发布、多个源码版本的管理问题,剩下的就是各个平台发布出包问题了。
各个平台如果需要同时发布出包,我们如果只有一台打包机器的情况下,需要手动设置的发布内容还是挺多的,而且容易出错。在此和大家探讨一种半自动化的方式,思路上主要作为抛砖引玉,让大家能找到适合自己的出包方式。如利用Jenkins的工作流,编写脚本全自动的出包方式等。
** 笔者目前采用的方式是半自动化的,因为出包需求并不是十分频繁。我们先上一个自动化脚本,如下图:**
ini
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
using UnityEditor;
namespace Simple.Build
{
//包体属性配置文件对象
public class PackageConfig
{
public string CompanyName;
public string ProductName;
public string PackageName;
/// <summary>
/// 新版本号,只能3位(Ios 限制)
/// </summary>
public string NewVersion;
/// <summary>
/// 最后一次打包的版本号,会在此基础上递增
/// </summary>
public string LastBuildVersion;
/// <summary>
/// 版本代号,会在此基础上递增
/// </summary>
public int VersionCode;
/// <summary>
/// 安卓KeyStore文件
/// </summary>
public string AndroidKeyStoreFile = "";
public string AndroidKeyStorePass = "";
public string AndroidAliasName = "";
public string AndroidAliasPass = "";
}
//包体设置对象
public class PackageSetting
{
//保存包体配置的json文件
private const string _configFile = "Assets/Scripts/Editor/BuildProcess/PackageConfig.json";
private static PackageConfig _config;
public static PackageConfig Config
{
get
{
if( _config == null )
{
if(!File.Exists(_configFile))
{
_config = new PackageConfig();
_config.CompanyName = "CompanyName";
_config.ProductName = "ProductName";
_config.PackageName = "com.PackageName";
_config.NewVersion = "1.0.0";
_config.LastBuildVersion = "1.0.0";
_config.VersionCode = 1;
}
else
{
var content = File.ReadAllText(_configFile);
_config = JsonConvert.DeserializeObject<PackageConfig>( content );
}
}
return _config;
}
}
public static void ClearConfigCache() { _config = null; }
public static void Save()
{
var content = JsonConvert.SerializeObject(_config, Formatting.Indented);
File.WriteAllText(_configFile, content);
}
public static void SetCompanyName(string name)
{
Config.CompanyName = name;
}
public static void SetProductName(string name)
{
Config.ProductName = name;
}
public static void SetNewVersion(string version)
{
Config.NewVersion = version;
}
public static void SetLastBuildVersion(string version)
{
Config.LastBuildVersion = version;
}
//获取打包版本号,自动设置新版本号(version)
public static string GetBuildVersion()
{
if(!string.IsNullOrEmpty(Config.NewVersion))
{//如果新版本号不为号,则以新版本号为基础
var v = Config.NewVersion;
var varr = v.Split('.').ToList();
//修正回3位的版本号
if(varr.Count > 3)
{
for(int i = varr.Count - 1; i > 2; i--)
{
varr.RemoveAt(i);
}
}
else if(varr.Count < 3)
{
for (int i = varr.Count; i < 3; i++)
{
varr.Add("0");
}
}
var nv = string.Join(".", varr);
return nv;
}
else
{//最后一次打包的版本号加1
var varr = Config.LastBuildVersion.Split('.');
var ncode = int.Parse(varr[2]) + 1;
varr[2] = ncode.ToString();
var nv = string.Join(".", varr);
return nv;
}
}
//获取最后一次打包的版本号(versionCode)
public static int GetBuildVersionCode()
{
return Config.VersionCode + 1;
}
}
}
上面的代码只要是对安卓包体的包名、标识符、versionCode(ios是buildNum)、密码等必须属性进行设置,并保存在json文件中。这样如果我们需要改这些属性时,只要改json文件即可,其它的都不需要理会。比如在我们切换分支到某个目标平台时,ProjectSetting文件下保存的包体包名versionCode等这些信息有可能会发生改变,而这些信息有可能不是当下分支平台的信息,这时还要检查或再设置一次,十分麻烦。有了上面的脚本,我们就不需要理会这一步了。
下面我们再写一个脚本,让出包的时候调用上面的脚本设置包体,如下图:
ini
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using Simple.Build;
using System.IO;
using Excel2json;
/// <summary>
/// googleplay平台打包设置
/// </summary>
public class BPGooglePlayPackageSet : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
public int callbackOrder => 10;
//打包前设置包体各个属性
public void OnPreprocessBuild(BuildReport report)
{
if (report.summary.platform != BuildTarget.Android)
return;
PackageSetting.ClearConfigCache();
var config = PackageSetting.Config;
PlayerSettings.Android.useCustomKeystore = true;
PlayerSettings.Android.keystoreName = config.AndroidKeyStoreFile;
PlayerSettings.Android.keystorePass = config.AndroidKeyStorePass;
PlayerSettings.Android.keyaliasName = config.AndroidAliasName;
PlayerSettings.Android.keyaliasPass = config.AndroidAliasPass;
PlayerSettings.companyName = config.CompanyName;
PlayerSettings.productName = config.ProductName;
PlayerSettings.applicationIdentifier = config.PackageName;
PlayerSettings.bundleVersion = PackageSetting.GetBuildVersion();
PlayerSettings.Android.bundleVersionCode = PackageSetting.GetBuildVersionCode();
}
///打包后保存设置
public void OnPostprocessBuild(BuildReport report)
{
Debug.Log($"--------------Ios CompanyName {PlayerSettings.companyName}");
Debug.Log($"--------------Ios ProductName {PlayerSettings.productName}");
Debug.Log($"--------------Ios PackageName {PlayerSettings.applicationIdentifier}");
Debug.Log($"--------------Ios Version {PlayerSettings.bundleVersion}");
Debug.Log($"--------------Ios VersionCode {PlayerSettings.Android.bundleVersionCode}");
//更新配置文件
var config = PackageSetting.Config;
config.LastBuildVersion = PlayerSettings.bundleVersion;
config.VersionCode = PlayerSettings.Android.bundleVersionCode;
config.NewVersion = "";
PackageSetting.Save();
}
}
将上面的所有脚本放在Editor文件夹下,这样只要各个平台打包发布时,都会自动的发布正确的包体了。
下面是ios平台分支的脚本:
ini
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using Simple.Build;
/// <summary>
/// appstore平台打包设置
/// </summary>
public class BPAppStorePackageSet : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
public int callbackOrder => 10;
public void OnPreprocessBuild(BuildReport report)
{
if (report.summary.platform != BuildTarget.iOS)
return;
PackageSetting.ClearConfigCache();
var config = PackageSetting.Config;
PlayerSettings.companyName = config.CompanyName;
PlayerSettings.productName = config.ProductName;
PlayerSettings.applicationIdentifier = config.PackageName;
PlayerSettings.bundleVersion = PackageSetting.GetBuildVersion();
PlayerSettings.iOS.buildNumber = PackageSetting.GetBuildVersionCode().ToString();
}
public void OnPostprocessBuild(BuildReport report)
{
Debug.Log($"--------------Ios CompanyName {PlayerSettings.companyName}");
Debug.Log($"--------------Ios ProductName {PlayerSettings.productName}");
Debug.Log($"--------------Ios PackageName {PlayerSettings.applicationIdentifier}");
Debug.Log($"--------------Ios Version {PlayerSettings.bundleVersion}");
Debug.Log($"--------------Ios VersionCode {PlayerSettings.iOS.buildNumber}");
//更新配置文件
var config = PackageSetting.Config;
config.LastBuildVersion = PlayerSettings.bundleVersion;
config.VersionCode = int.Parse(PlayerSettings.iOS.buildNumber);
config.NewVersion = "";
PackageSetting.Save();
}
}
部分自动化脚本布局如下图:
这样,我们从分支策略到团队协作,到多平台发布出包,基本上就探讨完毕了,希望对大家起到抛夸引玉的作用。