Last active
June 5, 2019 05:49
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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