using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using UnityEngine; using MCPForUnity.Editor.Helpers; namespace MCPForUnity.Editor.Tools.Physics { internal static class PhysicsValidationOps { private const string Cat_NonConvexMesh = "non_convex_mesh"; private const string Cat_MissingRigidbody = "missing_rigidbody"; private const string Cat_NonUniformScale = "non_uniform_scale"; private const string Cat_FastObjectDiscrete = "fast_object_discrete"; private const string Cat_MissingPhysicsMaterial = "missing_physics_material"; private const string Cat_CollisionMatrix = "collision_matrix"; private const string Cat_Mixed2D3D = "mixed_2d_3d"; public static object Validate(JObject @params) { var p = new ToolParams(@params); string dimension = (p.Get("dimension") ?? "both").ToLowerInvariant(); string targetStr = p.Get("target"); string searchMethod = p.Get("search_method"); int pageSize = p.GetInt("page_size") ?? p.GetInt("pageSize") ?? 50; int cursor = p.GetInt("cursor") ?? 0; var warnings = new List(); var categoryCounts = new Dictionary { { Cat_NonConvexMesh, 0 }, { Cat_MissingRigidbody, 0 }, { Cat_NonUniformScale, 0 }, { Cat_FastObjectDiscrete, 0 }, { Cat_MissingPhysicsMaterial, 0 }, { Cat_CollisionMatrix, 0 }, { Cat_Mixed2D3D, 0 }, }; int scanned = 0; if (!string.IsNullOrEmpty(targetStr)) { GameObject go = GameObjectLookup.FindByTarget( @params["target"], searchMethod ?? "by_name", true); if (go == null) return new ErrorResponse($"Target GameObject '{targetStr}' not found."); ValidateGameObject(go, dimension, warnings, categoryCounts); scanned = 1; } else { var rootObjects = GetAllRootGameObjects(); foreach (var root in rootObjects) { ValidateRecursive(root, dimension, warnings, categoryCounts, ref scanned); } if (dimension != "2d") CheckCollisionMatrix(warnings, categoryCounts); } int totalWarnings = warnings.Count; int clampedCursor = Math.Min(cursor, totalWarnings); var page = warnings.Skip(clampedCursor).Take(pageSize).ToList(); int? nextCursor = (clampedCursor + pageSize < totalWarnings) ? (int?)(clampedCursor + pageSize) : null; return new { success = true, message = totalWarnings == 0 ? $"No physics warnings found ({scanned} object(s) scanned)." : $"Found {totalWarnings} warning(s) across {scanned} object(s).", data = new { warnings = page, warning_count = totalWarnings, objects_scanned = scanned, page_size = pageSize, cursor = clampedCursor, next_cursor = nextCursor, summary = categoryCounts } }; } private static void ValidateRecursive(GameObject go, string dimension, List warnings, Dictionary categoryCounts, ref int scanned) { ValidateGameObject(go, dimension, warnings, categoryCounts); scanned++; for (int i = 0; i < go.transform.childCount; i++) { ValidateRecursive(go.transform.GetChild(i).gameObject, dimension, warnings, categoryCounts, ref scanned); } } private static void ValidateGameObject(GameObject go, string dimension, List warnings, Dictionary categoryCounts) { bool check3D = dimension == "3d" || dimension == "both"; bool check2D = dimension == "2d" || dimension == "both"; // Check 1: MeshCollider without Convex on non-kinematic Rigidbody if (check3D) { var rb = go.GetComponent(); if (rb != null && !rb.isKinematic) { foreach (var mc in go.GetComponents()) { if (!mc.convex) { warnings.Add( $"MeshCollider on '{go.name}' must be Convex for non-kinematic Rigidbody."); categoryCounts[Cat_NonConvexMesh]++; } } } } // Check 2: Collider without Rigidbody on non-static object if (check3D) { var colliders3D = go.GetComponents(); if (colliders3D.Length > 0 && go.GetComponent() == null && !go.isStatic) { bool hasAnimator = go.GetComponent() != null || HasAnimatorInParent(go); if (hasAnimator) { warnings.Add( $"'{go.name}' has a Collider but no Rigidbody. Moving it via Transform causes broadphase rebuild every frame."); } else { warnings.Add( $"[Info] '{go.name}' has a Collider but no Rigidbody. This is fine if the object isn't moved at runtime."); } categoryCounts[Cat_MissingRigidbody]++; } } if (check2D) { var colliders2D = go.GetComponents(); if (colliders2D.Length > 0 && go.GetComponent() == null && !go.isStatic) { bool hasAnimator = go.GetComponent() != null || HasAnimatorInParent(go); if (hasAnimator) { warnings.Add( $"'{go.name}' has a Collider2D but no Rigidbody2D. Moving it via Transform causes broadphase rebuild every frame."); } else { warnings.Add( $"[Info] '{go.name}' has a Collider2D but no Rigidbody2D. This is fine if the object isn't moved at runtime."); } categoryCounts[Cat_MissingRigidbody]++; } } // Check 3: Non-uniform scale { var scale = go.transform.lossyScale; bool hasCollider = go.GetComponent() != null || go.GetComponent() != null; if (hasCollider) { bool nonUniform = Mathf.Abs(scale.x - scale.y) > 0.01f || Mathf.Abs(scale.y - scale.z) > 0.01f; if (nonUniform) { warnings.Add( $"'{go.name}' has non-uniform scale ({scale.x:F2}, {scale.y:F2}, {scale.z:F2}) which degrades physics performance."); categoryCounts[Cat_NonUniformScale]++; } } } // Check 4: Fast object with Discrete collision detection if (check3D) { var rb = go.GetComponent(); if (rb != null && rb.collisionDetectionMode == CollisionDetectionMode.Discrete) { string nameLower = go.name.ToLowerInvariant(); if (nameLower.Contains("bullet") || nameLower.Contains("projectile") || nameLower.Contains("fast")) { warnings.Add( $"'{go.name}' uses Discrete collision detection but appears to be a fast-moving object. Consider ContinuousDynamic."); categoryCounts[Cat_FastObjectDiscrete]++; } } } // Check 5: Missing physics material if (check3D) { foreach (var col in go.GetComponents()) { if (col.sharedMaterial == null) { warnings.Add( $"[Info] Collider ({col.GetType().Name}) on '{go.name}' has no physics material (using defaults)."); categoryCounts[Cat_MissingPhysicsMaterial]++; } } } if (check2D) { foreach (var col in go.GetComponents()) { if (col.sharedMaterial == null) { warnings.Add( $"[Info] Collider2D ({col.GetType().Name}) on '{go.name}' has no physics material (using defaults)."); categoryCounts[Cat_MissingPhysicsMaterial]++; } } } // Check 7: 2D/3D physics mixing if (dimension == "both") { bool has3D = go.GetComponent() != null || go.GetComponent() != null; bool has2D = go.GetComponent() != null || go.GetComponent() != null; if (has3D && has2D) { var components3D = new List(); var components2D = new List(); if (go.GetComponent() != null) components3D.Add("Rigidbody"); foreach (var c in go.GetComponents()) components3D.Add(c.GetType().Name); if (go.GetComponent() != null) components2D.Add("Rigidbody2D"); foreach (var c in go.GetComponents()) components2D.Add(c.GetType().Name); warnings.Add( $"'{go.name}' has both 3D ({string.Join(", ", components3D)}) and 2D ({string.Join(", ", components2D)}) physics components."); categoryCounts[Cat_Mixed2D3D]++; } } } private static bool HasAnimatorInParent(GameObject go) { Transform parent = go.transform.parent; while (parent != null) { if (parent.GetComponent() != null) return true; parent = parent.parent; } return false; } // Check 6 (scene-wide): Unconfigured collision matrix private static void CheckCollisionMatrix(List warnings, Dictionary categoryCounts) { var populatedLayers = new List(); for (int i = 0; i < 32; i++) { if (!string.IsNullOrEmpty(LayerMask.LayerToName(i))) populatedLayers.Add(i); } bool allCollide = true; foreach (int i in populatedLayers) { foreach (int j in populatedLayers) { if (j > i) continue; if (UnityEngine.Physics.GetIgnoreLayerCollision(i, j)) { allCollide = false; break; } } if (!allCollide) break; } if (allCollide && populatedLayers.Count > 2) { warnings.Add( "All layers are set to collide with all other layers. Consider disabling unused layer pairs for performance."); categoryCounts[Cat_CollisionMatrix]++; } } private static List GetAllRootGameObjects() { var roots = new List(); for (int i = 0; i < UnityEngine.SceneManagement.SceneManager.sceneCount; i++) { var scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i); if (scene.isLoaded) roots.AddRange(scene.GetRootGameObjects()); } return roots; } } }