Update GameObject for two new features (#427)
Tackle the requests in Issue#267, tested and no bugs found. (1)LLM can duplicate an gameobject based on request (2)LLM can move a gameobject relative to another gameobjectmain
parent
aa47838bcd
commit
a69ce19a7b
|
|
@ -189,6 +189,10 @@ namespace MCPForUnity.Editor.Tools
|
||||||
return RemoveComponentFromTarget(@params, targetToken, searchMethod);
|
return RemoveComponentFromTarget(@params, targetToken, searchMethod);
|
||||||
case "set_component_property":
|
case "set_component_property":
|
||||||
return SetComponentPropertyOnTarget(@params, targetToken, searchMethod);
|
return SetComponentPropertyOnTarget(@params, targetToken, searchMethod);
|
||||||
|
case "duplicate":
|
||||||
|
return DuplicateGameObject(@params, targetToken, searchMethod);
|
||||||
|
case "move_relative":
|
||||||
|
return MoveRelativeToObject(@params, targetToken, searchMethod);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return new ErrorResponse($"Unknown action: '{action}'.");
|
return new ErrorResponse($"Unknown action: '{action}'.");
|
||||||
|
|
@ -898,6 +902,219 @@ namespace MCPForUnity.Editor.Tools
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duplicates a GameObject with all its properties, components, and children.
|
||||||
|
/// </summary>
|
||||||
|
private static object DuplicateGameObject(JObject @params, JToken targetToken, string searchMethod)
|
||||||
|
{
|
||||||
|
GameObject sourceGo = FindObjectInternal(targetToken, searchMethod);
|
||||||
|
if (sourceGo == null)
|
||||||
|
{
|
||||||
|
return new ErrorResponse(
|
||||||
|
$"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional parameters
|
||||||
|
string newName = @params["new_name"]?.ToString();
|
||||||
|
Vector3? position = ParseVector3(@params["position"] as JArray);
|
||||||
|
Vector3? offset = ParseVector3(@params["offset"] as JArray);
|
||||||
|
JToken parentToken = @params["parent"];
|
||||||
|
|
||||||
|
// Duplicate the object
|
||||||
|
GameObject duplicatedGo = UnityEngine.Object.Instantiate(sourceGo);
|
||||||
|
Undo.RegisterCreatedObjectUndo(duplicatedGo, $"Duplicate {sourceGo.name}");
|
||||||
|
|
||||||
|
// Set name (default: SourceName_Copy or SourceName (1))
|
||||||
|
if (!string.IsNullOrEmpty(newName))
|
||||||
|
{
|
||||||
|
duplicatedGo.name = newName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Remove "(Clone)" suffix added by Instantiate and add "_Copy"
|
||||||
|
duplicatedGo.name = sourceGo.name.Replace("(Clone)", "").Trim() + "_Copy";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle positioning
|
||||||
|
if (position.HasValue)
|
||||||
|
{
|
||||||
|
// Absolute position specified
|
||||||
|
duplicatedGo.transform.position = position.Value;
|
||||||
|
}
|
||||||
|
else if (offset.HasValue)
|
||||||
|
{
|
||||||
|
// Offset from original
|
||||||
|
duplicatedGo.transform.position = sourceGo.transform.position + offset.Value;
|
||||||
|
}
|
||||||
|
// else: keeps the same position as the original (default Instantiate behavior)
|
||||||
|
|
||||||
|
// Handle parent
|
||||||
|
if (parentToken != null)
|
||||||
|
{
|
||||||
|
if (parentToken.Type == JTokenType.Null ||
|
||||||
|
(parentToken.Type == JTokenType.String && string.IsNullOrEmpty(parentToken.ToString())))
|
||||||
|
{
|
||||||
|
// Explicit null parent - move to root
|
||||||
|
duplicatedGo.transform.SetParent(null);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GameObject newParent = FindObjectInternal(parentToken, "by_id_or_name_or_path");
|
||||||
|
if (newParent != null)
|
||||||
|
{
|
||||||
|
duplicatedGo.transform.SetParent(newParent.transform, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ManageGameObject.Duplicate] Parent '{parentToken}' not found. Keeping original parent.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default: same parent as source
|
||||||
|
duplicatedGo.transform.SetParent(sourceGo.transform.parent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark scene dirty
|
||||||
|
EditorUtility.SetDirty(duplicatedGo);
|
||||||
|
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||||
|
|
||||||
|
Selection.activeGameObject = duplicatedGo;
|
||||||
|
|
||||||
|
return new SuccessResponse(
|
||||||
|
$"Duplicated '{sourceGo.name}' as '{duplicatedGo.name}'.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
originalName = sourceGo.name,
|
||||||
|
originalId = sourceGo.GetInstanceID(),
|
||||||
|
duplicatedObject = Helpers.GameObjectSerializer.GetGameObjectData(duplicatedGo)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a GameObject relative to another reference object.
|
||||||
|
/// Supports directional offsets (left, right, up, down, forward, back) and distance.
|
||||||
|
/// </summary>
|
||||||
|
private static object MoveRelativeToObject(JObject @params, JToken targetToken, string searchMethod)
|
||||||
|
{
|
||||||
|
GameObject targetGo = FindObjectInternal(targetToken, searchMethod);
|
||||||
|
if (targetGo == null)
|
||||||
|
{
|
||||||
|
return new ErrorResponse(
|
||||||
|
$"Target GameObject ('{targetToken}') not found using method '{searchMethod ?? "default"}'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get reference object (required for relative movement)
|
||||||
|
JToken referenceToken = @params["reference_object"];
|
||||||
|
if (referenceToken == null)
|
||||||
|
{
|
||||||
|
return new ErrorResponse("'reference_object' parameter is required for 'move_relative' action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
GameObject referenceGo = FindObjectInternal(referenceToken, "by_id_or_name_or_path");
|
||||||
|
if (referenceGo == null)
|
||||||
|
{
|
||||||
|
return new ErrorResponse($"Reference object '{referenceToken}' not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get movement parameters
|
||||||
|
string direction = @params["direction"]?.ToString()?.ToLower();
|
||||||
|
float distance = @params["distance"]?.ToObject<float>() ?? 1f;
|
||||||
|
Vector3? customOffset = ParseVector3(@params["offset"] as JArray);
|
||||||
|
bool useWorldSpace = @params["world_space"]?.ToObject<bool>() ?? true;
|
||||||
|
|
||||||
|
// Record for undo
|
||||||
|
Undo.RecordObject(targetGo.transform, $"Move {targetGo.name} relative to {referenceGo.name}");
|
||||||
|
|
||||||
|
Vector3 newPosition;
|
||||||
|
|
||||||
|
if (customOffset.HasValue)
|
||||||
|
{
|
||||||
|
// Custom offset vector provided
|
||||||
|
if (useWorldSpace)
|
||||||
|
{
|
||||||
|
newPosition = referenceGo.transform.position + customOffset.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Offset in reference object's local space
|
||||||
|
newPosition = referenceGo.transform.TransformPoint(customOffset.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(direction))
|
||||||
|
{
|
||||||
|
// Directional movement
|
||||||
|
Vector3 directionVector = GetDirectionVector(direction, referenceGo.transform, useWorldSpace);
|
||||||
|
newPosition = referenceGo.transform.position + directionVector * distance;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ErrorResponse("Either 'direction' or 'offset' parameter is required for 'move_relative' action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
targetGo.transform.position = newPosition;
|
||||||
|
|
||||||
|
// Mark scene dirty
|
||||||
|
EditorUtility.SetDirty(targetGo);
|
||||||
|
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
|
||||||
|
|
||||||
|
return new SuccessResponse(
|
||||||
|
$"Moved '{targetGo.name}' relative to '{referenceGo.name}'.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
movedObject = targetGo.name,
|
||||||
|
referenceObject = referenceGo.name,
|
||||||
|
newPosition = new[] { targetGo.transform.position.x, targetGo.transform.position.y, targetGo.transform.position.z },
|
||||||
|
direction = direction,
|
||||||
|
distance = distance,
|
||||||
|
gameObject = Helpers.GameObjectSerializer.GetGameObjectData(targetGo)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a direction string to a Vector3.
|
||||||
|
/// </summary>
|
||||||
|
private static Vector3 GetDirectionVector(string direction, Transform referenceTransform, bool useWorldSpace)
|
||||||
|
{
|
||||||
|
if (useWorldSpace)
|
||||||
|
{
|
||||||
|
// World space directions
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case "right": return Vector3.right;
|
||||||
|
case "left": return Vector3.left;
|
||||||
|
case "up": return Vector3.up;
|
||||||
|
case "down": return Vector3.down;
|
||||||
|
case "forward": case "front": return Vector3.forward;
|
||||||
|
case "back": case "backward": case "behind": return Vector3.back;
|
||||||
|
default:
|
||||||
|
Debug.LogWarning($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward.");
|
||||||
|
return Vector3.forward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Reference object's local space directions
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case "right": return referenceTransform.right;
|
||||||
|
case "left": return -referenceTransform.right;
|
||||||
|
case "up": return referenceTransform.up;
|
||||||
|
case "down": return -referenceTransform.up;
|
||||||
|
case "forward": case "front": return referenceTransform.forward;
|
||||||
|
case "back": case "backward": case "behind": return -referenceTransform.forward;
|
||||||
|
default:
|
||||||
|
Debug.LogWarning($"[ManageGameObject.MoveRelative] Unknown direction '{direction}', defaulting to forward.");
|
||||||
|
return referenceTransform.forward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static object DeleteGameObject(JToken targetToken, string searchMethod)
|
private static object DeleteGameObject(JToken targetToken, string searchMethod)
|
||||||
{
|
{
|
||||||
// Find potentially multiple objects if name/tag search is used without find_all=false implicitly
|
// Find potentially multiple objects if name/tag search is used without find_all=false implicitly
|
||||||
|
|
@ -2560,4 +2777,4 @@ namespace MCPForUnity.Editor.Tools
|
||||||
// Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup.
|
// Removed GetGameObjectData, GetComponentData, and related private helpers/caching/serializer setup.
|
||||||
// They are now in Helpers.GameObjectSerializer
|
// They are now in Helpers.GameObjectSerializer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ from transport.legacy.unity_connection import async_send_command_with_retry
|
||||||
)
|
)
|
||||||
async def manage_gameobject(
|
async def manage_gameobject(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component"], "Perform CRUD operations on GameObjects and components."],
|
action: Annotated[Literal["create", "modify", "delete", "find", "add_component", "remove_component", "set_component_property", "get_components", "get_component", "duplicate", "move_relative"], "Perform CRUD operations on GameObjects and components."],
|
||||||
target: Annotated[str,
|
target: Annotated[str,
|
||||||
"GameObject identifier by name or path for modify/delete/component actions"] | None = None,
|
"GameObject identifier by name or path for modify/delete/component actions"] | None = None,
|
||||||
search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
|
search_method: Annotated[Literal["by_id", "by_name", "by_path", "by_tag", "by_layer", "by_component"],
|
||||||
|
|
@ -66,6 +66,20 @@ async def manage_gameobject(
|
||||||
# Controls whether serialization of private [SerializeField] fields is included
|
# Controls whether serialization of private [SerializeField] fields is included
|
||||||
includeNonPublicSerialized: Annotated[bool | str,
|
includeNonPublicSerialized: Annotated[bool | str,
|
||||||
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
|
"Controls whether serialization of private [SerializeField] fields is included (accepts true/false or 'true'/'false')"] | None = None,
|
||||||
|
# --- Parameters for 'duplicate' ---
|
||||||
|
new_name: Annotated[str,
|
||||||
|
"New name for the duplicated object (default: SourceName_Copy)"] | None = None,
|
||||||
|
offset: Annotated[list[float] | str,
|
||||||
|
"Offset from original/reference position - [x,y,z] or string '[x,y,z]'"] | None = None,
|
||||||
|
# --- Parameters for 'move_relative' ---
|
||||||
|
reference_object: Annotated[str,
|
||||||
|
"Reference object for relative movement (required for move_relative)"] | None = None,
|
||||||
|
direction: Annotated[Literal["left", "right", "up", "down", "forward", "back", "front", "backward", "behind"],
|
||||||
|
"Direction for relative movement (e.g., 'right', 'up', 'forward')"] | None = None,
|
||||||
|
distance: Annotated[float,
|
||||||
|
"Distance to move in the specified direction (default: 1.0)"] | None = None,
|
||||||
|
world_space: Annotated[bool | str,
|
||||||
|
"If True (default), use world space directions; if False, use reference object's local directions"] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
# Get active instance from session state
|
# Get active instance from session state
|
||||||
# Removed session_state import
|
# Removed session_state import
|
||||||
|
|
@ -113,12 +127,14 @@ async def manage_gameobject(
|
||||||
position = _coerce_vec(position, default=position)
|
position = _coerce_vec(position, default=position)
|
||||||
rotation = _coerce_vec(rotation, default=rotation)
|
rotation = _coerce_vec(rotation, default=rotation)
|
||||||
scale = _coerce_vec(scale, default=scale)
|
scale = _coerce_vec(scale, default=scale)
|
||||||
|
offset = _coerce_vec(offset, default=offset)
|
||||||
save_as_prefab = _coerce_bool(save_as_prefab)
|
save_as_prefab = _coerce_bool(save_as_prefab)
|
||||||
set_active = _coerce_bool(set_active)
|
set_active = _coerce_bool(set_active)
|
||||||
find_all = _coerce_bool(find_all)
|
find_all = _coerce_bool(find_all)
|
||||||
search_in_children = _coerce_bool(search_in_children)
|
search_in_children = _coerce_bool(search_in_children)
|
||||||
search_inactive = _coerce_bool(search_inactive)
|
search_inactive = _coerce_bool(search_inactive)
|
||||||
includeNonPublicSerialized = _coerce_bool(includeNonPublicSerialized)
|
includeNonPublicSerialized = _coerce_bool(includeNonPublicSerialized)
|
||||||
|
world_space = _coerce_bool(world_space, default=True)
|
||||||
|
|
||||||
# Coerce 'component_properties' from JSON string to dict for client compatibility
|
# Coerce 'component_properties' from JSON string to dict for client compatibility
|
||||||
if isinstance(component_properties, str):
|
if isinstance(component_properties, str):
|
||||||
|
|
@ -181,7 +197,15 @@ async def manage_gameobject(
|
||||||
"searchInChildren": search_in_children,
|
"searchInChildren": search_in_children,
|
||||||
"searchInactive": search_inactive,
|
"searchInactive": search_inactive,
|
||||||
"componentName": component_name,
|
"componentName": component_name,
|
||||||
"includeNonPublicSerialized": includeNonPublicSerialized
|
"includeNonPublicSerialized": includeNonPublicSerialized,
|
||||||
|
# Parameters for 'duplicate'
|
||||||
|
"new_name": new_name,
|
||||||
|
"offset": offset,
|
||||||
|
# Parameters for 'move_relative'
|
||||||
|
"reference_object": reference_object,
|
||||||
|
"direction": direction,
|
||||||
|
"distance": distance,
|
||||||
|
"world_space": world_space,
|
||||||
}
|
}
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
params = {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
|
@ -217,4 +241,4 @@ async def manage_gameobject(
|
||||||
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
return response if isinstance(response, dict) else {"success": False, "message": str(response)}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "message": f"Python error managing GameObject: {str(e)}"}
|
return {"success": False, "message": f"Python error managing GameObject: {str(e)}"}
|
||||||
Loading…
Reference in New Issue