Developer

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

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


はじめに

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

前回はBlockがBallによって破壊されるところまで作成しました。

しかし、Blockに衝突した後のBallの動きに違和感がありましたね?
何とBallはBlockを突き抜けてしまうのでした。。。
如何にBlockが破壊されてしまうとはいえどもBallが何の影響も受けずに直進し続けるのはちょっとおかしいですよね(笑)
そこで今回はBallがBlockに衝突した後跳ね返る処理を施しましょう!


第4段階:Blockを作成しよう!(3)

タイトルに「Blockを作成しよう!」とありますが、今回動きに変化が起こる主人公はBallですので、主にScriptを編集するのはBallになります(笑)
まずは、今回実装したいことを言語化してみましょう。
「Ballが跳ね返る。BallがBlockに衝突した時に。」
前回同様、文章の並びが不自然ですね。。。
前回と同じ流れで説明しますと、今回のメインの文は最初に書かれている「Ballが跳ね返る」です。
主語が「Ballが」となっているので、BallScriptを編集します。
次に、述語が「跳ね返る」となっているので、BallScriptの中で「跳ね返る」処理を実装します。ここで重要なのがどのメソッド内でその処理を実行するかです。
まあこれは、PaddleがBallを跳ね返す処理やBlockが消える処理をどのメソッドに書いたのかを思い出して頂ければそんなに難しくはないかと思います。
重要なのは跳ね返るタイミングでしたね。そこで上の文を再度確認すると、その条件を確認することができます。そう、「BallがBlockに衝突した時に」です。
「衝突した時」に動く(呼び出される)メソッドありましたよね。。。?そうです!OnTriggerEnter2D()です!
「衝突」という言葉に厳密に行くのであれば本当はOnTriggerEnter2D()ではなくOnCollisionEnter2D()なのですが、後者を使うと、ガチで衝突してしまい、各オブジェクトがもろにその影響を受けて吹っ飛んでしまったりするので、今回は実際に衝突せずに重なったことを検知するだけの前者を使います。
まとめると以下の様になります。
①BallScriptを編集する。
②OnTriggerEnter2D()の中にBallが跳ね返る処理を書く。
まあ①は既に作成済みなのでBallScriptをダブルクリックするだけです(笑)

次に②の処理ですね。これはまあとりあえずササっとできることだけ書いてしまいましょう。
今分かっていることはこれまでの知識的にOnTriggerEnter2D()内に跳ね返りの処理を書くんですよね?これを書き加えると↓の様になります。

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)
    {
        directionY = 1;
        //Blockから跳ね返る処理
    }
}

おや?何やら74行目に先客がいらっしゃいますね?
「directionY = 1」
これは「Paddleに衝突した時に、上方向に跳ね返る」という処理でしたね。
今回は「Blockに衝突した時に、ある方向に(この方向は後程考えましょう)跳ね返る」という処理です
おや?これは、Ballがどのオブジェクトに衝突したのかという場合分けが必要そうではありませんか?
言語化すると↓の様になります。
・衝突したオブジェクトがPaddleの場合 → directionY = 1
・衝突したオブジェクトがBlockの場合 → Blockから跳ね返る処理
場合分けなのでまあ普通にif文ですね。実装すると↓の様になります。

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)
    {
        //Paddleに衝突した場合。ifの中は後ほど詰めるので今は一旦false
        if (false)
        {
            directionY = 1;
        }
        //Blockに衝突した場合。ifの中は後ほど詰めるので今は一旦false
        if (false)
        {
            //Blockから跳ね返る処理
        }
    }
}

さあ、それではifの中ですね。ここでネックなのがどの様にして衝突したオブジェクトを判別するかということですよね。。。
実はこのヒントはOnTriggerEnter2D(Collider2D collision)メソッドに隠れています。そう、このメソッドの引数となっている「Collider2D collision」です。
ここ非常に重要なのですが、この引数「collision」には衝突したオブジェクトのコライダーが格納されます。
具体的に言いますと、例えばBallがPaddleに衝突すると、引数「collision」の中にはPaddleのコライダー「Box Collider 2D」が格納されます。
また、例えばBallがBlockに衝突すると、引数「collision」の中にはBlockのコライダー「Box Collider 2D」が格納されます。

え、コライダーを格納したところでどうすんの?と思うかもしれませんが、ここで魔法の様なことが起こります。
何とスクリプトではコンポーネントを辿ってオブジェクトを取得することができるのです!
より具体的に言いますと、OnTriggerEnter2D(Collider2D collision)で手に入れた「Box Collider 2D」を辿って、「Box Collider 2D」が引っ付いているオブジェクトであるPaddleを取得することができるのです!すごいですよね(笑)
これは「collision.gameObject」の様に書くことができます。
また、「そのオブジェクトの名前がPaddleの場合」という条件にしたいので、「collision.gameObject.name == “Paddle”」とすることができます。
同じく、「そのオブジェクトの名前がBlockの場合」という条件にしたいので、「collision.gameObject.name == “Block”」とすることができます。
これを実装すると↓の様になります。

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.name == "Block")
        {
            //Blockから跳ね返る処理
        }
    }
}

さあ、衝突したオブジェクトによる場合分けも完成したのでいよいよ最後の段階に入ります。
Ballが「Blockから跳ね返る処理」というところですね。
これまで同様、跳ね返る処理なので変数「directionX」と「directionY」を使い分けます。
問題はどの様な処理をさせたいかです。
ここで非常に難しいのが、Blockの上下に衝突するのか左右に衝突するのかによる場合分けです。

もし厳密にやるのであればこの場合分けは必須になりますが、これ難しすぎるので、すいません今回はこの場合分けを無くして簡略化します。
今回はどこから当たろうとdirectionX、directionY共にひっくり返すという処理にします。
これによって、多少違和感のある動きとはなりますが無事BallはBlockから跳ね返る様になります。
それでは、この多少違和感の残る処理を実装するに当たっての方針を考えましょう(笑)
今回の処理を言語化すると↓の様になります。

directionX、directionY共にひっくり返す

・directionXが1の場合directionXを-1にし、directionXが-1の場合directionXを1にする。
・directionYが1の場合directionYを-1にし、directionYが-1の場合directionYを1にする。

・directionXの符号(+や-)をひっくり返す
・directionYの符号(+や-)をひっくり返す

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

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.name == "Block")
        {
            directionX = -directionX;
            directionY = -directionY;
        }
    }
}

これで再生してみると。。。いかがでしょうか?やや違和感はあるものの、BallがBlockを破壊しつつ跳ね返ったのではないでしょうか?


おわりに

これにて無事にBlockが完成し、Ball、Paddle、Blockがそろったことでいよいよゲームらしくなってきました。
しかし画面を見てみると、破壊する対象であるBlockがポツンと1つしかなくて少し寂しいですよね?(笑)
そこで次回はこのBlockを効率よく複製していきましょう!
この複製ですが、簡単そうに見えますが、何となくでやろうとすると必ず失敗します(怖)
失敗を防ぐための技術がまた少々新しいものとなりますので、楽しみにしていてください(笑)
乞うご期待!
 


連載目次リンク

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

Unity実践編 - 目次リンク

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