うら干物書き

ゲームを作っています。

【Unity】簡易フェード制御コンポーネントについて考えてみる

 こんにちは、うら干物です。
 シーンを切り替える際に、フェードイン・アウトがあると見栄えがよくなりますよね。
 ただ、ゲームジャムのような時間が制限されているときには、なかなか手を付けにくいものでもあります。
 そんなときのために、フェード機能をもつコンポーネントをライブラリ化してすぐに使える状況にしておきましょう。
 今回はそんな簡易フェード制御用のコンポーネントを作成していきましょう。


この記事にはUnity5.4.0f3を使用しています。

今回のテーマ

 プロジェクトに簡単に組み込める形のものが作りたいですね。
 そのためPrefabなどリソースは用意せず、スクリプトのみで使えるようなものを目指して行きましょう。

uGUIの機能を使ってフェードに必要なもの揃える

 フェードの表現はuGUICanvasImageを使っていきましょう。
 前章でお話ししたとおり、Prefab化などはしたくないのでImageを表示するために必要なコンポーネントは全てAddComponent()を使って追加していきましょう。

Canvas

 uGUIのUIを表示するにはまずCanvasが必要です。
 最前面にしたいのでRenderModeScreenSpaceOverlayに、SortOrderには大きめの数値を入れておきましょう。

var canvas  = gameObject.AddComponent<Canvas>();  
canvas.renderMode   = RenderMode.ScreenSpaceOverlay;  
canvas.sortingOrder = 1000;  

CanvasScaler

 スクリーンサイズや解像度に関係なく、画面全体に表示させたいので、CanvasScalerも追加しましょう。
 UIScaleModeScaleWithScreenSizeにしてCanvasの拡縮をしてもらいましょう。
 使用するPixelを抑えるために、ReferenceResolutionを小さい数値にしておきましょう。

var scaler  = gameObject.AddComponent<CanvasScaler>();  
scaler.uiScaleMode          = CanvasScaler.ScaleMode.ScaleWithScreenSize;  
scaler.referenceResolution  = Vector2.one;  

Image

 最後にフェード用のImageを追加しましょう。
 これは現在の子オブジェクトとして作成しましょう。

var obj     = new GameObject( "FadeImage" );  
obj.transform.SetParent( transform, false );  
var image   = obj.AddComponent<Image>();  

 まとめるとこんな感じになります。

using UnityEngine;  
using UnityEngine.UI;  

public class FadeController : MonoBehaviour  
{  
    // UIよりも手前に表示するために大きい値をCanvasの順に指定しています。  
    // フェードスプライトより手前にものを表示する必要がある場合などは、値を調整する必要があります。  
    private readonly int    DEFAULT_CANVAS_ORDER    = 1000;  

    void Awake()  
    {  
        gameObject.name = "FadeController";  
        DontDestroyOnLoad( gameObject );  

        // Canvasの作成。  
        // Hierarchy上に別のCanvasがScreenSpaceOverlayである場合は、  
        // sortingOrderの値に気を付ける必要があります。  
        {  
            var canvas  = gameObject.AddComponent<Canvas>();  
            canvas.renderMode   = RenderMode.ScreenSpaceOverlay;  
            canvas.sortingOrder = DEFAULT_CANVAS_ORDER;  
        }  

        // CanvasScalerの作成。  
        // モードをScaleWithScreenSizeにしてreferenceResolutionを(1,1)にすることで、  
        // 解像度に関係なく画面全体に対応できる……はず。  
        {  
            var scaler  = gameObject.AddComponent<CanvasScaler>();  
            scaler.uiScaleMode          = CanvasScaler.ScaleMode.ScaleWithScreenSize;  
            scaler.referenceResolution  = Vector2.one;  
        }  

        // Fade用Spriteの作成。  
        {  
            var obj     = new GameObject( "FadeImage" );  
            obj.transform.SetParent( transform, false );  
            var image   = obj.AddComponent<Image>();  
        }  
    }  

} // class FadeController  

フェードアウト

 フェードアウトを実行する際に、必要なものは何でしょうか。
 まずは時間が必要ですね。フェードに何秒かかるのかを指定したいです。
 そしてです。何色のフェードにするのかも指定したいです。

public void FadeOut( float i_time, Color i_color )  
{  
    // フェード処理開始  
}  

フェードイン

 では今度はフェードインです。
 フェードインを行う場合は、すでに画面全体はフェードアウトで指定した色になっていますので、色の指定はいりませんね。
 時間の指定のみ大丈夫そうです。

public void FadeIn( float i_time )  
{  
    // フェード処理開始  
}  

フェード処理のデフォルト処理について

 さて、前章にてフェード開始時に引数について考えましたが、フェードの時間や色など指定することはおそらく少ないと思います。
 そのため、毎回フェード処理を行う際に同じ引数指定するのも面倒くさいので、引数を指定しない場合はデフォルトの数値を使うようにしましょう。
 デフォルト引数でもいいのですが、今回は関数のオーバーロードで表現しましょう。

private readonly float  DEFAULT_TIME    = 0.5f;  
private readonly Color  DEFAULT_COLOR   = Color.black;  

public void FadeOut( float i_time, Color i_color )  
{  
    // フェード処理開始  
}  
public void FadeOut()  
{  
    FadeOut( DEFAULT_TIME, DEFAULT_COLOR );  
}  
public void FadeOut( float i_time )  
{  
    FadeOut( i_time, DEFAULT_COLOR );  
}  
public void FadeOut( Color i_color )  
{  
    FadeOut( DEFAULT_TIME, i_color );  
}  

public void FadeIn( float i_time )  
{  
    // フェード処理開始  
}  
public void FadeIn()  
{  
    FadeIn( DEFAULT_TIME );  
}  

フェード処理の完了を判断する方法

 フェードを開始するのはよいのですが、どうやってフェード処理が完了すればよいのでしょうか。
 ざっと考えてみると、以下の3つの方法を思いつい来ました。

  • フェード処理の取得するプロパティを用意して、アクセスして判断する
    一番シンプルな方法がこちらですね。
    コンポーネントに状態用の列挙体を用意し、取得できるようにすれば完了を検知できます。

    • メリット
      • フェード処理を呼び出す側の使い方に柔軟に対応できる
    • デメリット
      • 毎回フェードの状態を確認する必要がある
      • フェードが完了し状態を確認する前に、別からフェード処理を呼ばれた場合、完了状態を正しく取得できない。
  • コールバック関数を指定し、フェード処理が完了したら呼ぶようにする。
    開始時に引数に、またはeventプロパティとしてコールバックを設定できるようにし、フェード完了をコールバック経由で知る方法です。

    • メリット
      • 毎フレームフェードの状態を確認する必要がない
      • コールバックをevent化またはリスト化することで、フェード処理の多重呼出しに対応できる
    • デメリット
      • 呼出し先でコールバック関数を作ってもらう必要がある
  • 関数をコルーチンで使用し、返り値で判断する
    IEnumeratorを返す形をとることで、処理の終了を判断する方法です。

    • メリット
      • コルーチンの関数先から呼び出すと、コードがすっきりして見やすい
    • デメリット
      • コルーチンありき

 コールバックで判断する方法がパフォーマンスがよさそうですが、ゲームジャムなどゲームのジャンルや状況がわからない点を考えると、状態の列挙体を用意する形が一番対応力がありそうです。
 というわけで今回は列挙体を用意する形でいきます。

public enum EFadeState  
{  
    In,         // フェードイン完了.  
    Out,        // フェードアウト完了.  
    Process,    // フェード処理中.  
}  

private EFadeState  m_state     = default( EFadeState );  

public EFadeState State  
{  
    get  
    {  
        return m_state;  
    }  
    private set  
    {  
        m_state = value;  
    }  
}  

多重呼出し対応について

 ここで問題になるのが、フェード処理を多重で呼び出された際の対応です。
 現在フェード中にも関わらず、別にフェード開始処理を呼ばれた際にどうするのか正しいのでしょうか。
 基本的には、次のフェード処理をスルーする上書きするかになります。
 どちらにしろ呼出し元のコンポーネントの処理が、フェード完了が次の処理に影響する場合は惨事になります
 スルーする場合は、フェード開始処理が失敗したことを返せば、現在フェード処理を行うことが出来ないことが判断できますが、上書きの場合は、先にフェード処理を呼んでいたコンポーネントはそのことを判断できないため、どうしようもありません。
 というわけで、フェード処理中に別でフェード開始処理を行おうとすると失敗するようにします。

フェード制御コンポーネントのアクセス方法

 最後にどのようにフェード制御コンポーネントにアクセスすればよいのでしょうか。
 一番簡単なのはシングルトン化してしまえばいいですよね。
 ただ前章でお話しした通り、多重にフェード開始処理を呼び出すことを推奨していないため、多数のコンポーネントがアクセス出来る状態はあまり望ましくありません。
 このコンポーネントはフェードに必要なコンポーネントを自動生成する仕組みになっているので、フェードを扱うコンポーネントがこのフェード制御コンポーネントを作成してもらうようにしましょう。
 生成したコンポーネントならアクセスは容易です。
 もちろん、シングルトンとして運用することも大丈夫です。その際は前章の多重呼出しにだけはお気を付けください。

完成、FadeController

 というわけで、駆け足でしたが以下のソースコードが完成したフェード制御コンポーネント、FadeControllerです。
 ゲームジャムなど、フェード処理をさっと作りたいときにご利用くださいませ。

【Unity】簡易フェード制御コンポーネントについて考えてみる FadeController

 以下が使用例です。
 ご参考になればと思います。

【Unity】簡易フェード制御コンポーネントについて考えてみる SceneController

f:id:urahimono:20160814151010g:plain