Files
coplaydev--unity-mcp/MCPForUnity/Editor/Tools/Physics/PhysicsValidationOps.cs
T
2026-07-13 12:49:17 +08:00

309 lines
12 KiB
C#

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<string>();
var categoryCounts = new Dictionary<string, int>
{
{ 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<string> warnings,
Dictionary<string, int> 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<string> warnings,
Dictionary<string, int> 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<Rigidbody>();
if (rb != null && !rb.isKinematic)
{
foreach (var mc in go.GetComponents<MeshCollider>())
{
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<Collider>();
if (colliders3D.Length > 0 && go.GetComponent<Rigidbody>() == null && !go.isStatic)
{
bool hasAnimator = go.GetComponent<Animator>() != 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<Collider2D>();
if (colliders2D.Length > 0 && go.GetComponent<Rigidbody2D>() == null && !go.isStatic)
{
bool hasAnimator = go.GetComponent<Animator>() != 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<Collider>() != null || go.GetComponent<Collider2D>() != 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<Rigidbody>();
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<Collider>())
{
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<Collider2D>())
{
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<Rigidbody>() != null || go.GetComponent<Collider>() != null;
bool has2D = go.GetComponent<Rigidbody2D>() != null || go.GetComponent<Collider2D>() != null;
if (has3D && has2D)
{
var components3D = new List<string>();
var components2D = new List<string>();
if (go.GetComponent<Rigidbody>() != null) components3D.Add("Rigidbody");
foreach (var c in go.GetComponents<Collider>()) components3D.Add(c.GetType().Name);
if (go.GetComponent<Rigidbody2D>() != null) components2D.Add("Rigidbody2D");
foreach (var c in go.GetComponents<Collider2D>()) 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<Animator>() != null)
return true;
parent = parent.parent;
}
return false;
}
// Check 6 (scene-wide): Unconfigured collision matrix
private static void CheckCollisionMatrix(List<string> warnings, Dictionary<string, int> categoryCounts)
{
var populatedLayers = new List<int>();
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<GameObject> GetAllRootGameObjects()
{
var roots = new List<GameObject>();
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;
}
}
}