【Unity】正解するまで無限ループする扉の実装方法

unity

この記事を読むと以下の動画みたいなことができます。

この扉のロジックは正解であれば、扉の番号がプラス1され、ハズレであればもう一度同じ扉が現れます。

準備

unityのバージョンは2022.3.32f1です。

扉のアセットは以下無料のドアを使います。Unity Asset Storeで追加して、package managerからダウンロード、インポートしてください。

Door Free Pack Aferar | 3D Interior | Unity Asset Store
Elevate your workflow with the Door Free Pack Aferar asset from Andrey Ferar. Find this & other Interior options on the ...

DOTweenも使用します。同様にアセットストアからダウンロードし、Unity内でセットアップをしてください。以下の記事をご参考ください。

カメラ演出は、Cinemachine を使っています。Cinemachineはアセット同様にpackage managerからインストールできます。

Cinemachineの入れ方

package managerのタブを開き、左上のプラスの右にpackagesというのがあるので、ここをUnity Registryに変更してください。そうするとCinemachineが見つかるのでインストールしてください。

どうやってやっているの?

順を追って説明するよ!

ゲームの仕様

  1. 複数の扉のうち、正解の扉を振る
  2. 正解の扉を開けば、次のステージへ進める
  3. 間違えの扉を開けると、元のステージに戻される

という仕様になっているよ!

スクリプト

スクリプトは3つ書きました!理解がむずかしいかもだけど、わかりやすく説明するね!

1. StageMaster.cs

このスクリプトはゲーム全体の進行管理をするマスタースクリプトです!
「今どのステージを表示してるか?」や、「ゲームが始まってるかどうか?」などの情報を、他のスクリプトからも簡単に確認できるようにしています。

シングルトン(Singleton)という仕組みを使って、どこからでも StageMaster.Instance でアクセスできるようにしてるよ!

シーンに常に1つだけ存在するようにしているよ!シングルトン使うと他のスクリプトから関数呼ぶのすごい楽になるから使って見てね!SoundManager作る時とか使えるよ

メンバ説明
GameObject[] stagesステージのプレハブを登録する配列。ゲーム内で使うステージ(扉のまとまり)をここに入れておくよ!(※インスペクターで手動登録が必要なので後ほど説明します)
GameObject currentStage現在表示されているステージ。自分でいじらなくてOK。プログラムが自動で更新してくれるよ!
bool isStartedゲームが始まったかどうかのフラグ。まだ開始してないのにドアをクリックされないように制御できるよ!
GameStart()ゲームを開始するときに呼び出す関数。isStarted を true に切り替えるだけなんだけど、とっても大事!
GetStage(int id)指定されたステージのプレハブを取り出す関数。エラー対策で、IDが配列の範囲外だったら警告を出してくれるよ!
// © yutaka ishida / matomato (https://ytk-unityblog.site/)

using UnityEngine;

/// <summary>
/// ステージ全体の状態を管理するクラス(シングルトン)
/// </summary>
public class StageMaster : MonoBehaviour
{
    // シングルトンインスタンス
    public static StageMaster Instance { get; private set; }

    [SerializeField] private GameObject[] stages; // 全ステージのプレハブ
    public bool isStarted { get; private set; } = false; // ゲーム開始フラグ
    public GameObject currentStage { get; set; } // 現在のステージインスタンス

    private void Awake()
    {
        // インスタンスがまだなければ自身を設定
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject); // シーンをまたいでも維持したい場合は有効化
        }
        else
        {
            Destroy(gameObject); // すでに存在する場合は重複防止で破棄
        }
    }

    /// <summary>
    /// ゲーム開始時に呼び出す
    /// </summary>
    public void Start()
    {
        GameStart();
        currentStage = stages[0];
    }


    /// <summary>
    /// ゲーム開始時に呼び出す
    /// </summary>
    public void GameStart()
    {
        isStarted = true;
    }

    /// <summary>
    /// ステージプレハブを取得する(読みやすさのための補助関数)
    /// </summary>
    public GameObject GetStage(int id)
    {
        if (id < 0 || id >= stages.Length)
        {
            Debug.LogWarning($"Stage ID {id} は範囲外です");
            return null;
        }

        return stages[id];
    }
}

2. StageScript.cs

このスクリプトは、「ドアを選んで、次に進む or 同じステージに戻る」というゲームの中核となる処理を担っています!またドアオブジェクトも管理してます!

  • 正解のドアをクリックしたら → 次のステージ
  • 間違えたドアをクリックしたら → 今のステージをもう一度

というロジックを管理しています!

しかもステージはカメラの前方に再生成されるので、「同じ場所をぐるぐる回っているような不思議な演出」にもなっているよ!

メンバ名内容
DoorScript[] doorsこのステージにある複数のドアを配列で管理
int stageIdこのステージが何番目かを管理(リロードのときに使う)
bool isFinalStage最終ステージかどうか。true なら Goal() を呼ぶ
GameObject currentStagePrefab次に表示するステージのプレハブ(主に使われないが残してある)
// © yutaka ishida / matomato (https://ytk-unityblog.site/)

using UnityEngine;
using DG.Tweening;

/// <summary>
/// ステージ内のドア操作やステージ遷移処理を管理するクラス
/// </summary>
public class StageScript : MonoBehaviour
{
    [SerializeField] private DoorScript[] doors; // ステージ内のすべてのドア
    [SerializeField] private int stageId; // このステージのID(リロード時などに使用)
    [SerializeField] private bool isFinalStage = false; // このステージが最終ステージかどうか
    [SerializeField] private GameObject currentStagePrefab; // 次に生成するステージのプレハブ

    /// <summary>
    /// ドアがクリックされたときに呼び出される処理
    /// </summary>
    public void OnDoorClicked(DoorScript door)
    {
        if (!StageMaster.Instance.isStarted || door.IsOpen) return;

        door.IsOpen = true;
        door.VirtualCamera?.SetActive(true);

        // ドア演出 → 成否判定 → 次ステージ or リロード の流れをDOTweenで管理
        Sequence sequence = DOTween.Sequence();
        sequence.AppendInterval(2f)
                .AppendCallback(() => door.OpenDoor())
                .OnComplete(() =>
                {
                    if (door.IsCorrectDoor)
                    {
                        if (isFinalStage)
                        {
                            Goal();
                        }
                        else
                        {
                            LoadNextStage();
                        }
                    }
                    else
                    {
                        ReloadCurrentStage();
                    }
                });
    }

    /// <summary>
    /// 最終ステージ到達時の処理(継承で拡張可能)
    /// </summary>
    protected virtual void Goal()
    {
        Debug.Log("Goal! 最終ステージに到達しました。");
    }

    /// <summary>
    /// 次のステージを読み込む
    /// </summary>
    private void LoadNextStage()
    {
        GameObject nextStagePrefab = StageMaster.Instance.GetStage(stageId++); // 任意のステージ(例: ID=10)
        if (nextStagePrefab != null)
        {
            SpawnStageAtCameraPosition(nextStagePrefab);
        }
    }

    /// <summary>
    /// 現在のステージを再読み込み
    /// </summary>
    private void ReloadCurrentStage()
    {
        GameObject reloadStagePrefab = StageMaster.Instance.GetStage(stageId);
        if (reloadStagePrefab != null)
        {
            SpawnStageAtCameraPosition(reloadStagePrefab);
        }
    }

    /// <summary>
    /// カメラ位置の前方に指定のステージを生成する
    /// </summary>
    private void SpawnStageAtCameraPosition(GameObject stagePrefab)
    {
        Vector3 cameraPos = Camera.main.transform.position;
        Vector3 spawnPos = new Vector3(cameraPos.x + 8f, 0f, cameraPos.z);

        GameObject stage = Instantiate(stagePrefab, spawnPos, Quaternion.identity);
        stage.SetActive(true);

        StageMaster.Instance.currentStage = stage;
    }
}

以下で、カメラの前方にオブジェクトがスポーンするようにしています!この 8f の部分は好きに変更してください。

Vector3 spawnPos = new Vector3(cameraPos.x +8f, 0f, cameraPos.z);

3. DoorScript.cs

このスクリプトは、各ドアの動きを管理するスクリプトです!
見た目のアニメーションだけでなく、「このドアは正解かどうか?」もここで管理してます!

変数名内容
door回転させるドア本体の GameObject(ヒンジとして動かす)
virtualCameraドアを開けたときに切り替えるカメラ(演出用)
isCorrectDoorこのドアが正解かどうか(インスペクターで設定が必要!
isClicked一度クリックされたドアかどうか。二重処理を防ぐためのフラグ
isOpen外部から「開いているかどうか」を参照したいとき用(get/set
// © yutaka ishida / matomato (https://ytk-unityblog.site/)

using UnityEngine;
using DG.Tweening;

/// <summary>
/// ドアの開閉処理と正解判定を管理するクラス
/// </summary>
public class DoorScript : MonoBehaviour
{
    [SerializeField] private GameObject door; // ドアのオブジェクト
    [SerializeField] private GameObject virtualCamera; // ドアに関連付ける仮想カメラ(演出用)
    [SerializeField] private bool isCorrectDoor; // このドアが正解かどうか

    private bool isClicked = false; // ドアがすでに操作されたか
    public bool IsOpen { get; set; } = false;

    public bool IsCorrectDoor => isCorrectDoor;
    public GameObject VirtualCamera => virtualCamera;

    /// <summary>
    /// ドアを開く処理(回転アニメーション)
    /// </summary>
    public void OpenDoor()
    {
        if (isClicked) return;

        isClicked = true;
        IsOpen = true;

        door.transform.DORotate(new Vector3(0, 90, 0), 1f, RotateMode.WorldAxisAdd)
            .SetEase(Ease.OutBack);

        virtualCamera?.SetActive(true);
    }

    /// <summary>
    /// ドアを閉じる処理(未使用なら削除可)
    /// </summary>
    public void CloseDoor()
    {
        if (!IsOpen) return;

        IsOpen = false;
        isClicked = false;

        door.transform.DORotate(Vector3.zero, 1f, RotateMode.FastBeyond360)
            .SetEase(Ease.InBack);

        virtualCamera?.SetActive(false);
    }
}

4. DoorClickHandler.cs

このスクリプトは、実際にプレイヤーがドアをクリックしたときに
「ドアが選ばれたよー!」という通知を StageScript に送る役割をしているよ!これをアタッチしているオブジェクトにBoxColiderなどのColiderが必須です!

// © yutaka ishida / matomato (https://ytk-unityblog.site/)

using UnityEngine;

/// <summary>
/// ドアのクリックイベントを検知し、StageScript に処理を渡す
/// </summary>
public class DoorClickHandler : MonoBehaviour
{
    [SerializeField] private DoorScript door; // 対象のドア(必須)

    private void OnMouseDown()
    {
        // 現在のステージが存在し、クリックされたドアが設定されているか確認
        if (StageMaster.Instance.currentStage != null && door != null)
        {
            // currentStage にアタッチされた StageScript を取得
            StageScript stageScript = StageMaster.Instance.currentStage.GetComponent<StageScript>();

            if (stageScript != null)
            {
                stageScript.OnDoorClicked(door); // ステージに通知して処理を実行
            }
        }
    }
}

実際にやってみよう!

次は新しいプロジェクトを立てて、準備の項目を済ませてください!

ドアのアセットのプロジェクトからDoorV1をプロジェクトに追加します。

Virtual Cameraを追加します!GameObejct >Cinemachine > Virtual Cameraと選択して下さい

DoorにVirtual Cameraを追加するとこんな感じ。子要素に追加してね!

ところでVirtual Cameraって何?っていうひとがいるかもだから簡単に説明すると
Virtual CameraはMain Cameraをその座標に移動させるものだと思ってればとりあえずは大丈夫だと思うよ!複数配置できて、優先順位もつけられるよ

そしたら先ほどのDoorScriptをDoorV1にアタッチしてください。その後、Doorの部分に開閉するドア部分のオブジェクトと、Virtual Cameraをアタッチしてください。IsCorrectDoorのフラグですが、一旦falseで登録します。

Virtual Cameraの位置ですが、アクティブにして扉の真正面に持ってきてください。そのあと非アクティブにして下さい!

パラメーターはこんな感じです。

ちょっと飛びますが、

・Box Colliderの追加 Box Collierは扉の形に沿ってEdit Colliderで編集して下さい。

・DoorClickHandlerのアタッチもお願いします。DoorClickHandlerに関してはDoorのオブジェクトに同じオブジェクトにアタッチしている。DoorScript指定してください。

ここまで来ればひとまずOKです!ドアの仕組みは完了。あとは簡単です!

複製のお時間です

次に今作ったドアを複製して3つにしよう!

次に、この3つを選択して親オブジェクトを作ろう。空オブジェクト生成してそこに3つ載っける感じでも大丈夫です。

そしたら、Stageと名前を変更しStageScriptをアタッチしてください。Doorsに今複製した3つを選択してください。他は一旦設定なしで大丈夫です。

また、Stageに対してVirtual Cameraを設置しましょう!3つの扉が見える位置がいいです。

今度はStageを複製してとりあえず4つにしたら、空オブジェクトを作りStageMasterと名付けます。

StageMasterにStageMaster.csをアタッチして、今複製した4つを登録しよう!

もう終盤!次に、Stage(1) ~ (3)を非アクティブ化してください

またStageScriptのstageIdを上から0順に番号を振っていってください。最後のステージであれば、IsFinalStageにチェックをして下さい!

また、Stageを展開してもらって扉の当たり外れなどはIsCorrectDoorのチェックで設定できます。とりあえずひとつだけ正解にしたいのでひとつだけをチェックにしました。

最後にカメラにPhysics Raycasterを追加してください!

これがないとクリックした時に判定してくれないです!

ついに完成!説明終わりです。

ご覧いただきありがとうございました!

問題などあればご気軽にXでDMください!

補足

Virtual Cameraの位置で生成扉の位置がズレるので以下で調整して見ください

Vector3 spawnPos = new Vector3(cameraPos.x + 5f, 0f, cameraPos.z-2);

ちょっとした豆知識

画面上のアイコンが邪魔な時があると思うんですが、Sceneウィンドウの右上の球体(画像で青いところ)をクリックして消せるよ

タイトルとURLをコピーしました