mcpField
U
unity-package-management

unity-package-management

github.com/unity-technologies开发60461 次安装今天更新
代码仓库

关于 unity-package-management

unity-package-management 是用于添加、移除、升级和查找 Unity Package Manager 包的技能。它优先使用项目的 Pipeline 包命令,并说明无可用 Editor 时的无界面 C# 备用方式。

主要功能

  1. 01添加、移除和升级 UPM 包
  2. 02搜索包并查看版本
  3. 03优先使用 Pipeline 包命令
  4. 04必要时使用无界面 C# 备用方案
  5. 05安装后验证包

适用场景

  1. 01管理现有 Unity 项目的依赖
  2. 02在 CI 中非交互式配置包
  3. 03检查包 ID 和可用版本

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/unity-technologies/unity-agent-plugin --skill unity-package-management

安装后在 Claude Code 中输入/unity-package-management确认已加载。

附带文件references/select-packages.md

详细介绍

Unity 包管理(优先使用 Pipeline 命令,C# 脚本作为备用方案)

使用项目 Pipeline 包的 package 命令,通过 unity command 添加、移除、升级和查找 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,或使用下方的备用方案。备用方案会启动自己的 Editor,因此在当前 Editor 已打开该项目时无法运行:请先让用户保存并关闭项目(参见备用方案的第一条规则)。

适用场景

  • 在现有项目或新建项目中添加、移除或升级一个或多个包。
  • 在 CI 中以非交互方式配置项目的包(没有 Editor 运行时使用备用脚本)。
  • 在依赖某个包之前,验证其 ID 是否存在,或查找其可用版本。
  • 判断游戏实际需要哪些包——参见 references/select-packages.md。

选择要安装的内容

只安装项目实际需要的内容,不要一股脑安装所有包;优先使用所选模板已提供的包(URP 模板已包含渲染管线、Input System 等)。按游戏类型 / 画面风格 / 平台 / 盈利方式选择包的对应关系,以及如何搜索注册表,详见 references/select-packages.md。生成一份去重后的包 ID 列表,并在安装前复述给用户。

后备方案:无法通过 Editor 操作(C# Client API)

仅在 package add 无法运行时使用此部分:没有打开的 Editor,且你不希望打开 Editor;项目没有 Pipeline 包;或者缺少命令组,且不想升级。此方案通过 Editor 以批处理模式运行的脚本,使用 Client.AddAndRemove 安装。

必须先关闭项目。 脚本会在第二个批处理模式的 Editor 中运行,而已经打开该项目的 Editor 会持有项目锁,导致批处理运行在脚本启动前就失败。unity editors running --format json 会列出所有已打开的 Editor,无论是否安装了 Pipeline 包(unity status 只会列出 Pipeline 项目,因此那里没有结果并不表示项目已关闭)。将每个 data.instances[].projectPath 与目标项目进行比较。同时检查 <project-path>/Temp/UnityLockfile 是否存在,并询问用户项目是否在 Editor 中打开。如果有 Editor 打开了该项目,或你无法确定,请用户保存工作并关闭项目,然后等待用户确认项目已关闭。如果批处理运行仍因锁而失败,则视为项目仍处于打开状态,并请用户关闭项目;不要重试,也不要移除锁文件。切勿自行关闭或终止用户的 Editor:其中可能有未保存的场景或 Play Mode 会话。如果用户希望继续保持打开状态,则不要运行后备方案:告知用户,必须等项目关闭或升级 Pipeline 包(先运行 unity pipeline upgrade,再运行 package add)后才能安装。

-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)——该调用会退出 Editor 并设置进程退出代码。

安装程序脚本

将以下脚本写入 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。
  • 升级/固定版本:为 ID 添加 @<version>(例如 [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 二进制文件,然后以批处理模式运行。仅当没有 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>"),即可导入并生成这些文件;需要以无界面方式执行时(例如在脚本或 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

注意事项

  • 这些 Editor 脚本是为了方便引导项目。可将它们保留在 Assets/Editor/ProjectBootstrap/ 中(除非调用,否则不会执行),也可以在设置完成后删除——由你决定;请告知用户。
  • 所有脚本都位于 Editor/ 下,因为它们使用了 UnityEditor;它们不会随构建一起发布。
  • 变现/后端包(com.unity.purchasing、com.unity.services.levelplay、UGS 包)通过同一机制安装,但实际的集成请使用专用技能:implement-in-app-purchases、levelplay-unity-integration、build-live-game。

评论

支持 Markdown