using System.Collections.Generic; using System.Text; using UnityEditor; using UnityEngine; namespace jeanf.scenemanagement.Editor { /// /// Catches the setup mistakes that make baked prop collision look broken rather than misconfigured: /// a missing (correct prefabs, no colliders, no clue why), /// unsupported collider types, and layers that cannot collide with the player. /// [InitializeOnLoad] public static class StaticColliderValidation { private const string LogPrefix = "[SceneManagement]"; static StaticColliderValidation() { EditorApplication.playModeStateChanged += state => { if (state == PlayModeStateChange.EnteredPlayMode) WarnIfBridgeMissing(); }; } // Closed SubScenes hide their contents at edit time, so "SubScenes are in use" is the signal // that baked props may exist — we cannot enumerate them to be sure. private static void WarnIfBridgeMissing() { var usesSubScenes = Object.FindObjectsByType( FindObjectsInactive.Include).Length > 0; if (!usesSubScenes) return; if (Object.FindAnyObjectByType(FindObjectsInactive.Include) != null) return; Debug.LogWarning($"{LogPrefix} SubScenes are in use but there is no StaticColliderBridge in the loaded " + "scenes — every prop baked with a StaticColliderAuthoring is NON-SOLID: the player walks through it. " + "Add a StaticColliderBridge component to an always-loaded GameObject (e.g. a manager in your main scene)."); } [MenuItem("Tools/Jeanf/SceneManagement/Validate Static Colliders")] public static void ValidatePrefabs() { var guids = AssetDatabase.FindAssets("t:Prefab"); var noSupported = new List(); var unsupported = new List(); var badLayer = new List(); var checkedCount = 0; foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var prefab = AssetDatabase.LoadAssetAtPath(path); if (prefab == null) continue; var authorings = prefab.GetComponentsInChildren(true); if (authorings == null || authorings.Length == 0) continue; foreach (var authoring in authorings) { checkedCount++; var colliders = authoring.IncludeChildren ? authoring.GetComponentsInChildren(true) : authoring.GetComponents(); var supported = 0; foreach (var collider in colliders) { if (collider == null) continue; if (collider.isTrigger && !authoring.IncludeTriggers) continue; if (StaticColliderBake.TryDescribe(collider, out _)) { supported++; if (!CollidesWithAnything(collider.gameObject.layer)) badLayer.Add($"{path} → '{collider.name}' on layer '{LayerMask.LayerToName(collider.gameObject.layer)}'"); } else { unsupported.Add($"{path} → '{collider.name}' ({collider.GetType().Name})"); } } if (supported == 0) noSupported.Add(path); } } if (checkedCount == 0) { Debug.Log($"{LogPrefix} Validate Static Colliders: no prefab uses StaticColliderAuthoring yet. " + "Add it to a prop prefab so its colliders survive being baked into a SubScene."); return; } var report = new StringBuilder(); if (noSupported.Count > 0) report.AppendLine($"No bakeable collider (nothing will block the player): {string.Join(", ", noSupported)}"); if (unsupported.Count > 0) report.AppendLine($"Unsupported collider type — only Box, Sphere and Capsule are baked: {string.Join(", ", unsupported)}"); if (badLayer.Count > 0) report.AppendLine($"Layer collides with NOTHING in the Physics Layer Collision Matrix, so the proxy cannot block anything: {string.Join(", ", badLayer)}"); if (report.Length == 0) Debug.Log($"{LogPrefix} Validate Static Colliders: {checkedCount} StaticColliderAuthoring component(s) checked, all good."); else Debug.LogWarning($"{LogPrefix} Validate Static Colliders ({checkedCount} checked):\n{report}"); } private static bool CollidesWithAnything(int layer) { for (var i = 0; i < 32; i++) if (!Physics.GetIgnoreLayerCollision(layer, i)) return true; return false; } } }