升级XR插件版本
This commit is contained in:
8
Packages/MCPForUnity/Runtime/Helpers.meta
Normal file
8
Packages/MCPForUnity/Runtime/Helpers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c8b3a2f4b0e42dba7f6d7c6e8a4c1f2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
273
Packages/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
Normal file
273
Packages/MCPForUnity/Runtime/Helpers/ScreenshotUtility.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnity.Runtime.Helpers
|
||||
//The reason for having another Runtime Utilities in additional to Editor Utilities is to avoid Editor-only dependencies in this runtime code.
|
||||
{
|
||||
public readonly struct ScreenshotCaptureResult
|
||||
{
|
||||
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;
|
||||
AssetsRelativePath = assetsRelativePath;
|
||||
SuperSize = superSize;
|
||||
IsAsync = isAsync;
|
||||
}
|
||||
|
||||
public string FullPath { get; }
|
||||
public string AssetsRelativePath { get; }
|
||||
public int SuperSize { get; }
|
||||
public bool IsAsync { get; }
|
||||
}
|
||||
|
||||
public static class ScreenshotUtility
|
||||
{
|
||||
private const string ScreenshotsFolderName = "Screenshots";
|
||||
private static bool s_loggedLegacyScreenCaptureFallback;
|
||||
private static bool? s_screenCaptureModuleAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the Screen Capture module (com.unity.modules.screencapture) is enabled.
|
||||
/// This module can be disabled in Package Manager > Built-in, which removes the ScreenCapture class.
|
||||
/// </summary>
|
||||
public static bool IsScreenCaptureModuleAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!s_screenCaptureModuleAvailable.HasValue)
|
||||
{
|
||||
// Check if ScreenCapture type exists (module might be disabled)
|
||||
s_screenCaptureModuleAvailable = Type.GetType("UnityEngine.ScreenCapture, UnityEngine.ScreenCaptureModule") != null
|
||||
|| Type.GetType("UnityEngine.ScreenCapture, UnityEngine.CoreModule") != null;
|
||||
}
|
||||
return s_screenCaptureModuleAvailable.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error message to display when Screen Capture module is not available.
|
||||
/// </summary>
|
||||
public const string ScreenCaptureModuleNotAvailableError =
|
||||
"The Screen Capture module (com.unity.modules.screencapture) is not enabled. " +
|
||||
"To use screenshot capture with ScreenCapture API, please enable it in Unity: " +
|
||||
"Window > Package Manager > Built-in > Screen Capture > Enable. " +
|
||||
"Alternatively, MCP for Unity will use camera-based capture as a fallback if a Camera exists in the scene.";
|
||||
|
||||
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)
|
||||
{
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
// Check if Screen Capture module is available (can be disabled in Package Manager > Built-in)
|
||||
if (IsScreenCaptureModuleAvailable)
|
||||
{
|
||||
ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, isAsync: true);
|
||||
ScreenCapture.CaptureScreenshot(result.AssetsRelativePath, result.SuperSize);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Module disabled - try camera fallback
|
||||
Debug.LogWarning("[MCP for Unity] " + ScreenCaptureModuleNotAvailableError);
|
||||
return CaptureWithCameraFallback(fileName, superSize, ensureUniqueFileName);
|
||||
}
|
||||
#else
|
||||
// Unity < 2022.1 - always use camera fallback
|
||||
return CaptureWithCameraFallback(fileName, superSize, ensureUniqueFileName);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static ScreenshotCaptureResult CaptureWithCameraFallback(string fileName, int superSize, bool ensureUniqueFileName)
|
||||
{
|
||||
if (!s_loggedLegacyScreenCaptureFallback)
|
||||
{
|
||||
Debug.Log("[MCP for Unity] Using camera-based screenshot capture. " +
|
||||
"This requires a Camera in the scene. For best results on Unity 2022.1+, ensure the Screen Capture module is enabled: " +
|
||||
"Window > Package Manager > Built-in > Screen Capture > Enable.");
|
||||
s_loggedLegacyScreenCaptureFallback = true;
|
||||
}
|
||||
|
||||
var cam = FindAvailableCamera();
|
||||
if (cam == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No camera found to capture screenshot. Camera-based capture requires a Camera in the scene. " +
|
||||
"Either add a Camera to your scene, or enable the Screen Capture module: " +
|
||||
"Window > Package Manager > Built-in > Screen Capture > Enable."
|
||||
);
|
||||
}
|
||||
|
||||
return CaptureFromCameraToAssetsFolder(cam, fileName, superSize, ensureUniqueFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures a screenshot from a specific camera by rendering into a temporary RenderTexture (works in Edit Mode).
|
||||
/// </summary>
|
||||
public static ScreenshotCaptureResult CaptureFromCameraToAssetsFolder(Camera camera, string fileName = null, int superSize = 1, bool ensureUniqueFileName = true)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
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);
|
||||
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('\\', '/');
|
||||
string assetsRelativePath = ToAssetsRelativePath(normalizedFullPath);
|
||||
|
||||
return new ScreenshotCaptureResult(normalizedFullPath, assetsRelativePath, size, isAsync);
|
||||
}
|
||||
|
||||
private static string ToAssetsRelativePath(string normalizedFullPath)
|
||||
{
|
||||
string projectRoot = GetProjectRootPath();
|
||||
string assetsRelativePath = normalizedFullPath;
|
||||
if (assetsRelativePath.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
assetsRelativePath = assetsRelativePath.Substring(projectRoot.Length).TrimStart('/');
|
||||
}
|
||||
return assetsRelativePath;
|
||||
}
|
||||
|
||||
private static string BuildFileName(string fileName)
|
||||
{
|
||||
string name = string.IsNullOrWhiteSpace(fileName)
|
||||
? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}"
|
||||
: fileName.Trim();
|
||||
|
||||
name = SanitizeFileName(name);
|
||||
|
||||
if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) &&
|
||||
!name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) &&
|
||||
!name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
name += ".png";
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string fileName)
|
||||
{
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
string cleaned = new string(fileName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
||||
|
||||
return string.IsNullOrWhiteSpace(cleaned) ? "screenshot" : cleaned;
|
||||
}
|
||||
|
||||
private static string EnsureUnique(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(path) ?? string.Empty;
|
||||
string baseName = Path.GetFileNameWithoutExtension(path);
|
||||
string extension = Path.GetExtension(path);
|
||||
int counter = 1;
|
||||
|
||||
string candidate;
|
||||
do
|
||||
{
|
||||
candidate = Path.Combine(directory, $"{baseName}-{counter}{extension}");
|
||||
counter++;
|
||||
} while (File.Exists(candidate));
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static string GetProjectRootPath()
|
||||
{
|
||||
string root = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
root = root.Replace('\\', '/');
|
||||
if (!root.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
root += "/";
|
||||
}
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b33f3e9c912b4f17a5a4374e4f6c2a91
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Packages/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef
Normal file
16
Packages/MCPForUnity/Runtime/MCPForUnity.Runtime.asmdef
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "MCPForUnity.Runtime",
|
||||
"rootNamespace": "MCPForUnity.Runtime",
|
||||
"references": [
|
||||
"Newtonsoft.Json"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 562a750ff18ee4193928e885c708fee1
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/MCPForUnity/Runtime/Serialization.meta
Normal file
8
Packages/MCPForUnity/Runtime/Serialization.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7e33d6224fe6473f9bc69fe6d40e508
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,451 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor; // Required for AssetDatabase and EditorUtility
|
||||
#endif
|
||||
|
||||
namespace MCPForUnity.Runtime.Serialization
|
||||
{
|
||||
public class Vector3Converter : JsonConverter<Vector3>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Vector3(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class Vector2Converter : JsonConverter<Vector2>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Vector2(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class QuaternionConverter : JsonConverter<Quaternion>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Quaternion value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WritePropertyName("w");
|
||||
writer.WriteValue(value.w);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Quaternion ReadJson(JsonReader reader, Type objectType, Quaternion existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Quaternion(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"],
|
||||
(float)jo["w"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class ColorConverter : JsonConverter<Color>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Color value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("r");
|
||||
writer.WriteValue(value.r);
|
||||
writer.WritePropertyName("g");
|
||||
writer.WriteValue(value.g);
|
||||
writer.WritePropertyName("b");
|
||||
writer.WriteValue(value.b);
|
||||
writer.WritePropertyName("a");
|
||||
writer.WriteValue(value.a);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Color ReadJson(JsonReader reader, Type objectType, Color existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Color(
|
||||
(float)jo["r"],
|
||||
(float)jo["g"],
|
||||
(float)jo["b"],
|
||||
(float)jo["a"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class RectConverter : JsonConverter<Rect>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Rect value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("width");
|
||||
writer.WriteValue(value.width);
|
||||
writer.WritePropertyName("height");
|
||||
writer.WriteValue(value.height);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Rect ReadJson(JsonReader reader, Type objectType, Rect existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Rect(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["width"],
|
||||
(float)jo["height"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class BoundsConverter : JsonConverter<Bounds>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Bounds value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("center");
|
||||
serializer.Serialize(writer, value.center); // Use serializer to handle nested Vector3
|
||||
writer.WritePropertyName("size");
|
||||
serializer.Serialize(writer, value.size); // Use serializer to handle nested Vector3
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Bounds ReadJson(JsonReader reader, Type objectType, Bounds existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
Vector3 center = jo["center"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
|
||||
Vector3 size = jo["size"].ToObject<Vector3>(serializer); // Use serializer to handle nested Vector3
|
||||
return new Bounds(center, size);
|
||||
}
|
||||
}
|
||||
|
||||
public class Vector4Converter : JsonConverter<Vector4>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Vector4 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("x");
|
||||
writer.WriteValue(value.x);
|
||||
writer.WritePropertyName("y");
|
||||
writer.WriteValue(value.y);
|
||||
writer.WritePropertyName("z");
|
||||
writer.WriteValue(value.z);
|
||||
writer.WritePropertyName("w");
|
||||
writer.WriteValue(value.w);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Vector4 ReadJson(JsonReader reader, Type objectType, Vector4 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
return new Vector4(
|
||||
(float)jo["x"],
|
||||
(float)jo["y"],
|
||||
(float)jo["z"],
|
||||
(float)jo["w"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe converter for Matrix4x4 that only accesses raw matrix elements (m00-m33).
|
||||
/// Avoids computed properties (lossyScale, rotation, inverse) that call ValidTRS()
|
||||
/// and can crash Unity on non-TRS matrices (common in Cinemachine components).
|
||||
/// Fixes: https://github.com/CoplayDev/unity-mcp/issues/478
|
||||
/// </summary>
|
||||
public class Matrix4x4Converter : JsonConverter<Matrix4x4>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, Matrix4x4 value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
// Only access raw matrix elements - NEVER computed properties like lossyScale/rotation
|
||||
writer.WritePropertyName("m00"); writer.WriteValue(value.m00);
|
||||
writer.WritePropertyName("m01"); writer.WriteValue(value.m01);
|
||||
writer.WritePropertyName("m02"); writer.WriteValue(value.m02);
|
||||
writer.WritePropertyName("m03"); writer.WriteValue(value.m03);
|
||||
writer.WritePropertyName("m10"); writer.WriteValue(value.m10);
|
||||
writer.WritePropertyName("m11"); writer.WriteValue(value.m11);
|
||||
writer.WritePropertyName("m12"); writer.WriteValue(value.m12);
|
||||
writer.WritePropertyName("m13"); writer.WriteValue(value.m13);
|
||||
writer.WritePropertyName("m20"); writer.WriteValue(value.m20);
|
||||
writer.WritePropertyName("m21"); writer.WriteValue(value.m21);
|
||||
writer.WritePropertyName("m22"); writer.WriteValue(value.m22);
|
||||
writer.WritePropertyName("m23"); writer.WriteValue(value.m23);
|
||||
writer.WritePropertyName("m30"); writer.WriteValue(value.m30);
|
||||
writer.WritePropertyName("m31"); writer.WriteValue(value.m31);
|
||||
writer.WritePropertyName("m32"); writer.WriteValue(value.m32);
|
||||
writer.WritePropertyName("m33"); writer.WriteValue(value.m33);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
public override Matrix4x4 ReadJson(JsonReader reader, Type objectType, Matrix4x4 existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return new Matrix4x4(); // Return zero matrix for null (consistent with missing field defaults)
|
||||
|
||||
if (reader.TokenType != JsonToken.StartObject)
|
||||
throw new JsonSerializationException($"Expected JSON object or null when deserializing Matrix4x4, got '{reader.TokenType}'.");
|
||||
|
||||
JObject jo = JObject.Load(reader);
|
||||
var matrix = new Matrix4x4();
|
||||
matrix.m00 = jo["m00"]?.Value<float>() ?? 0f;
|
||||
matrix.m01 = jo["m01"]?.Value<float>() ?? 0f;
|
||||
matrix.m02 = jo["m02"]?.Value<float>() ?? 0f;
|
||||
matrix.m03 = jo["m03"]?.Value<float>() ?? 0f;
|
||||
matrix.m10 = jo["m10"]?.Value<float>() ?? 0f;
|
||||
matrix.m11 = jo["m11"]?.Value<float>() ?? 0f;
|
||||
matrix.m12 = jo["m12"]?.Value<float>() ?? 0f;
|
||||
matrix.m13 = jo["m13"]?.Value<float>() ?? 0f;
|
||||
matrix.m20 = jo["m20"]?.Value<float>() ?? 0f;
|
||||
matrix.m21 = jo["m21"]?.Value<float>() ?? 0f;
|
||||
matrix.m22 = jo["m22"]?.Value<float>() ?? 0f;
|
||||
matrix.m23 = jo["m23"]?.Value<float>() ?? 0f;
|
||||
matrix.m30 = jo["m30"]?.Value<float>() ?? 0f;
|
||||
matrix.m31 = jo["m31"]?.Value<float>() ?? 0f;
|
||||
matrix.m32 = jo["m32"]?.Value<float>() ?? 0f;
|
||||
matrix.m33 = jo["m33"]?.Value<float>() ?? 0f;
|
||||
return matrix;
|
||||
}
|
||||
}
|
||||
|
||||
// Converter for UnityEngine.Object references (GameObjects, Components, Materials, Textures, etc.)
|
||||
public class UnityEngineObjectConverter : JsonConverter<UnityEngine.Object>
|
||||
{
|
||||
public override bool CanRead => true; // We need to implement ReadJson
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void WriteJson(JsonWriter writer, UnityEngine.Object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR // AssetDatabase and EditorUtility are Editor-only
|
||||
if (UnityEditor.AssetDatabase.Contains(value))
|
||||
{
|
||||
// It's an asset (Material, Texture, Prefab, etc.)
|
||||
string path = UnityEditor.AssetDatabase.GetAssetPath(value);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
writer.WriteValue(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Asset exists but path couldn't be found? Write minimal info.
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceID());
|
||||
writer.WritePropertyName("isAssetWithoutPath");
|
||||
writer.WriteValue(true);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's a scene object (GameObject, Component, etc.)
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceID());
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
#else
|
||||
// Runtime fallback: Write basic info without AssetDatabase
|
||||
writer.WriteStartObject();
|
||||
writer.WritePropertyName("name");
|
||||
writer.WriteValue(value.name);
|
||||
writer.WritePropertyName("instanceID");
|
||||
writer.WriteValue(value.GetInstanceID());
|
||||
writer.WritePropertyName("warning");
|
||||
writer.WriteValue("UnityEngineObjectConverter running in non-Editor mode, asset path unavailable.");
|
||||
writer.WriteEndObject();
|
||||
#endif
|
||||
}
|
||||
|
||||
public override UnityEngine.Object ReadJson(JsonReader reader, Type objectType, UnityEngine.Object existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
string strValue = reader.Value.ToString();
|
||||
|
||||
// Check if it looks like a GUID (32 hex chars, optionally with hyphens)
|
||||
if (IsValidGuid(strValue))
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(strValue.Replace("-", "").ToLowerInvariant());
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
if (asset != null) return asset;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset with GUID '{strValue}' as type '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Assume it's an asset path
|
||||
var loadedAsset = UnityEditor.AssetDatabase.LoadAssetAtPath(strValue, objectType);
|
||||
if (loadedAsset == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset at path '{strValue}' as type '{objectType.Name}'.");
|
||||
}
|
||||
return loadedAsset;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
JObject jo = JObject.Load(reader);
|
||||
|
||||
// Try to resolve by GUID first (for assets like ScriptableObjects, Materials, etc.)
|
||||
if (jo.TryGetValue("guid", out JToken guidToken) && guidToken.Type == JTokenType.String)
|
||||
{
|
||||
string guid = guidToken.ToString().Replace("-", "").ToLowerInvariant();
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
if (asset != null) return asset;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset with GUID '{guidToken}' as type '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to resolve by instanceID
|
||||
if (jo.TryGetValue("instanceID", out JToken idToken) && idToken.Type == JTokenType.Integer)
|
||||
{
|
||||
int instanceId = idToken.ToObject<int>();
|
||||
UnityEngine.Object obj = UnityEditor.EditorUtility.InstanceIDToObject(instanceId);
|
||||
if (obj != null)
|
||||
{
|
||||
// Direct type match
|
||||
if (objectType.IsAssignableFrom(obj.GetType()))
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Special case: expecting Transform but got GameObject - get its transform
|
||||
if (objectType == typeof(Transform) && obj is GameObject go)
|
||||
{
|
||||
return go.transform;
|
||||
}
|
||||
|
||||
// Special case: expecting a Component type but got GameObject - try to get the component
|
||||
if (typeof(Component).IsAssignableFrom(objectType) && obj is GameObject gameObj)
|
||||
{
|
||||
var component = gameObj.GetComponent(objectType);
|
||||
if (component != null)
|
||||
{
|
||||
return component;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] GameObject '{gameObj.name}' (ID: {instanceId}) does not have a '{objectType.Name}' component.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Type mismatch with no automatic conversion available
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Instance ID {instanceId} resolved to '{obj.GetType().Name}' but expected '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
// Instance ID lookup failed - this can happen if the object was destroyed or ID is stale
|
||||
string objectName = jo.TryGetValue("name", out JToken nameToken) ? nameToken.ToString() : "unknown";
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not resolve instance ID {instanceId} (name: '{objectName}') to a valid {objectType.Name}. The object may have been destroyed or the ID is stale.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there's an asset path in the object
|
||||
if (jo.TryGetValue("path", out JToken pathToken) && pathToken.Type == JTokenType.String)
|
||||
{
|
||||
string path = pathToken.ToString();
|
||||
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(path, objectType);
|
||||
if (asset != null)
|
||||
{
|
||||
return asset;
|
||||
}
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Could not load asset at path '{path}' as type '{objectType.Name}'.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Object format not recognized
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] JSON object missing 'instanceID', 'guid', or 'path' field for {objectType.Name} deserialization. Object: {jo.ToString(Formatting.None)}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Unexpected token type
|
||||
UnityEngine.Debug.LogWarning($"[UnityEngineObjectConverter] Unexpected token type '{reader.TokenType}' when deserializing {objectType.Name}. Expected Null, String, or Object.");
|
||||
return null;
|
||||
#else
|
||||
// Runtime deserialization is tricky without AssetDatabase/EditorUtility
|
||||
UnityEngine.Debug.LogWarning("UnityEngineObjectConverter cannot deserialize complex objects in non-Editor mode.");
|
||||
// Skip the current token to avoid breaking the reader state
|
||||
reader.Skip();
|
||||
// Return existing value since we can't deserialize without Editor APIs
|
||||
return existingValue;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a string looks like a valid GUID (32 hex chars, with or without hyphens).
|
||||
/// </summary>
|
||||
private static bool IsValidGuid(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str)) return false;
|
||||
string normalized = str.Replace("-", "");
|
||||
if (normalized.Length != 32) return false;
|
||||
foreach (char c in normalized)
|
||||
{
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e65311c160f0d41d4a1b45a3dba8dd5a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user