Parallels Desktop 35%OFF セール

【Unity】タワーディフェンス(25) 場面転換(トランジション)のエフェクト【クソゲー制作】

タワーディフェンスを作る(25)

Unity初心者が2Dタワーディフェンスを作っています。

今回は場面転換(トランジション)のエフェクトを実装していきます。なんと今回が最終回です。

環境
  • Mac mini (M1, 2020)
  • Unity 2022.3.36f1
目次

TransitionManagerスクリプトを作成

まず、トランジションを管理するスクリプトTransitionManagerを作成します。

STEP

「Assets/Scripts」フォルダに新規C#スクリプトを作成して名前を「TransitionManager」にします。

TransitionManager.csの内容は次のとおりです。

using UnityEngine;
using DG.Tweening; // DOTweenを使用するために必要
using System; // Actionデリゲートを使用するために必要

public class TransitionManager : MonoBehaviour
{
    // シングルトンインスタンス
    public static TransitionManager Instance { get; private set; }

    private void Awake()
    {
        // シングルトンパターンの実装
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            Destroy(gameObject); // すでに存在する場合はこのインスタンスを破棄
        }
    }

    // トランジションの方向を指定するためのenum
    public enum SlideDirection { Left, Right, Up, Down }

    /// <summary>
    /// パネルをフェードインで表示する
    /// </summary>
    /// <param name="panel">対象のUIパネル</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void FadeInPanel(GameObject panel, float duration = 0.5f, Action onComplete = null)
    {
        CanvasGroup canvasGroup = GetOrAddComponent<CanvasGroup>(panel); // CanvasGroupを取得または追加

        // 初期状態を設定
        canvasGroup.alpha = 0f;
        panel.transform.localScale = Vector3.one; // スケールをリセット
        panel.SetActive(true);

        // フェードインアニメーション
        canvasGroup.DOFade(1f, duration)
            .SetEase(Ease.OutQuad)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルをフェードアウトで非表示にする
    /// </summary>
    /// <param name="panel">対象のUIパネル</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void FadeOutPanel(GameObject panel, float duration = 0.5f, Action onComplete = null)
    {
        CanvasGroup canvasGroup = GetOrAddComponent<CanvasGroup>(panel);

        // フェードアウトアニメーション
        canvasGroup.DOFade(0f, duration)
            .SetEase(Ease.InQuad)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                panel.SetActive(false);
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルをスケールアップで表示する
    /// </summary>
    /// <param name="panel">対象のUIパネル</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void ScaleUpPanel(GameObject panel, float duration = 0.5f, Action onComplete = null)
    {
        // 初期状態を設定
        panel.transform.localScale = Vector3.zero;
        GetOrAddComponent<CanvasGroup>(panel).alpha = 1f; // 透明度をリセット
        panel.SetActive(true);

        // スケールアップアニメーション
        panel.transform.DOScale(Vector3.one, duration)
            .SetEase(Ease.OutBack)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルをスケールダウンで非表示にする
    /// </summary>
    /// <param name="panel">対象のUIパネル</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void ScaleDownPanel(GameObject panel, float duration = 0.5f, Action onComplete = null)
    {
        // スケールダウンアニメーション
        panel.transform.DOScale(Vector3.zero, duration)
            .SetEase(Ease.InBack)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                panel.SetActive(false);
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルを指定した方向からスライドインで表示する
    /// </summary>
    /// <param name="panel">対象のUIパネル(RectTransformを持つこと)</param>
    /// <param name="direction">スライドしてくる方向</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void SlideInPanel(GameObject panel, SlideDirection direction, float duration = 0.5f, Action onComplete = null)
    {
        RectTransform rectTransform = panel.GetComponent<RectTransform>();
        if (rectTransform == null)
        {
            Debug.LogError("SlideInPanel: 対象オブジェクトにRectTransformがありません。");
            return;
        }

        Vector2 originalPosition = rectTransform.anchoredPosition;
        Vector2 startPosition = GetOffScreenPosition(rectTransform, direction);

        // 初期状態を設定
        rectTransform.anchoredPosition = startPosition;
        GetOrAddComponent<CanvasGroup>(panel).alpha = 1f;
        panel.SetActive(true);

        // スライドアニメーション
        rectTransform.DOAnchorPos(originalPosition, duration)
            .SetEase(Ease.OutCubic)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルを指定した方向にスライドアウトで非表示にする
    /// </summary>
    /// <param name="panel">対象のUIパネル(RectTransformを持つこと)</param>
    /// <param name="direction">スライドしていく方向</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void SlideOutPanel(GameObject panel, SlideDirection direction, float duration = 0.5f, Action onComplete = null)
    {
        RectTransform rectTransform = panel.GetComponent<RectTransform>();
        if (rectTransform == null)
        {
            Debug.LogError("SlideOutPanel: 対象オブジェクトにRectTransformがありません。");
            return;
        }

        Vector2 targetPosition = GetOffScreenPosition(rectTransform, direction);

        // スライドアニメーション
        rectTransform.DOAnchorPos(targetPosition, duration)
            .SetEase(Ease.InCubic)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                panel.SetActive(false);
                // 元の位置に戻しておく(再表示時に備える)
                rectTransform.anchoredPosition = GetOffScreenPosition(rectTransform, GetOppositeDirection(direction));
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルを回転しながら表示する
    /// </summary>
    /// <param name="panel">対象のUIパネル</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="rotations">回転数 (デフォルトは2回転)</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void SpinInPanel(GameObject panel, float duration = 0.7f, float rotations = 2f, Action onComplete = null)
    {
        CanvasGroup canvasGroup = GetOrAddComponent<CanvasGroup>(panel);

        // 初期状態を設定
        panel.transform.localScale = Vector3.zero;
        panel.transform.rotation = Quaternion.identity; // 念のため回転をリセット
        canvasGroup.alpha = 0f;
        panel.SetActive(true);

        // 複合アニメーション
        panel.transform.DOScale(Vector3.one, duration).SetEase(Ease.OutBack).SetUpdate(true);

        // 「現在の状態(0度)」に「指定した角度から」変化させる
        panel.transform.DORotate(Vector3.zero, duration, RotateMode.FastBeyond360)
            .From(new Vector3(0, 0, 360f * rotations))
            .SetEase(Ease.OutCubic)
            .SetUpdate(true);

        canvasGroup.DOFade(1f, duration * 0.8f)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// パネルを回転しながら非表示にする
    /// </summary>
    /// <param name="panel">対象のUIパネル</param>
    /// <param name="duration">アニメーション時間</param>
    /// <param name="rotations">回転数 (デフォルトは2回転)</param>
    /// <param name="onComplete">完了時のコールバック</param>
    public void SpinOutPanel(GameObject panel, float duration = 0.7f, float rotations = 2f, Action onComplete = null)
    {
        CanvasGroup canvasGroup = GetOrAddComponent<CanvasGroup>(panel);

        // 複合アニメーション
        panel.transform.DOScale(Vector3.zero, duration).SetEase(Ease.InBack).SetUpdate(true);

        // 360度を超えて回転するようにモードを指定
        panel.transform.DORotate(new Vector3(0, 0, -360f * rotations), duration, RotateMode.FastBeyond360)
            .SetEase(Ease.InCubic)
            .SetUpdate(true);

        canvasGroup.DOFade(0f, duration)
            .SetUpdate(true)
            .OnComplete(() =>
            {
                panel.SetActive(false);
                panel.transform.rotation = Quaternion.identity;
                onComplete?.Invoke();
            });
    }

    /// <summary>
    /// オブジェクトに指定したコンポーネントが無ければ追加して取得する
    /// </summary>
    /// <param name="obj">対象のGameObject</param>
    private T GetOrAddComponent<T>(GameObject obj) where T : Component
    {
        T component = obj.GetComponent<T>();
        if (component == null)
        {
            component = obj.AddComponent<T>();
        }
        return component;
    }

    /// <summary>
    /// スライド用の画面外の座標を取得する
    /// </summary>
    /// <param name="rectTransform">対象のRectTransform</param>
    /// <param name="direction">スライド方向</param>
    private Vector2 GetOffScreenPosition(RectTransform rectTransform, SlideDirection direction)
    {
        RectTransform canvasRect = rectTransform.root.GetComponent<RectTransform>();
        Vector2 position = rectTransform.anchoredPosition;
        float panelWidth = rectTransform.rect.width * rectTransform.localScale.x;
        float panelHeight = rectTransform.rect.height * rectTransform.localScale.y;
        float canvasWidth = canvasRect.rect.width;
        float canvasHeight = canvasRect.rect.height;

        // アンカーを考慮して画面外の位置を計算 (アンカーが中央(0.5, 0.5)の場合)
        switch (direction)
        {
            case SlideDirection.Left:
                position.x = -(canvasWidth / 2 + panelWidth / 2);
                break;
            case SlideDirection.Right:
                position.x = canvasWidth / 2 + panelWidth / 2;
                break;
            case SlideDirection.Up:
                position.y = canvasHeight / 2 + panelHeight / 2;
                break;
            case SlideDirection.Down:
                position.y = -(canvasHeight / 2 + panelHeight / 2);
                break;
        }
        return position;
    }

    /// <summary>
    /// 反対の方向を取得する
    /// </summary>
    /// <param name="direction">元の方向</param>
    private SlideDirection GetOppositeDirection(SlideDirection direction)
    {
        switch (direction)
        {
            case SlideDirection.Left: return SlideDirection.Right;
            case SlideDirection.Right: return SlideDirection.Left;
            case SlideDirection.Up: return SlideDirection.Down;
            case SlideDirection.Down: return SlideDirection.Up;
            default: return direction;
        }
    }
}

以下の8種類の場面転換エフェクトのメソッドを定義しました。たぶん全部は使いません。

  • フェードイン/アウト: パネルが徐々に表示・非表示される
  • スケールアップ/ダウン: パネルが徐々に大きくなって表示される、小さくなって非表示になる
  • スライドイン/アウト: パネルが上下左右からスライドして現れる、スライドして消える
  • スピンイン/アウト: パネルが回転しながら表示・非表示される

このゲームは1つのシーン内でUIパネルの表示・非表示をして場面転換しています。なので、これらのメソッドは引数として対象のパネルを取ります。他に、アニメーション時間や完了時のコールバックを指定できます。

STEP

ヒエラルキーに空のオブジェクトを作成して、名前を「TransitionManager」にします。このオブジェクトにTransitionManagerスクリプトをアタッチします。

場面転換エフェクトの実装

これまで単にパネルのオン・オフをしていた処理を場面転換エフェクトのメソッドに置き換えます。

GameManagerスクリプトのChangeStateメソッドを修正します。

/// <summary>
/// ゲーム状態を変更する
/// </summary>
/// <param name="newState">新しいゲーム状態</param>
public void ChangeState(GameState newState)
{
    // ...省略...

    switch (newState)
    {
        case GameState.Title:
            // ...省略...
            // ここから修正
            //titlePanel.SetActive(true); // 削除
            if (previousState != GameState.Title) // 前の状態がタイトル以外ならば
            {
                // タイトルパネルをスケールアップで表示
                TransitionManager.Instance.ScaleUpPanel(titlePanel, 0.5f);
            }
            else // ゲーム開始時はエフェクトなし
            {
                titlePanel.SetActive(true); // タイトルパネルを表示
            }
            // ここまで
            // ...省略...
            break;

        case GameState.Preparing:
            // ...省略...
            // ここから修正
            //titlePanel.SetActive(false); // 削除
            // タイトルパネルをスケールダウンで非表示
            TransitionManager.Instance.ScaleDownPanel(titlePanel, 0.5f);
            // ここまで
            // ...省略...
            break;

        case GameState.Playing:
            // ...省略...
            // ここから修正
            //pausePanel.SetActive(false); // 削除
            // ポーズパネルをフェードアウトで非表示
            TransitionManager.Instance.FadeOutPanel(pausePanel, 0.5f);
            // ここまで
            // ...省略...
            break;

        case GameState.Paused:
            // ...省略...
            // ここから修正
            //pausePanel.SetActive(true); // 削除
            // ポーズパネルをフェードインで表示
            TransitionManager.Instance.FadeInPanel(pausePanel, 0.5f);
            // ここまで
            // ...省略...
            break;

        case GameState.GameOver:
            // ...省略...
            // ここから修正
            //gameOverPanel.SetActive(true); // 削除
            // ゲームオーバーパネルをスライドインで表示
            TransitionManager.Instance.SlideInPanel(gameOverPanel, TransitionManager.SlideDirection.Up, 0.5f);
            // ここまで
            // ...省略...
            break;

        case GameState.Victory:
            // ...省略...
            // ここから修正
            //stageClearPanel.SetActive(true); // 削除
            // ステージクリアパネルをスピンインで表示
            TransitionManager.Instance.SpinInPanel(stageClearPanel, 1f, 4f);
            // ここまで
            // ...省略...
            break;
    }
}

さいごに

はい、以上でタワーディフェンスの制作は終了とさせていただきます。去年の12月から始めたので、7ヶ月半かかりましたね。長かった。

成果物はいつものようにunityroomで公開する予定ですが、まだいろいろアレしたいので公開はもうちょっと先になりそうです。

でわでわ

タワーディフェンスを作る(25)

この記事が気に入ったら
いいね または フォローしてね!

シェアしてね

コメント

コメントする

目次