Unity项目GitHub托管实战:.meta、Git LFS与场景冲突
Unity 项目挂到 GitHub 上托管听上去就是把文件夹往仓库里一推的事真动手的人都知道这活儿坑有多深。我自己第一次把工程推上去git status刷出来两万多个待提交文件仓库体积十分钟内涨到 3 个 G队友 clone 下来打开工程贴图全变成粉色预制体引用集体丢失。后来折腾了几年从个人小 Demo 到七八个人同时开工的中型项目都踩过一遍才摸清一套相对稳妥的做法。这篇东西讲的就是 Unity 工程交给 Git 托管这件事本身哪些文件该进版本库、.meta文件为什么不能丢、二进制资源怎么处理、场景冲突怎么救、自动构建怎么配。内容偏实操适合刚把工程从本地搬到远程仓库的独立开发者也适合团队里负责搭基础设施的那位。不管你用的是哪种 Git 托管服务规则是通用的下面统一用 GitHub 举例。1. Unity 仓库失控的根源源码与产物的边界在哪Unity 的工程目录结构和普通代码项目差别很大。一个纯代码项目里几乎所有文件都是人手写的改一行 diff 就一行。Unity 不是这样它的目录里混着三类完全不同的东西你手写的 C# 脚本和配置、引擎自动生成的各种缓存、以及美术同学丢进来的二进制资源。这三类东西的版本控制策略完全相反混在一起提交仓库必然失控。搞清楚这条边界后面所有的忽略规则、LFS 规则、合并策略才有落脚点。1.1 第一次git status就冒出两万个文件是怎么回事新建的 Unity 工程打开一次编辑器之后根目录下会多出Library、Temp、Logs、obj、UserSettings这几个文件夹。Library是重点它存放的是引擎对资源的导入结果贴图压缩后的格式、模型的网格数据、脚本编译出来的程序集、着色器变体、资源依赖索引全在里面。它的体积通常是Assets目录的好几倍一个中等项目轻松几十个 G。关键在于Library是可再生的。它由Assets里的原始资源和ProjectSettings里的导入设置共同推导出来删掉之后重新打开工程Unity 会花几分钟到几十分钟重新生成一份。这份东西对版本控制毫无价值因为它是产物不是输入。更麻烦的是它由引擎版本、平台设置、机器环境共同决定A 同学电脑上的Library和 B 同学的根本不该一样强行同步反而会引发一堆莫名其妙的导入错误。同理Temp是编译和构建的临时目录obj是 C# 编译中间产物Logs是日志UserSettings存的是你这台机器上的编辑器布局、上次打开的场景、面板位置。这些全都不该进仓库。UserSettings尤其容易被误提交因为它里面有个Layouts文件夹看起来像是团队共享的界面配置实际上每个人习惯不同提交上去只会天天冲突。判断一个目录该不该提交问自己一句删掉它重新打开工程能不能自动恢复能恢复的一律忽略。1.2.meta文件不是垃圾它是整个工程的引用索引这是我见过最多人栽跟头的地方。Assets目录下每个资源旁边都有一个同名的.meta文件比如Player.prefab旁边有Player.prefab.meta。很多人第一眼看到觉得是冗余垃圾顺手加进忽略列表结果就是灾难开始。.meta里存的是一串 GUID 和这个资源的导入设置。Unity 内部所有的引用都不是按路径记的而是按 GUID 记的。场景里一个物体引用了一个材质存的是那个材质的 GUID预制体里挂了一个脚本存的是脚本的 GUID。只要.meta在你把资源从Assets/A挪到Assets/B/C所有引用照样有效因为 GUID 没变。删掉.meta重新导入Unity 会生成一个新的 GUID全工程所有指向旧 GUID 的引用瞬间变成Missing界面上就是粉色材质、空脚本槽。所以规则很硬.meta必须和资源一起提交一个都不能少。.gitignore里那条!/[Aa]ssets/**/*.meta的感叹号是反选作用是在整体忽略某些资源的同时保证Assets下的.meta不被误伤。这个细节很多人抄模板时根本没注意。还有一种情况更隐蔽多人协作时A 同学在本地新建了一个文件夹并放了资源git add的时候只加了资源文件没加.meta因为.meta在文件夹里默认被折叠了没看见。推上去之后B 同学拉下来Unity 给这个资源生成新 GUIDA 同学本地是老 GUID两边一合并引用全断。表现就是我这里好好的他那里全是粉色。1.3 二进制资源撞上 Git 的增量存储模型Git 最初是为纯文本设计的。它存文件的方式是存整个文件快照但压缩时对文本极其高效——两个版本的 C# 脚本只差一行压缩后几乎不占额外空间。二进制文件没这个待遇一张 4K 的 PSD 改了一个图层在 Git 眼里就是两个完全不同的 800MB 对象各存一份。改十次就是 8 个 G。Unity 项目里这种文件遍地都是.psd、.png、.fbx、.wav、.mp3、.tga、.exr、.blend。一个美术同学正常迭代一周仓库能涨几个 G然后就到了推送超时、clone 半小时的地步。解法是 Git LFSLarge File Storage。它的原理不复杂大文件本身不进 Git 的对象库而是上传到单独的存储区仓库里只留一个几百字节的文本指针文件内容大概是版本号、文件哈希、大小。检出的时候 Git 根据指针自动把真文件拉下来。这样仓库主体保持轻量克隆速度不受影响。LFS 有几个必须提前知道的限制。第一它不是免费的午餐各家托管服务对 LFS 流量和存储都有配额超了要么限速要么付费。第二LFS 只对配置之后新增的提交生效如果大文件已经进了历史光加.gitattributes没用得重写历史。第三LFS 指针文件本质是文本冲突时 Git 不能自动合并只能选一边。这三点后面都会展开讲。搞清楚这三层——产物该忽略、.meta必须留、二进制要走 LFS——Unity 仓库的地基才算打对。2. 开工前先定规矩忽略表与属性表的落地写法很多人的做法是先把工程推上去出问题了再补.gitignore。这个顺序是错的。.gitignore只对尚未被跟踪的文件生效一旦Library已经被提交过后面再加忽略规则Git 依然会继续跟踪它们你得手动git rm -r --cached清一遍。所以建仓库的第一件事不是git init而是把规则文件写好。2.1 用白名单思维反推忽略表写.gitignore的时候新手习惯一条条列不要什么写着写着总有漏网之鱼。更稳的思路是反过来先明确要提交的东西只有四类其余全部忽略。提交内容作用说明Assets/全部游戏资源和脚本含每个资源的.meta一个都不能少ProjectSettings/工程级配置标签层、物理、图形、输入、包管理设置Packages/manifest.json依赖包清单声明用了哪些包及版本Packages/packages-lock.json依赖锁定保证所有人装到完全相同的版本四类之外的东西除非有明确理由一律不进仓库。这套白名单思路的好处是就算以后 Unity 新增了某个临时目录默认它也是被忽略的不会因为规则没更新而误提交。Packages/packages-lock.json这条值得单独说。它记录了每个包解析后的精确版本和来源类似package-lock.json。没有它manifest.json里写的com.unity.textmeshpro: 3.0.6在不同机器上可能解析出不同的间接依赖导致我这儿能编译他那儿报错。这个文件一定要提交而且尽量别手动改。2.2 逐行拆解忽略表里那些容易被误删的条目下面这份是实践中最常用的一份我按块拆开讲为什么# ---------- 引擎生成的目录 ---------- [Ll]ibrary/ [Tt]emp/ [Oo]bj/ [Bb]uild/ [Bb]uilds/ [Ll]ogs/ [Uu]ser[Ss]ettings/ [Mm]emoryCaptures/ [Rr]ecordings/ # ---------- 自动生成的工程文件 ---------- *.csproj *.unityproj *.sln *.suo *.user *.userprefs *.pidb *.booproj *.svd *.pdb *.mdb *.opendb *.VC.db # ---------- 崩溃与构建产物 ---------- sysinfo.txt *.apk *.aab *.unitypackage *.app # ---------- 关键反选 ---------- !/[Aa]ssets/**/*.meta[Ll]ibrary/这个写法是大小写兼容。Windows 上 Unity 生成的是Library某些 Linux 环境下可能是library方括号匹配能同时覆盖。Build/和Builds/都要写因为不同人导出时习惯不同。*.csproj和*.sln这一组很多人不理解为什么要忽略明明是工程文件。原因是 Unity 会根据Assets下的脚本自动重新生成它们而且里面包含大量绝对路径和包引用。两个人如果 IDE 不同、装的插件不同这些文件天天冲突且冲突了也没法手工合并。忽略掉让 Unity 每次自己生成反而干净。*.apk、*.aab、*.unitypackage、*.app这些是构建产物体积巨大且可以随时重新打出来。特别是.unitypackage有人喜欢把第三方资源包整个提交一个包几百兆非常划不来——导进来之后的东西本来就进Assets了原始包没有保留价值。那条反选!/[Aa]ssets/**/*.meta是保命的。它的含义是即使上面的规则命中了某个文件只要它在Assets下且是.meta结尾就不要忽略。安全网性质的一条。还有一类需要视情况处理如果你用了 AddressablesAssets/AddressableAssetsData下会生成.bin缓存文件这些是可重建的可以忽略但AddressableAssetsData里的.asset配置必须提交。类似地Assets/StreamingAssets下的内容如果是构建时拷贝进去的注意别把中间产物一起提交了。2.3.gitattributesLFS 追踪和合并驱动都挂在这.gitignore决定什么不进仓库.gitattributes决定进了仓库的文件怎么被对待。Unity 项目里它承担两件事。第一件是 LFS 追踪。写法有两种风格按扩展名和按目录。按扩展名更直观# ---------- 文本规范化 ---------- * textauto # ---------- 二进制资源走 LFS ---------- *.psd filterlfs difflfs mergelfs -text *.png filterlfs difflfs mergelfs -text *.jpg filterlfs difflfs mergelfs -text *.tga filterlfs difflfs mergelfs -text *.exr filterlfs difflfs mergelfs -text *.fbx filterlfs difflfs mergelfs -text *.blend filterlfs difflfs mergelfs -text *.wav filterlfs difflfs mergelfs -text *.mp3 filterlfs difflfs mergelfs -text *.mp4 filterlfs difflfs mergelfs -text *.dll filterlfs difflfs mergelfs -text # ---------- Unity YAML 走智能合并 ---------- *.unity mergeunityyamlmerge eollf *.prefab mergeunityyamlmerge eollf *.asset mergeunityyamlmerge eollf *.mat mergeunityyamlmerge eollf *.anim mergeunityyamlmerge eollf *.controller mergeunityyamlmerge eollf *.meta mergeunityyamlmerge eollf-text这个标记容易被忽略但很重要它告诉 Git 不要对这个文件做换行符转换和文本 diff避免把二进制内容当文本处理导致损坏。.dll走 LFS 是个取舍插件 dll 通常几百 K 到几 M如果插件很少可以不追如果项目依赖大量第三方 SDK 建议追。第二件是给 Unity 的 YAML 文件绑定合并驱动unityyamlmerge这就是第 4 节要讲的智能合并先在这里埋好伏笔。规则文件写完之后务必用git check-ignore -v 路径验一下某个具体文件到底被哪条规则命中。这个命令排查规则冲突特别有效比肉眼读几十行规则快得多。2.4 序列化模式Force Text 必须在第一次提交之前打开这一步经常被跳过但它决定了整个团队以后处理冲突的难度上限。打开Edit Project Settings Editor找到Asset Serialization把Mode设成Force Text。新版 Unity 默认就是这个但老工程升级上来的不一定。这个设置的效果是场景、预制体、材质、动画控制器这些资源存成可读的 YAML 文本而不是二进制。差别有多大二进制模式下两个人同时改一个场景冲突了只能选我的或他的另一边的改动全丢没有第三条路。文本模式下Unity 提供了UnityYAMLMerge工具能按对象和字段粒度做三方合并冲突范围能从整个文件缩小到某个 GameObject 的某个属性。同一个面板里还有个Version Control选项设置成Visible Meta Files新版可能叫Visible Meta Files或已经是默认。它的作用是让.meta文件在文件系统里可见否则 Unity 会把它藏在内部Git 根本拿不到。这两项设置改完要提交ProjectSettings/EditorSettings.asset让整个团队统一。如果有人本地是二进制模式他提交的场景会把文本格式覆盖回去导致其他人下次打开发现格式变了一串无意义的 diff。配置好这两项地基就算彻底完工可以动手建仓库了。3. 半年老工程怎么搬进仓库一次完整的迁移实录新项目从空文件夹开始是最省事的。但现实中更常见的情况是工程已经做了大半年本地文件夹里堆着Library、Builds、各种备份文件现在才想起来要做版本控制。这种情况坑更多得按步骤来。3.1 备份、清空、断网验证三件套动手之前第一件事整个工程文件夹复制一份到别的盘。这不是客套话迁移过程中一不小心git clean -fdx就能把没提交的资源全删了我在一个项目上亲眼见过美术一下午的工作量蒸发。备份完按顺序做这几步关闭 Unity 编辑器。开着的时候Library里的文件被占用Git 操作可能失败而且编辑器在后台持续写入你根本不知道它什么时候改了东西。删掉Library、Temp、obj、Logs、UserSettings、Builds。删完确认工程目录下只剩Assets、ProjectSettings、Packages三个核心文件夹加上可能存在的.vs、.idea之类的 IDE 目录。把第 2 节写好的.gitignore和.gitattributes放到工程根目录。git init然后git add .先不要 commit。执行git status看暂存区里到底有多少文件、总体积多大。第 5 步是关键检查点。正常情况下一个中小项目的首次提交应该在几百 M 到 1 G 之间。如果显示好几千个文件、体积几个 G说明有东西没被忽略掉回头检查规则。可以用这条命令看暂存区各目录的体积分布git ls-files -s | awk {print $4} | xargs -I{} du -k {} 2/dev/null | sort -rn | head -30它会列出暂存区里体积最大的 30 个文件一眼就能看出是哪类资源在撑体积。如果全是大图说明 LFS 没配好或者配置了但没生效。最后一步验证git commit之后把整个工程文件夹改名不是删除然后从远程 clone 一份下来打开 Unity看是否报错、资源是否完整。这个断网验证能提前暴露所有.meta缺失、LFS 指针没拉全的问题比等到队友拉下来才发现要好得多。3.2 历史里已经塞进Library的补救手段更麻烦的情况是仓库已经用了一段时间Library早就被提交过好几版仓库体积已经失控。这时候加.gitignore是没用的因为 Git 依然跟踪着这些文件。第一步是让 Git 停止跟踪但保留本地文件git rm -r --cached Library Temp obj Logs UserSettings git commit -m chore: 停止跟踪引擎生成目录这样以后的提交干净了但历史里的那些大对象还在。.git文件夹依然臃肿clone 依然要拉几个 G。要彻底清理得重写历史。工具上官方推荐的git filter-repo比老的filter-branch快几十倍也安全得多# 安装需要 Python 环境 pip install git-filter-repo # 从全部历史中移除指定目录 git filter-repo --path Library --path Temp --path obj --invert-paths--invert-paths的含义是删除匹配到的路径配合多个--path就能一次性清掉多个目录。执行完之后本地历史被重写所有 commit hash 都会变必须用git push --force覆盖远程并且通知所有协作者重新 clone不能直接pull。这一步在多人项目里要挑个大家都不干活的时间段做不然有人推了旧历史上去一切白费。重写历史前建议先打个标签备份git tag backup-before-rewrite万一搞砸了还能从 reflog 或者备份标签找回来。3.3 把已经进历史的大文件迁到 LFSLFS 的坑在于它只对配置之后的提交生效。.gitattributes写好、git lfs track执行完之前提交的.psd依然是普通 Git 对象依然占着仓库体积。git filter-repo也能处理这个它有一个专门的--to-vcs或者配合git lfs migrate使用更直接# 把历史中所有匹配的文件转成 LFS 对象 git lfs migrate import --include*.psd,*.png,*.fbx,*.wav --everything--everything表示遍历所有分支和标签。执行完会重写这些文件的存储方式仓库里的普通对象变成指针真文件迁到 LFS 存储。之后再git lfs push --all origin把 LFS 对象推上去。这一步的风险点是如果团队里有人的本地仓库没同步这次重写他推上来的还是老格式会造成指针和实体混杂检出时报错smudge filter lfs failed。所以迁移前后一定要在群里明确通知所有人删掉本地仓库重新 clone 是最省心的做法。3.4 换台机器拉下来打不开通常是这几处没对上迁移完成后新同事 clone 下来打不开工程报错信息往往很含糊。按这个顺序排查命中率很高现象可能原因处理方式贴图全粉、脚本槽为空.meta没提交或被误忽略git ls-files拉下来的大文件只有几百字节LFS 没装或没拉取git lfs install后git lfs pull打开就提示版本不一致团队 Unity 版本不统一ProjectSettings/ProjectVersion.txt提交约定统一版本缺包、报package not foundpackages-lock.json没提交补提交并统一manifest.json输入系统/渲染管线报错ProjectSettings某个资产没提交检查ProjectSettings目录是否完整第一行那条命令特别实用能快速比对。正常情况Assets下的.meta数量和需要它的资源数量应当匹配每个文件夹也各有一个.meta。差得越多说明漏提交越严重。4. 场景与预制体冲突从满地狼藉到可控多人协作里最让人头疼的不是代码冲突代码冲突至少还能一行行看。场景冲突是真的会让人崩溃——两个人在同一个.unity文件里各改各的合并之后场景里冒出一堆重复物体、组件丢失、光照参数错乱排查起来比重做还慢。4.1 一个.unity文件的冲突为什么能废掉半天根源在于场景文件的组织方式。一个场景是单个 YAML 文件里面按--- !u!类型ID 对象ID分段每段描述一个对象。文件里的对象 ID 是本地生成的两个人各自新增物体ID 极可能撞上。Git 的三方合并只看文本行不懂 YAML 语义它会把两边的改动硬拼在一起拼出来的结果语法可能是合法的但逻辑完全错乱。更隐蔽的是删除 新增的交叉情况。A 删了一个物体B 在同位置改了这个物体的参数。文本合并的结果是物体既被删又被改最终表现可能是引用指向一个不存在的对象Unity 打开时报Missing Reference但你根本定位不到是哪个。所以对场景文件的处理原则很简单尽量避免冲突而不是解决了冲突。这听起来像废话但确实是最有效的手段。4.2UnityYAMLMerge的配置和它不生效的常见原因.gitattributes里那一行*.unity mergeunityyamlmerge只是声明这个类型的文件用名为unityyamlmerge的驱动来合并驱动本身得单独配置。打开全局或仓库级 Git 配置加上git config merge.unityyamlmerge.name Unity SmartMerge git config merge.unityyamlmerge.driver \Unity安装路径/Editor/Data/Tools/UnityYAMLMerge.exe\ merge -p %O %B %A %A路径按操作系统不同WindowsC:/Program Files/Unity/Hub/Editor/2022.3.xx/Editor/Data/Tools/UnityYAMLMerge.exemacOS/Applications/Unity/Hub/Editor/2022.3.xx/Unity.app/Contents/Tools/UnityYAMLMergeLinuxUnity安装目录/Editor/Data/Tools/UnityYAMLMerge四个参数的顺序%O %B %A %A分别代表 base共同祖先、ours当前分支、theirs被合并分支。注意最后写的是%A %A这是官方推荐写法表示合并结果输出到我们的文件里。配置完用一次真实冲突验证随便开两个分支各改一个场景里的不同物体然后 merge。如果弹出Automatic merge failed并且场景文件里出现了标记说明驱动没生效。常见原因有这么几个.gitattributes里写的是mergeunityyamlmerge但git config里的名字拼错或者大小写不一致路径里有空格却没加引号Git 解析时把路径截断了Unity 版本升级后旧路径失效2021.3.x换成了2022.3.x但配置没改用了 Git GUI 客户端客户端自己的合并工具覆盖了 Git 配置最后一条特别容易忽略。某些图形客户端有自己的外部合并工具设置优先级高于.gitattributes配了半天不生效就是因为客户端在中间拦截了。排查时先用命令行git merge试命令行生效了再调客户端。即便 SmartMerge 正常工作它也不是万能的。它擅长处理两边改了不同物体的不同属性对于两边改了同一个物体的同一个字段依然会冲突对于数组顺序变化、List 增删的处理也不总是正确。所以配好工具只是降低冲突频率不能指望它兜底。4.3 LFS 文件锁把覆盖别人的改动掐死在提交前美术资源有个特点PSD 文件没法做有意义的 diff。两个人都改同一张原画只能选一边。但比起选哪边更常见的问题是根本不知道对方也在改等推上去才发现覆盖了别人两小时的工作。LFS 提供了文件锁机制。开启方式是在.gitattributes里给需要保护的扩展名加上lockable*.psd filterlfs difflfs mergelfs -text lockable *.blend filterlfs difflfs mergelfs -text lockable然后git lfs lock 文件路径就能锁住这个文件。锁定状态会同步到远程其他人推同一文件时会被拒绝提示文件被锁。改完git lfs unlock释放。实际操作中要注意两点。一是锁是基于路径的改了文件名锁就失效了所以文件名尽量定了就别动。二是团队所有人必须都安装 LFS 并且git lfs install过否则锁机制形同虚设——没装 LFS 的客户端根本不认识这些指令。这套机制配合git lfs locks命令可以列出当前所有锁谁锁了什么、什么时候锁的一目了然。我们后来的做法是每天早上开工前跑一次确认自己要动的原画没被别人占着。4.4 分支模型和提交粒度比工具更影响体验工具再好也架不住所有人天天在同一个主分支上推。Unity 项目的特点是改一个功能经常要动场景、动预制体、动配置天然容易冲突。把分支切得合理一些冲突量能降一大截。小团队五到八人我推荐的做法是main分支保持随时可出包的状态只接受通过 PR 合并的改动每个人按功能开短分支命名带人名或功能名比如feature/inventory-system分支存活时间控制在三天以内超过就强制拆小场景文件默认只有一个人负责其他人需要调整就在预制体上改运行时再拼进场景最后一条是经验之谈。把一个关卡场景拆成多个预制体每个人只改自己负责的那块预制体冲突概率能降一个数量级。场景本身变成纯粹的装配清单改动极少。提交粒度上最忌讳的是攒一天然后一次提交。一个大提交里混着脚本改动、场景改动、配置改动出了问题根本没法回滚只能整体回退。合理的粒度是每完成一个小功能点提交一次提交信息写清楚动了哪块。Unity 有个小技巧提交前在编辑器里跑一次File Save Project确保所有改动已经落盘不然可能出现编辑器内存里有改动但没写到文件的情况。5. 自动出包把手动构建那二十分钟省掉工程托管好之后接下来最自然的一步就是自动构建。手动出包的问题是打开 Unity 等编译、切平台、点构建、等半小时、发现报错、重来。这套流程一天走三遍时间全耗在等待上。配好 CI 之后推代码就自动出包报错也能第一时间看到。5.1 许可证和凭据的准备GitHub Actions 的 runner 是干净的虚拟环境里面没装 Unity也没有你的许可证。所以第一步是把构建用的环境准备好。社区里有一套成熟的 Unity 构建 Action常被称为 GameCI它支持从加密的许可证文件激活。流程大致是这样本地激活一次 Unity 许可证拿到许可证文件.ulf把文件内容转成 base64base64 -w 0 Unity_v2022.x.ulf license.txt把这一长串字符存进仓库的Settings Secrets and variables Actions命名比如UNITY_LICENSE如果有 Personal 版许可证另外还需要UNITY_EMAIL、UNITY_PASSWORD两个 secret。这套凭据属于高度敏感信息务必确认仓库是私有的公开仓库里放这种东西等于把账号交出去。5.2 工作流文件逐段读在仓库根目录建.github/workflows/build.yml一份可用的最小配置大概长这样name: Build Unity Project on: push: branches: [ main ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: 拉取代码含 LFS 文件 uses: actions/checkoutv4 with: lfs: true fetch-depth: 0 - name: 恢复 Library 缓存 uses: actions/cachev4 with: path: Library key: Library-${{ hashFiles(Assets/**, Packages/**, ProjectSettings/**) }} restore-keys: | Library- - name: 执行构建 uses: game-ci/unity-builderv4 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: targetPlatform: WebGL unityVersion: auto - name: 上传构建产物 uses: actions/upload-artifactv4 with: name: build-webgl path: build几个参数值得展开。lfs: true必须有否则检出的是 LFS 指针文件Unity 打开时所有贴图都是坏的。fetch-depth: 0拉取完整历史某些版本号生成逻辑会用到 git 提交次数。targetPlatform支持StandaloneWindows64、Android、iOS、WebGL等一次配多个平台就是多开几个 job注意并行 job 会同时消耗构建配额。unityVersion: auto让 Action 自动读取ProjectSettings/ProjectVersion.txt里的版本号去拉对应编辑器镜像。这个镜像有几个 G第一次跑会慢之后有缓存就快了。5.3 缓存Library的两面性缓存Library是提速的关键。没缓存的话每次构建都要全量导入资源一个中等项目二十分钟起步。有缓存能压到三五分钟。但缓存也有坑。key用的是Assets、Packages、ProjectSettings三处内容的哈希只要这三处有任何变化key 就变了缓存失效重新全量导入。这是保守策略好处是不会用到过期缓存出错。更激进的策略是用固定 key 加restore-keys兜底能命中更久之前的缓存。代价是可能拿到跟当前代码不匹配的Library偶尔出现引用了一个已经被删掉的东西这类诡异报错。我的经验是项目初期用保守策略等构建稳定了再考虑放宽。还有一个细节Library缓存体积可能到几个 GGitHub 的缓存空间有上限超了会淘汰最旧的缓存。所以别把Builds之类的东西也塞进缓存路径。5.4 构建红了之后先查这几样CI 挂了的时候日志往往几百行直接看容易懵。按这个顺序查八成的失败都能定位许可证报错日志里出现No valid Unity license或License activation failed。通常是 secret 名字拼错、许可证过期、或者 base64 转换时多了换行。重新生成一遍 secret 基本能解决。LFS 文件拉取失败日志里出现smudge filter lfs failed或者This repository is over its data quota。前者检查 checkout 步骤有没有lfs: true后者是 LFS 流量用完了得清理历史或者升级配额。包解析失败报Cannot resolve package。检查packages-lock.json是否提交完整以及 CI 环境能不能访问包源。有些包依赖私有源需要额外配凭据。平台模块缺失报Build target not supported。构建 Linux 上出 Android 包需要预装 Android 模块。GameCI 的镜像默认带了一些但不是全都有需要时用with: androidTargetSdkVersion之类的参数指定或者换用带完整模块的镜像。构建超时一般是因为没缓上全量导入加上下载编辑器镜像超过默认时限。加大 timeout 或者优化缓存策略。构建产物那一步用upload-artifact存起来可以直接从 Actions 页面下载。如果要做发布可以在后面接一个自动创建 release 并附上产物的步骤这部分按各自需要扩展就行。6. 最后聊几条不太好写进文档的经验工程托管这件事规则层面其实不复杂难的是让整个团队保持一致。我遇到过的绝大多数事故都不是技术方案有问题而是有人绕过了方案临时手动改了一下.gitignore、图省事git add -A把所有东西都推上去、编辑器里忘了存盘就提交。有个做法我觉得挺管用在仓库根目录放一份CONTRIBUTING.md写清楚三条硬规矩——提交前跑一次git status确认没有Library新加资源确认.meta一起进暂存区动ProjectSettings的改动单独提一个 commit。文字不用多够明确就行。后来发现新人入职读三分钟能省下后面无数次救火。还有个检测手段可以在 CI 里加一个前置 job检查本次提交是否包含了不该出现的路径比如Library/、Temp/、*.csproj。命中就直接 fail比事后清理历史省事得多。规则写起来也就几行if git diff --name-only HEAD~1 HEAD | grep -E ^(Library|Temp|obj|Logs)/; then echo 提交中包含引擎生成目录请检查 .gitignore exit 1 fi这东西的价值在于它把事后补救变成了当场拦截。人工 review 靠不住机器检查才是真的靠得住。至于仓库规模的长期维护我自己的习惯是每个月看一眼仓库体积和 LFS 用量。如果 LFS 用量涨得异常快通常意味着有人在提交一些本该忽略的中间文件比如渲染出来的序列帧、录屏素材、或者从商店下的原始资源包。早点发现清理成本就低。最后补一句关于 Unity 版本的事。ProjectSettings/ProjectVersion.txt一定要提交并且团队约定统一版本别出现你用 2021 我用 2022 大家互相覆盖的局面。版本不一致导致的资源重新序列化是那种看起来默默无闻、实际天天在制造无意义 diff 的隐形杀手等你发现的时候历史里已经堆了几百个只改了一行版本号的提交了。