如果你是个Unity开发者,却还没把项目装进Git仓库里管起来,我强烈建议你今天就开始做这件事。原因很直白:Unity项目的目录结构天然就对Git不太友好,加上场景、Prefab、贴图这些资源的特殊性,如果不按正确姿势来,你早晚会被一堆莫名其妙的合并冲突、反复重新导入的Library目录、.meta文件报错折磨到心态爆炸。
这篇文章我会把Unity + Git + GitHub这套工业化版本控制方案的完整步骤整理成一份可直接照抄的清单,覆盖从Git安装、GitHub配置、Unity工程准备、首次提交,到日常分支协作、场景合并冲突处理的完整闭环。会专门解释每一步背后的原因,以及我这些年实际踩过的坑和总结出的经验,帮你少走弯路。
1. 为什么Unity项目用Git经常翻车
1.1 Unity工程结构对Git并不友好
很多人第一次把Unity项目放进Git仓库后,都会发现一个诡异现象:明明只改了一行脚本,提交时却冒出一堆完全不认识的文件变动,有的在Library目录里,有的是莫名其妙生成的临时文件。这个问题要搞明白,得先从Unity工程目录结构说起。
一个标准Unity项目的根目录大致长这样:
- Assets:存放所有我们真正关心的资源,包括场景、脚本、Prefab、材质、贴图、模型、音频。
- Library:Unity本地缓存目录,用来存储导入资源后的中间数据、Shader编译缓存、场景预览图等。它完全可以从Assets和ProjectSettings重建。
- ProjectSettings:项目配置,比如渲染管线、输入系统、物理设置等,都是文本文件。
- Packages:项目依赖的Unity包管理清单。
- Temp、Logs、Obj、UserSettings:临时文件、日志、编译中间文件、编辑器界面布局配置,都属于“本机私有状态”。
问题就出在Library和Temp这类目录上。有些人贪方便直接git add .把整个工程塞进仓库,结果仓库体积轻松超过好几个G,团队每个人拉下来都要重新处理一遍巨大的缓存目录,整个仓库越来越臃肿。更麻烦的是,哪怕你没有主动改动任何东西,每次打开Unity,Library里的文件可能都会变化,提交记录变得无比混乱。
还有就是Unity的资源序列化模式。默认情况下Unity会以Force Binary模式保存场景和Prefab,二进制文件在Git里完全没法做差异对比,两个人改了同一个场景,合并时只能干瞪眼。所以要做版本控制,第一步就是把这个选项改成Force Text,才能让场景、Prefab变成可读的YAML文本格式,Git才有办法做文本级别的合并。
1.2 团队协作中的真正痛点
在我的实际项目经历里,Unity上Git团队协作的痛点主要集中在几个地方。
第一个就是场景文件。就算你把序列化模式改成了Force Text,同一个.unity场景文件里依然可能存着大量元素,包括场景内所有物体的Transform、组件参数、Lightmap引用、烘焙数据等。两个人同时在同一个场景里摆UI、调灯光,提交时几乎必然冲突,而且冲突内容非常难看,一长串YAML标记夹杂着m_FileID和guid,手工合并简直灾难。
第二个是.meta文件问题。Unity为Assets下每个文件都分配了一个.meta文件,里面记录着GUID。不同文件、脚本、材质之间的引用关系靠的就是GUID。如果meta文件丢失或被改名,Unity会生成新的GUID,导致所有引用它的资源全部断开,材质变粉、脚本丢失、Prefab组件丢失。所以meta文件必须提交,但很多人不知道这一点,或者用了错误的隐藏meta文件设置,直接把整个项目引用的根基给毁了。
第三个是二进制大文件。项目美术资源里贴图、模型、音频动不动就是几十上百MB,直接放进Git仓库,仓库体积迅速膨胀,提交、拉取、克隆都变得极其缓慢,而且这些二进制文件没有增量存储的概念,每次改动都是整份重新保存。这时候就必须借助LFS来做大文件托管。
2. 从零到一的准备阶段
2.1 Git安装与全局配置
这一节只说最基础、最必须的步骤,Git安装本身并不复杂,但有几个细节值得注意。
Windows用户建议直接去Git官网下载Git for Windows安装包,装的时候注意几个选项:勾选“Add Git Bash to Windows PATH”,这样可以让你在CMD或PowerShell里也能直接用git命令;行结束符换行符转换建议选“Checkout as-is, commit as-is”,这样能避免因为Windows和Mac/Linux换行符差异导致的诡异diff。Mac用户可以直接brew install git,Linux用户根据发行版用apt或yum安装。装完先验证一下:
git --version接下来配置全局用户信息,这一步不配的话,你的每次提交都会被标记成未知用户,而且后面推送的时候极大概率认不出你:
git config --global user.name "你的名字" git config --global user.email "你的邮箱"我建议再顺手配一个默认分支名和常用别名:
git config --global init.defaultBranch main git config --global alias.st status git config --global alias.co checkout git config --global alias.ci commit全局配置里最重要的一点是:user.name和user.email必须和GitHub账号对得上,特别是你自己邮箱或GitHub noreply邮箱。如果你不想把真实邮箱暴露在公开仓库里,可以在GitHub的Settings -> Emails里找到noreply邮箱地址,把它配置成全局邮箱。
2.2 GitHub仓库准备与SSH配置
代码托管平台我这边默认用GitHub,因为它在业界生态最丰富,Pull Request、Actions、LFS这些功能都做得比较完善。操作上先注册并登录GitHub,点击右上角加号,选择New repository。创建仓库时有一个很关键的细节:如果本地已经有Unity工程,建议仓库初始化的选项全都不要勾,包括README、.gitignore、License,直接创建一个空仓库。这样能避免本地仓库和远程仓库从两个互不相关的历史节点开始,后续合并时多出不必要的麻烦。
连接GitHub的方式,我强烈推荐用SSH而不是HTTPS。HTTPS每次推送都要输用户名密码,而且现在GitHub已经不支持密码认证,只能用Personal Access Token,体验很差。SSH密钥配上之后,推送拉取都不需要再输密码。
生成SSH密钥很简单,在Git Bash里执行:
ssh-keygen -t ed25519 -C "你的注册邮箱"一路回车到底,会在用户目录的.ssh文件夹下生成id_ed25519和id_ed25519.pub两个文件。公钥内容查看一下:
cat ~/.ssh/id_ed25519.pub复制整段内容,打开GitHub的Settings -> SSH and GPG keys -> New SSH key,粘贴保存。然后测试连接:
ssh -T git@github.com如果出现Hi xxx! You've successfully authenticated, but GitHub does not provide shell access.这行提示,说明SSH配置成功。
这里还有个常见问题:公司电脑或者多人共用电脑时,一个账号部署多个SSH key是可以的,建议在创建密钥时用-f参数指定文件名区分,比如ssh-keygen -t ed25519 -C "邮箱" -f ~/.ssh/id_ed25519_work。多密钥情况下,要在~/.ssh/config里按Host区分用哪个密钥,比如:
Host github.com HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work2.3 Unity编辑器端关键设置
在Unity里动手之前,有两个编辑器设置必须先改。打开菜单Edit -> Project Settings -> Editor,看右下角的Asset Serialization区域:
- Asset Serialization Mode:必须改成Force Text。这样场景、Prefab、材质等资源才会以YAML文本格式保存,Git才能做文本差异和合并。
- Version Control Mode:必须改成Visible Meta Files。这一步是很多新手的盲区。只有在这个模式下,Assets下每个文件都会生成对应的.meta文件,这些meta文件才能被纳入版本控制。如果选的是Hidden Meta Files,meta文件不落盘,Git仓库里没有meta记录,换一个人拉下代码,Unity会对所有资源重新生成GUID,整个项目的引用结构直接崩塌。
这两个设置必须在创建工程初期就确认好,如果项目一开始是以二进制模式创建的,后期修改这个选项后,场景和Prefab会重新序列化,第一次提交前把这些设置统一好就行。
另外如果你所在团队是主机平台或WebGL开发者,建议再检查一下Project Settings -> Player里的Scripting Runtime Version和API Compatibility Level,这些在项目开始前确定好,能减少后期升级带来的不必要diff。不过这部分跟版本控制本身关系不大,属于顺手提醒。
3. 项目初始化:首次提交的完整操作
3.1 写一份正确的.gitignore
终于到了动手创建仓库的核心环节。假设你手上有一个创建好的Unity工程,第一步不是急着git init,而是先在工程根目录放一份正确的.gitignore。没有.gitignore直接提交的后果,我在第一节已经说过了,Library和Temp这些目录一旦进去,后面再想剔除很麻烦。
我直接把我日常使用的Unity.gitignore模板贴出来,这份是根据Unity官方模板再结合实际经验调整过的:
[Ll]ibrary/ [Tt]emp/ [Oo]bj/ [Bb]uild/ [Bb]uilds/ [Ll]ogs/ [Uu]serSettings/ [Uu]serData/ [Uu]serLibrary/ [Mm]emoryCaptures/ [Rr]ecordings/ *.csproj *.sln *.suo *.tmp *.user *.userprefs *.pidb *.booproj *.svd *.pdb *.mdb *.opendb *.VC.db .vs/ .idea/ .vscode/ .utmp/ .ucache/逐条解释一下为什么要ignore。Library、Temp、Obj、Logs、UserSettings这五个目录属于本机状态,每个人本地生成的都不一样,完全没有必要提交,也不应该提交,它们都可以从Assets和ProjectSettings里重新生成。Build和Builds是打包输出目录,打包产物不应该进仓库,制品应该走专门的发布渠道。Vs和VSCode是IDE配置目录,里面存的是本地调试配置,也会导致不同开发者之间互相污染。
需要注意的是,UserSettings/里包含的EditorLayout.state是个人编辑器窗口布局,有些人喜欢提交,但我个人倾向于不提交,因为团队每个人的屏幕尺寸、编辑器习惯不同,提交了反而容易让别人的布局被覆盖。
3.2 安装并配置Git LFS
接下来是Unity项目版本控制的另一个核心:Git LFS。LFS的全称是Large File Storage,简单理解就是GitHub官方提供的大文件存储方案,它把真正的大文件内容替换成一个指针存在仓库里,大文件本体存放在LFS服务器端。这样克隆仓库时不会把所有历史版本的大文件都拉到本地,仓库体积和操作速度都能得到显著改善。
安装LFS很简单,去Git官网下载Git LFS安装包,或者如果你用了Homebrew,直接:
brew install git-lfs装完先在任意目录跑一次全局初始化:
git lfs install然后在Unity工程根目录下执行:
git lfs track "*.psd" git lfs track "*.png" git lfs track "*.jpg" git lfs track "*.jpeg" git lfs track "*.tga" git lfs track "*.tif" git lfs track "*.tiff" git lfs track "*.gif" git lfs track "*.mp4" git lfs track "*.mov" git lfs track "*.avi" git lfs track "*.fbx" git lfs track "*.blend" git lfs track "*.obj" git lfs track "*.max" git lfs track "*.dae" git lfs track "*.3ds" git lfs track "*.wav" git lfs track "*.mp3" git lfs track "*.ogg" git lfs track "*.aiff" git lfs track "*.flac" git lfs track "*.dll" git lfs track "*.exe" git lfs track "*.app" git lfs track "*.so" git lfs track "*.jar" git lfs track "*.bin" git lfs track "*.bytes" git lfs track "*.asset"注意最后这行*.asset需要斟酌。普通脚本创建的ScriptableObject的asset文件是文本,不需要LFS;但项目如果做了Addressables或AssetBundle相关的asset资源,体积可能会很大,可以考虑纳入LFS。我的建议是:先只跟踪明确的大体积二进制格式,等确实遇到大体积asset时再单独加。
执行完LFS track之后,根目录下会生成一个.gitattributes文件,这个文件记录了哪些路径被LFS跟踪,务必提交到仓库里。你可以在项目里打开看一下,检查内容是否正确。
3.3 首次提交与推送到GitHub
准备工作做完就可以正式初始化仓库了。在Unity工程根目录打开Git Bash,依次执行:
git init git add . git commit -m "chore: 初始化Unity项目,配置Git和LFS" git branch -M main git remote add origin git@github.com:你的用户名/你的仓库名.git git push -u origin main执行到git add .的时候,建议先跑一下git status看一眼暂存清单,确认里面没有出现Library、Temp这种不该进来的文件。如果发现有,回头检查.gitignore是否生效。首次提交信息,我个人习惯用chore:前缀,标示这是一次工程初始化,不涉及具体功能。
推送成功后,去GitHub仓库页刷新,应该能看到完整的Unity工程文件。这里要提醒一下,首次推送大工程时,如果LFS里面有很多大文件,可能需要等一段时间,期间千万不要中途按Ctrl+C。如果网络不好导致推送失败,可以试着重跑一遍git push,git支持断点续传。
4. 日常开发流:分支模型、提交规范与代码审查
4.1 分支策略怎么选
仓库建好了,接下来是日常协作里最核心的问题:分支怎么用。很多人小团队或单人开发,图省事直接在main上push代码,这样做短期内没问题,但只要两个人同时开发不同功能,提交历史就会纠缠不清,回退、排查问题都很麻烦。
我推荐绝大多数Unity团队采用GitHub Flow分支模型,简单高效,足够支撑多数项目。核心规则只有几条:
- main分支永远保持可运行、可发布状态。
- 任何新功能、Bug修复、实验性改动,都基于最新的main拉一个feature分支。
- 分支命名建议带上类型和用途,比如
feature/player-movement、fix/ui-button-response、test/lighting-shader。 - 功能开发完成、自测通过后,通过Pull Request合回main,合回之前做代码评审。
如果你的项目处于中后期,迭代节奏变得很快,一次发布要同时管理多个版本,可以考虑在GitHub Flow基础上引入release分支。比如从main拉出release/2.0.0,工程师在release分支上做最后修复,main继续向下一个版本开发。但Unity项目一般不建议搞太复杂的Git Flow结构,因为场景和资源合并成本高,分支生命周期越长,最终合并时冲突越惨。
实际项目里最常见的坑是:一个人拉着feature分支开发了三个星期,期间main已经被别人推进了几十个commit,等到合并的时候,场景文件、灯光文件、烘焙数据冲突成一片,理都理不清。所以Unity项目做长生命周期功能分支时,一定要定期把main的更新合并回feature分支,保持feature分支离main尽量近。这个习惯能极大降低后续冲突的爆炸概率。
4.2 统一提交信息与PR流程
提交信息的规范化在Unity项目团队里尤其重要。因为Unity的YAML资源文件diff可读性差,如果提交信息再写得含糊其辞,过两周回头看根本无从下手。我给团队定的规范通常是:
- feat:新功能。比如
feat: 实现角色冲刺机制。 - fix:修复问题。比如
fix: 修复连续跳跃时动画卡顿。 - refactor:重构,不改功能只改结构。比如
refactor: 将战斗逻辑拆解为状态机。 - docs:文档变更。
- style:代码格式、空格、分号等不影响逻辑的改动。
- perf:性能优化。
- chore:构建配置、工具链、依赖调整等杂项。
规则不复杂,但统一要求之后,git历史看起来会非常舒服。另外建议强制要求:每个PR只做一件事,不要在一个PR里一边修Bug一边加新功能。Unity场景文件对改动粒度的敏感程度比普通代码高得多,改动混在一起,出问题后定位、回滚都极其痛苦。
PR流程方面,Unity项目除了让写代码的人看代码逻辑,还应该让场景相关的负责人仔细核对资源提交范围。我经常在评审时看到有人顺手把整个场景文件提交了,里面包含了一堆本地Debug用的临时物体,这种误操作在混合了代码和资源的Unity项目里太常见了。评审时拿diff软件仔细看.unity文件的变更内容,比嘴上喊着“注意安全”管用一百倍。
5. 场景合并冲突的终结方案
5.1 为什么Unity场景一合并就爆炸
代码文件冲突大多数情况下都还算好解决,因为diff工具能直观展示哪些行被改了。Unity场景文件就完全是另一个物种。哪怕把序列化改成文本,一个大型场景文件动辄几万行YAML,包含成千上万个物体的m_FileID、m_LocalPosition、m_Component引用。两个人同时往场景里各放一个道具,冲突区域可能相隔千里,但Git依然会把整个文件标记为conflict。
更深层的麻烦在于,场景里很多字段是Unity自动维护的,比如光照烘焙数据、导航网格数据、遮挡剔除数据,这些字段在visual元素没变甚至没有人为改动的情况下,也可能因为烘焙时机的不同而产生变化。这就导致一个很无语的现状:A只调了UI按钮文字,B只动了地形,两边merge时依然会冲突,而且冲突内容是一堆难以人工裁决的GUID和哈希值。
想避免场景冲突,最有效的手段是限流而非解决。具体做法就是把场景拆小:不要用一个大场景承载全部游戏内容,按区域、按系统拆成多个预制体Prefab,然后把Prefab放到场景中引用。这样不同人开发不同子系统的时候,操作的是不同Prefab,冲突概率呈指数级下降。我在团队的规范里明确要求:场景里只放“组装对象”和“场景特有逻辑”,所有可复用的实体、UI、交互物体一律做成Prefab。
5.2 UnityYAMLMerge智能合并配置
尽管做足了预防,场景冲突还是无法完全避免。这时候就要请出Unity官方提供的智能合并工具UnityYAMLMerge了。
UnityYAMLMerge是Unity编辑器自带的一个命令行工具,它比Git内置的diff合并聪明在哪里?它可以识别YAML结构,准确理解Unity场景、Prefab、Animator Controller这些资源文件的结构化特性,能在冲突时尽量把双方的不同修改合并到最终文件中,而不是像普通文本合并那样简单粗暴地标记冲突。
要使用UnityYAMLMerge,在Git配置里把它设为mergetool即可。Windows下通常这样配置:
git config --global merge.tool unityyamlmerge git config --global mergetool.unityyamlmerge.trustExitCode false git config --global mergetool.unityyamlmerge.cmd "'C:/Program Files/Unity/Hub/Editor/2021.3.11f1/Editor/Data/Tools/UnityYAMLMerge.exe' merge -p \"\$BASE\" \"\$REMOTE\" \"\$LOCAL\" \"\$MERGED\""注意UnityYAMLMerge的路径要替换成你本机安装的Unity版本对应路径。Mac上的路径一般是/Applications/Unity/Hub/Editor/2021.3.11f1/Unity.app/Contents/Tools/UnityYAMLMerge。
配置好后,当git merge遇到场景冲突时,执行:
git mergetoolGit会自动用UnityYAMLMerge打开冲突文件,工具会尽量把两边的修改都综合进结果文件里。这一步跑完之后,如果工具返回成功,说明合并基本完成,你只需要在Unity里打开场景检查一下关键物件是否完整,确认没被误删或错位,然后git add保存合并结果。
需要提醒的是,UnityYAMLMerge并不是万能药。它在处理“同一场景同一物体的同一属性被两边改成不同值”这类冲突时依然无能为力,这种情况它会退出,你还是得手工介入。另外它处理嵌套Prefab和Overrides相关的冲突时体验一般,因为Prefab Overrides在文件里的表达极其复杂。
5.3 需要手动处理时怎么办
如果某次场景冲突严重到UnityYAMLMerge也hold不住,就只能手工解决了。步骤大致如下:
先用任意编辑器打开冲突的场景文件,搜索<<<<<<<。你会看到Git标出的冲突区块。接下来根据冲突内容做判断:如果冲突标记内的两边改动只是新增了不同物体,且没有互相引用,就手动把两边内容都保留,删掉冲突标记;如果改动集中在同一个物体上,就需要你根据产品需求决定保留哪一边的版本,或者手动把两边的修改合并成一个最终结果。
手动处理场景冲突非常耗时,我有一次处理一个大型地编场景的冲突,光是最重要的地形层就花了一个下午。所以再次强调:能拆Prefab就拆Prefab,能让一个人专职负责某一个场景就绝不两个人同时碰同一个场景。这是Unity团队协作的底层逻辑,不要指望合并工具能拯救一切。
6. 常见问题与排查技巧实录
6.1 Library文件夹被提交了怎么办
这是新手最常见的事故。一旦Library进了仓库,仓库体积和提交记录都会被污染。处理办法是:先把Library从Git索引中移除,但保留本地文件。
git rm -r --cached Library同时确认.gitignore里已经有[Ll]ibrary/,然后提交一条清理记录:
git commit -m "chore: 移除误提交的Library缓存目录,更新.gitignore"推送远端。接着让所有同事做一次拉取,并在本地执行git rm -r --cached Library再commit。多写一步的目的是确保远端仓库历史中不再追踪Library文件,避免后续每个开发者本地还残留着旧索引。
这里有个要点:清理后第一次提交,diff会非常大,因为是从远端仓库删除大量文件,这是正常现象。不要让团队成员在删除过程中手抖把本地真实的Library给删了。只要大家本地的Library都还在,Everything正常。
6.2 .meta文件丢失导致引用错乱
.meta文件丢失通常是因为克隆或拉取时忽略meta、设置错误导致meta没有被提交,或者某些开发者本地的人为删除。在这些情况下,Unity会在打开项目时自动补一个新meta,但文件GUID和原版不同,所有引用该资源的物体都会断掉连接。
如果meta丢失发生在你还没有commit的情况下,最简单的恢复方法是从Git历史中找回原meta文件:
git checkout HEAD -- 路径/文件名.meta如果是要找回一个较早历史版本的meta,可以用git log --oneline找到那一版提交,再用git checkout <commit_id> -- 路径/文件名.meta。找回后提交即可。
如果meta文件彻底不存在且Git历史里也没有记录,那就只能手动重新关联引用了,这个就很痛苦,因为场景里所有连接到该资源的组件GUID都要改,不建议手工操作,能重做资源就重做资源。
防止这类事故的最有效方法还是那句话:Unity里设置成Force Text + Visible Meta Files,并且保证这些meta文件进入Git仓库,团队里任何人不要手动删除meta文件。
6.3 LFS配额不够怎么办
GitHub对LFS有免费配额限制,具体数值不同时期会调整,但大致在存储1GB、带宽每月1GB这个量级。一旦超了,仓库推送就会报LFS quota exceeded之类的错误。
解决办法有两个方向。第一个是清理历史中的LFS对象,重写历史释放存储空间,但这个操作会影响所有人,要在团队拉新代码之前统一协调。第二个是升级GitHub账号的LFS套餐,花钱买空间,适用于确实需要长期托管大量大文件的团队。
更治本的做法是:不要什么大文件都往LFS里扔。图片纹理这类资源,最好在导入Unity前就完成压缩,以更小的格式入仓库;模型文件做减面处理;音频尽量转成压缩格式。Unity项目本身有压缩设置,合理配置能省下大量体积。另外,已经在LFS里的大体积文件,如果确定不再使用,也要及时用LFS命令移除。
6.4 GitHub连接不稳定时的备案措施
这个相信不少开发者都遇到过:git push、git clone的时候,连接GitHub偶尔会超时、掉线甚至直接拒绝连接。因为是网络问题,不同网络环境下表现差异非常大,有时换一个网络就好了,有时需要重试多次。
我的日常做法是:把项目的远端同时配置为两个仓库,一个放在GitHub,一个放在其他可选的代码托管平台,作为备份和同步中转。具体执行是在项目根目录添加多个remote:
git remote add origin git@github.com:你的用户名/仓库名.git git remote add backup git@gitee.com:你的用户名/仓库名.git平常开发,两个远端都推:
git push origin main git push backup main这样即使GitHub临时连不上,代码也还完整保存在另一个远端和本地,不会影响开发节奏。等GitHub恢复后再补推一次就行。如果再遇到连接问题,可以试试更换DNS为公共DNS,或在网络相对空闲的时段重试,刚发布或大流量时段GitHub确实更容易不稳定。
还有一个实用小技巧:如果你为了下载某个开源仓库的压缩包而连不上GitHub,很多代码托管平台都提供“仓库导入”功能,直接把GitHub仓库的一键导入到那个平台再下载,速度通常会好很多。日常开发中尽量呆在本地分支操作,不要频繁依赖远程仓库,这样即使网络不好,本地开发流程也不受影响。
最后说点真心话
如果你刚开始给Unity项目配Git,我的的建议是:这一整套流程看着步骤多,其实核心就三件事,一是设置好Force Text和Visible Meta Files,二是写对.gitignore并配好LFS,三是坚持用分支开发和Pull Request,场景里能拆Prefab就拆Prefab。我早期在unity项目上翻过的最大的车,几乎全是这三件事没做好酿成的。
把这套流程走一遍之后,你就有了可以放心提交代码、协作开发、随时回滚的工程基础。再往后还能继续演化,比如接入GitHub Actions做自动化构建、配置CI/CD流水线、做版本发布管理,这些都是在这个地基上长出来的能力,先把今天这一套基础打牢,比什么都重要。