Files
aibis-dream/Assets/Editor/Window/AnimationClipGeneratorCore.cs
dingyuntian 04536c4b81 fix(editor): 修复 Aseprite JSON 尾随逗号解析问题
- 移除 AsepriteMeta 中未使用的 layers/slices 字段
- 新增 SanitizeJsonForUnity 正则清理尾随逗号
- 修复 Aseprite 导出 JSON 偶发尾随逗号导致 JsonUtility 解析失败
2026-07-04 23:12:30 +08:00

689 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.U2D.Sprites;
using UnityEngine;
namespace AibisDream.SystemEditor
{
public static class AnimationClipGeneratorCore
{
// 检查SpriteMode
public static SpriteImportMode CheckSpriteMode(string texturePath)
{
TextureImporter textureImporter = AssetImporter.GetAtPath(texturePath) as TextureImporter;
if (textureImporter == null)
{
throw new Exception($"无法加载纹理导入器: {texturePath}");
}
return textureImporter.spriteImportMode;
}
// 解析Aseprite JSON
public static ProcessedAnimationData ParseAsepriteJson(string jsonPath, string texturePath)
{
string jsonContent = File.ReadAllText(jsonPath);
// Unity的JsonUtility不支持Dictionary,需要手动解析frames对象
// 使用SimpleJSON或手动解析
ProcessedAnimationData result = ParseAsepriteJsonManual(jsonContent);
if (result.frames == null || result.frames.Count == 0)
{
throw new Exception("无效的Aseprite JSON格式:无法解析frames");
}
return result;
}
private static ProcessedAnimationData ParseAsepriteJsonManual(string jsonContent)
{
List<FrameData> frames = new List<FrameData>();
List<ProcessedAnimationClip> animations = new List<ProcessedAnimationClip>();
try
{
// 解析meta部分
int metaStart = jsonContent.IndexOf("\"meta\":");
if (metaStart == -1)
{
throw new Exception("找不到meta部分");
}
// 找到meta对象的结束位置
int metaBraceStart = jsonContent.IndexOf('{', metaStart);
int metaBraceEnd = FindMatchingBrace(jsonContent, metaBraceStart);
string metaJson = jsonContent.Substring(metaBraceStart, metaBraceEnd - metaBraceStart + 1);
metaJson = SanitizeJsonForUnity(metaJson);
AsepriteMeta meta = JsonUtility.FromJson<AsepriteMeta>(metaJson);
// 解析frameTags
if (meta.frameTags != null)
{
foreach (var tag in meta.frameTags)
{
List<int> frameIndices = new List<int>();
int start = tag.from;
int end = tag.to;
if (tag.direction == "forward" || tag.direction == "pingpong" || string.IsNullOrEmpty(tag.direction))
{
for (int i = start; i <= end; i++)
{
frameIndices.Add(i);
}
}
else if (tag.direction == "reverse")
{
for (int i = end; i >= start; i--)
{
frameIndices.Add(i);
}
}
animations.Add(new ProcessedAnimationClip
{
name = tag.name,
frameIndices = frameIndices,
loop = true,
frameDuration = 0,
sampleRate = 100
});
}
}
// 解析frames对象 - 使用正则表达式或手动解析
int framesStart = jsonContent.IndexOf("\"frames\":");
if (framesStart == -1)
{
throw new Exception("找不到frames部分");
}
// 找到frames对象的开始和结束
int framesBraceStart = jsonContent.IndexOf('{', framesStart);
int framesBraceEnd = FindMatchingBrace(jsonContent, framesBraceStart);
string framesContent = jsonContent.Substring(framesBraceStart + 1, framesBraceEnd - framesBraceStart - 1);
// 解析每个帧条目
ParseFramesContent(framesContent, frames);
// 按名称排序帧(确保顺序正确)
frames = frames.OrderBy(f => ExtractFrameNumber(f.name)).ThenBy(f => f.name).ToList();
}
catch (Exception e)
{
throw new Exception($"解析Aseprite JSON失败: {e.Message}\n{e.StackTrace}");
}
return new ProcessedAnimationData
{
frames = frames,
animations = animations
};
}
private static void ParseFramesContent(string framesContent, List<FrameData> frames)
{
int pos = 0;
while (pos < framesContent.Length)
{
// 跳过空白字符和逗号
while (pos < framesContent.Length && (char.IsWhiteSpace(framesContent[pos]) || framesContent[pos] == ','))
pos++;
if (pos >= framesContent.Length) break;
// 查找帧名称(在引号中)
if (framesContent[pos] != '"')
{
pos++;
continue;
}
int nameStart = pos + 1;
int nameEnd = framesContent.IndexOf('"', nameStart);
if (nameEnd == -1) break;
string frameName = framesContent.Substring(nameStart, nameEnd - nameStart);
pos = nameEnd + 1;
// 跳过冒号和空白
while (pos < framesContent.Length && (char.IsWhiteSpace(framesContent[pos]) || framesContent[pos] == ':'))
pos++;
if (pos >= framesContent.Length) break;
// 查找帧数据对象
if (framesContent[pos] != '{')
{
continue;
}
int dataStart = pos;
int dataEnd = FindMatchingBrace(framesContent, dataStart);
if (dataEnd == -1) break;
string frameDataJson = framesContent.Substring(dataStart, dataEnd - dataStart + 1);
pos = dataEnd + 1;
try
{
AsepriteFrameInfo frameInfo = JsonUtility.FromJson<AsepriteFrameInfo>(SanitizeJsonForUnity(frameDataJson));
if (frameInfo.frame != null)
{
frames.Add(new FrameData
{
name = frameName,
x = frameInfo.frame.x,
y = frameInfo.frame.y,
width = frameInfo.frame.w,
height = frameInfo.frame.h,
duration = frameInfo.duration
});
}
}
catch (Exception e)
{
Debug.LogWarning($"解析帧数据失败: {frameName}, 错误: {e.Message}");
}
}
}
private static int ExtractFrameNumber(string frameName)
{
// 尝试从帧名称中提取数字(例如 "调酒妹侧头 0.aseprite" -> 0
int lastSpace = frameName.LastIndexOf(' ');
if (lastSpace >= 0 && lastSpace < frameName.Length - 1)
{
string numberPart = frameName.Substring(lastSpace + 1);
int dotIndex = numberPart.IndexOf('.');
if (dotIndex > 0)
{
numberPart = numberPart.Substring(0, dotIndex);
}
if (int.TryParse(numberPart, out int number))
{
return number;
}
}
return int.MaxValue; // 如果无法解析,放在最后
}
/// <summary>
/// Unity JsonUtility 不支持尾随逗号,Aseprite 导出时偶尔会带上。
/// </summary>
private static string SanitizeJsonForUnity(string json)
{
return Regex.Replace(json, @",(\s*[}\]])", "$1");
}
private static int FindMatchingBrace(string str, int startIndex)
{
if (startIndex < 0 || startIndex >= str.Length) return -1;
int braceCount = 0;
bool foundStart = false;
for (int i = startIndex; i < str.Length; i++)
{
if (str[i] == '{')
{
braceCount++;
foundStart = true;
}
else if (str[i] == '}')
{
braceCount--;
if (braceCount == 0 && foundStart)
{
return i;
}
}
}
return -1;
}
// 解析手动JSON
public static ProcessedAnimationData ParseManualJson(string jsonPath, string texturePath)
{
string jsonContent = File.ReadAllText(jsonPath);
ManualJsonData manualData = JsonUtility.FromJson<ManualJsonData>(jsonContent);
if (manualData.layout == null || manualData.animations == null)
{
throw new Exception("无效的手动JSON格式");
}
// 验证layout参数
if (manualData.layout.rows <= 0 || manualData.layout.columns <= 0)
{
throw new Exception("rows和columns必须大于0");
}
// 读取图片尺寸
TextureImporter textureImporter = AssetImporter.GetAtPath(texturePath) as TextureImporter;
if (textureImporter == null)
{
throw new Exception($"无法加载纹理导入器: {texturePath}");
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
if (texture == null)
{
throw new Exception($"无法加载纹理: {texturePath}");
}
int imageWidth = texture.width;
int imageHeight = texture.height;
// 计算sprite尺寸
int spriteWidth = imageWidth / manualData.layout.columns;
int spriteHeight = imageHeight / manualData.layout.rows;
// 验证图片尺寸能被行列数整除
if (imageWidth % manualData.layout.columns != 0 || imageHeight % manualData.layout.rows != 0)
{
throw new Exception($"图片尺寸({imageWidth}x{imageHeight})无法被行列数({manualData.layout.columns}x{manualData.layout.rows})整除");
}
// 计算总帧数
int totalFrames = manualData.layout.frameCount ?? (manualData.layout.rows * manualData.layout.columns);
if (totalFrames > manualData.layout.rows * manualData.layout.columns)
{
throw new Exception($"frameCount({totalFrames})不能超过rows*columns({manualData.layout.rows * manualData.layout.columns})");
}
// 生成帧数据
List<FrameData> frames = new List<FrameData>();
bool isHorizontal = manualData.layout.direction == "horizontal";
for (int frameIndex = 0; frameIndex < totalFrames; frameIndex++)
{
int row, column;
if (isHorizontal)
{
// 横向排列:按行扫描,从左到右,从上到下
row = frameIndex / manualData.layout.columns;
column = frameIndex % manualData.layout.columns;
}
else
{
// 纵向排列:按列扫描,从上到下,从左到右
column = frameIndex / manualData.layout.rows;
row = frameIndex % manualData.layout.rows;
}
// 使用左上角为(0,0)的坐标系,向右向下为正轴
// 在SliceSprites中会统一转换为Unity的左下角坐标系
int x = column * spriteWidth;
int y = row * spriteHeight;
frames.Add(new FrameData
{
name = $"frame_{frameIndex}",
x = x,
y = y,
width = spriteWidth,
height = spriteHeight,
duration = manualData.frameDuration
});
}
// 处理动画片段
List<ProcessedAnimationClip> animations = new List<ProcessedAnimationClip>();
foreach (var anim in manualData.animations)
{
// 应用参数覆盖逻辑
float frameDuration = anim.frameDuration ?? manualData.frameDuration;
int sampleRate = anim.sampleRate ?? manualData.sampleRate;
animations.Add(new ProcessedAnimationClip
{
name = anim.name,
frameIndices = anim.frameIndices,
loop = anim.loop,
frameDuration = frameDuration,
sampleRate = sampleRate
});
}
return new ProcessedAnimationData
{
frames = frames,
animations = animations
};
}
// 根据JSON信息切图
public static void SliceSprites(string texturePath, List<FrameData> frames, Vector2 pivot)
{
TextureImporter textureImporter = AssetImporter.GetAtPath(texturePath) as TextureImporter;
if (textureImporter == null)
{
throw new Exception($"无法加载纹理导入器: {texturePath}");
}
// 获取纹理尺寸
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(texturePath);
if (texture == null)
{
throw new Exception($"无法加载纹理: {texturePath}");
}
int textureHeight = texture.height;
// 设置spriteMode为Multiple
textureImporter.spriteImportMode = SpriteImportMode.Multiple;
// 使用新的Sprite Editor Data Provider API (Unity 2020.2+)
var spriteDataProviderFactories = new SpriteDataProviderFactories();
spriteDataProviderFactories.Init();
var dataProvider = spriteDataProviderFactories.GetSpriteEditorDataProviderFromObject(textureImporter);
if (dataProvider == null)
{
throw new Exception($"无法获取Sprite Editor Data Provider: {texturePath}");
}
dataProvider.InitSpriteEditorDataProvider();
// 创建SpriteRect数组
// 注意:JSON中的坐标是以左上角为(0,0),向右向下为正轴
// Unity的纹理坐标系统是左下角为(0,0),向上为正轴
// 需要将Y坐标从左上角坐标系转换为Unity的左下角坐标系
var spriteRects = new SpriteRect[frames.Count];
for (int i = 0; i < frames.Count; i++)
{
var frame = frames[i];
// 坐标转换:从左上角(0,0)坐标系转换为Unity左下角(0,0)坐标系
// Unity Y = 纹理高度 - (原始Y + 高度)
float unityY = textureHeight - (frame.y + frame.height);
// 根据 pivot 值确定 alignment
SpriteAlignment alignment = SpriteAlignment.Custom;
if (pivot == new Vector2(0.5f, 0.5f))
alignment = SpriteAlignment.Center;
else if (pivot == new Vector2(0f, 1f))
alignment = SpriteAlignment.TopLeft;
else if (pivot == new Vector2(1f, 1f))
alignment = SpriteAlignment.TopRight;
else if (pivot == new Vector2(0f, 0f))
alignment = SpriteAlignment.BottomLeft;
else if (pivot == new Vector2(1f, 0f))
alignment = SpriteAlignment.BottomRight;
else if (pivot == new Vector2(0.5f, 1f))
alignment = SpriteAlignment.TopCenter;
else if (pivot == new Vector2(0.5f, 0f))
alignment = SpriteAlignment.BottomCenter;
else if (pivot == new Vector2(0f, 0.5f))
alignment = SpriteAlignment.LeftCenter;
else if (pivot == new Vector2(1f, 0.5f))
alignment = SpriteAlignment.RightCenter;
spriteRects[i] = new SpriteRect
{
name = frame.name,
rect = new Rect(frame.x, unityY, frame.width, frame.height),
pivot = pivot,
alignment = alignment
};
}
dataProvider.SetSpriteRects(spriteRects);
dataProvider.Apply();
AssetDatabase.ImportAsset(texturePath, ImportAssetOptions.ForceUpdate);
}
// 创建或更新 AnimationClip(若已存在则在原资源上就地更新,避免引用丢失)
public static AnimationClip CreateAnimationClip(
ProcessedAnimationClip animationData,
List<FrameData> frames,
Sprite[] sprites,
string outputPath,
bool isImageAnimation = false)
{
// 先计算目标资源路径
string fullPath = Path.Combine(outputPath, $"{animationData.name}.anim");
string assetPath = fullPath.Replace('\\', '/');
if (!assetPath.StartsWith("Assets/"))
{
// 转换为相对路径
string projectPath = Application.dataPath.Replace("Assets", "");
if (assetPath.StartsWith(projectPath))
{
assetPath = assetPath.Substring(projectPath.Length);
}
}
// 如果已经存在同名 AnimationClip,则复用该资源,在其上就地更新
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(assetPath);
bool isNewClip = false;
if (clip == null)
{
clip = new AnimationClip();
isNewClip = true;
}
clip.name = animationData.name;
// 设置采样率
AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(clip);
clip.frameRate = animationData.sampleRate;
// 创建Sprite关键帧
List<ObjectReferenceKeyframe> keyframes = new List<ObjectReferenceKeyframe>();
float currentTime = 0f;
foreach (int frameIndex in animationData.frameIndices)
{
if (frameIndex < 0 || frameIndex >= sprites.Length)
{
Debug.LogWarning($"帧索引{frameIndex}超出范围,跳过");
continue;
}
Sprite sprite = sprites[frameIndex];
if (sprite == null)
{
Debug.LogWarning($"帧索引{frameIndex}的Sprite为空,跳过");
continue;
}
// 使用帧的duration(如果是Aseprite格式)或动画的frameDuration(手动格式)
float frameDuration = frames[frameIndex].duration > 0
? frames[frameIndex].duration / 1000f // 毫秒转秒
: animationData.frameDuration / 1000f;
keyframes.Add(new ObjectReferenceKeyframe
{
time = currentTime,
value = sprite
});
currentTime += frameDuration;
}
if (keyframes.Count == 0)
{
throw new Exception($"动画片段{animationData.name}没有有效的关键帧");
}
// 设置循环
if (animationData.loop && keyframes.Count > 1)
{
// 添加最后一帧的重复,确保循环
keyframes.Add(new ObjectReferenceKeyframe
{
time = currentTime,
value = keyframes[^1].value
});
}
// 创建EditorCurveBinding - 根据动画类型选择不同的组件
EditorCurveBinding curveBinding = new EditorCurveBinding
{
path = "",
type = isImageAnimation ? typeof(UnityEngine.UI.Image) : typeof(SpriteRenderer),
propertyName = "m_Sprite"
};
// 设置曲线
AnimationUtility.SetObjectReferenceCurve(clip, curveBinding, keyframes.ToArray());
// 设置循环
settings.loopTime = animationData.loop;
AnimationUtility.SetAnimationClipSettings(clip, settings);
// 强制更新动画长度 (m_StopTime) - 修复复用已有 clip 时长度未更新的问题
float totalDuration = keyframes.Count > 0 ? keyframes[keyframes.Count - 1].time : 0f;
SerializedObject serializedClip = new SerializedObject(clip);
SerializedProperty nestedStopTimeProp = serializedClip.FindProperty("m_AnimationClipSettings.m_StopTime");
if (nestedStopTimeProp != null)
{
nestedStopTimeProp.floatValue = totalDuration;
serializedClip.ApplyModifiedProperties();
}
// 保存 / 更新 AnimationClip 资源
if (isNewClip)
{
// 不存在则创建新资源
AssetDatabase.CreateAsset(clip, assetPath);
}
else
{
// 已存在则标记为已修改,保持原 GUID 与引用
EditorUtility.SetDirty(clip);
}
AssetDatabase.SaveAssets();
return clip;
}
// 主流程:生成AnimationClip
public static void GenerateClips(string texturePath, string jsonPath, string outputPath, bool isImageAnimation = false, Vector2 pivot = default)
{
// 如果没有提供 pivot,使用默认值(中心)
if (pivot == default)
{
pivot = new Vector2(0.5f, 0.5f);
}
// 确保 pivot 值在有效范围内
pivot.x = Mathf.Clamp01(pivot.x);
pivot.y = Mathf.Clamp01(pivot.y);
try
{
EditorUtility.DisplayProgressBar("生成AnimationClip", "检查SpriteMode...", 0f);
// 检查SpriteMode
SpriteImportMode spriteMode = CheckSpriteMode(texturePath);
if (spriteMode == SpriteImportMode.Multiple)
{
Debug.Log("图片的SpriteMode已经是Multiple,将使用现有的Sprite切图");
}
EditorUtility.DisplayProgressBar("生成AnimationClip", "解析JSON...", 0.2f);
// 判断JSON类型并解析
ProcessedAnimationData processedData;
string jsonContent = File.ReadAllText(jsonPath);
if (jsonContent.Contains("\"type\":\"manual\"") || jsonContent.Contains("\"type\": \"manual\""))
{
// 手动格式
processedData = ParseManualJson(jsonPath, texturePath);
}
else
{
// Aseprite格式
processedData = ParseAsepriteJson(jsonPath, texturePath);
}
// Debug.Log($"processedData: {processedData.ToString()}");
EditorUtility.DisplayProgressBar("生成AnimationClip", "切图...", 0.4f);
// 如果是Single模式,需要切图
if (spriteMode == SpriteImportMode.Single)
{
SliceSprites(texturePath, processedData.frames, pivot);
// 重新加载以确保sprites可用
AssetDatabase.Refresh();
}
EditorUtility.DisplayProgressBar("生成AnimationClip", "加载Sprites...", 0.6f);
// 加载所有Sprites
Sprite[] allSpritesArray = AssetDatabase.LoadAllAssetsAtPath(texturePath)
.OfType<Sprite>()
.ToArray();
if (allSpritesArray.Length == 0)
{
throw new Exception("无法加载任何Sprite,请检查切图设置");
}
// 创建Sprite字典以便快速查找
Dictionary<string, Sprite> spriteDict = allSpritesArray.ToDictionary(s => s.name, s => s);
// 按照frames列表的顺序创建Sprite数组
Sprite[] allSprites = new Sprite[processedData.frames.Count];
for (int i = 0; i < processedData.frames.Count; i++)
{
string frameName = processedData.frames[i].name;
if (spriteDict.TryGetValue(frameName, out Sprite sprite))
{
allSprites[i] = sprite;
}
else
{
Debug.LogWarning($"找不到名为{frameName}的Sprite");
// 尝试使用索引查找
if (i < allSpritesArray.Length)
{
allSprites[i] = allSpritesArray[i];
}
}
}
// 确保输出目录存在
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}
EditorUtility.DisplayProgressBar("生成AnimationClip", "创建AnimationClip...", 0.8f);
// 为每个动画片段创建AnimationClip
int animationCount = processedData.animations.Count;
for (int i = 0; i < animationCount; i++)
{
var anim = processedData.animations[i];
EditorUtility.DisplayProgressBar("生成AnimationClip",
$"创建动画片段: {anim.name}", 0.8f + (i / (float)animationCount) * 0.2f);
CreateAnimationClip(anim, processedData.frames, allSprites, outputPath, isImageAnimation);
}
EditorUtility.ClearProgressBar();
Debug.Log($"成功生成{animationCount}个AnimationClip到: {outputPath}");
}
catch (Exception e)
{
EditorUtility.ClearProgressBar();
Debug.LogError($"生成AnimationClip失败: {e.Message}");
throw;
}
}
}
}