0からスタート!ゲーム開発ブログ

ゲーム開発に関する様々な記事を更新します

Collider+RigidBody 2D物理演算の当たり判定

1. はじめに

こんにちは、azarashin です。 Unity には物理演算を行うための様々な機能が備わっています。その中でも特にオブジェクト同士の当たり判定を検出し、互いに重ならないようにコントロールしてくれる機能は2D, 3D 問わず便利な機能です。

実はゲーム中のオブジェクトはすべて三角形の集合として構成されています。そのためオブジェクト同士の当たり判定と位置の修正を行うためには三角形同士の衝突と位置の修正を行う必要があります。この辺りの計算は若干複雑で線形代数を勉強する必要があるのですが、非常によく使う機能であるため開発者が個別に開発するのは効率が悪いです。 Unity ではこの複雑な計算を行い、衝突と位置の修正を手軽にやってくれる機能が備わっています。 そこで本記事では2D物理演算の当たり判定をCollider+RigidBody 2Dを用いて実現する方法について紹介していきたいと思います。

ポリゴン同士の接触

2. 画像の準備

今回はフルーツを上から落として積み重ねていくデモを作成します。 ここでフルーツの画像が必要になるのですが、今回はいらすとやさんのサイトで画像を調達することにしましょう。

www.irasutoya.com

3. フルーツのスプライトを表示するためのGameObject を生成する

何はともあれスプライトオブジェクトを生成しましょう。 Hierarchy 上にスプライト画像をドラッグ&ドロップすると、Sprite Renderer 付きのGameObject が生成されます。

Sprite Renderer 付きのGameObject を生成する

そしてこのGameObject にRigidbody2D コンポーネントを割り当てます。 この時Gravity Scale を0 にして重力による落下を防ぐようにします。

実は重力の影響を防ぐ方法は他にもあり、RigidBody2D のBody Type をDynamic から別のものに変更すれば落下しなくなります。しかしながら、Body Type をDynamic 以外のものにするとオブジェクト同士の衝突検出に影響がでるため、今回はBody Type をDynamic にしたままで落下を抑制するようにします。

Rigidbody2D コンポーネントを割り当てる

つづいてコライダも追加します。今回はフルーツの形状を意識した当たり判定にするため、Polygon Collider 2D を使用します。 Polygon Collider 2D を使うと複雑な形状同士の当たり判定を求めることができるようになりますが、 計算にかかる処理も増えてしまいますので、Polygon Collider 2D を使うべきかどうかは慎重に吟味します。

今回はフルーツと壁、フルーツ同士の衝突時に互いに重ならないようにするため、Is Trigger のチェックを外しておきます。

オブジェクト同士が重ならないようIs Trigger のチェックを外す

実際の当たり判定を確認しましょう。Scene 欄でスプライトオブジェクトに対する当たり判定領域が緑の線で示されているはずです。

緑の線(コライダ領域)でフルーツが囲まれていることを確認する

各フルーツの当たり判定を確認する

これらのフルーツ(スプライトオブジェクト)は何度も生成されるものですのでprefab 化しておきましょう。

prefab化する

4. 壁を作る

続いて壁を作成していきます。今回は単純な矩形をそのまま壁にしてしまいます。 Hierarchy 上で右クリックし、2D Object → Sprites → Square で矩形を生成します。

Square スプライトを生成する

いくつか矩形を生成し、大きさと位置を調整して壁にしていきます。

Square スプライトを組み合わせて壁と床にする

続いて壁にRigidbody 2D とBox Collider 2D を付与します。 この時のポイントは2つです。

  1. Body Type をStatic にして動かないようにする
  2. オブジェクト同士が重ならないようIs Trigger のチェックを外す

特に1つ目が重要で、これをStatic にしておかないと壁が落下してしまったり、フルーツの衝突によって壁が動いてしまいます。 壁が動いてしまうのを防ぐには他にも方法があり、Rigidbody 2D のBody Type をDynamic にした状態でConstraints の各項目にチェックを入れるやり方があります。

オブジェクトの位置・姿勢を固定する

しかしこのやり方だと後述するオブジェクトの静止判定(IsSleep)が上手くいかなくなってしまうため、今回はBody Type をStatic にする方法を選択します。

壁のRigidbody 2D とBoxCollider2D を追加・設定する

この後の当たり判定処理に利用するため、床にはFloor タグを割り当てておきます。

Floor タグ割り当て

5. フルーツを動かす

5.1. タグの割り当て

この後の当たり判定処理に利用するため、フルーツのprefab にはFruit タグを割り当てておきます。

Fruit タグ付与

5.2. スクリプトの作成

続いてフルーツを動かすためのコードを記述します。

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class Fruit : MonoBehaviour
{
    public enum FruitState
    {
        Moving, // 操作中
        Fixed // 移動確定した
    }

    private float _verticalSpeed = 1.0f; // 垂直方向の速度
    private float _horizontalSpeed = 1.0f; // 水平方向の速度
    public FruitState _state = FruitState.Moving;
    private Rigidbody2D _body; 

    /// <summary>
    /// 初期化する
    /// </summary>
    /// <param name="owner">フルーツの生成場所</param>
    /// <param name="verticalSpeed">垂直方向の速度</param>
    /// <param name="horizontalSpeed">水平方向の速度</param>
    public void Setup(Transform owner, float verticalSpeed, float horizontalSpeed)
    {
        _verticalSpeed = verticalSpeed;
        _horizontalSpeed = horizontalSpeed;
        _state = FruitState.Moving; 
        _body = GetComponent<Rigidbody2D>();
        _body.bodyType = RigidbodyType2D.Dynamic; // Kinematic にすると壁にぶつかっても反発しなくなるのでNG
        _body.gravityScale = 0.0f; // 最初は自由落下させない

        transform.position = owner.position; // フルーツの初期位置をこのFruitsGenerator オブジェクトに合わせる
        transform.Rotate(Vector3.forward * Random.value * 360.0f); // 前後方向を軸として適当に回転させる

        // オブジェクトの静止状態を判定するための閾値設定
        Physics2D.timeToSleep = 0.2f;
        Physics2D.linearSleepTolerance = 1f;
        Physics2D.angularSleepTolerance = 20f;
    }

    void Update()
    {
        if(_state == FruitState.Moving )
        {
            transform.position -= Vector3.up * _verticalSpeed * Time.deltaTime; // 落下させる

            if (Input.GetKey(KeyCode.A))
            { // 左へ移動
                transform.position += -Vector3.right * _horizontalSpeed * Time.deltaTime;
            }

            if (Input.GetKey(KeyCode.D))
            { // 右へ移動
                transform.position += Vector3.right * _horizontalSpeed * Time.deltaTime;
            }
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if(_state == FruitState.Fixed)
        {
            return; 
        }
        bool isTouchedToFruit = (collision.gameObject.CompareTag("Fruit"));
        bool isTouchedToFloor = (collision.gameObject.CompareTag("Floor"));
        if (isTouchedToFruit || isTouchedToFloor)
        {
            _state = FruitState.Fixed; // 固定された状態に状態遷移する
            _body.gravityScale = 1.0f; // 重力の影響を発生させる
        }
    }

    /// <summary>
    /// フルーツが静止したかどうかを判定する
    /// </summary>
    /// <returns>静止していればtrue, 静止していなければfalse</returns>
    public bool IsFixed()
    {
        bool isSleeping = _body.IsSleeping();
        return (_state == FruitState.Fixed) && isSleeping;
    }

}

以下、ポイントを絞って説明していきます。

5.2.1. フルーツの状態切り替え

    public enum FruitState
    {
        Moving, // 操作中
        Fixed // 移動確定した
    }

フルーツの状態として2つの状態を定義しています。 最初はMoving 状態で始まり、プレイヤーの入力を受け付けます。 その後何かに接触するとFixed 状態となり、フルーツを自由落下させます。 フルーツの動きが収まったら次のフルーツを落とし始めます。

5.2.2. フルーツの初期位置・姿勢

        transform.position = owner.position; // フルーツの初期位置をこのFruitsGenerator オブジェクトに合わせる
        transform.Rotate(Vector3.forward * Random.value * 360.0f); // 前後方向を軸として適当に回転させる

Transform owner はフルーツの発生位置を示すTransform で、フルーツを生成したら位置をここに合わせます。 またtransform.Rotate を呼び出し、フルーツの回転角度をランダムに決定します。

5.2.3. フルーツを移動させる

        if(_state == FruitState.Moving )
        {
            transform.position -= Vector3.up * _verticalSpeed * Time.deltaTime; // 落下させる

            if (Input.GetKey(KeyCode.A))
            { // 左へ移動
                transform.position += -Vector3.right * _horizontalSpeed * Time.deltaTime;
            }

            if (Input.GetKey(KeyCode.D))
            { // 右へ移動
                transform.position += Vector3.right * _horizontalSpeed * Time.deltaTime;
            }
        }

フルーツの状態がMoving 状態の時に限り(つまり他のフルーツや壁に接触する前に限り)、 A, D キーを入力するとフルーツの位置(transform.position)を変化させ、さらに等速度で落下させるようにしています。

5.2.4. フルーツと他のオブジェクトとの接触判定

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if(_state == FruitState.Fixed)
        {
            return; 
        }
        bool isTouchedToFruit = (collision.gameObject.CompareTag("Fruit"));
        bool isTouchedToFloor = (collision.gameObject.CompareTag("Floor"));
        if (isTouchedToFruit || isTouchedToFloor)
        {
            _state = FruitState.Fixed; // 固定された状態に状態遷移する
            _body.gravityScale = 1.0f; // 重力の影響を発生させる
        }
    }

フルーツの状態がFixed 状態でない場合(つまり他のフルーツや壁に接触する前に限り)、 壁(Floor)か他のフルーツ(Fruit)に接触したときに自身の状態をMoving 状態からFixed 状態に変化させています。 また、何かに接触したらRigidbody2D のgravityScale を1.0f にし、重力の影響を発生させてフルーツが崩れ落ちるようにしています。

5.2.5. フルーツが静止しているかどうかを調べる

    public bool IsFixed()
    {
        bool isSleeping = _body.IsSleeping();
        return (_state == FruitState.Fixed) && isSleeping;
    }

Rigidbody2D のIsSleeping メソッドを使用するとオブジェクトが静止しているかどうかを調べることができます。 このメソッドが静止状態を判定する時にいくつかの閾値が参照されています。 具体的には初期化時に設定した以下の閾値です。

        // オブジェクトの静止状態を判定するための閾値設定
        Physics2D.timeToSleep = 0.2f;
        Physics2D.linearSleepTolerance = 1f;
        Physics2D.angularSleepTolerance = 20f;

もし静止判定が上手くいかない場合、Rigidbody2D のBody Type とこれらの閾値とを見直してみてください。

5.3. Fruit クラススクリプトをフルーツの各prefab に割り当てる

以上のように作成したスクリプトをフルーツの各prefab に割り当ててください。

スクリプト割り当て

5.4. フルーツをシーンに設置する

これでフルーツをシーン上に設置してアプリを再生するとフルーツを動かすことができるようになります。

フルーツ設置

フルーツが落下する

6. 継続的にフルーツを生成する

先ほどのフルーツのスクリプトでフルーツを動かすことができるようになったので、 今度はフルーツを落としきった後に次のフルーツを落とせるようにしていきます。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class FruitsGenerator : MonoBehaviour
{
    [SerializeField]
    float _verticalSpeed = 2.0f; // 垂直方向の速度

    [SerializeField]
    float _horizontalSpeed = 10.0f; // 水平方向の速度


    [SerializeField]
    Fruit[] _prefabs;

    List<Fruit> _list; 

    // Start is called before the first frame update
    void Start()
    {
        _list = new List<Fruit>();
        GenerateFruit(); 
    }

    // Update is called once per frame
    void Update()
    {
        if(_list.All(s => s.IsFixed()))
        {
            GenerateFruit();
        }

    }

    private void GenerateFruit()
    {
        int index = Random.Range(0, _prefabs.Length);
        Fruit obj = Instantiate(_prefabs[index]);
        obj.Setup(transform, _verticalSpeed, _horizontalSpeed); 
        _list.Add(obj);
    }
}

6.1. フルーツをランダムに生成する

    [SerializeField]
    Fruit[] _prefabs;

上記のコードで予めフルーツのprefab をリストにして保持しておきます。 その上で、

    private void GenerateFruit()
    {
        int index = Random.Range(0, _prefabs.Length);
        Fruit obj = Instantiate(_prefabs[index]);
        obj.Setup(transform, _verticalSpeed, _horizontalSpeed); 
        _list.Add(obj);
    }

上記のメソッドを呼び出すことで、pfefab のリストからランダムに一つ選択し、 そのprefab を複製するようにしています。

6.2. すべてのフルーツが静止しているかどうかを監視する

        if(_list.All(s => s.IsFixed()))
        {
            GenerateFruit();
        }

C#拡張機能の一つにLINQ があり、 配列.All のように記述するとカッコ内の条件をすべて満たしているかどうかを判定することができます。 今回はフィールドに設置したすべてのフルーツ(_list)が静止しているかどうかを判定し、 静止していたら次のフルーツを生成するようにしています(GenerateFruit() の呼び出し)。

6.3. スクリプトの割り当て

適当なGameObject を生成し(FruitGenerator とします)、上記スクリプトを割り当てます。

スクリプト割り当て

7. 動作確認

以下のようになります。

デモ

8. 終わりに

本記事ではパズルゲームっぽい環境を構築し、2Dの複雑な形状に対して当たり判定を求める事例について紹介しました。 冒頭にも記述しましたが、矩形や円のような単純な形状と違って複雑な形状に対して当たり判定を求めるのには 計算時間がかかります。 本当に複雑な形状での判定が必要か?単純な形状でも大きな差はないのではないか?といったことを考えながら ゲームの機能性と性能とを両立させるよう吟味しましょう。 今回はコードの量が多くなってしまいましたが、当たり判定を活用したゲームプログラムの一部として参考になれば幸いです。