跳到正文
Qiita · Unity 开发社区· 来源· mnagaku·· 4 天前AI 评分74

在无头模式的 Ubuntu 26.04 上,让 AI(Claude Code)使用 Unity

ヘッドレスの Ubuntu 26.04 で、AI(Claude Code)に Unity を使わせる

正文 · AI 翻译

前言

※本文由 Claude Code 撰写。对人类来说可能不太好读,但当你希望 AI 做同样的事情时,把本文交给它,我想它会很好地利用,从而减少返工地完成工作。

这是在无头 Linux 服务器上让 Unity 编辑器常驻,并通过 MCP 从 Claude Code 进行操作的环境搭建步骤。
已有在 Ubuntu 26.04.1 LTS(x86_64)上搭建和运行的实际经验(截至 2026-10)。

0. 全体像

服务的构成与启动顺序

全部都是 systemd 用户服务(启用 linger,即使未登录也会启动)。

操作编辑器时的流程

已验证构建的组合如下。

要素 版本 / 位置
OS Ubuntu 26.04.1 LTS(x86_64)
运行用户 普通用户(本书中为 claude,组为 dev)
Unity CLI 1.0.0-beta.13(~/.local/bin/unity)
Unity 编辑器 6000.3.24f1 + Linux Build Support (IL2CPP)
Pipeline 包 com.unity.pipeline 0.7.0-exp.1
Claude Code Unity 插件 unity@unity-agent-plugin 0.1.2-beta

本书中,以下路径和名称作为示例使用。请根据您的环境进行替换。

例 意味
/srv/work/unitygame 工作根目录(Claude Code 的会话也在此处启动)
/srv/work/unitygame/tools 编辑器本体、辅助脚本、日志的存放位置
<project> Unity 项目文件夹名(/srv/work/unitygame/<project>)
<editor-version> 项目使用的 Unity 编辑器版本(例:6000.3.24f1)
unitygame Claude Code Remote Control 的实例名(与工作根目录的文件夹名相同)

1. 操作系统准备

1.1 软件包

sudo apt install -y \
  git libxml2-16 libglu1-mesa libopengl0 mesa-utils \
  xvfb dbus-x11 xdotool imagemagick \
  gnome-keyring python3-gi \
  clang lld cmake
软件包 用途
libxml2-16, libglu1-mesa, libopengl0, mesa-utils 运行编辑器所需的库,以及 GL 的运行确认
xvfb, dbus-x11 虚拟显示器和 D-Bus 会话
xdotool, imagemagick 点击没有屏幕的对话框,用 import 截屏
gnome-keyring, python3-gi Unity CLI 的登录信息保存位置及其解锁脚本
clang, lld, cmake 用于 IL2CPP 构建(如果不构建则可省略)

在 Ubuntu 26.04 中,libxml2 的名称是 libxml2-16。在较旧的发行版中,请安装 libxml2。

1.2 用户与目录

sudo mkdir -p /srv/work/unitygame
sudo chown root:dev /srv/work/unitygame && sudo chmod 2775 /srv/work/unitygame
mkdir -p /srv/work/unitygame/tools

1.3 注销后仍让用户服务保持运行

sudo loginctl enable-linger claude
loginctl show-user claude -p Linger    # Linger=yes になればよい

2. Unity CLI

2.1 安装

cd /tmp
curl -fsSL -o unity-cli-install.sh https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh
UNITY_CLI_CHANNEL=beta bash unity-cli-install.sh
~/.local/bin/unity --version
  • 可执行文件会被放入 ~/.local/bin/unity。请将 ~/.local/bin 加入 PATH。
  • 如果安装的是旧版本,可以用 unity self-update -y 更新到最新版。可以用 unity self-update --rollback 回退到上一个版本。

2.2 密钥环(登录信息的保存位置)

Unity CLI 会将登录信息保存到 Secret Service(gnome-keyring)。
在没有图形界面的环境中,启动时没有人来解锁密钥环,因此把密码放在文件里,并用 systemd 解锁。

  1. 创建密码文件(设置为只有本人可读的权限)。

    mkdir -p ~/.config/unity-headless && chmod 700 ~/.config/unity-headless
    (umask 077; printf '<任意のパスワード>' > ~/.config/unity-headless/keyring-pass)
    
  2. 用该密码创建 login 密钥环。

    systemctl --user stop gnome-keyring-daemon.service gnome-keyring-daemon.socket
    cat ~/.config/unity-headless/keyring-pass | gnome-keyring-daemon --daemonize --login --components=secrets,pkcs11
    cat ~/.config/unity-headless/keyring-pass | gnome-keyring-daemon --start --components=secrets,pkcs11
    ls ~/.local/share/keyrings/          # login.keyring ができていればよい
    systemctl --user start gnome-keyring-daemon.socket
    

    在 printf '\0' 等中传入“空密码”时,密码会变成 NUL 字符。结果导致默认密钥环没有被创建,unity auth login 在 KEYRING_WRITE_FAILED 处失败。

  3. 放置解锁脚本 tools/keyring-unlock.py(内容见附录 A)。
    gnome-keyring-daemon --unlock 会启动另一个守护进程,而不是 D-Bus 上已有的守护进程。因此,该脚本通过 D-Bus 调用 UnlockWithMasterPassword 来解锁。

  4. 放置 systemd 单元 unity-keyring-unlock.service(附录 B)。

2.3 登录与许可证

# ブラウザを開かせず、表示された URL を手元のブラウザで開いてサインインする
unset DISPLAY; export BROWSER=/bin/true
unity auth login
unity auth status          # "Signed in as ..." になればよい

# Personal ライセンス(オンライン有効化。Personal はオフライン(.alf)での有効化に対応していない)
unity license activate --personal --accept-eula
unity license status       # License: active
  • unity auth login 在显示 URL 后,会一直等待直到登录结束。从远程执行时,像 script -qfc "unity auth login" auth.log 那样把输出保留到文件中,会更容易确认 URL。
  • 登录状态在重启后仍会保留(前提是完成 2.2 中的解锁)。已在 CLI beta.13 中确认其可跨重启保持。

3. Unity 编辑器

3.1 安装

unity editors install-path /srv/work/unitygame/tools/editors
unity install <editor-version> --list-modules          # モジュール ID を確認する
unity install <editor-version> -m <Linux Build Support (IL2CPP) の ID> --accept-eula -y
unity editors default <editor-version>
unity editors list
  • <editor-version> 要与项目的 ProjectSettings/ProjectVersion.txt 中的 m_EditorVersion 保持一致(4.3)。
  • 通过 Unity Hub 等其他方式安装的编辑器,可以在 unity editors add <エディタのフォルダ> 中注册到 CLI。

3.2 首次启动时的对话框

由于没有图形界面,首次启动时编辑器可能会因模态对话框而卡住。卡住期间,Pipeline 的 API 也不会响应。
迄今为止出现过的对话框如下。

  • 使用条款(EULA)的确认
  • 项目升级确认(升级编辑器版本时)
  • URP 材质升级确认
  • “场景已在外部被更改”

处理流程如下。

export DISPLAY=:99
import -window root /tmp/screen.png                      # 画面を撮って状況を見る
xdotool search --name "Unity" | while read w; do echo "$w $(xdotool getwindowname $w)"; done
xdotool mousemove <x> <y> click 1                         # ボタンをクリック

一旦响应一次,之后就不会再出现。


4. 项目(由 git 管理的 Unity 项目)

这里将让在 GitHub 或 GitLab 等处由 git 管理的现有 Unity 项目能够在此服务器上打开。

4.1 连接到 git 托管服务(SSH)

  1. 创建用于此服务器的密钥,并注册到托管服务一侧。

    ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_<host> -C "<サーバー名>"
    cat ~/.ssh/id_ed25519_<host>.pub
    
    登録先 适用的用途
    仓库的 Deploy Key(如果也要 push,则需赋予写权限) 只需操作该仓库的情况。权限可以最小化
    用户账户的 SSH 密钥 需要处理多个仓库的情况
    • GitHub:仓库的 Settings → Deploy keys(Allow write access)
    • GitLab:仓库的 Settings → Repository → Deploy keys(Grant write permissions)
  2. 写入 ~/.ssh/config。由于 22 端口有时会被封锁,使用各服务面向 443 端口的主机更可靠。

    # GitHub の例
    Host github.com
      HostName ssh.github.com
      Port 443
      User git
      IdentityFile ~/.ssh/id_ed25519_<host>
      IdentitiesOnly yes
    
    # GitLab の例
    Host gitlab.com
      HostName altssh.gitlab.com
      Port 443
      User git
      IdentityFile ~/.ssh/id_ed25519_<host>
      IdentitiesOnly yes
    
  3. 确认连接。GitHub 显示 successfully authenticated、GitLab 显示 Welcome to GitLab 即为成功。

    ssh -T git@github.com     # または git@gitlab.com
    

只有 ssh 无法连接外网的情况
在已有搭建实绩的环境中,曾出现只有 ssh 命令的对外连接超时、而 Python 可以连接到同一目标地址的现象(原因不明)。
这种情况下,在各 Host 中加入下面一行,使其经由 Python 的中继脚本(附录 A.3)。

  ProxyCommand /usr/bin/python3 /srv/work/unitygame/tools/ssh-tcp-relay.py %h %p

无法使用 SSH 时,也可以用 HTTPS 和访问令牌获取。此时,请通过 git credential 的机制管理令牌,不要直接写在仓库的配置文件或 URL 中。

4.2 取得

cd /srv/work/unitygame
git clone git@<host>:<owner>/<repo>.git <project>
cd <project>
git switch <作業ブランチ>          # 必要なら
git lfs install && git lfs pull    # リポジトリが Git LFS を使っている場合(apt で git-lfs を入れておく)
  • 预先分出供 AI 使用的工作分支,就不会与人的作业混在一起,评审也更容易。
  • 提交者的姓名和邮箱不要设为服务器的共用配置,而是按仓库单位(git config user.name ...)或按每次提交(git -c user.name=... -c user.email=... commit)指定,这样更容易区分是谁的改动。

4.3 使编辑器版本保持一致

cat ProjectSettings/ProjectVersion.txt     # m_EditorVersion: <editor-version>
unity editors list                         # 同じバージョンが入っているか
  • 如果没有安装相同版本,请按照 3.1 的步骤安装。
  • 如果使用其他版本打开,会触发项目升级,大量文件将被改写。如需升级,请创建专用分支,确认运行情况后再提交。

4.4 Pipeline 包(用于让 AI 操作编辑器)

unity pipeline install --project-path /srv/work/unitygame/<project>
grep pipeline /srv/work/unitygame/<project>/Packages/manifest.json

Packages/manifest.json(以及 packages-lock.json)会发生变化。是否提交此更改,请根据实际运维情况决定。

方針 利点 欠点
提交并共享 这台服务器不需要特别的管理 其他开发者的编辑器也会加载实验版的包。在 Windows 上有时会出现防火墙确认画面
不提交 不影响其他开发者 在这台服务器上需要一直把 manifest.json 的差异留在本地,每次 pull 时都必须注意冲突

该包只接受来自本机内部的连接,而且必须进行令牌认证,因此无论采用哪种方针,安全性上的差异都几乎不存在(第 8 章)。

4.5 git 管理的前提

在 Unity 项目的 .gitignore 中,请确认至少以下内容已被排除(Unity 官方模板的 .gitignore 中已包含)。

[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ogs/
[Uu]ser[Ss]ettings/
  • Library/ 在 clone 之后并不存在。它会在首次用编辑器打开时生成,根据规模不同可能需要几分钟到几十分钟。
  • Pipeline 的连接信息(Library/Pipeline/)也会创建在 Library/ 中,因此不会进入仓库。
  • UserSettings/ 是每个人各自的设置。请不要共享。

4.6 辅助脚本

将以下文件放到 tools/ 中,并赋予执行权限(内容见附录 A)。

文件 用途
keyring-unlock.py 密钥环解锁(2.2)
wait-editor-ready.sh 等待编辑器返回 Pipeline 的工具列表。改写其中的项目路径
ssh-tcp-relay.py ssh 中继(4.1,仅在必要时)
chmod +x /srv/work/unitygame/tools/*.sh /srv/work/unitygame/tools/*.py

5. 使用 systemd 常驻运行

  1. 将 附录 B 的单元放到 ~/.config/systemd/user/ 中。

  2. 启用并启动。

    systemctl --user daemon-reload
    systemctl --user enable --now unity-xvfb.service unity-keyring-unlock.service unity-editor.service
    systemctl --user enable --now claude-rc@unitygame.service     # Claude Code を入れた後(6 章)
    

启动顺序如下。

  1. Xvfb 与密钥环解锁
  2. 编辑器的启动
  3. 等待 wait-editor-ready.sh 确认工具列表(最长 5 分钟)
  4. Claude Code 的启动

如果 Claude Code 先启动,MCP 的工具就不会被加载。因此,这里采用等待编辑器准备完成后再启动的顺序。

日志的输出位置如下。

  • 编辑器:/srv/work/unitygame/tools/editor.log
  • 各服务:journalctl --user -u <ユニット名>

6. Claude Code

6.1 安装与 Remote Control

curl -fsSL https://claude.ai/install.sh | bash      # ~/.local/bin/claude
claude                                              # 初回のみ対話的にログイン

放置 附录 B 中的 claude-rc@.service 及其 2 个 drop-in 文件并启用(第 5 章)。
实例名(@ 之后的部分)将成为 /srv/work/<名前> 的工作目录。
在同一台机器上可以并行运行其他项目(例如:claude-rc@webapp)。

6.2 Unity 的 MCP 服务器(仅在 unitygame 的会话中启用)

cd /srv/work/unitygame
claude mcp add -s local unity-editor-mcp -- /home/claude/.local/bin/unity mcp --project-path /srv/work/unitygame/<project>
claude mcp list        # unity-editor-mcp: ... ✔ Connected
  • 在 -s local 中注册后,仅在通过 /srv/work/unitygame 启动的会话中有效。
  • 使用 unity mcp configure claude-code 时,会以用户为单位(所有文件夹共通)进行注册。这种情况下,其他项目的会话也会加载 Unity 的工具,请注意。

6.3 Unity 官方插件(技能)

在 Claude Code 中执行以下内容。

/plugin marketplace add Unity-Technologies/unity-agent-plugin
/plugin install unity@unity-agent-plugin

即可使用 unity:unity-cli、unity:ui 等技能。构建实绩为 0.1.2-beta。


7. 動作確認

systemctl --user --no-pager list-units 'unity-*' 'claude-rc*'
journalctl --user -b -u unity-keyring-unlock -u unity-editor | grep -E "unlocked|ready"
#   → "keyring unlocked" と "editor ready: 151 tools"
unity auth status          # Signed in as ...
unity license status       # License: active
unity status               # 7800  ready  /srv/work/unitygame/<project>  <editor-version>
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:7800/api/status   # 401(トークンなしは拒否)で正常

在 Claude Code 的会话中,只要 editor_status 等工具返回 "status": "ready" 即完成。

最后,请重启机器并确认得到相同结果。


8. 已知注意事项

症状 原因与处理方法
unity auth login 为 KEYRING_WRITE_FAILED 没有默认密钥环,或者未解锁。按照 2.2 的步骤创建 login 密钥环
编辑器仍在运行,但 API 无响应 卡在模态对话框中。按照 3.2 的步骤截取屏幕,并用 xdotool 点击
会话中没有 Unity 的工具 Claude Code 比编辑器先启动了。确认是否存在 wait-unity-editor.conf 的 drop-in,然后重启会话
使用 pkill -f <パターン> 把自己的 shell 也一并结束 因为命令行中包含模式字符串的 shell 自身也会被匹配。确认 PID 后再执行 kill
执行 unity license 时,向许可证管理工具的日志 Unsupported protocol version 因为编辑器内置的旧版许可证管理工具不接受 CLI beta.13 的通信方式。CLI 会通过另行启动的工具进行处理,所以结果是正常的
editor.log 中的 Access token is unavailable; failed to update 因为是没有经过 CLI 而直接启动编辑器。许可证之后会正常加载,所以可以忽略
在 Windows 上会出现询问是否允许 Unity 通信的防火墙确认画面 因为 Pipeline 在所有接口上监听。来自外部的连接会由包一侧返回 403,所以选择“不允许”即可

Pipeline 包的安全性

  • 待ち受けは 0.0.0.0:7800 です(Mono の HttpListener の制約)。ただし、次の 3 つで守られています。
    • 拒绝来自非回环地址的连接,返回 403
    • 所有请求都要求 Bearer 令牌(没有则返回 401)
    • 拒绝来自浏览器的带 Origin 的请求
  • 每次启动编辑器时都会重新生成令牌。保存位置为 Library/Pipeline/.unity-pipeline-port(权限 600,不受 git 管理)。

9. 机密信息(不要放在仓库或共享位置的内容)

东西 場所
密钥环密码 ~/.config/unity-headless/keyring-pass(600)
Unity 登录信息 gnome-keyring 的 login 密钥环(~/.local/share/keyrings/)
Unity 许可证 通过 unity license activate 获取(与机器绑定)
用于 git 托管的 SSH 密钥 ~/.ssh/id_ed25519_<host>
Claude Code 的认证 ~/.claude/

附录 A. 辅助脚本

A.1 tools/keyring-unlock.py

#!/usr/bin/env python3
# gnome-keyring-daemon --unlock spawns a separate daemon instead of unlocking the one
# owning the session bus, so unlock through gnome-keyring's internal D-Bus interface.
import sys
import time
from pathlib import Path

from gi.repository import Gio, GLib

SVC = "org.freedesktop.secrets"
ROOT = "/org/freedesktop/secrets"
COLLECTION = f"{ROOT}/collection/login"
PASSWORD = Path.home().joinpath(".config/unity-headless/keyring-pass").read_bytes().strip()


def call(bus, path, iface, method, args, reply_type=None):
    return bus.call_sync(SVC, path, iface, method, args,
                         GLib.VariantType(reply_type) if reply_type else None,
                         Gio.DBusCallFlags.NONE, 10000, None)


def unlock():
    bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
    locked = bus.call_sync(SVC, COLLECTION, "org.freedesktop.DBus.Properties", "Get",
                           GLib.Variant("(ss)", ("org.freedesktop.Secret.Collection", "Locked")),
                           GLib.VariantType("(v)"), Gio.DBusCallFlags.NONE, 10000, None).unpack()[0]
    if not locked:
        return
    _, session = call(bus, ROOT, "org.freedesktop.Secret.Service", "OpenSession",
                      GLib.Variant("(sv)", ("plain", GLib.Variant("s", ""))), "(vo)").unpack()
    call(bus, ROOT, "org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface",
         "UnlockWithMasterPassword",
         GLib.Variant("(o(oayays))", (COLLECTION, (session, b"", PASSWORD, "text/plain"))))


for attempt in range(30):
    try:
        unlock()
        print("keyring unlocked")
        sys.exit(0)
    except GLib.Error as e:
        print(f"attempt {attempt + 1}: {e.message}", file=sys.stderr)
        time.sleep(2)
sys.exit(1)

A.2 tools/wait-editor-ready.sh

#!/usr/bin/env bash
# Exit 0 once the Editor serves its Pipeline tool list (port open alone is too early: the main thread may still be loading).
deadline=$((SECONDS + ${1:-300}))
while (( SECONDS < deadline )); do
  n=$(timeout 20 /home/claude/.local/bin/unity list --project-path /srv/work/unitygame/<project> --format tsv 2>/dev/null | tail -n +2 | wc -l)
  if (( n > 0 )); then
    echo "editor ready: $n tools"
    exit 0
  fi
  sleep 3
done
echo "editor not ready before timeout" >&2
exit 1

A.3 tools/ssh-tcp-relay.py

#!/usr/bin/env python3
"""TCP relay for ssh ProxyCommand.

The ssh binary on some machines cannot open outbound sockets (connect() times out
for every host/port), while Python can reach the same endpoints fine. Relaying the
transport through stdio sidesteps ssh's own connect.
"""
import socket
import sys
import threading

host, port = sys.argv[1], int(sys.argv[2])
sock = socket.create_connection((host, port), timeout=20)
sock.settimeout(None)


def pump(read, write, close):
    try:
        while True:
            chunk = read()
            if not chunk:
                break
            write(chunk)
    finally:
        close()


threading.Thread(
    target=pump,
    args=(lambda: sys.stdin.buffer.read1(65536),
          lambda b: (sock.sendall(b)),
          lambda: sock.shutdown(socket.SHUT_WR)),
    daemon=True,
).start()

pump(lambda: sock.recv(65536),
     lambda b: (sys.stdout.buffer.write(b), sys.stdout.buffer.flush()),
     lambda: None)

附录 B. systemd 用户单元

存放位置全部为 ~/.config/systemd/user/。

unity-xvfb.service

[Unit]
Description=Xvfb virtual display :99 for Unity Editor

[Service]
Type=simple
ExecStart=/usr/bin/Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

unity-keyring-unlock.service

[Unit]
Description=Unlock login keyring for Unity CLI tokens
After=dbus.socket gnome-keyring-daemon.socket

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/python3 /srv/work/unitygame/tools/keyring-unlock.py

[Install]
WantedBy=default.target

unity-editor.service

[Unit]
Description=Unity Editor (<project>) with Pipeline API on port 7800
Requires=unity-xvfb.service
After=unity-xvfb.service unity-keyring-unlock.service
Wants=unity-keyring-unlock.service

[Service]
Type=simple
Environment=DISPLAY=:99
WorkingDirectory=/srv/work/unitygame/<project>
ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do [ -S /tmp/.X11-unix/X99 ] && exit 0; sleep 1; done; exit 1'
ExecStart=/srv/work/unitygame/tools/editors/<editor-version>/Editor/Unity -projectPath /srv/work/unitygame/<project> -logFile /srv/work/unitygame/tools/editor.log
# Started = Pipeline tools listable. Non-fatal and bounded, so units ordered after this never wait more than ~5 min.
ExecStartPost=-/srv/work/unitygame/tools/wait-editor-ready.sh 300
TimeoutStartSec=360
TimeoutStopSec=60
Restart=on-failure
RestartSec=15

[Install]
WantedBy=default.target

claude-rc@.service

[Unit]
Description=Claude Code Remote Control (%i)
[Service]
WorkingDirectory=/srv/work/%i
ExecStart=%h/.local/bin/claude remote-control --name "%i"
Restart=always
RestartSec=10
[Install]
WantedBy=default.target

claude-rc@.service.d/wait-net.conf(所有实例共通:等待名称解析完成)

[Service]
ExecStartPre=/bin/sh -c 'until getent hosts api.anthropic.com >/dev/null; do sleep 2; done'
TimeoutStartSec=300

claude-rc@unitygame.service.d/wait-unity-editor.conf(仅 unitygame:等待编辑器准备完成)

[Unit]
Wants=unity-editor.service
After=unity-editor.service

1

前往点赞用户列表

0

注册为新用户,更便捷地使用 Qiita

  1. 你可以获得符合你需求的文章
  2. 你可以高效地回读有用的信息
  3. 你可以使用深色主题

注册后你可以做什么

来源:Qiita · Unity 开发社区 · qiita.com