Unityシーン間のフェード効果をたった2行で実現する方法

Unityのシーン遷移時に、指定した時間と色でフェードインアウトする簡単なスクリプトをつくってみた。

以下のFadeManager.csをプロジェクトに組み込んでおけば、ほかのスクリプトからわずか2行でコントロールできる。

ソースコード

//FadeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class FadeManager:MonoBehaviour{

    //フェード用のCanvasとImage
    private static Canvas fadeCanvas;
    private static Image fadeImage;

    //フェード用Imageの透明度
    private static float alpha = 0.0f;

    //フェードインアウトのフラグ
    public static bool isFadeIn = false;
    public static bool isFadeOut = false;

    //フェードしたい時間(単位は秒)
    private static float fadeTime = 0.2f;

    //遷移先のシーン番号
    private static int nextScene = 1;

    //フェード用のCanvasとImage生成
    static void Init()
    {
        //フェード用のCanvas生成
        GameObject FadeCanvasObject = new GameObject("CanvasFade");
        fadeCanvas = FadeCanvasObject.AddComponent<Canvas>();
        FadeCanvasObject.AddComponent<GraphicRaycaster>();
        fadeCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
        FadeCanvasObject.AddComponent<FadeManager>();

        //最前面になるよう適当なソートオーダー設定
        fadeCanvas.sortingOrder = 100;

        //フェード用のImage生成
        fadeImage = new GameObject("ImageFade").AddComponent<Image>();
        fadeImage.transform.SetParent(fadeCanvas.transform, false);
        fadeImage.rectTransform.anchoredPosition = Vector3.zero;

        //Imageサイズは適当に大きく設定してください
        fadeImage.rectTransform.sizeDelta = new Vector2(9999, 9999);
    }

    //フェードイン開始
    public static void FadeIn()
    {
        if (fadeImage == null) Init();
        fadeImage.color = Color.black;
        isFadeIn = true;
    } 

    //フェードアウト開始
    public static void FadeOut(int n)
    {
        if (fadeImage == null) Init();
        nextScene = n;
        fadeImage.color = Color.clear;
        fadeCanvas.enabled = true;
        isFadeOut = true;
    }

    void Update()
    {
        //フラグ有効なら毎フレームフェードイン/アウト処理
        if (isFadeIn)
        {
            //経過時間から透明度計算
            alpha -= Time.deltaTime / fadeTime;

            //フェードイン終了判定
            if (alpha <= 0.0f)
            {
                isFadeIn = false;
                alpha = 0.0f;
                fadeCanvas.enabled = false; 
            }

            //フェード用Imageの色・透明度設定
            fadeImage.color = new Color(0.0f, 0.0f, 0.0f, alpha);
        }
        else if (isFadeOut)
        {
            //経過時間から透明度計算
            alpha += Time.deltaTime / fadeTime;

            //フェードアウト終了判定
            if (alpha >= 1.0f)
            {
                isFadeOut = false;
                alpha = 1.0f;

                //次のシーンへ遷移
                SceneManager.LoadScene(nextScene);
            }

            //フェード用Imageの色・透明度設定
            fadeImage.color = new Color(0.0f, 0.0f, 0.0f, alpha);
        }
    }
}

Unityバージョン2018.1.4f1で動作確認済み。

使い方

上記のコードをコピペしてFadeManager.csというファイルをつくり、Unityのアセットに読み込んでおく。

FadeManager

特定のシーンをフェードインで始めたい場合、そのシーンのStart()内に以下の1行を追加する。

FadeManager.FadeIn();

現在のシーンをフェードアウトで終わって、次のシーンにフェードインしたい場合、任意のタイミングで次の1行を実行する。

FadeManager.FadeOut(/*遷移したいシーン番号*/);

簡単にフェードイン/アウトするだけなら、この2行で制御可能。それぞれ単独でも使用できる(フェードインせずに始めたシーンでもフェードアウト可能)。

上記FadeIn()とFadeOut()はともにpublic staticな関数なので、FadeManagerクラスからインスタンス生成せずに直接呼び出せる。

いくつかの実装方法

ここから先は個人的な開発メモ。

Unityでシーン間遷移のエフェクトを実装するには、さまざまな方法が考えられる。

各シーンにスクリプトを置く

とりあえず最初に試したのは、シーン内にあらかじめフェード効果用のCanvasを用意しておく方法。そこにアタッチしたスクリプトから、Imageの透明度を変化させればよい。

予想どおり簡単に実現できたが、問題点としてはエフェクトを入れたいすべてのシーンにCanvasオブジェクトを配置する必要がある。

代わりにCanvasをプレファブ化して各シーンで共用したり、スクリプトを分離して空のオブジェクトにくっつけたりする方法もある。しかしいずれの場合も、各シーンで専用のオブジェクトを準備する必要があり、手間が増える。

static関数の問題点

そこで主要機能をpublic staticな関数にして、ほかのスクリプトから直接呼ぶ方法を試してみた。これならわざわざ空オブジェクトを準備したりしなくても、外部から簡単に参照することができる。

Init関数のエラー

ところがそもそもフェード用スクリプト自体がシーンにアタッチされていないと、内部的にStart()もUpdate()も実行されない。またstatic関数から呼び出すメンバー変数・関数も、すべてstaticでなければならないという制約が生じる。

staticクラスの問題点

フェードエフェクトのインスタンスは、アプリ実行中にひとつだけ存在すれば十分。そこで、クラス自体をstaticで宣言する方法も考えてみた。

しかしこの場合は、Visual Studio上で「静的クラスでインスタンスのメンバーを宣言することはできません」というエラーが出てしまう。staticクラスではそもそもUpdate関数を実装できないのだ。

Update関数のエラー

そこでフェード用クラスではCanvasやImageを配置するだけにとどめ、ほかのシーンマネージャー的スクリプトからStart()、Update()で呼び出すようにしてみた。

これでも動くことは動くが、各シーンに空オブジェクトを配置するのと同じ気持ち悪さが残る。もっと簡単に実現できないだろうか。

Awake前に読み込み

こちらの記事を参考に、「マネージャー用のシーンを用意してAwake前に自動配置する」という方法を試してみた。

最初のシーンでは狙いどおりに動作したが、2つ目のシーン以降ではマネージャー用シーンが配置されず、うまく動かなかった。よく理解せずにコピペしただけなので、どこかで手順を間違ったのかもしれない。

完成版スクリプト

最終的に自分のわかるやり方で、各シーンから最小限のコードで呼び出せるよう工夫したのが、先に紹介したスクリプト。

結局MonoBehaviorを継承しつつ、自身のUpdate関数でフェード用画像の透明度をコントロールするようにした。全シーンにスクリプトを手動配置する必要もない。

ポイントはここ。

FadeCanvasObject.AddComponent();

Init()で生成したCanvasオブジェクトに、このスクリプト自体をアタッチしている。再帰処理になりそうだが、CanvasとImageはstatic宣言しているので多重生成されない。

設定できるのはエフェクトの継続時間とフェードの色のみ。

//フェードしたい時間(単位は秒)
private static float fadeTime = 0.2f;
//フェード用Imageの色・透明度設定
fadeImage.color = new Color(0.0f, 0.0f, 0.0, alpha);

あまり拡張性を考えない代わりに、スクリプト間の疎結合を実現できた。

color.aは直接設定できない

コードをなるべくシンプルにしようと思って、一点はまったところがある。

Graphic.colorのアルファ値は、なぜか直接セットできない。「変数ではないため、’Graphic.color’の戻り値を変更できません」とエラーが出てしまう。

Color Graphic.colorのエラー

いろいろ調べたが、結局以下のようにColorオブジェクトのインスタンスを毎回生成するしか思いつかなかった。アルファ値はstatic変数のalphaで保持して与えている。

fadeImage.color = new Color(0.0f, 0.0f, 0.0f, alpha);

エフェクト実行中、毎フレーム繰り返される処理なので冗長な気もする。動作パフォーマンスに影響はなさそうだが、何となくすっきりしない。

今後の改良点

今のところ、フェード時間や効果色をインスペクターからデザイナーが設定できないというデメリットがある。

もし「シーンごとにフェードのタイミングを変えたい」とか凝ったことをやりたいなら、対応する変数をpublicにすればいいと思う。そしてスクリプトを各シーンの適当なオブジェクトにアタッチして、個別に値をセットする。

動画編集ソフトに用意されているような派手なトランジション効果も、がんばればUnityで実装できると思う。しかし、たいていは黒か白で単純にフェードイン/アウトすれば間に合う場合が多いだろう。

スムーズにシーンを切り替えるには必須の機能だが、なぜかUnityには元から組み込まれていないのでつくったみた次第。フェード機能は需要がありそうなので、将来的にシーンのオプションとして実装されそうな気もする。

開発方針

シーン遷移を制御するマネージャー的なスクリプトは、「シングルトンで実現すべき」という話もある。しかしインスタンス生成を抑制するための余計なコードが増え、かえってバグの温床になりそうな予感もする。

デザインパターンは必要ない

複数のプログラマーが共同作業する大規模開発なら、統一したデザインパターンを意識すべきだろう。しかしひとりでつくる小規模なアプリなら、そこまで凝った仕掛けは必要ない。

ほかのプログラマー(あるいは未来の自分)に対して完璧にフェイルセーフであることよりも、中身が単純でデバッグしやすいことを優先した方がいい場合もある。

特に趣味レベルでたまにしかプログラミングしない人にとっては、コードは最小限にとどめた方が、見直しに時間もかからず効率的だ。あまり複雑なことをすると、あとで思い出せずに墓穴を掘るパターンが多い。

シンプルなコーディングを心がける

ここで言うシンプルというのは「構成が単純」「わかりやすい」という意味。変数・関数名は具体的かつ冗長に、コメントもくどいくらい残しておいた方が安全だ。

優秀なプログラマーが書いたスマートなコードは、高度に洗練されすぎて解読に時間がかかる。基本的に「将来の自分はアホになっている」という前提で、サルにもわかるくらい馬鹿ていねいに書いた方が安全だ。

近頃はswitch-caseすら自分には難しすぎるように感じる。すべてif-else分で条件分岐した方が、不測のトラブルを避けてリスクを減らせるように思う。