Unity跨平台发布失败的根因分析与七步排查法
1. 为什么“跨平台发布”在Unity项目里从来不是点几下Build就完事的事
我第一次把一个Unity项目从Windows本地测试直接拖进Mac上打包iOS时,信心满满地按下了Build按钮——结果卡在“Compiling scripts”阶段整整27分钟,最后弹出一行红色报错:“IL2CPP compilation failed for target ‘iOS’”。当时我盯着控制台里密密麻麻的C++编译错误,手边连一台真机都没有,Xcode版本、SDK路径、架构配置、脚本后端、托管调试符号……全在报错堆栈里搅成一团。那不是技术问题,是信任崩塌现场:你信誓旦旦写的C#逻辑,在另一套系统里根本没机会跑起来。
这就是Unity跨平台发布的真相——它不是“一次开发,到处运行”,而是“一次开发,N次适配,M次踩坑,K次重装环境”。Unity的跨平台能力强大得令人上头,但它的抽象层之下,藏着操作系统内核、硬件指令集、图形API、证书体系、商店审核规则、甚至开发者账号权限状态等十几层现实约束。你写的Input.GetMouseButtonDown(0)在PC上是鼠标左键,在Android上是触摸屏单点,在WebGL里可能压根不触发;你调用的System.IO.File.WriteAllText在桌面端畅通无阻,在iOS沙盒里直接抛出UnauthorizedAccessException;你依赖的UnityEngine.XR命名空间,在Switch或PlayStation上连编译都过不去。
关键词“Unity跨平台发布”背后,实际指向的是构建管道(Build Pipeline)的可控性、目标平台SDK的兼容性、资源与脚本的平台条件编译能力、以及发布流程中不可见但致命的元数据治理。它适合三类人深度参考:独立开发者要上线Steam+App Store+Google Play三端,团队技术负责人要统一多平台CI/CD流程,还有被客户临时要求“加个微信小游戏版”的程序员——别笑,上周真有朋友凌晨三点发我截图,说“微信小游戏导出后白屏,控制台只有一行‘Failed to load script’”。这不是玄学,是Unity WebGL构建链路里JS入口文件生成逻辑和微信引擎加载机制的隐式冲突。
这篇内容不讲“Unity能发布哪些平台”,那是官网文档干的事;也不教你怎么点菜单栏,那是新手教程该干的活。我要带你拆开Unity Build System的外壳,看清楚每一颗螺丝拧在哪、为什么必须这么拧、拧歪了会打滑还是崩牙。从编辑器内部的Platform Switching机制开始,到IL2CPP与Mono后端的本质差异,再到Android AAB签名配置里那个容易被忽略的v1SigningEnabled = false参数,全部基于真实项目复现、实测验证、反复推翻重来的经验沉淀。你不需要记住所有命令,但你会建立起一套判断逻辑:当某个平台构建失败时,第一反应不是重试,而是问自己——这次到底是Unity的锅,还是我漏掉了某层平台契约?
2. 平台切换不是UI操作,而是项目底层架构的实时重编译
很多人以为在Unity Editor顶部菜单栏点一下“File → Build Settings → Platform → iOS → Switch Platform”,就完成了平台切换。错。这一步只是告诉Unity:“接下来我要为iOS准备构建环境”,真正的动作发生在后台——Unity会强制重新加载所有脚本、重解析所有Assembly Definition Files(asmdef)、清空Library/ScriptAssemblies缓存、并根据目标平台重生成C#项目文件(.csproj)。这个过程耗时长短,直接暴露你项目架构的健康度。
2.1 切换平台时Unity到底在做什么?三个不可跳过的底层动作
第一,脚本后端(Scripting Backend)的强制对齐
Unity支持Mono(.NET Framework兼容)和IL2CPP(C++中间码)两种后端。Windows Standalone默认Mono,iOS/macOS强制IL2CPP,Android可选但推荐IL2CPP,WebGL必须IL2CPP。当你从Windows切换到iOS时,Unity不仅会切换后端,还会检查所有引用的DLL是否支持IL2CPP——比如你用了某个第三方插件,其.dll文件只包含x86机器码,没有提供对应的.il2cpp.metadata或.cpp源码,构建就会在“Generating C++ code”阶段失败。我见过最典型的案例是某款旧版JSON解析库,其.NET Standard 2.0 DLL在IL2CPP下无法反射获取属性,导致序列化全崩。解决方案不是换库,而是让插件作者提供源码或IL2CPP兼容版本,或者你自己用#if UNITY_IOS包裹掉相关调用。
第二,API兼容层(API Compatibility Level)的静默降级
Unity的Player Settings里有个“Api Compatibility Level”选项,默认是“.NET Standard 2.1”。但iOS和Android的.NET运行时并不完整支持2.1的所有特性。当你切换到移动端平台时,Unity会自动将实际编译目标降级为“.NET Standard 2.0”,但不会告诉你——它只是默默把Span<T>、Memory<T>等类型替换成兼容实现,而某些高度依赖这些特性的库(如最新版ImageSharp)会直接编译失败。实测发现,如果项目里用了System.Text.Json的JsonSerializerOptions.Default,在iOS上会因缺少IAsyncEnumerable<T>支持而报错。解决方法很反直觉:手动将Api Compatibility Level设为“.NET Standard 2.0”,并在代码里显式禁用不支持的特性,比如用new JsonSerializerOptions { WriteIndented = true }替代JsonSerializerOptions.Default。
第三,平台条件编译(Platform-Dependent Compilation)的全局重刷
Unity通过预处理器指令(如#if UNITY_ANDROID)控制代码分支,但这些指令的生效不是编译时才解析,而是在平台切换瞬间,Unity会扫描整个Assets目录,重新计算每个脚本的“有效平台集合”。这意味着:如果你在某个.cs文件里写了#if UNITY_EDITOR && UNITY_ANDROID,这个组合永远为false(EDITOR和ANDROID互斥),Unity会在切换平台时直接标记该文件为“无效脚本”,并在Console里报黄警告“Script is not compatible with current platform”。更隐蔽的问题是asmdef文件里的Platforms设置——比如你给一个叫“NetworkCore.asmdef”的程序集设置了"Platforms": ["Editor", "Standalone"],那么当你切换到iOS时,整个NetworkCore程序集都不会被包含进构建包,但Editor里调试一切正常,直到你真机测试才发现网络模块完全消失。这种问题极难定位,因为没有任何编译错误,只有运行时NullReferenceException。
提示:每次平台切换后,务必打开Console窗口,筛选“Warning”级别日志。重点关注三类信息:
- “Script is not compatible with current platform”(脚本平台兼容性问题)
- “Assembly definition file 'xxx.asmdef' has no platforms selected”(asmdef平台未配置)
- “The referenced script on this Behaviour is missing!”(脚本丢失,常因条件编译导致)
这些不是可以忽略的提示,而是构建失败的前兆信号。
2.2 真实项目中的平台切换陷阱:一个被低估的Library缓存问题
去年帮一个AR教育项目做多平台适配时,团队在Windows上开发,每周同步一次到Mac做iOS构建。某次切换平台后,iOS构建始终卡在“Building Player”阶段超过40分钟,且CPU占用率飙升到95%。排查过程极其痛苦:我们重装了Unity、重装了Xcode、重置了钥匙串,甚至格式化了Mac硬盘——都没用。最后发现根源在Library/Il2cppBuildCache目录。Unity在IL2CPP构建时,会将C++中间文件缓存于此,但不同Unity版本、不同Xcode版本、甚至不同macOS系统版本生成的缓存文件,存在ABI不兼容。当团队从Unity 2021.3.15f1升级到2021.3.18f1后,旧缓存未清理,新版本尝试复用损坏的.cpp.o文件,导致链接器无限循环。
解决方案极其简单粗暴:每次Unity版本更新、Xcode大版本升级、或平台切换失败超过两次,立即执行以下三步:
- 关闭Unity Editor
- 删除
ProjectName/Library/Il2cppBuildCache整个文件夹 - 删除
ProjectName/Library/ScriptAssemblies文件夹 - 重启Unity,等待重新编译脚本(约2-5分钟)
别嫌麻烦。我统计过,过去三年接手的37个跨平台项目中,有19个的首次构建失败,根源都是缓存污染。这不是玄学,是Unity构建系统为加速而设计的副作用——它假设你不会频繁变更底层工具链,但现实是,我们天天都在变。
2.3 平台切换的正确姿势:建立“平台就绪检查清单”
与其被动排错,不如主动防御。我在所有跨平台项目启动时,都会在项目根目录放一个PLATFORM_READINESS.md文件,内容如下(已精简为可直接复用的模板):
| 检查项 | 检查方式 | 不通过表现 | 解决方案 |
|---|---|---|---|
| 脚本后端一致性 | 查看Player Settings → Configuration → Scripting Backend | iOS/Android平台显示“Mono” | 手动改为“IL2CPP”,检查Console是否有“IL2CPP is not supported”警告 |
| API兼容性匹配 | 查看Player Settings → Other Settings → Api Compatibility Level | 构建时报“Type or namespace 'Span ' could not be found” | 改为“.NET Standard 2.0”,搜索代码中Span<T>/Memory<T>并替换为数组或List |
| asmdef平台白名单 | 用VS Code全局搜索.asmdef文件,检查"Platforms"字段 | 某功能模块在目标平台缺失 | 为对应asmdef添加目标平台,如"Platforms": ["Editor", "Standalone", "iOS", "Android"] |
| 原生插件平台支持 | 检查Plugins文件夹下各子目录(如Android、iOS、Standalone)是否存在对应平台文件 | 构建时报“Plugin 'xxx.dll' is not compatible with current platform” | 删除非目标平台插件,或使用#if UNITY_ANDROID包裹P/Invoke调用 |
| 资源平台条件加载 | 检查Resources.Load或Addressables.LoadAssetAsync调用,确认路径不含平台敏感字符 | Android上资源加载返回null | 避免在路径中使用#if UNITY_IOS拼接字符串,改用Addressables的Group分组管理 |
这个清单不是摆设。每次平台切换前,花90秒逐项核对,比构建失败后花3小时排查快得多。它把模糊的“感觉不对”转化成了可执行、可验证、可追溯的动作。
3. 构建失败的黄金排查链路:从红字报错到根因定位的七步法
Unity构建失败的报错信息,向来以“精准描述现象,刻意隐藏原因”著称。比如最常见的Error: CommandInvokationFailure: Gradle initialization failed.,它没告诉你Gradle在哪、初始化什么、失败是因为JDK版本不对、还是gradle.properties里某行配置错了。我总结了一套经过23个真实项目验证的“七步黄金排查法”,不依赖运气,只依赖逻辑链条。
3.1 第一步:锁定错误发生的具体阶段(Stage-Level Pinpointing)
Unity构建流程分为明确的六个阶段,每个阶段失败,排查方向完全不同:
| 阶段 | 触发位置 | 典型错误特征 | 根因高发区 |
|---|---|---|---|
| Pre-Build Validation | Build Settings窗口点击Build后瞬间 | “Build Failed: No scenes included in build”、“Script compilation error” | 场景未勾选、脚本语法错误、asmdef循环引用 |
| Script Compilation | 控制台出现“Compiling scripts…” | “CS0234: The type or namespace name 'XR' does not exist” | 缺少XR Plugin Management包、平台条件编译遗漏、Package Manager缓存损坏 |
| Asset Import & Processing | 控制台滚动大量“Importing xxx.png” | “Texture 'xxx' is using unsupported texture type” | Texture Type设为“Default”但用于UI,或Android平台未启用ETC2压缩 |
| IL2CPP/Managed Code Generation | 控制台卡在“Generating C++ code…”或“Running il2cpp.exe…” | “il2cpp.exe did not run properly!”、“Failed to resolve reference 'System.Numerics'” | 第三方DLL不支持IL2CPP、.NET Standard版本过高、unsafe代码未启用 |
| Platform-Specific Packaging | 控制台显示“Building Player for Android…” | “Failed to re-package resources.”、“AAPT: error: resource android:attr/lStar not found.” | Android SDK版本与Unity不匹配、build.gradle自定义配置冲突、AndroidManifest.xml格式错误 |
| Post-Build Signing & Finalization | 构建进度条走完但无输出文件 | “Failed to sign APK: jarsigner returned exit code of 1” | Keystore密码错误、Key alias不存在、jarsigner路径配置错误 |
关键技巧:不要一上来就搜报错全文。先看控制台日志里,错误信息出现在哪一行“Progress: X%”之后,再对照上表确定阶段。比如报错前最后一行是Progress: 65%,且日志里有il2cpp.exe字样,基本锁定在第四阶段——这时你该去查DLL兼容性,而不是去翻Android签名文档。
3.2 第二步:提取错误日志中的“唯一标识符”(Fingerprint Extraction)
Unity报错喜欢堆砌冗长路径和无关信息。真正有用的,是那些在全网几乎唯一的字符串。我称之为“错误指纹”。例如:
IL2CPP error for method 'System.Void UnityEngine.XR.ARSubsystems.XRCameraSubsystem::Start()'
→ 指纹是XR.ARSubsystems.XRCameraSubsystem::Start(),说明问题出在AR子系统启动逻辑,而非泛泛的“XR插件问题”。error: resource android:attr/lStar not found.
→ 指纹是android:attr/lStar,这是Android 12(API 31)新增属性,表明你的Android SDK或Gradle插件版本过低,需要升级到Android Gradle Plugin 7.0+。Failed to resolve reference 'System.Numerics'
→ 指纹是System.Numerics,这是.NET Standard 2.1引入的命名空间,说明Api Compatibility Level设得太高,需降级。
操作方法:复制报错信息,粘贴到浏览器搜索框,把路径、时间戳、随机哈希值全部删掉,只保留类名、方法名、属性名、错误码。然后加引号搜索,如"android:attr/lStar"。你会发现,90%的“疑难杂症”,其实早有Stack Overflow答案,只是你被Unity的冗长日志吓退了。
3.3 第三步:回滚到“最后已知良好状态”(Last Known Good State)
当排查陷入僵局,最高效的方法不是继续深挖,而是快速回滚。Unity提供了两个关键回滚锚点:
锚点一:Git Commit回滚
在每次成功构建并验证通过后,我强制要求团队执行:
git add . && git commit -m "BUILD SUCCESS: iOS v1.2.0, Unity 2021.3.18f1"这样,当新功能引入构建失败时,git reset --hard HEAD~1就能瞬间回到可构建状态。注意:这个Commit必须包含完整的Library文件夹(.gitignore里删掉/Library/行),因为Library里存着平台特定的编译产物。
锚点二:Unity Cloud Build历史版本
如果你用Unity Cloud Build,它的Build History页面会保存每次构建的完整日志、输出APK/IPA、甚至构建机环境快照。点击任意一次成功的构建,选择“Rebuild this version”,几秒钟就能拿到一个已验证的安装包。这比本地重装环境快十倍。
注意:回滚不是放弃排查,而是为了建立对比基线。拿到“好版本”后,用Beyond Compare对比Assets和ProjectSettings,往往能一眼看出问题所在——比如某人不小心把
PlayerSettings.Android.minSdkVersion从21改成16,导致Android 12设备无法安装。
3.4 第四步:隔离变量,构造最小可复现案例(Minimal Reproducible Case)
很多问题藏在项目复杂度里。我的标准操作是:新建一个空白Unity项目(相同Unity版本),然后按以下顺序逐步导入:
- 只导入
Packages/manifest.json中声明的官方包(URP、XR Plugin Management等) - 只导入出问题的脚本(如报错提到的
XRCameraSubsystem.cs) - 只导入报错涉及的资源(如
lStar错误,就只导入那个Android XML文件) - 只配置报错相关的Player Settings(如只设置Android SDK路径,其他全默认)
如果这个最小案例依然复现错误,说明问题与项目其他部分无关,可以放心提交给Unity官方论坛;如果不再复现,则说明是项目中某个隐藏依赖(如某个asmdef的Override References设置)在作祟。
去年有个项目,Android构建总在AAPT: error: resource android:attr/lStar失败。按上述步骤,最小案例里只放了AndroidManifest.xml和build.gradle,却怎么也复现不了。最后发现,是项目里一个叫AndroidResMerger.asmdef的程序集,其Override References勾选了UnityEngine.AndroidJNIModule,而这个模块在Unity 2021.3中已被弃用,导致AAPT调用链异常。这种问题,不靠最小案例隔离,根本找不到。
3.5 第五步:检查构建机环境一致性(Environment Consistency Check)
本地能构建成功,CI服务器却失败?90%是环境不一致。我维护一份ENV_CHECKLIST.md,每次配置新CI节点必查:
| 环境项 | 检查命令 | 正确值示例 | 不一致后果 |
|---|---|---|---|
| Unity版本 | unity -version | 2021.3.18f1 | 脚本编译失败、API行为差异 |
| JDK版本 | java -version | openjdk version "11.0.15" | Android构建卡死、jarsigner报错 |
| Android SDK路径 | echo $ANDROID_HOME | /Users/runner/Library/Android/sdk | “SDK not found”错误 |
| NDK版本 | cat $ANDROID_NDK_ROOT/source.properties | grep Pkg.Revision | Pkg.Revision = 23.1.7779620 | IL2CPP链接失败、ARM64崩溃 |
| Xcode版本 | xcode-select -p | /Applications/Xcode_14.2.app/Contents/Developer | iOS构建失败、bitcode错误 |
特别提醒:Unity Cloud Build虽然省心,但它默认的“Unity Version”和“Build Environment”是分离的。你选了Unity 2021.3.18f1,但Build Environment可能还是“macOS 12 + Xcode 13.4”。必须手动在Build Target Settings里,将Environment精确匹配到Xcode 14.2——否则即使Unity版本对,Xcode的SDK也会导致构建失败。
3.6 第六步:启用详细日志,捕获被截断的关键信息(Verbose Logging)
Unity默认日志会截断长路径和堆栈。在命令行构建时,加上-logFile和-verbose参数:
/Applications/Unity/Hub/Editor/2021.3.18f1/Unity.app/Contents/MacOS/Unity \ -batchmode \ -projectPath /path/to/your/project \ -buildTarget Android \ -buildPath /path/to/output/app-release.aab \ -logFile /path/to/log.txt \ -verbose重点看log.txt末尾的stderr部分,那里藏着被Unity UI过滤掉的原始错误。比如AAPT错误,UI里只显示“Failed to re-package resources”,而log.txt里会写出具体哪行XML、哪个属性、哪个SDK版本不支持。
3.7 第七步:终极手段——阅读Unity源码注释(Yes, Really)
Unity虽不开源,但它的官方包(如com.unity.xr.management)是开源的,GitHub地址公开。当报错指向某个Unity内部类(如XRCameraSubsystem),直接去GitHub搜索该类名,看它的OnEnable()、Start()方法里写了什么。你会发现,很多“玄学错误”其实是Unity故意抛出的——比如XRCameraSubsystem.Start()里有一行注释:// Throws if camera permission is not granted on Android,而你的AndroidManifest.xml里忘了加<uses-permission android:name="android.permission.CAMERA"/>。
这招看似硬核,实则高效。我曾用此法30分钟解决一个困扰团队两周的iOS ARKit黑屏问题:源码里发现ARKitSession::Initialize()方法会检查ARWorldTrackingConfiguration.IsSupported,而这个静态属性在模拟器上永远返回false——所以不是代码bug,是必须真机测试。这种信息,Unity文档里不会写,但源码注释里明明白白。
4. 各平台发布核心参数详解:从配置项到商业合规的硬性要求
跨平台发布不是技术通关就结束,每个平台都有自己的“游戏规则”。这些规则不写在Unity文档里,而藏在Apple Developer、Google Play Console、Microsoft Partner Center的审核指南中。我把它们提炼成Unity Player Settings里最关键的12个配置项,并标注其技术原理与商业后果。
4.1 iOS平台:证书、描述文件与Bitcode的三角困局
iOS发布最痛的不是技术,是证书体系。Unity Player Settings里三个关键字段,背后是苹果生态的强管控:
| 配置项 | Unity路径 | 技术原理 | 商业后果 | 实操要点 |
|---|---|---|---|---|
| Bundle Identifier | Player Settings → Publishing Settings → Bundle Identifier | iOS App的唯一身份证,格式com.company.appname,必须与Apple Developer Portal中注册的App ID完全一致 | 不一致→Xcode Archive失败;提交TestFlight时被拒 | 开发期用com.company.appname.dev,上线前改com.company.appname,切勿用通配符ID(*) |
| Team ID | Player Settings → Publishing Settings → Team ID | Apple Developer账号的10位字母数字ID,用于自动签名 | Team ID错误→无法生成Provisioning Profile;Archive时提示“No profiles for 'xxx' were found” | 在Xcode → Preferences → Accounts里查看,或登录developer.apple.com → Membership页面 |
| Enable Bitcode | Player Settings → Publishing Settings → Enable Bitcode | Bitcode是LLVM中间码,苹果要求App Store上传必须开启,但会导致构建时间增加30%,且某些C++插件不兼容 | 关闭→App Store Connect拒绝上传;开启→IL2CPP构建失败率上升 | Unity 2021.3+默认开启,若插件报错,优先尝试升级插件,而非关闭Bitcode |
真实案例:某教育App因第三方人脸识别SDK不支持Bitcode,团队被迫关闭该选项。结果App Store审核被拒,理由是“Your app includes a version of the Bitcode technology that is not supported”。苹果没说错——他们检测到二进制里有Bitcode段,但格式不标准。最终解决方案是:联系SDK厂商获取Bitcode兼容版本,耗时11天。
提示:iOS构建后生成的
.xcarchive文件,双击用Xcode打开,选择“Distribute App” → “App Store Connect”,全程无需手动操作Provisioning Profile。Unity 2020.3+已内置自动签名,前提是Team ID和Bundle ID正确。
4.2 Android平台:AAB格式、Target SDK与签名的合规红线
Google Play强制要求2021年8月起所有新应用必须上传Android App Bundle(.aab)格式,而非APK。这不仅是打包方式变化,更涉及签名、分发、动态交付的整套逻辑。
| 配置项 | Unity路径 | 技术原理 | 商业后果 | 实操要点 |
|---|---|---|---|---|
| Build System | Player Settings → Publishing Settings → Build System | 选“Gradle”(推荐)或“Internal”。Gradle支持AAB、动态功能模块、Play Asset Delivery | 选“Internal”→只能生成APK,无法上传Play Store | 始终选Gradle,Unity 2021.3+已移除Internal选项 |
| Target SDK Version | Player Settings → Publishing Settings → Target SDK Version | 应用声明支持的最高Android API级别。2023年11月起,新应用必须≥33(Android 13) | <33→Play Console拒绝上传;用户在Android 13+设备上可能闪退 | 在Unity Hub里安装Android SDK 33,然后在此处选择 |
| Custom Main Manifest | Player Settings → Publishing Settings → Custom Main Manifest | 替换默认AndroidManifest.xml,用于添加权限、Activity、meta-data | 忘记加<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>→Android 13设备无法通知 | 勾选后,Unity会生成Assets/Plugins/Android/AndroidManifest.xml,在此文件中添加所需配置 |
最关键的是签名配置。Unity不直接管理Keystore,而是通过baseProjectTemplate.gradle注入签名信息。正确做法是:
- 在
Assets/Plugins/Android/下创建mainTemplate.gradle(若不存在) - 在
android { signingConfigs { release { ... } } }块中,填入你的Keystore路径、密码、key alias、key password - 在
buildTypes { release { signingConfig signingConfigs.release } }中启用签名
错误做法:在Player Settings里填Keystore路径——Unity 2021.3+已移除此选项,填了也无效。
4.3 WebGL平台:内存限制、压缩算法与微信小游戏的特殊适配
WebGL不是“网页版Unity”,它是将C#编译为WebAssembly,在浏览器沙盒中运行。其限制比移动端更苛刻。
| 配置项 | Unity路径 | 技术原理 | 商业后果 | 实操要点 |
|---|---|---|---|---|
| Decompression Buffer Size | Player Settings → Publishing Settings → Decompression Buffer Size | WebGL加载时解压.wasm文件的内存缓冲区大小,默认128MB | 过小→加载卡死;过大→Chrome报“Out of memory” | 项目资源<100MB设256MB,>100MB设512MB,实测最稳 |
| Compression Format | Player Settings → Publishing Settings → Compression Format | 选“Brotli”(推荐)或“Gzip”。Brotli压缩率高30%,但需服务器支持Content-Encoding: br | 选Gzip→包体大30%,加载慢;服务器不支持Brotli→白屏 | Nginx配置:add_header Content-Encoding br;,Apache需mod_brotli |
| WebGL Memory Size | Player Settings → Publishing Settings → WebGL Memory Size | WASM线程可用内存上限,默认256MB | 过小→运行时OutOfMemoryException;过大→Chrome限制无法分配 | 大多数项目256MB足够,AR项目建议512MB |
微信小游戏是WebGL的“魔改版”。它要求:
- 所有资源必须放在
res/目录下(Unity默认是StreamingAssets/) - JS入口文件必须命名为
game.js(Unity生成的是Build/xxx.js) - 必须禁用
WebGLExceptionSupport(否则微信引擎报错)
解决方案:用Unity的IPostProcessBuildWithReport接口,在构建后自动重命名、移动文件、修改HTML模板。代码片段如下:
public class WeChatPostProcessor : IPostProcessBuildWithReport { public void OnPostProcessBuild(BuildReport report) { if (report.summary.platform == BuildTarget.WebGL) { string buildPath = report.summary.outputPath; // 将StreamingAssets移到res/ string streamingPath = Path.Combine(buildPath, "StreamingAssets"); string resPath = Path.Combine(buildPath, "res"); Directory.Move(streamingPath, resPath); // 修改index.html,加载game.js string htmlPath = Path.Combine(buildPath, "index.html"); string html = File.ReadAllText(htmlPath); html = html.Replace("Build/xxx.js", "game.js"); File.WriteAllText(htmlPath, html); } } }4.4 Windows/macOS Standalone:分辨率、DPI适配与Mac签名的硬性门槛
Standalone看似简单,实则暗坑最多。特别是macOS,自2019年起强制要求App必须用Apple Developer ID签名并公证(Notarization),否则用户下载后无法打开。
| 配置项 | Unity路径 | 技术原理 | 商业后果 | 实操要点 |
|---|---|---|---|---|
| Display Resolution | Player Settings → Resolution and Presentation | 设置默认分辨率、全屏模式、宽高比 | 分辨率设为“Default Fullscreen”→Mac上可能拉伸变形 | 推荐设“Run In Background”+“Default Resolutions”列表,让用户自选 |
| Mac App Sandbox | Player Settings → Publishing Settings → Mac App Sandbox | macOS沙盒机制,限制App访问文件系统、网络、摄像头 | 关闭→无法通过公证;开启→Application.persistentDataPath指向沙盒目录 | 必须开启,所有文件读写用Application.persistentDataPath,勿用绝对路径 |
| Code Sign Identity | Player Settings → Publishing Settings → Code Sign Identity | macOS签名证书名称,必须与钥匙串中“Developer ID Application”证书一致 | 名称错误→Xcode签名失败;无证书→无法公证 | 在钥匙串中查看证书名称,精确复制,包括空格和括号 |
公证(Notarization)流程:构建后得到.app文件 → 用codesign命令签名 → 用altool上传至Apple Notary Service → 等待邮件通知 → 用stapler命令将公证票证钉入App。整个流程可脚本化,我封装了一个notarize.sh,10秒完成。
5. CI/CD自动化:用GitHub Actions实现一键跨平台构建与分发
手工点Build按钮的时代已经结束。一个成熟的跨平台项目,必须有自动化流水线。我用GitHub Actions实现了Unity项目的全自动构建、测试、签名、上传,覆盖iOS、Android、WebGL三端,平均构建时间iOS 12分钟、Android 8分钟、WebGL 3分钟。
5.1 流水线设计哲学:分离构建与分发,避免单点故障
很多团队把所有事塞进一个Workflow,结果iOS证书过期,整个流水线瘫痪。我的设计是:
- Build Workflow:只负责编译出原始包(.ipa/.aab/.zip),不碰任何敏感凭证
- Sign & Distribute Workflow:接收Build产出,用Secrets注入证书,执行签名、公证、上传
这样,证书更新只需改Sign Workflow,不影响Build;Unity版本升级只需改Build Workflow,不影响分发。
5.2 核心Action:unity-builder与setup-unity的取舍
GitHub Marketplace有多个Unity Action,我只用两个:
game-ci/unity-builder@v3:专为Unity优化,支持缓存Library、增量构建、自动处理Unity Hub版本webbertakken/setup-unity@v2:轻量级,只安装Unity,适合需要精细控制环境的场景
推荐unity-builder,配置简洁:
- name: Build iOS uses: game-ci/unity-builder@v3 with: githubToken: ${{ secrets.GITHUB_TOKEN }} unityVersion: 2021.3.18f1 targetPlatform: iOS buildMethod: BuildScript.BuildIOS projectPath: . artifactsPath: ./Build/iOS关键参数buildMethod指向你项目里的静态方法,如:
public static class BuildScript { public static void BuildIOS() { string[] scenes = { "Assets/Scenes/Main.unity" }; BuildPipeline.BuildPlayer(scenes, "Build/iOS", BuildTarget.iOS, BuildOptions.AutoRunPlayer); } }5.3 安卓AAB自动上传Play Store:Service Account与Fastlane的无缝集成
上传AAB到Play Store,不能用个人Google账号,必须用Service Account。步骤:
- Google Cloud Console创建Service Account,下载JSON密钥
- Play Console → Setup → API access,关联Service Account
- 在GitHub Secrets里存入
PLAY_STORE_JSON_KEY(Base64编码后的JSON内容)
Workflow中用Fastlane自动上传:
- name: Upload to Play Store uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_JSON_KEY }} packageName: com.company.appname releaseFiles: ./Build/Android/app-release.aab track: internal userFraction: 0.1track: internal表示上传到内部测试轨道,userFraction: 0.1表示10%用户收到更新。Fastlane会自动处理版本号递增、变更日志、多语言支持。
5.4 iOS自动公证与TestFlight分发:Xcode CLI与notarytool的组合拳
iOS公证流程在CI中必须自动化。关键步骤:
- 用
xcode-command-line-tools安装Xcode命令行工具 - 用
codesign签名.app文件 - 用
notarytool上传公证请求 - 用
stapler钉入公证票证 - 用
xcodebuild打包.ipa并上传TestFlight
全部封装在shell脚本中,GitHub Actions调用:
- name: Notarize and Upload iOS run: | chmod +x ./scripts/notarize-ios.sh ./scripts/notarize-ios.sh env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} TEAM_ID: ${{ secrets.TEAM_ID }}notarize-ios.sh核心逻辑:
# 1. 签名 codesign --force --deep --