fix: improve manage_scene screenshot capture (#600)
* fix: improve manage_scene screenshot capture * fix: address PR review feedback for screenshot capture - Gate pre-2022 ScreenCapture fallback warning to log only once - Downgrade warning to Debug.Log to reduce log noise - Refactor path-building into shared PrepareCaptureResult() helper - Add conditional logging to catch blocks in BestEffortPrepareGameViewForScreenshot - Add timeout/failure logging to ScheduleAssetImportWhenFileExists - Fix grammar in README-DEV.md * fix(unity): resolve screenshot import callback type + FindObjectsOfType deprecation * chore: bump version to 9.2.0 * Update ManageScene.cs * Update ScreenshotUtility.cs * Update error logging in ManageScene.cs --------- Co-authored-by: Marcus Sanatan <msanatan@gmail.com> Co-authored-by: GitHub Actions <actions@github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>main
parent
c1d89c7680
commit
67dda7f9cc
|
|
@ -154,9 +154,16 @@ namespace MCPForUnity.Editor.Tools.GameObjects
|
||||||
}
|
}
|
||||||
else
|
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)
|
searchPoolComp = UnityEngine.Object.FindObjectsOfType(componentType, searchInactive)
|
||||||
.Cast<Component>()
|
.Cast<Component>()
|
||||||
.Select(c => c.gameObject);
|
.Select(c => c.gameObject);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
results.AddRange(searchPoolComp.Where(go => go != null));
|
results.AddRange(searchPoolComp.Where(go => go != null));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -384,34 +384,27 @@ namespace MCPForUnity.Editor.Tools
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int resolvedSuperSize = (superSize.HasValue && superSize.Value > 0) ? superSize.Value : 1;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScreenshotCaptureResult result = ScreenshotUtility.CaptureToAssetsFolder(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
|
else
|
||||||
{
|
{
|
||||||
// Edit Mode path: render from the best-guess camera using RenderTexture.
|
AssetDatabase.ImportAsset(result.AssetsRelativePath, ImportAssetOptions.ForceSynchronousImport);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
result = ScreenshotUtility.CaptureFromCameraToAssetsFolder(cam, fileName, resolvedSuperSize, ensureUniqueFileName: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
|
string verb = result.IsAsync ? "Screenshot requested" : "Screenshot captured";
|
||||||
|
string message = $"{verb} to '{result.AssetsRelativePath}' (full: {result.FullPath}).";
|
||||||
string message = $"Screenshot captured to '{result.AssetsRelativePath}' (full: {result.FullPath}).";
|
|
||||||
|
|
||||||
return new SuccessResponse(
|
return new SuccessResponse(
|
||||||
message,
|
message,
|
||||||
|
|
@ -420,6 +413,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
path = result.AssetsRelativePath,
|
path = result.AssetsRelativePath,
|
||||||
fullPath = result.FullPath,
|
fullPath = result.FullPath,
|
||||||
superSize = result.SuperSize,
|
superSize = result.SuperSize,
|
||||||
|
isAsync = result.IsAsync,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -429,6 +423,111 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
private static object GetActiveSceneInfo()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -668,7 +767,10 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
McpLog.Debug($"[ManageScene] Failed to enumerate components for '{go.name}': {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
var d = new Dictionary<string, object>
|
var d = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
|
|
@ -684,7 +786,7 @@ namespace MCPForUnity.Editor.Tools
|
||||||
{ "childrenTruncated", childrenTruncated },
|
{ "childrenTruncated", childrenTruncated },
|
||||||
{ "childrenCursor", childCount > 0 ? "0" : null },
|
{ "childrenCursor", childCount > 0 ? "0" : null },
|
||||||
{ "childrenPageSizeDefault", maxChildrenPerNode },
|
{ "childrenPageSizeDefault", maxChildrenPerNode },
|
||||||
{ "componentTypes", componentTypes }, // NEW: Lightweight component type list
|
{ "componentTypes", componentTypes },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeTransform && go.transform != null)
|
if (includeTransform && go.transform != null)
|
||||||
|
|
@ -721,57 +823,5 @@ namespace MCPForUnity.Editor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,58 +9,70 @@ namespace MCPForUnity.Runtime.Helpers
|
||||||
public readonly struct ScreenshotCaptureResult
|
public readonly struct ScreenshotCaptureResult
|
||||||
{
|
{
|
||||||
public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize)
|
public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize)
|
||||||
|
: this(fullPath, assetsRelativePath, superSize, isAsync: false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScreenshotCaptureResult(string fullPath, string assetsRelativePath, int superSize, bool isAsync)
|
||||||
{
|
{
|
||||||
FullPath = fullPath;
|
FullPath = fullPath;
|
||||||
AssetsRelativePath = assetsRelativePath;
|
AssetsRelativePath = assetsRelativePath;
|
||||||
SuperSize = superSize;
|
SuperSize = superSize;
|
||||||
|
IsAsync = isAsync;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FullPath { get; }
|
public string FullPath { get; }
|
||||||
public string AssetsRelativePath { get; }
|
public string AssetsRelativePath { get; }
|
||||||
public int SuperSize { get; }
|
public int SuperSize { get; }
|
||||||
|
public bool IsAsync { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ScreenshotUtility
|
public static class ScreenshotUtility
|
||||||
{
|
{
|
||||||
private const string ScreenshotsFolderName = "Screenshots";
|
private const string ScreenshotsFolderName = "Screenshots";
|
||||||
|
private static bool s_loggedLegacyScreenCaptureFallback;
|
||||||
|
|
||||||
|
private static Camera FindAvailableCamera()
|
||||||
|
{
|
||||||
|
var main = Camera.main;
|
||||||
|
if (main != null)
|
||||||
|
{
|
||||||
|
return main;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use FindObjectsOfType for Unity 2021 compatibility.
|
||||||
|
var cams = UnityEngine.Object.FindObjectsOfType<Camera>();
|
||||||
|
return cams.FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = null, int superSize = 1, bool ensureUniqueFileName = true)
|
public static ScreenshotCaptureResult CaptureToAssetsFolder(string fileName = null, int superSize = 1, bool ensureUniqueFileName = true)
|
||||||
{
|
{
|
||||||
int size = Mathf.Max(1, superSize);
|
|
||||||
string resolvedName = BuildFileName(fileName);
|
|
||||||
string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
|
|
||||||
Directory.CreateDirectory(folder);
|
|
||||||
|
|
||||||
string fullPath = Path.Combine(folder, resolvedName);
|
|
||||||
if (ensureUniqueFileName)
|
|
||||||
{
|
|
||||||
fullPath = EnsureUnique(fullPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
string normalizedFullPath = fullPath.Replace('\\', '/');
|
|
||||||
|
|
||||||
// Use only the file name to let Unity decide the final location (per CaptureScreenshot docs).
|
|
||||||
string captureName = Path.GetFileName(normalizedFullPath);
|
|
||||||
|
|
||||||
// Use Asset folder for ScreenCapture.CaptureScreenshot to ensure write to asset rather than project root
|
|
||||||
string projectRoot = GetProjectRootPath();
|
|
||||||
string assetsRelativePath = normalizedFullPath;
|
|
||||||
if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
#if UNITY_2022_1_OR_NEWER
|
#if UNITY_2022_1_OR_NEWER
|
||||||
ScreenCapture.CaptureScreenshot(assetsRelativePath, size);
|
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, isAsync: true);
|
||||||
|
ScreenCapture.CaptureScreenshot(result.AssetsRelativePath, result.SuperSize);
|
||||||
|
return result;
|
||||||
#else
|
#else
|
||||||
Debug.LogWarning("ScreenCapture is supported after Unity 2022.1. Using main camera capture as fallback.");
|
if (!s_loggedLegacyScreenCaptureFallback)
|
||||||
CaptureFromCameraToAssetsFolder(Camera.main, captureName, size, false);
|
{
|
||||||
#endif
|
Debug.Log("ScreenCapture is supported after Unity 2022.1. Using camera capture as fallback.");
|
||||||
|
s_loggedLegacyScreenCaptureFallback = true;
|
||||||
|
}
|
||||||
|
|
||||||
return new ScreenshotCaptureResult(
|
var cam = FindAvailableCamera();
|
||||||
normalizedFullPath,
|
if (cam == null)
|
||||||
assetsRelativePath,
|
{
|
||||||
size);
|
throw new InvalidOperationException("No camera found to capture screenshot.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return CaptureFromCameraToAssetsFolder(cam, fileName, superSize, ensureUniqueFileName);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -73,6 +85,54 @@ namespace MCPForUnity.Runtime.Helpers
|
||||||
throw new ArgumentNullException(nameof(camera));
|
throw new ArgumentNullException(nameof(camera));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, isAsync: false);
|
||||||
|
int size = result.SuperSize;
|
||||||
|
|
||||||
|
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
|
||||||
|
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
|
||||||
|
width *= size;
|
||||||
|
height *= size;
|
||||||
|
|
||||||
|
RenderTexture prevRT = camera.targetTexture;
|
||||||
|
RenderTexture prevActive = RenderTexture.active;
|
||||||
|
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
||||||
|
Texture2D tex = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
camera.targetTexture = rt;
|
||||||
|
camera.Render();
|
||||||
|
|
||||||
|
RenderTexture.active = rt;
|
||||||
|
tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||||
|
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
||||||
|
tex.Apply();
|
||||||
|
|
||||||
|
byte[] png = tex.EncodeToPNG();
|
||||||
|
File.WriteAllBytes(result.FullPath, png);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
camera.targetTexture = prevRT;
|
||||||
|
RenderTexture.active = prevActive;
|
||||||
|
RenderTexture.ReleaseTemporary(rt);
|
||||||
|
if (tex != null)
|
||||||
|
{
|
||||||
|
if (Application.isPlaying)
|
||||||
|
{
|
||||||
|
UnityEngine.Object.Destroy(tex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, bool isAsync)
|
||||||
|
{
|
||||||
int size = Mathf.Max(1, superSize);
|
int size = Mathf.Max(1, superSize);
|
||||||
string resolvedName = BuildFileName(fileName);
|
string resolvedName = BuildFileName(fileName);
|
||||||
string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
|
string folder = Path.Combine(Application.dataPath, ScreenshotsFolderName);
|
||||||
|
|
@ -85,43 +145,20 @@ namespace MCPForUnity.Runtime.Helpers
|
||||||
}
|
}
|
||||||
|
|
||||||
string normalizedFullPath = fullPath.Replace('\\', '/');
|
string normalizedFullPath = fullPath.Replace('\\', '/');
|
||||||
|
string assetsRelativePath = ToAssetsRelativePath(normalizedFullPath);
|
||||||
|
|
||||||
int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
|
return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, isAsync);
|
||||||
int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
|
}
|
||||||
width *= size;
|
|
||||||
height *= size;
|
|
||||||
|
|
||||||
RenderTexture prevRT = camera.targetTexture;
|
|
||||||
RenderTexture prevActive = RenderTexture.active;
|
|
||||||
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
camera.targetTexture = rt;
|
|
||||||
camera.Render();
|
|
||||||
|
|
||||||
RenderTexture.active = rt;
|
|
||||||
var tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
|
||||||
tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
|
||||||
tex.Apply();
|
|
||||||
|
|
||||||
byte[] png = tex.EncodeToPNG();
|
|
||||||
File.WriteAllBytes(normalizedFullPath, png);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
camera.targetTexture = prevRT;
|
|
||||||
RenderTexture.active = prevActive;
|
|
||||||
RenderTexture.ReleaseTemporary(rt);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private static string ToAssetsRelativePath(string normalizedFullPath)
|
||||||
|
{
|
||||||
string projectRoot = GetProjectRootPath();
|
string projectRoot = GetProjectRootPath();
|
||||||
string assetsRelativePath = normalizedFullPath;
|
string assetsRelativePath = normalizedFullPath;
|
||||||
if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
|
if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/');
|
assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/');
|
||||||
}
|
}
|
||||||
|
return assetsRelativePath;
|
||||||
return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildFileName(string fileName)
|
private static string BuildFileName(string fileName)
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,12 @@ X:\UnityProject\Library\PackageCache\com.coplaydev.unity-mcp@272123cfd97e
|
||||||
- **`max_nodes`**:默认 **1000**,限制 **1..5000**
|
- **`max_nodes`**:默认 **1000**,限制 **1..5000**
|
||||||
- **`include_transform`**:默认 **false**
|
- **`include_transform`**:默认 **false**
|
||||||
|
|
||||||
|
### `manage_scene(action="screenshot")`
|
||||||
|
|
||||||
|
- 将 PNG 保存到 `Assets/Screenshots/`。
|
||||||
|
- Unity **2022.1+**:通过 `ScreenCapture.CaptureScreenshot` 捕获 **Game View**,因此包含 `Screen Space - Overlay` UI。注意该写入是 **异步** 的,文件/导入可能会稍后出现。
|
||||||
|
- Unity **2021.3**:回退为将可用的 `Camera` 渲染到 `RenderTexture`(仅相机输出;不包含 `Screen Space - Overlay` UI)。
|
||||||
|
|
||||||
### `manage_gameobject(action="get_components")`
|
### `manage_gameobject(action="get_components")`
|
||||||
|
|
||||||
- **默认行为**:仅返回 **分页的组件元数据**(`typeName`, `instanceID`)。
|
- **默认行为**:仅返回 **分页的组件元数据**(`typeName`, `instanceID`)。
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,12 @@ Some Unity tool calls can return *very large* JSON payloads (deep hierarchies, c
|
||||||
- **`max_nodes`**: defaults to **1000**, clamped to **1..5000**
|
- **`max_nodes`**: defaults to **1000**, clamped to **1..5000**
|
||||||
- **`include_transform`**: defaults to **false**
|
- **`include_transform`**: defaults to **false**
|
||||||
|
|
||||||
|
### `manage_scene(action="screenshot")`
|
||||||
|
|
||||||
|
- Saves PNGs under `Assets/Screenshots/`.
|
||||||
|
- Unity **2022.1+**: captures the **Game View** via `ScreenCapture.CaptureScreenshot`, so `Screen Space - Overlay` UI is included. This write is **async**, so the file may appear/import a moment later.
|
||||||
|
- Unity **2021.3**: falls back to rendering the best available `Camera` into a `RenderTexture` (camera output only; `Screen Space - Overlay` UI is not included).
|
||||||
|
|
||||||
### `manage_gameobject(action="get_components")`
|
### `manage_gameobject(action="get_components")`
|
||||||
|
|
||||||
- **Default behavior**: returns **paged component metadata** only (`typeName`, `instanceID`).
|
- **Default behavior**: returns **paged component metadata** only (`typeName`, `instanceID`).
|
||||||
|
|
@ -391,4 +397,4 @@ Tests that trigger script compilation mid-run (e.g., `DomainReloadResilienceTest
|
||||||
- Run them first in the test suite (before backgrounding Unity)
|
- Run them first in the test suite (before backgrounding Unity)
|
||||||
- Use the `[Explicit]` attribute to exclude them from default runs
|
- Use the `[Explicit]` attribute to exclude them from default runs
|
||||||
|
|
||||||
**Note:** The MCP workflow itself is unaffected—socket messages provide external stimulus that keeps Unity responsive even when backgrounded. This limitation only affects Unity's internal test coroutine waits.
|
**Note:** The MCP workflow itself is unaffected—socket messages provide external stimulus that keeps Unity responsive even when backgrounded. This limitation only affects Unity's internal test coroutine waits.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue