5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】RaycastNonAllocの使い方

Posted at

今回の内容

Unityの物理演算において、「レイを飛ばして衝突判定を行う」処理のうち、RaycastNoAllocについて紹介します。

前座:Raycastとは

概要

Raycastとは、指定したポイントから「線」を発射し物理演算的に衝突する相手を探す機能です。任意の場所から任意の方向を探索できるため、たとえば「銃の射線が通っているか」のような判定を実現するのに用いることができます。

Ray1.gif

上記gifの実装コード
using UnityEngine;

namespace RaycastTest
{
    public class RaycastRenderer : MonoBehaviour
    {
        [SerializeField] private Transform _muzzle;
        [SerializeField] private float _distance = 100f;

        private RaycastHit? _hit;

        void FixedUpdate()
        {
            if (Physics.Raycast(_muzzle.position, _muzzle.forward, out var hit))
            {
                _hit = hit;
            }
            else
            {
                _hit = null;
            }
        }

        void OnDrawGizmos()
        {
            if (_hit.HasValue)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawRay(_muzzle.position, _muzzle.forward * _hit.Value.distance);
                Gizmos.DrawSphere(_hit.Value.point, 0.1f);
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawRay(_muzzle.position, _muzzle.forward * _distance);
            }
        }
    }
}

衝突対象すべてを取得できる「RaycastAll」

Raycastは、レイが衝突した最初のオブジェクトだけを取得しますが、RaycastAllを使用するとレイを飛ばした範囲ないで衝突したすべてのオブジェクトを取得できます。

RaycastAll.jpg
(衝突位置に配列のindex番号を描画)
RaycastAllは衝突したすべてのオブジェクトを取得できるが、衝突の順序は保証されない。

上記の実装コード
using UnityEditor;
using UnityEngine;

namespace RaycastTest
{
    public class RaycastAll : MonoBehaviour
    {
        [SerializeField] private Transform _muzzle;
        [SerializeField] private float _distance = 100f;

        private RaycastHit[] _lastHits;

        private void FixedUpdate()
        {
            _lastHits = Physics.RaycastAll(_muzzle.position, _muzzle.forward, _distance);
        }

        private void OnDrawGizmos()
        {
            if (_lastHits is { Length: > 0 })
            {
                Gizmos.color = Color.green;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);

                for (var index = 0; index < _lastHits.Length; index++)
                {
                    var hit = _lastHits[index];
                    Gizmos.DrawSphere(hit.point, 0.1f);
                    Handles.Label(hit.point + Vector3.up * 0.5f, $"{index}",
                        new GUIStyle
                        {
                            fontSize = 30,
                            normal = { textColor = Color.black }
                        });
                }
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);
            }
        }
    }
}

ただしこのRaycastAllですが、衝突したオブジェクトの数だけ配列を新たに生成してしまうためGC Allocが発生してしまいます。

GCAlloc.jpg

そこでRaycastAllの代わりに用いることができるのが、GC Allocを発生させないRaycastNoAllocです。

RaycastNoAlloc

RaycastNoAllocは、RaycastAllと同じように衝突したオブジェクトを取得することができますが、あらかじめ用意した配列に結果を格納するため新たなGC Allocを発生させません。

using UnityEditor;
using UnityEngine;

namespace RaycastTest
{
    public class RaycastNoAlloc : MonoBehaviour
    {
        [SerializeField] private Transform _muzzle;
        [SerializeField] private float _distance = 100f;

        private readonly RaycastHit[] _lastHits = new RaycastHit[10];
        private int _lastHitCount;

        private void FixedUpdate()
        {
            // Rayを別で作成
            var ray = new Ray(_muzzle.position, _muzzle.forward);

            // 格納先を指定してRaycastを実行
            // 戻り値はヒットした数
            _lastHitCount = Physics.RaycastNonAlloc(ray, _lastHits, _distance);
        }

        private void OnDrawGizmos()
        {
            if (_lastHitCount > 0)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);

                for (var index = 0; index < _lastHitCount; index++)
                {
                    var hit = _lastHits[index];
                    Gizmos.DrawSphere(hit.point, 0.1f);
                    Handles.Label(hit.point + Vector3.up * 0.5f, $"{index}",
                        new GUIStyle
                        {
                            fontSize = 30,
                            normal = { textColor = Color.black }
                        });
                }
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);
            }
        }
    }
}

RycstNoAlloc.jpg
(挙動はRaycastAllと基本同じ)

GCNoAlloc.jpg

RaycastNoAllocではGC Allocが発生していない。)

後述する注意点も存在はしますが、基本的にこっちのRaycastNoAllocの方がパフォーマンスが良いのでこちらを使うことを推奨します。

注意点

格納先の配列のサイズに注意

衝突対象を格納する配列のサイズが衝突したオブジェクト数よりも小さい場合、衝突情報の取りこぼしが発生します。またRaycastNoAllocRaycastAllと同様に衝突順序を保証しません。

「近いものを数個だけピックアップできればいいや」みたいなシチュエーションであったとしても、十分なバッファを確保しておかないと意図通りに動かない可能性があります。

torikoboshi.jpg
(大量に衝突しているが一部しか検出できていないし、配列のindexも近い順とは限らない)

using UnityEditor;
using UnityEngine;

namespace RaycastTest
{
    public class RaycastNoAlloc_SmallBuffer : MonoBehaviour
    {
        [SerializeField] private Transform _muzzle;
        [SerializeField] private float _distance = 100f;

        // バッファサイズが3️しかない
        private readonly RaycastHit[] _lastHits = new RaycastHit[3];
        private int _lastHitCount;

        private void FixedUpdate()
        {
            // Rayを別で作成
            var ray = new Ray(_muzzle.position, _muzzle.forward);

            // 格納先を指定してRaycastを実行
            // 戻り値はヒットした数
            _lastHitCount = Physics.RaycastNonAlloc(ray, _lastHits, _distance);
        }

        private void OnDrawGizmos()
        {
            if (_lastHitCount > 0)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);

                for (var index = 0; index < _lastHitCount; index++)
                {
                    var hit = _lastHits[index];
                    Gizmos.DrawSphere(hit.point, 0.1f);
                    Handles.Label(hit.point + Vector3.up * 0.5f, $"{index}",
                        new GUIStyle
                        {
                            fontSize = 30,
                            normal = { textColor = Color.black }
                        });
                }
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);
            }
        }
    }
}

Hitした数を必ず用いること

RaycastNoAllocは衝突時に指定した配列に格納しますが、その配列の内容はクリアされません。つまり前回の衝突情報がそのまま残っている可能性があります。そのため常にHitした数を確認して、配列のうち有効長部分のみを使用する必要があります。

using System.Linq;
using UnityEditor;
using UnityEngine;

namespace RaycastTest
{
    /// <summary>
    /// Hit数を使わなかった場合
    /// 【正しく動作しないコードです!】
    /// </summary>
    public class RaycastNoAlloc_NoHitCount : MonoBehaviour
    {
        [SerializeField] private Transform _muzzle;
        [SerializeField] private float _distance = 100f;

        private readonly RaycastHit[] _lastHits = new RaycastHit[10];

        private void FixedUpdate()
        {
            // Rayを別で作成
            var ray = new Ray(_muzzle.position, _muzzle.forward);
            
            Physics.RaycastNonAlloc(ray, _lastHits, _distance);
        }

        private void OnDrawGizmos()
        {
            // _lastHitsにヒットしたオブジェクトがあるか確認…をしているが、
            // 実際は配列には過去のヒット情報が残っているで意味がない処理になっている
            var isHit = _lastHits.Any(x => x.collider != null);

            if (isHit)
            {
                Gizmos.color = Color.green;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);

                for (var index = 0; index < _lastHits.Length; index++)
                {
                    var hit = _lastHits[index];
                    if (hit.collider == null) return;

                    Gizmos.DrawSphere(hit.point, 0.1f);
                    Handles.Label(hit.point + Vector3.up * 0.5f, $"{index}",
                        new GUIStyle
                        {
                            fontSize = 30,
                            normal = { textColor = Color.black }
                        });
                }
            }
            else
            {
                Gizmos.color = Color.red;
                Gizmos.DrawLine(_muzzle.position, _muzzle.position + _muzzle.forward * _distance);
            }
        }
    }
}

HitGoast.gif

(ヒット数を確認していないため過去のヒット情報が配列に残ったままでそこを参照してしまっている)

補足:Spanを使う

ちなみにこのような「配列から一部分を切り出して使う」場合はSpan<T>を使うと簡潔にかける場合があります。

private void FixedUpdate()
{
    var ray = new Ray(_muzzle.position, _muzzle.forward);

    // Raycastを実行
    var hitCount = Physics.RaycastNonAlloc(ray, _lastHits, _distance);

    if (hitCount > 0)
    {
        // Spanで配列を切り取り
        OnRaycastHit(_lastHits.AsSpan(0, hitCount));
    }
}

private void OnRaycastHit(ReadOnlySpan<RaycastHit> hits)
{
    // ヒットしたオブジェクトの情報を処理する
    foreach (var hit in hits)
    {
        Debug.Log($"Hit: {hit.collider.name}");
    }
}

小ネタ

ArrayPoolを使う

格納先の配列をフィールドで長期間保有したくない場合はArrayPool<T>を使うとGC Allocを発生させずに配列を使いまわすことができます。

// ArrayPoolから配列を借りる
var rentArray = ArrayPool<RaycastHit>.Shared.Rent(10);

// 使い終わったら返却する
ArrayPool<RaycastHit>.Shared.Return(rentArray);
using System;
using System.Buffers;
using UnityEngine;

namespace RaycastTest
{
    public class RaycastNoAlloc_ArrayPool : MonoBehaviour
    {
        [SerializeField] private Transform _muzzle;
        [SerializeField] private float _distance = 100f;

        private void FixedUpdate()
        {
            // Rayを別で作成
            var ray = new Ray(_muzzle.position, _muzzle.forward);

            // ArrayPoolから配列を借りる
            var rentArray = ArrayPool<RaycastHit>.Shared.Rent(10);

            try
            {
                // Raycastを実行
                var hitCount = Physics.RaycastNonAlloc(ray, rentArray, _distance);
                if (hitCount > 0)
                {
                    // Spanで配列を切り取り
                    OnRaycastHit(rentArray.AsSpan(0, hitCount));
                }
            }
            finally
            {
                // 使用後に確実に配列を返却
                ArrayPool<RaycastHit>.Shared.Return(rentArray);
            }
        }

        private void OnRaycastHit(ReadOnlySpan<RaycastHit> hits)
        {
            // ヒットしたオブジェクトの情報を処理する
            foreach (var hit in hits)
            {
                Debug.Log($"Hit: {hit.collider.name}");
            }
        }
    }
}

ArrayPoolの注意点

ただしArrayPool<T>を使用する場合は次の点に注意する必要があります。

  • 指定したサイズぴったりの配列が借りられるとは限らない
    • 「指定したサイズ以上の配列が借りられる」という仕様である
  • 返却を忘れるとメモリリークを起こす
    • try-finallyで確実に返却するようにするなどの対応が必要
  • 配列の内容は返却時に自動でクリアされない
    • 配列の中身が参照型を含む場合、延々にArrayPool上の配列が参照し続けたままになりうる
    • clearArray: trueを指定することで返却時に配列の内容をクリアを指示できる
    • RaycastHit構造体のため今回は気にしなくていい...※
  • 借りた配列は使い終わったら即返却すること
    • 長期間返却しない場合、プールのサイズが増加してメモリ使用量を増加させる可能性があ

RaycastHitは内部にColliderを持っているように見えるがこれはプロパティであり、実際に保持しているのはColliderInstanceID(int)です。

まとめ

RaycastNoAllocはGC Allocを発生させずに衝突情報を取得できるため、RaycastAllよりもパフォーマンスに優れています。しかし使用時には注意点がいくつかあるため、しっかりと理解してから使用するようにしましょう。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?