Buffered Raycast

I made this class to make the raycast more efficient by using a hit and distance buffer. It also can return all components of a given type hit by the raycast, and sort by distance. I made this a class because I figured I may use these features in other places.

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

namespace RPG.Control {
    
    /// <summary>
    /// This class is used to do raycast with a buffer.
    /// </summary>
    public class BufferedRaycast {

        private readonly RaycastHit[] _raycastBuffer;
        private readonly float[] _distanceBuffer;
        private int _hitSize;
        private readonly int _allowedSize;
        
        /// <summary>
        /// This method is used to create a new <see cref="BufferedRaycast"/>.
        /// </summary>
        /// <param name="allowedSize">The max number of <see cref="GameObject"/> that
        /// can be returned from the raycast.</param>
        public BufferedRaycast(int allowedSize) {
            this._allowedSize = allowedSize;
            _raycastBuffer = new RaycastHit[allowedSize];
            _distanceBuffer = new float[allowedSize];
        }

        /// <summary>
        /// This method is used to preform a raycast using the allocated buffers.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the raycast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <returns>The result of the raycast.</returns>
        public IEnumerable<RaycastHit> Raycast(Ray ray, bool sortByDistance = false, 
            float maxDistance = Mathf.Infinity , int? layerMask = null) {
            //do raycast
            DoRaycast(ray,maxDistance,layerMask);
            //sort raycast
            if(sortByDistance) SortResultsByDistance();
            //return the values
            for(var i = 0; i < _hitSize; i++) yield return _raycastBuffer[i];
        }

        /// <summary>
        /// This method is used to get all of the <see cref="MonoBehaviour"/> of the type
        /// <see cref="T"/> of all the <see cref="GameObject"/> hit by the raycast.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the raycast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <typeparam name="T">The type of <see cref="MonoBehaviour"/> you want to return from the raycast.</typeparam>
        /// <returns>All the <see cref="T"/> <see cref="MonoBehaviour"/> from the raycast. </returns>
        public IEnumerable<T> ComponentRaycast<T>(Ray ray, bool sortByDistance = false,
            float maxDistance = Mathf.Infinity , int? layerMask = null) {
            foreach(var hit in Raycast(ray, sortByDistance, maxDistance, layerMask))
                foreach(var component in hit.transform.GetComponents<T>())
                    yield return component;
        }
        
        /// <summary>
        /// This method is used to sort the result of
        /// a raycast by the distance of the GameObject.
        /// </summary>
        private void SortResultsByDistance() {
            for(var i = 0; i < _hitSize; i++) {
                _distanceBuffer[i] = _raycastBuffer[i].distance;
            }
            Array.Sort(_distanceBuffer,_raycastBuffer,0,_hitSize);
        }
        
        /// <summary>
        /// This method is used to do the actual raycast.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the raycast.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        private void DoRaycast(Ray ray, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            _hitSize = layerMask.HasValue ? 
                Physics.RaycastNonAlloc(ray, _raycastBuffer, maxDistance, layerMask.Value) : 
                Physics.RaycastNonAlloc(ray, _raycastBuffer, maxDistance);
        }
        
    }
}

If you use this you need to create an instance variable and instantiate it in the awake method. If you are doing raycasts in multiple parts of your code, use a separate BufferedRaycast for each place.

My interact with component method now looks like this.

private bool InteractWithComponent() {
    foreach(var hit in iCastableRaycast.ComponentRaycast<IRaycastable>(GetMouseRay(),true)){
        if(hit.HandleRaycast(this)) {
            SetCursor(hit.GetCursorType());
            return true;
        }
    }
    return false;
 }

Well done, this looks handy!

I have expanded the class so that it is more complete.

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

namespace RPG.Control {
    
    /// <summary>
    /// This class is used to do raycast with a buffer.
    /// </summary>
    public class BufferedRaycast {

        private readonly RaycastHit[] _raycastBuffer;
        private readonly float[] _distanceBuffer;
        private int _hitSize;
        private readonly int _allowedSize;
        
        /// <summary>
        /// This method is used to create a new <see cref="BufferedRaycast"/>.
        /// </summary>
        /// <param name="allowedSize">The max number of <see cref="GameObject"/> that
        /// can be returned from the raycast.</param>
        public BufferedRaycast(int allowedSize) {
            _allowedSize = allowedSize;
            _raycastBuffer = new RaycastHit[allowedSize];
            _distanceBuffer = new float[allowedSize];
        }

        /// <summary>
        /// This method is used to preform a raycast using the allocated buffers.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the raycast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <returns>The result of the raycast.</returns>
        public IEnumerable<RaycastHit> Raycast(Ray ray, bool sortByDistance = false, 
            float maxDistance = Mathf.Infinity , int? layerMask = null) {
            //do raycast
            DoRaycast(ray,maxDistance,layerMask);
            //sort raycast
            if(sortByDistance) SortResultsByDistance();
            //return the values
            for(var i = 0; i < _hitSize; i++) yield return _raycastBuffer[i];
        }
        
        /// <summary>
        /// This method is used to preform a raycast using the allocated buffers.
        /// </summary>
        /// <param name="origin">The starting position of the raycast.</param>
        /// <param name="direction">The direction of the raycast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <returns>The result of the raycast.</returns>
        public IEnumerable<RaycastHit> Raycast(Vector3 origin, Vector3 direction, bool sortByDistance = false, 
            float maxDistance = Mathf.Infinity , int? layerMask = null) {
            //do raycast
            DoRaycast(origin,direction,maxDistance,layerMask);
            //sort raycast
            if(sortByDistance) SortResultsByDistance();
            //return the values
            for(var i = 0; i < _hitSize; i++) yield return _raycastBuffer[i];
        }
        
        /// <summary>
        /// This method is used to preform a sphere cast using the allocated buffers.
        /// </summary>
        /// <param name="origin">The starting position of the sphere cast.</param>
        /// <param name="radius">The radius of the sphere cast.</param>
        /// <param name="direction">The direction of the sphere cast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <returns>The result of the raycast.</returns>
        public IEnumerable<RaycastHit> SphereCast(Vector3 origin, float radius, Vector3 direction,
            bool sortByDistance = false, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            //do the sphere cast
            DoSphereCast(origin,radius,direction,maxDistance,layerMask);
            //sort raycast
            if(sortByDistance) SortResultsByDistance();
            //return the values
            for(var i = 0; i < _hitSize; i++) yield return _raycastBuffer[i];
        }

        /// <summary>
        /// This method is used to preform a sphere cast using the allocated buffers.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the sphere cast.</param>
        /// <param name="radius">The radius of the sphere cast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <returns>The result of the raycast.</returns>
        public IEnumerable<RaycastHit> SphereCast(Ray ray, float radius,
            bool sortByDistance = false, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            //do the sphere cast
            DoSphereCast(ray,radius,maxDistance,layerMask);
            //sort raycast
            if(sortByDistance) SortResultsByDistance();
            //return the values
            for(var i = 0; i < _hitSize; i++) yield return _raycastBuffer[i];
        }

        /// <summary>
        /// This method is used to get all of the <see cref="MonoBehaviour"/> of the given type.
        /// <see cref="T"/> of all the <see cref="GameObject"/> hit by the raycast.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the raycast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <typeparam name="T">The type of <see cref="MonoBehaviour"/> you want to return from the raycast.</typeparam>
        /// <returns>All the <see cref="T"/> <see cref="MonoBehaviour"/> from the raycast. </returns>
        public IEnumerable<T> FilteredRaycast<T>(Ray ray, bool sortByDistance = false,
            float maxDistance = Mathf.Infinity , int? layerMask = null) {
            foreach(var hit in Raycast(ray, sortByDistance, maxDistance, layerMask))
                foreach(var component in hit.transform.GetComponents<T>())
                    yield return component;
        }
        
        /// <summary>
        /// This method is used to get all of the <see cref="MonoBehaviour"/> of the given type.
        /// <see cref="T"/> of all the <see cref="GameObject"/> hit by the raycast.
        /// </summary>
        /// <param name="origin">The starting position of the raycast.</param>
        /// <param name="direction">The direction of the raycast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <typeparam name="T">The type of <see cref="MonoBehaviour"/> you want to return from the raycast.</typeparam>
        /// <returns>All the <see cref="T"/> <see cref="MonoBehaviour"/> from the raycast. </returns>
        public IEnumerable<T> FilteredRaycast<T>(Vector3 origin, Vector3 direction, bool sortByDistance = false,
            float maxDistance = Mathf.Infinity , int? layerMask = null) {
            foreach(var hit in Raycast(origin, direction, sortByDistance, maxDistance, layerMask))
            foreach(var component in hit.transform.GetComponents<T>())
                yield return component;
        }

        /// <summary>
        /// This method is used to get all of the <see cref="MonoBehaviour"/> of the given type.
        /// <see cref="T"/> of all the <see cref="GameObject"/> hit by the raycast.
        /// </summary>
        /// <param name="origin">The starting position of the sphere cast.</param>
        /// <param name="radius">The radius of the sphere cast.</param>
        /// <param name="direction">The direction of the sphere cast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <typeparam name="T">The type of <see cref="MonoBehaviour"/> you want to return from the sphere cast.</typeparam>
        /// <returns>All the <see cref="T"/> <see cref="MonoBehaviour"/> from the sphere cast. </returns>
        public IEnumerable<T> FilteredSphereCast<T>(Vector3 origin, float radius, Vector3 direction,
            bool sortByDistance = false, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            foreach(var hit in SphereCast(origin, radius, direction, sortByDistance, maxDistance, layerMask))
                foreach(var component in hit.transform.GetComponents<T>())
                    yield return component;
        }
        
        /// <summary>
        /// This method is used to get all of the <see cref="MonoBehaviour"/> of the given type.
        /// <see cref="T"/> of all the <see cref="GameObject"/> hit by the raycast.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the sphere cast.</param>
        /// <param name="radius">The radius of the sphere cast.</param>
        /// <param name="sortByDistance">If true the results will be sorted by distance,
        /// otherwise the results will not be sorted.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        /// <typeparam name="T">The type of <see cref="MonoBehaviour"/> you want to return from the sphere cast.</typeparam>
        /// <returns>All the <see cref="T"/> <see cref="MonoBehaviour"/> from the sphere cast. </returns>
        public IEnumerable<T> FilteredSphereCast<T>(Ray ray, float radius,
            bool sortByDistance = false, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            foreach(var hit in SphereCast(ray, radius, sortByDistance, maxDistance, layerMask))
            foreach(var component in hit.transform.GetComponents<T>())
                yield return component;
        }
        
        /// <summary>
        /// This method is used to sort the result of
        /// a raycast by the distance of the GameObject.
        /// </summary>
        private void SortResultsByDistance() {
            for(var i = 0; i < _hitSize; i++) {
                _distanceBuffer[i] = _raycastBuffer[i].distance;
            }
            Array.Sort(_distanceBuffer,_raycastBuffer,0,_hitSize);
        }
        
        /// <summary>
        /// This method is used to do the actual raycast.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the raycast.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        private void DoRaycast(Ray ray, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            _hitSize = layerMask.HasValue ? 
                Physics.RaycastNonAlloc(ray, _raycastBuffer, maxDistance, layerMask.Value) : 
                Physics.RaycastNonAlloc(ray, _raycastBuffer, maxDistance);
        }
        
        /// <summary>
        /// This method is used to do the actual raycast.
        /// </summary>
        /// <param name="origin">The starting position of the ray cast.</param>
        /// <param name="direction">The direction of the ray cast.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        private void DoRaycast(Vector3 origin, Vector3 direction, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            _hitSize = layerMask.HasValue ? 
                Physics.RaycastNonAlloc(origin, direction, _raycastBuffer, maxDistance, layerMask.Value) : 
                Physics.RaycastNonAlloc(origin, direction, _raycastBuffer, maxDistance);
        }

        /// <summary>
        /// This method is used to do the actual sphere cast.
        /// </summary>
        /// <param name="origin">The starting position of the sphere cast.</param>
        /// <param name="radius">The radius of the sphere cast.</param>
        /// <param name="direction">The direction of the sphere cast.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        private void DoSphereCast(Vector3 origin, float radius, Vector3 direction,
            float maxDistance = Mathf.Infinity, int? layerMask = null) {
            _hitSize = layerMask.HasValue
                ? Physics.SphereCastNonAlloc(origin, radius, direction, _raycastBuffer, maxDistance, layerMask.Value)
                : Physics.SphereCastNonAlloc(origin, radius, direction, _raycastBuffer, maxDistance);
        }
        
        /// <summary>
        /// This method is used to do the actual sphere cast.
        /// </summary>
        /// <param name="ray">The <see cref="Ray"/> you want to use for the sphere cast.</param>
        /// <param name="radius">The radius of the sphere cast.</param>
        /// <param name="maxDistance">The maximum distance to travel along the <see cref="Ray"/>.</param>
        /// <param name="layerMask">(Optional)The <see cref="LayerMask"/> that will be used for
        /// the raycast.</param>
        private void DoSphereCast(Ray ray, float radius, float maxDistance = Mathf.Infinity, int? layerMask = null) {
            _hitSize = layerMask.HasValue
                ? Physics.SphereCastNonAlloc(ray, radius, _raycastBuffer, maxDistance, layerMask.Value)
                : Physics.SphereCastNonAlloc(ray, radius, _raycastBuffer, maxDistance);
        }

    }
}
1 Like

@Brandon_Anderson — Roughly two years later, thanks for sharing this alternative solution! Even after 25+ years of professional programming, I’m still learning new things from Sam, Rick, and Brian, as well as extremely knowledgeable fellow class participants such as yourself.

Great work!

I flagged this one too when I came upon it recently. I tried to implement the NonAlloc version of Raycast myself but quickly found there are many hidden nuances. It was not terribly hard to figure out but this code does a nice job of taking care of it for you.

The only change I would add to this code is the ability to dynamically resize the buffer.

Here’s my pseudocode within the DoRay/SphereCast method:

_hitSize = SphereCastNonAlloc() // or RayCast
while (_hitSize == _allowed size) {
    DoubleSizeOfBuffers(); //doubles the size of allowedSize, and the two buffers
    _hitSize = SphereCastNonAlloc(); // or RayCast
}

It might seem expensive to double the size of buffers and redo it, but the cost amortizes over time to be negligible and removes the guessing game of how big to make the buffers.

1 Like

With the changed implementation of BufferedRaycast the invocation needs to be updated, as well.

    public class PlayerController : MonoBehaviour
    {
        BufferedRaycast iCastableRaycast = null;

        private void Awake()
        {
            // [other initializations]
            // ...
            iCastableRaycast = new BufferedRaycast(16);
        }

        private bool InteractWithComponent()
        {
            // foreach (var hit in iCastableRaycast.ComponentRaycast<IRaycastable>(GetMouseRay(), true))
            foreach (var hit in iCastableRaycast.FilteredRaycast<IRaycastable>(GetMouseRay(), true))
            {
                if (hit.HandleRaycast(this))
                {
                    SetCursor(hit.GetCursorType());
                    return true;
                }
            }
            return false;
        }
    }

As mentioned in Revisiting RaycastAll vs. RaycastNonAlloc as well, setting up the scene for a suitable LayerMask and adding this to the call to FilteredRaycast<>() would be an obvious and straightforward way for further optimizations.
The BufferedRaycast already supports this…

Privacy & Terms