Skip to content

Instantly share code, notes, and snippets.

@jonagill
Last active June 14, 2018 18:00
Show Gist options
  • Save jonagill/16de46ec9716cf78b5841a15f0f17a4b to your computer and use it in GitHub Desktop.
Save jonagill/16de46ec9716cf78b5841a15f0f17a4b to your computer and use it in GitHub Desktop.
Tool to track what collisions are happening in a Unity game
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public class CollisionDebugger : EditorWindow
{
private const int MAX_SAMPLES = 300;
static CollisionDebugger()
{
EditorApplication.playModeStateChanged += OnPlaymodeStateChange;
EditorApplication.update += OnUpdate;
}
[MenuItem("Window/Collision Debugger")]
public static void CreateWindow()
{
var window = GetWindow<CollisionDebugger>("Collision Debugger");
window.autoRepaintOnSceneChange = true;
window.Show();
}
public enum CollisionEvent
{
Collision_Enter,
Collision_Stay,
Collision_Exit,
Trigger_Enter,
Trigger_Stay,
Trigger_Exit,
}
private class FrameSample
{
public string name;
public int frame;
public float timestamp;
public int collisionEventCount;
public int triggerEventCount;
public Dictionary<CollisionEvent, CollisionInfo[]> collisionEvents;
}
private class CollisionInfo
{
public GameObject gameObject;
public Collider otherCollider;
}
private static readonly List<FrameSample> frameSamples = new List<FrameSample>(MAX_SAMPLES);
private static readonly Dictionary<CollisionEvent, List<CollisionInfo>> cachedCollisionEvents = new Dictionary<CollisionEvent, List<CollisionInfo>>();
public static void ReportCollision(CollisionEvent eventType, GameObject gameObject, Collider otherCollider)
{
List<CollisionInfo> events;
if (!cachedCollisionEvents.TryGetValue(eventType, out events))
{
events = new List<CollisionInfo>();
cachedCollisionEvents[eventType] = events;
}
events.Add(new CollisionInfo()
{
gameObject = gameObject,
otherCollider = otherCollider
});
}
private static void OnUpdate()
{
if (Application.isPlaying && !EditorApplication.isPaused)
{
// Don't ever exceed the allocated number of samples
if (frameSamples.Count == frameSamples.Capacity)
{
frameSamples.RemoveAt(0);
}
var frameSample = CollectFrameSample();
frameSamples.Add(frameSample);
ClearCollisionBuffers();
}
}
private static void OnPlaymodeStateChange(PlayModeStateChange change)
{
if (change == PlayModeStateChange.EnteredPlayMode)
{
frameSamples.Clear();
}
}
private static FrameSample CollectFrameSample()
{
// Copy all the cached collision events from this frame into a new collection
var collisionEvents = new Dictionary<CollisionEvent, CollisionInfo[]>();
var orderedEvents = cachedCollisionEvents.OrderBy(pair => pair.Key);
foreach (var events in orderedEvents)
{
collisionEvents.Add(events.Key, events.Value.ToArray());
}
var collisionEventCount = collisionEvents
.Where(e =>
e.Key == CollisionEvent.Collision_Enter ||
e.Key == CollisionEvent.Collision_Stay ||
e.Key == CollisionEvent.Collision_Exit)
.Select(e => e.Value.Length)
.Sum();
var triggerEventCount = collisionEvents
.Where(e =>
e.Key == CollisionEvent.Trigger_Enter ||
e.Key == CollisionEvent.Trigger_Stay ||
e.Key == CollisionEvent.Trigger_Exit)
.Select(e => e.Value.Length)
.Sum();
var frame = Time.frameCount;
var frameName = string.Format(
"Frame {0} (C: {1}, T: {2})",
frame,
collisionEventCount,
triggerEventCount
);
return new FrameSample()
{
name = frameName,
frame = frame,
timestamp = Time.time,
collisionEventCount = collisionEventCount,
triggerEventCount = triggerEventCount,
collisionEvents = collisionEvents,
};
}
private static void ClearCollisionBuffers()
{
foreach (var pair in cachedCollisionEvents)
{
pair.Value.Clear();
}
}
private static void AttachDebugReporters()
{
var rigidbodies = FindObjectsOfType<Rigidbody>();
foreach (var rigidbody in rigidbodies)
{
if (!rigidbody.GetComponent<DebugCollisionReporter>())
{
rigidbody.gameObject.AddComponent<DebugCollisionReporter>();
}
}
}
private FrameSample detailSample;
private void Awake()
{
if (Application.isPlaying)
{
AttachDebugReporters();
}
}
private void OnGUI()
{
using (new EditorGUILayout.VerticalScope())
{
using (new EditorGUILayout.VerticalScope(GUILayout.ExpandHeight(true)))
{
var sampleToRender = detailSample;
if (DebugCollisionReporter.Count == 0)
{
EditorGUILayout.LabelField("No tracked rigidbodies");
}
else if (detailSample == null)
{
detailSample = DrawListGUI();
}
else
{
if (GUILayout.Button("Return"))
{
detailSample = null;
}
DrawDetailedGUI(sampleToRender);
}
if (sampleToRender != detailSample)
{
// Reset the scroll view if we've changed focus
detailedScrollViewPos = Vector2.zero;
}
}
EditorGUILayout.Separator();
GUI.enabled = Application.isPlaying;
if (GUILayout.Button("Refresh Tracked Rigidbodies"))
{
AttachDebugReporters();
}
GUI.enabled = true;
EditorGUILayout.Separator();
}
}
private Vector2 listScrollViewPos;
private FrameSample DrawListGUI()
{
FrameSample selectedSample = null;
using (var scrollView = new EditorGUILayout.ScrollViewScope(listScrollViewPos, "box"))
{
EditorGUI.indentLevel++;
listScrollViewPos = scrollView.scrollPosition;
foreach (var frameSample in frameSamples)
{
EditorGUILayout.PrefixLabel(frameSample.name);
if (GUILayout.Button("Details"))
{
selectedSample = frameSample;
}
}
EditorGUI.indentLevel--;
}
return selectedSample;
}
private Vector2 detailedScrollViewPos;
private void DrawDetailedGUI(FrameSample sample)
{
using (var scrollView = new EditorGUILayout.ScrollViewScope(detailedScrollViewPos, "box"))
{
EditorGUI.indentLevel++;
detailedScrollViewPos = scrollView.scrollPosition;
EditorGUILayout.Separator();
EditorGUILayout.LabelField(sample.name, EditorStyles.boldLabel);
EditorGUILayout.Separator();
foreach (var collisionEvent in sample.collisionEvents)
{
using (new EditorGUILayout.VerticalScope("box"))
{
EditorGUI.indentLevel++;
var eventType = collisionEvent.Key;
var collisions = collisionEvent.Value;
var eventName = eventType.ToString().Replace("_", " ");
var title = string.Format("{0} ({1})", eventName, collisions.Length);
EditorGUILayout.LabelField(title);
EditorGUI.indentLevel++;
foreach (var collision in collisions)
{
EditorGUILayout.LabelField(string.Format("{0}:", collision.gameObject.name));
using (new EditorGUILayout.VerticalScope())
{
EditorGUILayout.ObjectField("Object", collision.gameObject, typeof(Collider), true);
EditorGUILayout.ObjectField("Other", collision.otherCollider, typeof(Collider), true);
}
EditorGUILayout.Separator();
}
EditorGUI.indentLevel--;
EditorGUI.indentLevel--;
}
}
EditorGUI.indentLevel--;
}
}
}
#endif
#if UNITY_EDITOR
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Debug class that reports collisions on this object to CollisionDebugger
/// </summary>
public class DebugCollisionReporter : MonoBehaviour
{
public static int Count { get; private set; }
private void Awake()
{
Count++;
}
private void OnDestroy()
{
Count--;
}
private void OnCollisionEnter(Collision collision)
{
CollisionDebugger.ReportCollision(
CollisionDebugger.CollisionEvent.Collision_Enter,
gameObject,
collision.collider);
}
private void OnCollisionStay(Collision collision)
{
CollisionDebugger.ReportCollision(
CollisionDebugger.CollisionEvent.Collision_Stay,
gameObject,
collision.collider);
}
private void OnCollisionExit(Collision collision)
{
CollisionDebugger.ReportCollision(
CollisionDebugger.CollisionEvent.Collision_Exit,
gameObject,
collision.collider);
}
private void OnTriggerEnter(Collider other)
{
CollisionDebugger.ReportCollision(
CollisionDebugger.CollisionEvent.Trigger_Enter,
gameObject,
other);
}
private void OnTriggerStay(Collider other)
{
CollisionDebugger.ReportCollision(
CollisionDebugger.CollisionEvent.Trigger_Stay,
gameObject,
other);
}
private void OnTriggerExit(Collider other)
{
CollisionDebugger.ReportCollision(
CollisionDebugger.CollisionEvent.Trigger_Exit,
gameObject,
other);
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment