U
04 / 22 · 12 分钟
常见问题

编辑器正常、手机上一片黑

简短回答

为什么 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. 1

    打开 File▸Build Profiles(Unity 6 之前叫 Build Settings),选中平台,看 Scene List。游戏能走到的每个 scene 都得在里面 —— 不只开局那一个。

  2. 2

    index 0 那一行就是构建包启动进入的 scene。editor 无视这个数字,只打开你上次停在面前的那个 —— 所以这个 bug 只在真机上现身。

  3. 3

    每个 scene 都显式加进去,再把菜单或启动 scene 拖到 index 0。你没加的那个,照样能在 editor 里打开着、看起来毫无问题。

如果代码调用 LoadScene 去加载不在列表里的名字,Unity 只记一行错误,加载根本不会发生 —— 静悄悄,没有弹窗也没有异常。而代码之前做过的事全都还留着:藏起来的菜单、关掉的相机、销毁的 object。你看到的不是渲染失败,是一次从没到的加载留下的残骸。

先证明到底开了哪个 scene

一行代码就能终结这一整类瞎猜。在每个 scene 里挑一个物体,在 Awake 里打一条 Debug.Log,把 scene 自己的名字连同 SceneManager.sceneCountInBuildSettings —— 包里到底带了多少个 scene —— 一起打印出来。logcat 会同时回答手机开了什么,以及你要的那个有没有被打进去。

把等待遮住,而不是摊出来

第二种黑不是坏了,只是还没装修:引擎在干活,那段时间屏幕上什么都没有。套路就一个物体 —— 一层能活过换场景的黑纱,等加载完成,再在新场景里淡出。

FadeLoader.cs — put it on the Canvas itself, with a full-screen black Image as that Canvas's only child
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. 1

    打开 Project Settings▸Player,切到 Android 那个 tab,进入 Other Settings,把 Auto Graphics API 的勾去掉。

  2. 2

    删掉 Vulkan、只留 OpenGLES3,构建、运行。黑屏没了:那款芯片受不了你的 shader 提的某个要求。下结论之前把 Vulkan 放回列表末尾再测一次。

  3. 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、以及吃掉第一帧的那个异常。

terminal
# 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 课。