Git for Unity 的调教手册

本文章翻译自:https://hextantstudios.com/unity-using-git/

在Unity项目中使用Git作为版本控制手段之前,需要进行一些简单的初始化设置,这些操作都可以在命令行中快速完成。本文介绍如何使用Git和LFS(Large Files Storage,大文件存储)初始化一个新项目,以处理游戏开发所需的大量二进制文件。

配置Git以适应Unity项目

上面说到我们需要做一些必要的Git设置来让其更好配合Unity项目,但在此之前,我们先来看看Unity的基本设置。下面列出的设置项为较新版Unity(2020.3)的默认设置,虽然它们通常没有问题,但建议还是检查以确保万无一失。

  • Edit / Project Settings

    • Version Control

      • Mode : Visible Meta Files

  • Edit / Project Settings / Editor

    • AssetSerialization

      • Mode : Force Text

Version Control 设置为 Visible Meta Files 确保了Unity根据项目中的资源文件、目录生成的 .meta 文件不会被隐藏。Git命令行客户端始终会找到 .meta 文件(不论它们是否被隐藏),但将它们设置为可见,可以避免其他Git客户端找不到 .meta 文件而出错。

AssetSerialization设置为 Force Text 确保Unity以文本的方式保存资源,而不是以二进制的形式。这使得 merge 操作更为容易,并允许Git将“更改”(changes)存储为较小的增量信息,而不是文件的完整副本以节省存储空间。

配置Git用户设置

在创建或者克隆一个Git仓库之前,最好设置一下Git的用户设置(user settings)。

设置用户名和邮箱,这两项信息会在提交(commit)操作执行时用到:

git config --global user.name "XXX"

git config --global user.email "XXX@example.com"

这些设置会被保存在 ~/.gitconfig 文件中,每一个系统用户可以拥有一份,你可以通过执行下面的命令来打开它,并直接编辑:

git config --global -e (e代表edit,-e--edit 的别名)

在Win10系统中,global版的config文件在 (系统盘)C:\Users\UserName 目录下。

如果你想给某个仓库单独设置用户名与邮箱,只需要在创建好仓库后,执行上方去掉 --global 的命令或者将其替换为 --local

题外话:git的config文件分为三种,系统级别 --system ,包含系统所有用户和所有库的值,保存在安装路径下的 ./etc/gitconfig;用户级别 --global 具体到你的账户;以及仓库级别 --local ,位于仓库目录下的 .git 文件夹中。

这一步结束前,还有一些额外的用户设置值得你日后回顾:

default editor

diff and merge tools

command aliases

more

为Git配置Perforce P4Merge(可选)

(summid注)首先,什么是P4Merge?在P4Merge官网有介绍:P4Merge tracks and compares the effects of past and pending work for branches and individual files. You can even use it to resolve conflicts (especially with Git). 简单来说,它是一个帮助我们比较两个文件差异的工具,最重要的是,它是图形化且免费的。

下载安装完P4Merge(安装时可选择只安装Merge and Diff Tool组件,它才是P4Merge本体),要在Git中配置好P4Merge,只需要将下面的文本添加到 .gitconfig 文件中即可:

[diff]
    tool = p4merge
[difftool]
    prompt = false
[difftool "p4merge"]
    cmd = 'C:/Program Files/Perforce/p4merge.exe' \"$REMOTE\" \"$LOCAL\"
    prompt = false
[merge]
    tool = p4merge
[mergetool]
    keepBackup = false
[mergetool "p4merge"]
    cmd = 'C:/Program Files/Perforce/p4merge.exe' \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"
    trustExitCode = true

需要将其中cmd路径换成自己的P4Merge安装路径。

设置完毕后,在以后使用命令行时,凡是 diff 命令都可以用 difftool 代替,以打开图形化界面来比较文件差异。

为 Git 配置UnityYAMLMerge (可选)

(summid注)首先,什么是YAML?YAML全称是【YAML Ain’t a Markup Language】,翻译过来是【YAML不是一种标记语言】。看了它的全名,就好像看了全名一样。但YAML还有另一个名字,【Yet Another Markup Language】,【仍然是一种标记语言】,之所以有两种完全相反的意思,是因为YAML它具有标记语言的特点——比如它参考了XML,同时强调以数据为中心。你可以理解为类似XML、JSON等表达数据序列化的格式。Unity中的资源文件的信息,都是以YAML格式保存的(包括 .meta 文件),如果你用文本编辑器打开它们,你就会看到YAML本尊(前提是你之前将AssetSerialization选项设置为Force Text)。实际上,几乎所有资源文件,比如图片、视频、音频、预制件、地形文件等,它们本质都是【二进制】文件,想要比较、合并两个【二进制】文件,是非常困难的,对于计算机来说也是如此。为此,我们就需要 Git 在 YAML 的指导下进行合并。

Unity自带有帮助我们合并 Scene 和 Prefab 的工具——UnityYAMLMerge。该工具就在你UnityEditor安装目录下,~\Editor\Data\Tools\UnityYAMLMerge.exe 。美中不足的是,尽管绝大多数资源文件可以被序列化为YAML,但在合并工作开展时UnityYAMLMerge不会自动参与进来——除非你指定 force 模式。为了将该工具为我们所用,下面我们开始配置它。

我们需要在 .gitconfig 文件中增加一条新的 【merge tool】 条目,为了更方便使用,我们还将它设置为 Git 的默认合并工具。用下面的文本替换上一节中的合并工具:

[merge]
    tool = unityyamlmerge
    
[mergetool "unityyamlmerge"]
    trustExitCode = false
    keepBackup = false
    cmd = 'C:/Program Files/Unity/Editor/Data/Tools/UnityYAMLMerge.exe' merge -p \"$BASE\" \"$REMOTE\" \"$LOCAL\" \"$MERGED\"

注意你需要将 cmd 路径替换为自己使用的UnityEditor路径。

(summid注)你会发现,因为我们是在 global config 文件中配置的合并工具,意味着每当我们在不同Unity版本之间切换时,就面临是否需要修改配置的抉择——万一出现问题了呢。summid 这里建议将该配置放进 local config 文件中,毕竟一个项目对应一个Unity版本,这样就免去了修改 global config 的麻烦,坏处是每开一个新项目都需要配置一次。git 会优先满足仓库的配置文件,当仓库文件中不存在 key 时,才会去更高级的配置文件中寻找,这样也不用担心它会覆盖掉 global config 中的配置。

在 merge 场景或预制件遇到冲突时, git mergetool 会自动尝试用 UnityYAMLMerge 来解决冲突。当冲突没能成功解决时,会执行【回退操作(fallback)】,回退操作所采用的工具会记录在与 UnityYAMLMerge 同一目录下的 mergespecfile.txt 文件里。该文件里自带了一些 merge 工具(包括P4Merge),并且排在前面的工具会被优先采用。你可以编辑这个文件来自定义 fallback 工具,但注意当安装了新 Unity 后这个文件将被系统修改。

一般来说, Git 会在检测冲突时( pull 和 merge 命令)用 UnityYAMLMerge 来防止展示不存在(not truly exist)的冲突。例如,一个属性只是被简单地改变了位置,但它的值是没有变化的(For example, say a property was simply moved around, but its value was unchanged. )。并且 Git 的 merge 算法也会忽略一些真正的冲突,即使可能性非常小。为了发现这些情况,我们可以在 .gitattributes 文件里自定义一个 【merge driver】,并指定哪些文件需要用到它:

[merge "unityyamlmerge"]
     name = UnityYAMLMerge
     driver = 'C:/Program Files/Unity/Editor/Data/Tools/UnityYAMLMerge.exe' merge --force --fallback none %O %B %A %P

我们会在稍后讨论 .gitattributes 文件。

这里启用 force 选项的原因是,Git 在合并时对一个文件的不同版本采用完全随机的命名方式,并且 UnityYAMLMerge 不采用【destination】参数来检测文件类型。需要注意的是当前我还在尝试使用该 merge driver,它可能无法与除 P4Merge 以外的合并工具使用。(添加 describe 选项可以在 UnityYAMLMerge 检测到冲突时将其输出出来)

最后,上面的 cmddriver 值需要指定为你自己的路径。配置这些路径看起来非常麻烦,尤其是在使用 Unity Hub 时,因为这些路径会根据你使用的 Unity 版本变化而变化,但目前还没有很好的解决办法。

创建 Git 仓库

接下来为 Unity 工程创建一个新 Git 仓库:

cd /c/Projects/MyUnityProject
git init .

执行上面的命令后,会创建新 Git 仓库和一个 .git/ 目录,并且系统将输出下面的内容:

Initialized empty Git repository in C:/Projects/MyUnityProject/.git/

我们可以用 status 命令来确认当前仓库的状态:

git status -s

然后系统会输出类似下面的内容:

?? .vs/
?? Assembly-CSharp-Editor.csproj
?? Assembly-CSharp.csproj
?? Assets/
?? Library/
?? Logs/
?? MyUnityProject.sln
?? Packages/
?? ProjectSettings/
?? Temp/
?? obj/

?? 表示该文件或目录被 Git 检测到,但还没有纳入版本管理,即还没有提交过。在提交文件前,我们需要让 Git 忽略一些不用提交的文件和目录,例如临时文件或 Unity 可以重新生成的文件。

添加忽略文件和忽略目录(.gitignore)(必要)

要忽略某些文件或目录,我们需要在项目根目录创建 .gitignore 文件,并将需要忽略的文件、目录添加进去。(summid 注)在项目根目录创建不是必须的,你可以在仓库目录下其他位置创建它,将它放在根目录下只是比较通用和方便的做法,但你可以根据实际情况进行调整,例如你的仓库根目录项目根目录并不是一致的,项目根目录可能在仓库根目录的子目录下,因为你可能想要在仓库根目录下存放Unity项目以外的其他文件(项目文档、配置表等)。

.gitignore 文件的内容可能会比较多,但已经有许多模板供我们选择, GitHub上就有类似的仓库,其中记录了大多数项目的忽略配置文件模板,包括Unity项目。你可以在模板的基础上添加自己的忽略规则,其语法可以在 git官方文档中查询。

一旦我们配置好了 .gitignore 文件,再次执行下面的命令:

git status -s

之后将显示应该被纳入版本管理的文件(目录),除此以外的文件都是没必要同步的:

?? .gitignore
?? Assets/
?? Packages/
?? ProjectSettings/

需要注意的是,.gitignore 文件也需要被同步。

初始化 Git LFS (建议)

Git 仓库通常包含文件的所有修改记录。由于二进制文件无法根据其增量(差异)轻松存储,因此将声音、纹理文件等游戏资源添加到仓库可能会随着时间的推移导致性能问题,尤其是在克隆仓库副本时。

Git Large File Storage(LFS)扩展的出现就是为了解决该问题。它的工作原理是将小文件保存在仓库中,这些小文件仅指向仓库外部的实际文件。至于哪些文件的存储工作以这种方式进行,决定于我们指定了哪些文件类型为大文件类型(large file type)。在使用普通 Git 命令时,会根据需要传输大型文件。使用 LFS 的缺点是需要远程仓库支持该特性,比如 GitHub、Azure DevOps 或 Gitea

LFS 特性已经包含在 Git 命令行客户端和许多 GUI 客户端里,但在新仓库中还是要通过一些步骤来启用它。第一步,初始化 Git LFS:

git lfs install

这条命令只需要被执行一次,之后从远程仓库克隆的用户不需要再执行它(官方文档中注释:You only need to run this once per user account.)。如果 LFS 被成功初始化,会输出下面的内容:

Updated git hooks.
Git LFS initialized.

配置 Git LFS (.gitattributes)(建议)

下一个非常重要的步骤是指定那些文件应该被 Git LFS 所管理。这一步可通过 git lfs track 命令来完成,但当这里有非常多的文件类型需要配置时,直接编辑 .gitattributes 文件会更方便。.gitarrtibutes 还用于指定 Git 应如何处理文件类型的行尾,以及执行合并和比较差异的方法。更多信息可以在官方文档中找到。

这个文件的内容同样会比较多,这里有原作者的 .gitattributes 文件模板。我会介绍一些重要的部分,首先从一些宏开始,它会让后面的阅读和编辑更清晰:

# Macro for Unity YAML-based asset files.
[attr]unityyaml -text merge=unityyamlmerge diff

# Macro for all binary files that should use Git LFS.
[attr]lfs -text filter=lfs diff=lfs merge=lfs

unityyaml 宏用于被 Unity 创建的资源,这些资源用的是 YAML 格式。-text 选项是为了防止 Git 在不同平台上自动转换这些文件的换行符,这可能会导致一些问题。merge=unityyamlmerge 选项指示 Git 在合并时采用之前在 .gitconfig 中设置的 unityyamlmerge 工具。最后,diff 选项指示 Git 始终执行文本的差异。

lfs 宏用于所有二进制资源文件(包括 Unity 创建的)。宏后面的选项参数指示 Git 用 LFS 来管理这些文件。

下面的主要部分指定所有基于 Unity YAML 的资源文件采用 unityyaml 宏:

...
# Unity Text Assets
*.meta unityyaml
*.unity unityyaml
*.asset unityyaml
*.prefab unityyaml
...

下面部分指定一些 Unity 创建的二进制文件采用 lfs 宏:

...
# Unity Binary Assets
*.cubemap lfs
*.unitypackage lfs
# Note: Unity terrain assets must have "-Terrain" suffix.
*-[Tt]errain.asset -unityyaml lfs
# Note: Unity navmesh assets must have "-NavMesh" suffix.
*-[Nn]av[Mm]esh.asset -unityyaml lfs
...

因为 terrain 和 navmesh 没有自己的扩展名,所以用文件名后缀来区别它们和 .asset 文件。在命名它俩的时候记得匹配命名规则。理想情况下,Unity 会给它们采用独特的文件扩展名,因为它们似乎违反了我们之前设置的 Asset Serialization Mode 选项。对于已经存在这些资源的项目,在提交前应该重命名它们或修改配置文件中的匹配规则。

最后剩下的部分是非常多的二进制文件:

...
# Image
*.jpg lfs
*.jpeg lfs
*.png lfs
...
# Audio
*.mp3 lfs
*.ogg lfs
*.wav lfs
...

可以根据需要向里添加新的文件类型,但注意添加前没有新文件在暂存区里。如果你哪个部分设置出错,并且向仓库提交了二进制文件,你可以用 migrate 命令把它交给 LFS。

你可以检查特定文件被设置了哪些特性:

git check-attr -a Assets/Town-Outside-NavMesh.asset

按上面的配置来,会输出哪些部分用于 LFS:

Assets/Town-Outside-NavMesh.asset: diff: lfs
Assets/Town-Outside-NavMesh.asset: merge: lfs
Assets/Town-Outside-NavMesh.asset: text: unset
Assets/Town-Outside-NavMesh.asset: unityyaml: unset
Assets/Town-Outside-NavMesh.asset: lfs: set
Assets/Town-Outside-NavMesh.asset: filter: lfs

.gitignore 文件一样,GitHub 上也有收集 .gitattribute 文件模板的仓库,你可以根据模板修改为自己的版本。

向仓库提交文件

初始配置终于完成了,接下来开始提交文件到仓库里。我喜欢在第一次提交中检入 .gitignore.gitattribute 文件。首先,我们需要将它们放入 Git 的文件暂存区里:

git add .gitignore
git add .gitattributes

然后检查下仓库状态:

git status -s

Git 会输出类似下面的内容:

A  .gitattributes
A  .gitignore
?? Assets/
?? Packages/
?? ProjectSettings/

俩配置文件前面的 A 标致表示它们已经存入了暂存区。暂存区记录了新的或有改动的文件,在你执行 commit 命令后,暂存区的改动将被提交到仓库:

git commit -m "Added .gitignore and .gitattributes"

提交之后 Git 会显示类似下面的内容:

[master (root-commit) ebb2157] Added .gitignore and .gitattributes
 2 files changed, 191 insertions(+)
 create mode 100644 .gitattributes
 create mode 100644 .gitignore

这时如果我们再查看仓库状态,会发现 Git 不会再展示这两个配置文件的信息了,因为它们不是新文件,且自上次提交后没有被修改过。

下面我们提交所有文件到暂存区:

git add -A

或者:

git add .

这时 Git 可能会显示一些警告,比如:

warning: LF will be replaced by CRLF in Assets/Scripts/SimpleCameraController.cs.
The file will have its original line endings in your working directory
...

这是换行符导致的,你可以忽略它,但如果想关掉这个警告:

git config --global core.safecrlf false

现在该文件保存在了暂存区,执行提交命令将它提交至仓库:

git commit -m "Initial commit of project assets."

再次执行 git status -s 将不再显示该文件。

推送至远程仓库

之前的所有工作都是针对本地 Git 仓库的,现在该连接到远程仓库了。首先你得在 GitHub 或者 Azure DevOps 等托管网站上创建一个新仓库。这个步骤很简单,如果可以,在创建的时候选择【不要创建 ReadMe 文件】,这将让推送初始化的本地仓库更加容易。

在托管网站创建好仓库后,以 GitHub 为例,它会显示用于连接该仓库的 URL 。用 HTTPS 协议来建立连接的话,可在本地仓库目录下采用下面的命令:

git remote add origin https://...remote url...

现在,可以向远程仓库推送内容了:

git push origin main

将本地仓库的当前分支,推送到远程仓库的 main 分支上。

推送至远程仓库需要相应的权限,如用 SSH 来进行辨认该推送请求是否合法。

克隆远程仓库到本地

现在仓库已经被推送到了远程服务器上,其他人可以从远程服务器上将其克隆一份到本地(如果是私有仓库,请确保他们有被授予访问权限)。使用 clone 命令和仓库 URL 来进行克隆:

git clone https://...remote url...

这个命令会在当前目录下创建一个子目录,该子目录包含远程仓库的所有文件。

从远程仓库更新文件

当在仓库修改或添加文件后,应该将它们提交上去,以确保工作进度不会丢失,并且其他人能够获取这些修改后的文件,这一步和之前使用 addcommit 命令一样。在尝试推送更新前,最好先从远程服务器拉取一次更新。你可以使用 pull 命令:

git pull

如果有任何文件需要更新,pull 命令会将它们检索到本地存储库并将任何更改同步到你的本地仓库里。当有文件因为产生冲突而不能被自动合并时,Git 会启动配置好的合并工具。修改完有冲突的文件后,这些文件需要重新提交到暂存区,并执行一次 commit 命令,最后才能将所有更改推送到服务器。

在这点上,我经常切换至 GUI 客户端,因为它更便于我在提交前浏览有哪些修改。命令行客户端对于 Git 高级命令仍然很有用,如果修改的部分比较少,那么用命令行客户端会更快一些。

Git 命令别名

为了让命令行客户端使用起来更方便,Git 支持用户自定义命令别名(Aliases)。别名可以在 .gitconfig 文件中添加:

[alias]
  ...

如果别名以【 ! 】开头,那么 Git 会执行完整的 shell 命令。例如,我们创建一个 commit-all 别名,用于添加所有修改到暂存区,并且执行 commit 命令:

[alias]
    # Add all and commit:
    commit-all = "!git add -A;git commit" 

这个别名可以这样使用:

git commit-all -m "Another great commit."

对于只是 Git 命令参数的别名,【 ! 】 就不需要了。比如要创建一个用短输出格式显示仓库状态的别名:

# Short git status output:
s = "status -s"

别名可以更好地与命令行客户端一起工作,因为它可以为复杂的命令提供有意义的名称,并且在使用 【tab-completion】后,别名们会和 Git 内置命令一起显示出来:press TAB after typing git <space>

你可以在这里获取一些有用的别名清单

获取更多有关 Git 的姿势

Git 还有大量的重要信息,当然还有很多东西需要学习。希望本文有助于消除在你的项目中使用 Git 的一些疑惑。只要确保根据需要更新 .gitattributes 文件,以便 LFS 正确处理新的二进制文件类型!

要了解有关 Git 的更多信息,这里有一些很 awesome 的学习资源:

上一篇
下一篇