chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class CollisionMatrixOps
|
||||
{
|
||||
public static object GetCollisionMatrix(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
if (dimension != "3d" && dimension != "2d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
var layers = new List<object>();
|
||||
var populatedIndices = new List<int>();
|
||||
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
string name = LayerMask.LayerToName(i);
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
layers.Add(new { index = i, name });
|
||||
populatedIndices.Add(i);
|
||||
}
|
||||
|
||||
var matrix = new Dictionary<string, Dictionary<string, bool>>();
|
||||
|
||||
foreach (int i in populatedIndices)
|
||||
{
|
||||
string nameA = LayerMask.LayerToName(i);
|
||||
var row = new Dictionary<string, bool>();
|
||||
|
||||
foreach (int j in populatedIndices)
|
||||
{
|
||||
if (j > i) continue;
|
||||
string nameB = LayerMask.LayerToName(j);
|
||||
bool collides = dimension == "2d"
|
||||
? !Physics2D.GetIgnoreLayerCollision(i, j)
|
||||
: !UnityEngine.Physics.GetIgnoreLayerCollision(i, j);
|
||||
row[nameB] = collides;
|
||||
}
|
||||
|
||||
matrix[nameA] = row;
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Collision matrix retrieved ({dimension}).",
|
||||
data = new { layers, matrix }
|
||||
};
|
||||
}
|
||||
|
||||
public static object SetCollisionMatrix(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
if (dimension != "3d" && dimension != "2d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
var layerAToken = p.GetRaw("layer_a");
|
||||
var layerBToken = p.GetRaw("layer_b");
|
||||
|
||||
if (layerAToken == null)
|
||||
return new ErrorResponse("'layer_a' parameter is required.");
|
||||
if (layerBToken == null)
|
||||
return new ErrorResponse("'layer_b' parameter is required.");
|
||||
|
||||
int layerA = ResolveLayer(layerAToken);
|
||||
int layerB = ResolveLayer(layerBToken);
|
||||
|
||||
if (layerA < 0 || layerA >= 32)
|
||||
return new ErrorResponse($"Invalid layer_a: '{layerAToken}'. Layer not found or out of range.");
|
||||
if (layerB < 0 || layerB >= 32)
|
||||
return new ErrorResponse($"Invalid layer_b: '{layerBToken}'. Layer not found or out of range.");
|
||||
|
||||
bool collide = p.GetBool("collide", true);
|
||||
|
||||
if (dimension == "2d")
|
||||
{
|
||||
Physics2D.IgnoreLayerCollision(layerA, layerB, !collide);
|
||||
MarkSettingsDirty("ProjectSettings/Physics2DSettings.asset");
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Physics.IgnoreLayerCollision(layerA, layerB, !collide);
|
||||
MarkSettingsDirty("ProjectSettings/DynamicsManager.asset");
|
||||
}
|
||||
|
||||
string nameA = LayerMask.LayerToName(layerA);
|
||||
string nameB = LayerMask.LayerToName(layerB);
|
||||
if (string.IsNullOrEmpty(nameA)) nameA = layerA.ToString();
|
||||
if (string.IsNullOrEmpty(nameB)) nameB = layerB.ToString();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Collision between '{nameA}' and '{nameB}' set to {(collide ? "enabled" : "disabled")} ({dimension}).",
|
||||
data = new { layer_a = nameA, layer_b = nameB, collide, dimension }
|
||||
};
|
||||
}
|
||||
|
||||
private static void MarkSettingsDirty(string assetPath)
|
||||
{
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath(assetPath);
|
||||
if (assets != null && assets.Length > 0)
|
||||
EditorUtility.SetDirty(assets[0]);
|
||||
}
|
||||
|
||||
private static int ResolveLayer(JToken token)
|
||||
{
|
||||
if (token.Type == JTokenType.Integer)
|
||||
{
|
||||
int idx = token.Value<int>();
|
||||
return idx >= 0 && idx < 32 ? idx : -1;
|
||||
}
|
||||
string name = token.ToString();
|
||||
if (int.TryParse(name, out int parsed))
|
||||
return parsed >= 0 && parsed < 32 ? parsed : -1;
|
||||
return LayerMask.NameToLayer(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e370f7d6d864ffa847e821deaa6aee5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,527 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class JointOps
|
||||
{
|
||||
private static readonly Dictionary<string, Type> JointTypes3D = new Dictionary<string, Type>
|
||||
{
|
||||
{ "fixed", typeof(FixedJoint) },
|
||||
{ "hinge", typeof(HingeJoint) },
|
||||
{ "spring", typeof(SpringJoint) },
|
||||
{ "character", typeof(CharacterJoint) },
|
||||
{ "configurable", typeof(ConfigurableJoint) }
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, Type> JointTypes2D = new Dictionary<string, Type>
|
||||
{
|
||||
{ "distance", typeof(DistanceJoint2D) },
|
||||
{ "fixed", typeof(FixedJoint2D) },
|
||||
{ "friction", typeof(FrictionJoint2D) },
|
||||
{ "hinge", typeof(HingeJoint2D) },
|
||||
{ "relative", typeof(RelativeJoint2D) },
|
||||
{ "slider", typeof(SliderJoint2D) },
|
||||
{ "spring", typeof(SpringJoint2D) },
|
||||
{ "target", typeof(TargetJoint2D) },
|
||||
{ "wheel", typeof(WheelJoint2D) }
|
||||
};
|
||||
|
||||
public static object AddJoint(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetResult = p.GetRequired("target");
|
||||
var errorObj = targetResult.GetOrError(out string targetStr);
|
||||
if (errorObj != null) return errorObj;
|
||||
|
||||
var jointTypeResult = p.GetRequired("joint_type");
|
||||
errorObj = jointTypeResult.GetOrError(out string jointTypeStr);
|
||||
if (errorObj != null) return errorObj;
|
||||
|
||||
string searchMethod = p.Get("search_method");
|
||||
|
||||
GameObject go = FindTarget(@params["target"], searchMethod);
|
||||
if (go == null)
|
||||
return new ErrorResponse($"Target GameObject '{targetStr}' not found.");
|
||||
|
||||
string dimensionParam = p.Get("dimension")?.ToLowerInvariant();
|
||||
bool is3D = go.GetComponent<Rigidbody>() != null;
|
||||
bool has2DRb = go.GetComponent<Rigidbody2D>() != null;
|
||||
bool is2D;
|
||||
|
||||
if (dimensionParam == "2d")
|
||||
{
|
||||
if (!has2DRb)
|
||||
return new ErrorResponse($"Target '{go.name}' has no Rigidbody2D.");
|
||||
is2D = true;
|
||||
}
|
||||
else if (dimensionParam == "3d")
|
||||
{
|
||||
if (!is3D)
|
||||
return new ErrorResponse($"Target '{go.name}' has no Rigidbody.");
|
||||
is2D = false;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(dimensionParam))
|
||||
{
|
||||
return new ErrorResponse($"Invalid dimension: '{dimensionParam}'. Use '3d' or '2d'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Auto-detect; if both, prefer 3D (3D is the default physics)
|
||||
is2D = has2DRb && !is3D;
|
||||
}
|
||||
|
||||
if (!is2D && !is3D)
|
||||
return new ErrorResponse($"Target '{go.name}' has no Rigidbody or Rigidbody2D. Add one before adding a joint.");
|
||||
|
||||
string key = jointTypeStr.ToLowerInvariant();
|
||||
var typeMap = is2D ? JointTypes2D : JointTypes3D;
|
||||
|
||||
if (!typeMap.TryGetValue(key, out Type jointComponentType))
|
||||
{
|
||||
string validTypes = string.Join(", ", typeMap.Keys);
|
||||
string dimension = is2D ? "2D" : "3D";
|
||||
return new ErrorResponse($"Unknown {dimension} joint type: '{jointTypeStr}'. Valid types: {validTypes}.");
|
||||
}
|
||||
|
||||
// Validate connected body before mutating the scene
|
||||
string connectedBodyTarget = p.Get("connected_body");
|
||||
GameObject connectedGo = null;
|
||||
if (!string.IsNullOrEmpty(connectedBodyTarget))
|
||||
{
|
||||
connectedGo = FindTarget(new JValue(connectedBodyTarget), searchMethod);
|
||||
if (connectedGo == null)
|
||||
return new ErrorResponse($"Connected body GameObject '{connectedBodyTarget}' not found.");
|
||||
|
||||
if (is2D)
|
||||
{
|
||||
if (connectedGo.GetComponent<Rigidbody2D>() == null)
|
||||
return new ErrorResponse($"Connected body '{connectedGo.name}' has no Rigidbody2D.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (connectedGo.GetComponent<Rigidbody>() == null)
|
||||
return new ErrorResponse($"Connected body '{connectedGo.name}' has no Rigidbody.");
|
||||
}
|
||||
}
|
||||
|
||||
var joint = Undo.AddComponent(go, jointComponentType);
|
||||
if (joint == null)
|
||||
return new ErrorResponse($"Failed to add {jointComponentType.Name} to '{go.name}'.");
|
||||
|
||||
// Set connected body now that the joint is created
|
||||
if (connectedGo != null)
|
||||
{
|
||||
if (is2D)
|
||||
((Joint2D)joint).connectedBody = connectedGo.GetComponent<Rigidbody2D>();
|
||||
else
|
||||
((Joint)joint).connectedBody = connectedGo.GetComponent<Rigidbody>();
|
||||
}
|
||||
|
||||
// Set properties via reflection if provided
|
||||
var properties = p.GetRaw("properties") as JObject;
|
||||
if (properties != null)
|
||||
{
|
||||
SetPropertiesViaReflection(joint, properties);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(go);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"{jointComponentType.Name} added to '{go.name}'.",
|
||||
data = new
|
||||
{
|
||||
jointType = jointComponentType.Name,
|
||||
instanceID = joint.GetInstanceIDCompat(),
|
||||
gameObjectInstanceID = go.GetInstanceIDCompat()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object ConfigureJoint(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetResult = p.GetRequired("target");
|
||||
var errorObj = targetResult.GetOrError(out string targetStr);
|
||||
if (errorObj != null) return errorObj;
|
||||
|
||||
string searchMethod = p.Get("search_method");
|
||||
|
||||
GameObject go = FindTarget(@params["target"], searchMethod);
|
||||
if (go == null)
|
||||
return new ErrorResponse($"Target GameObject '{targetStr}' not found.");
|
||||
|
||||
string jointTypeStr = p.Get("joint_type");
|
||||
int? componentIndex = ParamCoercion.CoerceIntNullable(@params["componentIndex"] ?? @params["component_index"]);
|
||||
|
||||
if (componentIndex.HasValue && string.IsNullOrEmpty(jointTypeStr))
|
||||
return new ErrorResponse("component_index requires joint_type to be specified.");
|
||||
|
||||
Component joint = ResolveJoint(go, jointTypeStr, componentIndex, out int foundCount);
|
||||
if (joint == null)
|
||||
{
|
||||
if (componentIndex.HasValue && foundCount >= 0)
|
||||
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {foundCount} joint(s) on '{go.name}'.");
|
||||
|
||||
if (!string.IsNullOrEmpty(jointTypeStr))
|
||||
return new ErrorResponse($"No joint of type '{jointTypeStr}' found on '{go.name}'.");
|
||||
|
||||
// Check if multiple joints exist to give a better error
|
||||
var joints3D = go.GetComponents<Joint>();
|
||||
var joints2D = go.GetComponents<Joint2D>();
|
||||
int total = joints3D.Length + joints2D.Length;
|
||||
if (total > 1)
|
||||
return new ErrorResponse($"Multiple joints found on '{go.name}' ({total} total). Specify 'joint_type' to target a specific joint.");
|
||||
|
||||
return new ErrorResponse($"No joint found on '{go.name}'.");
|
||||
}
|
||||
|
||||
Undo.RecordObject(joint, $"Configure {joint.GetType().Name}");
|
||||
|
||||
var configured = new List<string>();
|
||||
|
||||
// Motor configuration (HingeJoint)
|
||||
var motorToken = p.GetRaw("motor") as JObject;
|
||||
if (motorToken != null)
|
||||
{
|
||||
if (joint is HingeJoint hingeForMotor)
|
||||
{
|
||||
var motor = hingeForMotor.motor;
|
||||
if (motorToken["targetVelocity"] != null)
|
||||
motor.targetVelocity = motorToken["targetVelocity"].Value<float>();
|
||||
if (motorToken["force"] != null)
|
||||
motor.force = motorToken["force"].Value<float>();
|
||||
if (motorToken["freeSpin"] != null)
|
||||
motor.freeSpin = motorToken["freeSpin"].Value<bool>();
|
||||
hingeForMotor.motor = motor;
|
||||
hingeForMotor.useMotor = true;
|
||||
configured.Add("motor");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Motor configuration is only supported on HingeJoint, not {joint.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Limits configuration (HingeJoint)
|
||||
var limitsToken = p.GetRaw("limits") as JObject;
|
||||
if (limitsToken != null)
|
||||
{
|
||||
if (joint is HingeJoint hingeForLimits)
|
||||
{
|
||||
var limits = hingeForLimits.limits;
|
||||
if (limitsToken["min"] != null)
|
||||
limits.min = limitsToken["min"].Value<float>();
|
||||
if (limitsToken["max"] != null)
|
||||
limits.max = limitsToken["max"].Value<float>();
|
||||
if (limitsToken["bounciness"] != null)
|
||||
limits.bounciness = limitsToken["bounciness"].Value<float>();
|
||||
hingeForLimits.limits = limits;
|
||||
hingeForLimits.useLimits = true;
|
||||
configured.Add("limits");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Limits configuration is only supported on HingeJoint, not {joint.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Spring configuration (HingeJoint / SpringJoint)
|
||||
var springToken = p.GetRaw("spring") as JObject;
|
||||
if (springToken != null)
|
||||
{
|
||||
if (joint is HingeJoint hingeForSpring)
|
||||
{
|
||||
var spring = hingeForSpring.spring;
|
||||
if (springToken["spring"] != null)
|
||||
spring.spring = springToken["spring"].Value<float>();
|
||||
if (springToken["damper"] != null)
|
||||
spring.damper = springToken["damper"].Value<float>();
|
||||
if (springToken["targetPosition"] != null)
|
||||
spring.targetPosition = springToken["targetPosition"].Value<float>();
|
||||
hingeForSpring.spring = spring;
|
||||
hingeForSpring.useSpring = true;
|
||||
configured.Add("spring");
|
||||
}
|
||||
else if (joint is SpringJoint springJoint)
|
||||
{
|
||||
if (springToken["spring"] != null)
|
||||
springJoint.spring = springToken["spring"].Value<float>();
|
||||
if (springToken["damper"] != null)
|
||||
springJoint.damper = springToken["damper"].Value<float>();
|
||||
if (springToken["targetPosition"] != null)
|
||||
springJoint.minDistance = springToken["targetPosition"].Value<float>();
|
||||
configured.Add("spring");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Spring configuration is only supported on HingeJoint/SpringJoint, not {joint.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Drive configuration (ConfigurableJoint)
|
||||
var driveToken = p.GetRaw("drive") as JObject;
|
||||
if (driveToken != null)
|
||||
{
|
||||
if (joint is ConfigurableJoint configJoint)
|
||||
{
|
||||
var xDriveToken = driveToken["xDrive"] as JObject;
|
||||
if (xDriveToken != null)
|
||||
{
|
||||
var xDrive = configJoint.xDrive;
|
||||
if (xDriveToken["positionSpring"] != null)
|
||||
xDrive.positionSpring = xDriveToken["positionSpring"].Value<float>();
|
||||
if (xDriveToken["positionDamper"] != null)
|
||||
xDrive.positionDamper = xDriveToken["positionDamper"].Value<float>();
|
||||
if (xDriveToken["maximumForce"] != null)
|
||||
xDrive.maximumForce = xDriveToken["maximumForce"].Value<float>();
|
||||
configJoint.xDrive = xDrive;
|
||||
}
|
||||
configured.Add("drive");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse($"Drive configuration is only supported on ConfigurableJoint, not {joint.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Direct property setting
|
||||
var properties = p.GetRaw("properties") as JObject;
|
||||
if (properties != null)
|
||||
{
|
||||
SetPropertiesViaReflection(joint, properties);
|
||||
configured.Add("properties");
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(joint);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Configured {joint.GetType().Name} on '{go.name}': {string.Join(", ", configured)}.",
|
||||
data = new
|
||||
{
|
||||
jointType = joint.GetType().Name,
|
||||
configured,
|
||||
instanceID = joint.GetInstanceIDCompat()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object RemoveJoint(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetResult = p.GetRequired("target");
|
||||
var errorObj = targetResult.GetOrError(out string targetStr);
|
||||
if (errorObj != null) return errorObj;
|
||||
|
||||
string searchMethod = p.Get("search_method");
|
||||
|
||||
GameObject go = FindTarget(@params["target"], searchMethod);
|
||||
if (go == null)
|
||||
return new ErrorResponse($"Target GameObject '{targetStr}' not found.");
|
||||
|
||||
string jointTypeStr = p.Get("joint_type");
|
||||
int? componentIndex = ParamCoercion.CoerceIntNullable(@params["componentIndex"] ?? @params["component_index"]);
|
||||
|
||||
if (componentIndex.HasValue && string.IsNullOrEmpty(jointTypeStr))
|
||||
return new ErrorResponse("component_index requires joint_type to be specified.");
|
||||
|
||||
var jointsToRemove = new List<Component>();
|
||||
|
||||
if (!string.IsNullOrEmpty(jointTypeStr))
|
||||
{
|
||||
// Remove specific joint type
|
||||
bool is2D = go.GetComponent<Rigidbody2D>() != null;
|
||||
var typeMap = is2D ? JointTypes2D : JointTypes3D;
|
||||
string key = jointTypeStr.ToLowerInvariant();
|
||||
|
||||
if (!typeMap.TryGetValue(key, out Type jointComponentType))
|
||||
{
|
||||
string validTypes = string.Join(", ", typeMap.Keys);
|
||||
string dimension = is2D ? "2D" : "3D";
|
||||
return new ErrorResponse($"Unknown {dimension} joint type: '{jointTypeStr}'. Valid types: {validTypes}.");
|
||||
}
|
||||
|
||||
var components = go.GetComponents(jointComponentType);
|
||||
|
||||
if (componentIndex.HasValue)
|
||||
{
|
||||
if (componentIndex.Value < 0 || componentIndex.Value >= components.Length)
|
||||
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {components.Length} '{jointComponentType.Name}' joint(s) on '{go.name}'.");
|
||||
jointsToRemove.Add(components[componentIndex.Value]);
|
||||
}
|
||||
else
|
||||
{
|
||||
jointsToRemove.AddRange(components);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove ALL joints (both 3D and 2D)
|
||||
var joints3D = go.GetComponents<Joint>();
|
||||
var joints2D = go.GetComponents<Joint2D>();
|
||||
jointsToRemove.AddRange(joints3D);
|
||||
jointsToRemove.AddRange(joints2D);
|
||||
}
|
||||
|
||||
if (jointsToRemove.Count == 0)
|
||||
{
|
||||
string typeInfo = string.IsNullOrEmpty(jointTypeStr) ? "" : $" of type '{jointTypeStr}'";
|
||||
return new ErrorResponse($"No joints{typeInfo} found on '{go.name}'.");
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
foreach (var joint in jointsToRemove)
|
||||
{
|
||||
Undo.DestroyObjectImmediate(joint);
|
||||
removedCount++;
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(go);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Removed {removedCount} joint(s) from '{go.name}'.",
|
||||
data = new
|
||||
{
|
||||
removedCount,
|
||||
gameObjectInstanceID = go.GetInstanceIDCompat()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static GameObject FindTarget(JToken targetToken, string searchMethod)
|
||||
{
|
||||
if (targetToken == null)
|
||||
return null;
|
||||
|
||||
if (targetToken.Type == JTokenType.Integer)
|
||||
{
|
||||
int instanceId = targetToken.Value<int>();
|
||||
return GameObjectLookup.FindById(instanceId);
|
||||
}
|
||||
|
||||
string targetStr = targetToken.ToString();
|
||||
|
||||
if (int.TryParse(targetStr, out int parsedId))
|
||||
{
|
||||
var byId = GameObjectLookup.FindById(parsedId);
|
||||
if (byId != null)
|
||||
return byId;
|
||||
}
|
||||
|
||||
return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true);
|
||||
}
|
||||
|
||||
private static Component ResolveJoint(GameObject go, string jointTypeStr, int? index, out int foundCount)
|
||||
{
|
||||
foundCount = -1;
|
||||
if (!string.IsNullOrEmpty(jointTypeStr))
|
||||
{
|
||||
bool is2D = go.GetComponent<Rigidbody2D>() != null;
|
||||
var typeMap = is2D ? JointTypes2D : JointTypes3D;
|
||||
string key = jointTypeStr.ToLowerInvariant();
|
||||
|
||||
if (typeMap.TryGetValue(key, out Type jointType))
|
||||
{
|
||||
if (index.HasValue)
|
||||
{
|
||||
var components = go.GetComponents(jointType);
|
||||
foundCount = components.Length;
|
||||
if (index.Value < 0 || index.Value >= components.Length)
|
||||
return null;
|
||||
return components[index.Value];
|
||||
}
|
||||
return go.GetComponent(jointType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Auto-detect: find the single joint on the GO
|
||||
var joints3D = go.GetComponents<Joint>();
|
||||
var joints2D = go.GetComponents<Joint2D>();
|
||||
|
||||
int totalCount = joints3D.Length + joints2D.Length;
|
||||
if (totalCount == 1)
|
||||
return joints3D.Length == 1 ? (Component)joints3D[0] : joints2D[0];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void SetPropertiesViaReflection(Component component, JObject properties)
|
||||
{
|
||||
var type = component.GetType();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
var propInfo = type.GetProperty(prop.Name, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (propInfo != null && propInfo.CanWrite)
|
||||
{
|
||||
try
|
||||
{
|
||||
object value = ConvertValue(prop.Value, propInfo.PropertyType);
|
||||
propInfo.SetValue(component, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[JointOps] Failed to set property '{prop.Name}': {ex.Message}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var fieldInfo = type.GetField(prop.Name, BindingFlags.Public | BindingFlags.Instance);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
object value = ConvertValue(prop.Value, fieldInfo.FieldType);
|
||||
fieldInfo.SetValue(component, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[JointOps] Failed to set field '{prop.Name}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static object ConvertValue(JToken token, Type targetType)
|
||||
{
|
||||
if (targetType == typeof(float))
|
||||
return token.Value<float>();
|
||||
if (targetType == typeof(int))
|
||||
return token.Value<int>();
|
||||
if (targetType == typeof(bool))
|
||||
return token.Value<bool>();
|
||||
if (targetType == typeof(string))
|
||||
return token.Value<string>();
|
||||
if (targetType == typeof(Vector3))
|
||||
{
|
||||
var arr = token as JArray;
|
||||
if (arr != null && arr.Count >= 3)
|
||||
return new Vector3(arr[0].Value<float>(), arr[1].Value<float>(), arr[2].Value<float>());
|
||||
}
|
||||
if (targetType == typeof(Vector2))
|
||||
{
|
||||
var arr = token as JArray;
|
||||
if (arr != null && arr.Count >= 2)
|
||||
return new Vector2(arr[0].Value<float>(), arr[1].Value<float>());
|
||||
}
|
||||
|
||||
return token.ToObject(targetType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e4c834ed54144f8bc1f8cb657209926
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
[McpForUnityTool("manage_physics", AutoRegister = false, Group = "core")]
|
||||
public static class ManagePhysics
|
||||
{
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null)
|
||||
return new ErrorResponse("Parameters cannot be null.");
|
||||
|
||||
var p = new ToolParams(@params);
|
||||
string action = p.Get("action")?.ToLowerInvariant();
|
||||
|
||||
if (string.IsNullOrEmpty(action))
|
||||
return new ErrorResponse("'action' parameter is required.");
|
||||
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
// --- Health check ---
|
||||
case "ping":
|
||||
return PhysicsSettingsOps.Ping(@params);
|
||||
|
||||
// --- Settings actions ---
|
||||
case "get_settings":
|
||||
return PhysicsSettingsOps.GetSettings(@params);
|
||||
case "set_settings":
|
||||
return PhysicsSettingsOps.SetSettings(@params);
|
||||
|
||||
// --- Collision matrix actions ---
|
||||
case "get_collision_matrix":
|
||||
return CollisionMatrixOps.GetCollisionMatrix(@params);
|
||||
case "set_collision_matrix":
|
||||
return CollisionMatrixOps.SetCollisionMatrix(@params);
|
||||
|
||||
// --- Physics material actions ---
|
||||
case "create_physics_material":
|
||||
return PhysicsMaterialOps.Create(@params);
|
||||
case "configure_physics_material":
|
||||
return PhysicsMaterialOps.Configure(@params);
|
||||
case "assign_physics_material":
|
||||
return PhysicsMaterialOps.Assign(@params);
|
||||
|
||||
// --- Joint actions ---
|
||||
case "add_joint":
|
||||
return JointOps.AddJoint(@params);
|
||||
case "configure_joint":
|
||||
return JointOps.ConfigureJoint(@params);
|
||||
case "remove_joint":
|
||||
return JointOps.RemoveJoint(@params);
|
||||
|
||||
// --- Query actions ---
|
||||
case "raycast":
|
||||
return PhysicsQueryOps.Raycast(@params);
|
||||
case "raycast_all":
|
||||
return PhysicsQueryOps.RaycastAll(@params);
|
||||
case "linecast":
|
||||
return PhysicsQueryOps.Linecast(@params);
|
||||
case "shapecast":
|
||||
return PhysicsQueryOps.Shapecast(@params);
|
||||
case "overlap":
|
||||
return PhysicsQueryOps.Overlap(@params);
|
||||
|
||||
// --- Force actions ---
|
||||
case "apply_force":
|
||||
return PhysicsForceOps.ApplyForce(@params);
|
||||
|
||||
// --- Rigidbody actions ---
|
||||
case "get_rigidbody":
|
||||
return PhysicsRigidbodyOps.GetRigidbody(@params);
|
||||
case "configure_rigidbody":
|
||||
return PhysicsRigidbodyOps.ConfigureRigidbody(@params);
|
||||
|
||||
// --- Validation ---
|
||||
case "validate":
|
||||
return PhysicsValidationOps.Validate(@params);
|
||||
|
||||
// --- Simulation ---
|
||||
case "simulate_step":
|
||||
return PhysicsSimulationOps.SimulateStep(@params);
|
||||
|
||||
default:
|
||||
return new ErrorResponse(
|
||||
$"Unknown action: '{action}'. Valid actions: ping, "
|
||||
+ "get_settings, set_settings, "
|
||||
+ "get_collision_matrix, set_collision_matrix, "
|
||||
+ "create_physics_material, configure_physics_material, assign_physics_material, "
|
||||
+ "add_joint, configure_joint, remove_joint, "
|
||||
+ "raycast, raycast_all, linecast, shapecast, overlap, "
|
||||
+ "apply_force, get_rigidbody, configure_rigidbody, validate, simulate_step.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"[ManagePhysics] Action '{action}' failed: {ex}");
|
||||
return new ErrorResponse(
|
||||
$"Error in action '{action}': {ex.Message}",
|
||||
new { stackTrace = ex.StackTrace }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 424786337ed3467e833bef1881138a37
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class PhysicsForceOps
|
||||
{
|
||||
public static object ApplyForce(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetResult = p.GetRequired("target");
|
||||
var errorObj = targetResult.GetOrError(out string targetStr);
|
||||
if (errorObj != null) return errorObj;
|
||||
|
||||
string searchMethod = p.Get("search_method");
|
||||
|
||||
GameObject go = FindTarget(@params["target"], searchMethod);
|
||||
if (go == null)
|
||||
return new ErrorResponse($"Target GameObject '{targetStr}' not found.");
|
||||
|
||||
// Detect dimension
|
||||
string dimensionParam = p.Get("dimension")?.ToLowerInvariant();
|
||||
bool has3DRb = go.GetComponent<Rigidbody>() != null;
|
||||
bool has2DRb = go.GetComponent<Rigidbody2D>() != null;
|
||||
bool is2D;
|
||||
|
||||
if (dimensionParam == "2d")
|
||||
is2D = true;
|
||||
else if (dimensionParam == "3d")
|
||||
is2D = false;
|
||||
else
|
||||
is2D = has2DRb && !has3DRb;
|
||||
|
||||
// Validate rigidbody exists
|
||||
if (is2D && !has2DRb)
|
||||
return new ErrorResponse($"Target '{go.name}' has no Rigidbody2D. Add one before applying force.");
|
||||
if (!is2D && !has3DRb)
|
||||
return new ErrorResponse($"Target '{go.name}' has no Rigidbody. Add one before applying force.");
|
||||
|
||||
// Validate not kinematic
|
||||
if (is2D)
|
||||
{
|
||||
var rb2d = go.GetComponent<Rigidbody2D>();
|
||||
if (rb2d.bodyType == RigidbodyType2D.Kinematic)
|
||||
return new ErrorResponse($"Cannot apply force to kinematic Rigidbody on '{go.name}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
if (rb.isKinematic)
|
||||
return new ErrorResponse($"Cannot apply force to kinematic Rigidbody on '{go.name}'.");
|
||||
}
|
||||
|
||||
string forceType = (p.Get("force_type") ?? "normal").ToLowerInvariant();
|
||||
|
||||
if (forceType == "explosion")
|
||||
return ApplyExplosionForce(p, go, is2D);
|
||||
|
||||
if (forceType == "normal")
|
||||
return ApplyNormalForce(p, go, is2D);
|
||||
|
||||
return new ErrorResponse($"Unknown force_type: '{forceType}'. Valid types: normal, explosion.");
|
||||
}
|
||||
|
||||
private static object ApplyNormalForce(ToolParams p, GameObject go, bool is2D)
|
||||
{
|
||||
var forceToken = p.GetRaw("force") as JArray;
|
||||
var torqueToken = p.GetRaw("torque");
|
||||
|
||||
if (forceToken == null && torqueToken == null)
|
||||
return new ErrorResponse("Either 'force' or 'torque' (or both) must be provided.");
|
||||
|
||||
string modeStr = p.Get("force_mode");
|
||||
var positionToken = p.GetRaw("position") as JArray;
|
||||
|
||||
var applied = new List<string>();
|
||||
var responseData = new Dictionary<string, object>
|
||||
{
|
||||
["target"] = go.name,
|
||||
["dimension"] = is2D ? "2d" : "3d",
|
||||
["force_type"] = "normal"
|
||||
};
|
||||
|
||||
if (is2D)
|
||||
{
|
||||
ForceMode2D mode2d = ForceMode2D.Force;
|
||||
if (!string.IsNullOrEmpty(modeStr))
|
||||
{
|
||||
if (!Enum.TryParse<ForceMode2D>(modeStr, true, out mode2d))
|
||||
return new ErrorResponse($"Invalid ForceMode2D: '{modeStr}'. Valid values: Force, Impulse.");
|
||||
|
||||
if (mode2d != ForceMode2D.Force && mode2d != ForceMode2D.Impulse)
|
||||
return new ErrorResponse($"ForceMode2D only supports Force and Impulse, not '{modeStr}'.");
|
||||
}
|
||||
|
||||
responseData["force_mode"] = mode2d.ToString();
|
||||
var rb2d = go.GetComponent<Rigidbody2D>();
|
||||
|
||||
if (forceToken != null)
|
||||
{
|
||||
if (forceToken.Count < 2)
|
||||
return new ErrorResponse("'force' array must contain at least 2 floats for 2D.");
|
||||
|
||||
var forceVec = new Vector2(forceToken[0].Value<float>(), forceToken[1].Value<float>());
|
||||
|
||||
if (positionToken != null)
|
||||
{
|
||||
if (positionToken.Count < 2)
|
||||
return new ErrorResponse("'position' array must contain at least 2 floats for 2D.");
|
||||
|
||||
var posVec = new Vector2(positionToken[0].Value<float>(), positionToken[1].Value<float>());
|
||||
rb2d.AddForceAtPosition(forceVec, posVec, mode2d);
|
||||
}
|
||||
else
|
||||
{
|
||||
rb2d.AddForce(forceVec, mode2d);
|
||||
}
|
||||
|
||||
responseData["force"] = new[] { forceVec.x, forceVec.y };
|
||||
applied.Add("force");
|
||||
}
|
||||
|
||||
if (torqueToken != null)
|
||||
{
|
||||
float torqueFloat = torqueToken.Value<float>();
|
||||
rb2d.AddTorque(torqueFloat, mode2d);
|
||||
responseData["torque"] = torqueFloat;
|
||||
applied.Add("torque");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ForceMode mode = ForceMode.Force;
|
||||
if (!string.IsNullOrEmpty(modeStr))
|
||||
{
|
||||
if (!Enum.TryParse<ForceMode>(modeStr, true, out mode))
|
||||
return new ErrorResponse($"Invalid ForceMode: '{modeStr}'. Valid values: Force, Impulse, Acceleration, VelocityChange.");
|
||||
}
|
||||
|
||||
responseData["force_mode"] = mode.ToString();
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
|
||||
if (forceToken != null)
|
||||
{
|
||||
if (forceToken.Count < 3)
|
||||
return new ErrorResponse("'force' array must contain at least 3 floats for 3D.");
|
||||
|
||||
var forceVec = new Vector3(
|
||||
forceToken[0].Value<float>(),
|
||||
forceToken[1].Value<float>(),
|
||||
forceToken[2].Value<float>());
|
||||
|
||||
if (positionToken != null)
|
||||
{
|
||||
if (positionToken.Count < 3)
|
||||
return new ErrorResponse("'position' array must contain at least 3 floats for 3D.");
|
||||
|
||||
var posVec = new Vector3(
|
||||
positionToken[0].Value<float>(),
|
||||
positionToken[1].Value<float>(),
|
||||
positionToken[2].Value<float>());
|
||||
rb.AddForceAtPosition(forceVec, posVec, mode);
|
||||
}
|
||||
else
|
||||
{
|
||||
rb.AddForce(forceVec, mode);
|
||||
}
|
||||
|
||||
responseData["force"] = new[] { forceVec.x, forceVec.y, forceVec.z };
|
||||
applied.Add("force");
|
||||
}
|
||||
|
||||
if (torqueToken != null)
|
||||
{
|
||||
var torqueArr = torqueToken as JArray;
|
||||
if (torqueArr == null || torqueArr.Count < 3)
|
||||
return new ErrorResponse("'torque' array must contain at least 3 floats for 3D.");
|
||||
|
||||
var torqueVec = new Vector3(
|
||||
torqueArr[0].Value<float>(),
|
||||
torqueArr[1].Value<float>(),
|
||||
torqueArr[2].Value<float>());
|
||||
rb.AddTorque(torqueVec, mode);
|
||||
responseData["torque"] = new[] { torqueVec.x, torqueVec.y, torqueVec.z };
|
||||
applied.Add("torque");
|
||||
}
|
||||
}
|
||||
|
||||
string appliedStr = string.Join(" and ", applied);
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Applied {appliedStr} to '{go.name}'.",
|
||||
data = responseData
|
||||
};
|
||||
}
|
||||
|
||||
private static object ApplyExplosionForce(ToolParams p, GameObject go, bool is2D)
|
||||
{
|
||||
if (is2D)
|
||||
return new ErrorResponse("Explosion force is only available for 3D physics.");
|
||||
|
||||
float? explosionForce = p.GetFloat("explosion_force");
|
||||
if (explosionForce == null)
|
||||
return new ErrorResponse("'explosion_force' is required for explosion force type.");
|
||||
|
||||
var explosionPosToken = p.GetRaw("explosion_position") as JArray;
|
||||
if (explosionPosToken == null || explosionPosToken.Count < 3)
|
||||
return new ErrorResponse("'explosion_position' array (3 floats) is required for explosion force type.");
|
||||
|
||||
float? explosionRadius = p.GetFloat("explosion_radius");
|
||||
if (explosionRadius == null)
|
||||
return new ErrorResponse("'explosion_radius' is required for explosion force type.");
|
||||
|
||||
float upwardsModifier = p.GetFloat("upwards_modifier") ?? 0f;
|
||||
|
||||
string modeStr = p.Get("force_mode");
|
||||
ForceMode mode = ForceMode.Force;
|
||||
if (!string.IsNullOrEmpty(modeStr))
|
||||
{
|
||||
if (!Enum.TryParse<ForceMode>(modeStr, true, out mode))
|
||||
return new ErrorResponse($"Invalid ForceMode: '{modeStr}'. Valid values: Force, Impulse, Acceleration, VelocityChange.");
|
||||
}
|
||||
|
||||
var explosionPos = new Vector3(
|
||||
explosionPosToken[0].Value<float>(),
|
||||
explosionPosToken[1].Value<float>(),
|
||||
explosionPosToken[2].Value<float>());
|
||||
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
rb.AddExplosionForce(explosionForce.Value, explosionPos, explosionRadius.Value, upwardsModifier, mode);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Applied explosion force to '{go.name}'.",
|
||||
data = new
|
||||
{
|
||||
target = go.name,
|
||||
dimension = "3d",
|
||||
force_type = "explosion",
|
||||
force_mode = mode.ToString(),
|
||||
explosion_position = new[] { explosionPos.x, explosionPos.y, explosionPos.z },
|
||||
explosion_force = explosionForce.Value,
|
||||
explosion_radius = explosionRadius.Value,
|
||||
upwards_modifier = upwardsModifier
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static GameObject FindTarget(JToken targetToken, string searchMethod)
|
||||
{
|
||||
if (targetToken == null)
|
||||
return null;
|
||||
|
||||
if (targetToken.Type == JTokenType.Integer)
|
||||
{
|
||||
int instanceId = targetToken.Value<int>();
|
||||
return GameObjectLookup.FindById(instanceId);
|
||||
}
|
||||
|
||||
string targetStr = targetToken.ToString();
|
||||
|
||||
if (int.TryParse(targetStr, out int parsedId))
|
||||
{
|
||||
var byId = GameObjectLookup.FindById(parsedId);
|
||||
if (byId != null)
|
||||
return byId;
|
||||
}
|
||||
|
||||
return GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0edad0a2fae04b7383acfb90fec107a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,533 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class PhysicsMaterialOps
|
||||
{
|
||||
public static object Create(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
var nameResult = p.GetRequired("name");
|
||||
var nameErr = nameResult.GetOrError(out string name);
|
||||
if (nameErr != null) return nameErr;
|
||||
|
||||
string folder = p.Get("path") ?? "Assets/Physics Materials";
|
||||
folder = AssetPathUtility.SanitizeAssetPath(folder);
|
||||
if (string.IsNullOrEmpty(folder))
|
||||
return new ErrorResponse("Invalid folder path.");
|
||||
|
||||
if (!EnsureFolderExists(folder, out string folderError))
|
||||
return new ErrorResponse(folderError);
|
||||
|
||||
if (dimension == "2d")
|
||||
return Create2D(name, folder, p);
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
return Create3D(name, folder, p);
|
||||
}
|
||||
|
||||
public static object Configure(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
var pathResult = p.GetRequired("path");
|
||||
var pathErr = pathResult.GetOrError(out string path);
|
||||
if (pathErr != null) return pathErr;
|
||||
|
||||
path = AssetPathUtility.SanitizeAssetPath(path);
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return new ErrorResponse("Invalid asset path.");
|
||||
|
||||
var properties = p.GetRaw("properties") as JObject;
|
||||
if (properties == null || properties.Count == 0)
|
||||
return new ErrorResponse("'properties' parameter is required and must be a non-empty object.");
|
||||
|
||||
if (dimension != "3d" && dimension != "2d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
if (dimension == "2d")
|
||||
return Configure2D(path, properties);
|
||||
|
||||
return Configure3D(path, properties);
|
||||
}
|
||||
|
||||
public static object Assign(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetToken = p.GetRaw("target");
|
||||
if (targetToken == null || string.IsNullOrEmpty(targetToken.ToString()))
|
||||
return new ErrorResponse("'target' parameter is required.");
|
||||
|
||||
var matPathResult = p.GetRequired("material_path");
|
||||
var matPathErr = matPathResult.GetOrError(out string materialPath);
|
||||
if (matPathErr != null) return matPathErr;
|
||||
|
||||
materialPath = AssetPathUtility.SanitizeAssetPath(materialPath);
|
||||
if (string.IsNullOrEmpty(materialPath))
|
||||
return new ErrorResponse("Invalid material path.");
|
||||
|
||||
string searchMethod = p.Get("search_method") ?? "by_name";
|
||||
string colliderType = p.Get("collider_type");
|
||||
int? componentIndex = ParamCoercion.CoerceIntNullable(p.GetRaw("componentIndex") ?? p.GetRaw("component_index"));
|
||||
|
||||
var go = GameObjectLookup.FindByTarget(targetToken, searchMethod);
|
||||
if (go == null)
|
||||
return new ErrorResponse($"GameObject not found: '{targetToken}'.");
|
||||
|
||||
// Try to load as 3D physics material first
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var mat3D = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(materialPath);
|
||||
#else
|
||||
var mat3D = AssetDatabase.LoadAssetAtPath<PhysicMaterial>(materialPath);
|
||||
#endif
|
||||
var mat2D = AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(materialPath);
|
||||
|
||||
if (mat3D == null && mat2D == null)
|
||||
return new ErrorResponse($"No physics material found at path: '{materialPath}'.");
|
||||
|
||||
// Try 3D colliders first
|
||||
if (mat3D != null)
|
||||
{
|
||||
var collider3D = FindCollider3D(go, colliderType, componentIndex);
|
||||
if (collider3D != null)
|
||||
{
|
||||
Undo.RecordObject(collider3D, "Assign Physics Material");
|
||||
collider3D.sharedMaterial = mat3D;
|
||||
EditorUtility.SetDirty(collider3D);
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Assigned 3D physics material to {collider3D.GetType().Name} on '{go.name}'.",
|
||||
data = new
|
||||
{
|
||||
gameObject = go.name,
|
||||
collider = collider3D.GetType().Name,
|
||||
materialPath
|
||||
}
|
||||
};
|
||||
}
|
||||
if (componentIndex.HasValue)
|
||||
{
|
||||
var type3D = !string.IsNullOrEmpty(colliderType) ? UnityTypeResolver.ResolveComponent(colliderType) : typeof(Collider);
|
||||
if (type3D != null && typeof(Collider).IsAssignableFrom(type3D))
|
||||
{
|
||||
int count3D = go.GetComponents(type3D).Length;
|
||||
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {count3D} '{type3D.Name}' collider(s) on '{go.name}'.");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(colliderType))
|
||||
{
|
||||
return new ErrorResponse($"Unknown or invalid 3D collider type: '{colliderType}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try 2D colliders
|
||||
if (mat2D != null)
|
||||
{
|
||||
var collider2D = FindCollider2D(go, colliderType, componentIndex);
|
||||
if (collider2D != null)
|
||||
{
|
||||
Undo.RecordObject(collider2D, "Assign Physics Material 2D");
|
||||
collider2D.sharedMaterial = mat2D;
|
||||
EditorUtility.SetDirty(collider2D);
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Assigned 2D physics material to {collider2D.GetType().Name} on '{go.name}'.",
|
||||
data = new
|
||||
{
|
||||
gameObject = go.name,
|
||||
collider = collider2D.GetType().Name,
|
||||
materialPath
|
||||
}
|
||||
};
|
||||
}
|
||||
if (componentIndex.HasValue)
|
||||
{
|
||||
var type2D = !string.IsNullOrEmpty(colliderType) ? UnityTypeResolver.ResolveComponent(colliderType) : typeof(Collider2D);
|
||||
if (type2D != null && typeof(Collider2D).IsAssignableFrom(type2D))
|
||||
{
|
||||
int count2D = go.GetComponents(type2D).Length;
|
||||
return new ErrorResponse($"component_index {componentIndex.Value} out of range. Found {count2D} '{type2D.Name}' collider(s) on '{go.name}'.");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(colliderType))
|
||||
{
|
||||
return new ErrorResponse($"Unknown or invalid 2D collider type: '{colliderType}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ErrorResponse($"No suitable collider found on '{go.name}'.");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Create helpers
|
||||
// =====================================================================
|
||||
|
||||
private static object Create3D(string name, string folder, ToolParams p)
|
||||
{
|
||||
float dynamicFriction = p.GetFloat("dynamic_friction") ?? 0.6f;
|
||||
float staticFriction = p.GetFloat("static_friction") ?? 0.6f;
|
||||
float bounciness = p.GetFloat("bounciness") ?? 0f;
|
||||
string frictionCombine = p.Get("friction_combine");
|
||||
string bounceCombine = p.Get("bounce_combine");
|
||||
|
||||
string assetPath = $"{folder}/{name}.physicMaterial";
|
||||
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath) != null)
|
||||
return new ErrorResponse($"A physics material already exists at '{assetPath}'. Use configure_physics_material to modify it.");
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var mat = new PhysicsMaterial(name)
|
||||
{
|
||||
dynamicFriction = dynamicFriction,
|
||||
staticFriction = staticFriction,
|
||||
bounciness = bounciness
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(frictionCombine) &&
|
||||
Enum.TryParse<PhysicsMaterialCombine>(frictionCombine, true, out var fc))
|
||||
mat.frictionCombine = fc;
|
||||
|
||||
if (!string.IsNullOrEmpty(bounceCombine) &&
|
||||
Enum.TryParse<PhysicsMaterialCombine>(bounceCombine, true, out var bc))
|
||||
mat.bounceCombine = bc;
|
||||
#else
|
||||
var mat = new PhysicMaterial(name)
|
||||
{
|
||||
dynamicFriction = dynamicFriction,
|
||||
staticFriction = staticFriction,
|
||||
bounciness = bounciness
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(frictionCombine) &&
|
||||
Enum.TryParse<PhysicMaterialCombine>(frictionCombine, true, out var fc))
|
||||
mat.frictionCombine = fc;
|
||||
|
||||
if (!string.IsNullOrEmpty(bounceCombine) &&
|
||||
Enum.TryParse<PhysicMaterialCombine>(bounceCombine, true, out var bc))
|
||||
mat.bounceCombine = bc;
|
||||
#endif
|
||||
|
||||
AssetDatabase.CreateAsset(mat, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created 3D physics material '{name}' at '{assetPath}'.",
|
||||
data = new
|
||||
{
|
||||
path = assetPath,
|
||||
dimension = "3d",
|
||||
dynamicFriction,
|
||||
staticFriction,
|
||||
bounciness,
|
||||
frictionCombine = mat.frictionCombine.ToString(),
|
||||
bounceCombine = mat.bounceCombine.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object Create2D(string name, string folder, ToolParams p)
|
||||
{
|
||||
float friction = p.GetFloat("friction") ?? 0.4f;
|
||||
float bounciness = p.GetFloat("bounciness") ?? 0f;
|
||||
|
||||
string assetPath = $"{folder}/{name}.physicsMaterial2D";
|
||||
|
||||
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath) != null)
|
||||
return new ErrorResponse($"A 2D physics material already exists at '{assetPath}'. Use configure_physics_material to modify it.");
|
||||
|
||||
var mat = new PhysicsMaterial2D(name)
|
||||
{
|
||||
friction = friction,
|
||||
bounciness = bounciness
|
||||
};
|
||||
|
||||
AssetDatabase.CreateAsset(mat, assetPath);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Created 2D physics material '{name}' at '{assetPath}'.",
|
||||
data = new
|
||||
{
|
||||
path = assetPath,
|
||||
dimension = "2d",
|
||||
friction,
|
||||
bounciness
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Configure helpers
|
||||
// =====================================================================
|
||||
|
||||
private static readonly HashSet<string> Valid3DMatKeys = new HashSet<string>
|
||||
{
|
||||
"dynamicfriction", "staticfriction", "bounciness", "frictioncombine", "bouncecombine"
|
||||
};
|
||||
|
||||
private static object Configure3D(string path, JObject properties)
|
||||
{
|
||||
// Validate all keys before applying any changes
|
||||
var unknown = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
if (!Valid3DMatKeys.Contains(key))
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
if (unknown.Count > 0)
|
||||
return new ErrorResponse(
|
||||
$"Unknown 3D physics material property(ies): {string.Join(", ", unknown)}.");
|
||||
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
var mat = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(path);
|
||||
#else
|
||||
var mat = AssetDatabase.LoadAssetAtPath<PhysicMaterial>(path);
|
||||
#endif
|
||||
if (mat == null)
|
||||
return new ErrorResponse($"No 3D physics material found at: '{path}'.");
|
||||
|
||||
Undo.RecordObject(mat, "Configure Physics Material");
|
||||
|
||||
var changed = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
switch (key)
|
||||
{
|
||||
case "dynamicfriction":
|
||||
mat.dynamicFriction = prop.Value.Value<float>();
|
||||
changed.Add("dynamicFriction");
|
||||
break;
|
||||
case "staticfriction":
|
||||
mat.staticFriction = prop.Value.Value<float>();
|
||||
changed.Add("staticFriction");
|
||||
break;
|
||||
case "bounciness":
|
||||
mat.bounciness = prop.Value.Value<float>();
|
||||
changed.Add("bounciness");
|
||||
break;
|
||||
case "frictioncombine":
|
||||
{
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!Enum.TryParse<PhysicsMaterialCombine>(prop.Value.ToString(), true, out var fc))
|
||||
return new ErrorResponse($"Invalid friction_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
|
||||
mat.frictionCombine = fc;
|
||||
changed.Add("frictionCombine");
|
||||
#else
|
||||
if (!Enum.TryParse<PhysicMaterialCombine>(prop.Value.ToString(), true, out var fc))
|
||||
return new ErrorResponse($"Invalid friction_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
|
||||
mat.frictionCombine = fc;
|
||||
changed.Add("frictionCombine");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case "bouncecombine":
|
||||
{
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
if (!Enum.TryParse<PhysicsMaterialCombine>(prop.Value.ToString(), true, out var bc))
|
||||
return new ErrorResponse($"Invalid bounce_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
|
||||
mat.bounceCombine = bc;
|
||||
changed.Add("bounceCombine");
|
||||
#else
|
||||
if (!Enum.TryParse<PhysicMaterialCombine>(prop.Value.ToString(), true, out var bc))
|
||||
return new ErrorResponse($"Invalid bounce_combine value: '{prop.Value}'. Valid values: Average, Minimum, Maximum, Multiply.");
|
||||
mat.bounceCombine = bc;
|
||||
changed.Add("bounceCombine");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(mat);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Updated {changed.Count} property(ies) on 3D physics material at '{path}'.",
|
||||
data = new { path, changed }
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> Valid2DMatKeys = new HashSet<string>
|
||||
{
|
||||
"friction", "bounciness"
|
||||
};
|
||||
|
||||
private static object Configure2D(string path, JObject properties)
|
||||
{
|
||||
// Validate all keys before applying any changes
|
||||
var unknown = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
if (!Valid2DMatKeys.Contains(key))
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
if (unknown.Count > 0)
|
||||
return new ErrorResponse(
|
||||
$"Unknown 2D physics material property(ies): {string.Join(", ", unknown)}.");
|
||||
|
||||
var mat = AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(path);
|
||||
if (mat == null)
|
||||
return new ErrorResponse($"No 2D physics material found at: '{path}'.");
|
||||
|
||||
Undo.RecordObject(mat, "Configure Physics Material 2D");
|
||||
|
||||
var changed = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
switch (key)
|
||||
{
|
||||
case "friction":
|
||||
mat.friction = prop.Value.Value<float>();
|
||||
changed.Add("friction");
|
||||
break;
|
||||
case "bounciness":
|
||||
mat.bounciness = prop.Value.Value<float>();
|
||||
changed.Add("bounciness");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(mat);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Updated {changed.Count} property(ies) on 2D physics material at '{path}'.",
|
||||
data = new { path, changed }
|
||||
};
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Assign helpers
|
||||
// =====================================================================
|
||||
|
||||
private static Collider FindCollider3D(GameObject go, string colliderType, int? index = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(colliderType))
|
||||
{
|
||||
var type = UnityTypeResolver.ResolveComponent(colliderType);
|
||||
if (type != null && typeof(Collider).IsAssignableFrom(type))
|
||||
{
|
||||
if (index.HasValue)
|
||||
{
|
||||
var components = go.GetComponents(type);
|
||||
if (index.Value < 0 || index.Value >= components.Length)
|
||||
return null;
|
||||
return components[index.Value] as Collider;
|
||||
}
|
||||
return go.GetComponent(type) as Collider;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index.HasValue)
|
||||
{
|
||||
var colliders = go.GetComponents<Collider>();
|
||||
if (index.Value < 0 || index.Value >= colliders.Length)
|
||||
return null;
|
||||
return colliders[index.Value];
|
||||
}
|
||||
|
||||
return go.GetComponent<Collider>();
|
||||
}
|
||||
|
||||
private static Collider2D FindCollider2D(GameObject go, string colliderType, int? index = null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(colliderType))
|
||||
{
|
||||
var type = UnityTypeResolver.ResolveComponent(colliderType);
|
||||
if (type != null && typeof(Collider2D).IsAssignableFrom(type))
|
||||
{
|
||||
if (index.HasValue)
|
||||
{
|
||||
var components = go.GetComponents(type);
|
||||
if (index.Value < 0 || index.Value >= components.Length)
|
||||
return null;
|
||||
return components[index.Value] as Collider2D;
|
||||
}
|
||||
return go.GetComponent(type) as Collider2D;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index.HasValue)
|
||||
{
|
||||
var colliders = go.GetComponents<Collider2D>();
|
||||
if (index.Value < 0 || index.Value >= colliders.Length)
|
||||
return null;
|
||||
return colliders[index.Value];
|
||||
}
|
||||
|
||||
return go.GetComponent<Collider2D>();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Folder helpers
|
||||
// =====================================================================
|
||||
|
||||
private static bool EnsureFolderExists(string folderPath, out string error)
|
||||
{
|
||||
error = null;
|
||||
if (string.IsNullOrWhiteSpace(folderPath))
|
||||
{
|
||||
error = "Folder path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
folderPath = folderPath.TrimEnd('/');
|
||||
|
||||
if (!folderPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(folderPath, "Assets", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
error = "Folder path must be under Assets/.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (AssetDatabase.IsValidFolder(folderPath))
|
||||
return true;
|
||||
|
||||
var parts = folderPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
string current = "Assets";
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
string next = current + "/" + parts[i];
|
||||
if (!AssetDatabase.IsValidFolder(next))
|
||||
{
|
||||
string guid = AssetDatabase.CreateFolder(current, parts[i]);
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
error = $"Failed to create folder: {next}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1397b3a4fc984774adf5ae6da58b7491
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,807 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class PhysicsQueryOps
|
||||
{
|
||||
public static object Raycast(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
var originArr = p.GetRaw("origin") as JArray;
|
||||
if (originArr == null)
|
||||
return new ErrorResponse("'origin' parameter is required (array of floats).");
|
||||
|
||||
var dirArr = p.GetRaw("direction") as JArray;
|
||||
if (dirArr == null)
|
||||
return new ErrorResponse("'direction' parameter is required (array of floats).");
|
||||
|
||||
float maxDistance = p.GetFloat("max_distance") ?? Mathf.Infinity;
|
||||
|
||||
if (dimension == "2d")
|
||||
return Raycast2D(originArr, dirArr, maxDistance, p);
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
return Raycast3D(originArr, dirArr, maxDistance, p);
|
||||
}
|
||||
|
||||
private static object Raycast3D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p)
|
||||
{
|
||||
if (originArr.Count < 3)
|
||||
return new ErrorResponse("3D raycast 'origin' requires [x, y, z].");
|
||||
if (dirArr.Count < 3)
|
||||
return new ErrorResponse("3D raycast 'direction' requires [x, y, z].");
|
||||
|
||||
var origin = new Vector3(
|
||||
originArr[0].Value<float>(), originArr[1].Value<float>(), originArr[2].Value<float>());
|
||||
var direction = new Vector3(
|
||||
dirArr[0].Value<float>(), dirArr[1].Value<float>(), dirArr[2].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal;
|
||||
string qti = p.Get("query_trigger_interaction");
|
||||
if (!string.IsNullOrEmpty(qti))
|
||||
{
|
||||
if (!System.Enum.TryParse(qti, true, out triggerInteraction))
|
||||
return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide.");
|
||||
}
|
||||
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
bool hit = UnityEngine.Physics.Raycast(origin, direction, out RaycastHit hitInfo, maxDistance, layerMask, triggerInteraction);
|
||||
|
||||
if (!hit)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Raycast did not hit anything.",
|
||||
data = new { hit = false }
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Raycast hit '{hitInfo.collider.gameObject.name}'.",
|
||||
data = new
|
||||
{
|
||||
hit = true,
|
||||
point = new[] { hitInfo.point.x, hitInfo.point.y, hitInfo.point.z },
|
||||
normal = new[] { hitInfo.normal.x, hitInfo.normal.y, hitInfo.normal.z },
|
||||
distance = hitInfo.distance,
|
||||
gameObject = hitInfo.collider.gameObject.name,
|
||||
instanceID = hitInfo.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = hitInfo.collider.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object Raycast2D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p)
|
||||
{
|
||||
if (originArr.Count < 2)
|
||||
return new ErrorResponse("2D raycast 'origin' requires [x, y].");
|
||||
if (dirArr.Count < 2)
|
||||
return new ErrorResponse("2D raycast 'direction' requires [x, y].");
|
||||
|
||||
var origin = new Vector2(originArr[0].Value<float>(), originArr[1].Value<float>());
|
||||
var direction = new Vector2(dirArr[0].Value<float>(), dirArr[1].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
Physics2D.SyncTransforms();
|
||||
|
||||
var hit = Physics2D.Raycast(origin, direction, maxDistance, layerMask);
|
||||
|
||||
if (hit.collider == null)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "2D Raycast did not hit anything.",
|
||||
data = new { hit = false }
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"2D Raycast hit '{hit.collider.gameObject.name}'.",
|
||||
data = new
|
||||
{
|
||||
hit = true,
|
||||
point = new[] { hit.point.x, hit.point.y },
|
||||
normal = new[] { hit.normal.x, hit.normal.y },
|
||||
distance = hit.distance,
|
||||
gameObject = hit.collider.gameObject.name,
|
||||
instanceID = hit.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = hit.collider.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object Overlap(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
string shape = p.Get("shape");
|
||||
if (string.IsNullOrEmpty(shape))
|
||||
return new ErrorResponse("'shape' parameter is required (sphere, box, capsule for 3D; circle, box, capsule for 2D).");
|
||||
|
||||
var posArr = p.GetRaw("position") as JArray;
|
||||
if (posArr == null)
|
||||
return new ErrorResponse("'position' parameter is required (array of floats).");
|
||||
|
||||
var sizeToken = p.GetRaw("size");
|
||||
if (sizeToken == null)
|
||||
return new ErrorResponse("'size' parameter is required.");
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
if (dimension == "2d")
|
||||
return Overlap2D(shape, posArr, sizeToken, layerMask);
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
return Overlap3D(shape, posArr, sizeToken, layerMask);
|
||||
}
|
||||
|
||||
private static object Overlap3D(string shape, JArray posArr, JToken sizeToken, int layerMask)
|
||||
{
|
||||
if (posArr.Count < 3)
|
||||
return new ErrorResponse("3D overlap 'position' requires [x, y, z].");
|
||||
|
||||
var position = new Vector3(
|
||||
posArr[0].Value<float>(), posArr[1].Value<float>(), posArr[2].Value<float>());
|
||||
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
Collider[] results;
|
||||
|
||||
switch (shape.ToLowerInvariant())
|
||||
{
|
||||
case "sphere":
|
||||
{
|
||||
float radius = sizeToken.Value<float>();
|
||||
results = UnityEngine.Physics.OverlapSphere(position, radius, layerMask);
|
||||
break;
|
||||
}
|
||||
case "box":
|
||||
{
|
||||
Vector3 halfExtents;
|
||||
if (sizeToken is JArray sizeArr && sizeArr.Count >= 3)
|
||||
halfExtents = new Vector3(
|
||||
sizeArr[0].Value<float>(), sizeArr[1].Value<float>(), sizeArr[2].Value<float>());
|
||||
else
|
||||
return new ErrorResponse("3D box overlap 'size' requires [halfX, halfY, halfZ].");
|
||||
results = UnityEngine.Physics.OverlapBox(position, halfExtents, Quaternion.identity, layerMask);
|
||||
break;
|
||||
}
|
||||
case "capsule":
|
||||
{
|
||||
if (sizeToken is JObject capsuleObj)
|
||||
{
|
||||
float radius = capsuleObj["radius"]?.Value<float>() ?? 0.5f;
|
||||
float height = capsuleObj["height"]?.Value<float>() ?? 2f;
|
||||
int direction = capsuleObj["direction"]?.Value<int>() ?? 1;
|
||||
|
||||
Vector3 point0, point1;
|
||||
float halfHeight = Mathf.Max(0, height / 2f - radius);
|
||||
switch (direction)
|
||||
{
|
||||
case 0: // X
|
||||
point0 = position + Vector3.left * halfHeight;
|
||||
point1 = position + Vector3.right * halfHeight;
|
||||
break;
|
||||
case 2: // Z
|
||||
point0 = position + Vector3.back * halfHeight;
|
||||
point1 = position + Vector3.forward * halfHeight;
|
||||
break;
|
||||
default: // Y (1)
|
||||
point0 = position + Vector3.down * halfHeight;
|
||||
point1 = position + Vector3.up * halfHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
results = UnityEngine.Physics.OverlapCapsule(point0, point1, radius, layerMask);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("3D capsule overlap 'size' requires {radius, height, direction}.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return new ErrorResponse($"Unknown 3D shape: '{shape}'. Valid: sphere, box, capsule.");
|
||||
}
|
||||
|
||||
return FormatOverlapResults(results, "3D");
|
||||
}
|
||||
|
||||
private static object Overlap2D(string shape, JArray posArr, JToken sizeToken, int layerMask)
|
||||
{
|
||||
if (posArr.Count < 2)
|
||||
return new ErrorResponse("2D overlap 'position' requires [x, y].");
|
||||
|
||||
var position = new Vector2(posArr[0].Value<float>(), posArr[1].Value<float>());
|
||||
|
||||
Physics2D.SyncTransforms();
|
||||
|
||||
Collider2D[] results;
|
||||
|
||||
switch (shape.ToLowerInvariant())
|
||||
{
|
||||
case "circle":
|
||||
{
|
||||
float radius = sizeToken.Value<float>();
|
||||
results = Physics2D.OverlapCircleAll(position, radius, layerMask);
|
||||
break;
|
||||
}
|
||||
case "box":
|
||||
{
|
||||
Vector2 size;
|
||||
if (sizeToken is JArray sizeArr && sizeArr.Count >= 2)
|
||||
size = new Vector2(sizeArr[0].Value<float>(), sizeArr[1].Value<float>());
|
||||
else
|
||||
return new ErrorResponse("2D box overlap 'size' requires [width, height].");
|
||||
results = Physics2D.OverlapBoxAll(position, size, 0f, layerMask);
|
||||
break;
|
||||
}
|
||||
case "capsule":
|
||||
{
|
||||
if (sizeToken is JObject capsuleObj)
|
||||
{
|
||||
float sizeX = capsuleObj["width"]?.Value<float>() ?? 1f;
|
||||
float sizeY = capsuleObj["height"]?.Value<float>() ?? 2f;
|
||||
var dir = CapsuleDirection2D.Vertical;
|
||||
string dirStr = capsuleObj["direction"]?.ToString();
|
||||
if (dirStr != null && dirStr.ToLowerInvariant() == "horizontal")
|
||||
dir = CapsuleDirection2D.Horizontal;
|
||||
results = Physics2D.OverlapCapsuleAll(position, new Vector2(sizeX, sizeY), dir, 0f, layerMask);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("2D capsule overlap 'size' requires {width, height, direction}.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return new ErrorResponse($"Unknown 2D shape: '{shape}'. Valid: circle, box, capsule.");
|
||||
}
|
||||
|
||||
return FormatOverlapResults2D(results, "2D");
|
||||
}
|
||||
|
||||
private static object FormatOverlapResults(Collider[] results, string dimension)
|
||||
{
|
||||
var colliders = new List<object>();
|
||||
foreach (var col in results)
|
||||
{
|
||||
colliders.Add(new
|
||||
{
|
||||
gameObject = col.gameObject.name,
|
||||
instanceID = col.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = col.GetType().Name
|
||||
});
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Overlap query found {colliders.Count} collider(s) ({dimension}).",
|
||||
data = new { colliders }
|
||||
};
|
||||
}
|
||||
|
||||
private static object FormatOverlapResults2D(Collider2D[] results, string dimension)
|
||||
{
|
||||
var colliders = new List<object>();
|
||||
foreach (var col in results)
|
||||
{
|
||||
colliders.Add(new
|
||||
{
|
||||
gameObject = col.gameObject.name,
|
||||
instanceID = col.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = col.GetType().Name
|
||||
});
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Overlap query found {colliders.Count} collider(s) ({dimension}).",
|
||||
data = new { colliders }
|
||||
};
|
||||
}
|
||||
|
||||
public static object Shapecast(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
string shape = p.Get("shape");
|
||||
if (string.IsNullOrEmpty(shape))
|
||||
return new ErrorResponse("'shape' parameter is required (sphere, box, capsule for 3D; circle, box, capsule for 2D).");
|
||||
|
||||
var originArr = p.GetRaw("origin") as JArray;
|
||||
if (originArr == null)
|
||||
return new ErrorResponse("'origin' parameter is required (array of floats).");
|
||||
|
||||
var dirArr = p.GetRaw("direction") as JArray;
|
||||
if (dirArr == null)
|
||||
return new ErrorResponse("'direction' parameter is required (array of floats).");
|
||||
|
||||
float maxDistance = p.GetFloat("max_distance") ?? Mathf.Infinity;
|
||||
|
||||
if (dimension == "2d")
|
||||
return Shapecast2D(shape, originArr, dirArr, maxDistance, p);
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
return Shapecast3D(shape, originArr, dirArr, maxDistance, p);
|
||||
}
|
||||
|
||||
private static object Shapecast3D(string shape, JArray originArr, JArray dirArr, float maxDistance, ToolParams p)
|
||||
{
|
||||
if (originArr.Count < 3)
|
||||
return new ErrorResponse("3D shapecast 'origin' requires [x, y, z].");
|
||||
if (dirArr.Count < 3)
|
||||
return new ErrorResponse("3D shapecast 'direction' requires [x, y, z].");
|
||||
|
||||
var origin = new Vector3(
|
||||
originArr[0].Value<float>(), originArr[1].Value<float>(), originArr[2].Value<float>());
|
||||
var direction = new Vector3(
|
||||
dirArr[0].Value<float>(), dirArr[1].Value<float>(), dirArr[2].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal;
|
||||
string qti = p.Get("query_trigger_interaction");
|
||||
if (!string.IsNullOrEmpty(qti))
|
||||
{
|
||||
if (!System.Enum.TryParse(qti, true, out triggerInteraction))
|
||||
return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide.");
|
||||
}
|
||||
|
||||
var sizeToken = p.GetRaw("size");
|
||||
if (sizeToken == null)
|
||||
return new ErrorResponse("'size' parameter is required.");
|
||||
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
bool hit;
|
||||
RaycastHit hitInfo;
|
||||
|
||||
switch (shape.ToLowerInvariant())
|
||||
{
|
||||
case "sphere":
|
||||
{
|
||||
float radius = sizeToken.Value<float>();
|
||||
hit = UnityEngine.Physics.SphereCast(origin, radius, direction, out hitInfo, maxDistance, layerMask, triggerInteraction);
|
||||
break;
|
||||
}
|
||||
case "box":
|
||||
{
|
||||
Vector3 halfExtents;
|
||||
if (sizeToken is JArray sizeArr && sizeArr.Count >= 3)
|
||||
halfExtents = new Vector3(
|
||||
sizeArr[0].Value<float>(), sizeArr[1].Value<float>(), sizeArr[2].Value<float>());
|
||||
else
|
||||
return new ErrorResponse("3D box shapecast 'size' requires [halfX, halfY, halfZ].");
|
||||
hit = UnityEngine.Physics.BoxCast(origin, halfExtents, direction, out hitInfo, Quaternion.identity, maxDistance, layerMask, triggerInteraction);
|
||||
break;
|
||||
}
|
||||
case "capsule":
|
||||
{
|
||||
float radius = sizeToken.Value<float>();
|
||||
Vector3 pt1, pt2;
|
||||
|
||||
var pt1Arr = p.GetRaw("point1") as JArray;
|
||||
var pt2Arr = p.GetRaw("point2") as JArray;
|
||||
|
||||
if (pt1Arr != null && pt1Arr.Count >= 3 && pt2Arr != null && pt2Arr.Count >= 3)
|
||||
{
|
||||
pt1 = new Vector3(pt1Arr[0].Value<float>(), pt1Arr[1].Value<float>(), pt1Arr[2].Value<float>());
|
||||
pt2 = new Vector3(pt2Arr[0].Value<float>(), pt2Arr[1].Value<float>(), pt2Arr[2].Value<float>());
|
||||
}
|
||||
else
|
||||
{
|
||||
float height = p.GetFloat("height") ?? 2f;
|
||||
int capsuleDirection = p.GetInt("capsule_direction") ?? 1;
|
||||
float halfHeight = Mathf.Max(0, height / 2f - radius);
|
||||
|
||||
switch (capsuleDirection)
|
||||
{
|
||||
case 0: // X
|
||||
pt1 = origin + Vector3.left * halfHeight;
|
||||
pt2 = origin + Vector3.right * halfHeight;
|
||||
break;
|
||||
case 2: // Z
|
||||
pt1 = origin + Vector3.back * halfHeight;
|
||||
pt2 = origin + Vector3.forward * halfHeight;
|
||||
break;
|
||||
default: // Y (1)
|
||||
pt1 = origin + Vector3.down * halfHeight;
|
||||
pt2 = origin + Vector3.up * halfHeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hit = UnityEngine.Physics.CapsuleCast(pt1, pt2, radius, direction, out hitInfo, maxDistance, layerMask, triggerInteraction);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return new ErrorResponse($"Unknown 3D shape: '{shape}'. Valid: sphere, box, capsule.");
|
||||
}
|
||||
|
||||
if (!hit)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Shapecast did not hit anything.",
|
||||
data = new { hit = false }
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Shapecast hit '{hitInfo.collider.gameObject.name}'.",
|
||||
data = new
|
||||
{
|
||||
hit = true,
|
||||
point = new[] { hitInfo.point.x, hitInfo.point.y, hitInfo.point.z },
|
||||
normal = new[] { hitInfo.normal.x, hitInfo.normal.y, hitInfo.normal.z },
|
||||
distance = hitInfo.distance,
|
||||
gameObject = hitInfo.collider.gameObject.name,
|
||||
instanceID = hitInfo.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = hitInfo.collider.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object Shapecast2D(string shape, JArray originArr, JArray dirArr, float maxDistance, ToolParams p)
|
||||
{
|
||||
if (originArr.Count < 2)
|
||||
return new ErrorResponse("2D shapecast 'origin' requires [x, y].");
|
||||
if (dirArr.Count < 2)
|
||||
return new ErrorResponse("2D shapecast 'direction' requires [x, y].");
|
||||
|
||||
var origin = new Vector2(originArr[0].Value<float>(), originArr[1].Value<float>());
|
||||
var direction = new Vector2(dirArr[0].Value<float>(), dirArr[1].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
float angle = p.GetFloat("angle") ?? 0f;
|
||||
|
||||
var sizeToken = p.GetRaw("size");
|
||||
if (sizeToken == null)
|
||||
return new ErrorResponse("'size' parameter is required.");
|
||||
|
||||
Physics2D.SyncTransforms();
|
||||
|
||||
RaycastHit2D hit;
|
||||
|
||||
switch (shape.ToLowerInvariant())
|
||||
{
|
||||
case "circle":
|
||||
{
|
||||
float radius = sizeToken.Value<float>();
|
||||
hit = Physics2D.CircleCast(origin, radius, direction, maxDistance, layerMask);
|
||||
break;
|
||||
}
|
||||
case "box":
|
||||
{
|
||||
Vector2 size;
|
||||
if (sizeToken is JArray sizeArr && sizeArr.Count >= 2)
|
||||
size = new Vector2(sizeArr[0].Value<float>(), sizeArr[1].Value<float>());
|
||||
else
|
||||
return new ErrorResponse("2D box shapecast 'size' requires [width, height].");
|
||||
hit = Physics2D.BoxCast(origin, size, angle, direction, maxDistance, layerMask);
|
||||
break;
|
||||
}
|
||||
case "capsule":
|
||||
{
|
||||
if (sizeToken is JObject sizeObj)
|
||||
{
|
||||
var capsuleSize = new Vector2(
|
||||
sizeObj["width"]?.Value<float>() ?? 1f,
|
||||
sizeObj["height"]?.Value<float>() ?? 2f);
|
||||
var capsuleDir = CapsuleDirection2D.Vertical;
|
||||
string dirStr = sizeObj["direction"]?.ToString();
|
||||
if (dirStr != null && dirStr.ToLowerInvariant() == "horizontal")
|
||||
capsuleDir = CapsuleDirection2D.Horizontal;
|
||||
hit = Physics2D.CapsuleCast(origin, capsuleSize, capsuleDir, angle, direction, maxDistance, layerMask);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse("2D capsule shapecast 'size' requires {width, height, direction}.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return new ErrorResponse($"Unknown 2D shape: '{shape}'. Valid: circle, box, capsule.");
|
||||
}
|
||||
|
||||
if (hit.collider == null)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "2D Shapecast did not hit anything.",
|
||||
data = new { hit = false }
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"2D Shapecast hit '{hit.collider.gameObject.name}'.",
|
||||
data = new
|
||||
{
|
||||
hit = true,
|
||||
point = new[] { hit.point.x, hit.point.y },
|
||||
normal = new[] { hit.normal.x, hit.normal.y },
|
||||
distance = hit.distance,
|
||||
gameObject = hit.collider.gameObject.name,
|
||||
instanceID = hit.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = hit.collider.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object RaycastAll(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
var originArr = p.GetRaw("origin") as JArray;
|
||||
if (originArr == null)
|
||||
return new ErrorResponse("'origin' parameter is required (array of floats).");
|
||||
|
||||
var dirArr = p.GetRaw("direction") as JArray;
|
||||
if (dirArr == null)
|
||||
return new ErrorResponse("'direction' parameter is required (array of floats).");
|
||||
|
||||
float maxDistance = p.GetFloat("max_distance") ?? Mathf.Infinity;
|
||||
|
||||
if (dimension == "2d")
|
||||
return RaycastAll2D(originArr, dirArr, maxDistance, p);
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
return RaycastAll3D(originArr, dirArr, maxDistance, p);
|
||||
}
|
||||
|
||||
private static object RaycastAll3D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p)
|
||||
{
|
||||
if (originArr.Count < 3)
|
||||
return new ErrorResponse("3D RaycastAll 'origin' requires [x, y, z].");
|
||||
if (dirArr.Count < 3)
|
||||
return new ErrorResponse("3D RaycastAll 'direction' requires [x, y, z].");
|
||||
|
||||
var origin = new Vector3(
|
||||
originArr[0].Value<float>(), originArr[1].Value<float>(), originArr[2].Value<float>());
|
||||
var direction = new Vector3(
|
||||
dirArr[0].Value<float>(), dirArr[1].Value<float>(), dirArr[2].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal;
|
||||
string qti = p.Get("query_trigger_interaction");
|
||||
if (!string.IsNullOrEmpty(qti))
|
||||
{
|
||||
if (!System.Enum.TryParse(qti, true, out triggerInteraction))
|
||||
return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide.");
|
||||
}
|
||||
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
RaycastHit[] hits = UnityEngine.Physics.RaycastAll(origin, direction, maxDistance, layerMask, triggerInteraction);
|
||||
System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance));
|
||||
|
||||
var hitsArray = new List<object>();
|
||||
foreach (var h in hits)
|
||||
{
|
||||
hitsArray.Add(new
|
||||
{
|
||||
point = new[] { h.point.x, h.point.y, h.point.z },
|
||||
normal = new[] { h.normal.x, h.normal.y, h.normal.z },
|
||||
distance = h.distance,
|
||||
gameObject = h.collider.gameObject.name,
|
||||
instanceID = h.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = h.collider.GetType().Name
|
||||
});
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"RaycastAll found {hits.Length} hit(s).",
|
||||
data = new { hit_count = hits.Length, hits = hitsArray }
|
||||
};
|
||||
}
|
||||
|
||||
private static object RaycastAll2D(JArray originArr, JArray dirArr, float maxDistance, ToolParams p)
|
||||
{
|
||||
if (originArr.Count < 2)
|
||||
return new ErrorResponse("2D RaycastAll 'origin' requires [x, y].");
|
||||
if (dirArr.Count < 2)
|
||||
return new ErrorResponse("2D RaycastAll 'direction' requires [x, y].");
|
||||
|
||||
var origin = new Vector2(originArr[0].Value<float>(), originArr[1].Value<float>());
|
||||
var direction = new Vector2(dirArr[0].Value<float>(), dirArr[1].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
Physics2D.SyncTransforms();
|
||||
|
||||
RaycastHit2D[] hits = Physics2D.RaycastAll(origin, direction, maxDistance, layerMask);
|
||||
|
||||
var hitsArray = new List<object>();
|
||||
foreach (var h in hits)
|
||||
{
|
||||
hitsArray.Add(new
|
||||
{
|
||||
point = new[] { h.point.x, h.point.y },
|
||||
normal = new[] { h.normal.x, h.normal.y },
|
||||
distance = h.distance,
|
||||
gameObject = h.collider.gameObject.name,
|
||||
instanceID = h.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = h.collider.GetType().Name
|
||||
});
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"RaycastAll found {hits.Length} hit(s).",
|
||||
data = new { hit_count = hits.Length, hits = hitsArray }
|
||||
};
|
||||
}
|
||||
|
||||
public static object Linecast(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
var startArr = p.GetRaw("start") as JArray;
|
||||
if (startArr == null)
|
||||
return new ErrorResponse("'start' parameter is required (array of floats).");
|
||||
|
||||
var endArr = p.GetRaw("end") as JArray;
|
||||
if (endArr == null)
|
||||
return new ErrorResponse("'end' parameter is required (array of floats).");
|
||||
|
||||
if (dimension == "2d")
|
||||
return Linecast2D(startArr, endArr, p);
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
return Linecast3D(startArr, endArr, p);
|
||||
}
|
||||
|
||||
private static object Linecast3D(JArray startArr, JArray endArr, ToolParams p)
|
||||
{
|
||||
if (startArr.Count < 3)
|
||||
return new ErrorResponse("3D linecast 'start' requires [x, y, z].");
|
||||
if (endArr.Count < 3)
|
||||
return new ErrorResponse("3D linecast 'end' requires [x, y, z].");
|
||||
|
||||
var start = new Vector3(
|
||||
startArr[0].Value<float>(), startArr[1].Value<float>(), startArr[2].Value<float>());
|
||||
var end = new Vector3(
|
||||
endArr[0].Value<float>(), endArr[1].Value<float>(), endArr[2].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal;
|
||||
string qti = p.Get("query_trigger_interaction");
|
||||
if (!string.IsNullOrEmpty(qti))
|
||||
{
|
||||
if (!System.Enum.TryParse(qti, true, out triggerInteraction))
|
||||
return new ErrorResponse($"Invalid query_trigger_interaction: '{qti}'. Valid: UseGlobal, Ignore, Collide.");
|
||||
}
|
||||
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
|
||||
bool hit = UnityEngine.Physics.Linecast(start, end, out RaycastHit hitInfo, layerMask, triggerInteraction);
|
||||
|
||||
if (!hit)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Linecast did not hit anything.",
|
||||
data = new { hit = false }
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Linecast hit '{hitInfo.collider.gameObject.name}'.",
|
||||
data = new
|
||||
{
|
||||
hit = true,
|
||||
point = new[] { hitInfo.point.x, hitInfo.point.y, hitInfo.point.z },
|
||||
normal = new[] { hitInfo.normal.x, hitInfo.normal.y, hitInfo.normal.z },
|
||||
distance = hitInfo.distance,
|
||||
gameObject = hitInfo.collider.gameObject.name,
|
||||
instanceID = hitInfo.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = hitInfo.collider.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object Linecast2D(JArray startArr, JArray endArr, ToolParams p)
|
||||
{
|
||||
if (startArr.Count < 2)
|
||||
return new ErrorResponse("2D linecast 'start' requires [x, y].");
|
||||
if (endArr.Count < 2)
|
||||
return new ErrorResponse("2D linecast 'end' requires [x, y].");
|
||||
|
||||
var start = new Vector2(startArr[0].Value<float>(), startArr[1].Value<float>());
|
||||
var end = new Vector2(endArr[0].Value<float>(), endArr[1].Value<float>());
|
||||
|
||||
int layerMask = ResolveLayerMask(p.Get("layer_mask"));
|
||||
|
||||
Physics2D.SyncTransforms();
|
||||
|
||||
var hit = Physics2D.Linecast(start, end, layerMask);
|
||||
|
||||
if (hit.collider == null)
|
||||
{
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "2D Linecast did not hit anything.",
|
||||
data = new { hit = false }
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"2D Linecast hit '{hit.collider.gameObject.name}'.",
|
||||
data = new
|
||||
{
|
||||
hit = true,
|
||||
point = new[] { hit.point.x, hit.point.y },
|
||||
normal = new[] { hit.normal.x, hit.normal.y },
|
||||
distance = hit.distance,
|
||||
gameObject = hit.collider.gameObject.name,
|
||||
instanceID = hit.collider.gameObject.GetInstanceIDCompat(),
|
||||
collider_type = hit.collider.GetType().Name
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static int ResolveLayerMask(string layerMaskStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(layerMaskStr))
|
||||
return ~0; // All layers
|
||||
|
||||
if (int.TryParse(layerMaskStr, out int mask))
|
||||
return mask;
|
||||
|
||||
int layer = LayerMask.NameToLayer(layerMaskStr);
|
||||
if (layer >= 0)
|
||||
return 1 << layer;
|
||||
|
||||
throw new ArgumentException($"Unknown layer name: '{layerMaskStr}'. Use a valid layer name or integer mask.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ff935d0645248f795cd0fa73e784be3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,437 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class PhysicsRigidbodyOps
|
||||
{
|
||||
private static readonly HashSet<string> Valid3DKeys = new HashSet<string>
|
||||
{
|
||||
"mass", "drag", "lineardamping", "angulardrag", "angulardamping",
|
||||
"usegravity", "iskinematic", "interpolation", "collisiondetectionmode", "constraints"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> Valid2DKeys = new HashSet<string>
|
||||
{
|
||||
"mass", "gravityscale", "drag", "lineardamping", "angulardrag", "angulardamping",
|
||||
"bodytype", "simulated", "collisiondetectionmode", "constraints"
|
||||
};
|
||||
|
||||
public static object GetRigidbody(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetToken = p.GetRaw("target");
|
||||
if (targetToken == null || string.IsNullOrEmpty(targetToken.ToString()))
|
||||
return new ErrorResponse("'target' parameter is required.");
|
||||
|
||||
string searchMethod = p.Get("search_method");
|
||||
string dimensionParam = p.Get("dimension")?.ToLowerInvariant();
|
||||
|
||||
var go = GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name");
|
||||
if (go == null)
|
||||
return new ErrorResponse($"GameObject not found: '{targetToken}'.");
|
||||
|
||||
bool has3D = go.GetComponent<Rigidbody>() != null;
|
||||
bool has2D = go.GetComponent<Rigidbody2D>() != null;
|
||||
|
||||
if (!has3D && !has2D)
|
||||
return new ErrorResponse($"No Rigidbody or Rigidbody2D found on '{go.name}'.");
|
||||
|
||||
bool is2D;
|
||||
if (dimensionParam == "2d")
|
||||
is2D = true;
|
||||
else if (dimensionParam == "3d")
|
||||
is2D = false;
|
||||
else
|
||||
is2D = has2D && !has3D;
|
||||
|
||||
if (is2D)
|
||||
return GetRigidbody2D(go);
|
||||
|
||||
return GetRigidbody3D(go);
|
||||
}
|
||||
|
||||
private static object GetRigidbody3D(GameObject go)
|
||||
{
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
return new ErrorResponse($"No Rigidbody found on '{go.name}'.");
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["target"] = go.name,
|
||||
["instanceID"] = go.GetInstanceIDCompat(),
|
||||
["dimension"] = "3d",
|
||||
["mass"] = rb.mass,
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
["linearDamping"] = rb.linearDamping,
|
||||
["angularDamping"] = rb.angularDamping,
|
||||
#else
|
||||
["linearDamping"] = rb.drag,
|
||||
["angularDamping"] = rb.angularDrag,
|
||||
#endif
|
||||
["useGravity"] = rb.useGravity,
|
||||
["isKinematic"] = rb.isKinematic,
|
||||
["position"] = new[] { rb.position.x, rb.position.y, rb.position.z },
|
||||
["rotation"] = new[] { rb.rotation.x, rb.rotation.y, rb.rotation.z, rb.rotation.w },
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
["velocity"] = new[] { rb.linearVelocity.x, rb.linearVelocity.y, rb.linearVelocity.z },
|
||||
#else
|
||||
["velocity"] = new[] { rb.velocity.x, rb.velocity.y, rb.velocity.z },
|
||||
#endif
|
||||
["angularVelocity"] = new[] { rb.angularVelocity.x, rb.angularVelocity.y, rb.angularVelocity.z },
|
||||
["interpolation"] = rb.interpolation.ToString(),
|
||||
["collisionDetectionMode"] = rb.collisionDetectionMode.ToString(),
|
||||
["constraints"] = rb.constraints.ToString(),
|
||||
["isSleeping"] = rb.IsSleeping(),
|
||||
["centerOfMass"] = new[] { rb.centerOfMass.x, rb.centerOfMass.y, rb.centerOfMass.z },
|
||||
["maxAngularVelocity"] = rb.maxAngularVelocity,
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
["maxLinearVelocity"] = rb.maxLinearVelocity,
|
||||
#endif
|
||||
};
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Retrieved Rigidbody state for '{go.name}'.",
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
private static object GetRigidbody2D(GameObject go)
|
||||
{
|
||||
var rb2d = go.GetComponent<Rigidbody2D>();
|
||||
if (rb2d == null)
|
||||
return new ErrorResponse($"No Rigidbody2D found on '{go.name}'.");
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["target"] = go.name,
|
||||
["instanceID"] = go.GetInstanceIDCompat(),
|
||||
["dimension"] = "2d",
|
||||
["mass"] = rb2d.mass,
|
||||
["gravityScale"] = rb2d.gravityScale,
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
["linearDamping"] = rb2d.linearDamping,
|
||||
["angularDamping"] = rb2d.angularDamping,
|
||||
#else
|
||||
["linearDamping"] = rb2d.drag,
|
||||
["angularDamping"] = rb2d.angularDrag,
|
||||
#endif
|
||||
["bodyType"] = rb2d.bodyType.ToString(),
|
||||
["simulated"] = rb2d.simulated,
|
||||
["position"] = new[] { rb2d.position.x, rb2d.position.y },
|
||||
["rotation"] = rb2d.rotation,
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
["velocity"] = new[] { rb2d.linearVelocity.x, rb2d.linearVelocity.y },
|
||||
#else
|
||||
["velocity"] = new[] { rb2d.velocity.x, rb2d.velocity.y },
|
||||
#endif
|
||||
["angularVelocity"] = rb2d.angularVelocity,
|
||||
["collisionDetectionMode"] = rb2d.collisionDetectionMode.ToString(),
|
||||
["constraints"] = rb2d.constraints.ToString(),
|
||||
["isSleeping"] = rb2d.IsSleeping(),
|
||||
["isAwake"] = rb2d.IsAwake(),
|
||||
["centerOfMass"] = new[] { rb2d.centerOfMass.x, rb2d.centerOfMass.y },
|
||||
};
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Retrieved Rigidbody2D state for '{go.name}'.",
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
public static object ConfigureRigidbody(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
|
||||
var targetToken = p.GetRaw("target");
|
||||
if (targetToken == null || string.IsNullOrEmpty(targetToken.ToString()))
|
||||
return new ErrorResponse("'target' parameter is required.");
|
||||
|
||||
var properties = p.GetRaw("properties") as JObject;
|
||||
if (properties == null || properties.Count == 0)
|
||||
return new ErrorResponse("'properties' parameter is required and must be a non-empty object.");
|
||||
|
||||
string searchMethod = p.Get("search_method");
|
||||
string dimensionParam = p.Get("dimension")?.ToLowerInvariant();
|
||||
|
||||
var go = GameObjectLookup.FindByTarget(targetToken, searchMethod ?? "by_name");
|
||||
if (go == null)
|
||||
return new ErrorResponse($"GameObject not found: '{targetToken}'.");
|
||||
|
||||
bool has3D = go.GetComponent<Rigidbody>() != null;
|
||||
bool has2D = go.GetComponent<Rigidbody2D>() != null;
|
||||
bool is2D;
|
||||
|
||||
if (dimensionParam == "2d")
|
||||
is2D = true;
|
||||
else if (dimensionParam == "3d")
|
||||
is2D = false;
|
||||
else
|
||||
is2D = has2D && !has3D;
|
||||
|
||||
if (is2D)
|
||||
return ConfigureRigidbody2D(go, properties);
|
||||
|
||||
return ConfigureRigidbody3D(go, properties);
|
||||
}
|
||||
|
||||
private static object ConfigureRigidbody3D(GameObject go, JObject properties)
|
||||
{
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
if (rb == null)
|
||||
return new ErrorResponse($"No Rigidbody found on '{go.name}'.");
|
||||
|
||||
// Validate all keys before applying any changes
|
||||
var unknown = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
if (!Valid3DKeys.Contains(key))
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
if (unknown.Count > 0)
|
||||
return new ErrorResponse(
|
||||
$"Unknown Rigidbody property(ies): {string.Join(", ", unknown)}.");
|
||||
|
||||
Undo.RecordObject(rb, "Configure Rigidbody");
|
||||
|
||||
var changed = new List<string>();
|
||||
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
switch (key)
|
||||
{
|
||||
case "mass":
|
||||
rb.mass = prop.Value.Value<float>();
|
||||
changed.Add("mass");
|
||||
break;
|
||||
case "drag":
|
||||
case "lineardamping":
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
rb.linearDamping = prop.Value.Value<float>();
|
||||
#else
|
||||
rb.drag = prop.Value.Value<float>();
|
||||
#endif
|
||||
changed.Add("linearDamping");
|
||||
break;
|
||||
case "angulardrag":
|
||||
case "angulardamping":
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
rb.angularDamping = prop.Value.Value<float>();
|
||||
#else
|
||||
rb.angularDrag = prop.Value.Value<float>();
|
||||
#endif
|
||||
changed.Add("angularDamping");
|
||||
break;
|
||||
case "usegravity":
|
||||
rb.useGravity = prop.Value.Value<bool>();
|
||||
changed.Add("useGravity");
|
||||
break;
|
||||
case "iskinematic":
|
||||
rb.isKinematic = prop.Value.Value<bool>();
|
||||
changed.Add("isKinematic");
|
||||
break;
|
||||
case "interpolation":
|
||||
{
|
||||
string val = prop.Value.ToString();
|
||||
if (Enum.TryParse<RigidbodyInterpolation>(val, true, out var interp))
|
||||
{
|
||||
rb.interpolation = interp;
|
||||
changed.Add("interpolation");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid interpolation value: '{val}'. Valid: None, Interpolate, Extrapolate.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "collisiondetectionmode":
|
||||
{
|
||||
string val = prop.Value.ToString();
|
||||
if (Enum.TryParse<CollisionDetectionMode>(val, true, out var mode))
|
||||
{
|
||||
rb.collisionDetectionMode = mode;
|
||||
changed.Add("collisionDetectionMode");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid collisionDetectionMode: '{val}'. Valid: Discrete, Continuous, ContinuousDynamic, ContinuousSpeculative.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "constraints":
|
||||
{
|
||||
var token = prop.Value;
|
||||
if (token.Type == JTokenType.Integer)
|
||||
{
|
||||
rb.constraints = (RigidbodyConstraints)token.Value<int>();
|
||||
changed.Add("constraints");
|
||||
}
|
||||
else
|
||||
{
|
||||
string val = token.ToString();
|
||||
if (Enum.TryParse<RigidbodyConstraints>(val, true, out var c))
|
||||
{
|
||||
rb.constraints = c;
|
||||
changed.Add("constraints");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid constraints value: '{val}'. Use enum name (e.g. 'FreezePositionX, FreezeRotationY') or int flags.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(rb);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Configured Rigidbody on '{go.name}': {string.Join(", ", changed)}.",
|
||||
data = new { target = go.name, dimension = "3d", changed }
|
||||
};
|
||||
}
|
||||
|
||||
private static object ConfigureRigidbody2D(GameObject go, JObject properties)
|
||||
{
|
||||
var rb2d = go.GetComponent<Rigidbody2D>();
|
||||
if (rb2d == null)
|
||||
return new ErrorResponse($"No Rigidbody2D found on '{go.name}'.");
|
||||
|
||||
// Validate all keys before applying any changes
|
||||
var unknown = new List<string>();
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
if (!Valid2DKeys.Contains(key))
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
if (unknown.Count > 0)
|
||||
return new ErrorResponse(
|
||||
$"Unknown Rigidbody2D property(ies): {string.Join(", ", unknown)}.");
|
||||
|
||||
Undo.RecordObject(rb2d, "Configure Rigidbody2D");
|
||||
|
||||
var changed = new List<string>();
|
||||
|
||||
foreach (var prop in properties.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant().Replace("_", "");
|
||||
switch (key)
|
||||
{
|
||||
case "mass":
|
||||
rb2d.mass = prop.Value.Value<float>();
|
||||
changed.Add("mass");
|
||||
break;
|
||||
case "gravityscale":
|
||||
rb2d.gravityScale = prop.Value.Value<float>();
|
||||
changed.Add("gravityScale");
|
||||
break;
|
||||
case "drag":
|
||||
case "lineardamping":
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
rb2d.linearDamping = prop.Value.Value<float>();
|
||||
#else
|
||||
rb2d.drag = prop.Value.Value<float>();
|
||||
#endif
|
||||
changed.Add("linearDamping");
|
||||
break;
|
||||
case "angulardrag":
|
||||
case "angulardamping":
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
rb2d.angularDamping = prop.Value.Value<float>();
|
||||
#else
|
||||
rb2d.angularDrag = prop.Value.Value<float>();
|
||||
#endif
|
||||
changed.Add("angularDamping");
|
||||
break;
|
||||
case "bodytype":
|
||||
{
|
||||
string val = prop.Value.ToString();
|
||||
if (Enum.TryParse<RigidbodyType2D>(val, true, out var bt))
|
||||
{
|
||||
rb2d.bodyType = bt;
|
||||
changed.Add("bodyType");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid bodyType: '{val}'. Valid: Dynamic, Kinematic, Static.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "simulated":
|
||||
rb2d.simulated = prop.Value.Value<bool>();
|
||||
changed.Add("simulated");
|
||||
break;
|
||||
case "collisiondetectionmode":
|
||||
{
|
||||
string val = prop.Value.ToString();
|
||||
if (Enum.TryParse<CollisionDetectionMode2D>(val, true, out var mode))
|
||||
{
|
||||
rb2d.collisionDetectionMode = mode;
|
||||
changed.Add("collisionDetectionMode");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid collisionDetectionMode: '{val}'. Valid: Discrete, Continuous.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "constraints":
|
||||
{
|
||||
var token = prop.Value;
|
||||
if (token.Type == JTokenType.Integer)
|
||||
{
|
||||
rb2d.constraints = (RigidbodyConstraints2D)token.Value<int>();
|
||||
changed.Add("constraints");
|
||||
}
|
||||
else
|
||||
{
|
||||
string val = token.ToString();
|
||||
if (Enum.TryParse<RigidbodyConstraints2D>(val, true, out var c))
|
||||
{
|
||||
rb2d.constraints = c;
|
||||
changed.Add("constraints");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid constraints value: '{val}'. Use enum name (e.g. 'FreezePositionX, FreezeRotation') or int flags.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(rb2d);
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Configured Rigidbody2D on '{go.name}': {string.Join(", ", changed)}.",
|
||||
data = new { target = go.name, dimension = "2d", changed }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a1859cc8a9e4c8799fab7e911151227
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,306 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class PhysicsSettingsOps
|
||||
{
|
||||
public static object Ping(JObject @params)
|
||||
{
|
||||
var gravity3d = UnityEngine.Physics.gravity;
|
||||
var gravity2d = Physics2D.gravity;
|
||||
var simMode = UnityPhysicsCompat.GetPhysicsSimulationMode().ToString();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Physics tool ready.",
|
||||
data = new
|
||||
{
|
||||
gravity3d = new[] { gravity3d.x, gravity3d.y, gravity3d.z },
|
||||
gravity2d = new[] { gravity2d.x, gravity2d.y },
|
||||
simulationMode = simMode,
|
||||
defaultSolverIterations = UnityEngine.Physics.defaultSolverIterations,
|
||||
defaultSolverVelocityIterations = UnityEngine.Physics.defaultSolverVelocityIterations,
|
||||
bounceThreshold = UnityEngine.Physics.bounceThreshold,
|
||||
sleepThreshold = UnityEngine.Physics.sleepThreshold,
|
||||
defaultContactOffset = UnityEngine.Physics.defaultContactOffset,
|
||||
queriesHitTriggers = UnityEngine.Physics.queriesHitTriggers
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object GetSettings(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
|
||||
if (dimension == "2d")
|
||||
{
|
||||
var g = Physics2D.gravity;
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Physics2D settings retrieved.",
|
||||
data = new
|
||||
{
|
||||
dimension = "2d",
|
||||
gravity = new[] { g.x, g.y },
|
||||
velocityIterations = Physics2D.velocityIterations,
|
||||
positionIterations = Physics2D.positionIterations,
|
||||
queriesHitTriggers = Physics2D.queriesHitTriggers,
|
||||
queriesStartInColliders = Physics2D.queriesStartInColliders,
|
||||
callbacksOnDisable = Physics2D.callbacksOnDisable,
|
||||
autoSyncTransforms = UnityPhysicsCompat.GetPhysics2DAutoSyncTransforms()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (dimension != "3d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
var g3 = UnityEngine.Physics.gravity;
|
||||
var simMode = UnityPhysicsCompat.GetPhysicsSimulationMode().ToString();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = "Physics3D settings retrieved.",
|
||||
data = new
|
||||
{
|
||||
dimension = "3d",
|
||||
gravity = new[] { g3.x, g3.y, g3.z },
|
||||
defaultContactOffset = UnityEngine.Physics.defaultContactOffset,
|
||||
sleepThreshold = UnityEngine.Physics.sleepThreshold,
|
||||
defaultSolverIterations = UnityEngine.Physics.defaultSolverIterations,
|
||||
defaultSolverVelocityIterations = UnityEngine.Physics.defaultSolverVelocityIterations,
|
||||
bounceThreshold = UnityEngine.Physics.bounceThreshold,
|
||||
defaultMaxAngularSpeed = UnityEngine.Physics.defaultMaxAngularSpeed,
|
||||
queriesHitTriggers = UnityEngine.Physics.queriesHitTriggers,
|
||||
queriesHitBackfaces = UnityEngine.Physics.queriesHitBackfaces,
|
||||
simulationMode = simMode,
|
||||
autoSyncTransforms = UnityPhysicsCompat.GetPhysicsAutoSyncTransforms()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static object SetSettings(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
var settings = p.GetRaw("settings") as JObject;
|
||||
|
||||
if (settings == null || settings.Count == 0)
|
||||
return new ErrorResponse("'settings' parameter is required and must be a non-empty object.");
|
||||
|
||||
if (dimension != "3d" && dimension != "2d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
if (dimension == "2d")
|
||||
return SetSettings2D(settings);
|
||||
|
||||
return SetSettings3D(settings);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> Valid3DKeys = new HashSet<string>
|
||||
{
|
||||
"gravity", "defaultcontactoffset", "sleepthreshold",
|
||||
"defaultsolveriterations", "defaultsolvervelocityiterations",
|
||||
"bouncethreshold", "defaultmaxangularspeed",
|
||||
"querieshittriggers", "querieshitbackfaces", "simulationmode",
|
||||
"autosynctransforms"
|
||||
};
|
||||
|
||||
private static object SetSettings3D(JObject settings)
|
||||
{
|
||||
// Validate all keys before applying any changes
|
||||
var unknown = new List<string>();
|
||||
foreach (var prop in settings.Properties())
|
||||
{
|
||||
if (!Valid3DKeys.Contains(prop.Name.ToLowerInvariant()))
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
if (unknown.Count > 0)
|
||||
return new ErrorResponse(
|
||||
$"Unknown 3D physics setting(s): {string.Join(", ", unknown)}.");
|
||||
|
||||
var changed = new List<string>();
|
||||
|
||||
foreach (var prop in settings.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant();
|
||||
switch (key)
|
||||
{
|
||||
case "gravity":
|
||||
{
|
||||
var arr = prop.Value as JArray;
|
||||
if (arr == null || arr.Count < 3)
|
||||
return new ErrorResponse("3D gravity requires [x, y, z] array.");
|
||||
UnityEngine.Physics.gravity = new Vector3(
|
||||
arr[0].Value<float>(), arr[1].Value<float>(), arr[2].Value<float>());
|
||||
changed.Add("gravity");
|
||||
break;
|
||||
}
|
||||
case "defaultcontactoffset":
|
||||
UnityEngine.Physics.defaultContactOffset = prop.Value.Value<float>();
|
||||
changed.Add("defaultContactOffset");
|
||||
break;
|
||||
case "sleepthreshold":
|
||||
UnityEngine.Physics.sleepThreshold = prop.Value.Value<float>();
|
||||
changed.Add("sleepThreshold");
|
||||
break;
|
||||
case "defaultsolveriterations":
|
||||
UnityEngine.Physics.defaultSolverIterations = prop.Value.Value<int>();
|
||||
changed.Add("defaultSolverIterations");
|
||||
break;
|
||||
case "defaultsolvervelocityiterations":
|
||||
UnityEngine.Physics.defaultSolverVelocityIterations = prop.Value.Value<int>();
|
||||
changed.Add("defaultSolverVelocityIterations");
|
||||
break;
|
||||
case "bouncethreshold":
|
||||
UnityEngine.Physics.bounceThreshold = prop.Value.Value<float>();
|
||||
changed.Add("bounceThreshold");
|
||||
break;
|
||||
case "defaultmaxangularspeed":
|
||||
UnityEngine.Physics.defaultMaxAngularSpeed = prop.Value.Value<float>();
|
||||
changed.Add("defaultMaxAngularSpeed");
|
||||
break;
|
||||
case "querieshittriggers":
|
||||
UnityEngine.Physics.queriesHitTriggers = prop.Value.Value<bool>();
|
||||
changed.Add("queriesHitTriggers");
|
||||
break;
|
||||
case "querieshitbackfaces":
|
||||
UnityEngine.Physics.queriesHitBackfaces = prop.Value.Value<bool>();
|
||||
changed.Add("queriesHitBackfaces");
|
||||
break;
|
||||
case "simulationmode":
|
||||
{
|
||||
string modeStr = prop.Value.ToString();
|
||||
if (!System.Enum.TryParse<UnityPhysicsCompat.SimulationMode>(modeStr, true, out var mode)
|
||||
|| mode == UnityPhysicsCompat.SimulationMode.Unknown)
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"Invalid simulationMode: '{modeStr}'. Valid: FixedUpdate, Update, Script.");
|
||||
}
|
||||
if (!UnityPhysicsCompat.TrySetPhysicsSimulationMode(mode))
|
||||
{
|
||||
return new ErrorResponse(
|
||||
$"simulationMode '{modeStr}' is not supported on this Unity version.");
|
||||
}
|
||||
changed.Add("simulationMode");
|
||||
break;
|
||||
}
|
||||
case "autosynctransforms":
|
||||
if (UnityPhysicsCompat.TrySetPhysicsAutoSyncTransforms(prop.Value.Value<bool>()))
|
||||
{
|
||||
changed.Add("autoSyncTransforms");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MarkDynamicsManagerDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Updated {changed.Count} physics 3D setting(s).",
|
||||
data = new { changed }
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> Valid2DKeys = new HashSet<string>
|
||||
{
|
||||
"gravity", "velocityiterations", "positioniterations",
|
||||
"querieshittriggers", "queriesstartincolliders",
|
||||
"callbacksondisable", "autosynctransforms"
|
||||
};
|
||||
|
||||
private static object SetSettings2D(JObject settings)
|
||||
{
|
||||
// Validate all keys before applying any changes
|
||||
var unknown = new List<string>();
|
||||
foreach (var prop in settings.Properties())
|
||||
{
|
||||
if (!Valid2DKeys.Contains(prop.Name.ToLowerInvariant()))
|
||||
unknown.Add(prop.Name);
|
||||
}
|
||||
if (unknown.Count > 0)
|
||||
return new ErrorResponse(
|
||||
$"Unknown 2D physics setting(s): {string.Join(", ", unknown)}.");
|
||||
|
||||
var changed = new List<string>();
|
||||
|
||||
foreach (var prop in settings.Properties())
|
||||
{
|
||||
string key = prop.Name.ToLowerInvariant();
|
||||
switch (key)
|
||||
{
|
||||
case "gravity":
|
||||
{
|
||||
var arr = prop.Value as JArray;
|
||||
if (arr == null || arr.Count < 2)
|
||||
return new ErrorResponse("2D gravity requires [x, y] array.");
|
||||
Physics2D.gravity = new Vector2(
|
||||
arr[0].Value<float>(), arr[1].Value<float>());
|
||||
changed.Add("gravity");
|
||||
break;
|
||||
}
|
||||
case "velocityiterations":
|
||||
Physics2D.velocityIterations = prop.Value.Value<int>();
|
||||
changed.Add("velocityIterations");
|
||||
break;
|
||||
case "positioniterations":
|
||||
Physics2D.positionIterations = prop.Value.Value<int>();
|
||||
changed.Add("positionIterations");
|
||||
break;
|
||||
case "querieshittriggers":
|
||||
Physics2D.queriesHitTriggers = prop.Value.Value<bool>();
|
||||
changed.Add("queriesHitTriggers");
|
||||
break;
|
||||
case "queriesstartincolliders":
|
||||
Physics2D.queriesStartInColliders = prop.Value.Value<bool>();
|
||||
changed.Add("queriesStartInColliders");
|
||||
break;
|
||||
case "callbacksondisable":
|
||||
Physics2D.callbacksOnDisable = prop.Value.Value<bool>();
|
||||
changed.Add("callbacksOnDisable");
|
||||
break;
|
||||
case "autosynctransforms":
|
||||
if (UnityPhysicsCompat.TrySetPhysics2DAutoSyncTransforms(prop.Value.Value<bool>()))
|
||||
{
|
||||
changed.Add("autoSyncTransforms");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MarkPhysics2DSettingsDirty();
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Updated {changed.Count} physics 2D setting(s).",
|
||||
data = new { changed }
|
||||
};
|
||||
}
|
||||
|
||||
private static void MarkDynamicsManagerDirty()
|
||||
{
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/DynamicsManager.asset");
|
||||
if (assets != null && assets.Length > 0)
|
||||
EditorUtility.SetDirty(assets[0]);
|
||||
}
|
||||
|
||||
private static void MarkPhysics2DSettingsDirty()
|
||||
{
|
||||
var assets = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/Physics2DSettings.asset");
|
||||
if (assets != null && assets.Length > 0)
|
||||
EditorUtility.SetDirty(assets[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd52d7a98bad4f54b23704757fa4e31e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,179 @@
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
using MCPForUnity.Runtime.Helpers;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.Physics
|
||||
{
|
||||
internal static class PhysicsSimulationOps
|
||||
{
|
||||
public static object SimulateStep(JObject @params)
|
||||
{
|
||||
var p = new ToolParams(@params);
|
||||
string dimension = (p.Get("dimension") ?? "3d").ToLowerInvariant();
|
||||
int steps = Mathf.Clamp(p.GetInt("steps") ?? 1, 1, 100);
|
||||
float stepSize = p.GetFloat("step_size") ?? Time.fixedDeltaTime;
|
||||
string targetStr = p.Get("target");
|
||||
string searchMethod = p.Get("search_method");
|
||||
|
||||
if (dimension != "3d" && dimension != "2d")
|
||||
return new ErrorResponse($"Invalid dimension: '{dimension}'. Use '3d' or '2d'.");
|
||||
|
||||
if (dimension == "2d")
|
||||
{
|
||||
Physics2D.SyncTransforms();
|
||||
for (int i = 0; i < steps; i++)
|
||||
Physics2D.Simulate(stepSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
var prevMode = UnityPhysicsCompat.GetPhysicsSimulationMode();
|
||||
if (prevMode != UnityPhysicsCompat.SimulationMode.Script)
|
||||
UnityPhysicsCompat.TrySetPhysicsSimulationMode(UnityPhysicsCompat.SimulationMode.Script);
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < steps; i++)
|
||||
UnityEngine.Physics.Simulate(stepSize);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (prevMode != UnityPhysicsCompat.SimulationMode.Unknown
|
||||
&& prevMode != UnityPhysicsCompat.SimulationMode.Script)
|
||||
{
|
||||
UnityPhysicsCompat.TrySetPhysicsSimulationMode(prevMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect rigidbody states after simulation
|
||||
List<object> rigidbodies;
|
||||
if (!string.IsNullOrEmpty(targetStr))
|
||||
{
|
||||
rigidbodies = CollectTargetRigidbody(targetStr, searchMethod, dimension);
|
||||
}
|
||||
else
|
||||
{
|
||||
rigidbodies = CollectActiveRigidbodies(dimension);
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
success = true,
|
||||
message = $"Executed {steps} physics step(s) ({dimension.ToUpper()}, step_size={stepSize:F4}s).",
|
||||
data = new
|
||||
{
|
||||
steps_executed = steps,
|
||||
step_size = stepSize,
|
||||
dimension,
|
||||
rigidbodies
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static List<object> CollectTargetRigidbody(string targetStr, string searchMethod, string dimension)
|
||||
{
|
||||
var results = new List<object>();
|
||||
var go = GameObjectLookup.FindByTarget(JToken.FromObject(targetStr), searchMethod ?? "by_name");
|
||||
if (go == null)
|
||||
return results;
|
||||
|
||||
if (dimension == "2d")
|
||||
{
|
||||
var rb2d = go.GetComponent<Rigidbody2D>();
|
||||
if (rb2d != null)
|
||||
{
|
||||
results.Add(new
|
||||
{
|
||||
name = go.name,
|
||||
instanceID = go.GetInstanceIDCompat(),
|
||||
position = new[] { rb2d.position.x, rb2d.position.y },
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
velocity = new[] { rb2d.linearVelocity.x, rb2d.linearVelocity.y },
|
||||
#else
|
||||
velocity = new[] { rb2d.velocity.x, rb2d.velocity.y },
|
||||
#endif
|
||||
angularVelocity = rb2d.angularVelocity
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var rb = go.GetComponent<Rigidbody>();
|
||||
if (rb != null)
|
||||
{
|
||||
results.Add(new
|
||||
{
|
||||
name = go.name,
|
||||
instanceID = go.GetInstanceIDCompat(),
|
||||
position = new[] { rb.position.x, rb.position.y, rb.position.z },
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
velocity = new[] { rb.linearVelocity.x, rb.linearVelocity.y, rb.linearVelocity.z },
|
||||
#else
|
||||
velocity = new[] { rb.velocity.x, rb.velocity.y, rb.velocity.z },
|
||||
#endif
|
||||
angularVelocity = new[] { rb.angularVelocity.x, rb.angularVelocity.y, rb.angularVelocity.z }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static List<object> CollectActiveRigidbodies(string dimension)
|
||||
{
|
||||
var results = new List<object>();
|
||||
const int maxResults = 50;
|
||||
|
||||
if (dimension == "2d")
|
||||
{
|
||||
var allRb2d = UnityFindObjectsCompat.FindAll<Rigidbody2D>();
|
||||
foreach (var rb2d in allRb2d)
|
||||
{
|
||||
if (results.Count >= maxResults) break;
|
||||
if (rb2d.bodyType == RigidbodyType2D.Static) continue;
|
||||
if (rb2d.IsSleeping()) continue;
|
||||
|
||||
results.Add(new
|
||||
{
|
||||
name = rb2d.gameObject.name,
|
||||
instanceID = rb2d.gameObject.GetInstanceIDCompat(),
|
||||
position = new[] { rb2d.position.x, rb2d.position.y },
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
velocity = new[] { rb2d.linearVelocity.x, rb2d.linearVelocity.y },
|
||||
#else
|
||||
velocity = new[] { rb2d.velocity.x, rb2d.velocity.y },
|
||||
#endif
|
||||
angularVelocity = rb2d.angularVelocity
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var allRb = UnityFindObjectsCompat.FindAll<Rigidbody>();
|
||||
foreach (var rb in allRb)
|
||||
{
|
||||
if (results.Count >= maxResults) break;
|
||||
if (rb.isKinematic) continue;
|
||||
if (rb.IsSleeping()) continue;
|
||||
|
||||
results.Add(new
|
||||
{
|
||||
name = rb.gameObject.name,
|
||||
instanceID = rb.gameObject.GetInstanceIDCompat(),
|
||||
position = new[] { rb.position.x, rb.position.y, rb.position.z },
|
||||
#if UNITY_6000_0_OR_NEWER
|
||||
velocity = new[] { rb.linearVelocity.x, rb.linearVelocity.y, rb.linearVelocity.z },
|
||||
#else
|
||||
velocity = new[] { rb.velocity.x, rb.velocity.y, rb.velocity.z },
|
||||
#endif
|
||||
angularVelocity = new[] { rb.angularVelocity.x, rb.angularVelocity.y, rb.angularVelocity.z }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c92b5fa2cfa491fb3d27996d0253617
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
@@ -0,0 +1,308 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8f89b02582b41d698a21d5fa78c3806
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
Reference in New Issue
Block a user