Developer

【初心者Unity】懐かしのゲームを作ってみよう!⑭
2022.02.25
Lv1

【初心者Unity】懐かしのゲームを作ってみよう!⑭


はじめに

今回は前回に引き続き、↓のゲームを作っていきます。

前回、ゲームオーバーの時の動きが完成しました。

ゲームオーバーと言えば次は。。。そう、ゲームクリアですよね!
ということで今回はゲームクリアの動きを実装していきましょう!
また、今回はゲームオーバーの時と流れがさほど変わらないので、サクッと進んで行きます。
細かいところが気になる方はゲームオーバーの記事も読みつつ、ついてきてください!


「GameClear」の文字を画面に出す

それではまずはゲームクリアを表す「GameClear」の文字を画面に出しましょう。
出し方は「GameOver」の時と同じ。
Canvasの子オブジェクトとしてTextを追加し、TextのInspectorを操作すれば完成です。
CanvasにTextを追加するので、Hierarchy上の「Canvas」を右クリックし、「UI」→「Text」で追加します。

ここで、Textが2つもあると我々にとってもプログラムにとっても判別しにくく不便なので、それぞれ名前を「GameOver」と「GameClear」に変更しましょう(名前を逆にしないように気を付けてくださいね)。

GameOverの方ですが、BallScriptで名前を少々利用しているので、今回の改名に伴ってBallScriptを↓の様に変更しましょう。

public class BallScript : MonoBehaviour
{
    Vector2 pos;
    int directionX;
    int directionY;
    GameObject gameOver;
    // Start is called before the first frame update
    void Start()
    {
        pos = transform.position;
        directionX = 1;
        directionY = 1;
        gameOver = GameObject.Find("Canvas").transform.Find("GameOver").gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        //斜め45度に進ませる
        Move();

        //右端に着いた時の処理
        if (IsRightEdge())
        {
            directionX = -1;
        }

        //左端に着いた時の処理
        if (IsLeftEdge())
        {
            directionX = 1;
        }

        //上端に着いた時の処理
        if (IsUpperEdge())
        {
            directionY = -1;
        }

        //下端に着いた時の処理
        if (IsLowerEdge())
        {
            directionX = 0;
            directionY = 0;
            gameOver.SetActive(true);
        }
    }

    void Move()
    {
        pos.x += 0.03f * directionX;
        pos.y += 0.03f * directionY;
        transform.position = pos;
    }

    bool IsRightEdge()
    {
        return pos.x > 2.5;
    }
    
    bool IsLeftEdge()
    {
        return pos.x < -2.5;
    }
    
    bool IsUpperEdge()
    {
        return pos.y > 5;
    }
    
    bool IsLowerEdge()
    {
        return pos.y < -5;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.name == "Paddle")
        {
            directionY = 1;
        }
        if (collision.gameObject.tag == "Block")
        {
            directionX = -directionX;
            directionY = -directionY;
        }
    }
}

次に、GameClearの方のプロパティを↓の様に調整します。

Pos X Pos Y Pos Z Width Height Text Font Size
0 300 0 1000 200 Game Clear 160

↓の様に現れたのではないでしょうか?

次に、文字が左によってしまっているのを↓の様に調整しましょう。

次に、色が背景と同化していて見にくいので文字の色を変更します。
今回はGameClearっぽく緑でいきましょう!

最後に、これはおまけですが文字をより目立たせるために太文字にしましょう。

ゲームクリアになるまで「GameClear」は画面に出てこないようにする

今、GameClearを画面に出すことができましたが、ゲームプレイ中この文字がずっと画面上に出ているのはおかしいですよね?
ということで一旦GameClearを画面上から消しましょう。
これはプログラム関係なく、画面上の操作で簡単に実装できます。
消したいオブジェクト、今回は「GameClear」を選択し、Inspecter上で名前の左のチェックを外す。これだけ!
いかがでしょうか?Hierarchy上では「GameClear」が残りつつ、画面から「GameClear」の文字が消えたのではないでしょうか?

ゲームクリアのタイミングはいつなのか?

GameClearを消したところで、次に考えなければならないのが、いつこの文字を出すか。つまりゲームクリアの定義をどうするか、です。
まあゲームクリアの定義は人それぞれかもしれませんが、今回はそれっぽく、すべてのBlockが消えたら、にしましょう。

「GameClear」の実装

GameClearを作成し、通常時は「GameClear」の文字を消し、ゲームクリアのタイミングが決まったところで、いよいよゲームクリアの動きを実装していきましょう。
実装の仕方ですが、いつも通りまずは言語化してからプログラムに落とし込みましょう。
今回実装したいプログラムを言語化すると。。。
「すべてのBlockが消えたら、GameClearの文字が現れる。」となります。
今回の主人公となるオブジェクトはGameClearなので、GameClearに新たにスクリプトを貼り付ければ良いのではないか、と今までの流れならなるのですが、前回同様、今回はGameClear自体には貼り付けません。
理由としては、ゲーム開始時にGameClearは無効化(Hierarchy上で透明に)されているからです。
無効化されているオブジェクトにアタッチされたスクリプトのメソッド(UpdateやOnTriggerEnter2D等)はゲームプレイ中に呼び出されないため、GameClearにアタッチしたスクリプトにプログラムを書いても動いてくれません。
そこで今回はゲームクリアのきっかけを作るオブジェクトであるBlockのスクリプトにプログラムを書いていきましょう。
しかしここでまたしても問題があることに気づきます。BlockのスクリプトでGameClearを制御したいところですが、Blockはどんどん消えて行ってしまいます。
消えゆくオブジェクトのスクリプトで制御するのは少々不安があります。。。
そこで、今回は消えゆくBlock達の親オブジェクトとして「Blocks」を作成し、BlocksのスクリプトBlocksScriptでGameClearを制御しましょう。
「Blocks」はあくまでBlock達を取りまとめる存在としてあれば良いので、ゲーム上に現れないオブジェクトである「空オブジェクト」で作成しましょう。
空オブジェクトの作成の仕方は、上のメニューバーから「GameObject」→「CreateEmpty」で作成できます。

名前を「Blocks」に変更したらBlock達をBlocksにドラッグアンドドロップしていきましょう。

因みに、この空オブジェクト、初期位置が変な数字になっているかと思うので、Inspector→Transform.Positionのx,y,zの値を全て0にしておきましょう。
さて、Block達の移動が完了したところで、「AddComponent」→「NewScript」でBlocksScriptを作成しましょう。

さあ、スクリプトの準備ができたところでいよいよプログラムにしていきますが、その前に今回の実装を言語化した「すべてのBlockが消えたら、GameClearの文字が現れる。」をよりプログラムに近づけていきます。

すべてのBlockが消えたら、GameClearの文字が現れる。

Blocksの子オブジェクトが全て無効化されたら、GameClearが有効化される

つまりBlocksは自分の子オブジェクトが全て無効化されたかどうかを判定する必要があります。
そこでBlocksにはStartメソッドで子オブジェクトの数を数えさせます。
そして無効化されたBlock(子オブジェクト)を数えていき、Blocksが最初に持っていた子オブジェクトの数と同数のBlockが無効化されたらGameClearを有効化する。という実装にしましょう。
まずは無効化されたBlock(子オブジェクト)を数える変数をBlocksScriptに用意しましょう。
子オブジェクトを数える変数なので「cntChildren」(countChildrenから)と定義しましょう。数えるので型はintです。
また、最初に無効化されているオブジェクトは0個なので、Startメソッド内で0で初期化しましょう。

public class BlocksScript : MonoBehaviour
{
    int cntChildren;
    // Start is called before the first frame update
    void Start()
    {
        cntChildren = 0;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

次に、GameClearが有効になる条件を実装します。
GameClearが有効になるのは「cntChildrenの値がBlocksの子オブジェクトの数と同じになった時」です。
ここで、子オブジェクトの数を数える必要が出てきますが、これは便利な定数「transform.childCount」が与えられています。
これの定数を利用することで、↓の様に実装することができます

public class BlocksScript : MonoBehaviour
{
    int cntChildren;
    // Start is called before the first frame update
    void Start()
    {
        cntChildren = 0;
    }

    // Update is called once per frame
    void Update()
    {
        if (cntChildren == transform.childCount)
        {
            //GameClearを有効化
        }
    }
}

次に、GameClearを有効化する機能の実装ですが、これはGameOverの有効化の実装と同じ手順で実装することができます。

GameObject型の変数「gameClear」を作成。

Startメソッドで変数gameClearにオブジェクトGameClearを参照させる。

ゲームクリアのタイミングで、gameClearをSetActive(true)で有効化。

これを実装すると↓の様になります。

public class BlocksScript : MonoBehaviour
{
    int cntChildren;
    GameObject gameClear;
    // Start is called before the first frame update
    void Start()
    {
        cntChildren = 0;
        gameClear = GameObject.Find("Canvas").transform.Find("GameClear").gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        if (cntChildren == transform.childCount)
        {
            gameClear.SetActive(true);
        }
    }
}

これで完成!かと思いきや、まだ1つやり残しがあります。
そうです、無効化された子オブジェクトの数を数える方法です。
このままではcntChildrenはずっと0のままで、いつまでたってもGameClearが有効化される条件を満たせません。
そこで、最後にcntChildrenをカウントアップさせる機能を実装します。
cntChildrenをカウントアップさせるのは実はそんなに難しくありません。厄介なのはその条件です。
そこでまずはその条件を言語化します。
カウントアップの条件は「Blockが無効化されること」です。つまり、この条件は親オブジェクトのスクリプトBlocksScriptよりも子オブジェクトのスクリプトBlockScriptで制御した方が便利です。
そこで今度はBlockScriptを開きましょう。
BlockScriptの中で、Blockが無効化されるタイミングを制御しているのはどこか見てみると、OnTriggerEnter2D()メソッドです。
そこで、この中にcntChildrenのカウントアップの処理を実装すればよいことが分かります。

public class BlockScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        this.gameObject.SetActive(false);
        //cntChildrenのカウントアップ処理
    }
}

カウントアップの処理ですが、要するに「cntChildrenに1足す」を実装すれば良いので、「cntChildren++」でよさそうな気がしますが、実はだめです。
なぜなら、変数cntChildrenはBlockScriptが持っているのではなく、BlocksScriptが持っているからです。
そこで、「BlocksにアタッチされたBlocksScriptのcntChildren」というように変数を指定する必要があります。
これをプログラムで書くと「blocks.GetComponent().cntChildren++;」となります。
ややこしいですが割と日本語まんまですよね?(笑)
ただ、ここで要注意なのが変数「blocks」が未定義なことです。
そこで、変数「blocks」をGameObject型で定義し、Startメソッドで変数blocksの参照をゲームオブジェクトBlocksにします。
Startメソッドの処理は、BlocksがBlock達の親メソッドであることを利用すると、少し簡単に書くことができます。
「Blocksはこのオブジェクトの親である」というニュアンスで、「blocks = transform.parent.gameObject;」で参照できます。
これらを実装すると↓の様になります。

public class BlockScript : MonoBehaviour
{
    GameObject blocks;
    // Start is called before the first frame update
    void Start()
    {
        blocks = transform.parent.gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        this.gameObject.SetActive(false);
        blocks.GetComponent<BlocksScript>().cntChildren++;
    }
}

しかしどうでしょう。正しく実装したにもかかわらず、19行目がコンパイルエラーしてしまっているのではないでしょうか?
この原因は、BlockScriptがBlocksScriptのcntChildrenを見ることができていないことが原因となります。
なぜ見れないのかというと、BlocksScriptでのcntChildrenの宣言を見れば分かります。
「int cntChildren;」となっていますね。
これは、intの前にアクセス修飾子が何もついていないので、アクセスレベルがprivateとなり、自クラス内でしか参照できず、BlockScriptで参照できなくなっています。
そこで、cntChildrenのアクセスレベルを、同一プロジェクト内からアクセス可能な「public」にします。するといかがでしょうか?BlockScriptの19行目のコンパイルエラーが取れたのではないでしょうか?

public class BlocksScript : MonoBehaviour
{
    public int cntChildren;
    GameObject gameClear;
    // Start is called before the first frame update
    void Start()
    {
        cntChildren = 0;
        gameClear = GameObject.Find("Canvas").transform.Find("GameClear").gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        if (cntChildren == transform.childCount)
        {
            gameClear.SetActive(true);
        }
    }
}

これでゲームを開始すると。。。いかがでしょうか?
最後のBlockが消えると同時にGameClearの文字が現れたのではないでしょうか?

しかしゲームクリアしているのにゲームが継続されているのは不自然ですよね?
ということで最後にゲームクリア時にBallの動きを止めて完成です。

ゲームクリア時にBallを止める

ゲームクリア時の動きなので、これもまたBlocksScriptの「cntChildrenがBlocksの子オブジェクトと同数になった時」の中に記述します。
Ballの動きの処理なので本来はBallScript内に書きたいところですが、処理のトリガーがBlocksScriptの中にあるので、BlocksScriptでオブジェクトBallを取ってきて動きの制御をしましょう。
Ballを取ってくる流れはこれまでと変わりません。
GameObject型の変数ballを定義し、Startメソッドで変数ballの参照をオブジェクトBallにすればオッケーです。
参照の取り方は、これまで色々やりましたが、今回はHierarchy上の名前で取ってきます。
すると↓の様になります。

public class BlocksScript : MonoBehaviour
{
    public int cntChildren;
    GameObject gameClear;
    GameObject ball;
    // Start is called before the first frame update
    void Start()
    {
        cntChildren = 0;
        gameClear = GameObject.Find("Canvas").transform.Find("GameClear").gameObject;
        ball = GameObject.Find("Ball");
    }

    // Update is called once per frame
    void Update()
    {
        if (cntChildren == transform.childCount)
        {
            gameClear.SetActive(true);
        }
    }
}

次に、「cntChildrenがBlocksの子オブジェクト数と同じになった時、Ballを止める」処理を書きます。
Ballを止める処理はGameOverの時と同じで、directionXとdirectionYをどちらも0にします。
これを実装すると↓の様になります。

public class BlocksScript : MonoBehaviour
{
    public int cntChildren;
    GameObject gameClear;
    GameObject ball;
    // Start is called before the first frame update
    void Start()
    {
        cntChildren = 0;
        gameClear = GameObject.Find("Canvas").transform.Find("GameClear").gameObject;
        ball = GameObject.Find("Ball");
    }

    // Update is called once per frame
    void Update()
    {
        if (cntChildren == transform.childCount)
        {
            gameClear.SetActive(true);
            ball.GetComponent<BallScript>().directionX = 0;
            ball.GetComponent<BallScript>().directionY = 0;
        }
    }
}

ここで、実装に問題はないはずなのにまたしてもコンパイルエラーが発生しています。
これは先ほどのcntChildrenと同じ理由で、「directionX」と「directionY」のアクセスレベルの問題となります。
そこで、BallScriptの「directionX」と「directionY」の変数宣言でアクセス修飾子「public」をつけてみましょう。
BlocksScriptのコンパイルエラーが解消されたのではないでしょうか?

public class BallScript : MonoBehaviour
{
    Vector2 pos;
    public int directionX;
    public int directionY;
    GameObject gameOver;
    // Start is called before the first frame update
    void Start()
    {
        pos = transform.position;
        directionX = 1;
        directionY = 1;
        gameOver = GameObject.Find("Canvas").transform.Find("GameOver").gameObject;
    }

    // Update is called once per frame
    void Update()
    {
        //斜め45度に進ませる
        Move();

        //右端に着いた時の処理
        if (IsRightEdge())
        {
            directionX = -1;
        }

        //左端に着いた時の処理
        if (IsLeftEdge())
        {
            directionX = 1;
        }

        //上端に着いた時の処理
        if (IsUpperEdge())
        {
            directionY = -1;
        }

        //下端に着いた時の処理
        if (IsLowerEdge())
        {
            directionX = 0;
            directionY = 0;
            gameOver.SetActive(true);
        }
    }

    void Move()
    {
        pos.x += 0.03f * directionX;
        pos.y += 0.03f * directionY;
        transform.position = pos;
    }

    bool IsRightEdge()
    {
        return pos.x > 2.5;
    }
    
    bool IsLeftEdge()
    {
        return pos.x < -2.5;
    }
    
    bool IsUpperEdge()
    {
        return pos.y > 5;
    }
    
    bool IsLowerEdge()
    {
        return pos.y < -5;
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.name == "Paddle")
        {
            directionY = 1;
        }
        if (collision.gameObject.tag == "Block")
        {
            directionX = -directionX;
            directionY = -directionY;
        }
    }
}

おわりに

これにて無事、クリア時の動きが完成しました!
何と1記事で実装してしまいました。少々大変でしたね。。。
これにてこのゲームを完成にしてしまっても良いのですが、最後に1つ。。。あと1つだけ機能を付けてこのゲームを完成させたいと思います。
まだ実装していない機能。そうです。「continue」ボタンです!
このままでは、ゲームを再起動しない限りこのゲームを繰り返し遊ぶことができません。
そこで次回はcontinueボタンを実装し、ゲームを再起動することなく繰り返し遊ぶことができるようにしましょう!
この機能でこのシリーズは完全に終了です。
ここまで長かったですね。。。
乞うご期待!

 


連載目次リンク

「初心者のための」Unityゲーム制作 目次

Unity実践編 - 目次リンク

実践Unityゲームプログラミング 連載目次