ボクセル3Dワールド #8 シーン遷移

# 目的
Unityでボクセルアートで作ったキャラの3Dゲームを作成します。
今回は、タイトルを表示するタイトルシーンを追加しシーン間の遷移（トランジション）を作成します。


# 環境
Unity2018.4.11f
Mac 10.15.2


# 要件
* ゲーム開始時のタイトルシーン追加
* タイトルシーンからメインシーンへ遷移させる
* フェードアニメーションで画面遷移させる

# 手順
1. タイトルシーン作成
2. フィールドを回転させる
3. アニメーショントランジション




# 1.タイトルシーン作成
Projectウインドウで`Create` -> `Scene`でTitleシーンを作成します。

<img width="95" alt="スクリーンショット 2020-04-01 14.55.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/a1a5914a-171c-a66d-d3ee-3a241e125e4d.png">

### UI作成
以下のようにUIを作成します。

<img width="172" alt="スクリーンショット 2020-04-01 14.58.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/9d7593bc-39f8-bace-f351-ba29523622ab.png">

* TitleText（Text）
* StartBtn（Button）

<img width="409" alt="スクリーンショット 2020-04-01 14.57.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/17835eba-02df-1e60-3e8e-fc178b339104.png">


### スクリプト
タイトルシーンを制御する`TitleDirector`スクリプトを作成します。
スタートボタンクリック時に画面遷移するようにSceneManagerを配置します。

```c#:TitleDirector.cs
using UnityEngine;
using UnityEngine.SceneManagement;  //追加

/// <summary>
/// タイトルシーンの制御
/// </summary>
public class TitleDirector : MonoBehaviour
{
    //スタートボタン
    public void OnPressStartBtn() {
        SceneManager.LoadScene("Main");
    }
}
```

### スタートボタンの設定
スタートボタンクリック時に`OnpressStartBtn`メソッドが発動するように設定します。

<img width="336" alt="スクリーンショット 2020-04-01 15.21.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/6b072182-096c-e40d-5968-5ea0a4ffceec.png">


### BuildSettingsにシーン追加
BuildSettingsにTilteシーンとMainシーンを追加します。
また順番もTitleシーンが最初になるようにします。

<img width="190" alt="スクリーンショット 2020-04-01 15.24.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/7eb9d02c-d009-682c-be04-13f2dd4c4551.png">

<img width="622" alt="スクリーンショット 2020-04-01 15.26.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/d1c178bc-be43-5c9f-7a2f-c0a18b38b185.png">

スタートボタンを押してメインシーンへ遷移すれば完了です。



# 2.フィールドを回転させる
このままだとタイトルシーンは背景もなく寂しいので、メインゲームで使うフィールドを配置して回転させる演出を追加します。

### フィールドをプレファブ化して配置
Mainシーンに配置してあるフィールド（Ground）をプレファブ化します。
Titleシーン上にプレファブ化したGroundを配置します。
併せて、サボテンのオブジェクトも中央に配置しておきます。

<img width="235" alt="スクリーンショット 2020-04-01 15.36.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/1e6403a2-4169-7450-a8b4-c44bf71a2121.png">

<img width="712" alt="スクリーンショット 2020-04-01 15.38.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/6c340404-0b3d-70db-b2a2-5fb1d8207181.png">


### サボテンを中心にカメラを回転
中央に配置したサボテンを中心にメインカメラを回転させるスクリプトを作成します。
一定スピードで円を描くように回転させます。
常にサボテンの方向を向くように設定することで、フィールドが回転しているように表現します。

```c#:TitleCameraController.cs
using UnityEngine;

/// <summary>
/// カメラをフィールドの中心の周りをゆっくりと回転させる
/// </summary>
public class TitleCameraController : MonoBehaviour
{
    public float speed = 0.1f;      // 回転スピード
    public float posY = 5f;         // カメラの高さ座標
    public GameObject centerObj;    // 中心となる対象オブジェクト
    float radius;                   // 回転する半径（中心オブジェクトとカメラの距離）

    void Start() {
        //半径を対象物とカメラとの距離から算出
        Vector2 dir = centerObj.transform.position - transform.position;
        radius = dir.magnitude;
    }

    void Update() {
        //カメラの高さ設定
        Vector3 pos = new Vector3(0, posY, 0);

        //Sin、Cosを使って円状になるように座標を計算する
        //円の直径分を掛け合わせることで中心オブジェクトの周りを半径分で周回する
        //中心オブジェクトのx,z座標を加算するとこで円の中心座標を変更する
        pos.x = 2 * radius * Mathf.Sin(Time.time * speed) + centerObj.transform.position.x;
        pos.z = 2 * radius * Mathf.Cos(Time.time * speed) + centerObj.transform.position.z;

        //カメラに計算された座標をセット
        transform.position = pos;

        //カメラを常に中心オブジェクトの方を向かせる
        transform.LookAt(centerObj.transform);
    }
}
```


### MainCameraにスクリプトをアタッチ
MainCameraに先ほど作成したスクリプトをアタッチします。
回転スピードやカメラのY座標（高さ）を調整します。
CenterObjeにHierarchyビューに配置してあるCactus（サボテン）をセットします。

<img width="351" alt="スクリーンショット 2020-04-01 15.58.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/e31373eb-db70-6bb0-88a8-64b5d8d64ca8.png">

最後にSceneビュー上で視点を変更してサボテンとの距離を調整します。
MainCameraを選択した状態で`GameObject` -> `Align With View`を選択してカメラの位置角度を自動設定します。

<img width="214" alt="スクリーンショット 2020-04-01 16.03.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/b2cd260f-7d49-c49e-b15f-7bf1fed54816.png">

<img width="632" alt="スクリーンショット 2020-04-01 16.02.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/ef3344ef-cf1f-9eff-6598-3be924d55d91.png">

MainCameraの`Clear Flags`を`Solid Color`に変更、Backgroundの色をお好きな色に変更します。
最終的にこのような感じでサボテンを中心にフィールドが回転していれば完成です。

<img width="400" alt="スクリーンショット 2020-04-01 16.06.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/9ec808fe-8923-cfc0-a504-29f5d9ba279e.png">



# 3.アニメーショントランジション
画面遷移時に画面全体にフェードをかけるアニメーションを作成します。

## UIにPanelオブジェクト追加
Panelを作成して画面全体に配置するようにします。

<img width="169" alt="スクリーンショット 2020-04-01 16.16.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/f8d45c89-098e-9f92-fcf9-51301718f892.png">

画面より少し大きめになるように設定します。
色は何色でも構いませんが今回は白色でフェードするようにします。
`Canvas Group`コンポーネントを追加します。
`Canvas Group`を追加することで対象UIの透明度を変化させることができるようになります。

<img width="348" alt="スクリーンショット 2020-04-01 16.48.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/d7c8060c-25e5-f82e-92c1-93924671d42c.png">


Panelオブジェクトはプレファブ化してHierarchyビュー上のPanelは削除します。

<img width="156" alt="スクリーンショット 2020-04-01 16.19.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260143/66f7fcf3-bd23-fb56-04ac-671d9d19d450.png">


## 画面遷移時にPanelを呼び出すスクリプト
`SceneTransitionManager`というファイル名でスクリプトを作成します。

シーン遷移制御スクリプトは少々複雑ですので段階を追って説明します。


### Ⅰ. シーン遷移の流れ説明

スタートボタンクリック
↓
シーン遷移アニメーション用のPanel（UI）を生成
↓
Alpha値0（透明）な状態から指定した時間をかけてAlpha値を1（非透明）に変化させる
↓
画面が完全にフェードしたタイミングで画面遷移させる処理を実行
↓
遷移後のシーン表示
↓
シーン遷移アニメーション用のPanel（UI）を生成
↓
Alpha値1（非透明）な状態から指定した時間をかけてAlpha値を0（透明）に変化させる
↓
シーン遷移アニメーション用のPanel（UI）を削除
↓
BGMスタートなどゲームスタート



### Ⅱ. アニメーション部分の処理
`SceneTransitionManager`スクリプト内に内部クラスとして２つのクラスを実装します。

* アニメーションを処理するクラス（SceneTransitionEventHandler）
* シーン遷移全体を制御するクラス（SceneTransitionManager）


フェード開始フラグが設定されたらAlpha値を変化させてフェードさせる処理を実装します。
isFadeOut、isFadeINそれぞれのフラグが立ったらUpdate内でalpha値を制御して徐々に透明および非透明にします。

```c#:SceneTransitionManager.cs
using System.Collections;
using UnityEngine;
using System;    //追加

/// <summary>
/// シーン遷移時のアニメーションイベント
/// </summary>
public class SceneTransitionEventHandler : MonoBehaviour {

    //アニメーション終了後に実行する処理
    public Action CompleteAction;

    //アニメーション時間
    public float time;

    //アニメーション開始フラグ
    public bool isFadeOut;
    public bool isFadeIn;


    void Update() {

        if(isFadeIn) {
            GetComponent<CanvasGroup>().alpha -= Time.deltaTime / time;
            if(GetComponent<CanvasGroup>().alpha <= 0) {
                isFadeIn = false;
                Destroy(gameObject, 0.5f);
                CompleteAction();
            }
        }

        if(isFadeOut) {
            GetComponent<CanvasGroup>().alpha += Time.deltaTime / time;
            if(GetComponent<CanvasGroup>().alpha >= 1) {
                isFadeOut = false;
                Destroy(gameObject, 0.5f);
                CompleteAction();
            }
        }
    }
}
```

ポイントを説明します。
以下の処理でアニメーション時間かけてalpha値を0→1、1→0にするようにします。

```c#
Time.deltaTime / time
```

完全に透明になったタイミングで画面遷移用アニメーションPanelは削除します。
遷移終了したタイミングでコールバックするように`CompleteAction`を呼びます。
後ほど説明しますが、`CompleteAction`には呼び出し元から渡された「遷移が終わったら実行して欲しい処理」が格納されております。

```c#
if(GetComponent<CanvasGroup>().alpha <= 0) {
    isFadeIn = false;
    Destroy(gameObject, 0.5f);
    CompleteAction();
}
```





### Ⅲ. コールバックの仕組み

シーン遷移を開始するメソッド部分を実装します。
どこからでも呼べる静的なクラスととして定義します。

```c#:SceneTransitionManager.cs
public class SceneTransitionEventHandler : MonoBehaviour {
    //・・・
}

/// <summary>
/// シーン遷移の制御
/// </summary>
public static class SceneTransitionManager {

}
```

静的なメソッドを３つ定義します。

`SceneTransitionEventHandler`
先に定義した内部クラスをPanelオブジェクトにアタッチさせてアニメーションを実行できるようにするためのメソッド

```c#
//イベントを制御するハンドラーを設定するメソッド
private static SceneTransitionEventHandler SetUpEventHandler(GameObject target) {
    SceneTransitionEventHandler eventHandler = target.AddComponent<SceneTransitionEventHandler>();
    return eventHandler;
}
```

`FadeIn`
フェードイン（遷移後のシーン側）を開始する。
引数に以下を指定します。

* フェードアニメーションさせるターゲットオブジェクト（この場合はPanel）
* アニメーション時間
* アニメーション開始までの遅延時間
* 遷移完了後に実行したい処理（Action）


```c#
public static IEnumerator FadeIn(GameObject target, float time, float delay, Action action = null) {
    //イベントを制御するハンドラーを設置する
    SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject);
    //アニメーション時間セット
    eventHandler.time = time;
    //透過度初期化
    target.GetComponent<CanvasGroup>().alpha = 1f;
    //遅延処理
    yield return new WaitForSeconds(delay);
    //イベント発動
    eventHandler.isFadeIn = true;
    eventHandler.CompleteAction = action;
}
```


`FadeOut`
フェードアウト（遷移前のシーン側）を開始する。
引数に以下を指定します。

* フェードアニメーションさせるターゲットオブジェクト（この場合はPanel）
* アニメーション時間
* アニメーション開始までの遅延時間
* 遷移完了後に実行したい処理（Action）


```c#
public static IEnumerator FadeOut(GameObject target, float time, float delay, Action action = null) {
    //イベントを制御するハンドラーを設置する
    SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject);
    //アニメーション時間セット
    eventHandler.time = time;
    //透過度初期化
    target.GetComponent<CanvasGroup>().alpha = 0f;
    //遅延処理
    yield return new WaitForSeconds(delay);
    //イベント発動
    eventHandler.isFadeOut = true;
    eventHandler.CompleteAction = action;
}
```

最終的には以下のようになります。

```c#:SceneTransitionManager.cs
using System.Collections;
using UnityEngine;
using System;

/// <summary>
/// シーン遷移時のアニメーションイベント
/// </summary>
public class SceneTransitionEventHandler : MonoBehaviour {

    //アニメーション終了後に実行する処理
    public Action CompleteAction;

    //アニメーション時間
    public float time;

    //アニメーション開始フラグ
    public bool isFadeOut;
    public bool isFadeIn;


    void Update() {

        if(isFadeIn) {
            GetComponent<CanvasGroup>().alpha -= Time.deltaTime / time;
            if(GetComponent<CanvasGroup>().alpha <= 0) {
                isFadeIn = false;
                Destroy(gameObject, 0.5f);
                CompleteAction();
            }
        }

        if(isFadeOut) {
            GetComponent<CanvasGroup>().alpha += Time.deltaTime / time;
            if(GetComponent<CanvasGroup>().alpha >= 1) {
                isFadeOut = false;
                Destroy(gameObject, 0.5f);
                CompleteAction();
            }
        }
    }
}

/// <summary>
/// シーン遷移の制御
/// </summary>
public static class SceneTransitionManager {

    //イベントを制御するハンドラーを設定するメソッド
    private static SceneTransitionEventHandler SetUpEventHandler(GameObject target) {
        SceneTransitionEventHandler eventHandler = target.AddComponent<SceneTransitionEventHandler>();
        return eventHandler;
    }


    /// <summary>
    /// フェードイン開始（遷移後シーン）
    /// </summary>
    /// <param name="target">ターゲットオブジェクト</param>
    /// <param name="time">アニメーション時間</param>
    /// <param name="delay">遅延時間</param>
    /// <param name="action">実行したい処理</param>
    public static IEnumerator FadeIn(GameObject target, float time, float delay, Action action = null) {
        //イベントを制御するハンドラーを設置する
        SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject);
        //アニメーション時間セット
        eventHandler.time = time;
        //透過度初期化
        target.GetComponent<CanvasGroup>().alpha = 1f;
        //遅延処理
        yield return new WaitForSeconds(delay);
        //イベント発動
        eventHandler.isFadeIn = true;
        eventHandler.CompleteAction = action;
    }

    /// <summary>
    /// フェードアウト開始（遷移前シーン）
    /// </summary>
    /// <param name="target">ターゲットオブジェクト</param>
    /// <param name="time">アニメーション時間</param>
    /// <param name="delay">遅延時間</param>
    /// <param name="action">実行したい処理</param>
    public static IEnumerator FadeOut(GameObject target, float time, float delay, Action action = null) {
        //イベントを制御するハンドラーを設置する
        SceneTransitionEventHandler eventHandler = SetUpEventHandler(target.gameObject);
        //アニメーション時間セット
        eventHandler.time = time;
        //透過度初期化
        target.GetComponent<CanvasGroup>().alpha = 0f;
        //遅延処理
        yield return new WaitForSeconds(delay);
        //イベント発動
        eventHandler.isFadeOut = true;
        eventHandler.CompleteAction = action;
    }
}
```


## 呼び出し側のスクリプト
画面遷移を開始する呼び出し側にスクリプトを追加します。

シーン遷移アニメーション用のUI（tranPanel）のプレファブを取得します。
Panelの親オブジェクトとして設定するCanvasも取得しておきます。

スタートボタンが押されたタイミングでtranPanelのインスタンスを生成し、親オブジェクトにCanvasを指定します。

コルーチンでシーン遷移開始のFadeOuｔメソッドを呼び出します。
引数にアニメーション時間と開始遅延時間を指定します。
ラムダ式の書き方で画面遷移処理完了後に実行したい処理（LoadScene）を書きます。

```c#:TitleDirector.cs
public class TitleDirector : MonoBehaviour
{
    //シーン遷移用（追加）
    GameObject tranPanelPrefab;
    GameObject canvas;

    void Start() {
        //シーン遷移用オブジェクト取得（追加）
        tranPanelPrefab = Resources.Load("Prefabs/TranPanel") as GameObject;
        canvas = GameObject.Find("Canvas");
    }

    //スタートボタン
    public void OnPressStartBtn() {

        //シーン遷移用オブジェクト生成（追加）
        GameObject tranPanel = Instantiate(tranPanelPrefab, canvas.transform);

        //シーン遷移アニメーション終了後にMainシーンへ遷移（ラムダ式）
        StartCoroutine(SceneTransitionManager.FadeOut(tranPanel, 1f, 0.5f, () => {
            SceneManager.LoadScene("Main");
        }));
    }
}
```

## シーン遷移後の受け取り側スクリプト
Mainシーンに切り替わったらFadeInを開始させます。
遷移が完了したタイミングでBGMをスタートさせます。

```c#:GameDirector.cs
/// <summary>
/// メインシーン全体を管理する
/// </summary>
public class GameDirector : MonoBehaviour
{
    //メニューウインドウUI
    GameObject menuWindow;

    //シーン遷移用（追加）
    GameObject tranPanelPrefab;
    GameObject canvas;

    //（追加）
    private void Awake() {

        //シーン遷移用オブジェクト取得（追加）
        tranPanelPrefab = Resources.Load("Prefabs/TranPanel") as GameObject;
        canvas = GameObject.Find("Canvas");

        //シーン遷移用オブジェクト生成（追加）
        GameObject tranPanel = Instantiate(tranPanelPrefab, canvas.transform);

        //シーン遷移アニメーション終了後にMainシーンへ遷移（ラムダ式）
        StartCoroutine(SceneTransitionManager.FadeIn(tranPanel, 1f, 0f, () => {
            //BGM再生
            GSound.Instance.PlayBgm("BGM_Field", true);
        }));
    }

    void Start()
    {
        //・・・
    }

    //メニューボタン
    public void OnPressMenuBtn() {
        //・・・
    }
}
```

TitleシーンからフェードしながらMainシーンへ遷移できれば完成です。


# 最後に
シーンを遷移させること事態は難しくありませんが、そのままだと一瞬で画面が切り替わり味気ないものとなってしまいます。
アニメーション効果をかけながら徐々にシーンを遷移させることでゲームのクオリティをあげることができます。

今回の一番のポイントは、Aという処理が完了したタイミングでBという処理を実行させる、という非同期処理を実装したことです。
コールバックのような仕組みはゲーム開発では欠かせませんが、複雑なコードですので、初心者には敷居が高くなってしまいます。
なるべくシンプルに実装したつもりですので、参考にしてみてください。