ゲーム開発で色々なミニゲームを作り、
Unityに慣れてきたら、
パズルゲーム制作にチャレンジしてみましょう!
パズルゲームは、ストーリーもないので、
ルールさえわかれば、サクッと遊べる有用なジャンルです。
(旅先でオセロやトランプをする感覚)
トランプなどのカードゲームを使って、
自分オリジナルのルールで遊んた経験のある方にとっては
Unityでパズルゲームを自作できるようになると、
遊びの創造が無限に広がるでしょう~^^
今回は、その入門として最適なパズルゲームを例に作り方を紹介します。
C#スクリプトだけ、プログラミング初心者にはとっつきにくいかもですが、頑張りましょう!
↓このような三目並べゲーム(◯×風ゲーム)を作ります
基本は知ってる!オンライン対戦ボードゲームを作りたいならコレ↓

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を作成します。
名前はお好みでつけ、ポジションはだいたい中央上くらいにします。
次に、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などで、以下のように編集していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
using System.Collections; 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など)で作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
using System.Collections; 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 < 3;i++) { for (int j = 0; j < 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 < 3; i++) { prefabNum = 0;//初期化 for (int j = 0; j < 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 < 3; i++) { prefabNum = 0; for (int j = 0; j < 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 < 3; i++) { prefabNum = 0; int up = 0;//上ずれ用 for (int j = i; j < 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 < 3; i++) { int down = 2;//下ずれ用 prefabNum = 0; for (int j = i; j < 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 < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i, j] == -1) //空きマスがあったら { cnt++;//カウントする } } } if(0== cnt) //空きマスがない場合 { resultObj.SetActive(true); infoTMP.text = "Draw!";//引き分け処理 draw = true; } nowPlayer++; if(2 <= nowPlayer) { nowPlayer = 0; } } } } public void OnClickRetry() { SceneManager.LoadScene("SampleScene"); } public void OnClickDebug()//デバッグ用(各配列の値を出力する) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { Debug.Log("(i,j) = (" + i + "," + j + ") = " + board[i, j]); } } } } |
このようなスクリプトになります。
2次元配列の場合、すべての値を-1に初期化したい場合は、
Start関数で、for文などのループで処理する方が楽です。
また、勝敗判定は、
勝ちパターンと比較するのでなく、連続するコマを検出する方法をとっています。
こちらのほうが勝ちパターンがたくさんあるようなゲームには適しているので、ご参考ください。
勝敗判定の仕方やマス目にコマを置けるかどうかの処理が、パズルゲームの種類によって様々です。
勉強したい方は、そこに注目して勉強するのが良いと思います。
また、おまけとして、デバッグ用の関数(OnClickDebug())も追加しておきました。
何かおかしい時は、ボタンなどで呼び出して確認してみると良いでしょう。
作ったボードゲームをオンライン対戦できるようにしたいなら↓
