using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Linq;
///
/// SpriteMetaRestorer Editor 窗口
///
/// 功能:将独立 .asset Sprite 文件的分割信息还原到 PNG.meta 的 spriteSheet.sprites 中。
/// 使用方法:
/// 1. 把 PNG 图片拖到"目标 PNG"槽位
/// 2. 把对应的 .asset Sprite 文件拖到列表区域(支持多选拖入)
/// 3. 点击"写入 Meta"完成还原
/// 4. 可选:勾选"写入后删除已融合的 .asset 文件"
///
/// 作者:AI Assistant
///
public class SpriteMetaRestorer : EditorWindow
{
#region 字段
/// 目标 PNG 纹理
private Texture2D targetTexture;
/// 待处理的 Sprite .asset 文件列表
private List assetList = new List();
/// 拖入区域的滚动位置
private Vector2 scrollPos;
/// 处理结果日志
private string logText = "";
private Vector2 logScrollPos;
/// 是否显示日志区域
private bool showLog = false;
/// 写入完成后是否删除已融合的 .asset 文件及其 .meta
private bool deleteAssetsAfterWrite = true;
/// 是否允许写入与 PNG 不匹配的 Sprite(textureGuid 不同)
private bool allowMismatchedSprites = false;
#endregion
#region 数据结构
/// 单个 Sprite .asset 的解析信息
private class SpriteAssetInfo
{
/// .asset 文件路径(Assets/...)
public string assetPath;
/// Sprite 名称(m_Name)
public string name;
/// 裁切矩形
public Rect rect;
/// 轴心点
public Vector2 pivot;
/// 边框
public Vector4 border;
/// spriteID(来自 .asset.meta 的 guid)
public string spriteID;
/// 该 .asset 引用的纹理 guid,用于校验是否与目标 PNG 匹配
public string textureGuid;
/// 解析是否成功
public bool valid;
public SpriteAssetInfo()
{
valid = false;
spriteID = "";
textureGuid = "";
rect = new Rect(0, 0, 0, 0);
pivot = new Vector2(0.5f, 0.5f);
border = new Vector4(0, 0, 0, 0);
}
}
#endregion
#region 菜单入口
[MenuItem("Tools/Sprite Meta Restorer")]
private static void ShowWindow()
{
// 获取已有窗口或创建新窗口
var window = GetWindow("Sprite Meta Restorer");
window.minSize = new Vector2(420, 500);
window.Show();
}
#endregion
#region GUI 绘制
private void OnGUI()
{
DrawHeader();
DrawTargetPNGSlot();
DrawAssetDropZone();
DrawActionBar();
DrawLogArea();
}
///
/// 顶部标题说明
///
private void DrawHeader()
{
EditorGUILayout.Space(8);
EditorGUILayout.HelpBox(
"将 Sprite .asset 文件的分割信息还原到 PNG.meta 的 spriteSheet.sprites 中。\n" +
"1. 拖入目标 PNG 图片\n" +
"2. 拖入对应的 .asset 文件(支持多选)\n" +
"3. 点击「写入 Meta」",
MessageType.Info
);
EditorGUILayout.Space(4);
}
///
/// 目标 PNG 拖拽槽位
///
private void DrawTargetPNGSlot()
{
EditorGUILayout.LabelField("目标 PNG", EditorStyles.boldLabel);
// 使用 ObjectField 支持拖拽和点击选择
Rect dropRect = EditorGUILayout.GetControlRect(GUILayout.Height(60));
GUI.Box(dropRect, "", GUI.skin.box);
var centeredStyle = new GUIStyle(GUI.skin.label)
{
alignment = TextAnchor.MiddleCenter,
fontSize = 12
};
if (targetTexture != null)
{
// 显示已选择的 PNG 信息
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
int texW = targetTexture.width;
int texH = targetTexture.height;
GUI.Label(dropRect, $"{Path.GetFileName(pngPath)}\n{texW} x {texH} px", centeredStyle);
}
else
{
GUI.Label(dropRect, "拖入 PNG 图片 或 点击选择", centeredStyle);
}
// 处理拖拽事件
if (Event.current.type == EventType.DragUpdated && dropRect.Contains(Event.current.mousePosition))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
Event.current.Use();
}
else if (Event.current.type == EventType.DragPerform && dropRect.Contains(Event.current.mousePosition))
{
DragAndDrop.AcceptDrag();
if (DragAndDrop.objectReferences.Length > 0)
{
var obj = DragAndDrop.objectReferences[0] as Texture2D;
if (obj != null)
{
string path = AssetDatabase.GetAssetPath(obj);
// 确保是 PNG 文件而非内部纹理
if (path.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
{
targetTexture = obj;
assetList.Clear();
logText = "";
}
else
{
Debug.LogWarning("[SpriteMetaRestorer] 请拖入 PNG 文件,不是内部纹理");
}
}
}
Event.current.Use();
}
// 点击选择
targetTexture = (Texture2D)EditorGUI.ObjectField(
new Rect(dropRect.x, dropRect.y, dropRect.width, dropRect.height),
targetTexture, typeof(Texture2D), false
);
// 检查选择后是否为 PNG
if (targetTexture != null)
{
string p = AssetDatabase.GetAssetPath(targetTexture);
if (!p.EndsWith(".png", System.StringComparison.OrdinalIgnoreCase))
{
targetTexture = null;
}
}
EditorGUILayout.Space(8);
}
///
/// .asset 文件拖拽列表区域
///
private void DrawAssetDropZone()
{
EditorGUILayout.LabelField($"Sprite Assets ({assetList.Count} 个)", EditorStyles.boldLabel);
// 列表区域
Rect listRect = EditorGUILayout.GetControlRect(false, GUILayout.Height(200));
GUI.Box(listRect, "");
// 空状态提示
if (assetList.Count == 0 && targetTexture != null)
{
// 创建带灰色的 GUIStyle
var hintStyle = new GUIStyle(GUI.skin.label);
hintStyle.alignment = TextAnchor.MiddleCenter;
hintStyle.fontSize = 12;
hintStyle.normal.textColor = Color.gray;
GUI.Label(listRect, "拖入 .asset Sprite 文件到这里\n(支持 Project 窗口多选拖入)", hintStyle);
}
// 处理拖拽
if (Event.current.type == EventType.DragUpdated && listRect.Contains(Event.current.mousePosition))
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
Event.current.Use();
}
else if (Event.current.type == EventType.DragPerform && listRect.Contains(Event.current.mousePosition))
{
DragAndDrop.AcceptDrag();
AddDraggedAssets(DragAndDrop.objectReferences);
Event.current.Use();
}
// 绘制列表(带滚动)
if (assetList.Count > 0)
{
var innerRect = new Rect(listRect.x + 4, listRect.y + 4, listRect.width - 8, listRect.height - 8);
scrollPos = GUI.BeginScrollView(innerRect, scrollPos, new Rect(0, 0, innerRect.width - 16, assetList.Count * 22));
for (int i = 0; i < assetList.Count; i++)
{
var info = assetList[i];
Rect rowRect = new Rect(0, i * 22, innerRect.width, 20);
// 背景色:有效=正常,无效=红底
Color origBg = GUI.backgroundColor;
GUI.backgroundColor = info.valid ? Color.green : Color.red;
// 名称
float nameW = rowRect.width * 0.35f;
GUI.Label(new Rect(rowRect.x, rowRect.y, nameW, 20), info.name);
// 坐标
float coordW = rowRect.width * 0.35f;
string coordText = $"({info.rect.x:F0},{info.rect.y:F0},{info.rect.width:F0}x{info.rect.height:F0})";
GUI.Label(new Rect(rowRect.x + nameW, rowRect.y, coordW, 20), coordText);
// 匹配状态
float statusW = rowRect.width * 0.15f;
if (targetTexture != null)
{
string pngGuid = GetAssetGuid(AssetDatabase.GetAssetPath(targetTexture) + ".meta");
bool matched = info.textureGuid == pngGuid;
// 正确方式:通过 GUIStyle.normal.textColor 设置颜色
var statusStyle = new GUIStyle(GUI.skin.label);
statusStyle.fontSize = 10;
statusStyle.normal.textColor = matched ? Color.green : Color.red;
GUI.Label(new Rect(rowRect.x + nameW + coordW, rowRect.y, statusW, 20),
matched ? "✓ 匹配" : "✗ 不匹配", statusStyle);
}
// 删除按钮
float btnW = rowRect.width * 0.12f;
if (GUI.Button(new Rect(rowRect.x + rowRect.width - btnW, rowRect.y, btnW, 20), "删除"))
{
assetList.RemoveAt(i);
i--;
}
GUI.backgroundColor = origBg;
}
GUI.EndScrollView();
}
EditorGUILayout.Space(6);
}
///
/// 操作按钮区
///
private void DrawActionBar()
{
EditorGUILayout.BeginHorizontal();
// 写入按钮
GUI.enabled = targetTexture != null && assetList.Count > 0;
if (GUILayout.Button("写入 Meta", GUILayout.Height(32)))
{
WriteSpritesToMeta();
}
GUI.enabled = true;
// 清空按钮
if (GUILayout.Button("清空列表", GUILayout.Height(32)))
{
assetList.Clear();
logText = "";
}
// 清空全部
if (GUILayout.Button("全部重置", GUILayout.Height(32)))
{
targetTexture = null;
assetList.Clear();
logText = "";
}
// 新增:删除所有与 PNG 不匹配的 Sprite
if (targetTexture != null && assetList.Count > 0)
{
if (GUILayout.Button("删除不匹配的 Sprite", GUILayout.Height(32)))
{
RemoveMismatchedSprites();
}
}
EditorGUILayout.EndHorizontal();
// 自动检测按钮
EditorGUILayout.Space(4);
if (GUILayout.Button("自动检测同目录 .asset 文件", GUILayout.Height(28)))
{
AutoDetectAssets();
}
// 删除 asset 选项
EditorGUILayout.Space(6);
deleteAssetsAfterWrite = EditorGUILayout.Toggle(
new GUIContent("写入后删除已融合的 .asset 文件",
"勾选后,写入 Meta 成功会自动删除列表中的 .asset 及其 .meta 文件"),
deleteAssetsAfterWrite
);
// 新增:是否允许写入与 PNG 不匹配的 Sprite
allowMismatchedSprites = EditorGUILayout.Toggle(
new GUIContent("允许写入与 PNG 不匹配的 Sprite",
"勾选:即使 Sprite 引用的纹理 guid 与当前 PNG 不同,也照样按其 rect 写入。\n不勾选:写入时会跳过这些不匹配的 Sprite。"),
allowMismatchedSprites
);
EditorGUILayout.Space(4);
}
///
/// 日志区域
///
private void DrawLogArea()
{
if (string.IsNullOrEmpty(logText)) return;
showLog = EditorGUILayout.Foldout(showLog, "处理日志");
if (showLog)
{
EditorGUILayout.BeginVertical(GUI.skin.box);
logScrollPos = EditorGUILayout.BeginScrollView(logScrollPos, GUILayout.Height(150));
EditorGUILayout.TextArea(logText, EditorStyles.wordWrappedLabel, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
}
#endregion
#region 拖拽处理
///
/// 处理从 Project 窗口拖入的对象
///
private void AddDraggedAssets(Object[] objects)
{
int added = 0;
int skipped = 0;
foreach (var obj in objects)
{
string path = AssetDatabase.GetAssetPath(obj);
// 只处理 .asset 文件
if (string.IsNullOrEmpty(path) ||
!path.EndsWith(".asset", System.StringComparison.OrdinalIgnoreCase))
{
skipped++;
continue;
}
// 检查是否已存在
if (assetList.Any(a => a.assetPath == path))
{
skipped++;
continue;
}
// 解析 .asset 文件
var info = ParseSpriteAsset(path);
if (info != null && info.valid)
{
assetList.Add(info);
added++;
}
else
{
Debug.LogWarning($"[SpriteMetaRestorer] 无法解析: {path}");
skipped++;
}
}
if (added > 0)
Debug.Log($"[SpriteMetaRestorer] 添加 {added} 个 .asset,跳过 {skipped} 个");
}
#endregion
#region 自动检测
///
/// 自动检测目标 PNG 同目录下的所有 Sprite 类型 .asset 文件
///
private void AutoDetectAssets()
{
if (targetTexture == null)
{
EditorUtility.DisplayDialog("提示", "请先选择目标 PNG", "确定");
return;
}
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
string pngDir = Path.GetDirectoryName(pngPath).Replace("\\", "/");
string pngGuid = GetAssetGuid(pngPath + ".meta");
if (string.IsNullOrEmpty(pngGuid))
{
EditorUtility.DisplayDialog("错误", "无法获取 PNG 的 guid", "确定");
return;
}
// 查找同目录下所有 .asset 文件
string[] assetFiles = Directory.GetFiles(pngDir, "*.asset");
int added = 0;
foreach (string file in assetFiles)
{
string assetPath = file.Replace("\\", "/");
// 跳过已存在的
if (assetList.Any(a => a.assetPath == assetPath))
continue;
// 解析并检查是否为 Sprite 类型且匹配当前 PNG
var info = ParseSpriteAsset(assetPath);
if (info != null && info.valid)
{
// 检查 textureGuid 是否匹配
if (info.textureGuid == pngGuid)
{
assetList.Add(info);
added++;
}
}
}
Debug.Log($"[SpriteMetaRestorer] 自动检测到 {added} 个匹配的 .asset 文件");
Repaint();
}
#endregion
#region .asset 解析
///
/// 解析单个 Sprite .asset 文件,提取名称、rect、pivot、border、textureGuid
///
private SpriteAssetInfo ParseSpriteAsset(string assetPath)
{
string fullPath = Path.GetFullPath(assetPath);
if (!File.Exists(fullPath))
return null;
string text = File.ReadAllText(fullPath, Encoding.UTF8);
// 检查是否是 Sprite 类型(!u!213),而非 MonoBehaviour(!u!114)
// Split by --- and check second document
string[] docs = text.Split(new[] { "---" }, System.StringSplitOptions.None);
if (docs.Length < 2 || !docs[1].Contains("!u!213"))
return null;
var info = new SpriteAssetInfo();
info.assetPath = assetPath;
// 提取 m_Name
info.name = ExtractField(text, "m_Name");
if (string.IsNullOrEmpty(info.name))
info.name = Path.GetFileNameWithoutExtension(assetPath);
// 提取 m_Rect
info.rect = ExtractRect(text);
// 提取 m_Pivot
info.pivot = ExtractVec2(text, "m_Pivot");
// 提取 m_Border
info.border = ExtractVec4(text, "m_Border");
// 提取 texture guid
info.textureGuid = ExtractTextureGuid(text);
// 提取 .asset.meta 的 guid 作为 spriteID
string metaPath = assetPath + ".meta";
if (File.Exists(metaPath))
{
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
info.spriteID = GetGuidFromText(metaText);
}
// 基本校验
info.valid = info.rect.width > 0 && info.rect.height > 0;
return info;
}
/// 提取 m_Name 字段值
private string ExtractField(string text, string fieldName)
{
int idx = text.IndexOf(fieldName + ":");
if (idx < 0) return "";
int start = idx + fieldName.Length;
// 跳过冒号后的空格
while (start < text.Length && (text[start] == ':' || text[start] == ' ' || text[start] == '\t'))
start++;
int end = start;
while (end < text.Length && text[end] != '\n' && text[end] != '\r')
end++;
return text.Substring(start, end - start).Trim();
}
/// 提取 m_Rect 块
private Rect ExtractRect(string text)
{
string marker = "m_Rect:";
int idx = text.IndexOf(marker);
if (idx < 0) return new Rect(0, 0, 0, 0);
// 找到 marker 之后的 x/y/width/height
string block = text.Substring(idx);
float x = ExtractFloatAfter(block, "x:");
float y = ExtractFloatAfter(block, "y:");
float w = ExtractFloatAfter(block, "width:");
float h = ExtractFloatAfter(block, "height:");
return new Rect(x, y, w, h);
}
/// 提取 m_Pivot 二维向量
private Vector2 ExtractVec2(string text, string fieldName)
{
string marker = fieldName + ":";
int idx = text.IndexOf(marker);
if (idx < 0) return new Vector2(0.5f, 0.5f);
string block = text.Substring(idx);
float x = ExtractFloatAfter(block, "x:");
float y = ExtractFloatAfter(block, "y:");
return new Vector2(x, y);
}
/// 提取 m_Border 四维向量
private Vector4 ExtractVec4(string text, string fieldName)
{
string marker = fieldName + ":";
int idx = text.IndexOf(marker);
if (idx < 0) return new Vector4(0, 0, 0, 0);
string block = text.Substring(idx);
float x = ExtractFloatAfter(block, "x:");
float y = ExtractFloatAfter(block, "y:");
float z = ExtractFloatAfter(block, "z:");
float w = ExtractFloatAfter(block, "w:");
return new Vector4(x, y, z, w);
}
/// 提取 m_RD.texture 中的 guid
private string ExtractTextureGuid(string text)
{
int idx = text.IndexOf("texture:");
if (idx < 0) return "";
string block = text.Substring(idx);
int guidIdx = block.IndexOf("guid:");
if (guidIdx < 0) return "";
int start = guidIdx + 5;
int end = start;
while (end < block.Length && block[end] != ',' && block[end] != '}' && block[end] != '\n')
end++;
return block.Substring(start, end - start).Trim();
}
/// 在文本中提取指定字段后的浮点数
private float ExtractFloatAfter(string text, string fieldName)
{
int idx = text.IndexOf(fieldName);
if (idx < 0) return 0f;
int start = idx + fieldName.Length;
// 跳过到数字
while (start < text.Length && !char.IsDigit(text[start]) && text[start] != '-' && text[start] != '.')
start++;
int end = start;
while (end < text.Length && (char.IsDigit(text[end]) || text[end] == '.' || text[end] == '-'
|| text[end] == 'e' || text[end] == 'E' || text[end] == '+'))
end++;
float result;
if (float.TryParse(text.Substring(start, end - start), System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out result))
return result;
return 0f;
}
#endregion
#region Meta 文件操作
///
/// 删除所有与当前 PNG 不匹配的 Sprite(textureGuid 不同)
///
private void RemoveMismatchedSprites()
{
if (targetTexture == null || assetList.Count == 0)
return;
string pngGuid = GetAssetGuid(AssetDatabase.GetAssetPath(targetTexture) + ".meta");
if (string.IsNullOrEmpty(pngGuid))
return;
int before = assetList.Count;
assetList.RemoveAll(a => a.textureGuid != pngGuid);
int removed = before - assetList.Count;
if (removed > 0)
{
Debug.Log($"[SpriteMetaRestorer] 已删除 {removed} 个与目标 PNG 不匹配的 Sprite");
Repaint();
}
}
///
/// 将解析到的 sprite 列表写入 .png.meta 的 spriteSheet.sprites 块
///
private void WriteSpritesToMeta()
{
if (targetTexture == null || assetList.Count == 0)
return;
string pngPath = AssetDatabase.GetAssetPath(targetTexture);
string metaPath = pngPath + ".meta";
if (!File.Exists(metaPath))
{
EditorUtility.DisplayDialog("错误", $"找不到 meta 文件:\n{metaPath}", "确定");
return;
}
string metaText = File.ReadAllText(metaPath, Encoding.UTF8);
StringBuilder sb = new StringBuilder();
string pngGuid = GetAssetGuid(metaPath);
// 构建 sprites YAML 文本
StringBuilder spritesYaml = new StringBuilder();
List nameFileIdEntries = new List();
foreach (var info in assetList)
{
if (!info.valid)
continue;
// ★★★ 新增:不匹配 PNG 时的处理逻辑 ★★★
bool matched = info.textureGuid == pngGuid;
if (!matched && !allowMismatchedSprites)
{
sb.AppendLine($" [跳过] {info.name}(textureGuid 不匹配当前 PNG)");
continue;
}
// ★★★ 原有写入逻辑保持不变 ★★★
spritesYaml.AppendLine(" - serializedVersion: 2");
spritesYaml.AppendLine($" name: {info.name}");
spritesYaml.AppendLine(" rect:");
spritesYaml.AppendLine(" serializedVersion: 2");
spritesYaml.AppendLine($" x: {FormatFloat(info.rect.x)}");
spritesYaml.AppendLine($" y: {FormatFloat(info.rect.y)}");
spritesYaml.AppendLine($" width: {FormatFloat(info.rect.width)}");
spritesYaml.AppendLine($" height: {FormatFloat(info.rect.height)}");
spritesYaml.AppendLine(" alignment: 0");
spritesYaml.AppendLine($" pivot: {{x: {FormatFloat(info.pivot.x)}, y: {FormatFloat(info.pivot.y)}}}");
spritesYaml.AppendLine($" border: {{x: {FormatFloat(info.border.x)}, y: {FormatFloat(info.border.y)}, z: {FormatFloat(info.border.z)}, w: {FormatFloat(info.border.w)}}}");
spritesYaml.AppendLine(" outline: []");
spritesYaml.AppendLine(" physicsShape: []");
spritesYaml.AppendLine(" tessellationDetail: -1");
spritesYaml.AppendLine(" bones: []");
spritesYaml.AppendLine($" spriteID: {info.spriteID}");
spritesYaml.AppendLine(" internalID: 0");
spritesYaml.AppendLine(" vertices: []");
spritesYaml.AppendLine(" indices: ");
spritesYaml.AppendLine(" edges: []");
spritesYaml.AppendLine(" weights: []");
nameFileIdEntries.Add($" {info.name}: 0");
sb.AppendLine($" [+] {info.name} rect=({info.rect.x:F0},{info.rect.y:F0},{info.rect.width:F0}x{info.rect.height:F0})");
}
string spritesBlock = spritesYaml.ToString().TrimEnd('\n', '\r');
string spritesContent = " sprites:\n" + spritesBlock + "\n";
// 替换 meta 中的 sprites 块(保持你原来的逻辑)
string result;
if (metaText.Contains("sprites: []"))
{
result = ReplaceFirst(metaText, " sprites: []", spritesContent);
}
else
{
int idx = metaText.IndexOf(" sprites:");
if (idx >= 0)
{
int endIdx = FindBlockEnd(metaText, idx);
result = metaText.Substring(0, idx) + spritesContent + metaText.Substring(endIdx);
}
else
{
result = metaText;
}
}
// 更新 nameFileIdTable(保持你原来的逻辑)
if (nameFileIdEntries.Count > 0)
{
string tableBlock = " nameFileIdTable:\n" + string.Join("\n", nameFileIdEntries) + "\n";
int tblIdx = result.IndexOf(" nameFileIdTable:");
if (tblIdx >= 0)
{
int tblEndIdx = FindBlockEnd(result, tblIdx);
result = result.Substring(0, tblIdx) + tableBlock + result.Substring(tblEndIdx);
}
}
File.WriteAllText(metaPath, result, Encoding.UTF8);
AssetDatabase.ImportAsset(pngPath);
int validCount = assetList.Count(a => a.valid);
sb.AppendLine($"\n[完成] 已写入 {validCount} 个 Sprite 到:");
sb.AppendLine($" {metaPath}");
sb.AppendLine("\n→ Unity 已自动 Reimport,请在 Inspector 中确认。");
logText = sb.ToString();
showLog = true;
}
///
/// 替换字符串中第一次出现的 oldText
///
private string ReplaceFirst(string text, string oldText, string newText)
{
int idx = text.IndexOf(oldText);
if (idx < 0) return text;
return text.Substring(0, idx) + newText + text.Substring(idx + oldText.Length);
}
///
/// 从 blockStartIndex 开始,找到当前 YAML 块的结束位置。
/// 块以 " keyName:" 开头,后续行要么以 " - " 继续列表项,
/// 要么以更多缩进(6+ 空格或 " ")作为子属性。
/// 遇到同级缩进(4空格+非"-"非空格)的行时结束。
///
private int FindBlockEnd(string text, int blockStartIndex)
{
int i = blockStartIndex;
// 跳过 " sprites:" 这一行
int lineEnd = text.IndexOf('\n', i);
if (lineEnd < 0) return text.Length;
i = lineEnd + 1;
while (i < text.Length)
{
// 找到当前行开头
int nextNewline = text.IndexOf('\n', i);
if (nextNewline < 0) nextNewline = text.Length;
string line = text.Substring(i, nextNewline - i);
// 空行不结束块(跳过)
if (line.Trim().Length == 0)
{
i = nextNewline + 1;
continue;
}
// 以 " - " 开头 → 仍是列表项,继续
if (line.StartsWith(" - "))
{
i = nextNewline + 1;
continue;
}
// 以 " "(6空格)开头 → 子属性,继续
if (line.StartsWith(" "))
{
i = nextNewline + 1;
continue;
}
// 以 " "(4空格)开头但不是 "- " → 下一个同级字段,块结束
if (line.StartsWith(" ") && !line.StartsWith(" - ") && !line.StartsWith(" "))
{
return i;
}
// 其他情况(缩进更少),块结束
return i;
}
return text.Length;
}
///
/// 格式化浮点数:整数值不带小数点
///
private string FormatFloat(float v)
{
if (v == Mathf.RoundToInt(v))
return Mathf.RoundToInt(v).ToString();
return v.ToString("G7", System.Globalization.CultureInfo.InvariantCulture);
}
#endregion
#region 工具方法
///
/// 从 meta 文本提取 guid
///
private string GetAssetGuid(string metaPath)
{
if (!File.Exists(metaPath)) return "";
string text = File.ReadAllText(metaPath, Encoding.UTF8);
return GetGuidFromText(text);
}
private string GetGuidFromText(string text)
{
int idx = text.IndexOf("guid:");
if (idx < 0) return "";
int start = idx + 5;
int end = start;
while (end < text.Length && text[end] != '\n' && text[end] != '\r')
end++;
return text.Substring(start, end - start).Trim();
}
#endregion
}