チュートリアル作成日
2020年9月9日
ゲームのルール
5種類のキャンディがヨコ5×タテ7に並んでいる。同種のキャンディがタテかヨコに3つ以上並ぶように、プレイヤーはキャンディの位置を操作する。3つ以上キャンディが並ぶとキャンディは消え、空いたスペースを埋めるようにキャンディが落下してくる。
制作環境
Unity2019.4.6f1
C#
完成予想画像
遊び方
プレイヤーはキャンディを上下左右にスワイプし、隣接するキャンディと位置を入れ替える。入れ替えた結果、タテかヨコに3つ以上同じキャンディが並ぶとそのキャンディが消える。空いたスペースを埋めるようにキャンディが落下してくる。
2D。
名前、保存場所は自由。
2~3分で、プロジェクトが作成される。
AndroidへSwitch Platform
<Unity>--<File>--<Build Settings>
Androidを選択して、<Switch Platform>をクリック。2分程度で処理が終わる。
処理が終わったら、右上Xボタンで<Build Settings>を閉じる。
画面設定を16:9 Portraitに。
<Game>ウィンドウにて、サイズを<16:9 Portrait>にする。
<Scene>の名前を変更する。
<Project>--<Scene>フォルダの<SampleScene>で右クリック。名前を<Game>に変更。
ポップアップが出るので、<Reload>をクリック。
3つのフォルダを作る。
<Project>--<Assets>にて右クリック、<Create>--<Folder>を選択。名前を<Images>にする。
同じ要領で、<Prefabs>と<Scripts>のフォルダも作成する。
キャンディの画像を入手する。
UnityのSpriteでもよいが、今回はキャンディの画像を入手する。
https://opengameart.org/content/candy-48x48
画像をUnityへ取り込む。
<Project>--<Images>フォルダ内に、ダウンロードした画像をドラッグアンドドロップ。
<Project>--<Images>--<candies>を選択。1つのキャンディが48×48ピクセルで作成されているということなので、<Inspector>にて以下のように設定。
<Texture Type>--> Sprite (2D and UI)
<Sprite Mode>--> Multiple
<Pixel Per Unit>--> 48
<Mesh Type>--> Tight
設定したら、<Sprite Editor>をクリックして、<Sprite Editor>ウィンドウを開く。その前に何かポップアップが出たら、<Apply>をクリック。
以下の画像を参考に、<Slice>--<Grid By Cell Count>--> C:8 R:2として、<Slice>をクリック。
最後に、<Apply>をクリック。
すると、16個のキャンディに分割される。
キャンディオブジェクトを作成する。
<Project>--<Images>--<candies>から、今作った16個のうち、どれかひとつを<Hierarchy>ウィンドウにドラッグアンドドロップ。
<Hierarchy>ウィンドウにあるキャンディを選択して、<Inspector>--<SpriteRenderer>--<Sprite>の「的」のようなマークをクリックすると、Unityに取り込んだ画像を選べるようになる。
今回は<candies_0>を選んだ。
赤いので、オブジェクトの名前を<RedCandy>と変更。
タグをつける。
<Inspector>--<Tag>にて、<Add Tag>。<Red>タグを新規作成して、このオブジェクトに設定する。(タグのつけ方がわからない場合、「ツムツム風チュートリアル」をやってみてください)
コライダーをつける。
<Add Component>--<Physics2D>--<Circle Collider 2D>を選択。
5種類のキャンディを作る。
今作った<RedCandy>を4つ複製し(Ctrl+D)、<SpriteRenderer>--<Sprite>にて画像を選択して、色などに応じた名前をつける。あわせてタグも作ってつける。
たとえば以下のように設定。
<RedCandy>--<Red>Tag
<BlueCandy>--<Blue>Tag
<OrangeCandy>--<Orange>Tag
<PinkCandy>--<Pink>Tag
<GreenCandy>--<Green>Tag
タグをつけ忘れないように注意する。
プレハブ化する。
<Hierarchy>ウィンドウにある5つのキャンディを、ひとつずつ<Project>--<Prefabs>フォルダにドラッグアンドドロップしてプレハブにする。
<Hierarchy>ウィンドウにて青いアイコンで表示されたらプレハブ化されたことになる。
5つすべてプレハブにしたら、<Hierarchy>ウィンドウにある5つのキャンディは削除する。
GameControllerオブジェクトを作成する。
<Hierarchy>にて、<Create Empty>を作成し、名前を<GameController>にする。
スクリプトコンポーネントを作成。
<Project>--<Scripts>フォルダ内にて右クリック、<Create>--<C# Script>を選択。名前を<GameController>とする。
<GameController>スクリプトを、<GameController>オブジェクトにアタッチ。
スクリプトをダブルクリックして、Visual Studioへ。
スクリプトを書く。
このスクリプトでやりたいことは、
・キャンディを5×7に並べる。
・タテかヨコに3つ同種のキャンディがそろったら消す。
・空いたスペースに新しくキャンディを追加する。
の主に3つである。
キャンディを5×7に並べるなら、<Candy>オブジェクトをそのように並べればよいだけだが、キャンディを消したり、空いたスペースに新しくキャンディを作ったり、ということを考えると、配列を用意して、キャンディの存在を管理しておく必要がある。
画面の見た目は<Candy>ゲームオブジェクトを表示させ、内部的には配列で管理する。
public class GameController : MonoBehaviour
{
public GameObject[] Candies;
//配列の大きさを定義。
private int width = 5;
private int height = 7;
//publicでGameObject型の配列を作る。
public GameObject[,] candyArray = new GameObject[5, 7];
void Start()
{
CreateCandies();
}
void CreateCandies()
{
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
int r = Random.Range(0, 5);
var candy = Instantiate(Candies[r]);
//画面の見た目として、candyのtransform.positionを設定
candy.transform.position = new Vector2(i, j);
//画面に5×7の表があるイメージで、キャンディの座標をそのまま配列のIndexに利用して、配列の要素にCandyを入れている。
candyArray[i, j] = candy;
}
}
}
-----------------------
保存してUnityに戻る。
<Hierarchy>--<GameController>を選択し、<Inspector>--<GameController(Script)>--<Candies>に、<Project>--<Prefabs>の5つのキャンディをアタッチ。右上のカギマークをクリックするとやりやすい。作業が終わったら、カギマークをクリックして、ロックを外す。
テストプレイすると、キャンディが並ぶが、画面の右上になってしまっている。
<Hierarchy>--<Main Camera>を選択し、<Inspector>にて、
<Transform>--<Position>--> X:2 Y:4 Z:-10
<Camera>--<Clear Flags>--> Solid Color
<Background>--> R;128 G;255 B;255
<Projection>--> Orthgraphic
<Size>--> 5.5
に設定。
背景色は、お好みで。
テストプレイして、試してみる。
CandyMoveスクリプトを作成する。
<Project>--<Scripts>フォルダ内にて右クリック、<Create>--<C# Script>を選択。名前を<CandyMove>とする。
<CandyMove>スクリプトを、5つのキャンディプレハブにアタッチ。
ひとつのプレハブを選んでおいて、<Inspector>のカギマークをクリックしてロックし、<Scripts>--<CandyMove>をドラッグアンドドロップするとやりやすい。
スクリプトを書く。
5つともアタッチできたら、<CandyMove>をダブルクリックして、Visual Studioへ。
このスクリプトでやりたいことは、
・キャンディをスワイプすると、隣のキャンディと位置が入れ替わる。
・下にスペースができたら落下する。
の主に2つである。
スワイプの考え方は、指を置いた座標と、指を離した座標を比較する、というものである。
冒頭に、以下のように記述する。
public class CandyMove : MonoBehaviour
{
//GameControllerスクリプトを使うので、指定する。
private GameController gameControllerCS;
//自身の入っている配列の座標
public int column;//列
public int row;//行
//スワイプしたときの座標を確認するための変数
private Vector2 fingerDown;
private Vector2 fingerUp;
private Vector2 distance;
//隣のキャンディ
private GameObject neighborCandy;
//3つ並んでいるとき知らせる
public bool isMatching;
//移動前の座標
public Vector2 myPreviousPos;
--------------------
isMatchingと、myPreviousPosは、あとで使うことになる。publicのものが多い。
Start関数に以下のように記述する。
void Start()
{
gameControllerCS = FindObjectOfType<GameController>();
//自分の位置を座標配列の番号(Index)にあてておく。
column = (int)transform.position.x;
row = (int)transform.position.y;
//スタート位置を記録する。
myPreviousPos = new Vector2(column,row);
}
--------------------
transform.positionはfloat型なので、(int)と前置してint型に強制的に直す。キャストという。
指を置いたときのOnMouseDown関数と、指を離したときのOnMouseUp関数を作る。これはUnityの機能なので、この名前で関数を作る。
//指をおいたとき
private void OnMouseDown()
{
fingerDown = Camera.main.ScreenToWorldPoint(Input.mousePosition);
}
//指を離したとき
private void OnMouseUp()
{
fingerUp = Camera.main.ScreenToWorldPoint(Input.mousePosition);
//2点のベクトルの差を計算
distance = fingerUp - fingerDown;
moveCandies();
}
----------------------
Camera.main.ScreenToWorldPoint(Input.mousePosition)というのが難しい。
これは、指を置いた座標を連絡するコードである。Screenとはスマートフォン画面のことで、指は画面を触る。しかし、ゲーム内部の世界はScreenではなく、World座標というもので管理されているので、スマートフォン画面の位置をゲームの世界の座標に翻訳しているのである。
moveCandies関数を作る。
長くなるが、コメントを参考に理解していただきたい。
上下左右の判定は、fingerDownとfingerUpの2点を結ぶベクトルであるdistanceを利用している。
このmoveCandies関数では、配列情報だけを操作しており、キャンディオブジェクトを動かしていないので見た目は変わらない。見た目は、あとでUpdate関数で動かす。
void moveCandies()
{
//右にスワイプしていたなら。(Mathf.Absとは絶対値を示す)
if (distance.x>=0 && Mathf.Abs(distance.x)>Mathf.Abs(distance.y))
{
//自身が一番右にいない場合、となりのキャンディと位置を交換する
if (column<4)
{
//右隣りのキャンディ情報をneighborCandyに代入
neighborCandy = gameControllerCS.candyArray[column + 1, row];
//隣のキャンディを1列左へ。
neighborCandy.GetComponent<CandyMove>().column -= 1;
//自身は1列右へ。
column += 1;
}
}
//左にスワイプしていたなら。
if (distance.x < 0 && Mathf.Abs(distance.x) > Mathf.Abs(distance.y))
{
//自身が一番左にいない場合、となりのキャンディと位置を交換する
if (column > 0)
{
//左隣りのキャンディ情報を取得
neighborCandy = gameControllerCS.candyArray[column - 1, row];
//隣のキャンディを1列右へ。
neighborCandy.GetComponent<CandyMove>().column += 1;
//自身は1列左へ。
column -= 1;
}
}
//上にスワイプしていたなら。
if (distance.y >= 0 && Mathf.Abs(distance.x) < Mathf.Abs(distance.y))
{
//自身が一番上にいない場合、となりのキャンディと位置を交換する
if (row < 6 )
{
//上のキャンディ情報を取得
neighborCandy = gameControllerCS.candyArray[column, row+1];
//隣のキャンディを1行下へ。
neighborCandy.GetComponent<CandyMove>().row -= 1;
//自身は1行上へ。
row += 1;
}
}
//下にスワイプしていたなら。
if (distance.y < 0 && Mathf.Abs(distance.x) < Mathf.Abs(distance.y))
{
//自身が一番下にいない場合、となりのキャンディと位置を交換する
if (row > 0)
{
//下のキャンディ情報を取得
neighborCandy = gameControllerCS.candyArray[column, row - 1];
//隣のキャンディを1行上へ。
neighborCandy.GetComponent<CandyMove>().row += 1;
//自身は1行下へ。
row -= 1;
}
}
}
----------------------------
column、rowやプラスマイナスを間違えないように注意する。
キャンディが移動したときに、配列の要素も入れ替える必要がある。新しく、publicなSetCandyToArray関数を作る。
//CandyArray配列に、自身を格納する。
public void SetCandyToArray()
{
gameControllerCS.candyArray[column, row] = gameObject;
}
------------------------------
次にUpdate関数を記述する。
moveCandies関数でcolumnやrowを変更されたキャンディが動くようにする。
void Update()
{
//現在の座標と、column、rowの値が異なるとき。
if (transform.position.x!=column || transform.position.y!=row)
{
//column,rowの位置に徐々に移動する。
transform.position = Vector2.Lerp(transform.position, new Vector2(column, row), 0.3f);
//現在の位置と、目的地(column,row)との距離を測る。
Vector2 dif = (Vector2)transform.position - new Vector2(column, row);
//目的地との距離が0.1fより小さくなったら。
if (Mathf.Abs(dif.magnitude)<0.1f)
{
transform.position = new Vector2(column, row);
//自身をCandyArray配列に格納する。
SetCandyToArray();
}
}
}
------------------------
キャンディの隣への移動に、Vector2.Lerpを使った。これにより、徐々に移動しているように見える。そして、目的地との距離が十分近づいたなら、キャンディを新しい目的地にセットする。
次に、下にスペースがあったら落下するFallCandy関数を作る。
void FallCandy()
{
//自分のいた配列を空にする
gameControllerCS.candyArray[column, row] = null;
//自分を下に移動させる
row -= 1;
}
-----------------
自分のいた場所をnullにしてから、自身を1行下に行くように指示する。rowの値を変えてやれば、Update関数が働いて下に移動する。
先につくったUpdate関数のif文の下に、以下を追記。
//自分が0行目(一番下)ではなく、かつ、下にキャンディがない場合、落下させる
else if (row>0 && gameControllerCS.candyArray[column,row-1]==null)
{
FallCandy();
}
---------------
else ifとする。
まだキャンディを消す処理を作っていないので、このFallCandy関数は動かない。
ここまでのところを、Unityに戻ってテストプレイしてみよう。
うまく動いただろうか?
<GameController>スクリプトへ移動。
キャンディがタテかヨコに3つ以上並んでいるかどうか判定する関数を作るが、2つ必要となる。
まずひとつは、ゲーム開始時点である。ゲーム開始のときにランダムに並んだキャンディがすでに3つそろっているときは、そこで消去してやる必要がある。
もうひとつは、プレイ中である。
内容はほぼ同一である。
Start関数でCreateCandies関数を実行して35個のキャンディを並べた直後に実行する、CheckStartset関数を作る。
3つ並んでいるかどうかの考え方は、以下のとおりである。
ヨコの並びについては、自身と右隣りともう一つ右隣りのキャンディのタグが同じならば、3つ並んでいると考えられる。
キャンディは横に5つ並んでいる。
01234
ooooo <--Candy
したがって、まず(012)を比較し、次に(123)を比較し、最後に(234)を比較すればよい。(0123)と4つ並んでいる場合でもこの方法で対処できる。
タテも同様に、自身とひとつ上と二つ上のキャンディのタグが同じならば、3つ並んでいる。
6 o
5 o
4 o
3 o
2 o
1 o
0 o
と、7つ並んでいる。
下から(012)、(123)、と比較していき、(456)まで比較すればよい。
void Start()
{
CreateCandies();
}
void CreateCandies()
{
(中略)
CheckStartset();
}
void CheckStartset()
{
//下の行からヨコのつながりを確認
for (int i = 0; i < height; i++)
{
//右から2つ目以降は確認不要(width-2)
for (int j = 0; j < width-2; j++)
{
//同じタグのキャンディが3つ並んでいたら。X座標がjなので注意。
//念のため、ふたつの式それぞれをカッコで囲んでいる。
if ((candyArray[j,i].tag==candyArray[j+1,i].tag) && (candyArray[j, i].tag == candyArray[j + 2, i].tag))
{
//CandyのisMatchingをtrueに
candyArray[j, i].GetComponent<CandyMove>().isMatching = true;
candyArray[j + 1, i].GetComponent<CandyMove>().isMatching = true;
candyArray[j + 2, i].GetComponent<CandyMove>().isMatching = true;
}
}
}//
//左の列からタテのつながりを確認
for (int i = 0; i < width; i++)
{
//上から2つ目以降は確認不要。height-2
for (int j = 0; j < height-2; j++)
{
//Y座標がj。
if ((candyArray[i,j].tag==candyArray[i,j+1].tag) && (candyArray[i,j].tag==candyArray[i,j+2].tag))
{
candyArray[i, j].GetComponent<CandyMove>().isMatching = true;
candyArray[i, j+1].GetComponent<CandyMove>().isMatching = true;
candyArray[i , j+2].GetComponent<CandyMove>().isMatching = true;
}
}
}
}
同じく<GameController>スクリプトの冒頭に、以下を追記。
private List<GameObject> deleteList = new List<GameObject>();
-------------------------
そして、先ほどのCheckStartset関数の続きに、以下を追記。
//isMatching=trueのものをListに入れる
foreach (var item in candyArray)
{
if (item.GetComponent<CandyMove>().isMatching)
{
deleteList.Add(item);
}
}
//List内にキャンディがある場合
if (deleteList.Count>0)
{
//該当する配列をnullにして(内部管理)、キャンディを消去する(見た目)。
foreach (var item in deleteList)
{
candyArray[(int)item.transform.position.x, (int)item.transform.position.y] = null;
Destroy(item);
}
//Listを空っぽに。
deleteList.Clear();
//空欄に新しいキャンディを入れる。
SpawnNewCandy();
}
--------------------------
消したキャンディの入っていた配列をnullにしてやるところがポイント。
前節の最後にSpawnNewCandy関数を実行するように記述した。
したがって、SpawnNewCandy関数を新しく作る。
考え方は、配列がnullとなっているところに、キャンディを生成して入れてやる、というものである。
//空欄に新しいキャンディを生成
void SpawnNewCandy()
{
for (int i = 0; i < width; i++)
{
for (int j = 0; j < height; j++)
{
if (candyArray[i, j] == null)
{
int r = Random.Range(0, 5);
var candy = Instantiate(Candies[r]);
//見た目の処理
candy.transform.position = new Vector2(i, j+0.3f);
//内部管理の処理
candyArray[i, j] = candy;
}
}
}
}
-----------------------
キャンディの発生位置を(i,j+0.3f)として、少し上にずらしてある。別に(i,j)でも構わない。少しだけ上にしてやることで、キャンディが画面に現れたときに、ちょっと下に落ちる感じになる。
これでゲーム開始時の基本的なスクリプトは記述した。
ゲーム開始時の動きとしては、まずCreateCandies関数でランダムに35個ならべ、CheckStartset関数で、その中に3つ以上並んでいるものがあれば消去し、SpawnNewCandy関数で空いたところに新しくキャンディを入れ、また3つ以上並んでいるところがないか確認し、あれば消去し、新しくキャンディを入れる、ということを繰り返し、最終的に3つ以上キャンディがそろっていない状態になればゲーム開始となる。
したがって、SpawnNewCandy関数からCheckStartset関数につなげてやる必要があるのだが、それだけだと延々とループ状態になってしまうので、ループを抜け出すように印をつける必要がある。
<GameController>スクリプトの冒頭に、以下を追記。
private bool isStart;
-----------------
そして、SpawnNewCandy関数の最後に以下を追記。
//まだゲーム開始してないときは3つ揃ってないか確認。
if (isStart == false)
{
CheckStartset();
}
-----------------
そしてCheckStartset関数に以下を追記。
//List内にキャンディがある場合
if (deleteList.Count>0)
{
(中略)
}
else//Listにキャンディがない場合。
{
//ゲーム開始。
isStart = true;
}
-----------------
これでゲームをスタートできる。deleteListにキャンディがない場合、isStart=trueにしている。これにより、SpawnNewCandy関数からCheckStartset関数につながらなくなる。
長くなったので、後編に続く。