i-school - 【2D】壁の中を貫通する弾を制御する
 壁や床のオブジェクトにコライダーが設定されている前提です。

 壁や床のコライダー内部に弾が生成されてしまった場合、そのままですと弾が壁や床の中を貫通します。
それを是とするゲーム表現であれば問題ありませんが、壁や床を弾が貫通するのを良しとしないゲームの場合には回避策が必要になります。


<実装動画>
動画ファイルへのリンク

 今回は3つのアプローチ例を掲載します。



<アプローチ1 ー壁や床側を主体として回避する方法ー>


 2Dゲーム用のコライダー、およびタイルマップのコライダーに対しての実装例です。

 avoidCollider 変数にアサインされた壁や床のコライダーを感知させて
そのコライダーの内部に生成される想定で、OnTriggerStay2D メソッドを利用して対処する方法です。

 この例の場合、コライダーから見て、弾であるかを判定しています。
各コライダーごとにこのスクリプトをアタッチして利用します。

 弾のゲームオブジェクトには Bullet という名前のクラス(弾の制御を行うクラス)がアタッチされている前提です。


using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class AvoidBulletExample : MonoBehaviour
{
    [SerializeField] private Collider2D avoidCollider;  // 対象となるコライダーのアサイン


    private void OnTriggerStay2D(Collider2D other)
    {
        // 壁や床に対して侵入しているコライダーにアタッチされているクラスを確認して弾であるかを判定する
        if(other.gameObject.TryGetComponent(out Bullet bullet))
        {
            // OnTriggerStay2Dイベントが発生した際の処理
            Debug.Log($"OnTriggerStay2D event triggered with collider: { bullet.gameObject.name }");

            // ここで弾を消す
            Destroy(bullet.gameObject);
        }
    }
}



 上記の処理を UniRx に用意されている .OnTriggerStay2DAsObservable に置き換えた場合の例です。


using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class AvoidBulletExample : MonoBehaviour
{
    [SerializeField] private Collider2D avoidCollider;  // 対象となるコライダーのアサイン


    void Start()
    {
        // OnTriggerStay2DのイベントをUniRxで購読
        avoidCollider
	  .OnTriggerStay2DAsObservable()
	  .Where(otherCollider => otherCollider.gameObject.TryGetComponent(out Bullet bullet))
            .Subscribe(otherCollider =>
            {
                // OnTriggerStay2Dイベントが発生した際の処理
                Debug.Log($"OnTriggerStay2D event triggered with collider: { otherCollider.name }");

                // ここで弾を消す(otherCollider を利用する)
		if(otherCollider.gameObject.TryGetComponent(out Bullet bullet)){
		    Destroy(bullet.gameObject);
		}
            })
            .AddTo(this); // サブスクリプションを破棄するために、このコンポーネントが破棄された時に自動的に破棄するように登録
    }
}


 
 なお、タイルマップの場合に限りですが、TilemapCollider2D コンポーネントに加えて
Composit Collider2D コンポーネントを利用してタイルマップのコライダーを結合している場合、この処理は正常に動作しません
 
その場合、タイルマップにアタッチされている Composit Collider2D コンポーネントをリムーブすれば
上記のスクリプトが正常に動作して、弾が消えるようになります。

 ただし、Composit Collider2D コンポーネントをリムーブしてしまうと
タイルマップのコライダーがグループではなく単体動作するようになるので、滑らかな床の判定はなくなり、コライダーの計算負荷が増えます。

 この辺りはゲームの表現とトレードオフの関係性になると思いますので、
ご自分のイメージに近い表現の実装を採用するようにしてみてください。


<アプローチ2 ー弾側を主体として回避する方法ー>


 別のアプローチとしては、弾の生成時に、弾の発射される位置のタイルマップを確認し、
そのタイルマップにコライダーが適用されている場合には弾を生成しない、あるいは、生成してすぐに破壊する、という方法もあります。

 アプローチ1とは異なり、該当するコライダーをアサインするのではなく
レイヤーの指定(例えば Ground といったレイヤー) を行うことで、弾側から見てコライダーを特定しています。

 そのため、こちらのスクリプトは弾の生成役のゲームオブジェクトに記述する処理になります。



 弾を生成するクラス内に、下記の処理を追加します。


    public LayerMask obstacleLayer; // 壁や障害物のレイヤーマスク
    private float radius = 0.1f;    // コライダー判定用の円の半径


    // コライダーの有無を判定するメソッド
    private bool CheckColliderAtPosition(Vector2 position)
    {
        Collider2D hitCollider = Physics2D.OverlapCircle(position, radius, obstacleLayer);
        return hitCollider != null;
    }


 OverlapCircle メソッドの第2引数 radius がコライダー判定用の円の半径です。
ここでは 0.1 としていますが、タイルマップ1つ分のサイズになるように調整する感じです。
 このように戻り値を持つコライダー判定用のメソッドを用意しておいて、弾の生成後にこのメソッドを実行します。

 上記の処理を使う際には、下記のような構成で利用します。

        // 弾の生成
        
        // コライダーの有無を判定して結果を取得
        bool hasCollider = CheckColliderAtPosition([弾の生成位置]);

        if (hasCollider)
        {
            // コライダーがある場合は弾をすぐに破壊。+ 演出
            Destroy(bullet);

            return;
        }
        
        // コライダーがない場合は通常の処理を続行


 こういった形で分岐を作り、弾の生成時にコライダー内部にいないかを判定してから、弾を発射させる方法です。


<スクリプト全文>


 弾の生成クラスに組み込んだ形で、サンプル掲載しておきます。 

using UnityEngine;

public class BulletController : MonoBehaviour
{
    public LayerMask obstacleLayer; // 壁や障害物のレイヤーマスク


    // 弾の生成メソッド
    public void SpawnBullet(Vector2 spawnPosition)
    {
        // 弾の生成
        GameObject bullet = Instantiate(gameObject, spawnPosition, Quaternion.identity);

        // コライダーの有無を判定して結果を取得
        bool hasCollider = CheckColliderAtPosition(spawnPosition);

        if (hasCollider)
        {
            // コライダーがある場合は弾をすぐに破壊
            Destroy(bullet);
        }
        else
        {
            // コライダーがない場合は通常の処理を続行
            // ここで弾の速度や移動方向などの設定を行うことができます
        }
    }

    // コライダーの有無を判定するメソッド
    private bool CheckColliderAtPosition(Vector2 position)
    {
        Collider2D hitCollider = Physics2D.OverlapCircle(position, 0.1f, obstacleLayer);
        return hitCollider != null;
    }
}



<アプローチ3 ータイルマップのコライダーを主体として回避する方法ー>


 こちらの方法は2の方法をベースにしたタイルマップ限定の方法です。
タイルマップのコライダーを参照して、弾がコライダー内部に生成された場合には破壊することで回避します。

 下記の処理を弾の生成用のクラスに追加します。

    public Tilemap obstacleTilemap; // 壁や障害物のTilemap


    // タイルマップを利用してコライダーの有無を判定するメソッド
    private bool CheckColliderAtPosition(Vector2 position)
    {
        // タイル座標に変換
        Vector3Int cellPosition = obstacleTilemap.WorldToCell(position);

        // タイルの取得
        TileBase tile = obstacleTilemap.GetTile(cellPosition);

        // タイルが存在し、TileColliderがある場合はコライダーがあると判定
        return tile != null && tile.GetComponent<TilemapCollider2D>() != null;
    }


 こちらの場合だと、レイヤーではなくタイルマップの情報が必要になります。
ここでは1つだけですが、実際にマップの数だけ判定用のタイルマップ情報が必要になるため、アサイン情報が大量になります。

 そのため、実際のプロジェクトでの運用を考えた場合は、下記のようになります。


    public Tilemap[] obstacleTilemaps; // 壁や障害物のTilemapの配列


    // 複数のタイルマップを利用してコライダーの有無を判定するメソッド
    private bool CheckColliderAtPosition(Vector2 position)
    {
        foreach (var tilemap in obstacleTilemaps)
        {
            // タイル座標に変換
            Vector3Int cellPosition = tilemap.WorldToCell(position);

            // タイルの取得
            TileBase tile = tilemap.GetTile(cellPosition);

            // タイルが存在し、TileColliderがある場合はコライダーがあると判定
            if (tile != null && tile.GetComponent<TileCollider>() != null)
            {
                return true; // コライダーが見つかれば即座にtrueを返して終了
            }
        }

        return false; // 複数のタイルマップをチェックしてもコライダーが見つからない場合はfalseを返す
    }

 マップを追加するごとにアサイン情報を増やさないとならない部分と、他に比べて処理が長いのがネックです。


<スクリプト全文>


 弾の生成クラスに組み込んだ形で、サンプル掲載しておきます。 

using UnityEngine;
using UnityEngine.Tilemaps;

public class BulletController : MonoBehaviour
{
    public Tilemap[] obstacleTilemaps; // 壁や障害物のTilemapの配列


    // 弾の生成メソッド
    public void SpawnBullet(Vector2 spawnPosition)
    {
        // 弾の生成
        GameObject bullet = Instantiate(gameObject, spawnPosition, Quaternion.identity);

        // コライダーの有無を判定して結果を取得
        bool hasCollider = CheckColliderAtPosition(spawnPosition);

        if (hasCollider)
        {
            // コライダーがある場合は弾をすぐに破壊
            Destroy(bullet);
        }
        else
        {
            // コライダーがない場合は通常の処理を続行
            // ここで弾の速度や移動方向などの設定を行うことができます
        }
    }

    // 複数のタイルマップを利用してコライダーの有無を判定するメソッド
    private bool CheckColliderAtPosition(Vector2 position)
    {
        foreach (var tilemap in obstacleTilemaps)
        {
            // タイル座標に変換
            Vector3Int cellPosition = tilemap.WorldToCell(position);

            // タイルの取得
            TileBase tile = tilemap.GetTile(cellPosition);

            // タイルが存在し、TileColliderがある場合はコライダーがあると判定
            if (tile != null && tile.GetComponent<TileCollider>() != null)
            {
                return true; // コライダーが見つかれば即座にtrueを返して終了
            }
        }

        return false; // 複数のタイルマップをチェックしてもコライダーが見つからない場合はfalseを返す
    }
}

 obstacleTilemapsに複数のタイルマップを設定し、for文で各タイルマップを順にチェックすることができます。
最初に見つかったコライダーがあるタイルマップで弾を破壊するようにしています。


まとめ


 アプローチの方法は他にもあります。色々な方法を試してみるとよいでしょう。

 今回のアプローチの中であれば、2の方法の方が汎用性が高く、アサイン情報もレイヤーの指定(Ground)1個だけで済むので、
採用するなら、アプローチ2のソースコードの方がおすすめです。