今回の内容
Unityの物理演算において、「レイを飛ばして衝突判定を行う」処理のうち、RaycastNoAlloc
について紹介します。
前座:Raycastとは
概要
Raycast
とは、指定したポイントから「線」を発射し物理演算的に衝突する相手を探す機能です。任意の場所から任意の方向を探索できるため、たとえば「銃の射線が通っているか」のような判定を実現するのに用いることができます。
上記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
を使用するとレイを飛ばした範囲ないで衝突したすべてのオブジェクトを取得できます。
(衝突位置に配列の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が発生してしまいます。
そこで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);
}
}
}
}
(RaycastNoAlloc
ではGC Allocが発生していない。)
後述する注意点も存在はしますが、基本的にこっちのRaycastNoAlloc
の方がパフォーマンスが良いのでこちらを使うことを推奨します。
注意点
格納先の配列のサイズに注意
衝突対象を格納する配列のサイズが衝突したオブジェクト数よりも小さい場合、衝突情報の取りこぼしが発生します。またRaycastNoAlloc
はRaycastAll
と同様に衝突順序を保証しません。
「近いものを数個だけピックアップできればいいや」みたいなシチュエーションであったとしても、十分なバッファを確保しておかないと意図通りに動かない可能性があります。
(大量に衝突しているが一部しか検出できていないし、配列の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);
}
}
}
}
(ヒット数を確認していないため過去のヒット情報が配列に残ったままでそこを参照してしまっている)
補足: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
を持っているように見えるがこれはプロパティであり、実際に保持しているのはCollider
のInstanceID(int)
です。
まとめ
RaycastNoAlloc
はGC Allocを発生させずに衝突情報を取得できるため、RaycastAll
よりもパフォーマンスに優れています。しかし使用時には注意点がいくつかあるため、しっかりと理解してから使用するようにしましょう。