Skip to content

Instantly share code, notes, and snippets.

@shanecelis
Last active June 5, 2019 05:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save shanecelis/4d418a013fd2403d58c19e68be88925f to your computer and use it in GitHub Desktop.
Save shanecelis/4d418a013fd2403d58c19e68be88925f to your computer and use it in GitHub Desktop.
A fix for Unity's PhysicsRaycaster that would throw NullReferenceExceptions when Max Ray Intersections is not zero.
// https://bitbucket.org/Unity-Technologies/ui/raw/9f418c4767c47d0c71f1727eb42a9a9024e9ecc0/UnityEngine.UI/EventSystem/Raycasters/PhysicsRaycaster.cs
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace UnityEngine.EventSystems
{
/// <summary>
/// Simple event system using physics raycasts.
/// </summary>
[AddComponentMenu("Event/Physics Raycaster")]
[RequireComponent(typeof(Camera))]
public class MyPhysicsRaycaster : BaseRaycaster
{
/// <summary>
/// Const to use for clarity when no event mask is set
/// </summary>
protected const int kNoEventMaskSet = -1;
protected Camera m_EventCamera;
/// <summary>
/// Layer mask used to filter events. Always combined with the camera's culling mask if a camera is used.
/// </summary>
[SerializeField]
protected LayerMask m_EventMask = kNoEventMaskSet;
/// <summary>
/// The max number of intersections allowed. 0 = allocating version anything else is non alloc.
/// </summary>
[SerializeField]
protected int m_MaxRayIntersections = 0;
protected int m_LastMaxRayIntersections = 0;
RaycastHit[] m_Hits;
protected RaycastHitComparer m_RaycastHitComparer = new RaycastHitComparer();
protected MyPhysicsRaycaster()
{}
public override Camera eventCamera
{
get
{
if (m_EventCamera == null)
m_EventCamera = GetComponent<Camera>();
return m_EventCamera ?? Camera.main;
}
}
/// <summary>
/// Depth used to determine the order of event processing.
/// </summary>
public virtual int depth
{
get { return (eventCamera != null) ? (int)eventCamera.depth : 0xFFFFFF; }
}
/// <summary>
/// Event mask used to determine which objects will receive events.
/// </summary>
public int finalEventMask
{
get { return (eventCamera != null) ? eventCamera.cullingMask & m_EventMask : kNoEventMaskSet; }
}
/// <summary>
/// Layer mask used to filter events. Always combined with the camera's culling mask if a camera is used.
/// </summary>
public LayerMask eventMask
{
get { return m_EventMask; }
set { m_EventMask = value; }
}
public int maxRayIntersections
{
get { return m_MaxRayIntersections; }
set { m_MaxRayIntersections = value; }
}
protected void ComputeRayAndDistance(PointerEventData eventData, out Ray ray, out float distanceToClipPlane)
{
ray = eventCamera.ScreenPointToRay(eventData.position);
// compensate far plane distance - see MouseEvents.cs
float projectionDirection = ray.direction.z;
distanceToClipPlane = Mathf.Approximately(0.0f, projectionDirection)
? Mathf.Infinity
: Mathf.Abs((eventCamera.farClipPlane - eventCamera.nearClipPlane) / projectionDirection);
}
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
// Cull ray casts that are outside of the view rect. (case 636595)
if (eventCamera == null || !eventCamera.pixelRect.Contains(eventData.position))
return;
Ray ray;
float distanceToClipPlane;
ComputeRayAndDistance(eventData, out ray, out distanceToClipPlane);
int hitCount = 0;
if (m_MaxRayIntersections == 0)
{
if (MyReflectionMethodsCache.Singleton.raycast3DAll == null)
return;
m_Hits = MyReflectionMethodsCache.Singleton.raycast3DAll(ray, distanceToClipPlane, finalEventMask);
hitCount = m_Hits.Length;
}
else
{
if (MyReflectionMethodsCache.Singleton.getRaycastNonAlloc == null)
return;
if (m_LastMaxRayIntersections != m_MaxRayIntersections)
{
m_Hits = new RaycastHit[m_MaxRayIntersections];
m_LastMaxRayIntersections = m_MaxRayIntersections;
}
hitCount = MyReflectionMethodsCache.Singleton.getRaycastNonAlloc(ray, m_Hits, distanceToClipPlane, finalEventMask);
}
/** Bug Fix
=======
Bug Behavior
------------
Setup a PhysicsRaycaster on a camera and set the "Max Ray
Intersections" to 5. Open scene BUG DEMO and mouse over the
scene and a NullReferenceException is thrown. You can drag the
cube around sometimes.
Bug Diagnosis
-------------
The code throwing the exception is from line 133
of this file[1].
gameObject = m_Hits[b].collider.gameObject,
The null object is the collider. How can it be null? It seems
that if you set the "Max Ray Intersections", you have a
fixed-size array of m_Hits. However, the original call to sort
does not take this into account.
System.Array.Sort(m_Hits, (r1, r2) => r1.distance.CompareTo(r2.distance));
Bug Fix
-------
An easy fix would be to do this:
System.Array.Sort(m_Hits, 0, hitCount, (r1, r2) => r1.distance.CompareTo(r2.distance));
Unfortunately, there's no Sort<T>(T[], Int32, Int32,
Comparison<T>) that uses the same comparison delegate. So
instead I implemented a IComparison<RaycastHit> and pass that in
and it seems the bug is resolved.
Open scene BUG FIX. Drag the cube around. Observe no null
reference exceptions.
[1]: https://bitbucket.org/Unity-Technologies/ui/src/9f418c4767c47d0c71f1727eb42a9a9024e9ecc0/UnityEngine.UI/EventSystem/Raycasters/PhysicsRaycaster.cs#lines-133
*/
if (hitCount > 1)
// System.Array.Sort(m_Hits, 0, hitCount, (r1, r2) => r1.distance.CompareTo(r2.distance));
System.Array.Sort(m_Hits, 0, hitCount, m_RaycastHitComparer);
if (hitCount != 0)
{
for (int b = 0, bmax = hitCount; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = m_Hits[b].collider.gameObject,
module = this,
distance = m_Hits[b].distance,
worldPosition = m_Hits[b].point,
worldNormal = m_Hits[b].normal,
screenPosition = eventData.position,
index = resultAppendList.Count,
sortingLayer = 0,
sortingOrder = 0
};
resultAppendList.Add(result);
}
}
}
protected class RaycastHitComparer : IComparer<RaycastHit>
{
public int Compare(RaycastHit r1, RaycastHit r2)
{
return r1.distance.CompareTo(r2.distance);
}
}
}
}
namespace UnityEngine.UI
{
internal class MyReflectionMethodsCache
{
public delegate bool Raycast3DCallback(Ray r, out RaycastHit hit, float f, int i);
public delegate RaycastHit2D Raycast2DCallback(Vector2 p1, Vector2 p2, float f, int i);
public delegate RaycastHit[] RaycastAllCallback(Ray r, float f, int i);
public delegate RaycastHit2D[] GetRayIntersectionAllCallback(Ray r, float f, int i);
public delegate int GetRayIntersectionAllNonAllocCallback(Ray r, RaycastHit2D[] results, float f, int i);
public delegate int GetRaycastNonAllocCallback(Ray r, RaycastHit[] results, float f, int i);
// We call Physics.Raycast and Physics2D.Raycast through reflection to avoid creating a hard dependency from
// this class to the Physics/Physics2D modules, which would otherwise make it impossible to make content with UI
// without force-including both modules.
public MyReflectionMethodsCache()
{
var raycast3DMethodInfo = typeof(Physics).GetMethod("Raycast", new[] {typeof(Ray), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int)});
if (raycast3DMethodInfo != null)
raycast3D = (Raycast3DCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(Raycast3DCallback), raycast3DMethodInfo);
var raycast2DMethodInfo = typeof(Physics2D).GetMethod("Raycast", new[] {typeof(Vector2), typeof(Vector2), typeof(float), typeof(int)});
if (raycast2DMethodInfo != null)
raycast2D = (Raycast2DCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(Raycast2DCallback), raycast2DMethodInfo);
var raycastAllMethodInfo = typeof(Physics).GetMethod("RaycastAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (raycastAllMethodInfo != null)
raycast3DAll = (RaycastAllCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(RaycastAllCallback), raycastAllMethodInfo);
var getRayIntersectionAllMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (getRayIntersectionAllMethodInfo != null)
getRayIntersectionAll = (GetRayIntersectionAllCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRayIntersectionAllCallback), getRayIntersectionAllMethodInfo);
var getRayIntersectionAllNonAllocMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionNonAlloc", new[] { typeof(Ray), typeof(RaycastHit2D[]), typeof(float), typeof(int) });
if (getRayIntersectionAllNonAllocMethodInfo != null)
getRayIntersectionAllNonAlloc = (GetRayIntersectionAllNonAllocCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRayIntersectionAllNonAllocCallback), getRayIntersectionAllNonAllocMethodInfo);
var getRaycastAllNonAllocMethodInfo = typeof(Physics).GetMethod("RaycastNonAlloc", new[] { typeof(Ray), typeof(RaycastHit[]), typeof(float), typeof(int) });
if (getRaycastAllNonAllocMethodInfo != null)
getRaycastNonAlloc = (GetRaycastNonAllocCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRaycastNonAllocCallback), getRaycastAllNonAllocMethodInfo);
}
public Raycast3DCallback raycast3D = null;
public RaycastAllCallback raycast3DAll = null;
public Raycast2DCallback raycast2D = null;
public GetRayIntersectionAllCallback getRayIntersectionAll = null;
public GetRayIntersectionAllNonAllocCallback getRayIntersectionAllNonAlloc = null;
public GetRaycastNonAllocCallback getRaycastNonAlloc = null;
private static MyReflectionMethodsCache s_ReflectionMethodsCache = null;
public static MyReflectionMethodsCache Singleton
{
get
{
if (s_ReflectionMethodsCache == null)
s_ReflectionMethodsCache = new MyReflectionMethodsCache();
return s_ReflectionMethodsCache;
}
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment