Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,16 @@ internal static List<GameObject> FindObjectsInternal(
}
else
{
#if UNITY_2023_1_OR_NEWER
var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude;
searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None)
.Cast<Component>()
.Select(c => c.gameObject);
#else
searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
.Cast<Component>()
.Select(c => c.gameObject);
#endif
Comment on lines +157 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the file
find . -name "ManageGameObjectCommon.cs" -type f

Repository: CoplayDev/unity-mcp

Length of output: 127


🏁 Script executed:

# Read the file around the specified lines to understand context
cat -n MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs | sed -n '140,180p'

Repository: CoplayDev/unity-mcp

Length of output: 2371


🏁 Script executed:

# Search for findAll variable usage in the file
rg -n "findAll" MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs

Repository: CoplayDev/unity-mcp

Length of output: 370


🏁 Script executed:

# Read the broader context to see how findAll is used and what happens when it's false
cat -n MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs | sed -n '1,50p'

Repository: CoplayDev/unity-mcp

Length of output: 2302


🏁 Script executed:

# Check what happens around line 209 where findAll is used
cat -n MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs | sed -n '200,220p'

Repository: CoplayDev/unity-mcp

Length of output: 1026


🏁 Script executed:

# Look at the FindObjectsInternal method signature and overall structure
cat -n MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs | sed -n '30,45p'

Repository: CoplayDev/unity-mcp

Length of output: 812


🏁 Script executed:

# Check if there are other search methods to understand the pattern
cat -n MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs | sed -n '50,180p'

Repository: CoplayDev/unity-mcp

Length of output: 7042


🌐 Web query:

FindObjectsOfType InstanceID sorting Unity documentation

💡 Result:

Short answer: Unity's Object.FindObjectsOfType always sorts its results by InstanceID. Use Object.FindObjectsByType with FindObjectsSortMode.None if you don't need sorting (faster). [1][2][3]

Sources:

  • Unity docs: Object.FindObjectsOfType (notes it always sorts by InstanceID; recommends FindObjectsByType). [1]
  • Unity docs: FindObjectsSortMode enum (InstanceID vs None). [2]
  • Community confirmation / example showing ordering by GetInstanceID. [3]

🌐 Web query:

FindObjectsByType FindObjectsSortMode.None documentation Unity 2023.1

💡 Result:

FindObjectsByType is documented in the Unity 2023.1 Scripting API. Use the FindObjectsSortMode.None option to return results unsorted (fastest, order not guaranteed).

Key points

  • Signature/examples: Object.FindObjectsByType(Type type, FindObjectsSortMode sortMode) and generic overloads (also overloads with FindObjectsInactive). Example: FindObjectsByType(FindObjectsSortMode.None). [1]
  • FindObjectsSortMode.None: "Don't sort the objects." It's the least resource‑intensive option; result order may vary between calls. [2]
  • Notes: This API finds loaded (non-asset) objects; it doesn't return assets or objects with HideFlags.DontSave. Use Resources.FindObjectsOfTypeAll to include assets. FindObjectsOfType (legacy) is deprecated in favor of FindObjectsByType which lets you choose sorting. [3]

Example

  • C#: var found = FindObjectsByType(FindObjectsSortMode.None); [1]

Sources
[1] Unity Scripting API — Object.FindObjectsByType (2023.1).
[2] Unity Scripting API — FindObjectsSortMode.None.
[3] Unity Scripting API — Object.FindObjectsOfType (notes on deprecation and behavior).


Preserve deterministic selection when only returning first match.

FindObjectsSortMode.None produces unsorted results, whereas the previous FindObjectsOfType always returns results sorted by InstanceID. Since the code returns only the first match when findAll is false (line 211), this sorting change can alter which object is selected. To preserve legacy behavior, use FindObjectsSortMode.InstanceID when findAll is false.

🔧 Suggested adjustment
`#if` UNITY_2023_1_OR_NEWER
                            var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude;
+                           var sortMode = findAll ? FindObjectsSortMode.None : FindObjectsSortMode.InstanceID;
-                           searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None)
+                           searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, sortMode)
                                .Cast<Component>()
                                .Select(c => c.gameObject);
`#else`
                            searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
                                .Cast<Component>()
                                .Select(c => c.gameObject);
`#endif`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if UNITY_2023_1_OR_NEWER
var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude;
searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, FindObjectsSortMode.None)
.Cast<Component>()
.Select(c => c.gameObject);
#else
searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
.Cast<Component>()
.Select(c => c.gameObject);
#endif
`#if` UNITY_2023_1_OR_NEWER
var inactive = searchInactive ? FindObjectsInactive.Include : FindObjectsInactive.Exclude;
var sortMode = findAll ? FindObjectsSortMode.None : FindObjectsSortMode.InstanceID;
searchPoolComp = UnityEngine.Object.FindObjectsByType(componentType, inactive, sortMode)
.Cast<Component>()
.Select(c => c.gameObject);
`#else`
searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
.Cast<Component>()
.Select(c => c.gameObject);
`#endif`
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Tools/GameObjects/ManageGameObjectCommon.cs` around lines
157 - 166, The current use of FindObjectsSortMode.None in the FindObjectsByType
call can change which object is returned when only the first match is used (see
findAll), so change the sort mode based on whether findAll is requested: when
findAll is false use FindObjectsSortMode.InstanceID to retain legacy
deterministic ordering, otherwise use FindObjectsSortMode.None (or the existing
behavior); update the FindObjectsByType invocation that sets searchPoolComp
(using componentType and searchInactive) to pick the sort mode conditionally so
the first selected gameObject remains stable.

}
results.AddRange(searchPoolComp.Where(go => go != null));
}
Expand Down
200 changes: 125 additions & 75 deletions MCPForUnity/Editor/Tools/ManageScene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,34 +384,27 @@ private static object CaptureScreenshot(string fileName, int? superSize)
try
{
int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1;
ScreenshotCaptureResult result;

if (Application.isPlaying)
// Best-effort: ensure Game View exists and repaints before capture.
if (!Application.isBatchMode)
{
result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true);
EnsureGameView();
}
else
{
// Edit Mode path: render from the best-guess camera using RenderTexture.
Camera cam = Camera.main;
if (cam == null)
{
// Use FindObjectsOfType for Unity 2021 compatibility
var cams = UnityEngine.Object.FindObjectsOfType<Camera>();
cam = cams.FirstOrDefault();
}

if (cam == null)
{
return new ErrorResponse("No camera found to capture screenshot in Edit Mode.");
}
ScreenshotCaptureResult result = ScreenshotUtility.CaptureToAssetsFolder(fileName, resolvedSuperSize, ensureUniqueFileName: true);

result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(cam, fileName, resolvedSuperSize, ensureUniqueFileName: true);
// ScreenCapture.CaptureScreenshot is async. Import after the file actually hits disk.
if (result.IsAsync)
{
ScheduleAssetImportWhenFileExists(result.AssetsRelativePath, result.FullPath, timeoutSeconds: 30.0);
}
else
{
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
}

AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);

string message = $"Screenshot captured to '{result.AssetsRelativePath}' (full: {result.FullPath}).";
string verb = result.IsAsync ? "Screenshot requested" : "Screenshot captured";
string message = $"{verb} to '{result.AssetsRelativePath}' (full: {result.FullPath}).";

return new SuccessResponse(
message,
Expand All @@ -420,6 +413,7 @@ private static object CaptureScreenshot(string fileName, int? superSize)
path = result.AssetsRelativePath,
fullPath = result.FullPath,
superSize = result.SuperSize,
isAsync = result.IsAsync,
}
);
}
Expand All @@ -429,6 +423,111 @@ private static object CaptureScreenshot(string fileName, int? superSize)
}
}

private static void EnsureGameView()
{
try
{
// Ensure a Game View exists and has a chance to repaint before capture.
try
{
if (!EditorApplication.ExecuteMenuItem("Window/General/Game"))
{
// Some Unity versions expose hotkey suffixes in menu paths.
EditorApplication.ExecuteMenuItem("Window/General/Game %2");
}
}
catch (Exception e)
{
try { McpLog.Debug($"[ManageScene] screenshot: failed to open Game View via menu item: {e.Message}"); } catch { }
}

try
{
var gameViewType = Type.GetType("UnityEditor.GameView,UnityEditor");
if (gameViewType != null)
{
var window = EditorWindow.GetWindow(gameViewType);
window?.Repaint();
}
}
catch (Exception e)
{
try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Game View: {e.Message}"); } catch { }
}

try { SceneView.RepaintAll(); }
catch (Exception e)
{
try { McpLog.Debug($"[ManageScene] screenshot: failed to repaint Scene View: {e.Message}"); } catch { }
}

try { EditorApplication.QueuePlayerLoopUpdate(); }
catch (Exception e)
{
try { McpLog.Debug($"[ManageScene] screenshot: failed to queue player loop update: {e.Message}"); } catch { }
}
}
catch (Exception e)
{
try { McpLog.Debug($"[ManageScene] screenshot: EnsureGameView failed: {e.Message}"); } catch { }
}
}

private static void ScheduleAssetImportWhenFileExists(string assetsRelativePath, string fullPath, double timeoutSeconds)
{
if (string.IsNullOrWhiteSpace(assetsRelativePath) || string.IsNullOrWhiteSpace(fullPath))
{
McpLog.Warn("[ManageScene] ScheduleAssetImportWhenFileExists: invalid paths provided, skipping import scheduling.");
return;
}

double start = EditorApplication.timeSinceStartup;
int failureCount = 0;
bool hasSeenFile = false;
const int maxLoggedFailures = 3;
EditorApplication.CallbackFunction tick = null;
tick = () =>
{
try
{
if (File.Exists(fullPath))
{
hasSeenFile = true;

AssetDatabase.ImportAsset(assetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
McpLog.Debug($"[ManageScene] Imported asset at '{assetsRelativePath}'.");
EditorApplication.update -= tick;
return;
}
}
catch (Exception e)
{
failureCount++;

if (failureCount <= maxLoggedFailures)
{
McpLog.Warn($"[ManageScene] Exception while importing asset '{assetsRelativePath}' from '{fullPath}' (attempt {failureCount}): {e}");
}
}

if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
{
if (!hasSeenFile)
{
McpLog.Warn($"[ManageScene] Timed out waiting for file '{fullPath}' (asset: '{assetsRelativePath}') after {timeoutSeconds:F1} seconds. The asset was not imported.");
}
else
{
McpLog.Warn($"[ManageScene] Timed out importing asset '{assetsRelativePath}' from '{fullPath}' after {timeoutSeconds:F1} seconds. The file existed but the asset was not imported.");
}

EditorApplication.update -= tick;
}
};

EditorApplication.update += tick;
}

private static object GetActiveSceneInfo()
{
try
Expand Down Expand Up @@ -668,7 +767,10 @@ private static object BuildGameObjectSummary(GameObject go, bool includeTransfor
}
}
}
catch { }
catch (Exception ex)
{
McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}");
}

var d = new Dictionary<string, object>
{
Expand All @@ -684,7 +786,7 @@ private static object BuildGameObjectSummary(GameObject go, bool includeTransfor
{ "childrenTruncated", childrenTruncated },
{ "childrenCursor", childCount > 0 ? "0" : null },
{ "childrenPageSizeDefault", maxChildrenPerNode },
{ "componentTypes", componentTypes }, // NEW: Lightweight component type list
{ "componentTypes", componentTypes },
};

if (includeTransform && go.transform != null)
Expand Down Expand Up @@ -721,57 +823,5 @@ private static string GetGameObjectPath(GameObject go)
}
}

/// <summary>
/// Recursively builds a data representation of a GameObject and its children.
/// </summary>
private static object GetGameObjectDataRecursive(GameObject go)
{
if (go == null)
return null;

var childrenData = new List<object>();
foreach (Transform child in go.transform)
{
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
}

var gameObjectData = new Dictionary<string, object>
{
{ "name", go.name },
{ "activeSelf", go.activeSelf },
{ "activeInHierarchy", go.activeInHierarchy },
{ "tag", go.tag },
{ "layer", go.layer },
{ "isStatic", go.isStatic },
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
{
"transform",
new
{
position = new
{
x = go.transform.localPosition.x,
y = go.transform.localPosition.y,
z = go.transform.localPosition.z,
},
rotation = new
{
x = go.transform.localRotation.eulerAngles.x,
y = go.transform.localRotation.eulerAngles.y,
z = go.transform.localRotation.eulerAngles.z,
}, // Euler for simplicity
scale = new
{
x = go.transform.localScale.x,
y = go.transform.localScale.y,
z = go.transform.localScale.z,
},
}
},
{ "children", childrenData },
};

return gameObjectData;
}
}
}
Loading