
1. 项目概述为什么Unity Package发布值得你投入精力如果你是一个Unity开发者无论是独立制作人还是团队中的一员大概率都遇到过这样的场景你写了一个特别好用的编辑器扩展比如一个能批量重命名资源的工具或者封装了一套通用的UI管理器又或者是一套处理特定游戏逻辑比如技能系统、对话系统的脚本框架。一开始你可能只是复制粘贴脚本文件或者用Unity Package Manager (UPM) 从Git仓库直接拉取。但当你的工具需要更新、需要管理依赖、或者想分享给更多同事甚至开源社区时简单粗暴的文件复制就显得力不从心了。这时一个结构规范、可被UPM直接识别和管理的Unity Package就成了最佳解决方案。它不仅仅是一个压缩包而是一个自带版本号、依赖声明、文档和示例的标准化交付物。对于团队内部它能确保所有成员使用统一、最新版本的内部工具极大提升协作效率和代码质量。对于社区发布一个高质量的Package是展示你技术实力、构建个人品牌、甚至开启商业化道路的绝佳方式。然而从一堆散乱的脚本到一个能顺利发布、安装、更新的成熟Package中间有不少“坑”等着你。这篇文章我就结合自己多次打包和发布内部工具、开源组件以及商业资产的经验带你走一遍完整的流程并重点分享那些文档里不会写、但实际开发中一定会遇到的“避坑”要点。2. 核心概念与Package结构解析在动手之前我们必须彻底理解Unity Package到底是什么以及它的标准结构。这能帮你从根源上避免后续90%的路径和依赖问题。2.1 Unity Package的两种形态与UPMUnity Package本质上是一个遵循特定结构的文件夹。它主要通过Unity Package Manager (UPM) 进行管理。你需要了解它的两种主要来源形态本地包Local Package 这是开发阶段最常用的形式。它直接指向你本地硬盘上的一个文件夹。UPM会读取这个文件夹内的package.json文件来识别它。这种方式的优势是即时修改、即时生效非常适合开发和调试。托管包Hosted Package 这是发布后的形态。它通常托管在一个Git仓库如GitHub、GitLab或一个私有的NPM/Scoped Registry服务器上。UPM通过仓库地址如https://github.com/username/repo.git或Registry配置来获取和更新它。无论哪种形态核心都是那个名为package.json的清单文件以及一套约定的目录结构。2.2 标准Package目录结构详解一个最小化但功能完整的Package目录结构通常如下所示MyAwesomeTool/ ├── package.json # 包的“身份证”核心配置文件 ├── README.md # 项目说明文档 ├── CHANGELOG.md # 版本更新日志 ├── LICENSE.md # 开源许可证 ├── Third Party Notices.md # 第三方版权声明如有 ├── Editor/ # 编辑器扩展脚本 │ └── MyToolEditor.cs ├── Runtime/ # 运行时脚本 │ └── MyToolCore.cs ├── Tests/ # 测试脚本可选但推荐 │ ├── Editor/ │ └── Runtime/ ├── Samples~ / # 示例场景和脚本注意波浪线 │ └── ExampleScene.unity └── Documentation~ / # 文档可选也可用README └── index.md这里有几个关键点需要特别注意package.json 这是包的灵魂。它定义了包的名称、版本、显示名、描述、依赖等所有元数据。一个错误的package.json会导致包根本无法被识别。Samples~和Documentation~目录后的波浪线 (~) 这不是笔误。在Unity项目中以波浪线结尾的文件夹会被Unity视为“特殊文件夹”其内容不会被包含在包的编译和发布中。当用户通过UPM的“Samples”列表导入示例时Unity才会将这些文件夹复制到用户的Assets/Samples目录下。这避免了示例代码污染用户的项目也避免了示例中的脚本被意外编译。这是第一个容易踩的坑如果你希望提供示例必须使用Samples~这个命名约定。Editor与Runtime分离 这是Unity脚本编译的基本规则。所有只在Unity编辑器中运行的脚本如自定义Inspector、菜单项、窗口工具必须放在Editor文件夹或其子目录下。所有需要在游戏运行时包括编辑器播放模式执行的脚本则放在Runtime下。混放会导致编译错误。测试文件夹 虽然可选但我强烈建议为你的包编写单元测试。Tests文件夹下的内容同样遵循Editor和Runtime的分离原则。这些测试不会被打包进发布版本但能保证你包的核心逻辑健壮性。3. 从零开始创建并配置你的第一个Package理解了结构我们现在动手创建一个最简单的工具包。假设我们要创建一个名为“com.yourcompany.simpletimer”的简易倒计时管理器。3.1 初始化Package结构与package.json首先在Unity项目之外或Assets同级目录创建一个新文件夹例如SimpleTimer。然后在文件夹根目录创建package.json文件。package.json的内容是重中之重。下面是一个最基础的配置示例并附带了每个字段的详细解释{ name: com.yourcompany.simpletimer, version: 1.0.0, displayName: Simple Timer Manager, description: 一个轻量级、可扩展的倒计时与延时任务管理器。, unity: 2021.3, unityRelease: f1, documentationUrl: https://github.com/yourcompany/SimpleTimer/wiki, changelogUrl: https://github.com/yourcompany/SimpleTimer/blob/main/CHANGELOG.md, licensesUrl: https://github.com/yourcompany/SimpleTimer/blob/main/LICENSE.md, dependencies: { com.unity.nuget.newtonsoft-json: 3.0.2 }, keywords: [ timer, countdown, utility, tools ], author: { name: Your Name, email: your.emailexample.com, url: https://yourwebsite.com } }字段深度解析与避坑指南name(包名) 这是包的唯一标识符也是最大的坑点之一。它必须采用反向域名Reverse Domain Name的格式如com.companyname.toolname或io.github.username.library。这确保了全球唯一性避免命名冲突。切忌使用纯单词如“SimpleTimer”这会导致UPM无法正确处理或在未来与其他包冲突。version(版本) 必须遵循 语义化版本规范 (SemVer) 即主版本号.次版本号.修订号。例如1.0.0表示首个稳定版。修复Bug发布1.0.1增加向后兼容的功能发布1.1.0做出不兼容的API更改则发布2.0.0。UPM依赖版本号进行更新判断。unity与unityRelease 指定你的包兼容的Unity最低版本。unity: 2021.3表示兼容2021.3系列。unityRelease: f1进一步指定到特定的补丁版本如f1。通常只写unity字段即可。务必准确填写如果你用了2022.3的新API却声明兼容2020.3用户安装时会报错或行为异常。dependencies(依赖) 声明你的包所依赖的其他UPM包。格式为包名: 版本范围。例如com.unity.textmeshpro: 3.0.0。版本范围可以用^兼容更新如^3.0.0表示3.0.0 4.0.0或~允许修订号更新如~3.0.0表示3.0.0 3.1.0。关键避坑点不要将Unity引擎内置模块如UnityEngine.UI或用户Assets目录下的内容声明为依赖。只声明需要通过UPM安装的第三方包。3.2 编写核心脚本与示例接下来创建必要的运行时脚本。在SimpleTimer文件夹下创建Runtime文件夹并在其中创建SimpleTimer.cs// Runtime/SimpleTimer.cs using System; using System.Collections.Generic; using UnityEngine; namespace YourCompany.Tools { public class SimpleTimer : MonoBehaviour { private static SimpleTimer _instance; private static SimpleTimer Instance { get { if (_instance null) { GameObject go new GameObject([SimpleTimer]); _instance go.AddComponentSimpleTimer(); DontDestroyOnLoad(go); } return _instance; } } private class TimerData { public float duration; public float elapsed; public Action onComplete; public bool isLooping; } private ListTimerData _timers new ListTimerData(); public static int StartTimer(float duration, Action onComplete, bool loop false) { var timer new TimerData { duration duration, elapsed 0f, onComplete onComplete, isLooping loop }; Instance._timers.Add(timer); return Instance._timers.Count - 1; // 返回一个简单的ID } public static void StopTimer(int timerId) { if (Instance._timers.Count timerId timerId 0) { Instance._timers[timerId] null; // 标记为移除 } } void Update() { for (int i _timers.Count - 1; i 0; i--) { var timer _timers[i]; if (timer null) { _timers.RemoveAt(i); continue; } timer.elapsed Time.deltaTime; if (timer.elapsed timer.duration) { timer.onComplete?.Invoke(); if (timer.isLooping) { timer.elapsed 0f; } else { _timers[i] null; } } } // 清理所有被标记为null的计时器 _timers.RemoveAll(t t null); } } }然后创建示例。在根目录创建Samples~文件夹并在其中创建一个简单的示例场景和脚本向用户展示如何使用这个计时器。重要提示 示例中的脚本应该引用你的包命名空间并且确保示例场景能够独立运行。在Samples~目录下你可以像在普通Assets目录下一样组织场景和脚本。3.3 在本地项目中测试你的Package包结构搭建好后最关键的一步是在发布前进行充分的本地测试。有两种主要方式通过本地路径添加推荐打开你的目标Unity项目。打开Window Package Manager。点击左上角的“”按钮选择“Add package from disk...”。浏览并选择你刚才创建的SimpleTimer文件夹包含package.json的目录。Unity会立即将该文件夹识别为一个本地包并出现在Package Manager列表中。你可以像使用其他官方包一样使用它并且对本地文件夹的任何修改都会实时反映在项目中。通过manifest.json添加打开项目根目录下的Packages/manifest.json文件。在dependencies区块中添加一行com.yourcompany.simpletimer: file:../SimpleTimer。这里的file:协议后面跟的是相对于manifest.json文件的路径。保存文件Unity会自动刷新并导入该包。本地测试的核心验证点安装与导入 包能否被正确识别和导入Package Manager中显示的名称、版本、描述是否正确功能测试 在项目中编写测试脚本调用你的包API所有功能是否按预期工作示例导入 在Package Manager中点击你的包下方是否有“Samples”区域能否正确将示例导入到Assets/Samples目录下依赖解析 如果你的包声明了依赖如Newtonsoft Json在首次导入时UPM是否会自动安装这些依赖编辑器脚本 如果你的包包含Editor目录下的脚本相关的菜单项、自定义Inspector是否正常显示和工作4. 发布Package从本地到团队与社区本地测试通过后就可以考虑发布了。发布的目标决定了你采用的方式。4.1 发布到Git仓库开源/内部共享这是最灵活、最常用的方式尤其适合开源项目或小团队内部共享。步骤初始化Git仓库 在你的SimpleTimer目录下执行git init。创建.gitignore文件 确保忽略不必要的文件如*.csproj,*.sln,obj/,Library/,Temp/等。一个针对Unity包的.gitignore可以参考Unity官方模板。提交代码 将你的包所有文件提交到Git。推送到远程仓库 推送到GitHub、GitLab、Gitee或你公司的私有Git服务器。通过Git URL安装 其他人可以在其Unity项目的Package Manager中点击“” “Add package from git URL...”然后输入你的仓库地址。例如https://github.com/yourusername/SimpleTimer.git。如果想安装特定分支或标签可以在URL后加上#分支名或#标签如...#v1.0.0。Git发布避坑指南包含package.json的目录必须是仓库根目录 UPM通过Git URL克隆仓库后会直接在根目录寻找package.json。如果你的包在一个子目录里比如/src/MyPackage用户将无法直接通过Git URL安装。这时你需要使用subdirectory参数但UPM原生不支持。更常见的做法是使用一个专门的仓库来存放一个包。使用Git标签管理版本 强烈建议使用Git的标签Tag来对应包的版本号。例如发布v1.0.0时在代码提交后打上git tag v1.0.0并推送到远程。这样用户可以通过#v1.0.0来安装特定版本保持稳定性。处理大文件或二进制资源 如果包内包含大型二进制文件如图片、模型、音频考虑使用Git LFS大文件存储来管理避免仓库体积膨胀。同时在package.json中可以使用_fingerprint字段来帮助Unity进行增量更新但这属于高级用法。4.2 发布到私有NPM Registry企业级方案对于中大型团队拥有多个内部包且需要严格的版本管理和权限控制时搭建私有的NPM Registry是更专业的选择。Unity的UPM兼容NPM协议。常见方案Verdaccio 一个轻量级、开源的私有NPM代理注册表可以轻松在内部服务器部署。Azure Artifacts/GitHub Packages/GitLab Package Registry 各大云平台或代码托管平台提供的包管理服务通常与CI/CD流水线集成得很好。发布到NPM Registry的流程配置Registry 在包目录下创建或编辑.npmrc文件指定你的私有Registry地址和认证信息。登录Registry 在命令行执行npm login --registryhttps://your-private-registry.com。发布包 执行npm publish。这会将你的整个包目录根据.npmignore过滤后打包上传到Registry。在Unity中配置Scoped Registry 用户需要在项目的Packages/manifest.json文件中添加scopedRegistries配置告诉Unity去你的私有Registry查找特定作用域scope的包。例如你的包名是com.yourcompany.simpletimer那么scope就是com.yourcompany。// 用户的 manifest.json { scopedRegistries: [ { name: Your Company Registry, url: https://your-private-registry.com, scopes: [com.yourcompany] } ], dependencies: { com.yourcompany.simpletimer: 1.0.0, ... } }NPM发布避坑指南.npmignorevs.gitignorenpm publish会使用.npmignore文件来决定哪些文件不上传到Registry。如果没有.npmignore则会使用.gitignore。务必确保Samples~和Tests等目录被正确包含或排除。通常你希望发布的产品代码不包含测试和示例源文件因为它们可能通过其他方式提供但Unity的Samples~机制比较特殊需要根据你的分发策略决定。版本号冲突 不能发布相同版本号的包到同一个Registry。每次发布前需递增package.json中的版本号。认证与权限 确保发布和安装的机器都有正确的访问令牌Token和权限否则会遇到403或404错误。4.3 发布为.tgz归档文件离线或特定分发有时你可能需要将包打包成一个单独的文件进行分发比如通过邮件发送、放在内网共享盘或者作为某些离线环境的安装源。创建.tgz文件在命令行中进入你的包根目录。执行打包命令排除不需要的文件。例如# 在包根目录执行 tar -czvf ../SimpleTimer-1.0.0.tgz --exclude.git --excludenode_modules --exclude*.tgz .这会生成一个SimpleTimer-1.0.0.tgz文件。通过.tgz文件安装用户可以将.tgz文件放在本地某个路径。在Unity的Package Manager中点击“” “Add package from tarball...”然后选择这个.tgz文件即可。避坑点.tgz文件安装后Unity会将其解压到本地的全局包缓存中。用户后续无法直接修改这个包的内容。更新时需要提供新的.tgz文件并重新安装。这种方式不利于持续更新。5. 高级配置、优化与自动化当你的包逐渐成熟你会需要考虑更多提升质量和效率的方面。5.1 利用asmdef管理程序集定义默认情况下Unity会将所有脚本编译到同一个程序集中。对于包开发强烈建议使用程序集定义文件Assembly Definition File,.asmdef来精确控制编译边界、依赖和性能。为什么使用asmdef编译隔离与增量编译 修改包内代码时只会重新编译包自身的程序集而不是整个项目极大提升编译速度。清晰的依赖管理 在.asmdef文件中可以显式声明该程序集依赖哪些其他程序集包括其他包的程序集或项目中的程序集。这能提前发现缺失的引用避免运行时错误。代码访问控制 你可以创建InternalsVisibleTo属性让测试程序集能访问包内部internal的类而不需要将它们设为public。如何配置在Runtime和Editor文件夹根目录分别创建.asmdef文件。例如Runtime/SimpleTimer.asmdef 引用UnityEngine、UnityEngine.CoreModule以及你依赖的其他包的程序集。Editor/SimpleTimer.Editor.asmdef 除了引用运行时程序集还需要引用UnityEditor相关的程序集。避坑指南循环依赖 确保程序集依赖关系是单向的不能形成环。例如Editor程序集可以依赖Runtime程序集但反过来不行。平台兼容性 在.asmdef的“Platforms”设置中注意Editor程序集应该只勾选“Editor”而Runtime程序集则根据你的脚本实际运行的平台如Standalone, Android, iOS进行勾选。版本控制.asmdef文件是文本文件需要一并提交到版本控制。5.2 编写高质量的文档与示例一个没有文档的包就像没有说明书的产品用户体验会大打折扣。除了根目录的README.md我建议README.md 这是门面。应包含包简介、快速开始安装和最简单的使用示例、功能特性、API概览、详细文档链接、贡献指南、许可证信息。CHANGELOG.md 严格按照 Keep a Changelog 格式编写。清晰地列出每个版本新增、更改、修复和废弃的内容。这是维护者与使用者沟通的桥梁。内联XML注释与API文档生成 在C#代码中使用标准的XML注释为你的公共类、方法、属性添加说明。/// summary /// 启动一个一次性计时器。 /// /summary /// param nameduration计时器时长秒。/param /// param nameonComplete计时结束时调用的回调函数。/param /// returns返回一个计时器ID可用于提前停止计时器。/returns public static int StartTimer(float duration, Action onComplete)然后你可以使用像 Doxygen 或 DocFX 这样的工具自动从代码注释生成漂亮的API参考网站。丰富的示例Samples 提供多个示例场景覆盖从基础到高级的所有主要功能。每个示例场景最好配有一个简短的说明脚本或注释。示例是最好的文档。5.3 集成CI/CD实现自动化发布手动打包、打标签、发布的过程繁琐且易错。对于开源项目或团队项目集成持续集成/持续部署CI/CD是必由之路。一个典型的GitHub Actions工作流可能包含以下步骤触发条件 当向main分支推送标签如v*.*.*时触发。构建验证 在一个干净的Unity环境中可以使用Docker镜像如unityci/editor导入你的包运行单元测试如果有确保核心功能正常。版本号提取与验证 从Git标签中提取版本号并与package.json中的版本号进行比对校验确保一致。创建发布包 将包目录打包成.tgz或.zip文件并生成对应的.meta文件如果需要。发布到GitHub Releases 将打包好的文件作为资产Asset附加到GitHub Release中并自动生成Release说明可以从CHANGELOG.md提取。可选发布到NPM Registry 如果配置了私有Registry自动执行npm publish。通过CI/CD你只需要在本地打好标签并推送到远程剩下的所有发布流程都会自动、可靠地完成。6. 避坑指南那些我踩过的“坑”与解决方案最后分享一些在实际开发和发布过程中遇到的典型问题及其解决方案希望能帮你节省大量排查时间。6.1 路径与符号链接问题问题 在Mac或Linux系统开发包路径中包含了符号链接Symlink导致Unity在导入时出现奇怪的文件丢失或编译错误。解决方案 尽量避免在包目录结构中使用符号链接。如果必须使用例如链接到一个共享的资源库请确保所有协作者和CI/CD环境都有相同的符号链接设置或者考虑使用Git Submodule或更稳定的依赖管理方式。6.2 版本依赖冲突问题 你的包依赖Newtonsoft.Json的^13.0.0但用户项目或其他包依赖^12.0.0。UPM无法解析这种冲突导致安装失败或运行时异常。解决方案尽量使用宽泛的版本范围 如果没有使用新版本的特定API可以将依赖声明为较宽的范围如com.unity.nuget.newtonsoft-json: 12.0.0 - 13.0.0。使用Assembly Versioning 如果冲突无法避免可以考虑将你依赖的库如Newtonsoft.Json通过ILMerge或类似工具合并到你自己的程序集中但这会增大包体积并可能带来许可问题需谨慎评估。明确文档说明 在包的README中明确指出已知的依赖冲突并提供解决方案。6.3 平台相关代码处理问题 你的包中有只在特定平台如iOS、Android下才需要编译的代码直接放在Runtime下会导致在其他平台编译错误。解决方案 使用Unity的平台依赖编译指令。#if UNITY_IOS // iOS专用代码 #elif UNITY_ANDROID // Android专用代码 #endif同时确保你的.asmdef文件包含了所有目标平台。对于需要调用原生Native插件的情况将插件文件.a,.so,.dll放在Plugins/[Platform]目录下Unity会自动为对应平台选择正确的文件。6.4 包缓存导致的更新延迟问题 你发布了包的新版本如1.0.1但用户通过Git URL安装时Unity似乎没有拉取最新代码。解决方案 Unity UPM和Git都有缓存机制。可以尝试以下步骤在Package Manager中点击包名右侧的“...”菜单选择“Remove”然后重新添加。清除Unity的本地包缓存。缓存位置通常在%userprofile%\AppData\Local\Unity\cache\packages(Windows) 或~/Library/Unity/cache/packages(Mac)。对于Git依赖可以尝试在Git URL后添加特定的提交哈希值而非分支名以确保获取的是确切版本。6.5 发布后才发现重大Bug问题 包发布后尤其是发布到公开Registry或GitHub发现了一个严重Bug需要紧急修复。解决方案立即下架错误版本如果可能 在NPM Registry中可以使用npm unpublish但注意公开Registry对unpublish有时间限制。在GitHub上可以删除对应的Git标签和Release但这会影响已下载的用户。发布修订版本 遵循SemVer立即修复Bug并发布一个修订版本如从1.0.0到1.0.1。在CHANGELOG.md中明确说明这是紧急Bug修复。沟通 如果用户群体固定如内部团队通过邮件、群聊等方式通知大家立即更新。对于开源项目在GitHub Issue或Release页面发布公告。教训 强化发布前的测试流程包括单元测试、集成测试以及在多个Unity版本下的兼容性测试。考虑引入“预发布”标签如1.0.0-preview.1让小部分用户先行试用。