Unityのゲーム作りをさくっと解説(4)
久しぶりの更新です(二度目)
やっと時間がとれそうです……m(__)m
今回はクリックしたときの判定など、よりゲームらしくしていきます。
GUIを先に作ってしまう(uGUI)
説明が前後してしまいますが、GUI関連のオブジェクトだけを先に置かせていただきます。(クリック判定にイベントシステムを使いたいので)
UnityのGUIの歴史
昔のUnityのバージョンにもGUIシステムは一応ありましたが、それがゲーム中で使うにはあまりにもショボかったため昔は皆 NGUI 等の有料アセットを利用していました。
しかし、Unity4.6あたりで公式に新しいGUIが実装されました。このシステム公式では「Unity の新しい UI システム 」等と書かれていますが、一般的には「uGUI」と呼ばれています。(古い方のGUIシステムはエディタ等で使うため IMGUI として現在でも残っています。)
uGUIをシーンに追加
さっそくuGUIをシーンに追加してみましょう。
GameObject => UI => Text
を選択すると、CanvasとEventSystemがまだない場合にはシーンに追加され、TextオブジェクトがCanvasの下に追加されます。とりあえず追加したTextオブジェクトの名前を分かりやすくScoreTextに変更しておきます。
(先にイベントシステムだけ触りたい場合は単にEventSystemだけを追加しても良いです。なお、EventSystemはシーン中に複数同時に作成することは出来ません。)
CanvasとTextの設定
Canvasの設定を少しだけ触ってみます。
Canvasでは、UIをシーン内のどこに描画するかを設定します。Render ModeをScreen Space - Cameraとすると、カメラに合わせて描画します。
- Render Cameraで描画する基準となるカメラの指定を忘れずに。
- Plane DistanceではUIを描画するカメラからの距離を設定します。普通はオブジェクトの手前にUIが描画されるようにします。
Canvas Scalerでは、画面のサイズが変更されたときにどのようにスケーリングするかを設定します。UI Scale ModeをScale With Screen Sizeにすると、画面サイズに合わせてスケーリングします。
- Reference Resolutionで、基準となる解像度を指定します。
また、先ほど作られた"ScoreText"オブジェクト内のTextコンポーネントで、表示するテキストを設定することが出来ます。
現在は仮のテキストを置いておきます。
GameManagerのシングルトン化
GameManagerを他のオブジェクトのプログラム中から触りたいので、シングルトンとして扱います。この場合GameManagerもしくはそのアタッチされたGameObjectを一つしか使わない前提となります。シングルトンって何?と気になる方は「シングルトンパターン」等で調べてみて下さい。
基本はGameManagerクラスのstatic変数を用意して、作成時に自身を入れる、これだけです。
多重作成を防ぐために、代入時に判定し、すでにある場合はDestroyします。
また、外部からはgetのみを呼べるようにし、setは呼べないようにしておきます。
現在はまだ行いませんが、シーン遷移した場合もこのGemeObjectを残すようにDontDestroyOnLoad()を呼び出しておきます。
// シングルトン
private static GameManager _instance = null;
public static GameManager Instance
{
get{return _instance;}
}
(略)
private void Awake()
{
if (Instance == null)
{
_instance = this;
}
else
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
}
クリックイベントの処理
ここまでで前準備が終わりました。
ピースをクリックした判定を、 イベントシステム で受け取り、処理を行います。
ピース側の処理
まず下記のようなPieceControl.csスクリプトを作り、各ピースのプレハブにアタッチしておきます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems; // ← 必要
public class PieceControl : MonoBehaviour, IPointerClickHandler{
// ピースの種類(色)
public int PieceType;
// 選択されたピースのフラグ
public bool SelectFlag;
// 選択されたピースに接続しているかのフラグ
public bool JoinFlag;
(略)
public void OnPointerClick(PointerEventData eventData)
{
GameManager.Instance.OnClickPiece(this);
}
}
ついでに自身の色の情報も割り当てます。各ピースのプレハブのPieceTypeに、異なる数値を割り当てておきます。
SelectFlag, JoinFlagはこの記事の下の方で扱います。
イベントシステムからクリックイベントを受け取るために、IPointerClickHandlerインターフェースを継承しています。
Colliderは既に(衝突判定を行うために)追加済みですので、実行中にGame画面をクリックするとOnPointerClick()関数がイベントシステムより呼び出されます。
クリックされたことを検知したときに、GameManager側のOnClickPiece()を呼び出します。GameManagerはシングルトン化しているので、FindComponent()等で検索しなくても済みます。
GameManager側の処理
GameManagerクラスに、次の処理を追加・変更します。
まずは、毎回ピースをシーン内から検索しなくても済むように、リストで管理します。
Update()の生成処理も書き換えます。
// 生成したピースのインスタンス
public List<PieceControl> pieceList;// ピースがクリックされたときに呼ばれる
(略)
void Update () {
// ------------- ピース生成処理 ---------------
// 一定回数呼ばれる毎にピース生成処理を行う
tCount++;
if (tCount >= genFrame)
{
if (pieceList.Count < pieceCountMax) // ←リストから判定
{
// 存在するピースの数が一定数以下の場合、新しくピースを生成する
int pieceColor = Random.Range(0, 4);
float pieceX = Random.Range(-2.0f, 2.0f);
float pieceY = 6.0f;
GameObject pieceObject = Instantiate(piecePrefab[pieceColor], new Vector3(pieceX, pieceY), Quaternion.identity);
pieceList.Add(pieceObject.GetComponent<piececontrol>()); // ←追加
}
tCount = 0;
}
}
// 対象のピースを削除する
public void EracePiece(PieceControl piece)
{
// リストから除去
pieceList.Remove(piece);
Destroy(piece.gameObject);
}
単純に C#のListコレクション で管理しています。生成時にリストに追加し、削除時にリストから除去しています。
次にGameManagerにクリック処理が呼ばれたときの関数を追加します。前段落のGameManager.Instance.OnClickPiece(this);で呼び出しているところです。
// ピースがクリックされたときに呼ばれる
public void OnClickPiece(PieceControl _piece)
{
EracePiece(_piece);
}
これでクリックすると、ピースが一個消え、新しいピースが一個降るようになります。
繋がったピースの判定
クリックしたピースと同じ色で繋がったピースを消すようにしたいと思います。
この辺りからやや難解になりそうですが、
まずはソース一式を貼ります。
public void OnClickPiece(PieceControl _piece)
{
// クリックしたピースとの接続判定を行います。
OnSelectPiece(_piece);
// 接続されたピースを全て消します。
pieceList.FindAll(x => x.JoinFlag).ForEach(x =>
{
EracePiece(x);
});
}
// ピースが選択されたときに呼ばれる
public void OnSelectPiece(PieceControl _piece)
{
// フラグを一旦クリア
pieceList.ForEach(x => { x.SelectFlag = false; x.JoinFlag = false; });
pieceList.Find(x => x == _piece).SelectFlag = true;
// ------------接続されたピースを探索します。------------------
// 接続されたピースのリスト
List<PieceControl> joinedPieceList = new List<PieceControl>();
joinedPieceList.Add(_piece);
// 接続判定対象の残りのピース
List<PieceControl> fieldPieceList = new List<PieceControl>(pieceList);
fieldPieceList.Remove(_piece);
// 接続されたピースのフラグをON
while (true)
{
List<PieceControl> addJoinedList = new List<PieceControl>();
foreach (var joinedPiece in joinedPieceList)
{
foreach (var fieldPiece in fieldPieceList)
{
// 色が違うか
if (fieldPiece.PieceType != joinedPiece.PieceType) continue;
// ピース間の距離が一定以上か
float d = Vector3.Distance(fieldPiece.transform.position, joinedPiece.transform.position);
if (d <= pieceRadius * 3.0f) // 半径1個分のスペースは許容する
{
// これも接続する。接続リストの末尾に追加する。
addJoinedList.Add(fieldPiece);
}
}
}
// もう接続ピースが無い場合は抜けます。
if (addJoinedList.Count == 0) break;
// 接続リストに追加します。
joinedPieceList.AddRange(addJoinedList);
// 未接続リストから除外します。
addJoinedList.ForEach(x=> fieldPieceList.Remove(x));
}
// 接続フラグを立てます。
joinedPieceList.ForEach(x => x.JoinFlag = true);
}
特に
pieceList.FindAll(x => x.JoinFlag).ForEach(x => EracePiece(x););
て何だ! と感じる方が多いと思われます。
これはラムダ式と言います。上の例では最初のFindAll()関数ではpieceListの要素のうちからJoinFlagが立ったオブジェクトを集めたものをリストとして取得して、次のForEach()関数では前述のリストの各要素毎に、EracePiece()関数を呼び出しています。
そもそもListって何? コレクションって何? なC#やプログラムの初心者さん向けでやる内容じゃないと思いますが
許してください(´・ω・`)
代わりにラムダ式を使わないでforeach文などを使って書くと以下のようになります。
public void OnClickPiece(PieceControl _piece)
{
// クリックしたピースとの接続判定を行います。
OnSelectPiece(_piece);
// 接続されたピースを全て消します。
List<PieceControl> listJoinedPiece = new List<PieceControl>();
foreach(var x in pieceList)
{
if (x.JoinFlag) listJoinedPiece.Add(x);
}
foreach(var x in listJoinedPiece)
{
EracePiece(x);
}
}
// ピースが選択されたときに呼ばれる
public void OnSelectPiece(PieceControl _piece)
{
// フラグを一旦クリア
foreach (PieceControl p in pieceList)
{
p.SelectFlag = false;
p.JoinFlag = false;
}
// 選択したピースのフラグをON
foreach (PieceControl p in pieceList)
{
if (p == _piece)
{
p.SelectFlag = true;
break;
}
}
// ------------接続されたピースを探索します。------------------
// 接続されたピースのリスト
List<PieceControl> joinedPieceList = new List<PieceControl>();
joinedPieceList.Add(_piece);
// 接続判定対象の残りのピース
List<PieceControl> fieldPieceList = new List<PieceControl>(pieceList);
fieldPieceList.Remove(_piece);
// 接続されたピースのフラグをON
while (true)
{
List<PieceControl> addJoinedList = new List<PieceControl>();
foreach (var joinedPiece in joinedPieceList)
{
foreach (var fieldPiece in fieldPieceList)
{
// 色が違うか
if (fieldPiece.PieceType != joinedPiece.PieceType) continue;
// ピース間の距離が一定以上か
float d = Vector3.Distance(fieldPiece.transform.position, joinedPiece.transform.position);
if (d <= pieceRadius * 3.0f) // 半径1個分のスペースは許容する
{
// これも接続する。接続リストの末尾に追加する。
addJoinedList.Add(fieldPiece);
}
}
}
// もう接続ピースが無い場合は抜けます。
if (addJoinedList.Count == 0) break;
// 接続リストに追加します。
joinedPieceList.AddRange(addJoinedList);
// 未接続リストから除外します。
foreach(var x in addJoinedList)
{
fieldPieceList.Remove(x);
}
}
// 接続フラグを立てます。
foreach (var x in joinedPieceList)
{
x.JoinFlag = true;
}
}
どちらが分かりやすいでしょうか?
初心者向けではないとは思いますが、この考え方は色々な言語で役に立つと思います。多分。
話を戻します。
上記の処理のうちOnSelectPiece()では、選択したピースと同色で繋がっているピース全てをリストアップし、JoinFlag を立てています。全てのピースの繋がりを調べて、繋がっているピースをjoinedPieceListに貯め、最後にJoinFlag を立てます。
次に JoinFlag が立ったピースを全て削除します。
基本のゲームシステムが出来上がりました。
次回は、演出を加えて豪華にしていきます!