/* * 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 } }