using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Editor.Utilities { public class SpriteColliderGenerator : EditorWindow { private Vector2 scrollPosition; [Tooltip("List of GameObjects with SpriteRenderers to generate colliders for")] private List selectedObjects = new List(); [Tooltip("Controls how much to simplify the collider shape (lower values create more complex colliders)")] private float simplificationTolerance = 0.05f; [Tooltip("When enabled, removes any existing PolygonCollider2D components before adding new ones")] private bool replaceExistingColliders = true; [Tooltip("When enabled, applies colliders to all child objects with SpriteRenderers")] private bool applyToChildren = false; [Tooltip("When enabled, allows scaling the collider outward or inward from the sprite center")] private bool offsetFromCenter = false; [Tooltip("Distance to offset the collider from the sprite outline (positive values expand, negative values contract)")] private float offsetDistance = 0f; [Tooltip("When enabled, creates trigger colliders instead of solid colliders")] private bool generateTriggerColliders = false; [Tooltip("Threshold for transparency detection (pixels with alpha below this value are considered transparent)")] private int alphaCutoff = 128; // Used when generating colliders (0-255) [Tooltip("Controls the level of detail for the generated collider (affects vertex count)")] private int detailLevel = 2; // 1 = low, 2 = medium, 3 = high [Tooltip("When enabled, shows a preview of the colliders in the scene view before generating them")] private bool previewColliders = true; [Tooltip("Color used for previewing colliders in the scene view")] private Color previewColor = new Color(0.2f, 1f, 0.3f, 0.5f); private List previewMeshes = new List(); [MenuItem("Tools/Sprite Collider Generator")] public static void ShowWindow() { GetWindow("Sprite Collider Generator"); } private void OnDisable() { // Clean up any preview meshes when window is closed foreach (var mesh in previewMeshes) { if (mesh != null) { DestroyImmediate(mesh); } } previewMeshes.Clear(); } private void OnGUI() { EditorGUILayout.BeginVertical(); EditorGUILayout.LabelField("Sprite Collider Generator", EditorStyles.boldLabel); EditorGUILayout.HelpBox("Select GameObjects with SpriteRenderers and generate accurate PolygonCollider2D components based on the sprite outlines.", MessageType.Info); EditorGUILayout.Space(); // Object selection section EditorGUILayout.LabelField("Selected Objects", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(new GUIContent("Add Selected GameObjects", "Add GameObjects currently selected in the scene or project to the list for processing."))) { AddSelectedGameObjects(); } EditorGUILayout.EndHorizontal(); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150)); for (int i = 0; i < selectedObjects.Count; i++) { EditorGUILayout.BeginHorizontal(); selectedObjects[i] = (GameObject)EditorGUILayout.ObjectField(selectedObjects[i], typeof(GameObject), true); if (GUILayout.Button("X", GUILayout.Width(20))) { selectedObjects.RemoveAt(i); i--; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.EndScrollView(); if (GUILayout.Button(new GUIContent("Clear All", "Remove all objects from the selection list."))) { selectedObjects.Clear(); } EditorGUILayout.Space(); // Collider generation options EditorGUILayout.LabelField("Generation Options", EditorStyles.boldLabel); // Detail level for collider generation (affects vertex count) string[] detailOptions = new string[] { "Low", "Medium", "High" }; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(new GUIContent("Detail Level:", "Controls the level of detail for the generated collider (affects vertex count)."), GUILayout.Width(180)); detailLevel = EditorGUILayout.Popup(detailLevel - 1, detailOptions) + 1; EditorGUILayout.EndHorizontal(); // Simplification tolerance (how much to simplify the collider) EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(new GUIContent("Simplification Tolerance:", "Controls how much to simplify the collider shape (lower values create more complex colliders)."), GUILayout.Width(180)); simplificationTolerance = EditorGUILayout.Slider(simplificationTolerance, 0.01f, 0.2f); EditorGUILayout.EndHorizontal(); // Alpha cutoff for transparency EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(new GUIContent("Alpha Cutoff (0-255):", "Threshold for transparency detection (pixels with alpha below this value are considered transparent)."), GUILayout.Width(180)); alphaCutoff = EditorGUILayout.IntSlider(alphaCutoff, 0, 255); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(); // Additional options replaceExistingColliders = EditorGUILayout.Toggle( new GUIContent("Replace Existing Colliders", "When enabled, removes any existing PolygonCollider2D components before adding new ones."), replaceExistingColliders); applyToChildren = EditorGUILayout.Toggle( new GUIContent("Apply To Children", "When enabled, applies colliders to all child objects with SpriteRenderers."), applyToChildren); generateTriggerColliders = EditorGUILayout.Toggle( new GUIContent("Generate Trigger Colliders", "When enabled, creates trigger colliders instead of solid colliders."), generateTriggerColliders); // Offset option offsetFromCenter = EditorGUILayout.Toggle( new GUIContent("Offset From Center", "When enabled, allows scaling the collider outward or inward from the sprite center."), offsetFromCenter); if (offsetFromCenter) { EditorGUI.indentLevel++; offsetDistance = EditorGUILayout.FloatField( new GUIContent("Offset Distance", "Distance to offset the collider from the sprite outline (positive values expand, negative values contract)."), offsetDistance); EditorGUI.indentLevel--; } // Preview option previewColliders = EditorGUILayout.Toggle( new GUIContent("Preview Colliders", "When enabled, shows a preview of the colliders in the scene view before generating them."), previewColliders); if (previewColliders) { EditorGUI.indentLevel++; previewColor = EditorGUILayout.ColorField( new GUIContent("Preview Color", "Color used for previewing colliders in the scene view."), previewColor); // Create a horizontal layout for the preview buttons EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(new GUIContent("Update Preview", "Refresh the preview display in the scene view."))) { GenerateColliderPreviews(); } if (GUILayout.Button(new GUIContent("Clear Preview", "Remove all preview colliders from the scene view."))) { ClearPreviews(); } EditorGUILayout.EndHorizontal(); EditorGUI.indentLevel--; } EditorGUILayout.Space(); // Generate colliders button GUI.enabled = selectedObjects.Count > 0; if (GUILayout.Button(new GUIContent("Generate Colliders", "Create polygon colliders for all selected sprites based on current settings."))) { GenerateColliders(); } GUI.enabled = true; EditorGUILayout.EndVertical(); // Force the scene view to repaint if we're showing previews if (previewColliders && Event.current.type == EventType.Repaint) { SceneView.RepaintAll(); } } private void AddSelectedGameObjects() { foreach (GameObject obj in Selection.gameObjects) { if (!selectedObjects.Contains(obj)) { // Only add if it has a SpriteRenderer or any of its children do if (obj.GetComponent() != null || (applyToChildren && obj.GetComponentInChildren() != null)) { selectedObjects.Add(obj); } } } } private void GenerateColliderPreviews() { // Clean up existing preview meshes foreach (var mesh in previewMeshes) { if (mesh != null) { DestroyImmediate(mesh); } } previewMeshes.Clear(); if (!previewColliders || selectedObjects.Count == 0) return; foreach (var obj in selectedObjects) { if (obj == null) continue; var spriteRenderers = applyToChildren ? obj.GetComponentsInChildren() : new SpriteRenderer[] { obj.GetComponent() }; foreach (var renderer in spriteRenderers) { if (renderer == null || renderer.sprite == null) continue; Sprite sprite = renderer.sprite; List paths = GetSpritePaths(sprite, simplificationTolerance); if (paths.Count == 0) continue; foreach (var path in paths) { // Create a preview mesh from the path Mesh previewMesh = CreateMeshFromPath(path, renderer.transform); if (previewMesh != null) previewMeshes.Add(previewMesh); } } } } /// /// Clears all preview meshes from the scene view /// private void ClearPreviews() { foreach (var mesh in previewMeshes) { if (mesh != null) { DestroyImmediate(mesh); } } previewMeshes.Clear(); // Force a repaint of the scene view SceneView.RepaintAll(); } private Mesh CreateMeshFromPath(Vector2[] path, Transform transform) { if (path.Length < 3) return null; Mesh mesh = new Mesh(); // Convert the path to 3D vertices and apply the sprite's transform Vector3[] vertices = new Vector3[path.Length]; for (int i = 0; i < path.Length; i++) { // Convert the local position to world space using the transform vertices[i] = transform.TransformPoint(new Vector3(path[i].x, path[i].y, 0)); } // Triangulate the polygon Triangulator triangulator = new Triangulator(path); int[] triangles = triangulator.Triangulate(); mesh.vertices = vertices; mesh.triangles = triangles; mesh.RecalculateNormals(); return mesh; } private void GenerateColliders() { int successCount = 0; List errors = new List(); Undo.RecordObjects(selectedObjects.ToArray(), "Generate Sprite Colliders"); foreach (var obj in selectedObjects) { if (obj == null) continue; try { var spriteRenderers = applyToChildren ? obj.GetComponentsInChildren() : new SpriteRenderer[] { obj.GetComponent() }; foreach (var renderer in spriteRenderers) { if (renderer == null || renderer.sprite == null) continue; // Check if we're working with a prefab bool isPrefab = PrefabUtility.IsPartOfPrefabAsset(renderer.gameObject); GameObject targetObject = renderer.gameObject; if (isPrefab) { // If it's a prefab, we need special handling string prefabPath = AssetDatabase.GetAssetPath(targetObject); targetObject = PrefabUtility.LoadPrefabContents(prefabPath); SpriteRenderer prefabRenderer = targetObject.GetComponent(); if (prefabRenderer == null || prefabRenderer.sprite == null) { PrefabUtility.UnloadPrefabContents(targetObject); continue; } if (GenerateColliderForRenderer(prefabRenderer)) { // Save the changes to the prefab PrefabUtility.SaveAsPrefabAsset(targetObject, prefabPath); successCount++; } PrefabUtility.UnloadPrefabContents(targetObject); } else { // For scene objects, just generate the collider directly if (GenerateColliderForRenderer(renderer)) { successCount++; } } } } catch (Exception e) { errors.Add($"{obj.name}: {e.Message}"); } } // Clean up any preview meshes as we've now generated real colliders ClearPreviews(); if (successCount > 0) { Debug.Log($"Successfully generated colliders for {successCount} sprite(s)."); } if (errors.Count > 0) { Debug.LogError($"Errors occurred while generating colliders:\n{string.Join("\n", errors)}"); } } private bool GenerateColliderForRenderer(SpriteRenderer renderer) { if (renderer == null || renderer.sprite == null) return false; Sprite sprite = renderer.sprite; GameObject targetObject = renderer.gameObject; // Remove existing colliders if specified if (replaceExistingColliders) { PolygonCollider2D[] existingColliders = targetObject.GetComponents(); foreach (var collider in existingColliders) { Undo.DestroyObjectImmediate(collider); } } // Create a new polygon collider PolygonCollider2D polygonCollider = Undo.AddComponent(targetObject); if (polygonCollider == null) return false; // Set as trigger if specified polygonCollider.isTrigger = generateTriggerColliders; // Get paths from the sprite List paths = GetSpritePaths(sprite, simplificationTolerance); if (paths.Count == 0) return false; // Apply offset if needed if (offsetFromCenter && offsetDistance != 0) { for (int i = 0; i < paths.Count; i++) { Vector2[] offsetPath = new Vector2[paths[i].Length]; for (int j = 0; j < paths[i].Length; j++) { // Calculate direction from center (0,0) to the point Vector2 dir = paths[i][j].normalized; // Apply offset in that direction offsetPath[j] = paths[i][j] + dir * offsetDistance; } paths[i] = offsetPath; } } // Set the paths on the collider polygonCollider.pathCount = paths.Count; for (int i = 0; i < paths.Count; i++) { polygonCollider.SetPath(i, paths[i]); } return true; } private List GetSpritePaths(Sprite sprite, float tolerance) { List result = new List(); if (sprite == null) return result; // Get the raw physics shape data from the sprite int physicsShapeCount = sprite.GetPhysicsShapeCount(); if (physicsShapeCount == 0) { // Use the sprite's bounds if no physics shape is defined Vector2[] boundingBoxPath = new Vector2[4]; Bounds bounds = sprite.bounds; boundingBoxPath[0] = new Vector2(bounds.min.x, bounds.min.y); boundingBoxPath[1] = new Vector2(bounds.min.x, bounds.max.y); boundingBoxPath[2] = new Vector2(bounds.max.x, bounds.max.y); boundingBoxPath[3] = new Vector2(bounds.max.x, bounds.min.y); result.Add(boundingBoxPath); return result; } // Adjust the detail level based on the setting float actualTolerance = tolerance; switch (detailLevel) { case 1: // Low actualTolerance = tolerance * 2.0f; break; case 2: // Medium - default actualTolerance = tolerance; break; case 3: // High actualTolerance = tolerance * 0.5f; break; } // Get all physics shapes from the sprite for (int i = 0; i < physicsShapeCount; i++) { List path = new List(); sprite.GetPhysicsShape(i, path); // Apply simplification if needed if (actualTolerance > 0.01f) { path = SimplifyPath(path, actualTolerance); } if (path.Count >= 3) // Need at least 3 points for a valid polygon { result.Add(path.ToArray()); } } return result; } private List SimplifyPath(List points, float tolerance) { if (points.Count <= 3) return points; // Implementation of Ramer-Douglas-Peucker algorithm for simplifying a polygon List result = new List(); List markers = new List(new int[points.Count]); markers[0] = 1; markers[points.Count - 1] = 1; SimplifyDouglasPeucker(points, tolerance, markers, 0, points.Count - 1); for (int i = 0; i < points.Count; i++) { if (markers[i] == 1) { result.Add(points[i]); } } return result; } private void SimplifyDouglasPeucker(List points, float tolerance, List markers, int start, int end) { if (end <= start + 1) return; float maxDistance = 0; int maxIndex = start; Vector2 startPoint = points[start]; Vector2 endPoint = points[end]; // Find the point furthest from the line segment for (int i = start + 1; i < end; i++) { float distance = PerpendicularDistance(points[i], startPoint, endPoint); if (distance > maxDistance) { maxDistance = distance; maxIndex = i; } } // If the furthest point is beyond tolerance, mark it for keeping and recurse if (maxDistance > tolerance) { markers[maxIndex] = 1; SimplifyDouglasPeucker(points, tolerance, markers, start, maxIndex); SimplifyDouglasPeucker(points, tolerance, markers, maxIndex, end); } } private float PerpendicularDistance(Vector2 point, Vector2 lineStart, Vector2 lineEnd) { if (lineStart == lineEnd) return Vector2.Distance(point, lineStart); float dx = lineEnd.x - lineStart.x; float dy = lineEnd.y - lineStart.y; // Normalize float norm = Mathf.Sqrt(dx * dx + dy * dy); if (norm < float.Epsilon) return Vector2.Distance(point, lineStart); dx /= norm; dy /= norm; // Calculate perpendicular distance float px = point.x - lineStart.x; float py = point.y - lineStart.y; float projectionLength = px * dx + py * dy; Vector2 projection = new Vector2( lineStart.x + projectionLength * dx, lineStart.y + projectionLength * dy); return Vector2.Distance(point, projection); } // Scene view event handling for previewing colliders [InitializeOnLoadMethod] static void Initialize() { SceneView.duringSceneGui += OnSceneGUI; } static void OnSceneGUI(SceneView sceneView) { // Find all open collider generator windows var windows = Resources.FindObjectsOfTypeAll(); foreach (var window in windows) { window.DrawColliderPreviews(sceneView); } } void DrawColliderPreviews(SceneView sceneView) { if (!previewColliders || previewMeshes.Count == 0) return; // Draw all preview meshes with the selected color Material previewMaterial = new Material(Shader.Find("Hidden/Internal-Colored")); previewMaterial.SetPass(0); previewMaterial.SetColor("_Color", previewColor); if (Event.current.type == EventType.Repaint) { GL.PushMatrix(); GL.MultMatrix(Matrix4x4.identity); // Enable blending for transparency GL.Begin(GL.TRIANGLES); GL.Color(previewColor); foreach (var mesh in previewMeshes) { if (mesh != null) { // Draw each triangle in the mesh for (int i = 0; i < mesh.triangles.Length; i += 3) { Vector3 v0 = mesh.vertices[mesh.triangles[i]]; Vector3 v1 = mesh.vertices[mesh.triangles[i + 1]]; Vector3 v2 = mesh.vertices[mesh.triangles[i + 2]]; GL.Vertex(v0); GL.Vertex(v1); GL.Vertex(v2); } } } GL.End(); GL.PopMatrix(); // Also draw the outline GL.PushMatrix(); GL.MultMatrix(Matrix4x4.identity); GL.Begin(GL.LINES); // Set a more visible outline color Color outlineColor = new Color(previewColor.r, previewColor.g, previewColor.b, 1f); GL.Color(outlineColor); foreach (var mesh in previewMeshes) { if (mesh != null) { // Create a dictionary to track which edges we've drawn HashSet drawnEdges = new HashSet(); // Draw edges of each triangle for (int i = 0; i < mesh.triangles.Length; i += 3) { DrawEdgeIfNotDrawn(mesh.vertices[mesh.triangles[i]], mesh.vertices[mesh.triangles[i + 1]], drawnEdges); DrawEdgeIfNotDrawn(mesh.vertices[mesh.triangles[i + 1]], mesh.vertices[mesh.triangles[i + 2]], drawnEdges); DrawEdgeIfNotDrawn(mesh.vertices[mesh.triangles[i + 2]], mesh.vertices[mesh.triangles[i]], drawnEdges); } } } GL.End(); GL.PopMatrix(); } } private void DrawEdgeIfNotDrawn(Vector3 v1, Vector3 v2, HashSet drawnEdges) { // Create a unique key for this edge (order vertices to ensure uniqueness) string edgeKey; if (v1.x < v2.x || (v1.x == v2.x && v1.y < v2.y)) edgeKey = $"{v1.x},{v1.y},{v1.z}_{v2.x},{v2.y},{v2.z}"; else edgeKey = $"{v2.x},{v2.y},{v2.z}_{v1.x},{v1.y},{v1.z}"; // Only draw if we haven't drawn this edge yet if (!drawnEdges.Contains(edgeKey)) { GL.Vertex(v1); GL.Vertex(v2); drawnEdges.Add(edgeKey); } } } // Helper class for triangulating polygons public class Triangulator { private List m_points; public Triangulator(Vector2[] points) { m_points = new List(points); } public int[] Triangulate() { List indices = new List(); int n = m_points.Count; if (n < 3) return indices.ToArray(); int[] V = new int[n]; if (Area() > 0) { for (int v = 0; v < n; v++) V[v] = v; } else { for (int v = 0; v < n; v++) V[v] = (n - 1) - v; } int nv = n; int count = 2 * nv; for (int v = nv - 1; nv > 2; ) { if ((count--) <= 0) return indices.ToArray(); int u = v; if (nv <= u) u = 0; v = u + 1; if (nv <= v) v = 0; int w = v + 1; if (nv <= w) w = 0; if (Snip(u, v, w, nv, V)) { int a, b, c, s, t; a = V[u]; b = V[v]; c = V[w]; indices.Add(a); indices.Add(b); indices.Add(c); for (s = v, t = v + 1; t < nv; s++, t++) V[s] = V[t]; nv--; count = 2 * nv; } } indices.Reverse(); return indices.ToArray(); } private float Area() { int n = m_points.Count; float A = 0.0f; for (int p = n - 1, q = 0; q < n; p = q++) { Vector2 pval = m_points[p]; Vector2 qval = m_points[q]; A += pval.x * qval.y - qval.x * pval.y; } return (A * 0.5f); } private bool Snip(int u, int v, int w, int n, int[] V) { int p; Vector2 A = m_points[V[u]]; Vector2 B = m_points[V[v]]; Vector2 C = m_points[V[w]]; if (Mathf.Epsilon > (((B.x - A.x) * (C.y - A.y)) - ((B.y - A.y) * (C.x - A.x)))) return false; for (p = 0; p < n; p++) { if ((p == u) || (p == v) || (p == w)) continue; Vector2 P = m_points[V[p]]; if (InsideTriangle(A, B, C, P)) return false; } return true; } private bool InsideTriangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P) { float ax, ay, bx, by, cx, cy, apx, apy, bpx, bpy, cpx, cpy; float cCROSSap, bCROSScp, aCROSSbp; ax = C.x - B.x; ay = C.y - B.y; bx = A.x - C.x; by = A.y - C.y; cx = B.x - A.x; cy = B.y - A.y; apx = P.x - A.x; apy = P.y - A.y; bpx = P.x - B.x; bpy = P.y - B.y; cpx = P.x - C.x; cpy = P.y - C.y; aCROSSbp = ax * bpy - ay * bpx; cCROSSap = cx * apy - cy * apx; bCROSScp = bx * cpy - by * cpx; return ((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f)); } } }