mcpField
U
unity-package-management

unity-package-management

github.com/unity-technologies开发1.2k7.5k 次安装1 天前更新
代码仓库

关于 unity-package-management

unity-package-management 是面向 Unity 项目维护者的技能指南,说明如何通过 Pipeline 命令管理 UPM 包,并在无法使用 Editor 命令时采用 C# Client API 回退。它还涵盖包发现、版本验证和无界面安装流程。

主要功能

  1. 01添加、移除和升级 UPM 包
  2. 02搜索包及检查可用版本
  3. 03优先通过 Pipeline 命令操作
  4. 04无 Editor 时使用 C# 回退脚本
  5. 05以无界面模式安装和验证包
  6. 06触发资源导入并生成 .meta 文件

适用场景

  1. 01为现有 Unity 项目配置依赖
  2. 02在 CI 中非交互安装包
  3. 03安装前核对包 ID 和版本
  4. 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。

评论

支持 Markdown