U
04 / 22 · 12 分
FAQ

エディタでは動くのにスマホだと黒画面になる

短い答え

Unity のゲームを実機で開くと黒画面になるのはなぜ?

実機の黒画面は、起動 scene が Build Scenes に無い、有効なカメラが無い、画面いっぱいの Image が全てを覆う、shader が端末で動かない——同じ顔をした 4 つの別物です。60 秒で音と所要時間に分け、adb logcat -s Unity に原因を名乗らせましょう。

スマホの黒画面は不具合 1 つではなく、外からは同じ顔に見える 4 つの集合体です。ファイルを 1 つも開かずに 3 つは切り分けできます——音が鳴り続けているか、数秒で映像が戻るか、logcat に何も出ないか。自分の種類を特定して、その 1 つだけ直す。当てずっぽうで 10 回ビルドするより速いです。

1 — 黒画面を 3 種類に分ける

ロゴの後、Unity 自体も 1〜2 秒は黒を描きますし、スマホが最初の scene に着くのはエディタよりはるかに遅い。何かを変える前に、今の黒がどの種類か十秒だけ見てください。種類ごとに原因の候補リストが別になります。

  • ずっと黒で最初の scene が出ない:スマホが入る scene が思っていたものと違うか、その scene で何も描画されていないか。セクション 2 と 3。

  • 数秒の黒あとゲームが立つ:ごく正常な起動で、前にローディング画面が無いだけ。スマホのストレージもコールドスタートの IL2CPP もキャッシュより遅く、そちらの待ちは一度も体験していません。

  • 音楽が鳴ったまま黒:ロジックは生きていて映像だけが無い。原因はカメラ、その上に描画された UI、または端末が拒んだ shader——セクション 3、4、5。

2 — 最初の scene がビルドに入っていない

Build Scenes 一覧に無い scene はゲームにもありません。APK に入らず、スマホにも無く、名前で呼んでも届かず、ビルドではあのファイルが単に存在しないのです。「エディタでは正常なのにスマホは黒い」が存在する理由の大半が、このパネル 1 枚です。

  1. 1

    File▸Build Profiles(Unity 6 以前は Build Settings)でプラットフォームを選び、Scene List を確認します。ゲームが到達する scene は全部載せる必要があります。開始画面だけ、では足りません。

  2. 2

    index 0 の行が、ビルド後のゲームの起動 scene です。エディタはその番号を無視して最後に開いていたものを立ち上げるため、この不具合は実機でしか現れません。

  3. 3

    scene は明示的に全部追加し、メニューか起動 scene を index 0 へ。まだ追加していない scene も、エディタでは綺麗に開けたままなので気づけません。

一覧に無い名前が LoadScene に渡ると、Unity はエラーを 1 行残すだけで読み込みは起きません——ポップアップも例外もなく静かに。コードが既にやった処理はそのまま残ります。隠したメニュー、切ったカメラ、破棄した object。見えているのは描画の失敗ではなく、到着しなかった読み込みの残骸です。

どの scene が開いたか証明する

この種の推測は 1 行で終わります。すべての scene のどこか 1 つの Awake に Debug.Log を置き、scene 自身の名前と SceneManager.sceneCountInBuildSettings——ビルドが実際に抱える scene 数——を出力させます。logcat の 1 行が「スマホが開いたもの」と「同梱されていたか」を同時に答えてくれます。

待ち時間を見せるのではなく隠す

二つ目の黒は故障ではなく未舗装です。エンジンが働く間、画面に何も無いだけ。パターンは object 1 つ——scene 切り替えを生き延べる黒い覆いが、読み込みを待ってから新しい scene 上でフェードアウトするだけです。

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 の表示を留める方法です。スライダーを繋いだ版は 別の scene へ切り替える記事にあります。

3 — カメラに描画対象が無い

画面は、カメラが見ていると言うもののままです。有効なカメラが 1 台もない scene は黒を描きます——エディタの 'No cameras rendering' は実行時に存在しません。カメラがあると分かったら、質問は「動いているか」から「何が見えているか」に変わります。

  • 有効なカメラ 1 台に MainCamera タグ。チェックを外した object や component は何も描かず、起動時に無効化するものの下にあるカメラもろとも消えます。Camera.main の NullReferenceException は、誰もあのタグを持っていない合図です。

  • プレイヤーが床の下にスポーンする:Plane の下から始まったカプセルは落ち続け、追従カメラも一緒に下へ行き、すべてが Far 平面の外へ出ます。エディタなら落下の始まりは見えますが、スマホでは黒いだけ。collider が無い床も同じ話で、直すのはカメラではなく scene です。

  • クリップ平面とマスク:500 m のレベルで Far を 100 にすれば空しか見えなく、Culling Mask が対象の層を外したカメラはスカイボックス以外を描きません。どちらも Camera コンポーネントの上に乗っていて、除外は数秒です。

5 秒で切り分ける方法があり、先に戻る価値があります。カメラの Clear Flags を Solid Color、Background Color を派手な色——空室と取り違えない色なら何でも——にしてビルドします。画面が派手になったら:カメラは正常で、黒はからっぽの視界だっただけ。どこに立つか culling を見てください。黒のままなら:スマホが開いた scene に有効なカメラが無く、セクション 2 に戻ります。

manager がシングルトンで Awake の初期化がどの scene が先に走るかに依存するなら、それが本当のバグです。直す手掛かりは GameManager を正直に保つ 4 つのルール側にあります。

4 — UI が画面いっぱいに描画されている

Screen Space - Overlay の Canvas は、シーン内の位置に関係なくカメラの出力の上に常に描画されます。その中の画面いっぱいの Image 1 つ——背景、フェードの覆い、置き忘れたパネル——で、ゲームは蓋をされたまま動いています。音が出ていて黒いなら、このセクションかカメラのセクション、それ以外ではありません。

  • 上がらない覆い:alpha を 0 に戻すはずだった黒 Image。コードが scene 読み込みで破棄された object に乗っていたか、マウスしか出さないイベントを待っているか——新しい Input System の Mouse.current にタッチは届かないので、フェードはノート PC では動き、スマホでは動き出しません。

  • 兄弟順が描画順:Canvas の最後の子どもが前の子どもしか重ねます。アンカーが何を言っても無駄で、ボタンの後に「ついで」で足した黒 Image はボタンもその後ろの何もかも覆ってしまいます。

  • Screen Space - Camera で plane distance が Near 平面の内側、あるいは違うカメラに向いている:Canvas はレンズの前の平面 1 枚になり、たいてい黒で、その向こうのワールドに全く関心がありません。

ビルド 2 回で決着します。Hierarchy で Canvas のチェックを外してビルド:ワールドが見えれば犯人は UI で、開く object も決まります。黒のままなら上の Clear Flags 確認へ——答えはカメラか scene。どちらも無反応なら UI は無実で、残る 2 つの原因は次のセクションです。

5 — スマホが実行を拒む shader:ピンクと黒

ピンクも黒も発生源は同じ——GPU が使えないものを渡された——ですが言うことが別で、その違いが直す場所を教えてくれます。ピンクは派手で正直:そのファイルにこのプラットフォーム対応の subshader が無いので、Unity がエラー用マテリアルを描いています。黒や透明は静か:shader は受理され、その瞬間に要った部分が無かっただけです。

  • エディタまで含めてどこもピンク:pipeline の不一致。Project Settings > Graphics にプロジェクトのレンダーパイプラインが書かれていて、URP Asset が在れば Built-in の Standard マテリアルが、何も無いのに URP/Lit が並んでいればそちらがピンクになります。直すのはマテリアルかパイプライン側で、ライティングではありません。

  • スマホだとピンク、エディタでは綺麗:graphics API のせいです。ドライバは対応機能の主張がバラバラで、shader model 4.5 や compute shader、テセレーションを要求する subshader は、OpenGLES3 は良いが Vulkan では機嫌を損ねるスマホに拒まれることがあります——逆もあり得ます。

  • エディタでは正常なのに実機で見えない・黒い:実行時に選ばれたバリアントがビルドから削られています。Unity は実際に見たバリアントしか残さず、5 秒目にコードが切り替える keyword は誰も見たことがありません。shader を Project Settings > Graphics > Always Included Shaders へ、あるいはロード画面の間に ShaderVariantCollection を warm します。

  1. 1

    Project Settings▸Player の Android タブに切り替え、Other Settings で Auto Graphics API のチェックを外します。

  2. 2

    Vulkan を消して OpenGLES3 を残し、ビルドして実行。黒が消えたら、そのチップセットは shader の要求を嫌がっています。判断を下す前に Vulkan を一覧の最後尾に戻してもう一度試してください。

  3. 3

    OpenGLES3 でも依然黒い:graphics API は犯人ではありません。5 分で 2 つを除外できたので、ログ読みへ。セクション 7 です。

6 — 非力な端末と、届かない最初のフレーム

スマホは最初の scene を読み込む瞬間に render target を確保し、同時に texture、シャドウマップ、ポストプロセスも引き込みます——ミドルレンジ機では 1 フレームに数百メガバイトの要求が乗ります。デスクトップなら例外を返すところ、モバイルのドライバは多くが「何も描かれない surface」を渡すだけで、メッセージは 1 つも出ません。

  • URP Asset の HDR と MSAA:タイル方式のモバイル GPU ではどちらも高く、特定の Adreno や Mali ドライバで最初のフレームを黒くした実績も両方にあります。mobile 用 asset で両方のチェックを外してビルド。6 インチではほぼ何も失わず、調査自体が終わる可能性があります。

  • リアルタイムシャドウと品質レベル:Max Distance 40、Cascade Count 1、解像度 1024。ビルドは Project Settings > Quality の Android 列でチェックしたレベルを使い、ツールバーで選んだレベルではない点を思い出してください。実機のレベルが違うなら、テストしていたのは別のゲームです。

  • texture のメモリ:フラッグシップでは完璧なのに 4 年前のミドルレンジでは黒いなら、たいてい予算超過です。Max Size 1024、Android は ASTC 6x6、Read/Write Enabled はオフ。コースの他の場所と同じ数字です。

  • 30 を要求して 60 を追わない:ゲームを起動する object の Awake で Application.targetFrameRate = 30 を Screen.sleepTimeout と共に設定します。限界の端末でも安定 30 なら遊べますが、起動時に 60 と 15 のあいだで失速すれば、黒画面として報告されるほど長く死んだように見えます。

7 — 推測をやめる唯一の方法が logcat

ここまでのは全て仮説です。スマホ側はどれが真か承知していて、ケーブルを刺した瞬間に口に出します。コマンド 1 つ、最初の 20 行が「読み込まれた 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 が無い、つまりビルドは思っていた scene を開いていません。

  • Awake か Start 内の NullReferenceException、または Scene ... has not been added to the build settings。前者は manager が、追加し忘れた scene にしかないものを取りに行っている状態。後者はセクション 2 の話で、この記事唯一、手掛かりではなく完成した回答になっている行です。

  • Out of memory、GL_OUT_OF_MEMORY、Failed to create——セクション 6。あるいは Unity のバージョン行以降が完全に沈黙:描画が始まる前にアプリが死んだことで、インストール・ABI・署名の問題であり黒画面ではありません。

60 秒で、どの黒画面か分かる

  • ✓音は鳴っている? ならロジックは生きていて、問題は描画側:カメラ、UI、shader の順。
  • ✓数秒で勝手に戻った? 壊れておらず、ローディング画面が無いだけ。セクション 2 の覆いが回答です。
  • ✓アプリが自分で終わった? それはクラッシュで黒画面ではない。設定を変える前に logcat を実行。
  • ✓scene は Build Scenes 一覧に、index 0 にあるか? scene 名は大文字小文字を区別し、.unity ファイルのままの綴り。
  • ✓Clear Flags を Solid Color、背景を magenta にして 1 回ビルド:ピンクならカメラは動いていて何も見ていない、黒なら有効なカメラが無い。
  • ✓Canvas のチェックを外してワールドが見えた:蓋は UI。Vulkan を外して黒が消えたならドライバ側で、scene の問題ではない。

映像が映るようになると、スマホは別のことで文句を言い始めます:5 分熱くなった後のフレームレートと、手元のモニターの形に合わせて作った UI です。ここから続くのは あらゆる画面への対応と ビルドからデバッグの流れ。

これを作るレッスン

ビルド、インストール、実機でのデバッグ

この記事はそれだけで完結するレシピです。コースでは同じものを、5 つのタブを貫くひとつのプロジェクトの一部として作ります — モバイルへ書き出す のレッスン 05。