Unityシーン間のフェードインアウトを2行で実現できる簡単スクリプト

Unityのシーン遷移時に、指定した時間でフェードイン/アウトするシンプルなスクリプトをつくったので紹介したい。

いろいろ試行錯誤した結果、シングルトンでもstaticクラスでもない無難な方法に辿りついた。以下のFadeManager.csファイルをプロジェクトに読み込んでおけば、他のスクリプトからわずか2行でコントロールできる、超お手軽なサンプルだ。

一人開発用のミニマムコード

Unityで複数シーンをまたいで変数を保持するには、static変数を使うのが手っ取り早い。しかしオブジェクト指向のプログラミングで、グローバルな変数を乱用するのはポリシーに反している気がする。

たとえばシーン間をまたぐエフェクトの実装も、いくつか方法があってどれがベストなのかわからない。シーンのマネージャー的なスクリプトはシングルトンで実現すべき、という話もあるが、一人でつくる小規模なアプリではかえってバグの温床になりそうだ。

見ず知らずのプログラマーと共同作業するような大規模開発なら、デザインパターンを意識した方が無難だろう。しかし野良プログラマーの趣味レベルのコーディングとしては、あとから見直したときに理解しやすい、最小限な構成の方がいい。他人(あるいは未来の自分)から見て丁寧にFail-Safeで実装されていることより、中身が単純でデバッグしやすい方が楽だし簡単だ。

どんなゲームやアプリでも、頻繁に使うと思うシーン間フェードイン/アウトの機能を、自分なりに一番簡単と思う方法で実装してみた。

シーンに置かないとUpdateされない

とりあえず最初に試したのは、シーン内にあらかじめフェード効果用のCanvasを用意してそこにアタッチしたスクリプトからImageの透明度を変化させる方法。誰でも思いつくとおり簡単に実現できるが、問題点としてはエフェクトを入れたいすべてのシーンにこのCanvasオブジェクトを配置する必要がある。

せめて共通化するためにCanvasをPrefab化したり、スクリプトを分離して空のオブジェクトにくっつけたりする方法もある。それでも全シーンにフェードスクリプト用の空オブジェクトを配置するのは面倒だ。

主要機能をpublic staticな関数にして、他のスクリプトから呼ぶ方法も試してみた。これならわざわざ空オブジェクトにスクリプトをくっつけなくても、staticなため外部から簡単に参照することができる。※ただしそこから呼ぶ内部の変数・関数もすべてstaticにしておく必要がある。

ところがそもそもフェード用スクリプト自体がシーン内にアタッチされていないと、StartもUpdateも実行されないようだ。結局よくやるようにCreate Emptyして、スクリプトをAdd Componentするしかないのだろうか…

staticクラスでUpdate使えない

一方、フェードエフェクトのインスタンスはアプリの実行中に1つあれば十分なので、クラス自体をstaticで宣言することもできる。しかしこの場合も、Visual Studio上で「静的クラスでインスタンスのメンバーを宣言することはできません」というエラーが出て、Update関数が実装できなくなる。

フェード用のクラス内でUpdateを呼ぶのが変なのかと思い、あくまでCanvasやImageを配置するだけのシンプルな設計にしてみた。この場合は他のシーンマネージャー的なスクリプトからStartやUpdateで毎回参照することになるが、かえって余計なコードが増えてしまう。フェードスクリプト自体はミニマムになったが、各シーンに空オブジェクトを配置するのと同じような気持ち悪さを感じる。

先達の方法を試してみようと思って、こちらの記事を参考に、「マネージャー用のシーンを用意してAwake前に自動配置する」というやり方を取り入れてみた。最初のシーンでは狙い通り動作するが、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(1920, 1080);
    }

    //フェードイン開始
    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);
        }
    }
}

結局MonoBehaviorを継承しつつ、自身のUpdateでフェード用画像の透明度をコントロールする方法に戻した。

工夫としては、自ら生成したフェード用のCanvasに自身のスクリプトをアタッチすることで、「事前にシーンに手動配置することなしに」外部から呼べば勝手にシーンに組み込まれるようにしている。ちゃんとシーンに存在するので、Update関数も問題なく動作する。

外部からの参照方法

他のスクリプトから呼び出す方法としては、シーン開始時にStart()内で、

FadeManager.FadeIn();

と書けばフェードインする。次のシーンに移りたい時も、

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

と1行書くだけでフェードアウトしながらシーンを切り替えてくれる。FadeIn()とFadeOut()はともにpublic staticな関数なので、インスタンス生成せずに直接呼び出せる。

一応内部でCanvasオブジェクトのnullチェックをしているので、フェードインを経ずに入って来たシーンでフェードアウトしても問題はない。

メリットとデメリット

この方法のメリットは、UnityのプロジェクトにFadeManager.csを入れておけば、各シーンにフェード用のオブジェクトを一切配置しなくて済むことだ。別のスクリプトから上記の2行で参照すればいいだけなので、作業の見落としを防げる。何の拡張性もないシンプル機能に抑えた代わりに、スクリプト間の疎結合を実現できた。

デメリットとしては、フェード時間やフェード時の画面の色などをインスペクターからデザイナーが設定できない点だ。もしシーンごとに設定を変えたいとか凝ったことをやりたいなら、素直にそれらの変数をpublicで公開して、シーン内のオブジェクトにアタッチすべきだろう。パワーポイントや動画編集ソフトにプリセットされているトランジション効果みたいに、派手なエフェクトもがんばれば実現できると思う。

しかしたいていのアプリでは、単純に黒くなるか白くなってフェードイン/アウトする画面効果だけで十分に役立つと思う。むしろ頻出すぎるので、Unityに元から組み込まれていてもおかしくない機能だ。あるいは自分が知らないだけで、すでに存在するのかもしれない。

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

コードもなるべくシンプルにしようと思ったが、一点はまったのはGraphic.colorのアルファ値を直接いじれないという点。

fadeImage.color.a = alpha;

と設定できればシンプルだが、「変数ではないため、’Graphic.color’の戻り値を変更できません」とエラーが出てしまう。

エフェクト実行中は毎フレームごとに繰り返されるので冗長に思うが、結局以下のようにColorオブジェクトのインスタンスを生成するしかなかった。

fadeImage.color.a = alpha;

他の人のやり方も調べたが、これ以外の実装が見当たらなかったので、今はこう書くしかないのだろう。

スポンサードリンク