Developer

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

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


はじめに

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

前回、画面上に「GameOver」の文字を出すことができました。

しかし今のところこの文字はゲームプレイ中ずっと画面に出続けてしまうのでした。。。
「GameOver」ですから、通常時はこの文字は隠れていて、ゲームオーバーになったタイミングになって初めてこの文字が画面上に出てきた方が良いですよね?
ということで今回は「GameOver」が画面上に現れるタイミングを制御しましょう!


ゲームオーバーのタイミングはいつなのか?

ここで重要になってくるのはゲームオーバーのタイミングです。まあゲームオーバーの定義は人それぞれかもしれませんが、今回はそれっぽく、Ballが画面の下の端に触れたらにしましょう。

ゲームオーバーになるまで「GameOver」は画面に出てこないようにする

さて、ゲームオーバーのタイミングが決まったところで早速プログラムを書いていきたいところですが、その前にまずやっておかなければならないことがあります。
それは、プレイ開始時には「GameOver」が画面に出ていない状態にすることです。
だってそうですよね?ゲームプレイ中に文字が出ているのは変ですよね?(笑)
これはプログラムとは関係なく、画面上の操作で簡単に実装できます。
因みにこれ、前に一度実装しているのですが覚えていますか?
そう!オブジェクトを画面に出したり消したりする。。。Blockを画面上から消す動きで実装しました!
その時の動きと全く同じです。
消したいオブジェクト、今回は「Text」を選択し、Inspecter上で名前の左のチェックを外す。これだけ!
いかがでしょうか?Hierarchy上では「Text」が残りつつ、画面から「GameOver」の文字が消えたのではないでしょうか?

「GameOver」の実装

ゲームオーバーのタイミングが決まり、通常時は「GameOver」の文字が消えたところで、ゲームオーバーの動きを実装していきましょう。
実装の仕方ですが、いつも通りまずは言語化してからプログラムに落とし込みましょう。
今回実装したいプログラムを言語化すると。。。
「Ballが下の端に触れたら、GameOverの文字が現れる。」となります。
今回の主人公となるオブジェクトはTextなので、Textに新たにスクリプトを貼り付ければ良いのではないか、と今までの流れならなるのですが、今回はBallに既に貼り付けてあるスクリプトに書き足していきましょう。
理由としては、ゲーム開始時にTextは無効化(Hierarchy上で透明に)されているからです。
無効化されているオブジェクトにアタッチされたスクリプトのメソッド(UpdateやOnTriggerEnter2D等)はゲームプレイ中に呼び出されないため、Textにアタッチしたスクリプトにプログラムを書いても動いてくれません。
そこで今回はゲームオーバーのきっかけを作るオブジェクトであるBallのスクリプトにプログラムを書いていきましょう。
Ballのスクリプトの開き方は、Hierarchy上のBallをクリックし、Inspecter上のBallScriptをダブルクリックします。

それではスクリプトを開いたところで、先程の言葉をプログラムに書いていきますが、まず注目すべきは「GameOver」が現れるタイミングです。
これは、「Ballが下の端に触れたら」と書かれています。
そこで今回注目する所はUpdateメソッド内の「下端に着いた時の処理」の部分です。

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

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

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

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

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

        //下端に着いた時の処理
        if (IsLowerEdge())
        {
            directionY = 1;
        }
    }

    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;
        }
    }
}

現在この中の処理は「directionY = 1;」となっています。これは単純に下向きだったBallの方向を上向きに変更するだけの物でしたね? 今回は違います。Ballの向きを変更するのではなく、「GameOverの文字が現れる。」を実装したいんでしたよね? そこで「GameOverの文字が現れる。」という言葉をよりプログラムに近づけましょう。 「GameOverの文字が現れる。」とは要するに「Textオブジェクトが有効化される。」ということです。 オブジェクトの有効化。。。どこかで実装しましたね。。。そうです。しつこいですがBlockで実装しました。 「Blockが消える。BallがBlockに衝突した時に。」を実装する時にBlockScriptにオブジェクトの無効化を書いたのを覚えていますか? さて、どの様な書き方をしたでしょうか。。。? そうです。「SetActive()」メソッドを使いました。 SetActive()の引数を「false」にするとオブジェクトを無効化できました。 今回は有効化なので。。。「SetActive(true)」ですね! これをBallScriptの「下端に着いた時の処理」の中に書けば良いのかな。。。?と思うかもしれませんが、まだこれでは不十分です。 「どのオブジェクトを」有効化するのかSetActive()の前に書かなければなりません。 今回有効化したいのは「Text」ですよね? ということで、「下端に着いた時の処理」の中に「text.SetActive(true);」を書き足してみましょう。

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

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

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

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

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

        //下端に着いた時の処理
        if (IsLowerEdge())
        {
            directionY = 1;
            text.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;
        }
    }
}

これで「変数textが参照しているオブジェクトを有効化する」という機能が実装されましたが、「変数text」が未定義なのでコンパイルエラーしていますね(笑) そこでまずは「変数text」を定義しましょう。「変数text」はゲームオブジェクトを参照する変数なので、型は「GameObject」にしましょう。

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

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

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

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

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

        //下端に着いた時の処理
        if (IsLowerEdge())
        {
            directionY = 1;
            text.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;
        }
    }
}

これで一先ずコンパイルエラーは解消されたかと思いますが、このままだと「text.SetActive(true);」が呼び出された時に「NullReferenceException」という実行時エラーが発生してしまいます。 理由は単純で、変数textがまだ何も参照していないので、SetActive(true)をしようにもどのオブジェクトを有効化すれば良いの?となってしまうからです。 そこで最後に変数textにゲームオブジェクト「Text」を参照してもらいましょう。 ゲームオブジェクトの参照を取る動きは、基本的にゲーム開始時に一回だけ動けば良いので、Startメソッドの中に書いてあげましょう。 変数textがゲームオブジェクトTextを参照する方法はいくつかありますが、今回はHierarchy上の名前から取得しましょう。 名前からオブジェクトを取得するメソッドとして「GameObject.Find()」があります。 これを使えばよいのかな?ということで「text = GameObject.Find(“Text”).gameObject;」と書けばよいのかな?と思うかもしれませんが、今回は可読性(プログラムの読みやすさ。書いた本人以外の人があるプログラムを見たときに、より読みやすい方が保守しやすい)を意識し、少し長ったらしく書いてみます。Textはどこに配置されているかと言うと、Hierarchy直下ではなく、Hierarchyの下のCanvasの子オブジェクトとして配置されています。

そこで、検索の仕方としては「Canvasという名前のオブジェクトの子オブジェクトのTextと言う名前のオブジェクト」という書き方で検索します。 長ったらしいですね。。。(笑) これをプログラムにすると「text = GameObject.Find(“Canvas”).transform.Find(“Text”).gameObject;」の様になります。 やっぱりながったらしい。。。 これを↓の様にStartメソッドに書くと。。。

public class BallScript : MonoBehaviour
{
    Vector2 pos;
    int directionX;
    int directionY;
    GameObject text;
    // Start is called before the first frame update
    void Start()
    {
        pos = transform.position;
        directionX = 1;
        directionY = 1;
        text = GameObject.Find("Canvas").transform.Find("Text").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())
        {
            directionY = 1;
            text.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;
        }
    }
}

いかがでしょうか?
↓の様に、Ballが下端に着くと「GameOver」が画面上に現れるのではないでしょうか?

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

ゲームオーバー時にBallを止める

ゲームオーバー時の動きなので、これもまた「下端に着いた時の処理」の中に記述します。
Ballが飛び回る動きはBallScriptのMove()メソッドに書いていますが、これを見ると、

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

Ballが動く方向を制御する変数「directionX」と「directionY」があります。
これらの値はこれまで「1」と「-1」にすることで使ってきましたが、もちろん他の値にすることもできます。
絶対値が1より大きくなれば(例えば「7」や「-8」等)Ballが早くなり、絶対値が1より小さくなれば(例えば「0.2」や「-0.3」等)Ballが遅くなります。
つまりこの変数を利用すればBallを遅くする。。。何なら「止める」こともできるんですね!
止めるのは簡単。絶対値が最も小さいとき、つまり「directionX = 0」かつ「directionY = 0」の時に止まります。
そこで、「下端に着いた時の処理」の中にこれらを記述すれば、ゲームオーバー時にBallは動きを止めます。

public class BallScript : MonoBehaviour
{
    Vector2 pos;
    int directionX;
    int directionY;
    GameObject text;
    // Start is called before the first frame update
    void Start()
    {
        pos = transform.position;
        directionX = 1;
        directionY = 1;
        text = GameObject.Find("Canvas").transform.Find("Text").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;
            text.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;
        }
    }
}

おわりに

これにて無事、ゲームオーバー時の動きが完成しました!
ここまで長かったですね。。。
あともう一息でこのゲームも完成です。
まだ実装していない機能。そうです。ゲームオーバーと言えばゲームクリアがありますよね。。。?
ということで次回はゲームクリアの処理を実装しましょう!
実装の流れは大体ゲームオーバーと同じなのでそんなに難しくありません。
直近数回分の記事の復習という感覚で実装していきましょう!
乞うご期待!

 


連載目次リンク

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

Unity実践編 - 目次リンク

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