资讯动态

Unity自定义包(UPM)实战:从代码拷贝到模块化包管理

发布时间:2026/10/3 2:57:49 来源:尧图企业网站定制
写Unity开发这么多年我越来越觉得一件事真正拉开项目效率差距的往往不是某个炫酷的渲染技巧而是代码的组织方式。今天想聊聊Unity自定义包Custom Package也就是通过UPMUnity Package Manager管理自己写的模块化代码。这东西乍一听像是个“工程管理”话题很多人在小项目里根本懒得碰但只要你经历过“把一份代码从A项目复制到B项目然后改bug改到怀疑人生”的窘境就能理解自定义包到底解决了什么问题。简单说自定义包就是把你的通用功能——比如UI框架、数据管理器、工具脚本、甚至美术资源——按照Unity官方包的标准结构打包放进Package Manager里统一管理。它能做的不仅仅是“复制粘贴”而是让代码有版本、有依赖、有目录规范还能通过Git、私有服务器甚至本地路径直接分发。适合谁适合所有不想被“代码拷贝地狱”拖死的Unity开发者不管你是做独立小游戏还是在团队里写业务这套东西都能用得上。这篇文章我会从为什么需要自定义包讲起带你把包的目录结构、manifest配置、依赖管理、常见坑全部过一遍。不会念官方文档只会说人话讲实操。内容不短建议收藏了慢慢看。1. 为什么要用自定义包从“复制粘贴”到“包管理”1.1 代码复用的痛点你肯定遇到过先想想最原始的工作流写了一个通用工具类比如一个简单的对象池、一个音频管理器新项目要用直接把脚本文件拖过去。听起来没什么问题但真实情况是这样第一你拖过去的文件可能依赖其他脚本。比如你的对象池依赖一个自定义的泛型单例基类单例基类又依赖一个生命周期管理接口。手动拷贝很容易漏点开一看几十个编译错误还得一个一个补依赖。第二你改了这个工具类原项目里的那份并不会自动更新。等两个月后回原项目开发发现同一个文件在原项目和新项目已经是两个不同的版本修了的bug没同步过去新加的接口又互相冲突。第三团队协作的时候更麻烦。同事给你的“通用模块”是以压缩包发过来的里面还带了一堆Editor脚本和第三方依赖怎么放放Assets下怕污染工程不放又没法用。没有统一规范每个人管理方式都不一样项目越做越乱。我见过最夸张的一个项目Assets根目录下平铺了十几个命名各异的文件夹里面全是复制的通用代码有些文件名后面带着“_copy”“_final_v2”这种后缀。这种项目能跑但没人敢重构因为根本理不清依赖关系。1.2 UPM包管理Unity给的“官方答案”Unity提供了一套包管理机制——UPMUnity Package Manager最开始是用来管官方包和第三方商店包的比如URP、Timeline、Input System这些。但UPM的设计本身就是开放的它允许你把自己的代码也做成包放进同一个管理体系中。用包的方式管理自己的代码本质上就是给代码做了三件事规范、版本和依赖。规范包有固定的目录结构和配置清单package.json代码放在哪个文件夹哪些进运行时哪些只给编辑器用都有约定。版本每次改动可以打版本号项目里锁定你需要的版本。升级包就是一次普通的依赖更新而不是手动覆盖文件。依赖包可以声明它依赖哪些其他包。安装这个包时依赖会自动补齐不用再担心“漏拷贝”问题。打个比方以前你的模块是“散装零件”每次组装产品都得自己去仓库翻零件UPM自定义包则是“标准零件盒”盒子写了规格、标了版本、附了清单拿到手直接装上就能用。1.3 哪些功能适合做成自定义包不是所有代码都值得做成包要把最通用、最稳定的部分抽出来。我个人总结的判据有三个跨项目复用、逻辑相对独立、接口相对稳定。跨项目复用这个功能会不会在多个项目里用到只在一个项目里用的业务逻辑强行做成包反而增加复杂度。逻辑相对独立是否不依赖你的业务数据、不依赖特定场景的脚本如果它跟具体的GameObject、Prefab绑死那做包的意义就不大。接口相对稳定你愿不愿意长期维护它包是可以给多个项目使用的你改接口等于让所有依赖它的项目都要跟着调整这本身就是一种倒逼你设计好接口的方式。实际开发里适合做包的典型包括UI框架、音频管理、存档系统、事件总线、对象池、通用编辑器工具、资源加载管理、网络请求封装。这些都是稳定又通用的底层能力。反观那些“登录流程”“背包逻辑”这类偏业务的功能如果只是单一项目使用就不太建议做成包。2. 自定义包的目录结构先看懂规矩再动手搭建2.1 最小可用的包需要什么创建一个自定义包本质上就是创建一个符合UPM规范的文件夹。最小的包只需要两样东西一个package.json文件加一个放脚本的文件夹。先看一个最简结构MyPackage/ ├── package.json ├── Runtime/ │ └── MyManager.cs └── Editor/ └── MyMenu.cspackage.json包的身份证声明包的名称、版本、描述、依赖等信息。Runtime文件夹放运行时脚本也就是游戏运行时会加载的代码。Editor文件夹放编辑器脚本只在Unity编辑器环境运行比如菜单按钮、Inspector扩展。命名必须是“Editor”Unity才认得。把文件夹复制或放置到项目的Packages目录下不作为嵌入包或者在manifest.json里声明路径Unity就会把整个文件夹识别为一个包。写一个最简单的package.json{ name: com.mycompany.mypackage, version: 1.0.0, displayName: My Package, description: A package for managing audio in Unity projects., unity: 2021.3, author: { name: Your Name, email: youexample.com } }name字段是包的唯一标识格式推荐“com.公司名.包名”这样能避免重名冲突。version必须遵循语义化版本规范比如1.2.3这种格式。displayName是在Package Manager窗口中显示的名字。unity声明支持的Unity版本。author虽然不是必填但写上对团队维护有帮助。2.2 目录约定的进阶玩法Samples、Tests、Documentation一个真正良心的包光有Runtime和Editor还不太够官方包的目录约定里还有几个比较实用的MyPackage/ ├── package.json ├── Runtime/ ├── Editor/ ├── Tests/ ├── Documentation~/ ├── Samples~/ ├── CHANGELOG.md ├── LICENSE.md └── README.mdTests放测试脚本通常配合Unity Test Framework用确保包的逻辑没问题。Documentation~文档目录。名字后面带波浪号~是有讲究的Unity会把这个文件夹当成隐藏目录不参与编译和资源导入但是包管理器能识别。这样文档文件不会污染工程。Samples~样例目录跟Documentation同理带的~让Unity在工程里不直接导入它而是通过Package Manager的“Import Sample”按钮来导入。CHANGELOG.md更新日志记录每个版本改了什么。LICENSE.md许可证如果包要开放给别人用这个很重要。README.md包的介绍和使用说明。上面这些可选目录建议至少把Samples和Documentation加上。尤其是Samples很多开发者拿到一个陌生人写的包最先看的不是文档而是“有没有Sample可以直接拖进去跑”。2.3 Assembly Definition程序集作用域控制很多刚接触自定义包的人会忽略一个关键点程序集定义。UPM包默认情况下包内所有脚本会跟其他包、Assets下的所有脚本混合编译编译进同一个Assembly-CSharp程序集。这在小包时没问题但包一旦复杂起来问题就暴露了。建议是给每个核心目录加上asmdef文件比如Runtime下放一个MyPackage.Runtime.asmdefEditor下放一个MyPackage.Editor.asmdef。这样做有三个好处编译粒度拆分改包内代码时Unity只需要重新编译包对应的程序集而不是整个项目减少编译时间。引用关系清晰Especially当你要把包发布成测试版本时能明确声明这个包依赖哪些程序集。避免命名冲突多个包如果有同名类没asmdef就会编译冲突有了asmdef反而能隔离。asmdef文件本质是个JSON文件最简单的长这样{ name: MyPackage.Runtime, rootNamespace: MyCompany.MyPackage, references: [ UnityEngine.CoreModule, UnityEngine.UIModule ], includePlatforms: [], excludePlatforms: [], allowUnsafeCode: false, overrideReferences: false, precompiledReferences: [], autoReferenced: true, defineConstraints: [], versionDefines: [], noEngineReferences: false }关键字段是name、references、autoReferenced。references里填的是依赖的其他程序集名字autoReferenced表示这个asmdef是否会被自动引用。编辑器代码的程序集建议设“includePlatforms”为Editor这样保证不会打进运行时。这块刚开始有点绕但自动化能力非常强。等你的包越来越大给团队用的时候asmdef会让你少很多麻烦。3. 实操把自定义包装进项目的几种方式3.1 本地文件夹直接引用开发期的黄金方案在开发调试阶段最清爽的方式是直接引用本地路径。打开项目根目录下的Packages/manifest.json加上一行{ dependencies: { com.mycompany.mypackage: file:../../MyPackage } }这个file:后面可以是相对路径也可以是绝对路径。相对路径是相对于项目根目录的我一般推荐把包目录放在项目的上一级方便多个项目共用一份源码。用这种方式安装的包是本地包local package修改包里的代码Unity会自动刷新效果跟直接放在Assets里一样但又能享受包管理的结构。我日常开发里的通用模块基本都是用这种方式管理的。注意一个细节路径不能直接指向一个项目内的Assets子目录也不能有中文或特殊字符否则解析会有问题。路径结尾不需要加斜杠反斜杠偶尔能用但强烈建议统一用正斜杠。3.2 Git URL直接拉取团队分发最常用本地路径适合自己开发团队成员协作的话最好的方式是Git URL。只要你的包代码托管在Git仓库上Unity在安装包时可以直接从Git拉取。同样是在manifest.json里{ dependencies: { com.mycompany.mypackage: https://github.com/yourname/mypackage.git } }也可以指定分支、标签、commitcom.mycompany.mypackage: https://github.com/yourname/mypackage.git#v1.2.0配合Git标签使用相当于在一行里完成了“版本管理”。团队里其他人第一次打开工程Unity会自动拉取对应版本的包完全不需要手动拷贝。这里有个经验如果你用Git URL方式包的仓库不适合放整个Unity项目最好单独建仓库只放包的内容。整个Unity项目作为仓库会有大量工程文件干扰包的分发。3.3 嵌入式包Embedded Package永久留在项目里如果你希望某个包直接成为项目不可分割的一部分不想引用外部路径可以把包直接放到项目的Packages目录下子文件夹YourProject/ ├── Assets/ ├── Packages/ │ ├── manifest.json │ ├── packages-lock.json │ └── com.mycompany.mypackage/ │ ├── package.json │ └── Runtime/Unit会自动把Packages目录下的每个子文件夹当成包这就是嵌入式包embedded package。它的特点是修改不需要外部依赖代码就在工程内部。跟项目的版本控制走大家一起提交。这种方式适合那些“虽然做成包但不打算给其他项目复用”的特殊模块。不过要注意嵌入式包里的代码也是会被Producer/链接编译的packages-lock.json里会有记录如果手写manifest不必额外添加依赖直接放在Packages目录下即可生效。嵌入包的好处是“随工程走”坏处是“不独立”。如果你的包有多个版本可以用于不同项目就不建议嵌入。要么本地路径要么Git URL。3.4 用Package Manager窗口操作除了手写文件也可以直接在Unity的Window - Package Manager窗口里操作。点击左上角的号选择“Add package from git URL”粘贴Git地址或者选择“Add package from disk”选择本地文件夹。这里有一个实操经验通过窗口添加的包Unity会自动把地址写入manifest.json但路径方式加的本地包在窗口里显示为“local”Git方式显示为“git”。如果后续想修改版本窗口里点Update能调整。但手写manifest其实更可控我更喜欢每次直接改文件因为能精确看到所有依赖。4. 包与包之间的依赖管理别让“版本地狱”找上门4.1 依赖声明的两种方式一个包依赖其他包跟项目依赖一个包本质是一样的。比如你的网络包依赖JSON库不能指望使用者再手动装一遍而是包的package.json里直接声明{ name: com.mycompany.network, version: 2.0.0, dependencies: { com.unity.nuget.newtonsoft-json: 3.0.2, com.mycompany.mypackage: 1.0.0 } }依赖可以引用官方包也可以引用你的另一个自定义包。这样Unity在安装network包时会自动解析并安装依赖。这里有个坑依赖的版本号写法比较严格。Unity的包依赖支持精确版本“1.0.0”也支持Minimal版本“1.0.0”或带区间的“1.2.0 - 2.0.0”。示例如下dependencies: { com.mycompany.core: 1.1.0 }数字写错或者不兼容Unity会直接在包解析时报错。排查这种问题要看Editor日志里有没有“version does not satisfy”这类关键字。4.2 packages-lock.json锁定的秘密Unity里有个文件叫packages-lock.json它记录的是当前项目所有包解析后的实际版本。类似于JavaScript的package-lock.json和pnpm-lock.yaml。第一次解析完包依赖Unity会把具体版本、来源path路径或git commit都写进这个文件。这个文件极其重要强烈建议提交到版本控制里。否则每个同事打开工程时Unity都需要重新解析包版本。如果某个Git URL的分支 HEAD悄悄变了那每个人的包版本实际上不一致运行结果就可能互不相同到时候定位问题相当痛苦。我见过一个团队因为没人提交packages-lock.json结果同一个人在不同时间拉同一个分支得到的包版本不一样一个老Bug时有时无。最后查出来是某个Git分支被强制推送更新了。从那以后我把lock文件提交加入团队规范。4.3 依赖冲突怎么办假设项目依赖包A和包BA依赖C的1.0版本B依赖C的1.2版本。Unity无法同时装两个版本的C这时会发生版本冲突。Unity的包管理器策略是尽量选较高版本。如果1.2和1.0之间不兼容那总有一个包会出问题。我的处理建议分三步第一如果冲突的包是自己的包看升级哪个依赖更合理修改package.json后重新解析。 第二如果冲突是官方包比如一个包的依赖版本要求老旧builtin包则要在manifest里手动直接声明一个兼容的版本。 第三万不得已可以在项目中直接改依赖把B的依赖版本从“1.2.0”临时改成项目里已解析的版本。这种问题是包管理里比较烦的但比手动复制粘贴“所有代码全部乱掉”还是好处理得多因为错误会直接报在解析阶段而不是运行期。5. 实操过程全记录从零做一个本地自定义包这一节我按自己正常操作的习惯完整走一遍流程。以“做一个简单的事件系统包”为例方便你理解每一步的落地。5.1 准备包目录和package.json我习惯先建一个临时目录比如D:/UnityPackages/EventSystem然后在里面创建package.json{ name: com.example.eventsystem, version: 0.1.0, displayName: Example Event System, description: A lightweight event bus for decoupled communication., unity: 2021.3, keywords: [event, bus, messaging], author: { name: Your Name } }version我写0.1.0因为是第一个版本。如果你后面要继续迭代这个版本号就是整个生命周期的起点。显示名跟包名不同前者是给人看的后者是机器用的二者别混了。5.2 创建Runtime代码创建Runtime目录加一个EventBus.csusing System; using System.Collections.Generic; namespace Example.Events { public static class EventBus { private static readonly DictionaryType, ListDelegate handlers new DictionaryType, ListDelegate(); public static void SubscribeT(ActionT handler) where T : struct { var type typeof(T); if (!handlers.TryGetValue(type, out var list)) { list new ListDelegate(); handlers[type] list; } list.Add(handler); } public static void UnsubscribeT(ActionT handler) where T : struct { var type typeof(T); if (handlers.TryGetValue(type, out var list)) { list.Remove(handler); } } public static void PublishT(T eventData) where T : struct { var type typeof(T); if (!handlers.TryGetValue(type, out var list)) return; for (int i list.Count - 1; i 0; i--) { if (list[i] is ActionT action) { action(eventData); } } } } }这是最简单的无GC事件总线部分场景下仍有包装成本但演示足够代码放好。虽然没加asmdef但为了教学清晰先不动。正式项目建议加。5.3 创建Samples样例建一个Samples~目录里面放一个ExampleUsage.cs和对应的README说明。因为Samples~带波浪号Unity不会把它当成Assets导入只有通过Package Manager窗口点击“Import”才会拷贝到Asset下。Samples目录内部结构需要在package.json里声明。给package.json补上samples字段{ samples: [ { displayName: Basic Usage, description: A simple scene showing how to subscribe and publish events., path: Samples~/BasicUsage } ] }This方式的好处是Package Manager里会看到“Import”点了之后会生成一份到Samples文件夹供开发者运行参考。我见过的很多团队根本不放Samples别人拿到包还要自己摸索降低了复用率。放一个Sample比写一堆文档更能快速传达用法。5.4 在项目里引用打开目标项目的Packages/manifest.json在dependencies里加com.example.eventsystem: file:D:/UnityPackages/EventSystem保存后切回Unity等待包解析完成。通常几秒钟后Package Manager里就能看到这个包出现在列表里Runtime下的脚本会被编译进Assembly-CSharp如果没有asmdef的话。如果脚本有编译错误Console面板会直接报出来。这里提醒一个新手容易踩的坑包的路径不能放在项目Assets目录下否则Unity会当成普通资源导入重复编译而且不符合包管理规范。包必须放在Assets之外。5.5 给包加Update方法需要生命周期管理EventBus例子简单但它没有跟Unity生命周期挂钩。如果要做MonoBehaviour相关的通用模块比如定时器、协程管理器建议包内提供一个MonoBehaviour的驱动组件。以协程管理器为例可以在Runtime里放一个CoroutineRunner MonoBehaviour然后包的Manager类在静态构造函数中创建一个挂载该组件的GameObject。这样包能自驱动不需要使用者手动放任何Prefab到场景。但有个注意点Runtime代码可以直接new GameObject但创建出来的GameObject不能主动响应场景卸载。需要的时候可以在包内监听场景切换或者在DontDestroyOnLoad里保活。这个属于进阶设计包的稳定性和生命周期设计往往比功能逻辑本身更考验人。6. 常见问题与排查技巧实录6.1 包没出现在Package Manager列表里好几个朋友问过“我明明把文件夹放到Packages目录下了怎么窗口里没有”原因大概率是包的package.json格式有问题。UPM解析很严格name、version字段必须是合法字符串name必须符合反向域名风格否则直接不识别。还有一种情况你的文件夹不叫“com.xxx.xxx”格式虽然嵌入式包不要求名字带com但包含特殊字符会解析失败。排查方式打开Window - Package Manager点击Advanced下拉确认已勾选Show Preview Packages或Show Embedded Packages。如果包还是没有打开Packages/packages-lock.json看看有没有报错。实在不行看Editor日志里有没有package load相关错误。6.2 Unity一直提示解析失败或循环引用最常见的是包A依赖包B包B的package.json里又依赖包A。这种循环依赖在UPM里会被判定为无法解析。处理办法是重新设计包的依赖关系把公共部分下沉到一个基础包A和B都依赖基础包。还有一种情况是版本号写了一个尚未存在的版本Unity解析不出来。比如包的版本是1.0.0你在依赖里写“2.0.0”或“1.0.1”必然失败。先打开包的package.json确认实际版本再写依赖。6.3 修改包代码后项目没反应如果是本地路径引用的包修改包内的.cs文件Unity会自动重新编译一般几秒内生效。如果没反应先看Console有没有编译报错——如果包代码引用了一个不存在的类整个程序集会全部编译失败Assets里的代码也会一起变红。如果包是通过Git URL安装的改动包源码不会同步到本地。Unity拉取的是Git仓库的一个快照本地再做修改重启编辑器后也会被Git版本覆盖掉。这就是为什么要在开发阶段用本地路径而不是图省事直接挂Git链接。6.4 包代码能用但Package Manager版本号一直是0.0.0这个情况碰到过一次package.json里没写versionUnity会默认成0.0.0。虽然也能用但后续依赖版本管理会混乱。补上version重新解析。另外displayName写中文虽然可以显示但有些老版本Unity显示会乱码建议用英文。6.5 打Android包时报错Editor下却一切正常这种情况多半是包内混用了编辑器专用代码。如果你在Runtime脚本里写了using UnityEditor;Editor下Unity会自动忽略很多问题但打包时会直接报编译错误。排查方式看包内是否引用了只能Editor用的命名空间比如UnityEditor、UnityEditor.SceneManagement。如果有把相关代码移到Editor目录并加上asmdef或者在#if UNITY_EDITOR条件编译指令里包裹。另一个隐蔽点包内没有asmdef时代码会被编进Assembly-CSharp一些编辑器脚本也会被编译进运行时打包时自然露馅。6.6 表问题排查速查表现象可能原因处理方式包不在列表package.json格式错误、name不符合规范检查JSON格式与name字段重新打开窗口解析失败版本号不存在、循环依赖核对所有包的版本号重构依赖分层修改包代码不生效包以Git URL安装开发期改用相对路径的file:方式打包报编译错误Runtime引用了UnityEditor移动代码到Editor目录或加UNITY_EDITOR宏packages-lock.json频繁变动分支引用不稳定、未提交lock规范Git分支使用强制提交lock文件7. 把自定义包用在真实项目里三个完整落地场景7.1 场景一SolidWorks模型导入Unity的处理工具包跟标题相关的很多热搜词比如solidworks模型导入unity3d其实是一个很适合做自定义包的应用场景。SolidWorks导出的模型通常需要转换成Unity能正常使用的格式这一步牵扯很多脏活累活坐标系转换、单位缩放、材质匹配、Nurbs曲面转网格等。处理流程通常是从SolidWorks导出STP或STEP格式再用第三方工具转成FBX或OBJ。对模型进行坐标修正SolidWorks默认Z轴向上Unity是Y轴向上。处理单位换算SolidWorks设计单位可能是毫米导入Unity后要统一成米或按项目精度设置。材质分配与UV修复很多CAD模型没有规范UV直接导入会显示烂掉。这些操作完全可以做成一个“CAD模型导入辅助包”包含编辑器窗口、自动坐标系转换脚本、批量材质映射器、单位换算工具。放到团队里每个做机械仿真或工业展示项目的同事拿到包就能用不用每次打开一个新工程都重新写一遍转换逻辑。这个例子说明了自定义包的一个核心价值把一段“脏活、累活、重复活”封装成一个可靠的工具而不是丢给每个开发者各自解决。7.2 场景二Unity3D视频流播放模块再比如unity3d视频流很多人做项目要用视频流播放比如大屏展示、监控画面、视频会议看板。Unity自带的VideoPlayer对于本地视频文件很好用但视频流RTSP/RTMP/HTTP-FLV之类的支持就不够直接。实际工程中通常的做法接第三方渲染方案比如用原生插件解码再喂给Unity的Texture。或者用Unity的VideoPlayer加载远程URL但延迟和格式兼容是个问题。更复杂的场景需要对视频流做缓冲、重连、多路混流。这些能力如果是散落在各个项目里每次接一个视频项目就头疼一遍。如果打包成“视频流播放包”对外暴露一套接口Play(url)、Stop()、SetChannel(int)、OnFrameReady等内部的解码、纹理更新、错误重试逻辑全部封装起来。后续项目接入时一行代码搞定。视频流包里面还要考虑平台差异Windows、Android、iOS可能要用不同的解码器、UI层如何显示RawImage还是RenderTexture、性能开销解码占用CPU或GPU。包的架构设计往往在这类有平台差异的模块里最能体现价值。7.3 场景三unity3d简单小游戏项目的模块复用有热词是unity3d简单小游戏项目其实很多小游戏看起来简单框架并不简单。我做过的很多休闲小游戏里可以抽取的通用能力非常集中场景加载与过渡效果存档与设置管理音频与背景音乐UI弹窗管理本地排行榜和“分享回调”处理这些能力在小游戏里几乎都是标配。如果从第一个项目就开始将这些模块做成自定义包第二个小游戏项目可能只需要两天就把框架搭完剩下的时间全花在玩法和内容上。我见过独立开发者用这种方式一个月连出三个小游戏的。7.4 场景四团队资产沉淀与企业内部包管理自定义包的价值还有一个被低估的面向团队资产沉淀。很多工作室喜欢把所有东西塞进“公共资源服务器”但从没想过给代码和资源做一个正式的包格式。用UPM自定义包不仅把“工具代码”标准化了连美术资产的常用预制体、材质库、Shader都能打包。具体做法是把一个完整的资源库比如“通用UI预制体包”“地编常用Shader包”都按package.json规范组织好。美术和程序都能用Package Manager来浏览、导入、升版。而不是在共享网盘里找一个“最新最终版.zip”。这点对团队的规范化帮助极大代价是初期要花一点时间把资源整理成包结构。但只要形成习惯后续每次沉淀都会越来越轻松。8. 构建与发布的进阶考量8.1 如何让包跨项目真正版本化前面说到Git URL方式可以持续拉取但很多团队不重视版本标签。比如你在Git上更新了包的代码但没有打tag那么所有依赖该Git URL的项目会在Unity重新解析时拉到最新代码。这听起来挺方便但隐患很大。假如你的包更新后引入了Breaking Change而几个老项目同时被拉到了新代码运行直接报错。你还能不能定位到是“哪个版本引入的变化”正确的做法是每次修改包的代码处理完bug后给Git打tag比如v1.2.0。项目的manifest.json里固定写死“#v1.2.0”。以后新项目要升级手动去改版本号后续新改动打到v1.3.0。这样既保留了修复方便又保留了可追溯性。8.2 包命名与命名空间规范包名name字段和代码命名空间namespace要保持统一。比如包名是com.example.eventsystem代码命名空间就应该是Example.Events或者Example.EventSystem别用既不相关又没有辨识度的命名空间。这个细节会直接影响使用体验。如果你在Assembly Definition里配了rootNamespace新建脚本时默认命名空间就会跟包名关联上。我见过一个包名字叫com.foo.core代码命名空间却是Game.Utils使用者得去翻源码才能搞懂这个类的归属。8.3 UPM包的离线分发有些项目不能联网或者团队服务器在隔离环境里Git URL方式没法用。这种情况可以把打包成.tgz的包文件本地共享。Unity的Package Manager支持从Tarball安装com.example.eventsystem: file:D:/UnityPackages/eventsystem-1.2.0.tgz或者com.example.eventsystem: https://artifacts.example.com/packages/eventsystem-1.2.0.tgzUnity上传内网包管理服务例如部署一个Artifactory之类的后团队就可以像使用官方包一样使用自己发布的包。这个做法适合中大型公司流程上需要更严谨的CI配合但稳定性和权限控制是网络路径方式比不了的。8.4 从自定义包到私有Registry当你的包数量增长到几十个或者要跨部门分享时本地路径和Git URL会很分散。Unity支持配置自定义的RegistryScoped Registry在manifest.json里加一段配置scopedRegistries: [ { name: MyCompany Registry, url: https://myregistry.example.com/npm/, scopes: [com.example] } ]scopes声明了这个Registry上包含哪些命名空间。这样你的包可以直接通过“Add package by name”的方式安装输入com.example.eventsystemUnity会到私有Registry拉取。私有Registry是自定义包的终极形态需要搭建NPM兼容的服务器。如果你的团队还没到那个阶段本地路径和Git URL已经足够覆盖日常开发。先跑起来再逐步演进。9. 我的经验小结做包之前想清楚三件事开发自定义包做了这么久踩了不少坑也积累了一些经验。如果让我给刚上手的开发者总结就是三条第一先定边界。不要想着“把所有通用代码放一个包里”。一个包一个职责管线清晰很多。宁可包数量多一点也不要搞一个“万能核心库”出来。万能库最后一定变成谁都不敢改的“泥潭”。第二先有使用场景再做包。不要为了“规范化”硬把一个项目里的代码抽出来。如果只有这个项目用到抽出来意味着你要为它维护版本、写文档、处理依赖全是额外成本。等第二个项目确定要用它了再抽成包也不迟。第三持续维护贯穿始终。包不是写完了就完事了。每次在项目里使用它遇到的问题、需要的新能力都要回写到包里然后通过升版让所有项目共享。这才是一个包“活”起来的标志。放一个我常用的包结构模板你拿着可以改MyToolkit/ ├── package.json ├── README.md ├── Runtime/ │ ├── MyToolkit.Runtime.asmdef │ ├── Core/ │ └── Features/ ├── Editor/ │ ├── MyToolkit.Editor.asmdef │ └── Tools/ ├── Samples~/ │ └── Basic/ ├── Tests/ │ └── Runtime/ └── Documentation~/这个模板既给了目录也给了asmdef拆分。你可以在新建包时直接复制这个结构比每次从头建快很多。10. 写在最后的操作小技巧文章到这儿主体内容差不多了。分享一个我自己用得很顺的小技巧配合Packages/manifest.json的修改还可以在manifest里加自定义配置。比如你的包需要读取一些全局配置Unity的UPM包虽然更多意义上是“代码单元”但它也支持在包的Runtime层通过PlayerPrefs或ScriptableObject做配置。不同的是我习惯在包的README里把配置说明写清楚而不是把配置藏在代码里。另一个小技巧是给本地包建一个“开发项目”的工作区。我在开发某个包时会专门建一个空的Unity工程只挂这个包的本地引用用来快速测试包的运行效果。开发包时不要在主业务项目里改包代码来调试否则你的测试场景、测试数据很容易混进主项目里把包的性能和依赖关系污染了。自定义包这事儿难点不在技术而在于“习惯养成”。你只要认真做完一次就会彻底放弃之前的复制粘贴流。以后再看Unity工程源码你会不由自主地开始想这里能不能抽一个包那边能不能独立出来——你的工程组织能力就是在这些想法里一点一点涨起来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑