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

を選択すると、CanvasEventSystemがまだない場合にはシーンに追加され、TextオブジェクトがCanvasの下に追加されます。とりあえず追加したTextオブジェクトの名前を分かりやすくScoreTextに変更しておきます。

(先にイベントシステムだけ触りたい場合は単にEventSystemだけを追加しても良いです。なお、EventSystemはシーン中に複数同時に作成することは出来ません。)

f:id:chirotec:20180428172141p:plain

f:id:chirotec:20180428172137p:plain

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で、基準となる解像度を指定します。

f:id:chirotec:20180428194641p:plain

また、先ほど作られた"ScoreText"オブジェクト内のTextコンポーネントで、表示するテキストを設定することが出来ます。

現在は仮のテキストを置いておきます。

f:id:chirotec:20180428194638p:plain

 

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はこの記事の下の方で扱います。

f:id:chirotec:20180428194644p:plain

 

イベントシステムからクリックイベントを受け取るために、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);
    }

これでクリックすると、ピースが一個消え、新しいピースが一個降るようになります。

f:id:chirotec:20180428203828g:plain

繋がったピースの判定 

クリックしたピースと同じ色で繋がったピースを消すようにしたいと思います。

この辺りからやや難解になりそうですが、

まずはソース一式を貼ります。

    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 が立ったピースを全て削除します。

f:id:chirotec:20180428205428g:plain

 

基本のゲームシステムが出来上がりました。

次回は、演出を加えて豪華にしていきます!