聊聊Unity项目管理的那些事:Git-flow和Unity
0x00 前言
目前所在的团队实行敏捷开发已经有了一段时间了。敏捷开发中重要的一个话题便是如何对项目进行恰当的版本管理。项目从最初使用svn到之后的Git One Track策略再到现在的GitFlow策略,中间有经验也有教训,所以记录在本文,既是和各位朋友交流也供自己日后查阅。
0x01 基础:Unity项目如何做版本管理?
为什么更喜欢git?
初来项目组到时候,项目还在使用SVN作为版本管理的工具。作为一个不喜欢SVN的人,自然而然想到了换用git来做版本管理。这里当然并不是说svn不如git好,只是它们的思路的确是不一样的。 与SVN相比,git是一个分布式的版本管理工具。这一点可能是我喜欢git胜过svn的一个决定性原因。 当我们使用git从远端版本库/服务器上chect out代码后,git会在自己的机器上克隆一个自己的本地版本库。这样我们在本地就实现了版本的管理,而不必像svn那样必须和服务器连接。举一个例子,当我们在本地开发自己的功能时一旦不小心有了错误的操作,我们只需要在本地进行版本回退即可。如果使用svn的话,这种问题的修改似乎就变得不那么方便了。 喜欢git的另一个原因就是使用git的分支了。我们可以在本地的同一个工作目录下快速的切换不同的分支,每个分支之间都是隔离的。当我们不想影响主分支的时候,可以十分轻松的利用git创建一个新的分支进行开发。 总之,使用git替换svn作为团队的新的版本管理工具之后,团队的开发效率提高了很多。
git和Unity
既然项目组决定采用git作为新的版本管理工具,那么首先的一点就是我们要先确认哪些文件是需要纳入版本管理的。同时,在确认需要管理的文件时,顺便重新规整一下整个项目的目录结构,不仅仅是为了更加便于git进行版本管理,同时也可以更好的维护项目。
上图便是一个典型的Unity项目的默认目录。我们可以看到默认的Unity项目的目录下就已经有很多文件和文件夹了。 但是,作为版本管理,我们通常只需要关注两个文件夹即可。即:Assets文件夹和ProjectSettings文件夹。 其中,Assets文件夹主要用来存放项目的资源,例如脚本文件、贴图、材质、声音资源等等。 而ProjectSettings文件夹则用来存放一些项目的设置,例如输入设置、物理系统的设置、Player设置、Layer、Tags等等。我们可以在Unity的编辑器中的Edit->Project Settings菜单来调整这些设置信息。 默认目录下的其余文件或文件夹都可以由这两个文件夹的内容生成出来。 例如,如果Assets文件夹中包括C#脚本文件,则Unity会在目录生成C#工程文件Assembly-CSharp.csproj。 除此之外, Unity的一些特殊的文件夹也会生成一些工程文件。例如Editor文件夹内如果有C#脚本,则会生成一个Assembly-CSharp-Editor工程文件。 除了这些工程文件之外,Unity还会生成一些文件夹。例如Library、Temp、obj这三个文件夹。它们同样是Unity自动生成的。 Library文件夹的内容主要是在项目中导入资源时产生的一些本地的缓存。 Temp和obj这两个文件夹则是在项目构建时产生的临时文件。 因此,我们可以通过git的.gitignore文件来将不需要被git管理的文件/文件夹添加到忽略列表。下面是github提供的一份适用于Unity项目的.gitignore文件列表。
/[Ll]ibrary/ /[Tt]emp/ /[Oo]bj/ /[Bb]uild/ /[Bb]uilds/ /Assets/AssetStoreTools* # Autogenerated VS/MD/Consulo solution and project files ExportedObj/ .consulo/ *.csproj *.unityproj *.sln *.suo *.tmp *.user *.userprefs *.pidb *.booproj *.svd # Unity3D generated meta files *.pidb.meta # Unity3D Generated File On Crash Reports sysinfo.txt # Builds *.apk *.unitypackage
除此之外,我们还需要将Unity为导入资源生成的.meta文件也纳入版本管理,和相应的资源一同维护。meta文件的重要性在于Unity会利用它来处理对应资源之间的引用关系。
为了处理资源之间的引用关系,Unity在序列化时会通过两个数据来保证正确的引用关系。即文件GUID和以及本地ID,其中文件GUID便保存在meta文件中。
fileFormatVersion: 2 guid: 437eb1afce72bb44ca24c6ac3fe90c1d timeCreated: 1453720237 licenseType: Store NativeFormatImporter: userData: assetBundleName: assetBundleVariant:
只要使用文本工具打开meta文件即可以看到其内容。
另外还需要注意到的事情便是git版本管理时的文件冲突问题。通常当我们合并两个相同的地方都有修改的分支时,都会产生冲突。
一旦git不知道如何自动合并,就需要我们来手动解决冲突了。如果文件是文本文件,git便会在有冲突的地方做上标记(如 <<<<<<< HEAD ==== >>>>>>> HASH_ID等),参考这些标记我们可以很方便的解决冲突问题。
但是,如果文件是二进制的文件,一旦发生冲突则很难查看git插入的冲突标识,解决起来是比较棘手的。因此Unity项目的资产序列化最好生成文本文件,而不是二进制文件。我们可以在设置中选择序列化的策略。
这样场景文件和prefab等文件就会被序列化为yaml文本文件:
%YAML 1.1 %TAG !u! tag:unity3d.com,2011: --- !u!29 &1 SceneSettings: m_ObjectHideFlags: 0 m_PVSData: m_PVSObjectsArray: [] m_PVSPortalsArray: [] m_OcclusionBakeSettings: smallestOccluder: 5 smallestHole: 0.25 backfaceThreshold: 100 --- !u!104 &2 RenderSettings:
智能合并场景和prefab文件
将场景文件和Prefab文件序列化为yaml文本文件之后,仍然有可能会在合并时遇到产生冲突的情况。不过好在Unity在5.X版本之后已经提供了一个便于处理合并的工具,并且已经集成在Editor中。你可以在你的Unity安装目录下的Unity/Editor/Data/Tools文件夹中找到这个名为YAML Merge的工具。
它的使用方法也十分简单,以使用git作为版本管理工具为例。我们需要在.gitconfig文件中加上下面这几行配置:
[merge] tool = unityyamlmerge [mergetool "unityyamlmerge"] trustExitCode = false cmd = '<path to UnityYAMLMerge>' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
这样当两个人同时修改同一个场景,并且进行合并时,之前的git会报告这个冲突,让我们来手动解决该冲突。但是配置了YAML Merge工具之后的git会调用YAML Merge工具,并自动解决冲突。 更多相关的内容,各位可以去参考Unity官方的手册(https://docs.unity3d.com/Manual/SmartMerge.html)。
0x02 问题:大文件如何处理?
游戏开发,特别是3D游戏开发时会使用到很多美术资源、音频视频资源等等。如何把这些大型的二进制文件集成到git工作流中就成为了一个需要思考的问题。
由于git在处理二进制文件时默认会压缩并存储二进制文件的所有完整版本,如果二进制文件很多,这种做法显然不是最优,换句话说git在存储二进制文件时的效率不高。
针对这个问题,github开源了一个git的拓展,即Git Large File Storage(简称LFS),git大文件存储。规避了git传统的处理方法,Git LFS处理大型二进制文件的方式是用“文本指针”替换它们。
这些文本指针实际上是包含二进制文件信息的文本文件。文本指针存储在Git中,而大文件本身通过HTTPS托管在Git LFS服务器上。Git LFS 使用引用小文本文件指针指向存储在服务器的大型文件。
从git lfs的官网下载并安装好之后,我们就可以在项目中使用lfs来管理我们的大型二进制文件了。相关操作也很简单:
git lfs track "*.<file_extensions>"
例如我们要管理png文件,只需要输入git lfs track "*.png"就好了。
当然,我们也可以直接修改项目目录下的.gitattributes文件。
*.png filter=lfs diff=lfs merge=lfs -text
需要说明的是,很多公司使用的gitlab也已经宣布支持Git LFS了。所以项目的管理没有必要美术用svn、程序用git了。
0x03 策略:Git One Track 和 GitFlow
使用版本工具,除了要能正确合理的使用它之外,作为项目的管理者,还需要清楚一些和版本管理相关的策略。
因此,在本文的最后我们再聊聊使用git作为版本管理工具的管理策略吧。
简单的说,git的管理策略目前有两大流派。平时和同事聊天或和别的公司的朋友交流时也能够感觉的到,即Git One Track和Git-flow。
One Track
One Track简单的说,就是整个团队在开发项目时都在同一个分支上进行。这也就意味着开发阶段的所有工作都集中在同一个分支,例如新功能开发、bug的修复。当然,One Track策略并不意味着只有一个分支,而是只有一个开发分支。当达到团队设定的里程碑时,可以开一个新的分支用来维护这个基本稳定的版本,这个维护分支只进行维护的工作,而不进行开发的工作。同时,开发分支继续进行最新的开发工作。
使用这种策略的最大特点就是大家都在同一个分支上工作,因此每次提交代码都有可能会有冲突。为了减少冲突,团队也常常会提高提交的频率,同时每次提交的颗粒度都比较小。同时,管理成本比较低,整个团队的学习成本也比较低。
在我之前的项目中,参与过一个刚从svn切换到git的团队,我们使用过一段时间One Track的工作方式,可以看到这种策略对整个团队接触和适应git还是很有好处的。
但是我相信更多的人还是更推崇另外一种策略,即Git-Flow策略。
GitFlow
首先我相信很多人一定在哪里会见过下面这张图:
这张图已经能很好的说明了gitflow了。即任何变更都是一个分支。
可以看到,这张图中的分支虽然很多,但是大体上可以分为两类。即主要分支和辅助分支。
主要分支
主要分支即git默认的mater分支以及一个主开发分支develop。
master分支是git默认的主分支,平时团队不在该分支上进行开发。而主开发分支develop则管理着开发人员提交的代码,当代码稳定时或固定一个周期,将develop分支上的代码合并到主分支。
辅助分支
辅助分支是团队每个开发人员都能接触到的,常见的辅助分支包括:
- 功能分支
- 发布分支
- 修复分支
这三类分支都有其对应的使用场景。
开发新的功能时,需要从主开发分支上创建一个新的功能分支,待该分支上的功能开发完毕之后,再合并会主功能分支。
发布分支则是在版本发布时创建的分支, 按照产品里程碑的需求包括应该完成的功能。
修复分支则是当出现bug时,为了不影响开发分支,因此创建出一个新的分支来修改bug,之后再合并回开发分支。
因此我们可以看到,GitFlow的策略无论是开发功能还是修复bug都是以分支的方式来进行。这样做的好处当然是管理上十分干净。但是由于功能开发时间相对要长、代码提交的粒度相对较大,因此在分支合并的时候有可能会出现冲突的问题,另外一个问题是对整个团队的要求要比One Track策略大。
不过,并没有最完美的方案,有的也仅仅是更适合团队的方案。例如很多团队包括我现在都更喜欢将两种方式混合使用,例如针对One Track都在同一个分支上开发,可能不够干净,我们就可以适当的开一个新的分支也用来开发。针对GitFlow提交合并时代码粒度大、冲突多,我们就每天都同步一次代码而不必等整个功能都完成再合并到主开发分支。
最后也希望大家也一起来聊聊项目管理,特别时游戏项目管理的一些经验吧。