在无头模式的 Ubuntu 26.04 上,让 AI(Claude Code)使用 Unity
ヘッドレスの Ubuntu 26.04 で、AI(Claude Code)に Unity を使わせる
前言
※本文由 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 解锁。
-
创建密码文件(设置为只有本人可读的权限)。
mkdir -p ~/.config/unity-headless && chmod 700 ~/.config/unity-headless (umask 077; printf '<任意のパスワード>' > ~/.config/unity-headless/keyring-pass) -
用该密码创建
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处失败。 -
放置解锁脚本
tools/keyring-unlock.py(内容见附录 A)。
gnome-keyring-daemon --unlock会启动另一个守护进程,而不是 D-Bus 上已有的守护进程。因此,该脚本通过 D-Bus 调用UnlockWithMasterPassword来解锁。 -
放置 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)
-
创建用于此服务器的密钥,并注册到托管服务一侧。
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)
-
写入
~/.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 -
确认连接。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 常驻运行
-
将 附录 B 的单元放到
~/.config/systemd/user/中。 -
启用并启动。
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 章)
启动顺序如下。
- Xvfb 与密钥环解锁
- 编辑器的启动
-
等待
wait-editor-ready.sh确认工具列表(最长 5 分钟) - 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
前往点赞用户列表
0
注册为新用户,更便捷地使用 Qiita
- 你可以获得符合你需求的文章
- 你可以高效地回读有用的信息
- 你可以使用深色主题
来源:Qiita · Unity 开发社区 · qiita.com