游戏多平台分支与打包策略

游戏开发,免不了要上多个平台,如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();
    }
}

部分自动化脚本布局如下图:

这样,我们从分支策略到团队协作,到多平台发布出包,基本上就探讨完毕了,希望对大家起到抛夸引玉的作用。

相关推荐
吾名招财4 天前
unity3d入门教程五
游戏引擎·unity3d
charon87784 天前
虚幻引擎 | 实时语音转口型 Multilingual lipsync
人工智能·游戏·语音识别·游戏开发
吾名招财4 天前
unity3d入门教程六
游戏引擎·unity3d
吾名招财4 天前
unity3d入门教程七
游戏引擎·unity3d
Thomas_YXQ6 天前
Unity3D 实现水体交互详解
开发语言·unity·编辑器·交互·unity3d·游戏开发
北冥没有鱼啊6 天前
ue5 伤害插件
游戏·ue5·ue4·游戏开发·虚幻
Coding小宇8 天前
静下心来我还是可以的-unitywebgl 滑动条增加
经验分享·unity·unity3d
吾名招财8 天前
unity3d入门教程二
游戏·unity3d
charon877816 天前
虚幻引擎VR游戏开发02 | 性能优化设置
游戏·游戏引擎·vr·游戏开发·虚幻·技术美术
Fuliy9621 天前
游戏开发设计模式之模板方法模式
java·游戏·设计模式·游戏开发·模板方法模式