尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity资源离线化:UPM缓存备份与本地包还原实战

Unity资源离线化:UPM缓存备份与本地包还原实战 1. 先搞清楚你买的资产到底存在哪几个地方听到资源访问被切断这类消息的时候大部分人的第一反应是去翻订单记录看看自己买过哪些资产。这个动作方向是反的。订单记录只是凭证它证明你曾经拥有过下载权但真正决定你的工程明天还能不能打开的东西全都躺在你本机的某几个目录里而不是在云端。我见过太多团队翻车在这一步以为资产买过了就一直在结果发现新机器上重新拉工程时Asset Store 的下载入口已经点不动了Unity 编辑器里 Package Manager 那几个第三方包全部报Package [com.xxx.yyy] cannot be found工程直接起不来。这不是危言耸听这是离线化没做好时的必然结果。不管你听到的倒计时是 28 天还是 30 天真正属于你的倒计时起点是你第一次发现下载按钮变灰的那一天。从那一刻开始你能动用的资源就只有本机磁盘上的存量。所以这篇东西不讲情绪只讲怎么在窗口期里把存量完整落到硬盘上并且验证它真的还能还原回来。1.1 资产的四种存在形态别只备份其中一种Unity 生态里的资产其实分四类很多人的备份只做了其中一类然后在还原时才明白漏了哪一类第一类是 UPM 包Package Manager 包包括官方包和第三方通过 UPM 分发的包。它们通常缓存在两个位置工程内的Library/PackageCache以及全局缓存的packages目录。这一类是重新装一遍就行的重灾区因为很多人默认它随时能下回来。第二类是.unitypackage形式的传统资产包也就是 Asset Store 上大多数模型、贴图、插件素材的形态。它们下载后会落在本机的下载缓存目录里而不是自动进入你的工程。第三类是已经导入工程、躺在Assets/目录下被改动过的资产内容。这一类最容易被忽略因为它在工程里看起来已经是我的了但一旦脱离工程目录单独拷出去材质引用、Prefab 关联、Shader 依赖很容易断。第四类是账号与许可证层包括编辑器激活文件、账号绑定的下载权限凭证、私有 registry 的访问配置。这一层平时完全不可见出事的时候却决定你能不能启动编辑器。把这四类分开列清楚是因为它们的备份手段完全不同。混在一起想最后一定是某一类漏掉。1.2 访问中断时最先崩的是哪一环按我的实际经验崩溃顺序几乎固定先崩的是新环境搭建也就是团队里新来的同事或者你换了台机器发现拉不下来包接着崩的是CI 构建机因为构建机通常不保存人肉下载的缓存它会直接去远端拉最后崩的才是已有工程本身因为已经缓存在Library里的东西还能撑一阵子直到你手贱点了Clear Cache或者清了Library目录。注意Library目录是可再生的但它再生的前提是包能下回来。一旦远端不可达Library/PackageCache就是唯一的救命稻草千万不要在没备份的情况下去清它。所以备份的优先级顺序应该是全局包缓存 → 工程内 PackageCache → 传统.unitypackage下载目录 → 许可证与配置。这个顺序不是我拍脑袋定的而是按丢失后重建难度排的越靠前的越难重建。2. 备份方案怎么设计三层落盘模型直接开始拷文件的结果通常是拷了一堆散乱目录半年后自己都看不懂哪个是最新版。我在几个项目上摸索下来的做法是分三层落盘每层职责明确互相不重叠。2.1 三层分别管什么第一层是包缓存层职责是原样保全。把全局包缓存目录和工程内PackageCache整目录拷走不做任何重命名、不做任何整理。这一层的价值在于它是最完整的包含所有历史版本还原时直接扔回原位即可。第二层是本地 UPM 包层职责是长期可维护。把关键资产从缓存里挑出来加上package.json改造成标准的本地 UPM 包。这一层是给人看的、给版本控制的可以直接进 Git。第三层是环境层职责是能开机。包含编辑器安装包本体保留对应版本的离线安装器、许可证文件、私有 registry 配置、以及一份manifest.json与packages-lock.json的快照。这三层的区别很关键第一层是死数据第二层是活数据第三层是运行条件。只有第一层你能恢复但没法维护只有第二层你能维护但漏掉了大量边角依赖只有第三层你连包都装不上。2.2 目录清单与校验哈希怎么定落盘之前先定一份清单。我的习惯是建一个UnityBackup根目录下面按层分UnityBackup/ ├── packages/ # 第一层全局包缓存原样拷贝 ├── localpkg/ # 第二层本地 UPM 包 ├── editor/ # 第三层编辑器离线安装器 许可证 ├── manifest-snapshot/ # 第三层各工程的 manifest 与 lock 文件快照 └── checksums/ # 校验哈希目录定好了接下来必须做校验。这一步九成的人会跳过然后在还原的时候才发现某个.tgz拷到一半断过、某个目录因为权限问题只拷了一半。校验不复杂就是给每个文件算 SHA256存一份 JSON 清单还原前跑一遍比对。脚本我在第 5 节给出直接能抄。提示校验清单一定要和备份数据分开放。放在同一个磁盘上磁盘挂了两个一起没。2.3 磁盘选型和冗余备份放在哪这个问题比想象中重要。用机械硬盘做大容量冷备是合理的但别买那种没有校验能力的杂牌硬盘盒——我遇到过一次拷贝过程中静默损坏文件大小对、内容错靠哈希才查出来。我的实际配置是一份在本地 NAS一份在移动固态关键项目再对整个备份目录做一次压缩包并算哈希扔到一个异地介质上。听着夸张但当你真的遇到需要从零重建一台构建机的时候你会庆幸多花了这半小时。另外包缓存目录里会有大量temp、.tmp之类的中间文件拷贝时排除掉能省不少空间也能避免把半成品状态一起冻进去。3. 实操第一步把 UPM 缓存完整落盘这一节是纯操作跟着走就行。3.1 找到真正的缓存目录Unity 的全局包缓存位置跟操作系统和版本都有关系下面这几个是最常见的找不到的话以官方文档和你机器上的实际路径为准操作系统全局包缓存常见位置Windows%LOCALAPPDATA%\Unity\cache\packagesmacOS~/Library/Unity/cache/packagesLinux~/.config/unity3d/cache/packages在这个目录下面你会看到按 registry 分文件夹的结构比如packages.unity.com下面就是一堆com.unity.xxx1.2.3这样的目录。每个目录里是该版本的完整解包内容有些还带.tgz原始包。这些全部都要拷。工程内的缓存位置是工程根目录/Library/PackageCache。注意一个坑在很多机器上这个目录里的子目录其实是符号链接指向全局缓存。直接拷贝时如果工具默认不解引用符号链接你会拷到一堆空目录或者快捷方式。3.2 拷贝命令与参数说明Windows 上用 robocopyrobocopy %LOCALAPPDATA%\Unity\cache\packages ^ D:\UnityBackup\packages ^ /E /COPY:DAT /R:2 /W:1 ^ /XD temp .tmp /NFL /NDL /NP /TEE ^ /LOG:D:\UnityBackup\copy-packages.log参数解释一下/E是包含所有子目录含空目录/COPY:DAT只拷数据、属性和时间戳不拷 ACL避免跨机器权限问题/R:2 /W:1是重试两次、每次等一秒防止卡在某个坏文件上无限重试/XD排除临时目录/NP关掉百分比让日志干净点。macOS 和 Linux 上用 rsyncrsync -a --delete-after \ --excludetemp --exclude*.tmp \ $HOME/Library/Unity/cache/packages/ \ /Volumes/Backup/UnityPackages/-a是归档模式会保留权限、时间、符号链接。这里有个选择要不要加-L解引用符号链接。我的建议是加因为目标是异地还原对方机器的链接关系大概率对不上解引用成真实文件更稳代价是占空间。3.3 拷贝完必须做的两件事第一件记录源目录的文件总数和总大小和目标目录比对。数量对不上就是漏了。第二件抽样打开几个package.json看是不是完整 JSON能解析说明文件没截断。我在一次跨盘拷贝里遇到过磁盘写入缓存导致最后几个文件是零字节的情况大小看着差不多实际打开是空的。所以抽样验证这一步别省。注意工程内的Library/PackageCache和全局缓存高度重叠如果空间紧张优先保全局缓存它覆盖更全。但如果是某个工程用了本地file:引用的包那个包不在全局缓存里必须单独处理。4. 实操第二步把 .unitypackage 转成本地 UPM 包这一步是整篇里最有价值的部分。很多人止步于我把.unitypackage存了一堆在硬盘上结果还原的时候要一个个手动双击导入几十个资产导入一整天还容易漏。4.1 为什么推荐 UPM 而不是散装 .unitypackage.unitypackage是一次性导入工具它没有版本概念没有依赖声明导入时机不受控同一个包导入两次会产生重复资源。而本地 UPM 包有明确的name、version、dependencies可以通过manifest.json声明式引用Unity 会自动处理加载顺序和依赖关系。更重要的是本地 UPM 包可以进 Git。一个Packages/目录加一个manifest.json整个团队的依赖就锁死了谁拉下来都是同一套东西。这在远端不可达的时候价值是断层式的。4.2 package.json 怎么写一个最小可用的本地包目录结构是com.yourstudio.localfoo/ ├── package.json ├── Runtime/ │ ├── Scripts/ │ └── Materials/ ├── Editor/ └── README.mdpackage.json内容示例{ name: com.yourstudio.localfoo, version: 1.0.0, displayName: Local Foo Snapshot, description: 第三方资产本地化快照来源于已购资产仅用于内部工程依赖锁定。, unity: 2021.3, dependencies: { com.unity.textmeshpro: 3.0.6 }, keywords: [local, snapshot, internal], author: { name: Your Team, email: devexample.com } }几个要点name必须是小写加点的反向域名格式不能有大写和空格version遵循语义化版本跟原资产的实际版本号对齐方便日后比对unity字段写你项目实际用的最低版本dependencies只写真正需要的写多了会引入不必要的外部拉取。4.3 manifest.json 里怎么引用本地路径在工程根目录的Packages/manifest.json里加上{ dependencies: { com.unity.cinemachine: 2.9.7, com.unity.textmeshpro: 3.0.6, com.yourstudio.localfoo: file:../LocalPackages/com.yourstudio.localfoo } }路径规则要注意file:后面的相对路径是相对于工程根目录也就是包含Packages和Assets的那一层来算的不是相对于Packages目录。上面这个写法意味着本地包放在工程的上一级的LocalPackages里。绝对路径也能用Windows 上写成file:D:/LocalPackages/com.yourstudio.localfoomacOS 上写file:/Users/you/LocalPackages/com.yourstudio.localfoo。但绝对路径没法进版本控制团队协作会踩坑只在临时调试时用。4.4 从工程里批量导出 .unitypackage如果你手上的资产还在工程里想一次性导出成.unitypackage存档可以在工程里放一个编辑器脚本using System; using System.IO; using UnityEditor; using UnityEngine; public static class BatchPackageExporter { [MenuItem(Tools/备份/批量导出选中目录)] public static void ExportSelected() { var guids Selection.assetGUIDs; if (guids null || guids.Length 0) { Debug.LogWarning(请先在 Project 窗口选中要导出的目录或文件。); return; } var projectRoot Path.GetDirectoryName(Application.dataPath); var outDir Path.Combine(projectRoot, ExportedPackages); Directory.CreateDirectory(outDir); foreach (var guid in guids) { var assetPath AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(assetPath)) continue; var name Path.GetFileName(assetPath.TrimEnd(/)); var stamp DateTime.Now.ToString(yyyyMMdd-HHmmss); var outPath Path.Combine(outDir, ${name}_{stamp}.unitypackage); AssetDatabase.ExportPackage( assetPath, outPath, ExportPackageOptions.Recurse | ExportPackageOptions.IncludeDependencies); Debug.Log($已导出{outPath}); } AssetDatabase.Refresh(); } }ExportPackageOptions.IncludeDependencies会把Assets/目录内的依赖一起打包注意它不包含UPM 包依赖。也就是说如果这个资产依赖某个 UPM 包导出出来的包导入到别的工程后依然会去拉那个 UPM 包。这正是为什么 4.2 那一步不能省。脚本放进Assets/Editor/下等编译完菜单栏就会出现入口。选中要导出的目录点一下几秒钟的事比一个个右键导出快太多。5. 实操第三步还原演练与回归验证备份做完不等于安全做一次真实的还原演练才算数。我的做法是找一台干净的机器或者把工程目录和缓存目录全部临时改名模拟什么都没有的状态然后走一遍完整还原流程。5.1 还原顺序顺序不能乱乱了会浪费大量时间排错安装对应版本的 Unity 编辑器用离线安装器别走在线安装。恢复许可证文件到系统对应位置确认编辑器能正常打开、能进设置界面。把备份的全局包缓存整目录拷回原位置。把LocalPackages目录放到工程上确认manifest.json里的file:路径指向正确。打开工程观察 Package Manager 是否有报错。打开一个关键场景检查材质、Shader、Prefab 引用是否完整。第 6 步是最容易出问题的。有些资产在导入时会生成中间资源比如 ScriptableObject 配置、Shader 变体集合这些如果在导出时没包含进来场景里会显示粉红材质。遇到这种情况不要慌通常是 Shader 没加载检查一下包内Shader目录是否存在以及是否被manifest.json正确引用。5.2 自动校验脚本还原之后跑一遍哈希比对确认文件和备份时完全一致import hashlib import json import pathlib import sys CHUNK 1 20 def sha256_of(path: pathlib.Path) - str: h hashlib.sha256() with path.open(rb) as f: for block in iter(lambda: f.read(CHUNK), b): h.update(block) return h.hexdigest() def scan(root: pathlib.Path) - dict: result {} for p in sorted(root.rglob(*)): if p.is_file() and not p.name.endswith(.tmp): rel p.relative_to(root).as_posix() result[rel] sha256_of(p) return result def make_manifest(root: str, out_file: str) - None: data scan(pathlib.Path(root)) with open(out_file, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f已写入 {len(data)} 条记录到 {out_file}) def verify(root: str, manifest_file: str) - int: with open(manifest_file, encodingutf-8) as f: old json.load(f) cur scan(pathlib.Path(root)) missing [k for k in old if k not in cur] changed [k for k in old if k in cur and old[k] ! cur[k]] extra [k for k in cur if k not in old] print(f缺失文件 {len(missing)} 个) print(f内容变更 {len(changed)} 个) print(f新增文件 {len(extra)} 个) for k in missing[:20]: print( MISSING:, k) for k in changed[:20]: print( CHANGED:, k) return 0 if not missing and not changed else 1 if __name__ __main__: if sys.argv[1] make: make_manifest(sys.argv[2], sys.argv[3]) else: sys.exit(verify(sys.argv[2], sys.argv[3]))用法就是两条命令备份时python hashcheck.py make D:/UnityBackup D:/UnityBackup/checksums/backup.json还原后python hashcheck.py verify D:/UnityBackup D:/UnityBackup/checksums/backup.json。返回码非零就说明有问题可以直接接进 CI 流水线。5.3 packages-lock.json 才是真正的锁Packages/manifest.json是你写的声明Packages/packages-lock.json是 Unity 解析出来的实际结果。后者记录了每个包的精确版本、来源、哈希、深度。锁文件一定要进版本控制而且在备份快照里单独存一份。这么做的原因是manifest.json里写com.unity.textmeshpro: 3.0.6看起来是锁死了但如果这个版本在某处不可达Unity 在解析时可能会做别的处理导致实际装上的东西和你以为的不一样。有了锁文件还原时的解析路径就完全确定。提示每次改完依赖并且确认工程跑得没问题就把manifest.json和packages-lock.json一起提交并且顺手同步到备份快照目录。这两个文件加起来通常不到 100KB成本极低收益极高。6. 常见问题与排查速查前面讲的是怎么做对这一节讲做错了之后怎么救。我把实际踩过的坑整理成了表格。6.1 典型报错与对策现象大概率原因处理方式Package [com.xxx] cannot be found包不在缓存也不在本地路径Unity 尝试远端拉取失败检查Library/PackageCache是否有该包检查manifest.json里file:路径是否写错打开工程后材质全粉红Shader 未加载或包未正确解析查看 Console 有无 Shader 错误确认包内 Shader 目录完整、依赖包已就位Package Manager 界面一直转圈编辑器尝试访问不可达的 registry断开不必要的 registry 配置或改为本地缓存优先减少网络超时本地包改了代码但工程没变本地包被 Unity 缓存进Library/PackageCache改本地包后需要 Unity 重新解析必要时删掉对应缓存目录再打开工程新同事拉工程后缺一半资源只提交了manifest.json没提交本地包目录把LocalPackages一并纳入版本控制或用子模块方式管理CI 构建失败但本机正常构建机没有包缓存把备份的缓存目录预置到构建机镜像里或让构建机走内网 registry拷贝后文件大小为 0拷贝中断或磁盘写入异常用哈希清单验证重拷问题文件6.2 我自己的避坑清单第一条永远不要在没备份的情况下去清Library。这个目录看起来是临时文件但它里面最关键的PackageCache是不可再生的。我见过有人为了排查一个编译错误清掉Library结果因为包下不回来工程两天没恢复。第二条本地 UPM 包的version字段不要随便改。改了就相当于一个新包所有引用它的工程都会触发重新解析。如果你只是想改里面的资源内容保持版本号不变直接改内容Unity 会重新导入。第三条不要用绝对路径做长期依赖。file:D:/xxx在自己机器上跑得好好的换台机器全废。除非是临时验证否则一律用相对于工程根目录的路径。第四条团队里要有一个人专门负责依赖基线。这个人负责维护LocalPackages目录、manifest.json、packages-lock.json这三样东西的一致性别人不要随意改。我参与过的一个项目就是因为三个人各自改了依赖最后合并出一堆冲突花了两天才理清。第五条定期做还原演练不是做完备份就结束。每季度找台机器跑一次从零还原能发现绝大多数隐患。演练失败的代价远小于真出事时才发现备份不可用的代价。7. 长期防御让第三方依赖不再成为单点风险临时应急解决的是这个月怎么办但只要你还在做长期项目就必须考虑结构性的问题。7.1 自建私有 registry把依赖收进内网本地file:引用适合小规模包多了以后管理成本会上升。这时候可以上一个私有 registry。常见的做法是在内网跑一个轻量的 npm 兼容 registry 服务然后把打包好的.tgz推上去。配置上在Packages/manifest.json里加{ scopedRegistries: [ { name: internal, url: http://10.0.0.20:4873, scopes: [com.yourstudio] } ], dependencies: { com.yourstudio.localfoo: 1.0.0 } }scopes表示只有com.yourstudio开头的包走这个 registry其余包照常走默认源。这样就形成了一个混合结构内部资产走内网官方包走默认源互不干扰。万一外部源不可达内部资产至少还是通的开发不至于完全停摆。私有 registry 的另一个好处是版本可追溯。谁在什么时候推了哪个版本一清二楚出问题回滚也方便。7.2 工程约定要写进文档不能靠口头传技术方案只是一半另一半是约定。我在项目里固定了几条规矩写在README里新同事入职第一件事就是读它LocalPackages目录随工程一起提交不允许加进.gitignore。Assets/下的第三方资产必须先本地化为 UPM 包再在工程里引用禁止直接往Assets里拖。任何依赖变更必须同时提交manifest.json和packages-lock.json。每个季度的第一个周一做一次冷还原演练记录结果。备份目录的哈希清单每次备份后重新生成历史清单保留最近四次。这些规矩看起来啰嗦但它们的本质是把依赖管理从个人习惯变成工程制度。个人习惯靠不住制度靠得住。还有一点值得强调把第三方资产本地化不只是为了应对外部不可达它本身就能提升工程的可维护性。依赖声明清晰了新环境搭建时间从一天缩短到十分钟版本锁定了不会再出现昨天还能跑今天不行了的玄学问题资产进版本控制了谁改了什么东西都有记录。这些收益是实打实的。最后分享一个我在实际迁移中总结的小技巧迁移前先在原机器上把工程完整打开一次让 Unity 把该生成的中间资源全部生成好然后再做备份。这样备份下来的是已经被 Unity 消化过的状态还原时的意外会少很多。反过来如果从一个从没打开过的工程目录直接拷中间资源缺失还原时可能触发大量重新导入反而更容易暴露问题。另外一个经验是备份完成后不要马上把源目录清理掉。留至少一个完整的项目周期作为缓冲确认新备份真的能还原、能跑通、能过 CI再去清理。我吃过一次亏备份完就把旧缓存删了结果新备份里有一个包的符号链接没解引用还原时才发现那时候旧数据已经没了只能厚着脸皮找同事要。
返回列表