Unity

【Unityパズルゲーム制作入門】〇×風ゲームの作り方【1次元配列および2次元配列の両パターンを解説】

本記事内には、アフィリエイトリンクを含む場合があります

ゲーム開発で色々なミニゲームを作り、
Unityに慣れてきたら、
パズルゲーム制作にチャレンジしてみましょう!

パズルゲームは、ストーリーもないので、
ルールさえわかれば、サクッと遊べる有用なジャンルです。
(旅先でオセロやトランプをする感覚)

トランプなどのカードゲームを使って、
自分オリジナルのルールで遊んた経験のある方にとっては
Unityでパズルゲームを自作できるようになると、
遊びの創造が無限に広がるでしょう~^^

今回は、その入門として最適なパズルゲームを例に作り方を紹介します。
C#スクリプトだけ、プログラミング初心者にはとっつきにくいかもですが、頑張りましょう!

↓このような三目並べゲーム(◯×風ゲーム)を作ります

基本は知ってる!オンライン対戦ボードゲームを作りたいならコレ↓

【Unityボードゲーム開発】オンライン対戦できるターン制ゲームの作り方【マッチングに必要なロビーシステムも】私はゲーム開発の中でも、オンライン対応にこだわってゲームを開発してきました。 代表作の"Our Casual Battlefield"...

1.オブジェクトの準備

大前提として、
Unityの2Dゲームプロジェクトを選択して、起動します。

シーンにて
2DオブジェクトのSpriteのSqureを作成します。
Positionはx=-1,y=1、Scaleはxもyも0.8にします。

そのSqureに「Box Collider 2D」コンポーネントをアタッチします。

これがボードゲームの1マスに相当します。

これをCtrl+Dで9個に複製し、並べます。
図のように、-1、0、1というPositionの間隔で並べます。
x,yの座標でいえば、
(x,y)=(-1,1), (-1,0), (-1,-1), (0,1), (0,0), (0,-1), (1,1), (1,0), (1,-1)
の9個です。

scaleを0.8にしているので、ちょうど間にラインが入って、
わざわざラインを作る必要もありません^^

 

次は、ゲームとして最低限必要なUIを配置していきます。

ゲーム制作でよく使われるTextMeshProを作成します。
名前はお好みでつけ、ポジションはだいたい中央上くらいにします。

ちなみにTextMeshProを作成すると、こういうウィンドウが出ます。
上の「Import TMP Essenntials」をクリックし、最低限必要なインポートを終えたら、閉じましょう。

次に、Imageを作成します。
RectTransformの左の方の四角いアイコンをクリックして、
青矢印が広がったアイコン(全画面にストレッチ)を選択します。
その上で、Left、Top、Right、Bottomを0にすると、画面全体にImageが広がります。

Imageのcolorは、透明度のある黒に設定します。

次に、Buttonを作成します。
ドラッグ&ドロップで、先ほどのImageの配下に移動させましょう。
Buttonの配下のTextの内容は、「Retry」としておきます。
(つまり、これはゲームをリトライするためのButtonです^^)

UIのオブジェクト作成は以上です。

 

次にボードゲームで扱うコマを作成します。

SpriteのCircleとDiamondの2種類を作成します。
CircleのScaleはxとyを0.8に、
DiamondのScaleはxを0.8、yを1.6にすると、イイ感じに1マスに収まるようになります。
またOrder In Layerを1にして、
必ずマスよりも前面に来るようにしておきます。

その2つをAssetsフォルダに(整理したい人はPrefabsフォルダを作って)、
ドラッグ&ドロップしてプレファブ化します。

※プレファブ化すると、いくつもシーンに生成することができるオブジェクトになります。

一度プレファブ化したら、シーン上にはその2つは要らないので、削除します。

ゲームに必要なオブジェクトはこれで全てです(少ない!)

これらの少ない素材をスクリプトでゲームにしていきます。

2.スクリプトでゲームを動かす

Assetsフォルダで、C#Scriptを作成し、GameManagerと名付けます。

それをダブルクリックして、VS codeなどで、以下のように編集していきます。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;

public class GameManager : MonoBehaviour
{
    public GameObject prefabCircle;
    public GameObject prefabDiamond;
    public GameObject resultObj;
    public TextMeshProUGUI infoTMP;
    int nowPlayer;
    int[] board =
    {
    -1,-1,-1,
    -1,-1,-1,
    -1,-1,-1,
    }; //9個のint型の1次元配列
    bool win;
    bool draw;

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        if(win||draw)return;

        infoTMP.text = (nowPlayer +1) + "P's Turn";
        bool next = false; //手番終了や勝敗判定開始のためのフラグ
        if(Input.GetMouseButtonUp(0)) //クリック離したとき
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//Ray(光線)にメインカメラ上でタップした位置情報と方向を代入
            RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction); //hitにRayがコライダーに当たった情報を代入
            if(null !=hit.collider)
            {
                Vector3 pos = hit.collider.gameObject.transform.position; //当たったオブジェクトのポジ(座標)を取得
                int x = (int)pos.x + 1; //取得した座標から配列の要素番号に戻す1次処理、(-1,-1)なら(0,0)へ
                int y = (int)pos.y + 1;
                int idx = x + y * 3; //配列の要素番号にする2次処理
                if( -1 == board[idx]) // その要素(マス)にまだプレファブが置かれていない(=-1)場合 
                {
                    GameObject prefab = prefabCircle;
                    if(1 == nowPlayer) prefab = prefabDiamond; //プレイヤー番号が1ならDiamondのプレファブを置く
                    Instantiate(prefab, pos, Quaternion.identity);
                    board[idx] = nowPlayer; //要素の値に今のプレイヤー番号を入れて-1(空)じゃないようにする
                    next = true;//勝敗チェック可能
                }
            }
        }
        if(next)//勝敗チェック開始
        {
            win = false;
            List<int[]> lines = new List<int[]>(); //配列のリストに勝ちパターンを格納
            lines.Add(new int[] {0, 1, 2});
            lines.Add(new int[] {3, 4, 5});
            lines.Add(new int[] {6, 7, 8});
            lines.Add(new int[] {0, 3, 6});
            lines.Add(new int[] {1, 4, 7});
            lines.Add(new int[] {2, 5, 8});
            lines.Add(new int[] {0, 4, 8});
            lines.Add(new int[] {2, 4, 6});
            foreach (int[] v in lines)//長さが決まっている配列の全要素を処理するならforeach
            {
                bool issame = true;//勝ちパターンと同じかどうかのフラグ、はじめはtrueに
                for(int i = 0; i < v.Length; i++) //v(int配列)の長さは3
                {
                    int idx0 = v[0];//int配列の一番初めの要素の値をidx0に代入
                    int idx1 = v[i];//他の要素の値をidx1に代入して検証していく
                    if(0 > board[idx1]|| board[idx0] != board[idx1])
                    {
                        issame = false; //1要素でも勝ちパターンでなければfalseへ
                    }
                }
                if(issame) //勝ちパターンであるなら
                {
                    win = true;
                }
            }
            if(win)
            {
                resultObj.SetActive(true);//結果UI表示
                infoTMP.text = (nowPlayer +1) + "P Win!";//テキスト表示変更
            }
            else //勝ちでなかった場合
            {
                //引き分け
                int cnt = 0; //空いてる場所を数える用
                for (int i = 0; i < board.Length; i++)
                {
                    if(-1 == board[i]) //デフォルトでboardの要素はすべて-1なので
                    {
                        cnt++; //数えていく
                    }
                }
                if(0== cnt) //空いてる場所がない場合
                {
                    resultObj.SetActive(true);
                    infoTMP.text = "Draw!";
                    draw = true;
                }
                //手番を回す
                nowPlayer++;
                if(2 <= nowPlayer)
                {
                    nowPlayer = 0; //Player番号は0か1の繰り返し
                }
            }
        }
    }

    public void OnClickRetry()
    {
        SceneManager.LoadScene("SampleScene");
    }
}

 


スクリプトの内容は基本的なことですが、
C#というプログラミング言語の初心者には、複雑に感じるかもしれません。

それぞれの意味は、「//~」でコメント書きしているので、参考ください。

まずボードゲームを作る際は、必ずと言ってよいほど、1次元や2次元の配列を宣言して使います。
上のスクリプトでは、3行3列でたったの9マスなので、1次元配列を使った管理方法です。

今回は、シーンにすでにつくられたボードに駒を置いていくだけなので、
Start関数で、特別に何か処理することはありません。

Update関数のみでゲームは進行します。
ここに焦点を当てて、少し解説します。

画面クリックでオブジェクトを扱うなら"Rayを飛ばす"

下の文は、テンプレといってよいほど、ボードゲーム作成では頻出です。
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
カメラからみてクリックした方向に光線を飛ばす処理です。

光線が当たった判定hitを情報に、
そこにあるゲームオブジェクトのポジションをVector3 posで取得します。

ボードゲームでなければ、そこにそのままプレファブ生成することもありますが、
ボードゲームでは1マスに収まるようにプレファブを置かねばなりません。
(同じマスに置けないなどがあるため、ボードゲームでは配列を扱う宿命にあるといえます)

 

オブジェクトポジションから配列の要素番号を算出する

はじめにオブジェクトを-1,0,1を使ったキリの良いx,y座標で並べています。

なので、光線衝突で取得したゲームオブジェクトのポジションは、
-1,0,1の数値で構成される座標となります。

例えば、
当たったオブジェクトのポジション(x,y)座標が(-1,-1)は
画面でいうと下段左のSqure、
その隣の下段中央のSqureの座標は(0,-1)、
その右のSqureの座標は(1,-1)です。

9つのSqureを、下段から左、中央、右にかけて数えていくと、
その番号と配列の要素番号を対応させられれば、
ボードゲームのマス目を、1次元配列によって管理できるという仕組みです。
(こんがらがってきたら下の図を見ましょう^^;)

取得した座標(x,y)、それぞれにいったん+1して
「x+y*3」という式に入れれば、
配列の要素番号に対応する数値(整数)となります。
スクリプトでは、そのように計算し、idxという変数に代入しています。

これで配列の要素番号からマス目の座標が紐づくので、
マス目の状況を配列で管理できるようになります。

配列のint型の値によってプレファブを置けるか判定

スクリプトでは、配列の全要素の「値」を、デフォルトで-1に宣言しています。
Playerがプレファブを置くことで、この値を変更させていきます。
値はそのPlayer番号の、0または1とします。

もし、そのマス目(その配列要素)の値が-1なら、まだプレファブが置かれていない、つまり、置けるということです。

配列とは、0から始まる配列の要素番号とそれに値(intやstringなど)を格納することができます。

今回はint型の配列、要素数は9、値は全て-1なので、正確に宣言すると
int[] board = new int[9]{-1, -1, -1, -1, -1, -1, -1, -1, -1}
となります。

 

勝敗チェック

このゲームは自分のコマが3つ並べば、勝利です。
3つ並ぶパターンは数えられるくらいなので(縦3横3斜2の8つ)、
それらパターンをListに格納しておき、
毎ターン、ボードの状況がそれと同じかどうかで判定します。

ここで、二重ループを使います。
(ここから複雑になってきます!)

3つ並ぶパターン(int型配列)を要素に持つList(List<int[]> lines)をforeachで回し、
さらに、そのパターン(int[] v)の1要素ずつををforでチェックします。

※foreachはListの要素全てを処理、forは指定した回数を処理する

特に、forでは1要素目と2要素目、3要素目を比較して、boardの配列において、それらが示すマス目が同じ値(コマをおいて変化する数(0または1))かどうかチェックします。

「同じでない、もしくは、まだコマが置かれていなくて値が-1」なら、
issameという差し当たりのboolはfalseとなり、
フラグwinがtrueになることはありません。

 

引き分けの判定

毎ターン、フラグwinがtrueにならなければ、ゲームは引き分けか続行かを判定していきます。

このゲームでは、すべてのマスにコマを置いても、勝敗が付かなければ、引き分けです。

なので、board配列(の値に)に空きマス(-1)があるかないかで決めます。
空きマスチェックはforでboard配列の全要素を(board.Lengthで)調べます。

その結果、空きマスの数cntが0なら、結果UIと、TextMeshProの内容を”引き分け(DRAW)”を表示します。

なお、これらの処理をするUpdate関数の最後に手番を回します。
”勝ち”または”引き分け”にならなければ、手番が回る仕組みです。

 

3.スクリプトとオブジェクトの紐づけ

GameManager.csができたら、空のゲームオブジェクトにアタッチして、プレファブやUIのオブジェクトを紐づけます。

また、RetryボタンのOnClickにGameManagerのOnClickretry関数を紐づけます。

これでゲームが完成です!

 

パズルゲームはスクリプトの設計作業がほとんどで、
製作者のC#の理解と使いこなすスキルが求められます。

特に、配列やループ処理についての理解が必要です。
自分でアレンジしてみて、
オリジナルゲームを作ってみると理解が深まるでしょう~。

 

そして、実はここからが本題です。

実際に色々なパズルゲームを作っていくなら、
2次元配列を使って作成する方法が王道です。
(マス目ももっと多いはずなので)

なので、同ゲームを
2次元配列を使って作成するスクリプトバージョン
次の項で紹介します。

配列の理解さえあれば、むしろこっちのほうが簡単です。

4.2次元配列 ver.

先ほど作ったオブジェクトはそのままで、GameManager.csの内容を変更します。

2パターンを比べたい場合、違う名前のスクリプト(GM.csなど)で作成しましょう。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;

public class GM : MonoBehaviour
{
     public GameObject prefabCircle;
    public GameObject prefabDiamond;
    public GameObject resultObj;
    public TextMeshProUGUI infoTMP;
    int nowPlayer;

    int[,] board = new int[3, 3];//3*3のint型2次元配列を定義
    bool win;
    bool draw;
    // Start is called before the first frame update
    void Start()
    {
        //2次元配列boardの全ての値を初期化(-1に)
        for (int i = 0; i &lt; 3;i++)
        {
            for (int j = 0; j &lt; 3;j++)
            {
                board[i, j] = -1;
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        if(win||draw)return;

        infoTMP.text = (nowPlayer +1) + "P's Turn";
        bool next = false;
        if(Input.GetMouseButtonUp(0))
        {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
            if(null !=hit.collider)
            {
                Vector3 pos = hit.collider.gameObject.transform.position;
                int x = (int)pos.x + 1;
                int y = (int)pos.y + 1;
                if( -1 == board[y,x])
                {
                    GameObject prefab = prefabCircle;
                    if(1 == nowPlayer) prefab = prefabDiamond;
                    Instantiate(prefab, pos, Quaternion.identity);//ここまでは1次元配列ver.と同じ記載
                    board[y,x] = nowPlayer; //2次元配列なので[x,y]にプレイヤー番号を入れる
                    next = true;
                }
            }
        }
        if(next)//勝敗チェック開始
        {
            win = false;
            int prefabNum;//プレファブが並ぶ数をカウントする変数
        
            //横並びの調査
            for (int i = 0; i &lt; 3; i++)
            {
                prefabNum = 0;//初期化
                for (int j = 0; j &lt; 3; j++)
                {
                    if (board[i, j] == -1 || board[i, j] != nowPlayer)//空きマスまた相手のコマがある場合
                    {
                        prefabNum = 0;//並びをリセット
                    }
                    else
                    {
                        prefabNum++;//自分のコマならカウント
                    }
        
                    if (prefabNum == 3)//3つ揃ったら勝ち
                    {
                        resultObj.SetActive(true);
                        infoTMP.text = (nowPlayer +1) + "P Win!";
                        win = true;
                    }
                }
            }
        
            //縦並びの調査
            for (int i = 0; i &lt; 3; i++)
            {
                prefabNum = 0;
                for (int j = 0; j &lt; 3; j++)
                {
                    if (board[j, i] == -1 || board[j, i] != nowPlayer)
                    {
                        prefabNum = 0;
                    }
                    else
                    {
                        prefabNum++;
                    }

                    if (prefabNum == 3)
                    {
                        resultObj.SetActive(true);
                        infoTMP.text = (nowPlayer +1) + "P Win!";
                        win = true;
                    }
                }
            }
        
            //斜め並び(右上がり)の調査
            for (int i = 0; i &lt; 3; i++)
            {
                prefabNum = 0;
                int up = 0;//上ずれ用
        
                for (int j = i; j &lt; 3; j++)
                {
                    if (board[j, up] == -1 || board[j, up] != nowPlayer)
                    {
                        prefabNum = 0;
                    }
                    else
                    {
                        prefabNum++;
                    }

                    if (prefabNum == 3)
                    {
                        resultObj.SetActive(true);
                        infoTMP.text = (nowPlayer +1) + "P Win!";
                        win = true;
                    }
        
                    up++;
                }
            }

            //斜め並び(右下がり)の調査
            for (int i = 0; i &lt; 3; i++)
            {
                int down = 2;//下ずれ用
                prefabNum = 0;
        
                for (int j = i; j &lt; 3; j++)
                {
                    if (board[j, down] == -1 || board[j, down] != nowPlayer)
                    {
                        prefabNum = 0;
                    }
                    else
                    {
                        prefabNum++;
                    }

                    if (prefabNum == 3)
                    {
                        resultObj.SetActive(true);
                        infoTMP.text = (nowPlayer +1) + "P Win!";
                        win = true;
                    }
        
                    down--;
                }
            }
            if(!win)
            {
                //引き分け
                int cnt = 0; //空いてる場所を数える用
                for (int i = 0; i &lt; 3; i++)
                {
                    for (int j = 0; j &lt; 3; j++)
                    {
                        if (board[i, j] == -1) //空きマスがあったら
                        {
                            cnt++;//カウントする
                        }
                    }
                }
                    
                if(0== cnt) //空きマスがない場合
                {
                    resultObj.SetActive(true);
                    infoTMP.text = "Draw!";//引き分け処理
                    draw = true;
                }

                nowPlayer++;
                if(2 &lt;= nowPlayer)
                {
                    nowPlayer = 0;
                }
            }
            
        }
    }

    public void OnClickRetry()
    {
        SceneManager.LoadScene("SampleScene");
    }
    public void OnClickDebug()//デバッグ用(各配列の値を出力する)
    {
        for (int i = 0; i &lt; 3; i++)
        {
            for (int j = 0; j &lt; 3; j++)
            {
                Debug.Log("(i,j) = (" + i + "," + j + ") = " + board[i, j]);
            }
        }
    }
}

このようなスクリプトになります。

2次元配列の場合、すべての値を-1に初期化したい場合は、
Start関数で、for文などのループで処理する方が楽です。

また、勝敗判定は、
勝ちパターンと比較するのでなく、連続するコマを検出する方法をとっています。
こちらのほうが勝ちパターンがたくさんあるようなゲームには適しているので、ご参考ください。

勝敗判定の仕方やマス目にコマを置けるかどうかの処理が、パズルゲームの種類によって様々です。
勉強したい方は、そこに注目して勉強するのが良いと思います。

また、おまけとして、デバッグ用の関数(OnClickDebug())も追加しておきました。
何かおかしい時は、ボタンなどで呼び出して確認してみると良いでしょう。

作ったボードゲームをオンライン対戦できるようにしたいなら↓

【Unityボードゲーム開発】オンライン対戦できるターン制ゲームの作り方【マッチングに必要なロビーシステムも】私はゲーム開発の中でも、オンライン対応にこだわってゲームを開発してきました。 代表作の"Our Casual Battlefield"...
ABOUT ME
いなも@システマライフハッカー
”仙豆”を開発することを夢見て、健康食品会社で働いていたものの、2016年に出会ったロシアの武術”システマ”こそ、その糸口があると感銘し、勝手にシステマ普及活動を始める。 一方で、クリエイティブなモノ作りが好きで、DX社会で楽しみを見出せる"Unity”を活かして、”スマートかつ快適な暮らし”のヒントを発信している。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA