☰
Unity命令行打包实战:bat与shell脚本驱动自动化构建
2026/10/14 6:56:43 网站建设 项目流程

1. 为什么要在Unity项目里引入命令行打包

做过Unity项目的人都知道,打包这件事本身并不复杂,点几下菜单就能出包。但一旦进入团队协作或者持续集成的环节,手动点菜单就变成了一个巨大的瓶颈。我经历过最夸张的一次,一个项目要同时出Windows、macOS、Linux三个平台的包,每次版本迭代都要重复一遍“切平台、等编译、改输出目录、压缩、上传”的流程,一个下午就没了。后来我把这套流程全部交给命令行脚本,打包时间从原来的四十分钟压缩到十分钟以内,而且再也不用手动干预。

这篇文章要聊的,就是怎么用bat 脚本(Windows 批处理)和shell 脚本(macOS/Linux 下的命令行脚本)来驱动 Unity 的 exe 打包流程。核心思路其实一句话就能说清楚:Unity 提供了命令行调用接口,我们可以通过-executeMethod参数指定一个静态方法,让 Unity 在启动时自动执行打包逻辑,而 bat 和 shell 脚本负责在外部调度这个调用、传递参数、处理输出。听起来简单,但真正落地的时候,参数怎么传、路径怎么处理、返回值怎么判断、日志怎么收集,每一个环节都有坑。

这篇文章适合谁看?如果你是一个人做独立游戏,手动打包完全够用,可能不需要这套东西。但如果你在一个小团队里,或者你正在搭建自动化构建流程,又或者你只是单纯想解放双手、不想每次发版都重复机械操作,那这套方案就非常值得花时间研究。我会从整体设计思路讲起,然后拆解核心实现细节,再给出完整的实操步骤和踩坑记录,尽量让不同基础的人都能照着做出来。

2. 整体方案设计与核心思路拆解

2.1 命令行打包的基本原理

Unity 的命令行调用机制其实很朴素。当你用命令行启动 Unity 可执行文件时,可以附加一系列参数,其中最关键的是-batchmode、-quit和-executeMethod。-batchmode告诉 Unity 以无界面模式运行,不弹窗口;-quit表示执行完指定方法后自动退出;-executeMethod则指定一个静态方法作为入口,Unity 会在项目加载完成后调用它。

这个静态方法通常放在 Editor 目录下的某个脚本里,比如BuildScript.cs。方法内部调用BuildPipeline.BuildPlayer来完成实际的打包。外部脚本要做的,就是拼出正确的命令行、启动 Unity 进程、等待它结束、检查退出码和日志。

为什么选择 bat 和 shell 而不是 Python 或 Node?原因很实际:bat 和 shell 是操作系统自带的,不需要额外安装运行时环境。在 CI 机器上,少一个依赖就少一个故障点。而且 bat 和 shell 跟系统层面的交互最直接,调用 Unity、处理文件、压缩、上传,都是它们擅长的领域。当然,如果你团队里所有人都熟悉 Python,用 Python 写调度层也完全没问题,核心逻辑是一样的。

2.2 为什么要把打包逻辑拆成两层

这里有一个很重要的设计决策:打包逻辑到底放在 Unity 的 C# 脚本里,还是放在外部脚本里?我的做法是拆成两层,外部脚本负责调度和环境准备,C# 脚本负责 Unity 内部的构建操作。

这样拆的好处很明显。外部脚本处理的是“什么时候打包、打什么平台、输出到哪里、打包后做什么”,这些是流程层面的东西,跟 Unity 本身无关。C# 脚本处理的是“怎么设置 PlayerSettings、怎么收集场景、怎么调用 BuildPipeline”,这些是 Unity 特有的逻辑。两层各司其职,修改其中一层不会影响另一层。

举个例子,如果哪天要把输出从本地目录改成上传到某个存储服务,只需要改外部脚本,C# 脚本完全不用动。反过来,如果 Unity 升级导致 BuildPipeline 的 API 变了,也只需要改 C# 脚本,外部调度逻辑不受影响。这种解耦在长期维护中会省下大量时间。

2.3 参数传递的设计考量

外部脚本调用 Unity 时,需要把一些信息传进去,比如目标平台、输出路径、版本号、是否开启调试模式等。Unity 命令行本身支持自定义参数,格式是-key value,然后在 C# 脚本里通过Environment.GetCommandLineArgs()读取。

我见过两种参数传递风格。一种是把所有参数都拼在命令行里,另一种是外部脚本生成一个配置文件,Unity 脚本去读这个文件。两种方式各有优劣。命令行参数更直接,但参数多了之后命令行会变得很长,而且不同平台对命令行长度有限制。配置文件方式更灵活,但多了一步文件读写,而且要注意路径和编码问题。

我的选择是混合使用:核心参数(平台、输出路径)走命令行,因为它们是每次打包都可能变化的;固定配置(公司名、产品名、图标路径)走配置文件或者直接写在 C# 脚本里,因为它们很少变。这样既保持了灵活性,又不会让命令行过于臃肿。

3. 核心细节解析与实操要点

3.1 Unity 端 C# 打包脚本的关键实现

先来看 Unity 这边的核心脚本。这个脚本必须放在Assets/Editor目录下,因为BuildPipeline是 Editor 专属的 API,打包到运行时程序集里会报错。

using UnityEditor; using UnityEngine; using System; using System.Linq; public class BuildScript { static string[] GetScenes() { return EditorBuildSettings.scenes .Where(s => s.enabled) .Select(s => s.path) .ToArray(); } public static void BuildWindows() { string outputPath = GetArg("-outputPath"); if (string.IsNullOrEmpty(outputPath)) { outputPath = "Build/Windows/Game.exe"; } BuildPlayerOptions options = new BuildPlayerOptions(); options.scenes = GetScenes(); options.locationPathName = outputPath; options.target = BuildTarget.StandaloneWindows64; options.options = BuildOptions.None; BuildReport report = BuildPipeline.BuildPlayer(options); if (report.summary.result == BuildResult.Succeeded) { Debug.Log("BUILD_SUCCESS: " + outputPath); EditorApplication.Exit(0); } else { Debug.LogError("BUILD_FAILED: " + report.summary.result); EditorApplication.Exit(1); } } static string GetArg(string name) { string[] args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { if (args[i] == name && i + 1 < args.Length) { return args[i + 1]; } } return null; } }

这段代码有几个关键点值得展开说。第一,GetScenes方法从EditorBuildSettings里读取已启用的场景,而不是硬编码场景路径。这样在 Build Settings 里调整场景列表后,打包脚本不需要跟着改。第二,BuildPlayerOptions的locationPathName对于 Windows 平台来说,应该指向.exe文件的完整路径,Unity 会自动生成同名的_Data文件夹。第三,构建完成后根据BuildReport的结果决定退出码,0 表示成功,1 表示失败,外部脚本可以根据退出码判断打包是否成功。

注意:EditorApplication.Exit的调用非常重要。如果不调用它,Unity 在 batchmode 下可能不会自动退出,导致外部脚本一直等待。虽然-quit参数理论上会让 Unity 在 executeMethod 完成后退出,但在某些版本下行为不一致,显式调用 Exit 更保险。

3.2 Windows 端 bat 脚本的编写要点

bat 脚本的核心任务是找到 Unity 的安装路径、拼出命令行、启动进程、检查结果。下面是一个可用的模板:

@echo off setlocal enabledelayedexpansion set UNITY_PATH=C:\Program Files\Unity\Hub\Editor\2022.3.10f1\Editor\Unity.exe set PROJECT_PATH=%~dp0.. set OUTPUT_PATH=%PROJECT_PATH%\Build\Windows\Game.exe set LOG_PATH=%PROJECT_PATH%\Build\build.log if not exist "%UNITY_PATH%" ( echo Unity not found at %UNITY_PATH% exit /b 1 ) if not exist "%PROJECT_PATH%\Assets" ( echo Project path invalid: %PROJECT_PATH% exit /b 1 ) echo Starting build... "%UNITY_PATH%" -batchmode -quit -projectPath "%PROJECT_PATH%" -executeMethod BuildScript.BuildWindows -outputPath "%OUTPUT_PATH%" -logFile "%LOG_PATH%" set EXIT_CODE=%ERRORLEVEL% echo Build finished with exit code: %EXIT_CODE% if %EXIT_CODE% neq 0 ( echo Build failed. Check log: %LOG_PATH% exit /b %EXIT_CODE% ) echo Build succeeded: %OUTPUT_PATH% exit /b 0

这里有几个细节需要特别注意。%~dp0表示脚本所在的目录,%~dp0..就是上一级目录,也就是项目根目录。这样脚本放在项目里的任何位置都能正确找到项目路径。-logFile参数把 Unity 的日志输出到指定文件,方便打包失败时排查问题。如果不指定,日志会输出到默认位置,找起来比较麻烦。

ERRORLEVEL是 bat 脚本里判断上一条命令退出码的方式。Unity 正常退出时返回 0,异常退出时返回非零值。但这里有个坑:某些 Unity 版本在 batchmode 下即使打包失败也可能返回 0,所以不能完全依赖退出码,还要检查日志里是否有BUILD_SUCCESS或BUILD_FAILED标记。

3.3 macOS/Linux 端 shell 脚本的编写要点

shell 脚本的整体逻辑跟 bat 类似,但语法和路径处理方式不同:

#!/bin/bash set -e UNITY_PATH="/Applications/Unity/Hub/Editor/2022.3.10f1/Unity.app/Contents/MacOS/Unity" PROJECT_PATH="$(cd "$(dirname "$0")/.." && pwd)" OUTPUT_PATH="$PROJECT_PATH/Build/macOS/Game.app" LOG_PATH="$PROJECT_PATH/Build/build.log" if [ ! -f "$UNITY_PATH" ]; then echo "Unity not found at $UNITY_PATH" exit 1 fi if [ ! -d "$PROJECT_PATH/Assets" ]; then echo "Project path invalid: $PROJECT_PATH" exit 1 fi echo "Starting build..." "$UNITY_PATH" -batchmode -quit \ -projectPath "$PROJECT_PATH" \ -executeMethod BuildScript.BuildMac \ -outputPath "$OUTPUT_PATH" \ -logFile "$LOG_PATH" EXIT_CODE=$? echo "Build finished with exit code: $EXIT_CODE" if [ $EXIT_CODE -ne 0 ]; then echo "Build failed. Check log: $LOG_PATH" exit $EXIT_CODE fi echo "Build succeeded: $OUTPUT_PATH"

set -e让脚本在遇到错误时立即退出,避免错误累积。$(cd "$(dirname "$0")/.." && pwd)是获取脚本所在目录的上一级目录的绝对路径,比相对路径更可靠。macOS 上 Unity 的可执行文件在.app包内部,路径跟 Windows 不同,需要根据实际安装位置调整。

提示:在 macOS 上,如果 Unity 是通过 Hub 安装的,路径通常是/Applications/Unity/Hub/Editor/<版本号>/Unity.app/Contents/MacOS/Unity。如果手动安装的,路径可能不同,用mdfind或者直接去 Applications 目录里找一下。

4. 完整实操流程与关键环节实现

4.1 环境准备与路径确认

在开始写脚本之前,先把环境理清楚。你需要确认三件事:Unity 的安装路径、项目的根目录路径、期望的输出路径。这三个路径在脚本里都会用到,任何一个搞错都会导致打包失败。

Unity 安装路径的确认方法:Windows 上打开 Unity Hub,在“安装”标签页里可以看到每个版本的安装位置,点击齿轮图标选择“在资源管理器中显示”就能定位到Unity.exe。macOS 上在 Finder 里进入/Applications/Unity/Hub/Editor/目录,找到对应版本,右键点击Unity.app选择“显示包内容”,然后进入Contents/MacOS/就能看到Unity可执行文件。

项目根目录的确认方法:项目根目录是包含Assets、ProjectSettings、Packages这三个文件夹的目录。脚本里用%~dp0..或者$(cd "$(dirname "$0")/.." && pwd)来获取,前提是脚本放在项目内的某个子目录里,比如Tools/build.bat或Scripts/build.sh。

输出路径的确认方法:Windows 平台输出到.exe文件,比如Build/Windows/Game.exe。macOS 平台输出到.app文件夹,比如Build/macOS/Game.app。Linux 平台输出到一个可执行文件,比如Build/Linux/Game.x86_64。输出目录如果不存在,Unity 会自动创建,但父目录必须存在,否则可能报错。

4.2 编写并放置 C# 打包脚本

在 Unity 项目的Assets/Editor目录下新建一个 C# 脚本,命名为BuildScript.cs。把前面给出的代码复制进去,然后根据你的项目情况做调整。

如果你的项目需要打多个平台,可以在同一个脚本里写多个静态方法,比如BuildWindows、BuildMac、BuildLinux,每个方法设置不同的BuildTarget和输出路径。外部脚本通过-executeMethod BuildScript.BuildWindows这样的方式指定调用哪个方法。

如果你需要在打包前做一些准备工作,比如修改版本号、切换宏定义、清理旧文件,可以在BuildPlayer之前插入相应的代码。比如修改版本号:

PlayerSettings.bundleVersion = GetArg("-version") ?? PlayerSettings.bundleVersion;

这样外部脚本就可以通过-version 1.2.3来指定版本号,不需要手动去 ProjectSettings 里改。

4.3 编写外部调度脚本

根据你的操作系统,选择 bat 或 shell 脚本。脚本放在项目根目录下的Tools文件夹里,命名为build_windows.bat或build_mac.sh。

脚本里的路径要根据实际情况修改。特别是UNITY_PATH,一定要指向你本机安装的 Unity 版本。如果团队里不同成员的 Unity 安装路径不一样,可以考虑把路径提取到一个单独的配置文件里,或者通过环境变量传入。

脚本写好后,先手动运行一次,观察输出。Windows 上双击 bat 文件或者在命令行里执行,macOS/Linux 上先chmod +x build_mac.sh赋予执行权限,然后./build_mac.sh运行。

4.4 验证打包结果与日志检查

打包完成后,先检查输出目录里是否有预期的文件。Windows 平台应该有.exe文件和同名的_Data文件夹。macOS 平台应该有.app文件夹。Linux 平台应该有可执行文件。

然后检查日志文件。日志里会记录 Unity 启动、项目加载、脚本编译、资源导入、构建过程等所有信息。如果打包失败,日志里通常会有明确的错误信息,比如脚本编译错误、资源丢失、平台模块未安装等。

一个常见的失败原因是 Unity 没有安装目标平台的构建模块。比如要打 Windows 包,但 Unity 安装时没有勾选 Windows Build Support。这种情况下日志里会提示缺少模块,需要回到 Unity Hub 里补充安装。

另一个常见原因是脚本编译错误。如果 C# 脚本里有语法错误,Unity 在 batchmode 下会编译失败,然后executeMethod找不到指定的方法,打包直接中断。所以每次修改 C# 脚本后,最好先在 Unity 编辑器里确认没有编译错误,再跑命令行打包。

5. 常见问题与排查技巧实录

5.1 打包失败但退出码为 0

这是最让人头疼的情况。脚本显示打包成功,但输出目录里什么都没有,或者文件不完整。原因通常是 Unity 在 batchmode 下某些错误不会导致非零退出码。

排查方法:在 C# 脚本里显式检查BuildReport的结果,如果失败就调用EditorApplication.Exit(1)。同时在日志里搜索BUILD_SUCCESS和BUILD_FAILED标记,外部脚本根据标记判断而不是只看退出码。

5.2 路径中包含空格导致命令解析失败

Windows 上如果项目路径或 Unity 路径包含空格,命令行参数必须用引号包裹。bat 脚本里用"%UNITY_PATH%"和"%PROJECT_PATH%"这样的形式。shell 脚本里用"$UNITY_PATH"和"$PROJECT_PATH"。

但引号本身也可能带来问题。比如-executeMethod BuildScript.BuildWindows这个参数不需要引号,如果加了引号反而可能导致 Unity 解析错误。原则是:路径类参数加引号,方法名和简单值不加引号。

5.3 Unity 进程卡住不退出

有时候 Unity 在 batchmode 下会卡住,既不输出日志也不退出。常见原因是某个脚本在executeMethod里进入了死循环,或者弹出了模态对话框等待用户输入。

排查方法:先看日志最后输出到哪里,定位到卡住的位置。如果是对话框问题,检查代码里是否有EditorUtility.DisplayDialog之类的调用,在 batchmode 下应该跳过这些交互。如果是死循环,那就只能改代码了。

预防措施:在外部脚本里加超时机制。bat 脚本可以用timeout命令配合start /wait,shell 脚本可以用timeout命令。超过指定时间还没结束就强制终止 Unity 进程,并标记为失败。

5.4 不同 Unity 版本的行为差异

Unity 的命令行接口在不同版本之间有一些细微差异。比如-quit参数在某些版本下不会在executeMethod完成后立即退出,需要显式调用EditorApplication.Exit。又比如BuildReport的 API 在 2018 版本之后才有,更早的版本需要用BuildPipeline.BuildPlayer的返回值来判断。

应对策略:在脚本里做版本判断,或者干脆统一团队使用的 Unity 版本。如果必须支持多个版本,把版本相关的逻辑封装成条件编译,比如#if UNITY_2018_1_OR_NEWER。

5.5 常见问题速查表

问题现象可能原因排查方法解决方案
打包失败但退出码为0Unity batchmode 退出码不可靠检查日志中是否有 BUILD_FAILEDC# 脚本显式 Exit(1),外部脚本检查日志标记
路径含空格导致失败命令行参数未加引号检查脚本中路径变量是否用引号包裹路径类参数统一加双引号
Unity 进程卡住不退出死循环或模态对话框查看日志最后输出位置加超时机制,移除 batchmode 下的交互调用
提示缺少平台模块Unity 未安装对应 Build Support日志中搜索 "module" 或 "support"通过 Unity Hub 补充安装
脚本编译错误导致打包中断C# 脚本有语法错误日志中搜索 "error CS"先在编辑器里确认无编译错误
输出目录为空输出路径配置错误检查 locationPathName 参数确认路径指向正确的文件而非目录

实操心得:我习惯在打包脚本里加一个“预检查”环节,在调用 Unity 之前先检查 Unity 路径是否存在、项目路径是否有效、输出目录的父目录是否可写。这些检查花不了几秒钟,但能在打包失败时快速定位问题,避免浪费时间在 Unity 启动和项目加载上。

6. 进阶技巧与流程优化

6.1 多平台批量打包

如果每次发版都需要出多个平台的包,可以写一个总控脚本,依次调用各个平台的打包脚本。Windows 上用 bat 的call命令,macOS/Linux 上用 shell 的函数调用。

总控脚本的好处是可以统一管理版本号、输出目录、日志收集。比如先设置一个VERSION变量,然后传给每个平台的打包脚本,确保所有平台的包版本一致。

6.2 打包后自动压缩与归档

打包完成后,通常需要把输出文件压缩成一个压缩包,方便分发。Windows 上可以用tar命令(Windows 10 之后自带)或者调用 7-Zip。macOS/Linux 上直接用tar或zip。

压缩时注意排除不必要的文件,比如日志、调试符号。如果输出目录里有大量小文件,压缩过程可能会比较慢,可以考虑用-mx参数调整压缩级别,在速度和体积之间取平衡。

6.3 与版本管理工具结合

如果项目使用 Git 管理代码,可以在打包脚本里自动获取当前提交的哈希值和分支名,写入版本号或者输出文件名。这样每个包都能追溯到对应的代码版本,排查问题时非常有用。

获取 Git 信息的命令:git rev-parse --short HEAD获取短哈希,git rev-parse --abbrev-ref HEAD获取分支名。在 bat 和 shell 里都可以通过命令替换的方式获取。

6.4 日志的收集与分析

每次打包都会生成日志,时间长了日志文件会占用大量空间。建议在脚本里加一个清理逻辑,只保留最近 N 次的日志,或者按日期归档。

日志分析方面,可以写一个简单的脚本,在打包失败时自动提取日志中的错误行,输出到控制台或者发送通知。这样不需要每次都打开日志文件从头翻到尾。

7. 我踩过的坑与个人体会

第一次写这套脚本的时候,我以为把命令行拼对就行了,结果在实际运行中遇到了各种意想不到的问题。最典型的一次是路径里的空格导致 Unity 找不到项目,日志里只显示“项目路径无效”,排查了半天才发现是引号的问题。还有一次是 Unity 版本升级后-quit参数行为变了,打包完成后进程不退出,CI 任务一直挂在那里,最后加了显式的EditorApplication.Exit才解决。

另一个深刻的体会是:日志一定要输出到文件。早期我依赖控制台输出,但在 CI 环境里控制台输出经常被截断或者丢失,打包失败后完全不知道发生了什么。后来强制要求-logFile参数,所有日志落盘,排查效率提升了很多。

还有一点是关于退出码的。不要完全信任 Unity 的退出码,它有时候会骗你。最可靠的方式是在 C# 脚本里显式控制退出码,并且在日志里打上明确的成功/失败标记,外部脚本根据标记来判断。这套组合拳打下来,打包流程的可靠性会高很多。

最后分享一个小技巧:如果你的项目比较大,打包时间很长,可以在脚本里加一个进度提示。比如每完成一个阶段就输出一行日志,这样在 CI 上能看到打包进行到哪一步了,不至于干等着不知道卡在哪里。这个习惯在排查超时问题时特别有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询