跳转到内容

42. 版本管理和备份

约 50 分钟 难度:进阶 动手章

第一个 VRChat 世界做了一个月,文件夹已经从最初几十 MB 长到了几个 GB。某天打开项目,Unity 报「场景文件损坏」。前一晚上烘焙光照时电源跳了一次。今天才发现。

或者:上周改的某版同步逻辑能跑,今天改完抢分逻辑发现门又坏了。想回到上周那版,找不到「上周的版本」是哪个状态。Ctrl+Z 撤销不动,Unity 重启后所有撤销栈都丢了。

或者:项目下一步要拉一位朋友合作。两个人改同一个 Scene,怎么合并?

这三件事,纯靠手动复制粘贴文件夹解决不了。版本控制就是为这件事设计的。但 Unity 项目的版本控制有几个坑必须先填,否则会出现「仓库 5 GB,第一次 push 就被 GitHub 拒绝」「合并冲突全是天书无法手动解决」「打开项目所有引用都丢了」这类典型事故。

这一章把一份 VRChat / Unity 工程化基线一次铺好。完成之后这个项目能放心 git push,能跨机器同步,能两个人合作开发。

git init 之前先改两件 Unity 项目设置。这两件不改,后面 .gitignore 和合并工具的工作都白做。

打开菜单 EditProject SettingsEditor,找到 Asset Serialization 一栏,把 Mode 改成 Force Text

Editor 设置(Project Settings → Editor)
Asset Serialization
Mode Force Text ← 必须改成这个

这告诉 Unity:所有项目资源(Scene、Prefab、Material、Animator 等)一律以 YAML 文本格式存储,而不是二进制。

为什么这件事很重要:

  • 二进制文件 Git 几乎用不了:两个版本之间的差异显示为「整个文件改了」,没法看具体改了什么。三个人协作时,三份二进制场景文件几乎必然冲突且无法合并。
  • YAML 文本可读、可 diff、可合并:Smart Merge(下面讲)才能在 YAML 上跑。
  • VRChat 模板默认是 Force Text:用 VCC 创建的世界项目通常已经是这个设置,但合作开发时拉到的别人项目不一定。开始之前确认一遍。

同一个 Editor 设置面板里:

Version Control
Mode Visible Meta Files ← 默认就是这个,确认一下

Unity 给每个资源(每张图、每段音频、每个 .cs 脚本、每个 Prefab)都配了一份同名的 .meta 文件。.meta 文件存了这个资源的 GUID(全局唯一标识)和 import 配置(图的压缩格式、模型的导入选项、脚本的执行顺序等)。

GUID 是 Unity 内部用来在场景里互相引用的地址。Hierarchy 里某个 Prefab 引用了 MyMaterial.mat,引用的不是文件路径,是 MyMaterial.mat.meta 里的 GUID。

.meta 必须跟资源文件一起提交到版本控制。漏一个,引用就断了。

Asset Serialization 改完之后,下一件事是告诉 Git 哪些目录不要进版本控制。.gitignore(Git 的忽略清单)就是做这件事的文件。Unity 项目根目录下有一些纯缓存目录,加起来动辄几个 GB,每次打开项目都重新生成。这些不能进仓库。

GitHub 官方维护的 Unity.gitignore 是社区共识基线。在项目根目录新建 .gitignore,复制这份模板进去,再补几条 VRChat 相关的:

.gitignore
# ===== Unity 自动生成的目录(必须忽略)=====
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
/[Mm]emoryCaptures/
/[Rr]ecordings/
*.log
# ===== IDE / 自动生成的解决方案文件 =====
.vs/
.idea/
.gradle/
ExportedObj/
*.csproj
*.unityproj
*.sln
*.suo
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
# ===== Unity 构建产物 =====
*.apk
*.aab
*.app
*.unitypackage
*.unitypackage.meta
# ===== Crashlytics / 测试 / Addressables =====
crashlytics-build.properties
sysinfo.txt
mono_crash.*
InitTestScene*.unity*
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
/ServerData/
/[Aa]ssets/StreamingAssets/aa*
# ===== VRChat / UdonSharp 自动生成 =====
/[Aa]ssets/Resources/SerializedUdonPrograms/
/[Aa]ssets/Resources/SerializedUdonPrograms.meta
*.asset.bytes
# ===== Blender 备份 =====
*.blend1
*.blend1.meta

每段对应一种「这玩意 Unity 自己会生成,进仓库只会让仓库变大」的情况:

目录是什么为什么不进仓库
Library/Unity 项目缓存(导入的资源、生成的 .NET 程序集)几 GB,且换台机器打开项目会自动重建
Temp/当次 Unity 启动的临时文件Unity 关闭就该删的
Obj/脚本编译中间产物编译完就不需要了
Logs/Editor 日志跟开发本身无关
Builds/本地构建产物几百 MB,不该进版本控制
*.csproj / *.slnIDE 自动生成的解决方案每次打开项目重新生成
SerializedUdonPrograms/UdonSharp 编译产物每次脚本变化时重新生成

留下来要进版本控制的核心是 Assets/Packages/ProjectSettings/ 三份目录,外加一个 Packages/manifest.json(VCC 的依赖清单)。这几样合起来就是 Unity 项目可以被精确重建的所有信息。

设置和 .gitignore 都准备好之后,在项目根目录开仓库:

Terminal window
cd f:\GMPU\VRC\YourWorldProject
git init
git add .gitignore
git commit -m "chore: add unity gitignore"
git add .
git commit -m "init: vrchat world project baseline"

第一个 commit 单独提交 .gitignore 是为了防止「忘了加 ignore 直接 add 全部」把 Library/ 一起塞进去。装好 ignore 再 git add . 时,被忽略的目录就不会进仓库。

第一次 git status 看到的应该是 Assets/ 下的全部文件、Packages/manifest.jsonProjectSettings/ 里十几份 .asset 文件。Library/Temp/Builds/ 不该出现。如果出现,说明 .gitignore 路径写错了或者已经被 Git 跟踪过(要 git rm --cached -r Library/ 一次后再提交)。

Unity 项目即便排除了缓存,仍然有不少大文件:FBX 模型可能上百 MB、PSD 源文件、烘焙的 Lightmap、长音频、视频。这些文件 Git 处理起来效率很低(每改一次会保存整份),仓库会快速膨胀。

Git LFS(Large File Storage)是 Git 的扩展,把大文件存到独立的对象存储里,仓库本身只保留一个指针。每次 clone(把远端仓库完整拉到本地)时按需下载实际文件。

VRChat 项目里典型要走 LFS 的文件类型:

.gitattributes
# 模型
*.fbx filter=lfs diff=lfs merge=lfs -text
*.obj filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
# 纹理
*.psd filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
# 音频
*.wav filter=lfs diff=lfs merge=lfs -text
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
# 视频
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text
# 烘焙数据
*.unity3d filter=lfs diff=lfs merge=lfs -text

启用 LFS:

Terminal window
git lfs install
git add .gitattributes
git commit -m "chore: enable git lfs for large binaries"

注意 LFS 要在文件第一次提交之前配置好。已经用普通 Git 提交过的大文件需要用 git lfs migrate 迁移历史。先配 LFS 再开始正式提交,最省事。

GitHub 免费账号包含一定量 Git LFS 存储和带宽配额;截至 2026 年,GitHub Free / Pro 账号是 10 GiB 存储 + 10 GiB 带宽。VRChat 世界项目里素材量大的仍然容易超额。具体额度可能变化,写作时以 GitHub Docs 为准;超了之后可以买 GitHub LFS 配额、或者自托管 Gitea / 用 Cloudflare R2 做 LFS 存储后端。

不想用 LFS 的简单替代方案:把大尺寸源文件(PSD、Blend)放到云盘(OneDrive / Google Drive / 坚果云),仓库里只保留导出后的 .png / .fbx 缩小版。代价是源文件不在版本控制里,回滚不到。

第五节:场景文件冲突用 UnityYAMLMerge

Section titled “第五节:场景文件冲突用 UnityYAMLMerge”

两个人合作时最怕的不是脚本冲突,是场景冲突。MyWorld.unity 是一份大文本文件,里面是 Unity 自己的 YAML 序列化,一个 GameObject 涉及十几行 fileID / GUID 引用。Git 默认的合并算法对这种文件没辙。

Unity 自带一个 Smart Merge(智能合并)工具:UnityYAMLMerge。它能理解 Unity YAML 的语义结构(GameObject、Component、Reference),按对象级别合并而不是按文本行。

Unity 官方文档给的是 Git mergetool 配置。它不会在普通 git merge 时自动接管所有 .unity / .prefab 文件,而是在发生冲突后,你运行 git mergetool 时调用 UnityYAMLMerge。

打开 Git 配置文件(项目根目录下的 .git/config,或全局 ~/.gitconfig)加上:

[merge]
tool = unityyamlmerge
[mergetool "unityyamlmerge"]
trustExitCode = false
cmd = '<UNITY_PATH>/Editor/Data/Tools/UnityYAMLMerge.exe' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"

<UNITY_PATH> 是当前用的 Unity 安装目录,常见路径如 C:/Program Files/Unity/Hub/Editor/2022.3.22f1/Editor/Data/...(VRChat 当前 SDK 用的 Unity 版本以 VCC 推荐版本 为准)。

碰到场景或 Prefab 冲突时先 git status 看冲突文件,再运行:

Terminal window
git mergetool

UnityYAMLMerge 会尝试自动合并大部分变更。仍有冲突的地方,它会按 mergespecfile.txt 里配置的后备合并工具继续处理。

VRChat 项目用 VCC(Creator Companion)管理依赖。这给版本控制带来几个特殊点:

Packages/ 目录:里面有 manifest.jsonvpm-manifest.json 等。这些 JSON 要进版本控制,记录了项目依赖哪些 SDK 版本、哪些社区包。

Packages/com.xxx/ 子目录:VCC 解压的 SDK 实际内容(VRC SDK Worlds / UdonSharp 等)。这些是大文件,但因为它们被 vpm-manifest.json 锁死了版本,可以选择忽略:另一台机器拉下项目后用 VCC Open Project 会自动按 manifest 还原。社区两种做法都有:

  • 进版本控制(保险):拉项目就能直接打开,不依赖 VCC 网络。
  • 不进版本控制(仓库小):只提交 manifest,每次新机器打开靠 VCC 重新拉。

第一种(进版本控制)适合多数个人项目。手动添加 .gitignore 规则把第二种排除掉的话,可以加:

/Packages/com.vrchat.
# 选择性:让 VCC 在新机器上重新解包 SDK
# /Packages/com.vrchat.*.meta

UdonSharp 自动生成产物:UdonSharp 编译时会在 Assets/Resources/SerializedUdonPrograms/ 下生成一堆 .asset 文件,这些是 Udon 字节码缓存,每次脚本变化时重新生成。它们可以被 ignore(上面那份 .gitignore 模板已经包含),但要小心 .asset 这个后缀也用于其它合法 ScriptableObject。如果项目里没有别的 .asset 文件冲突,按上面那份模板照写就行。

本地 git init 解决「能回退到上周」的问题。但本地仓库只在这台机器上,硬盘坏了就一起没。远端备份是另一件事。

VRChat 个人项目里常用的一种远端方案:

方案上手难度隐私成本
GitHub Private 仓库私密(需 GitHub 账号)免费(个人无限私库),LFS 超 1 GB 要付费
Gitee Private 仓库私密免费(中国大陆访问稳定),文件 100 MB 上限
GitLab Private 仓库私密免费(含较多 LFS 配额)
自建 Gitea完全私密免费 + 服务器成本

GitHub 仓库设为 Private,仓库内容只有自己(和被邀请的合作者)能看,世界源码不会被外人看到。

新仓库建好后:

Terminal window
git remote add origin git@github.com:你的账号/你的世界.git
git branch -M main
git push -u origin main

之后每写完一段:

Terminal window
git add .
git commit -m "feat: 加了第二盏灯的同步逻辑"
git push

push 后远端就有了一份。本地硬盘坏了用别的机器 git clone 拉回来,从最近一次 push 那个时间点继续。

版本控制是「能回退到任何过去版本」。但它不是万能保险。几种值得叠加的备份手段:

云盘整目录定期同步:OneDrive / 坚果云 / Google Drive 把项目根目录设为同步目录(注意 Library/ 这种动态目录会让云盘频繁同步,可以单独排除)。云盘的好处是版本之外还有「整盘镜像」,万一仓库损坏也能恢复。

Unity 内置的 Asset Database 备份:Unity 启动时会自检 Library/ 一致性。损坏时尝试 Reimport All(菜单 AssetsReimport All)能修复多数情况。

外部硬盘月度全量:每月一次把整个项目(包括 Library/)打包到外接硬盘。频率不高但能扛住灾难性事故。

重要里程碑打 taggit tag v0.1 / git tag publish-1.0,让关键版本能直接通过名字而不是 hash 找回来。

Terminal window
git tag publish-1.0 -m "首次公开发布版本"
git push origin publish-1.0

世界发布前打一个 tag,发布后再做大改时若发现新版本翻车,能直接 git checkout publish-1.0 回到那个稳定点重新发。

挑一个或几个练手:

  • 当前项目里检查 Editor 设置里 Asset Serialization 是不是 Force TextVersion Control 是不是 Visible Meta Files。两项都对了之后再 git init,第一次 commit 之前看 git status 是不是只有 Assets/ Packages/ ProjectSettings/ 三个目录。
  • 复制一份当前世界场景(Project 面板 Ctrl+D),命名 MyWorld_Backup.unity,打开新场景随手挪一两个对象、提交。再回到原场景挪几个对象、提交。然后试着用 git merge 把两个分支合一下,看 UnityYAMLMerge 能不能自动处理。
  • 把当前项目 git push 到 GitHub Private 仓库,到另一台机器(或者新建一个临时目录)git clone 下来,用 VCC 打开看场景能不能正常加载。这一步走通了,跨机器协作的基础就有了。

走完之后这个项目就具备了「能回退、能跨机器、能多人合作」的工程基础。