脱出系ゲームの作り方4(押し順パズル)

脱出系ゲームの作り方 Unityプログラミング

みなさま、こんにちは。さて、前回は3桁暗号パズルを作成しました。今回は、押し順パズルを作ってみましょう。今回のものも筆者が独自に考えたものですので、間違いや非効率なところもあるかもしれませんので、ご注意ください。

使用するバージョン

Unity 6000.3.8f1

パズルの種類

脱出系ゲームでよく使われるパズルには以下のものがあります。

パズルの種類
  • 3桁暗号パズル
  • 押し順パズル
  • 並べ替えパズル

前回ご紹介した「3桁暗号パズル」は、とても拡張性の高いパズルです。桁を増やしたり、数字の代わりにアルファベットを表示したりすることで、全く別のパズルであるかのように見せることができます。

今回ご紹介する「押し順パズル」は、A、B、Cなど書かれたボタンを指示された順序どおりに押していくといった類のパズルです。こちらも、とても拡張性の高いパズルです。ボタンはいくらでも増やせるし、ボタンの表記もさまざま変えられます。一筆書きのパターン入力もこのパズルの亜種といえるでしょう。

押し順パズル

「並べ替えパズル」については、別の機会に説明します。

完成後のもの

今回は最終的に以下のようなパズルを作成します。

押し順パズル完成図

作り方(配置)

さて、やっていきましょう。前回のプロジェクトを開いてください。TitleというSceneはScene1という名称に変更してください。そして、新しくSceneを作成し、Scene2という名称にしてください。

Scene2

Scene2を開いてください。Canvasがないので作成してください。

Scene2にCanvas作成

Canvasに3つButtonをつくり、以下のように配置してください。Textも以下のように変更してください。

ABCボタンを配置

以上で配置は完了です。

作り方(プログラミング)

では、コードを書いていきましょう。やりたいことは次の通りです。

やりたいこと
  • 正解はBCABACにしたい。(つまり、押す回数は6回)
  • 直近6回の押し順を常に把握したい。
  • ボタンを押した直後に、押し順の正誤をチェックしたい。
  • 正解の時にConsoleに「おめでとう!」と表示したい。

AssetsフォルダのScriptsフォルダの中にPuzzle2というクラスファイルを作成してください。そして、classの中身は以下のようにしてください。コードは6章で解説します。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Puzzle2 : MonoBehaviour
{
    // 【1】ボタン
    [SerializeField] Button[] Buttons;

    // 【2】正解の数字
    readonly int[] Corrects = { 1, 2, 0, 1, 0, 2 }; // A=0, B=1, C=2, BCABAC=120102

    // 【3】押し順を記録しておくキュー
    Queue<int> InputQueue = new();

    // 【4】初期設定
    void Start()
    {
        // ボタンにメソッドを割り当てる
        for (int i = 0; i < Buttons.Length; ++i)
        {
            int id = i;
            Buttons[i].onClick.AddListener(() => Button_Click(id));
        }

        // キューに初期値{-1,-1,-1,-1,-1,-1}を入れる
        for (int i = 0; i < Corrects.Length; ++i)
        {
            InputQueue.Enqueue(-1);
        }
    }

    // 【5】廃棄処理
    void OnDestroy()
    {
        // ボタンのメソッドを消す
        for (int i = 0; i < Buttons.Length; ++i)
        {
            Buttons[i].onClick.RemoveAllListeners();
        }

        // キューを消す
        if (InputQueue != null)
        {
            InputQueue.Clear();
            InputQueue = null;
        }
    }

    // 【6】ABCボタン
    void Button_Click(int id)
    {
        InputQueue.Enqueue(id);
        InputQueue.Dequeue();

        if (CheckResult())
        {
            Debug.Log("おめでとう!");
        }
        else
        {
            Debug.Log("ざんねん");
        }
    }

    // 【7】結果を比較検査するメソッド
    bool CheckResult()
    {
        bool result = true;
        int i = 0;

        foreach (int val in InputQueue)
        {
            if (val != Corrects[i])
            {
                result = false;
                break;
            }
            i++;
        }

        return result;
    }
}

割り当て

HierarchyにCreate Emptyでオブジェクトを作り、Puzzleという名前にしてください。そして、Puzzle2というクラスファイルをAdd Componentで追加してください。そして、インスペクターでボタンを割り当ててください。

  • Buttons Element 0: ボタンA
  • Buttons Element 1: ボタンB
  • Buttons Element 2: ボタンC
Puzzle2の割り当て

では、プレイボタンを押してプレイしてみてください。どのボタンを押しても毎回Console出力されますよね。押し順が「BCABAC」に一致する時だけ「おめでとう!」が出力され、それ以外は「ざんねん」が出力されていれば、成功ですね!おめでとうございます!

コードの解説

さて、コードの解説をしていきます。大変ですが、頑張りましょう。

【1】はインスペクターから割り当てるボタンUIです。

【2】は正解のパターンを配列で持たせています。Aを0、Bを1、Cを2に対応させています。この数字はボタンの配列などの要素番号とも、もちろん一致します。

【3】はQueue(キュー)です。直近6回の押し順を記録するために使用します。QueueではなくListを使うこともできます。ですが、押し順パズルの場合は、Queueを使うのがベストでしょう。Queueについては【6】のところで解説します。

【4】はStart関数です。この中で、ボタンにメソッドを割り当てています。また、InputQueueに初期値として-1を6個追加しています。

【5】はOnDestroy関数です。このゲームオブジェクトが破棄される時に、ボタンのメソッドを消し、InputQueueをクリアして消しています。

    // 【1】ボタン
    [SerializeField] Button[] Buttons;

    // 【2】正解の数字
    readonly int[] Corrects = { 1, 2, 0, 1, 0, 2 }; // A=0, B=1, C=2, BCABAC=120102

    // 【3】押し順を記録しておくキュー
    Queue<int> InputQueue = new();

    // 【4】初期設定
    void Start()
    {
        // ボタンにメソッドを割り当てる
        for (int i = 0; i < Buttons.Length; ++i)
        {
            int id = i;
            Buttons[i].onClick.AddListener(() => Button_Click(id));
        }

        // キューに初期値{-1,-1,-1,-1,-1,-1}を入れる
        for (int i = 0; i < Corrects.Length; ++i)
        {
            InputQueue.Enqueue(-1);
        }
    }

    // 【5】廃棄処理
    void OnDestroy()
    {
        // ボタンのメソッドを消す
        for (int i = 0; i < Buttons.Length; ++i)
        {
            Buttons[i].onClick.RemoveAllListeners();
        }

        // キューを消す
        if (InputQueue != null)
        {
            InputQueue.Clear();
            InputQueue = null;
        }
    }

【6】がボタンのメソッドの本体です。InputQueueはQueueであり、今回はその要素はIntegerにしてあります。

Queueとは?

Queueは『操作が制限されたList』のようなものです(でもその代わり、処理が軽くて高速です)。制限というのは「先入れ先出し」という仕組みのことです。「先入れ先出し」という言葉は少々わかりにくいですが、「古いものから先に取り出す」ということです。新しく入れたものはQueueの末尾に必ず配置されます。そして、取り出す時にはQueueの先頭からしか取り出せないのです。文房具にロケット鉛筆というものがありますが、まさにそういう仕組みです入れた順番が完璧に保証されるという仕組みなのです。

InputQueue.Enqueue(id)が、Queueの末尾に要素を付け足すメソッドです。idはボタンの要素番号です。このメソッドで、押されたボタンの要素番号をQueueの末尾に付け足しているのです。

InputQueue.Dequeue()が、Queueの先頭の要素を削除するメソッドです。int a = InputQueue.Dequeue()と書けば、削除と同時に値を取り出すこともできます。今回は削除のみ行なっています。

そういうわけで、Button_Clickの冒頭で、押されたボタンのidをEnqueueで足し、Dequeue一番古いものを消すと言うことをやっています。足してすぐ消すのでInputQueueの長さは6個に維持されます。

【7】がCorrectsの正解パターンとInputQueueのパターンを比較するメソッドです。Queueの要素にはfor文ではアクセスできません。ですから、わざわざforeach文を使用しています。foreachでInputQueue要素を1つずつ抜き出して、対応するCorrectsの要素と1つずつ比較しています。不一致の場合はその場で比較を終了し、メソッドを切り上げています。

    // 【6】ABCボタン
    void Button_Click(int id)
    {
        InputQueue.Enqueue(id);
        InputQueue.Dequeue();

        if (CheckResult())
        {
            Debug.Log("おめでとう!");
        }
        else
        {
            Debug.Log("ざんねん");
        }
    }

    // 【7】結果を比較検査するメソッド
    bool CheckResult()
    {
        bool result = true;
        int i = 0;

        foreach (int val in InputQueue)
        {
            if (val != Corrects[i])
            {
                result = false;
                break;
            }
            i++;
        }

        return result;
    }
}

解説は以上です。

おわりに

今回のプログラミングコードはボタン3つに限定されるものではありません。ボタンを9つにして、3x3に並べるのも面白いかもしれません。ぜひ、ご活用していただければと思います。

コメント