编辑器正常、手机上一片黑
为什么 Unity 游戏构建到手机上就黑屏?
真机上的黑屏其实是四个 bug 共用一张脸:启动场景没进 Build Scenes、场景里没有启用中的相机、一张铺满全屏的 Image 盖住一切,或者 shader 在设备上跑不起来。先用六十秒按声音和耗时把它们分开,再让 adb logcat -s Unity 直接说出原因。
手机上的黑屏不是一个 bug,而是四个从外面看长得一模一样的问题。其中三个,你不用打开任何文件就能分开:声音还在不在放、画面几秒后会不会自己回来、logcat 有没有说话。先确定你中的是哪一个,再只修那一个,别凭感觉构建十次。
1 — 先把黑屏分成三种
在自家 logo 之后,Unity 本身也会画一两秒黑屏,而手机走到第一个 scene 比你的 editor 慢得多。改任何东西之前,先花十秒看清楚你手里到底是哪种黑。每一种对应的原因清单都不一样。
一直是黑的、第一个 scene 始终没出现:要么手机启动进的不是你以为的那个,要么那里根本没东西在被渲染。见第 2、3 节。
黑了几秒之后游戏出来了:正常的启动,只是前面没有加载画面挡着。手机存储和冷启动的 IL2CPP 都比 editor 的缓存慢,而那段你从来没等过。
音乐还在放、屏幕却是黑的:逻辑活着,只是画面没了。这是相机、盖在上面的 UI,或者设备拒绝运行的 shader —— 第 3、4、5 节。
2 — 第一个 scene 没进构建
没在 Build Scenes 列表里的 scene,就不在游戏里。不在 APK 里、不在手机上、用名字也叫不到 —— 跑构建包时那个文件就是不存在。'编辑器里好好的、手机上一片黑'这句话之所以存在,根源大多就是这一块面板。
- 1
打开 (Unity 6 之前叫 Build Settings),选中平台,看 Scene List。游戏能走到的每个 scene 都得在里面 —— 不只开局那一个。
- 2
index 0 那一行就是构建包启动进入的 scene。editor 无视这个数字,只打开你上次停在面前的那个 —— 所以这个 bug 只在真机上现身。
- 3
每个 scene 都显式加进去,再把菜单或启动 scene 拖到 index 0。你没加的那个,照样能在 editor 里打开着、看起来毫无问题。
如果代码调用 LoadScene 去加载不在列表里的名字,Unity 只记一行错误,加载根本不会发生 —— 静悄悄,没有弹窗也没有异常。而代码之前做过的事全都还留着:藏起来的菜单、关掉的相机、销毁的 object。你看到的不是渲染失败,是一次从没到的加载留下的残骸。
先证明到底开了哪个 scene
一行代码就能终结这一整类瞎猜。在每个 scene 里挑一个物体,在 Awake 里打一条 Debug.Log,把 scene 自己的名字连同 SceneManager.sceneCountInBuildSettings —— 包里到底带了多少个 scene —— 一起打印出来。logcat 会同时回答手机开了什么,以及你要的那个有没有被打进去。
把等待遮住,而不是摊出来
第二种黑不是坏了,只是还没装修:引擎在干活,那段时间屏幕上什么都没有。套路就一个物体 —— 一层能活过换场景的黑纱,等加载完成,再在新场景里淡出。
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
// CanvasGroup and this script go on a ROOT Canvas. DontDestroyOnLoad does
// nothing to an object that has a parent, so the Canvas is what has to
// survive. Its only child is a black Image stretched over the whole screen.
[RequireComponent(typeof(CanvasGroup))]
public class FadeLoader : MonoBehaviour
{
public float fadeTime = 0.35f;
CanvasGroup veil;
void Awake()
{
veil = GetComponent<CanvasGroup>();
veil.alpha = 0f;
// A faded-out veil must not swallow taps meant for the game.
veil.interactable = false;
veil.blocksRaycasts = false;
DontDestroyOnLoad(gameObject);
}
public void Load(string sceneName)
{
StartCoroutine(Transition(sceneName));
}
IEnumerator Transition(string sceneName)
{
// 1. Go to black while the old scene is still there.
yield return To(1f);
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
// A scene missing from the Build Scenes list cannot load in a build:
// Unity logs the error and hands back null. Without this guard the
// next line throws and the veil stays up forever.
if (op == null)
{
Debug.LogError("Not in Build Profiles: " + sceneName);
yield return To(0f);
yield break;
}
// 2. Wait. The veil is up, so the wait reads as black, not as broken.
while (!op.isDone)
yield return null;
// 3. Fade the new scene in.
yield return To(0f);
}
IEnumerator To(float target)
{
float from = veil.alpha;
for (float t = 0f; t < fadeTime; t += Time.deltaTime)
{
veil.alpha = Mathf.Lerp(from, target, t / fadeTime);
yield return null;
}
veil.alpha = target;
}
}AsyncOperation 的 progress 会在 scene 等待出场时停在 0.9,所以百分比进度条要用 op.progress / 0.9f;而 allowSceneActivation = false 就是让已加载完的 scene 先别激活、等进度条跑完。接好 slider 的那一版在 切换场景那篇里。
3 — 相机没东西可渲染
屏幕就是相机说它看到的那个东西。加载进来的场景里没有任何启用中的相机,画出来就是黑的 —— editor 里那句 'No cameras rendering' 在运行时并不存在。确认有相机之后,问题就从它在不在工作,变成它能看见什么。
一台启用中的相机,带 MainCamera 标签。取消勾选的 object 或组件什么都不渲染;挂在被代码开局关掉的东西下面的相机,会跟它一起没。Camera.main 抛 NullReferenceException,就说明没人挂着那个标签。
玩家在地板下面出生:从 Plane 下方开始的胶囊一直掉,跟随相机跟着下去,所有物体都跑出 far plane。editor 里你至少还能看到开始下坠那一下;到手机上就只剩黑。一块没有 collider 的地板是同一个故事,而且要修的是场景,不是相机。
裁剪平面和遮罩:500 米的关卡里把 far plane 设成 100,你只看得到天空;Culling Mask 把你的层排除掉的相机,除了天空盒什么都不画。两样都在 Camera 组件上,也都花几秒就能排除。
有一个五秒的拆分办法,值得在往下读之前先做一次。把相机的 Clear Flags 设成 Solid Color,Background Color 选一个扎眼的颜色 —— 品红,任何你不会误认成空房间的颜色 —— 然后构建。屏幕变扎眼了:相机渲染得好好的,你的黑只是一个空的视线,去查它站在哪、cull 了什么。屏幕还是黑的:说明手机打开的那个 scene 里没有启用中的相机,回到第 2 节。
如果你的 manager 是 singleton,而它在 Awake 里的初始化取决于哪个 scene 先跑,那才是真正的 bug —— 要照着改的是那份让 GameManager 保持规矩 的四条规则。
4 — UI 画满了整个屏幕
一个 Screen Space - Overlay 的 Canvas,永远画在相机产出的一切之上,不管场景里的东西各在什么地方。它里面只要有一张铺满全屏的 Image —— 背景、淡入淡出的黑纱、一块你忘了的面板 —— 游戏就是被盖着盖子在跑。有声音又是黑的,只能是这一节或相机那一节,不会有第三种。
掀不起来的黑纱:一张本来该由代码把 alpha 拉回 0 的黑色 Image。要么代码待在一个刚被场景加载销毁的物体上,要么它在等一个只有鼠标会发、手指不会发的事件 —— 新 Input System 里读 Mouse.current,触屏什么都传不过来,于是淡入在笔记本上跑了、在手机上永远没开始。
兄弟顺序就是绘制顺序:Canvas 里最后一个孩子会盖在前面那些之上,anchor 说什么都没用。一块在按钮之后才加进来、打着背景旗号的黑色 Image,会把按钮和按钮背后的一切一起盖住。
Screen Space - Camera 的 plane distance 落在 near 裁剪平面里面,或者对着的不是你以为那台相机:Canvas 就变成挡在镜头前的一张平面,通常是黑的,对身后的世界毫无兴趣。
两次构建就能定案。在 Hierarchy 里把 Canvas 的勾去掉再构建:世界出现了,那就是 UI,你也知道该打开哪个物体。还是黑的:跑上面那个 Clear Flags 测试,答案在相机或场景。两下都没变化 —— UI 是清白的,剩下两个原因就是下面两节。
5 — 手机跑不动的 shader:粉色,和黑色
粉色和黑色来自同一个地方 —— GPU 被塞了它用不了的东西 —— 但说的是两回事,而差别正好告诉你该修什么。粉色很大声也很诚实:那个文件里没有 subshader 支持当前平台,于是 Unity 直接画错误材质。黑色或看不见就安静多了:shader 被收下了,只是那一刻你需要的那一块不在包里。
到处都粉、连 editor 里也粉:pipeline 对不上。Project Settings > Graphics 里写着项目用的渲染管线 —— 那里挂了 URP Asset,Built-in 的 Standard 材质就变粉;那里什么都没有、项目里却全是 URP/Lit,也一样变粉。要改材质或者管线,别去改灯光。
手机上是粉的、editor 里却干净:graphics API 的问题。各家驱动对自己支持什么各说各话,一个开口就要 shader model 4.5、compute shader 或曲面细分的 subshader,很可能被一台用 OpenGLES3 很愉快、遇到 Vulkan 就翻脸的手机拒掉 —— 反过来也一样。
编辑器里好好的、设备上却看不见或变黑的物体:你运行时选中的变体被从包里剥掉了。Unity 只保留它见过的变体,而你代码在第 5 秒才打开的 keyword 它从没见过。把对应 shader 放进 Project Settings > Graphics > Always Included Shaders,或者趁加载画面开着时 warm 一份 ShaderVariantCollection。
- 1
打开 ,切到 Android 那个 tab,进入 Other Settings,把 Auto Graphics API 的勾去掉。
- 2
删掉 Vulkan、只留 OpenGLES3,构建、运行。黑屏没了:那款芯片受不了你的 shader 提的某个要求。下结论之前把 Vulkan 放回列表末尾再测一次。
- 3
在 OpenGLES3 下还是黑的:graphics API 不是元凶。五分钟排除了两件事 —— 去读日志,第 7 节。
6 — 性能差的设备,和永远等不到的前几帧
手机在加载第一个 scene 的那一刻分配渲染目标,同时还在拉贴图、shadow map 和后处理 —— 在中档设备上,这是一次帧内要几百兆的请求。桌面机付不起会甩你一个异常;很多移动驱动只会给你一块永远没人画的 surface,而且一句话都不说。
URP asset 里的 HDR 和 MSAA:在基于 tile 的移动 GPU 上都很贵,而且都有过在特定 Adreno、Mali 驱动上把头几帧搞黑的记录。在移动端那份 URP asset 上把两个勾都取消再构建。6 英寸屏幕上你几乎没失去什么,但整个排查可能就此结束。
实时阴影和画质等级:Max Distance 40、Cascade Count 1、分辨率 1024,并且要记得构建包用的是 Project Settings > Quality 里 Android 那列勾上的等级,不是 editor 工具栏当前那个。手机上等级不一样,说明你一直在测另一款游戏。
贴图内存:旗舰机上一切完美、四年前的中档机上却是黑的,通常已经超了预算。Max Size 1024、Android 用 ASTC 6x6、Read/Write Enabled 关掉 —— 还是这门课其他地方一直在用的那几个数字。
只要 30,别再追 60:在开局那个物体的 Awake 里设 Application.targetFrameRate = 30,配上 Screen.sleepTimeout。榨到极限但稳住 30 的设备还能玩;一台启动时在 60 和 15 之间挣扎的,会装死足够久,久到被人报成黑屏。
7 — logcat 是唯一不用再猜的办法
上面所有这些都是假设。手机早就知道哪一条是真的,只要你把它插上,它就大声说出来:一条命令,头二十行就会点名加载了哪个 scene、设备选了哪个 graphics API、以及吃掉第一帧的那个异常。
# start clean, then show only Unity's own lines
adb logcat -c && adb logcat -s Unity
# when the app closes itself: Unity plus the Android crash report
adb logcat -c && adb logcat Unity:V CRASH:V AndroidRuntime:E '*:S'
# write it to a file so you can search it calmly
adb logcat -s Unity > phone.txt紧跟在 GPU 名字后面、写着 Vulkan 或 OpenGLES3 的那行 —— 那才是手机实际选中的 API。如果它跟你测试用的不一样,该回头改的是 graphics API 列表,不是 lightmap。
第 2 节里你自己加的那条启动日志。没有这一行就是没有那个 scene:构建包打开的并不是你以为的那个。
Awake 或 Start 里的 NullReferenceException,或者 Scene ... has not been added to the build settings。前者说明你的 manager 在伸手拿那个只存在于你忘记添加的场景里的东西。后者就是第 2 节,也是全文唯一一条本身就是完整答案、而不只是线索的行。
Out of memory、GL_OUT_OF_MEMORY、Failed to create —— 第 6 节。或者 Unity 版本行之后一整面沉默:app 在渲染开始前就死了,那是安装、ABI 或签名的毛病,不是黑屏。
60 秒,知道这是哪种黑屏
- ✓有声音吗?那逻辑还活着,问题在渲染:相机、UI,然后才是 shader。
- ✓过几秒自己回来了?什么都没坏 —— 你只是没有加载画面。第 2 节那层黑纱就是修法。
- ✓app 自己关了?那是崩溃不是黑屏 —— 改任何设置之前先把 logcat 跑一遍。
- ✓scene 在 Build Scenes 列表里吗?在 index 0 吗?名字区分大小写,拼法跟 .unity 文件一致。
- ✓Clear Flags 用 Solid Color、背景设成品红,构建一次:屏幕变粉 = 相机活着但什么也没看见;还是黑 = 没有启用中的相机。
- ✓把 Canvas 的勾去掉、世界突然出现:盖子就是 UI。而删掉 Vulkan 后黑屏没了,那是驱动的问题,不是你的场景。
画面一旦出来,手机就会开始抱怨别的:热了五分钟之后的帧率,还有那块照着你显示器形状搭出来的 UI。接下来该看的是 适配所有屏幕 和 构建加调试的完整流程。
做出这个的那一课
构建、安装并在真机上调试
这篇指南单独成篇,是一份可以直接照着做的配方。在课程里,同样的东西会作为贯穿全部五个章的那个项目的一部分来搭建 —— 导出到手机,第 05 课。