关于 unity-package-management
unity-package-management 是面向 Unity 项目维护者的技能指南,说明如何通过 Pipeline 命令管理 UPM 包,并在无法使用 Editor 命令时采用 C# Client API 回退。它还涵盖包发现、版本验证和无界面安装流程。
主要功能
- 01添加、移除和升级 UPM 包
- 02搜索包及检查可用版本
- 03优先通过 Pipeline 命令操作
- 04无 Editor 时使用 C# 回退脚本
- 05以无界面模式安装和验证包
- 06触发资源导入并生成 .meta 文件
适用场景
- 01为现有 Unity 项目配置依赖
- 02在 CI 中非交互安装包
- 03安装前核对包 ID 和版本
- 04无界面导入资源并生成元数据文件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ unity-technologies/ skills --skill unity-package-management安装后在 Claude Code 中输入/unity-package-management确认已加载。
附带文件references/select-packages.md
详细介绍
Unity 包管理(优先使用 Pipeline 命令,C# 脚本作为备用方案)
通过 unity command 运行项目的 Pipeline 包中的 package 命令,添加、移除、升级和查找 UPM(Unity Package Manager)包。只有在没有 Editor 能执行这些命令时,才使用脚本驱动 UnityEditor.PackageManager.Client 的无界面备用方案(参见 备用方案)。不要手动编辑 Packages/manifest.json:Package Manager 能正确解析依赖和兼容版本,而手动编辑经常会导致解析失败。
本指南是对 unity-cli 技能的补充;该技能介绍 Editor 安装、项目创建、构建/测试,以及 unity command、unity status 和 unity pipeline install。
优先使用 package 命令
Pipeline 包会注册一个 package 命令组,包含 add、remove、list、resolve、search 和 status。这些命令需要一个正在运行的 Editor,且其项目中包含 com.unity.pipeline(使用 unity pipeline install 添加,然后运行 unity open,再使用 unity status --until-ready 等待就绪)。每个命令的参数都由 Editor 提供,因此首次调用前请先查看参数。不要猜测参数名称。
unity status --until-ready --project-path "<project-path>"
unity command package --project-path "<project-path>" # the package commands this Editor has
unity command --query package --format json --project-path "<project-path>" # each command's parameter schema
unity command package search --project-path "<project-path>" # then pass the parameters the schema lists
unity command package add --project-path "<project-path>" # add one or more packages by id
unity command package list --project-path "<project-path>" # confirm what is installed
按架构名称在命令名后传入参数(unity command <group> <command> --<parameter> <value>)。如果架构允许接受多个 ID,请在一次 package add 调用中添加所需的所有包。如果需要指定最低版本,请按架构说明的方式固定版本(@<version> ID 格式属于下文的 C# 备用方案)。使用 package status 或 package list 验证,并使用 package remove 移除包。非零退出码表示失败,请阅读错误信息。
如果 unity command package 报告没有此命令组,说明项目的 Pipeline 包版本过旧:运行 unity pipeline upgrade,或使用下文的备用方案。
适用场景
- 在现有项目或新创建的项目中添加、移除或升级一个或多个包。
- 在 CI 中以非交互方式配置项目的包(没有 Editor 运行时使用备用脚本)。
- 在依赖某个包之前,验证包 ID 是否存在,或查找其可用版本。
- 确定游戏实际需要哪些包——参见 references/select-packages.md。
选择要安装的内容
只安装项目实际需要的内容,不要一概而论;优先使用所选模板已提供的包(URP 模板已包含渲染管线、Input System 等)。游戏类型 / 视觉风格 / 平台 / 变现方式与包的对应关系,以及如何搜索注册表,见 references/select-packages.md。整理出一份去重后的包 ID 列表,并在安装前复述给用户。
备用方案:没有 Editor 可供询问时(C# Client API)
仅当 package add 无法运行时使用此部分:没有打开 Editor 且不希望打开、项目没有 Pipeline 包,或缺少命令组且不希望升级。此方案通过 Editor 在批处理模式下运行的脚本,调用 Client.AddAndRemove 完成安装。
-quit 问题——为什么安装时不能使用 unity run
Client.Add / Client.AddAndRemove 是异步的:它们会返回一个 Request,只有在之后的 EditorApplication.update 轮次中才会完成(UPM 子进程会通过 Editor 的主循环泵将结果传回,因此使用 while (!req.IsCompleted) 阻塞式忙等会导致死锁)。-executeMethod 返回后,Editor 必须保持运行,直到请求完成。
不能将 unity run 用于安装程序:它的默认路径会注入 -quit(参见 unity-cli 技能中的保留标志)。使用 -quit 时,Editor 会在方法返回的瞬间退出——此时 UPM 尚未完成解析,因此软件包永远不会安装,回调也永远不会运行。
**解决方案:**直接启动 Editor 二进制文件,使用 -batchmode,不带 -quit。Editor 会保持运行,EditorApplication.update 会持续轮询,轮询回调会运行,并在完成时自行调用 EditorApplication.Exit(code)——该调用会同时退出并设置进程退出代码。
安装程序脚本
将以下内容写入 Assets/Editor/ProjectBootstrap/PackageInstaller.cs。它必须放在 Editor/ 文件夹下(或 Editor 专用程序集内),因为它使用了 UnityEditor。
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
namespace ProjectBootstrap
{
// Installs (and optionally removes) a fixed set of packages via the PackageManager
// Client API, headless-safe.
public static class PackageInstaller
{
// EDIT this list to match the package selection (see references/select-packages.md).
static readonly string[] PackagesToAdd =
{
"com.unity.inputsystem",
"com.unity.cinemachine",
"com.unity.render-pipelines.universal",
// "[email protected]" // pin a version with @ when a minimum is required
};
// Optionally drop packages in the same resolution pass (e.g. a template default you don't want).
static readonly string[] PackagesToRemove = { };
const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow
static AddAndRemoveRequest _request;
static double _deadline;
// Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install (NO -quit)
public static void Install()
{
if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)
{
Debug.Log("[PackageInstaller] Nothing to do.");
EditorApplication.Exit(0);
return;
}
Debug.Log($"[PackageInstaller] Adding: {string.Join(", ", PackagesToAdd)}");
_request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);
_deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
EditorApplication.update += Poll;
}
static void Poll()
{
if (_request == null) return;
if (!_request.IsCompleted)
{
if (EditorApplication.timeSinceStartup > _deadline)
{
EditorApplication.update -= Poll;
Debug.LogError("[PackageInstaller] Timed out waiting for UPM.");
EditorApplication.Exit(2);
}
return;
}
EditorApplication.update -= Poll;
if (_request.Status == StatusCode.Success)
{
var names = _request.Result.Select(p => $"{p.name}@{p.version}");
Debug.Log($"[PackageInstaller] Resolved: {string.Join(", ", names)}");
EditorApplication.Exit(0);
}
else
{
Debug.LogError($"[PackageInstaller] Failed: {_request.Error?.message}");
EditorApplication.Exit(1);
}
}
}
}
AddAndRemove 会在单次 UPM 解析过程中安装整个软件包集合——比每个软件包调用一次 Client.Add 更快,也更不容易出错。
使用一个脚本添加 / 移除 / 升级:
- 添加:将 ID 列入
PackagesToAdd。 - 移除:将 ID 列入
PackagesToRemove。 - 升级 / 固定版本:添加带有
@<version>的 ID(例如[email protected])。如果不指定版本,解析时会选择最新的兼容版本。
查找 / 验证软件包
添加之前,可在注册表中搜索以确认 ID 是否存在或列出其版本。在 Editor 内调用的 Client.SearchAll() / Client.Search("<id>") 也是异步的,因此它们与安装程序使用相同的轮询并调用 Exit 的模式,以及相同的无头运行方式。编写 Assets/Editor/ProjectBootstrap/PackageSearch.cs:
using System.Linq;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
namespace ProjectBootstrap
{
public static class PackageSearch
{
const double TimeoutSeconds = 120;
static SearchRequest _request;
static double _deadline;
// Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll (NO -quit)
public static void SearchAll()
{
_request = Client.SearchAll(); // or Client.Search("com.unity.cinemachine")
_deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;
EditorApplication.update += Poll;
}
static void Poll()
{
if (_request == null) return;
if (!_request.IsCompleted)
{
if (EditorApplication.timeSinceStartup > _deadline)
{
EditorApplication.update -= Poll;
Debug.LogError("[PackageSearch] Timed out.");
EditorApplication.Exit(2);
}
return;
}
EditorApplication.update -= Poll;
if (_request.Status == StatusCode.Success)
{
foreach (var p in _request.Result.OrderBy(p => p.name))
Debug.Log($"[PackageSearch] {p.name}@{p.versions.latestCompatible} {p.displayName}");
Debug.Log($"[PackageSearch] {_request.Result.Length} packages found.");
EditorApplication.Exit(0);
}
else
{
Debug.LogError($"[PackageSearch] Failed: {_request.Error?.message}");
EditorApplication.Exit(1);
}
}
}
}
_request.Result 是一个 PackageInfo[];每个条目都包含 name、displayName、description 和 versions(.latest、.latestCompatible、.all)。如果想不启动 Editor,仅在终端检查某个已知 ID(而不是自由文本搜索),请直接查询注册表——参见 references/select-packages.md。
无头运行(直接调用 Editor,不带 -quit)
根据版本解析 Editor 二进制文件,然后在批处理模式下运行。脚本会通过 EditorApplication.Exit 自行退出,因此不要传入 -quit:
VERSION="<version>" # e.g. 6000.0.47f1 (or an installed version)
PROJECT="<project-path>"
METHOD="ProjectBootstrap.PackageInstaller.Install" # or ...PackageSearch.SearchAll
# Install directory of that editor (Hub layout), via the unity CLI
ED=$(unity editors path "$VERSION" --format json | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['path'])")
# Resolve the executable per-OS (handles both "dir containing Unity.app" and the ".app" itself)
case "$(uname)" in
Darwin) if [ -d "$ED/Unity.app" ]; then UNITY_BIN="$ED/Unity.app/Contents/MacOS/Unity";
elif [[ "$ED" == *.app ]]; then UNITY_BIN="$ED/Contents/MacOS/Unity";
else UNITY_BIN="$ED/Unity"; fi ;;
Linux) UNITY_BIN="$ED/Editor/Unity" ;;
*) UNITY_BIN="$ED/Editor/Unity.exe" ;; # Windows (Git Bash / MSYS); use Editor\Unity.exe in PowerShell
esac
"$UNITY_BIN" -batchmode -projectPath "$PROJECT" -executeMethod "$METHOD" -logFile -
echo "Exit code: $?" # 0 = success, 1 = UPM error, 2 = timeout
-logFile - 会将 Editor 日志(包括 [PackageInstaller] / [PackageSearch] 行)流式输出到 stdout,以便你查看解析进度并读取任何 UPM 错误。如果你所用构建版本中的 unity editors path 输出格式不同,请改用 unity editors --installed --format json 获取目录。
验证
# Every requested id should appear as a dependency
cat "<project-path>/Packages/manifest.json"
确认运行以 0 退出,并且列表中的每个软件包都存在于 manifest.json 中。如果软件包解析失败,系统会记录 _request.Error.message;请阅读该信息,并对照注册表检查 ID 和版本。Editor 自身的日志(包括 [PackageInstaller] 行)就是上文通过 -logFile - 流式输出到的 stdout——请在那里读取,不要通过 unity logs 查看(它显示的是 CLI 自身的日志,而不是 Editor 的日志)。
无界面导入并保存(生成 .meta 文件)
脚本或工具写入新的 .cs/资产文件后,Unity 必须导入这些文件,才能为每个资产生成所需的 .meta 文件——每个 .cs/资产都必须与对应的 .meta 一起提交。仅需打开项目一次(unity open "<project-path>"),Unity 就会导入并生成这些文件;需要在无界面环境中(通过脚本或 CI)执行时,请使用此方法。
与软件包安装程序不同,此操作是同步的——返回前即已完成——因此可通过 unity run 安全运行(它注入的 -quit 不会造成影响;此方法也会调用 EditorApplication.Exit,以便正常退出并返回正确的退出代码)。写入 Assets/Editor/ProjectBootstrap/ProjectSaver.cs:
using UnityEditor;
using UnityEngine;
namespace ProjectBootstrap
{
public static class ProjectSaver
{
// Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll
public static void SaveAll()
{
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
AssetDatabase.SaveAssets();
Debug.Log("[ProjectSaver] Assets imported and saved.");
EditorApplication.Exit(0);
}
}
}
unity run "<project-path>" --editor-version <version> \
-- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll
说明
- 这些编辑器脚本是为了方便初始化而提供的。可将它们保留在
Assets/Editor/ProjectBootstrap/(除非调用,否则不会执行任何操作),也可以在 设置完成后删除——由你决定;请告知用户。 - 所有脚本都位于
Editor/下,因为它们使用UnityEditor;不会包含在构建中。 - 变现/后端软件包(
com.unity.purchasing、com.unity.services.levelplay、UGS 软件包)通过相同机制安装,但实际的集成应通过专用技能完成:implement-in-app-purchases、levelplay-unity-integration、 build-live-game。