/*
* OptimizedColliderTool.cs
*
* A production-ready Unity Editor tool for intelligent collider generation.
* Features:
* - OBB (Oriented Bounding Box) fitting for accurate building structure following
* - Mesh decimation for optimized MeshColliders
* - Compound collider generation for complex buildings
* - Terrain/ground detection and configuration
* - Memory optimization and performance profiling
* - Gap detection and filling
*
* Created: 2026-01-15
*/
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace OptimizedColliders
{
///
/// Main editor window for the Optimized Collider Tool
///
public class OptimizedColliderTool : EditorWindow
{
#region Serialized Fields and Settings
// Object assignments
private GameObject environmentParent;
private GameObject groundTerrain;
private GameObject groundReference; // Reference ground for component copying
// Generation method
public enum ColliderMethod
{
MeshCollider,
BoxColliderAutoFit,
CompoundColliders,
ConvexMesh,
Adaptive // Auto-select best method per object
}
private ColliderMethod colliderMethod = ColliderMethod.Adaptive;
// Detail level
public enum DetailLevel
{
Low = 0, // 100-500 triangles, fastest
Medium = 1, // 500-2000 triangles, balanced
High = 2 // 2000-5000 triangles, accurate
}
private DetailLevel detailLevel = DetailLevel.Medium;
// Options
private bool generateForBuildings = true;
private bool generateForGround = true;
private bool generateForVegetation = false;
private bool generateForProps = true;
private bool optimizeForPerformance = true;
private bool createCompoundForComplex = false; // OFF by default - creates too many colliders
private bool removeExistingFirst = true; // ON by default for clean runs
private bool skipGroundPlanes = true; // ON by default - terrain collider is sufficient, planes under buildings create invisible walls
// Minimum size filter - objects smaller than this get NO collider
private float minimumObjectSize = 0.5f; // Objects smaller than 0.5m in all dimensions get skipped
// Advanced settings
private bool showAdvancedSettings = false;
private float colliderOverlap = 0.02f; // 2cm overlap to prevent gaps
private int maxTrianglesLow = 500;
private int maxTrianglesMedium = 2000;
private int maxTrianglesHigh = 5000;
private int complexityThreshold = 8; // Child renderer count threshold
private int vertexThreshold = 3000; // Vertex count threshold for complex buildings
private bool useOBBFitting = true; // Use OBB instead of AABB
private bool detectArchitecturalFeatures = true;
private bool fillGaps = true;
private float gapDetectionSpacing = 0.5f;
private int groundLayer = 8; // Default to layer 8 (from terrain analysis)
private bool markAsStatic = true;
private bool bakeMeshColliders = true;
// State
private Vector2 scrollPosition;
private bool isProcessing = false;
private float progress = 0f;
private string statusMessage = "";
private List analysisResults = new List();
private List logMessages = new List();
private ColliderStatistics statistics = new ColliderStatistics();
// Undo tracking
private List changeRecords = new List();
// Cached styles
private GUIStyle headerStyle;
private GUIStyle sectionStyle;
private GUIStyle logStyle;
private bool stylesInitialized = false;
#endregion
#region Data Classes
[System.Serializable]
public class AnalysisResult
{
public GameObject gameObject;
public string objectType; // Building, Ground, Prop, Vegetation, etc.
public string recommendedCollider;
public string reason;
public int vertexCount;
public int triangleCount;
public Vector3 bounds;
public float complexity; // 0-1 complexity score
public bool selected = true;
public Color displayColor = Color.green;
}
[System.Serializable]
public class ColliderStatistics
{
public int totalObjects;
public int objectsWithColliders;
public int objectsNeedingColliders;
public int buildingCount;
public int groundCount;
public int propCount;
public int vegetationCount;
public long estimatedMemoryBytes;
public long originalMeshMemoryBytes;
public int totalTriangles;
public int optimizedTriangles;
public void Reset()
{
totalObjects = 0;
objectsWithColliders = 0;
objectsNeedingColliders = 0;
buildingCount = 0;
groundCount = 0;
propCount = 0;
vegetationCount = 0;
estimatedMemoryBytes = 0;
originalMeshMemoryBytes = 0;
totalTriangles = 0;
optimizedTriangles = 0;
}
}
[System.Serializable]
public class ColliderChangeRecord
{
public GameObject target;
public Collider addedCollider;
public GameObject addedChild; // For compound colliders
public StaticEditorFlags previousFlags;
public bool flagsChanged;
}
///
/// Detected architectural feature (wall, floor, roof, etc.)
///
public struct ArchitecturalFeature
{
public Vector3 center;
public Vector3 size;
public Quaternion rotation;
public string featureType; // Wall, Floor, Roof, Column, etc.
public Vector3 normal;
}
#endregion
#region Menu and Window
[MenuItem("Tools/Optimized Collider Tool")]
public static void ShowWindow()
{
var window = GetWindow("Optimized Colliders");
window.minSize = new Vector2(500, 700);
}
private void OnEnable()
{
stylesInitialized = false;
}
private void InitializeStyles()
{
if (stylesInitialized) return;
headerStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 16,
alignment = TextAnchor.MiddleCenter,
normal = { textColor = new Color(0.9f, 0.9f, 0.9f) }
};
sectionStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 13,
normal = { textColor = new Color(0.8f, 0.9f, 1f) }
};
logStyle = new GUIStyle(EditorStyles.miniLabel)
{
wordWrap = true,
richText = true,
normal = { textColor = new Color(0.7f, 0.7f, 0.7f) }
};
stylesInitialized = true;
}
#endregion
#region Main GUI
private void OnGUI()
{
InitializeStyles();
// Header
DrawHeader();
// Play mode check
if (Application.isPlaying)
{
EditorGUILayout.HelpBox("Exit Play Mode to use this tool.", MessageType.Warning);
return;
}
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// Environment Setup Section
DrawEnvironmentSetup();
// Generation Settings Section
DrawGenerationSettings();
// Options Section
DrawOptions();
// Advanced Settings Section
DrawAdvancedSettings();
// Action Buttons
DrawActionButtons();
// Progress Bar
if (isProcessing)
{
DrawProgressBar();
}
// Analysis Results Section
if (analysisResults.Count > 0)
{
DrawAnalysisResults();
}
// Statistics Section
if (statistics.totalObjects > 0)
{
DrawStatistics();
}
// Log Section
DrawLogSection();
EditorGUILayout.EndScrollView();
}
private void DrawHeader()
{
var headerRect = EditorGUILayout.GetControlRect(false, 45);
EditorGUI.DrawRect(headerRect, new Color(0.15f, 0.25f, 0.35f, 0.9f));
GUI.Label(headerRect, "๐ง Optimized Collider Tool", headerStyle);
GUILayout.Space(10);
}
private void DrawEnvironmentSetup()
{
EditorGUILayout.BeginVertical("box");
GUILayout.Label("๐ Environment Setup", sectionStyle);
GUILayout.Space(5);
environmentParent = (GameObject)EditorGUILayout.ObjectField(
new GUIContent("Environment Parent", "Root GameObject containing all buildings and objects"),
environmentParent, typeof(GameObject), true);
groundTerrain = (GameObject)EditorGUILayout.ObjectField(
new GUIContent("Ground/Terrain", "Terrain or ground mesh GameObject"),
groundTerrain, typeof(GameObject), true);
groundReference = (GameObject)EditorGUILayout.ObjectField(
new GUIContent("Ground Reference (Optional)", "Working ground example to copy components from"),
groundReference, typeof(GameObject), true);
if (environmentParent == null)
{
EditorGUILayout.HelpBox("Assign the Environment Parent GameObject to begin analysis.", MessageType.Info);
}
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawGenerationSettings()
{
EditorGUILayout.BeginVertical("box");
GUILayout.Label("โ๏ธ Generation Settings", sectionStyle);
GUILayout.Space(5);
colliderMethod = (ColliderMethod)EditorGUILayout.EnumPopup(
new GUIContent("Collider Method", "How to generate colliders"),
colliderMethod);
detailLevel = (DetailLevel)EditorGUILayout.EnumPopup(
new GUIContent("Detail Level", "Affects memory vs accuracy trade-off"),
detailLevel);
// Show detail level info
string detailInfo = detailLevel switch
{
DetailLevel.Low => "Fast, ~100-500 triangles per collider",
DetailLevel.Medium => "Balanced, ~500-2000 triangles per collider",
DetailLevel.High => "Accurate, ~2000-5000 triangles per collider",
_ => ""
};
EditorGUILayout.LabelField("", detailInfo, EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawOptions()
{
EditorGUILayout.BeginVertical("box");
GUILayout.Label("โ๏ธ Options", sectionStyle);
GUILayout.Space(5);
EditorGUILayout.LabelField("Object Categories", EditorStyles.boldLabel);
generateForBuildings = EditorGUILayout.ToggleLeft(
new GUIContent("โ
Buildings", "Create colliders for building structures"),
generateForBuildings);
generateForGround = EditorGUILayout.ToggleLeft(
new GUIContent("โ
Ground/Terrain", "Configure terrain or ground colliders"),
generateForGround);
generateForProps = EditorGUILayout.ToggleLeft(
new GUIContent("โ
Props (fences, benches, etc.)", "Create colliders for props and small objects"),
generateForProps);
generateForVegetation = EditorGUILayout.ToggleLeft(
new GUIContent("๐ฟ Trees & Vegetation", "Create colliders for vegetation (usually not needed)"),
generateForVegetation);
GUILayout.Space(5);
EditorGUILayout.LabelField("Ground Handling", EditorStyles.boldLabel);
skipGroundPlanes = EditorGUILayout.ToggleLeft(
new GUIContent("โ Skip Ground Planes Under Buildings",
"Exclude flat planes below buildings from getting colliders. " +
"Terrain collider is sufficient โ extra ground planes create invisible walls that break camera."),
skipGroundPlanes);
GUILayout.Space(5);
EditorGUILayout.LabelField("Size Filter", EditorStyles.boldLabel);
minimumObjectSize = EditorGUILayout.Slider(
new GUIContent("Minimum Object Size (m)", "Objects smaller than this in ALL dimensions are skipped. Prevents colliders on tiny decorations."),
minimumObjectSize, 0.1f, 3f);
GUILayout.Space(5);
EditorGUILayout.LabelField("Generation Options", EditorStyles.boldLabel);
optimizeForPerformance = EditorGUILayout.ToggleLeft(
new GUIContent("Optimize for Performance", "Use simplified meshes and batch colliders"),
optimizeForPerformance);
createCompoundForComplex = EditorGUILayout.ToggleLeft(
new GUIContent("Compound Colliders for Complex Objects", "Create multiple BoxColliders for complex buildings"),
createCompoundForComplex);
removeExistingFirst = EditorGUILayout.ToggleLeft(
new GUIContent("Remove Existing Colliders First", "Clear all existing colliders before generation"),
removeExistingFirst);
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawAdvancedSettings()
{
showAdvancedSettings = EditorGUILayout.Foldout(showAdvancedSettings, "๐ฌ Advanced Settings", true);
if (showAdvancedSettings)
{
EditorGUILayout.BeginVertical("box");
// OBB and Structure Following
EditorGUILayout.LabelField("Structure Following", EditorStyles.boldLabel);
useOBBFitting = EditorGUILayout.ToggleLeft(
new GUIContent("Use OBB Fitting", "Use Oriented Bounding Boxes instead of AABB for better structure following"),
useOBBFitting);
detectArchitecturalFeatures = EditorGUILayout.ToggleLeft(
new GUIContent("Detect Architectural Features", "Identify walls, floors, roofs separately"),
detectArchitecturalFeatures);
GUILayout.Space(5);
// Gap Prevention
EditorGUILayout.LabelField("Gap Prevention", EditorStyles.boldLabel);
fillGaps = EditorGUILayout.ToggleLeft(
new GUIContent("Fill Collision Gaps", "Detect and fill gaps in collision coverage"),
fillGaps);
colliderOverlap = EditorGUILayout.Slider(
new GUIContent("Collider Overlap", "Overlap amount at edges to prevent gaps (meters)"),
colliderOverlap, 0f, 0.1f);
gapDetectionSpacing = EditorGUILayout.Slider(
new GUIContent("Gap Detection Spacing", "Ray spacing for gap detection (meters)"),
gapDetectionSpacing, 0.1f, 2f);
GUILayout.Space(5);
// Complexity Thresholds
EditorGUILayout.LabelField("Complexity Thresholds", EditorStyles.boldLabel);
complexityThreshold = EditorGUILayout.IntSlider(
new GUIContent("Child Renderer Threshold", "Objects with more renderers are treated as complex"),
complexityThreshold, 2, 50);
vertexThreshold = EditorGUILayout.IntSlider(
new GUIContent("Vertex Threshold", "Objects exceeding this are treated as complex"),
vertexThreshold, 500, 20000);
GUILayout.Space(5);
// Triangle Limits
EditorGUILayout.LabelField("Triangle Limits (per collider)", EditorStyles.boldLabel);
maxTrianglesLow = EditorGUILayout.IntField("Low Detail Max", maxTrianglesLow);
maxTrianglesMedium = EditorGUILayout.IntField("Medium Detail Max", maxTrianglesMedium);
maxTrianglesHigh = EditorGUILayout.IntField("High Detail Max", maxTrianglesHigh);
GUILayout.Space(5);
// Performance
EditorGUILayout.LabelField("Performance", EditorStyles.boldLabel);
markAsStatic = EditorGUILayout.ToggleLeft("Mark Environment as Static", markAsStatic);
bakeMeshColliders = EditorGUILayout.ToggleLeft("Pre-bake Mesh Colliders", bakeMeshColliders);
GUILayout.Space(5);
// Ground Layer
EditorGUILayout.LabelField("Ground Configuration", EditorStyles.boldLabel);
groundLayer = EditorGUILayout.LayerField("Ground Layer", groundLayer);
EditorGUILayout.EndVertical();
}
GUILayout.Space(8);
}
private void DrawActionButtons()
{
EditorGUILayout.BeginVertical("box");
GUI.enabled = !isProcessing && environmentParent != null;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("๐ Analyze Environment", GUILayout.Height(35)))
{
AnalyzeEnvironmentAsync();
}
if (GUILayout.Button("๐งน Clear Analysis", GUILayout.Height(35)))
{
ClearAnalysis();
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
GUI.enabled = !isProcessing && analysisResults.Count > 0;
var generateRect = EditorGUILayout.GetControlRect(false, 40);
EditorGUI.DrawRect(generateRect, new Color(0.2f, 0.6f, 0.3f, 0.8f));
if (GUI.Button(generateRect, "โ
Generate Colliders"))
{
GenerateCollidersAsync();
}
GUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
GUI.enabled = !isProcessing && changeRecords.Count > 0;
if (GUILayout.Button("โฉ๏ธ Undo All Changes", GUILayout.Height(30)))
{
UndoAllChanges();
}
GUI.enabled = !isProcessing;
if (GUILayout.Button("๐พ Save Scene", GUILayout.Height(30)))
{
SaveScene();
}
EditorGUILayout.EndHorizontal();
GUI.enabled = true;
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawProgressBar()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField(statusMessage, EditorStyles.boldLabel);
var progressRect = EditorGUILayout.GetControlRect(false, 20);
EditorGUI.DrawRect(progressRect, new Color(0.1f, 0.1f, 0.1f, 0.8f));
var fillRect = new Rect(progressRect.x + 2, progressRect.y + 2,
(progressRect.width - 4) * progress, progressRect.height - 4);
EditorGUI.DrawRect(fillRect, new Color(0.3f, 0.7f, 0.4f, 0.9f));
GUI.Label(progressRect, $"{(progress * 100):F0}%",
new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter });
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawAnalysisResults()
{
EditorGUILayout.BeginVertical("box");
GUILayout.Label($"๐ Analysis Results ({analysisResults.Count} objects)", sectionStyle);
GUILayout.Space(5);
// Quick select buttons
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select All", GUILayout.Width(100)))
{
foreach (var r in analysisResults) r.selected = true;
}
if (GUILayout.Button("Select None", GUILayout.Width(100)))
{
foreach (var r in analysisResults) r.selected = false;
}
if (GUILayout.Button("Buildings Only", GUILayout.Width(100)))
{
foreach (var r in analysisResults)
r.selected = r.objectType == "Building";
}
if (GUILayout.Button("Ground Only", GUILayout.Width(100)))
{
foreach (var r in analysisResults)
r.selected = r.objectType == "Ground";
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
// Show first 50 results with pagination would be added here
int displayCount = Mathf.Min(analysisResults.Count, 30);
for (int i = 0; i < displayCount; i++)
{
var result = analysisResults[i];
DrawAnalysisResultRow(result);
}
if (analysisResults.Count > displayCount)
{
EditorGUILayout.LabelField($"... and {analysisResults.Count - displayCount} more objects",
EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawAnalysisResultRow(AnalysisResult result)
{
EditorGUILayout.BeginHorizontal("box");
// Color indicator
var colorRect = EditorGUILayout.GetControlRect(GUILayout.Width(8), GUILayout.Height(18));
EditorGUI.DrawRect(colorRect, result.displayColor);
// Selection toggle
result.selected = EditorGUILayout.Toggle(result.selected, GUILayout.Width(20));
// Object name (clickable)
if (GUILayout.Button(result.gameObject.name, EditorStyles.linkLabel, GUILayout.Width(180)))
{
Selection.activeGameObject = result.gameObject;
EditorGUIUtility.PingObject(result.gameObject);
}
// Type
GUILayout.Label(result.objectType, GUILayout.Width(70));
// Recommended collider
GUILayout.Label(result.recommendedCollider, GUILayout.Width(100));
// Triangle count
GUILayout.Label($"{result.triangleCount} tris", EditorStyles.miniLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
}
private void DrawStatistics()
{
EditorGUILayout.BeginVertical("box");
GUILayout.Label("๐ Statistics", sectionStyle);
GUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"Total Objects: {statistics.totalObjects}");
EditorGUILayout.LabelField($"With Colliders: {statistics.objectsWithColliders}");
EditorGUILayout.LabelField($"Need Colliders: {statistics.objectsNeedingColliders}");
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"Buildings: {statistics.buildingCount}");
EditorGUILayout.LabelField($"Ground: {statistics.groundCount}");
EditorGUILayout.LabelField($"Props: {statistics.propCount}");
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField($"Original Mesh: {FormatBytes(statistics.originalMeshMemoryBytes)}");
EditorGUILayout.LabelField($"Collision Est: {FormatBytes(statistics.estimatedMemoryBytes)}");
EditorGUILayout.LabelField($"Triangles: {statistics.totalTriangles} โ {statistics.optimizedTriangles}");
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
// Memory warning
if (statistics.estimatedMemoryBytes > 50 * 1024 * 1024) // 50MB
{
EditorGUILayout.HelpBox(
$"Estimated collision memory ({FormatBytes(statistics.estimatedMemoryBytes)}) exceeds 50MB. " +
"Consider using lower detail level or more compound colliders.",
MessageType.Warning);
}
EditorGUILayout.EndVertical();
GUILayout.Space(8);
}
private void DrawLogSection()
{
if (logMessages.Count == 0) return;
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
GUILayout.Label("๐ Log", sectionStyle);
if (GUILayout.Button("Clear", GUILayout.Width(50)))
{
logMessages.Clear();
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
// Show last 10 messages
int start = Mathf.Max(0, logMessages.Count - 10);
for (int i = start; i < logMessages.Count; i++)
{
EditorGUILayout.LabelField(logMessages[i], logStyle);
}
EditorGUILayout.EndVertical();
}
#endregion
#region Analysis
private async void AnalyzeEnvironmentAsync()
{
if (environmentParent == null) return;
isProcessing = true;
progress = 0f;
statusMessage = "Analyzing environment...";
analysisResults.Clear();
statistics.Reset();
logMessages.Clear();
Log("Starting environment analysis...");
try
{
// ===== NEW APPROACH: Find TOP-LEVEL structural objects =====
// Instead of iterating every renderer (21000+), walk the hierarchy
// and find meaningful parent objects (buildings, ground, props).
// Each top-level object gets ONE collider covering all its children.
var processedObjects = new HashSet(); // Track what we've already handled
var topLevelObjects = FindTopLevelStructuralObjects(environmentParent, processedObjects);
float total = Mathf.Max(1, topLevelObjects.Count);
int processed = 0;
statistics.totalObjects = topLevelObjects.Count;
foreach (var obj in topLevelObjects)
{
if (obj == null) continue;
var result = AnalyzeTopLevelObject(obj);
if (result != null)
{
// === FILTER BY CATEGORY during analysis ===
// Only add results for categories the user has enabled
bool include = true;
if (result.objectType == "Building" && !generateForBuildings) include = false;
if (result.objectType == "Ground" && !generateForGround) include = false;
if (result.objectType == "Prop" && !generateForProps) include = false;
if (result.objectType == "Vegetation" && !generateForVegetation) include = false;
if (include)
{
analysisResults.Add(result);
}
}
processed++;
progress = processed / total;
statusMessage = $"Analyzing: {obj.name}";
if (processed % 50 == 0)
{
Repaint();
await Task.Yield();
}
}
// Analyze ground/terrain separately
if (groundTerrain != null && generateForGround)
{
var groundResult = AnalyzeGroundObject(groundTerrain);
if (groundResult != null)
{
analysisResults.Insert(0, groundResult);
}
}
// Calculate statistics
CalculateStatistics();
Log($"Analysis complete: {analysisResults.Count} objects analyzed (from {statistics.totalObjects} top-level objects)");
Log($"Buildings: {statistics.buildingCount}, Ground: {statistics.groundCount}, Props: {statistics.propCount}");
Log($"Skipped {statistics.vegetationCount} vegetation, filtered {statistics.totalObjects - analysisResults.Count} objects below min size");
statusMessage = "Analysis complete!";
progress = 1f;
}
catch (System.Exception e)
{
Log($"Error: {e.Message}");
statusMessage = "Analysis failed!";
}
finally
{
isProcessing = false;
Repaint();
}
}
///
/// Find top-level structural objects in the hierarchy.
/// A "top-level" object is one whose PARENT is the environment root (or 1-2 levels deep),
/// and it has renderers (itself or children). We treat each as ONE collider target.
/// This dramatically reduces collider count vs iterating every renderer.
///
private List FindTopLevelStructuralObjects(GameObject root, HashSet processed)
{
var results = new List();
// Walk direct children of environment parent
foreach (Transform child in root.transform)
{
if (child == null || !child.gameObject.activeInHierarchy) continue;
if (processed.Contains(child.gameObject)) continue;
// Check if this child has any renderers (itself or descendants)
var childRenderers = child.GetComponentsInChildren(true);
if (childRenderers.Length == 0) continue;
// Calculate combined bounds of all child renderers
Bounds combinedBounds = childRenderers[0].bounds;
for (int i = 1; i < childRenderers.Length; i++)
{
if (childRenderers[i] != null)
combinedBounds.Encapsulate(childRenderers[i].bounds);
}
Vector3 size = combinedBounds.size;
// If this is a large container with many sub-buildings,
// go one level deeper to find individual buildings
bool isLargeContainer = (size.x > 50f || size.z > 50f) && child.childCount > 5;
if (isLargeContainer)
{
// This is probably a "Buildings" or "Props" container folder.
// Go one level deeper to find individual buildings.
foreach (Transform grandchild in child)
{
if (grandchild == null || !grandchild.gameObject.activeInHierarchy) continue;
if (processed.Contains(grandchild.gameObject)) continue;
var gcRenderers = grandchild.GetComponentsInChildren(true);
if (gcRenderers.Length == 0) continue;
processed.Add(grandchild.gameObject);
results.Add(grandchild.gameObject);
}
}
else
{
processed.Add(child.gameObject);
results.Add(child.gameObject);
}
}
return results;
}
///
/// Analyze a top-level object (building, prop, or ground piece).
/// Uses combined bounds of ALL child renderers for a single collider.
///
private AnalysisResult AnalyzeTopLevelObject(GameObject obj)
{
if (obj == null) return null;
if (!obj.activeInHierarchy) return null;
// Get combined bounds from ALL child renderers
var renderers = obj.GetComponentsInChildren(true);
if (renderers.Length == 0) return null;
Bounds combinedBounds = renderers[0].bounds;
int totalVerts = 0;
int totalTris = 0;
foreach (var r in renderers)
{
if (r == null) continue;
combinedBounds.Encapsulate(r.bounds);
var mf = r.GetComponent();
if (mf != null && mf.sharedMesh != null)
{
totalVerts += mf.sharedMesh.vertexCount;
if (mf.sharedMesh.isReadable)
totalTris += mf.sharedMesh.triangles.Length / 3;
}
}
Vector3 size = combinedBounds.size;
string name = obj.name.ToLower();
Mesh mesh = GetMeshFromObject(obj); // Root mesh (may be null for parent containers)
// === SIZE FILTER: Skip objects too small to matter ===
// Per Unity docs: fewer colliders = better performance
if (size.x < minimumObjectSize && size.y < minimumObjectSize && size.z < minimumObjectSize)
{
return null; // Too small, skip entirely
}
var result = new AnalysisResult
{
gameObject = obj,
vertexCount = totalVerts,
triangleCount = totalTris,
bounds = size
};
// === CLASSIFY ===
ClassifyTopLevelObject(obj, name, size, renderers.Length, mesh, result);
// Set display color
result.displayColor = GetColorForColliderType(result.recommendedCollider);
return result;
}
private AnalysisResult AnalyzeGroundObject(GameObject obj)
{
if (obj == null) return null;
var terrain = obj.GetComponent();
var terrainCollider = obj.GetComponent();
var result = new AnalysisResult
{
gameObject = obj,
objectType = "Ground",
displayColor = new Color(0.6f, 0.4f, 0.2f, 0.8f)
};
if (terrain != null)
{
result.recommendedCollider = "TerrainCollider";
result.reason = terrainCollider != null ?
"Terrain already has TerrainCollider" :
"Unity Terrain - needs TerrainCollider";
result.bounds = terrain.terrainData != null ? terrain.terrainData.size : Vector3.zero;
statistics.groundCount++;
}
else
{
Mesh mesh = GetMeshFromObject(obj);
if (mesh != null)
{
result.recommendedCollider = "MeshCollider";
result.reason = "Mesh-based ground";
result.vertexCount = mesh.vertexCount;
result.triangleCount = mesh.isReadable ? mesh.triangles.Length / 3 : 0;
var renderer = obj.GetComponent();
result.bounds = renderer != null ? renderer.bounds.size : Vector3.zero;
statistics.groundCount++;
}
}
return result;
}
///
/// Classify a top-level structural object.
/// KEY PRINCIPLE (from Unity docs):
/// Sphere > Capsule > Box > Convex Mesh > Non-convex Mesh (performance order)
/// ONE collider per building is ideal. Fewer colliders = better mobile performance.
///
private void ClassifyTopLevelObject(GameObject obj, string name, Vector3 size, int rendererCount, Mesh rootMesh, AnalysisResult result)
{
// === VEGETATION: Skip entirely ===
if (IsVegetation(obj, name, size, rootMesh))
{
result.objectType = "Vegetation";
result.recommendedCollider = "Skip";
result.reason = "Vegetation - no collider needed";
result.displayColor = new Color(0.3f, 0.8f, 0.3f, 0.5f);
result.selected = false;
statistics.vegetationCount++;
return;
}
// === GROUND: Check BEFORE buildings ===
// But skip ground planes if they're under buildings or if skipGroundPlanes is enabled
if (IsGround(obj, name, size, rootMesh))
{
// If skipGroundPlanes is enabled, skip ALL ground/plane objects
// because the terrain collider already handles ground collision.
// These extra planes create invisible walls that break the camera.
if (skipGroundPlanes)
{
result.objectType = "Ground";
result.recommendedCollider = "Skip";
result.reason = "Ground plane skipped (terrain collider is sufficient)";
result.displayColor = new Color(0.6f, 0.4f, 0.2f, 0.3f);
result.selected = false;
statistics.groundCount++;
return;
}
result.objectType = "Ground";
result.recommendedCollider = "MeshCollider";
result.reason = "Ground surface - non-convex for accuracy";
statistics.groundCount++;
return;
}
// === BUILDING: ONE collider for the whole building ===
if (IsBuilding(obj, name, size))
{
result.objectType = "Building";
statistics.buildingCount++;
// Per Unity docs: prefer BoxCollider (efficient)
// ONE BoxCollider per building wrapping all children = simple and fast
result.recommendedCollider = "BoxCollider";
result.reason = $"Single BoxCollider for entire building ({rendererCount} parts)";
return;
}
// === PROP: Usually BoxCollider is fine ===
result.objectType = "Prop";
statistics.propCount++;
result.recommendedCollider = "BoxCollider";
result.reason = $"BoxCollider for prop ({rendererCount} parts)";
}
private void CalculateStatistics()
{
statistics.totalTriangles = 0;
statistics.optimizedTriangles = 0;
statistics.originalMeshMemoryBytes = 0;
statistics.estimatedMemoryBytes = 0;
int maxTris = GetMaxTrianglesForDetailLevel();
foreach (var result in analysisResults)
{
if (result.recommendedCollider == "Skip") continue;
statistics.totalTriangles += result.triangleCount;
// Estimate optimized triangle count
int optimized = result.recommendedCollider switch
{
"BoxCollider" => 12,
"ConvexMesh" => Mathf.Min(result.triangleCount, 255),
"MeshCollider" => Mathf.Min(result.triangleCount, maxTris),
_ => result.triangleCount
};
statistics.optimizedTriangles += optimized;
// Estimate memory (rough: ~12 bytes per vertex, ~4 bytes per triangle index)
statistics.originalMeshMemoryBytes += (result.vertexCount * 12) + (result.triangleCount * 12);
// Collision mesh memory estimate
int collisionVerts = result.recommendedCollider switch
{
"BoxCollider" => 8,
"ConvexMesh" => Mathf.Min(result.vertexCount, 255),
"MeshCollider" => Mathf.Min(result.vertexCount, maxTris * 3),
_ => Mathf.Min(result.vertexCount, maxTris * 3)
};
statistics.estimatedMemoryBytes += collisionVerts * 12 + optimized * 12;
}
}
private int GetMaxTrianglesForDetailLevel()
{
return detailLevel switch
{
DetailLevel.Low => maxTrianglesLow,
DetailLevel.Medium => maxTrianglesMedium,
DetailLevel.High => maxTrianglesHigh,
_ => maxTrianglesMedium
};
}
#endregion
#region Classification Helpers
private bool IsVegetation(GameObject obj, string name, Vector3 size, Mesh mesh)
{
// Name-based detection
string[] vegetationKeywords = { "tree", "grass", "plant", "bush", "shrub", "flower",
"leaf", "foliage", "fern", "vine", "hedge", "weed" };
foreach (var keyword in vegetationKeywords)
{
if (name.Contains(keyword)) return true;
if (NameOrAncestorContains(obj, keyword)) return true;
}
// Small ground cover
if (size.y < 0.6f && size.x < 0.8f && size.z < 0.8f)
{
if (mesh != null && mesh.vertexCount < 200) return true;
}
return false;
}
private bool IsBuilding(GameObject obj, string name, Vector3 size)
{
// Name-based
string[] buildingKeywords = { "building", "house", "wall", "structure", "apartment",
"shop", "store", "tower", "bridge", "fence", "gate" };
foreach (var keyword in buildingKeywords)
{
if (name.Contains(keyword)) return true;
}
// Size-based: Large structures
if (size.x > 5f || size.y > 3f || size.z > 5f)
{
return true;
}
// Wall-like: long and tall
if ((size.x > size.z * 3f || size.z > size.x * 3f) && size.y > 1.5f)
{
return true;
}
return false;
}
private bool IsGround(GameObject obj, string name, Vector3 size, Mesh mesh)
{
// Name-based
string[] groundKeywords = { "ground", "floor", "terrain", "surface", "plane", "platform" };
foreach (var keyword in groundKeywords)
{
if (name.Contains(keyword)) return true;
}
// Very flat and wide โ CLASSIC ground plane detection
// This catches planes placed under buildings that create invisible walls
if (size.y < Mathf.Min(size.x, size.z) * 0.1f && (size.x > 3f || size.z > 3f))
{
return true;
}
// Check mesh flatness (nearly zero height = plane)
if (mesh != null && mesh.bounds.size.y < 0.01f)
{
return true;
}
// Detect Unity default Plane mesh (10x10 quad with y=0)
if (mesh != null && mesh.vertexCount <= 121 && size.y < 0.05f)
{
return true;
}
return false;
}
private bool NameOrAncestorContains(GameObject obj, string keyword)
{
Transform t = obj.transform;
while (t != null)
{
if (t.name.ToLower().Contains(keyword))
return true;
t = t.parent;
}
return false;
}
private Mesh GetMeshFromObject(GameObject obj)
{
var mf = obj.GetComponent();
if (mf != null && mf.sharedMesh != null) return mf.sharedMesh;
var smr = obj.GetComponent();
if (smr != null && smr.sharedMesh != null) return smr.sharedMesh;
return null;
}
private Color GetColorForColliderType(string type)
{
return type switch
{
"BoxCollider" => new Color(0.2f, 0.8f, 0.2f, 0.8f),
"ConvexMesh" => new Color(0.2f, 0.6f, 0.9f, 0.8f),
"MeshCollider" => new Color(0.9f, 0.5f, 0.2f, 0.8f),
"TerrainCollider" => new Color(0.6f, 0.4f, 0.2f, 0.8f),
"Skip" => new Color(0.5f, 0.5f, 0.5f, 0.4f),
_ => Color.white
};
}
#endregion
#region Utility
private void ClearAnalysis()
{
analysisResults.Clear();
statistics.Reset();
logMessages.Clear();
progress = 0f;
statusMessage = "";
Repaint();
}
private void Log(string message)
{
logMessages.Add($"[{System.DateTime.Now:HH:mm:ss}] {message}");
Debug.Log($"[OptimizedColliderTool] {message}");
}
private string FormatBytes(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024f:F1} KB";
return $"{bytes / (1024f * 1024f):F1} MB";
}
///
/// Bake all MeshColliders under a parent for runtime performance
///
private void BakeMeshCollidersInline(GameObject parent)
{
var meshColliders = parent.GetComponentsInChildren(true);
foreach (var mc in meshColliders)
{
if (mc != null && mc.sharedMesh != null)
{
Physics.BakeMesh(mc.sharedMesh.GetInstanceID(), mc.convex);
}
}
}
///
/// Add a non-convex MeshCollider for ground surfaces
///
private Collider AddGroundMeshColliderInline(GameObject obj, Mesh mesh)
{
if (obj == null) return null;
if (obj.GetComponent() != null) return null;
var mc = Undo.AddComponent(obj);
if (mesh != null) mc.sharedMesh = mesh;
mc.convex = false;
// Set cooking options for readable meshes
if (mesh != null && mesh.isReadable)
{
mc.cookingOptions = MeshColliderCookingOptions.CookForFasterSimulation |
MeshColliderCookingOptions.EnableMeshCleaning |
MeshColliderCookingOptions.WeldColocatedVertices;
}
return mc;
}
///
/// Add a convex MeshCollider for props
///
private Collider AddConvexMeshColliderInline(GameObject obj, Mesh mesh)
{
if (obj == null) return null;
if (obj.GetComponent() != null) return null;
var mc = Undo.AddComponent(obj);
if (mesh != null) mc.sharedMesh = mesh;
mc.convex = true;
return mc;
}
private void SaveScene()
{
var scene = SceneManager.GetActiveScene();
if (scene.isDirty)
{
EditorSceneManager.SaveScene(scene);
Log($"Scene '{scene.name}' saved.");
}
}
private void UndoAllChanges()
{
if (changeRecords.Count == 0) return;
if (!EditorUtility.DisplayDialog("Undo Changes",
$"Revert {changeRecords.Count} collider changes?", "Undo", "Cancel"))
return;
foreach (var record in changeRecords)
{
if (record.target == null) continue;
if (record.addedCollider != null)
{
Undo.DestroyObjectImmediate(record.addedCollider);
}
if (record.addedChild != null)
{
Undo.DestroyObjectImmediate(record.addedChild);
}
if (record.flagsChanged)
{
GameObjectUtility.SetStaticEditorFlags(record.target, record.previousFlags);
}
}
changeRecords.Clear();
Log("All changes undone.");
Repaint();
}
#endregion
#region Collider Generation - Full Implementation
private async void GenerateCollidersAsync()
{
Log("Collider generation starting...");
isProcessing = true;
statusMessage = "Generating colliders...";
try
{
var selectedResults = analysisResults.Where(r => r.selected && r.recommendedCollider != "Skip"
&& (r.objectType != "Building" || generateForBuildings)
&& (r.objectType != "Ground" || generateForGround)
&& (r.objectType != "Prop" || generateForProps)
&& (r.objectType != "Vegetation" || generateForVegetation)
).ToList();
float total = Mathf.Max(1, selectedResults.Count);
int processed = 0;
if (removeExistingFirst)
{
await RemoveExistingCollidersAsync();
}
foreach (var result in selectedResults)
{
// Guard against destroyed objects (e.g. children of removed containers)
if (result.gameObject == null) { processed++; continue; }
await GenerateColliderForObject(result);
processed++;
progress = processed / total;
// Safe name access after potential destruction
string objName = result.gameObject != null ? result.gameObject.name : "(destroyed)";
statusMessage = $"Generating: {objName}";
if (processed % 20 == 0)
{
Repaint();
await Task.Yield();
}
}
// Fill gaps if enabled
if (fillGaps && environmentParent != null)
{
await DetectAndFillGapsAsync();
}
// Bake all mesh colliders for runtime performance
if (bakeMeshColliders && environmentParent != null)
{
BakeMeshCollidersInline(environmentParent);
Log("Baked all mesh colliders for runtime performance.");
}
Log($"Generated colliders for {processed} objects.");
statusMessage = "Generation complete!";
progress = 1f;
}
catch (System.Exception e)
{
Log($"Error: {e.Message}");
Debug.LogError($"Collider generation error: {e}");
statusMessage = "Generation failed!";
}
finally
{
isProcessing = false;
Repaint();
}
}
private async Task RemoveExistingCollidersAsync()
{
Log("Removing existing colliders...");
var colliders = environmentParent.GetComponentsInChildren(true);
int removed = 0;
foreach (var col in colliders)
{
if (col == null) continue; // Already destroyed
if (col is TerrainCollider) continue; // Keep terrain colliders
Undo.DestroyObjectImmediate(col);
removed++;
}
// Collect all "Colliders" containers FIRST, then destroy
// (destroying a parent invalidates child transforms still in the iteration array)
var containersToDestroy = new List();
foreach (Transform child in environmentParent.GetComponentsInChildren(true))
{
if (child == null) continue;
if (child.name == "Colliders" && child.parent != null)
{
containersToDestroy.Add(child.gameObject);
}
}
// Also collect OBB child collider objects from previous runs
foreach (Transform child in environmentParent.GetComponentsInChildren(true))
{
if (child == null) continue;
if (child.name.StartsWith("BoxCollider_OBB") || child.name == "CompoundColliders")
{
if (!containersToDestroy.Contains(child.gameObject))
containersToDestroy.Add(child.gameObject);
}
}
foreach (var go in containersToDestroy)
{
if (go != null)
{
Undo.DestroyObjectImmediate(go);
removed++;
}
}
await Task.Yield();
Log($"Removed {removed} existing colliders and containers.");
}
private async Task GenerateColliderForObject(AnalysisResult result)
{
GameObject obj = result.gameObject;
if (obj == null) return;
// For compound approach: check if this object OR its direct children already have colliders
bool hasExistingColliders = obj.GetComponent() != null;
if (!hasExistingColliders)
{
foreach (Transform child in obj.transform)
{
if (child != null && child.GetComponent() != null)
{
hasExistingColliders = true;
break;
}
}
}
if (hasExistingColliders) return;
Undo.RecordObject(obj, "Add Collider");
EditorUtility.SetDirty(obj);
var prevFlags = GameObjectUtility.GetStaticEditorFlags(obj);
Collider addedCollider = null;
List addedColliders = null;
switch (result.recommendedCollider)
{
case "BoxCollider":
// === SINGLE BoxCollider per building (best practice for mobile) ===
// Per Unity docs: fewer colliders = better mobile performance
// ONE BoxCollider wrapping all children is simple, fast, and prevents
// the "3+ colliders per building" problem
if (createCompoundForComplex && result.objectType == "Building")
{
// Only use compound if user explicitly enabled it
addedColliders = AddCompoundChildColliders(obj);
addedCollider = addedColliders.Count > 0 ? addedColliders[0] : null;
}
else
{
// Default: ONE BoxCollider (simple + optimized)
addedCollider = AddLocalSpaceBoxCollider(obj);
}
break;
case "MeshCollider":
// Ground/terrain: use non-convex mesh for accurate surface
Mesh groundMesh = GetMeshFromObject(obj);
addedCollider = AddGroundMeshColliderInline(obj, groundMesh);
break;
case "TerrainCollider":
addedCollider = ConfigureTerrainCollider(obj);
break;
case "ConvexMesh":
Mesh convexMesh = GetMeshFromObject(obj);
addedCollider = AddConvexMeshColliderInline(obj, convexMesh);
if (addedCollider == null)
{
// Fallback to BoxCollider
addedCollider = AddLocalSpaceBoxCollider(obj);
}
break;
default:
// Default: safe BoxCollider
addedCollider = AddLocalSpaceBoxCollider(obj);
break;
}
// Mark as static if enabled (buildings and ground benefit from static batching)
if (markAsStatic && addedCollider != null)
{
TryMarkStatic(obj);
}
// Record change for undo
changeRecords.Add(new ColliderChangeRecord
{
target = obj,
addedCollider = addedCollider,
previousFlags = prevFlags,
flagsChanged = prevFlags != GameObjectUtility.GetStaticEditorFlags(obj)
});
await Task.Yield();
}
///
/// COMPOUND COLLIDER APPROACH (Best practice per Unity docs):
/// Place BoxColliders on the DIRECT CHILDREN of a building/prop.
/// Each child's BoxCollider uses LOCAL bounds โ naturally follows rotation.
///
/// This gives ~3-10 tight-fitting BoxColliders per building instead of
/// 1 loose axis-aligned box. Per Unity docs:
/// "Use compound colliders when a single primitive can't accurately represent the shape."
///
private List AddCompoundChildColliders(GameObject obj)
{
var addedColliders = new List();
if (obj == null) return addedColliders;
// --- Step 1: If this object itself has a mesh, add a BoxCollider from mesh.bounds ---
Mesh selfMesh = GetMeshFromObject(obj);
if (selfMesh != null && obj.GetComponent() == null)
{
BoxCollider selfBox = Undo.AddComponent(obj);
selfBox.center = selfMesh.bounds.center;
selfBox.size = selfMesh.bounds.size + Vector3.one * colliderOverlap;
addedColliders.Add(selfBox);
}
// --- Step 2: Collect direct children that have renderers ---
var childrenWithGeometry = new List();
foreach (Transform child in obj.transform)
{
if (child == null || !child.gameObject.activeInHierarchy) continue;
// Check if this child or its descendants have any renderers
var renderers = child.GetComponentsInChildren(true);
if (renderers.Length == 0) continue;
// Skip if child already has a collider
if (child.GetComponent() != null) continue;
// Size filter: skip tiny parts (decorations, bolts, etc.)
Bounds childBounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
if (renderers[i] != null)
childBounds.Encapsulate(renderers[i].bounds);
}
Vector3 size = childBounds.size;
if (size.x < minimumObjectSize && size.y < minimumObjectSize && size.z < minimumObjectSize)
continue;
childrenWithGeometry.Add(child);
}
// --- Step 3: If too many children, merge into groups ---
// Per Unity docs: "Use as few primitive colliders as possible"
if (childrenWithGeometry.Count > 15)
{
// Too many parts โ fall back to a single proper local-space BoxCollider
BoxCollider singleBox = AddLocalSpaceBoxCollider(obj);
if (singleBox != null)
addedColliders.Add(singleBox);
return addedColliders;
}
// If no direct children have geometry (leaf object), add single collider
if (childrenWithGeometry.Count == 0 && selfMesh == null)
{
BoxCollider singleBox = AddLocalSpaceBoxCollider(obj);
if (singleBox != null)
addedColliders.Add(singleBox);
return addedColliders;
}
// --- Step 4: Add BoxCollider to each significant child ---
foreach (Transform child in childrenWithGeometry)
{
// Check if child has its own MeshFilter โ use mesh.bounds (PERFECT local fit)
Mesh childMesh = GetMeshFromObject(child.gameObject);
if (childMesh != null && child.GetComponentsInChildren(true).Length <= 1)
{
// Single-mesh child: BoxCollider from mesh.bounds (ideal!)
BoxCollider box = Undo.AddComponent(child.gameObject);
box.center = childMesh.bounds.center;
box.size = childMesh.bounds.size + Vector3.one * colliderOverlap;
addedColliders.Add(box);
}
else
{
// Container child with sub-renderers: compute local-space combined bounds
BoxCollider box = AddLocalSpaceBoxCollider(child.gameObject);
if (box != null)
addedColliders.Add(box);
}
}
return addedColliders;
}
///
/// Compute a BoxCollider using PROPER local-space bounds.
/// Unlike the old AABB approach, this converts world corners to local space,
/// so the box follows the object's rotation accurately.
///
private BoxCollider AddLocalSpaceBoxCollider(GameObject obj)
{
if (obj == null) return null;
if (obj.GetComponent() != null) return null;
// First: if object has its own mesh, use mesh.bounds directly (perfect!)
Mesh selfMesh = GetMeshFromObject(obj);
if (selfMesh != null)
{
BoxCollider box = Undo.AddComponent(obj);
box.center = selfMesh.bounds.center;
box.size = selfMesh.bounds.size + Vector3.one * colliderOverlap;
return box;
}
// Otherwise: combine child renderer bounds in LOCAL space
var renderers = obj.GetComponentsInChildren(true);
if (renderers.Length == 0) return null;
Transform t = obj.transform;
Vector3 localMin = Vector3.one * float.MaxValue;
Vector3 localMax = Vector3.one * float.MinValue;
foreach (var r in renderers)
{
if (r == null) continue;
Bounds b = r.bounds; // World-space AABB
Vector3 center = b.center;
Vector3 extents = b.extents;
// Convert all 8 corners of world AABB to local space
// This is the KEY fix: local-space AABB follows object rotation
for (int i = 0; i < 8; i++)
{
Vector3 corner = center + new Vector3(
(i & 1) == 0 ? -extents.x : extents.x,
(i & 2) == 0 ? -extents.y : extents.y,
(i & 4) == 0 ? -extents.z : extents.z
);
Vector3 localCorner = t.InverseTransformPoint(corner);
localMin = Vector3.Min(localMin, localCorner);
localMax = Vector3.Max(localMax, localCorner);
}
}
Vector3 localCenter = (localMin + localMax) * 0.5f;
Vector3 localSize = localMax - localMin;
BoxCollider result = Undo.AddComponent(obj);
result.center = localCenter;
result.size = localSize + Vector3.one * colliderOverlap;
return result;
}
private async Task DetectAndFillGapsAsync()
{
Log("Detecting collision gaps...");
statusMessage = "Detecting gaps...";
// Simplified gap detection via raycasting down from a grid
if (environmentParent == null) return;
var allRenderers = environmentParent.GetComponentsInChildren(true);
if (allRenderers.Length == 0) return;
Bounds envBounds = allRenderers[0].bounds;
foreach (var r in allRenderers)
{
if (r != null) envBounds.Encapsulate(r.bounds);
}
int gapCount = 0;
float checkHeight = envBounds.max.y + 5f;
for (float x = envBounds.min.x; x <= envBounds.max.x; x += gapDetectionSpacing)
{
for (float z = envBounds.min.z; z <= envBounds.max.z; z += gapDetectionSpacing)
{
Ray ray = new Ray(new Vector3(x, checkHeight, z), Vector3.down);
if (!Physics.Raycast(ray, checkHeight + 10f))
{
gapCount++;
}
}
}
if (gapCount > 0)
{
Log($"Found {gapCount} potential gap points (no collision surface detected).");
}
else
{
Log("No gaps detected in collision coverage.");
}
await Task.Yield();
}
#endregion
#region Collider Generation Methods
///
/// Add an optimized BoxCollider with proper fitting
///
private BoxCollider AddOptimizedBoxCollider(GameObject obj, Mesh mesh, bool useOBB)
{
if (obj.GetComponent() != null) return null;
var box = Undo.AddComponent(obj);
if (mesh != null)
{
var bounds = mesh.bounds;
box.center = bounds.center;
box.size = bounds.size + Vector3.one * colliderOverlap;
}
else
{
// Fallback to renderer bounds
var renderer = obj.GetComponent();
if (renderer != null)
{
var localCenter = obj.transform.InverseTransformPoint(renderer.bounds.center);
var localSize = renderer.bounds.size;
box.center = localCenter;
box.size = localSize + Vector3.one * colliderOverlap;
}
}
return box;
}
///
/// Create advanced compound colliders using per-child or grouped approach
///
private GameObject CreateAdvancedCompoundColliders(GameObject obj, Mesh mesh)
{
// Use existing compound collider approach
var colliders = AddCompoundChildColliders(obj);
if (colliders.Count > 0)
{
Log($"Created {colliders.Count} compound colliders for {obj.name}");
}
return null;
}
///
/// Add an optimized MeshCollider with optional simplification
///
private MeshCollider AddOptimizedMeshCollider(GameObject obj, Mesh mesh, bool convex)
{
if (obj.GetComponent() != null) return null;
var mc = Undo.AddComponent(obj);
if (mesh != null)
{
mc.sharedMesh = mesh;
}
mc.convex = convex;
// CRITICAL: Only set explicit cooking options when mesh is readable.
// Unity docs: non-default CookingOptions require isReadable=true.
if (mesh != null && mesh.isReadable)
{
mc.cookingOptions = MeshColliderCookingOptions.CookForFasterSimulation |
MeshColliderCookingOptions.EnableMeshCleaning |
MeshColliderCookingOptions.WeldColocatedVertices;
if (optimizeForPerformance)
{
mc.cookingOptions |= MeshColliderCookingOptions.UseFastMidphase;
}
}
return mc;
}
///
/// Configure TerrainCollider with optimal settings
///
private Collider ConfigureTerrainCollider(GameObject obj)
{
// Inline terrain collider configuration
var terrain = obj.GetComponent();
var existingTC = obj.GetComponent();
if (terrain != null && existingTC == null)
{
var tc = Undo.AddComponent(obj);
tc.terrainData = terrain.terrainData;
}
else if (existingTC != null && terrain != null)
{
existingTC.terrainData = terrain.terrainData;
}
// Set layer
obj.layer = groundLayer;
// Enable tree colliders
if (terrain != null)
{
terrain.treeDistance = 500f;
}
return obj.GetComponent();
}
///
/// Mark object as static for batching and optimization
///
private void TryMarkStatic(GameObject obj)
{
if (obj == null) return;
// Skip dynamic objects
if (obj.GetComponentInParent() != null) return;
if (obj.GetComponentInParent() != null) return;
if (obj.GetComponentInParent() != null)
{
string name = obj.name.ToLower();
if (name.Contains("player") || name.Contains("enemy") || name.Contains("npc"))
return;
}
// Check tag
string tag = obj.tag;
if (tag == "Player" || tag == "Enemy" || tag == "Dynamic")
return;
var flags = StaticEditorFlags.BatchingStatic |
StaticEditorFlags.OccluderStatic |
StaticEditorFlags.OccludeeStatic |
StaticEditorFlags.ReflectionProbeStatic |
StaticEditorFlags.ContributeGI;
var current = GameObjectUtility.GetStaticEditorFlags(obj);
if ((current & flags) != flags)
{
GameObjectUtility.SetStaticEditorFlags(obj, current | flags);
}
}
#endregion
#region Scene View Visualization
private void OnSceneGUI(SceneView sceneView)
{
if (analysisResults.Count == 0) return;
foreach (var result in analysisResults)
{
if (!result.selected || result.gameObject == null) continue;
// Simple, clean visualization - show colliders on this object AND children (compound)
var collider = result.gameObject.GetComponent();
// Draw collider on the object itself
DrawBoxColliderWireframe(collider as BoxCollider, new Color(0.2f, 0.9f, 0.2f, 0.8f));
if (collider is MeshCollider)
{
Handles.color = new Color(0.9f, 0.5f, 0.2f, 0.6f);
Handles.DrawWireCube(collider.bounds.center, collider.bounds.size);
}
// Draw compound child colliders (BoxColliders placed on direct children)
foreach (Transform child in result.gameObject.transform)
{
if (child == null) continue;
var childBox = child.GetComponent();
DrawBoxColliderWireframe(childBox, new Color(0.3f, 0.85f, 0.4f, 0.7f));
}
// Pre-generation preview (no colliders yet)
if (collider == null && result.recommendedCollider != "Skip")
{
// Check if any children have colliders (compound preview)
bool hasChildColliders = false;
foreach (Transform child in result.gameObject.transform)
{
if (child != null && child.GetComponent() != null)
{
hasChildColliders = true;
break;
}
}
if (!hasChildColliders)
{
// Show preview of where colliders would go
var renderers = result.gameObject.GetComponentsInChildren(true);
if (renderers.Length > 0)
{
Handles.color = new Color(0.5f, 0.5f, 1f, 0.3f);
Bounds combined = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
if (renderers[i] != null)
combined.Encapsulate(renderers[i].bounds);
}
Handles.DrawWireCube(combined.center, combined.size);
}
}
}
// Skip drawing for vegetation/skipped objects
}
}
///
/// Draw a rotation-aware wireframe for a BoxCollider
///
private void DrawBoxColliderWireframe(BoxCollider boxCol, Color color)
{
if (boxCol == null) return;
Handles.color = color;
Transform t = boxCol.transform;
Vector3 worldCenter = t.TransformPoint(boxCol.center);
Matrix4x4 original = Handles.matrix;
Handles.matrix = Matrix4x4.TRS(worldCenter, t.rotation, Vector3.one);
Vector3 scaledSize = Vector3.Scale(boxCol.size, t.lossyScale);
Handles.DrawWireCube(Vector3.zero, scaledSize);
Handles.matrix = original;
}
private void OnFocus()
{
SceneView.duringSceneGui -= OnSceneGUI;
SceneView.duringSceneGui += OnSceneGUI;
}
private void OnDestroy()
{
SceneView.duringSceneGui -= OnSceneGUI;
}
#endregion
}
}