☰
Unity自定义包开发实战:从模块复用到团队依赖管理
2026/10/3 18:38:56 网站建设 项目流程

1. 为什么自定义包值得研究

很多 Unity 开发者第一次接触 Unity Custom Package 自定义包这个概念时容易误解,以为这只是把代码换个地方放而已。其实真不是这样,自从 Unity 引入 Package Manager 之后,自定义包就不只是“文件夹里多一个 package.json”那么简单,它直接改变了我组织项目的方式。

我长期同时维护好几个 Unity 项目,有面向移动端的,有做编辑器工具的,也有专门做原型验证的小项目。过去我习惯把公共代码复制进每个项目,比如对象池、存档模块、UI 框架、网络连接管理。一开始确实顺手,但半年之后问题全来了:项目 A 的对象池是 v1,项目 B 里被同事改成了 v2,项目 C 又被人回退到旧逻辑,因为新逻辑有点问题。每当我需要给公共模块加功能或修 Bug,就要在多个项目里逐一搜索、替换、测试,开销极其巨大。这还没算上那些藏在 Assets 里的 prefab、shader、材质球,复制粘贴根本无法自动追踪依赖关系。

自定义包正是解决这个痛点的标准化方案。它把代码、资源、配置、程序和文档统一到一个带有版本信息的包里,由 Unity 自带的 UPM 统一管理。你可以把包放到本地磁盘某个目录,也可以推到 Git 仓库,然后在任意项目的 manifest.json 里添加一行依赖约定,整个包就自动被安装并参与编译。更新的流程也变得清晰:改包,发版本,项目方自己决定什么时候升级依赖。

这个方案非常适合这几类人:经常重复造轮子的独立游戏开发者、想把公共模块统一管理的开发团队、需要把编辑器小工具分发给同事的技术美术或工具开发者。如果你只是在一个小游戏项目里写几段一次性脚本,那自定义包确实不是刚需;但只要你开始跨项目沉淀代码,它是迟早要掌握的基础知识。

1.1 复制粘贴维护公共代码的真实代价

我先说说痛点,因为只有理解了痛,你才会真的建包。复制粘贴公共代码听上去省事,实际维护成本远比你想象的高。

首先是对版本失去感知。你今天把角色控制器代码复制到项目 B,明天又复制一次覆盖,却根本不知道两份代码是否完全一致。假如其中有一段在项目 B 已经适配过特殊需求,下次再复制就会直接把项目 A 的版本覆盖进去,Bug 就这样产生了。团队里只要有两个以上的人参与,这个风险会成倍放大。一个人认为“我复制的是最新版”,另一个人认为“我这边改得更好”,最后代码在多个项目里各自演化了,谁也没法保证它们一致。

其次是资源依赖被撕开。Unity 的开发特点是代码和资源深度绑定,公共模块往往不只是脚本,还有对应的 shader、材质、ScriptableObject 配置、动画控制器。你用复制脚本的方式搬过来,脚本能跑起来,材质引用却断裂了,外表立刻出现紫红色警告。于是你又得手动把公用资源重复导一份,重复导入又带来了 GUID 冲突和引用错乱。我团队里就出现过几乎每个项目都有两份同名材质球但参数不同的问题,找 Bug 找得头大。

再者是没有升级机制。你发现对象池有内存泄漏问题,修好了,然后你挨个项目手动复制一遍修改。你会发现切换分支、重新打开项目、运行测试、提交代码,这一套流程要重复 N 次。如果每次都有细微差异,你连测试都懒得做全,最终得到一个“虽然修了 Bug 但在某些项目里可能没修干净”的状态。时间久了,公共代码就是一团烂账。

1.2 自定义包到底改变了什么

把同样的模块放进一个自定义包之后,整体逻辑完全不一样了。

包本身有独立的版本号。比如你的存档系统是 1.2.0,它有明确的 release note,有对应的 Git tag。项目 A 引用 1.2.0,项目 B 暂时停留在 1.1.4,这个状态是可以被 manifest.json 明确记录下来的。谁也不需要“猜”哪个项目在用哪版代码,打开文件就能看到。

包的依赖关系也是显式的。如果这个包依赖另一个包,比如对象池包依赖一个数学工具包,你可以在 package.json 里声明,UPM 会自动把依赖包一并解析安装。这比你在项目里手动放两个文件夹要可靠得多,因为 Unity 会检查版本兼容性,冲突时会直接报错而不是静默覆盖。

最重要的变化是边界。包内部可以定义自己的 asmdef(Assembly Definition),它只暴露你想暴露的内容,其他内部实现全部私有化。这其实是在代码层面画了一条清晰的线:模块外部只能通过约定好的公开 API 来使用模块,不能随手改动内部结构。在多人团队里,这条边界极其重要,它能防止公共代码被项目里的临时需求越改越乱。

我个人的感受是,自定义包把“这段代码是这个项目的一部分”彻底变成了“这个项目的一部分取决于这段代码”。依赖关系被显式记录、版本被管理、边界被定义,后面所有协作和迭代都建立在这个基础上,心里踏实很多。

2. 制作一个自定义包的最小可运行流程

理解概念之后,我们直接动手。对一个 Unity 开发者来说,第一次建自定义包不需要把结构想得非常复杂。你只需要准备一个文件夹、一个 package.json、几个脚本,然后在项目里把这个包注册上即可。走通这条路,再慢慢增加资源和工具。

2.1 package.json 的字段说明和一份最小样例

自定义包的核心注册文件是 package.json,它放在包根目录下。Unity 从 2019.1 开始全面使用 Package Manager,这个文件就是包的身份证。下面是一份最小可用的 package.json:

{ "name": "com.example.savesystem", "version": "1.0.0", "displayName": "Example Save System", "description": "一个轻量的存档管理模块,支持 Json 与 PlayerPrefs 两种后端。", "unity": "2021.3", "dependencies": { "com.unity.textmeshpro": "3.0.6" } }

我逐个说说这些字段的实际含义。

  • name:包的全局唯一标识,格式必须像反向域名,比如 com.company.module。这个字段一旦确定就不要轻易改,因为项目 manifest.json 里记录的就是这个值,改名等于换包,所有引用方都要更新。
  • version:语义化版本号,一般遵循主版本.次版本.修订号的规则。UPM 在解决依赖时会参考这个版本号,所以不要乱填。
  • displayName:显示在 Package Manager 窗口里的包名,可以写成用户友好的名字。
  • description:包的说明文字,会展示在包详情面板。写清楚它负责什么功能,依赖哪些外围条件,对团队其他成员帮助很大。
  • unity:声明这个包支持的 Unity 版本。实测下来,如果包使用了某些仅高版本支持的 API,这里就写对应版本,否则不写也行。
  • dependencies:包的依赖列表。UPM 会自动解析,比一个人手动安装依赖要可靠得多。要注意的是,这里填的依赖版本是一个区间表达式,比如 "1.0.0" 表示精确版本,"1.2.3" 表示至少这个版本,实际解析规则以 Unity 在 UPM 中的取值约定为准。

除了这些,包可能还会用到 author(作者信息)、keywords(搜索关键词)、hideInEditor(是否在包列表里隐藏)等字段。新手阶段知道这些就够用了,后面按需扩展。

2.2 把脚本按 Runtime 和 Editor 分开放

包建成之后,脚本不是随便丢进文件夹就完事。由于包会被 UPM 纳入项目的编译流程,你最好从一开始就把代码按照运行时机拆分清楚,否则日后会有很多意想不到的编译问题和包体大小问题。

常规做法是在包根目录下建 Runtime 和 Editor 两个文件夹。Runtime 里放最终运行时执行的代码,比如存档管理器、对象池、网络客户端、视频播放组件。Editor 里放只在编辑器环境下运行的代码,比如编辑器窗口、自定义 Inspector、菜单命令、资源导入辅助工具。Editor 文件夹里的代码如果被放进了 Runtime,Unity 不会主动报错,但打包时这些编辑器 API 会被带进最终程序变大,严重时还会因为 UnityEngine.Range 等异常直接编译失败。

在 Unity 社区里,一些老教程喜欢把包代码直接用 Assets/ 作为目录名,这是历史遗留习惯。新版 UPM 更推荐用 Runtime/Editor 这种明确命名的目录,因为它与 asmdef 的默认规则完全一致:放在 Runtime 下的程序集默认只参与运行时编译,Editor 下的程序集默认标记为 EditorOnly,彼此隔离,互不干扰。你不需要额外手写任何判断,只要目录对了,后面的构建行为基本都是合理默认。

2.2.1 私有程序集与 asmdef 设计

如果你希望包内部结构再严谨一点,就需要在 Runtime 和 Editor 下分别放置 asmdef 文件。asmdef 是 Unity 的程序集定义文件,它把代码编译成一个独立的程序集,从而控制引用关系。

最常见的做法是建两个 asmdef:一个叫 com.example.savesystem.Runtime.asmdef,一个叫 com.example.savesystem.Editor.asmdef。Runtime 程序集只声明对 UnityEngine 核心模块和它实际依赖的第三方包的引用,Editor 程序集则要在 references 里额外引用 Runtime 程序集。这样设计的好处是:你想让包的公开 API 区域和内部实现区域彻底隔离,外部代码能看到的就是你在 Runtime 程序集里标记为 public 的那部分。内部辅助类即使写成 public,但程序集名不同,外部引用通常不会乱进。

asmdef 文件本身是 JSON,内容不长。如果你不想完全手写,可以直接在 Unity 里右键文件夹创建 Assembly Definition,然后通过 Inspector 配置引用。不过我还是推荐了解一下手写结构,下面的例子可以作为参考:

{ "name": "ExampleSaveSystem.Runtime", "rootNamespace": "Example.Saves", "references": [], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false }

其中的 rootNamespace 很有用,它会让新生成的代码文件自动带命名空间前缀,省去很多手动添加命名空间的工作。

2.3 把本地的包注册进 Unity 项目

包做出来后,第一次注册到项目里的方式有三种,我逐个说一下适用场景。

第一种是通过 Package Manager 窗口添加本地包。打开 Window > Package Manager,点击左上角加号,选择 Add package from disk,然后定位到包的根目录,选中 package.json 即可。这种方式适合你正在桌面某个目录里开发包,要多项目联调的情况。它会把包加入 manifest.json 的 dependencies,记录为 file: 协议路径。

第二种是直接在 manifest.json 里写入本地路径。例如:

{ "dependencies": { "com.example.savesystem": "file:../../Packages/ExampleSaveSystem" } }

这种方式适合用文本编辑器手动管理依赖,或者想在脚本里批量添加依赖的场景。路径可以是绝对路径,但我建议用相对路径,这样整个项目目录被挪动时依赖仍能正常工作。

第三种是作为一个链接方式,放在项目的 Packages 文件夹下直接作为 embedded package。在 Unity 中,工程根目录下默认有 Packages/manifest.json,而 Packages 文件夹本身也可以直接放自定义包的目录。如果包就躺在那里,Unity 会自动把它视为 embedded,这个包的状态会被固定住,不会因为 manifest.json 的解析结果而改变。

对于本地开发阶段,我个人最喜欢 Add package from disk 的方式,它会直接在项目里生成显式的 file: 依赖,包更新时只需要替换目录内容并点击刷新,就能看到最新代码。切记不要把 file: 路径依赖提交到需要多人协作的仓库,除非你对路径的稳定性非常有把握,否则它很容易在不同开发机上失效。

3. 从实用角度设计包内模块

包能跑起来之后,关键的思维转变是:你现在不是在给某个项目写代码,而是在设计一个可以被多个项目复用的独立模块。这要求你从命名空间、公共 API、资源组织、示例代码等维度重新思考包的结构。

3.1 一个完整的存档模块包案例分析

我拿一个存档模块为例来说,因为它几乎每个项目都能用到,结构也足够有代表性。

包的目录结构可以是:

ExampleSaveSystem/ ├── package.json ├── Runtime/ │ ├── ExampleSaveSystem.Runtime.asmdef │ ├── SaveManager.cs │ ├── SaveData.cs │ └── Storage/ │ ├── IDataStorage.cs │ ├── JsonStorage.cs │ └── PlayerPrefsStorage.cs ├── Editor/ │ ├── ExampleSaveSystem.Editor.asmdef │ ├── SaveConfigWindow.cs │ └── SaveDataInspector.cs ├── Samples/ │ └── BasicSave/ │ ├── ExampleSaveUsage.cs │ └── ExampleSaveUsage.unity └── Documentation/ └── README.md

我们关注几个设计细节。

SaveManager.cs 里我要做一个公开门面类,团队所有项目都只通过它来读写存档,不直接接触 Storage 接口。这相当于把包的内部实现变化与外部调用隔离。今天我用 PlayerPrefs 存,明天改成 Json 文件存,只要 SaveManager 的 API 不变,其他项目一行都不用改。这种设计是模块化开发的精髓。

至于 SaveData 这种数据类,我通常会做成可序列化的基础类。由于存档数据在多个项目里差异很大,我不打算在包里写死字段,而是让使用方继承自己的数据模型。这要求包的公开 API 里预留泛型方法,比如 Save (string key, T data) 和 Load (string key)。在包内实现时注意反射和序列化的性能,避免在频繁存档的帧里做过多额外处理。

这个包还引出一个重要理念:包不应该知道业务项目里的细节。如果一个包依赖了某个项目自定义的 MonoBehaviour,这个包基本上就不可能被复用到其他项目了。因此,设计包时,你要反复问自己:这一层逻辑是通用能力,还是业务逻辑?通用能力放包里,业务逻辑留在项目里,这是包设计最核心的分界。

3.2 把 Prefab 和 Shader 资源也放进包里

包不只是装代码,它也完全可以装 Prefab、材质、Shader、ScriptableObject、纹理等资源。这对做 UI 组件库、特效库、工具库的人来说尤其重要。

在包内放置资源时,有一个必须注意的点:包内一切资源都使用 GUID 引用。也就是说,你在包的某个 Prefab 里引用了一份材质、一个脚本或另一个 Prefab,它们只要都从属于同一个项目,Unity 会自动通过 GUID 解析。只要包在项目里被正常安装,资源引用就不会断。这比复制粘贴方式下经常出现的“引用找不到”问题要稳定得多。

为了测试包内资源是否完全孤立可复用,我习惯用一个小技巧:把所有依赖项和资源放到一个临时空项目里,只通过 manifest 安装这个包,然后打开其中资源随意点击。如果编辑器控制台没有任何缺失引用的报错,这个包就是健康可分发的。如果出现引用丢失,多半是包内某个资源引用了包外的对象,或者 asmdef 的引用配置有遗漏。

对于比较重的资源,比如多个 4K 贴图、大量光照贴图,我不建议直接塞进包主体,因为每个项目安装这个包时都会被迫导入它们。更好的做法是把这些资源放到 Samples 文件夹里,让使用者按需添加。UPM 的 Samples 是包的示范资源目录,项目里可以通过 Package Manager 窗口单独 Import 某个 Sample,这样既提供了演示,又不强制占用项目体积。很多知名 UI 插件包就是这么做的,例如 Toony Colors Pro 的样例场景。

3.3 顺势聊两个常见的实际扩展场景:外部模型导入和视频流模块

如果你日常经常处理 SolidWorks 之类的外部 CAD 模型导入,也会发现自定义包是个好载体。你可以把模型导入工具链封装成一个 Editor 包:里面包含导入配置向导、格式检测、材质映射规则,以及常用的批处理命令。这样当你的团队从 SolidWorks 导出细分模型再导入 Unity 时,走的都是同一套标准化流程,生成的资源明显更可控。相比让每个人手动拖入模型、再手工调参数,这个包的价值就在于把经验固化成了自动化逻辑。

做游戏里的视频流或者动态内容展示时也同样如此。把视频解码、播放、字幕同步、事件回调封装成一个 Runtime 包,项目只需简单传入 VideoClip 或视频地址,就能得到统一的播放接口。视频解码涉及很多平台差异,封装成包后,平台相关代码被隔离在包内部的编译分支里,项目代码看起来就非常干净。用的时候你的项目处于移动端还是 PC 后台,不需要感知这些差异,包自己会在合适的平台分支上做处理。

我并不是建议你一味追求抽象,而是想说清楚:只要一个功能有可能被多个项目复用,就应该考虑把它从项目代码里剥离成包。这不是过度设计,而是对自己未来工作量的明确减负。

4. 把自定义包变成团队基础设施

当包在自己的项目里稳定运行之后,下一步就可以考虑把它发布到 Git 仓库,让团队成员按需引用。这一节讲的是把本地包升级为 Git 依赖的完整做法,以及这个过程中的版本管理策略。

4.1 推送到 Git 仓库并用 URL 安装

目前 Unity 支持通过 Git URL 直接把包作为依赖安装,这是团队协作最容易上手的发布方式。你需要先把包目录变成一个独立的 Git 仓库,然后推送到远程服务器。

操作步骤很简单,我先说 Git 侧的做法:

  1. 在本地创建一个新目录,把包的 package.json、Runtime、Editor、Samples 等全部放进去。
  2. 在包根目录执行 git init,添加远程仓库地址,提交初始代码。
  3. 把包推送到远程仓库,例如 git push -u origin main。
  4. 为关键的稳定版本打 tag,比如 git tag v1.2.0,然后推送 tag。

然后在项目侧,你可以通过 Package Manager 窗口操作,也可以直接修改 manifest.json。如果使用 Git URL 引用,文档里常见两种写法:

{ "dependencies": { "com.example.savesystem": "https://github.com/example/ExampleSaveSystem.git#v1.2.0" } }

也可以加通配符版本范围,不过推荐固定 tag,因为这样项目的依赖是可复现的。对稳定性要求很高的项目,固定 tag 是不二之选;如果你比较追求多项目同步更新,也可以直接指向分支名,风险自负。

这里要特别说一个坑:如果你把包含 file: 路径的 manifest.json 提交到团队仓库,其他人拉下来时会发现包根本不存在,因为路径指向的是你自己机器的目录。这大概是所有自定义包落地时最容易出现的协作事故。所以团队协作一定要优先使用 Git URL 的引用方式,把路径依赖留给单机开发或联调阶段。

4.2 版本号策略与依赖冲突处理

包一旦有多个项目引用,版本就成了团队协作的核心语言。版本号不是随便填的,它的语义直接影响依赖解析。按语义化版本规则来说,修复 Bug 和内部实现优化应该升修订号,例如 1.0.0 到 1.0.1;新增不破坏原有 API 的功能应该升次版本号,例如 1.0.1 到 1.1.0;破坏性 API 修改必须升主版本号,例如 1.1.0 到 2.0.0。

为什么这么讲究?因为 UPM 在解析依赖时有一套自己的规则。假如项目已经安装了版本 1.1.2 的包 A,而另一个包 B 要求包 A 至少 1.2.0,只要 1.2.0 与 1.1.2 不是破坏性变更,UPM 大概率会解析出更高版本。但如果你在主版本号做了破坏性改动却没有升级主版本,整个依赖树就乱了,多个包之间会出现莫名其妙的 API 不匹配。

我自己踩过最大的坑是包 A 依赖包 B,而项目里包 A 和包 B 是从两个不同开发者的仓库直接装的。由于没有统一版本策略,最终项目在编译阶段出现了大量重名类、重复命名空间冲突。处理办法只能回滚依赖版本。所以从第一天起,团队就要规定:发布包版本前必须写 changelog 并遵守语义化版本号,否则协作越深入越难收拾。

4.2.1 内网私有 Git 仓库的补充建议

如果你的团队在局域网内开发,不希望把代码推到公网,也可以用自建的 Git 服务,比如 GitLab CE 或 Gitea。Unity 的 Git URL 依赖只要是一个可以被正常 clone 的 Git 地址都行,不区分是否公网。需要提醒的是,URL 中如果包含认证信息,Unity 编辑器自身对账号认证的支持比较有限,推荐在开发机上提前配置好 SSH 密钥,这样 UPM 拉取包的时候不会遇到权限卡顿。

4.3 包内容之外:文档和变更日志

团队级的自定义包一定要配套文档。我在包内固定维护 Documentation/ 和 CHANGELOG.md。文档负责讲清 API 用法、依赖关系和常见配置;变更日志负责记录每个版本的改动点。这两样东西平时看起来不产生代码,但团队里任何一个人接手时说“这个包怎么用”,你不需要口头解释,只需要甩出文档位置,沟通成本能降一大截。

另外一个常被忽略的点是包的 Sample 示例。Sample 里的代码质量一定要高,因为它是别人在编辑器里按 Import 按钮后看到的第一印象。如果 Sample 里堆满了旧 API 或让人困惑的写法,使用者会直接对包的可靠性产生怀疑。

5. 常见问题与排查实录

自定义包并不神秘,但它毕竟是加在 Unity 项目上的一层组织方式,总会有一些容易踩的坑。这些坑大多不致命,但排查起来很影响心情。下面这节我按自己实操中遇到的高频问题做了一份速查记录,也附上了排查思路。

5.1 编译错误:程序集引用不明确

最常见的错误是包里的代码报错找不到类型或方法,这通常是 asmdef 的引用配置出了问题。

举个例子,你可能在 Runtime 脚本里用了 Newtonsoft.Json,但包的 asmdef references 里没有添加 Newtonsoft.Json。这时编译会报“当前上下文中不存在名称 JsonConvert”,即便这个包在 Unity 默认程序集里能正常使用。为什么?因为 asmdef 一旦生效,包内代码的可见引用范围就受程序集约束了,它不会自动引用项目里的其他程序集,除非你在 asmdef 里显式添加。

排查思路很简单:先打开包的 asmdef 文件,检查 references 列表里有没有包含需要引用的程序集名。如果是第三方 DLL,还需要确认 precompiledReferences 或 UnityEngine 模块引用是否完备。也可以先在 Unity 里点选报错的脚本,看 Inspector 里那个三角形的警告,它会直接告诉你这个脚本属于哪个程序集,以及有哪些引用缺失。

另外要留意循环依赖。如果你的包 A 引用了包 B,而包 B 又引用了包 A,UPM 在编译阶段会报程序集循环引用错误。解决这种问题需要重新审视包的功能边界,尽量保证依赖方向是单向的,或者把相互引用的公共部分抽离成更底层的包。

5.2 包显示状态不对:embedded、local、git 傻傻分不清

Package Manager 窗口里有些包显示 for development,有些显示 Source: Local,有些显示 Source: Git。这些状态差异不是随机的。for development 通常意味着包被嵌入到项目 Packages 目录,也就是 embedded;Local 通常意味着来自 file: 路径;Git 则代表来自远程 Git 仓库。

排查时最常遇到的问题是把包从 Git 切换到了本地 file: 路径,但 Package Manager 里版本始终还是旧的。这个现象往往是你没有删除锁文件。Unity 在项目的 Packages/packages-lock.json 中记录了每个包的解析结果,包括实际版本和来源地址。如果你修改了 manifest.json 里某个包的来源,但没有让 UPM 重新解析,它可能继续使用锁文件中的旧记录。此时可以删除 packages-lock.json 后重新打开项目,强制 UPM 重新解析所有依赖。不过这个操作影响全局依赖,执行前最好确认没有其他同事正在同时改动依赖。

5.3 资源导入失败和刷新不及时的避坑记录

修改包内脚本后有时你会发现项目里的调用方还停留在旧版 API,或者新代码编译了半天还在报错。这大概率是包没有及时刷新,特别是当包目录不在项目内部时,Unity 的自动监视机制偶尔不会立即感知外部目录变化。

我在实际工作里会形成两个习惯:一是在包目录和项目目录同时打开编辑器窗口,修改包后直接切回 Unity,等右下角编译转圈结束再运行;二是遇到顽固不刷新时手动执行 Assets > Refresh(或按 Ctrl+R),必要时关闭并重新打开项目。千万别在编辑器还在编译时强行改包文件,会导致下一次 import 偶尔出现半写入的临时状态,宁愿等稳定了再动。

5.4 阅读 locked file 的注意事项

UPM 默认的 packages-lock.json 对团队版本一起步是非常重要的。它保证了不同开发者机器上安装的依赖版本完全一致。我自己会在项目开新分支时特意看一眼 packages-lock.json 的 diff;如果某个同事升级了一个包,这个文件通常会被改动。保持这个文件的追踪状态,你就能对项目依赖变化历史一目了然。

顺带一提,如果团队采用 Git 依赖,我强烈建议不要轻易把 packages-lock.json 加入 .gitignore。一旦忽略它,你就会失去依赖解析的一致性保护。一个开发机上的包版本是 1.2.0,另一个开发机却装上了 1.3.0,最终问题定位时间会成倍增加。

6. 我个人实践中的一点体会和下一步思路

折腾自定义包很多次之后,我的核心体会是:这个功能不是给项目代码做“搬家”,而是强迫你拿出一套管理模块边界的标准。没有自定义包时,我写代码凭感觉,公共代码散落在项目各处;有了自定义包后,我会下意识先问自己“这段代码将来会不会被别的项目用到”。这个思维转变对代码质量的影响比表面看到的那层目录结构大得多。

如果你是第一次尝试,我建议不要一上来就把现有项目拆得七零八落。挑一个足够简单又确实重复使用的模块,比如存档、音频管理或对象池,把它独立成一个包,先走通本地流程,再走通 Git 分发。这个过程里你自然会遇到命名空间、程序集引用、资源放置这些具体问题,解决这些问题的经验远比读十篇概念分析文章更值钱。

后续如果你愿意继续扩展,可以研究一下用 UPM 提供的入口脚本自动化生成包配置,比如为每个新模块一键生成 package.json 和 asmdef 文件。也可以为自己的自定义包写一套专门的生命周期测试,在 CI 里构建一个空项目再安装包,确保包的搭建信息在干净环境下仍然成立。这些都是把模块化开发推向正规化的进阶方向,但它仍然要建立在一个基础之上:先把自己的公共代码从项目里解放出来,放到一个带着版本和边界的独立包里。这一步一旦迈出去,后续的团队协作和项目迭代都会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询