using System; using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; namespace AibisDream.FrameAnimation.Editor { public static class FrameAnimationImportSampleBuilder { private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation/Import"; private const string ReadOnlyTexturePath = SampleFolder + "/中文预切图.png"; private const string WritableTexturePath = SampleFolder + "/中文自动切图.png"; private const string ObjectJsonPath = SampleFolder + "/中文Object.json"; private const string ArrayJsonPath = SampleFolder + "/中文Array.json"; private const string GraphPath = SampleFolder + "/ImportSampleGraph.asset"; private const string IdleNodeId = "40000000000000000000000000000001"; private const string BlinkNodeId = "40000000000000000000000000000002"; private const string SharedNodeId = "40000000000000000000000000000003"; private const string IdleEdgeId = "40000000000000000000000000000011"; private const string BlinkEdgeId = "40000000000000000000000000000012"; private const string IdleFlowId = "SampleIdleToSharedFlow"; private const string BlinkFlowId = "SampleBlinkToSharedFlow"; [MenuItem("Tools/Frame Animation/Rebuild Import Sample")] private static void BuildFromMenu() { BuildFromCommandLine(); } public static void BuildFromCommandLine() { EnsureFolder(SampleFolder); if (AssetDatabase.LoadAssetAtPath(GraphPath) != null) { AssetDatabase.DeleteAsset(GraphPath); } WriteTexture(ReadOnlyTexturePath); WriteTexture(WritableTexturePath); WriteTextAsset(ObjectJsonPath, CreateObjectJson("中文预切图.png", "待机", "眨眼")); WriteTextAsset(ArrayJsonPath, CreateArrayJson("中文自动切图.png", "转身", "惊讶")); AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); var readOnlyTexture = AssetDatabase.LoadAssetAtPath(ReadOnlyTexturePath); var objectJson = AssetDatabase.LoadAssetAtPath(ObjectJsonPath); if (!AsepriteJsonParser.TryParse(objectJson.text, "sample-slicing", out var document, out var parseIssue)) { throw new InvalidOperationException(parseIssue.Message); } var slicingSource = new FrameAnimationImportSource( "Prepare Read Only Texture", readOnlyTexture, objectJson, new Vector2(0.5f, 0.5f), true); var slicingPreview = new FrameAnimationImportSourcePreview(slicingSource) { Document = document }; var slicingPlan = FrameAnimationSpriteUtility.BuildPlan(slicingSource, document, slicingPreview); if (slicingPreview.HasErrors) { throw new InvalidOperationException(string.Join("\n", System.Linq.Enumerable.Select( slicingPreview.Issues, issue => issue.Message))); } FrameAnimationSpriteUtility.ApplyPlan(slicingPlan); var graph = ScriptableObject.CreateInstance(); graph.Configure( "ImportSampleGraph", "Aseprite 导入与稳定刷新样例", Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), string.Empty); graph.AddImportSource(new FrameAnimationImportSource( "Object / 只读切图", AssetDatabase.LoadAssetAtPath(ReadOnlyTexturePath), AssetDatabase.LoadAssetAtPath(ObjectJsonPath), new Vector2(0.5f, 0.5f), false)); graph.AddImportSource(new FrameAnimationImportSource( "Array / 自动切图", AssetDatabase.LoadAssetAtPath(WritableTexturePath), AssetDatabase.LoadAssetAtPath(ArrayJsonPath), new Vector2(0.5f, 0.5f), true)); AssetDatabase.CreateAsset(graph, GraphPath); AssetDatabase.SaveAssets(); var preview = FrameAnimationImportService.PreviewAll(graph); if (!FrameAnimationImportService.Apply(preview, true, out var error)) { throw new InvalidOperationException(error + "\n" + string.Join("\n", System.Linq.Enumerable.SelectMany(preview.Sources, source => System.Linq.Enumerable.Select(source.Issues, issue => issue.Message)))); } ExtendForWorkspaceSample(graph); EditorUtility.SetDirty(graph); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Selection.activeObject = graph; Debug.Log($"Frame Animation import sample rebuilt: {GraphPath}"); } [MenuItem("Tools/Frame Animation/Upgrade Phase 4 Sample (Non-Destructive)")] public static void UpgradePhase4Sample() { var graph = AssetDatabase.LoadAssetAtPath(GraphPath); if (graph == null) { Debug.LogError($"Frame Animation sample Graph not found: {GraphPath}"); return; } var idleClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "待机"); var blinkClip = graph.Clips.FirstOrDefault(clip => clip?.ImportInfo?.SourceTagName == "眨眼"); var sharedClip = AssetDatabase.LoadAssetAtPath( "Assets/GameContent/Test/FrameAnimation/Idle.asset") ?? graph.Clips.FirstOrDefault(clip => clip != null && !clip.IsImported); if (idleClip == null || blinkClip == null || sharedClip == null || !graph.Clips.Contains(sharedClip)) { Debug.LogError("Phase 4 sample upgrade requires 待机、眨眼 and a Manual Clip already referenced by the Graph."); return; } Undo.RecordObject(graph, "Upgrade Frame Animation Phase 4 Sample"); var idleNode = GetOrAddNode(graph, IdleNodeId, idleClip, "待机 Node"); var blinkNode = GetOrAddNode(graph, BlinkNodeId, blinkClip, "眨眼 Node"); var sharedNode = graph.Nodes.FirstOrDefault(node => node != null && node.InternalId == SharedNodeId); if (sharedNode == null) { sharedNode = new AnimationNode(sharedClip.Id, "Shared Idle Node", FrameClipEndBehavior.Loop, internalId: SharedNodeId); graph.AddNode(sharedNode); } GetOrAddEdge(graph, IdleEdgeId, idleNode, sharedNode); GetOrAddEdge(graph, BlinkEdgeId, blinkNode, sharedNode); GetOrAddFlow(graph, IdleFlowId, "待机到共享 Idle", idleNode, new Color(0.24f, 0.65f, 1f)); GetOrAddFlow(graph, BlinkFlowId, "眨眼到共享 Idle", blinkNode, new Color(1f, 0.62f, 0.24f)); graph.EditorData.GetOrCreateNodeData(IdleNodeId, new Vector2(0f, 0f)); graph.EditorData.GetOrCreateNodeData(BlinkNodeId, new Vector2(0f, 220f)); graph.EditorData.GetOrCreateNodeData(SharedNodeId, new Vector2(360f, 110f)); EditorUtility.SetDirty(graph); AssetDatabase.SaveAssets(); Selection.activeObject = graph; Debug.Log("Frame Animation Phase 4 sample upgraded without rebuilding existing assets or sources."); } private static AnimationNode GetOrAddNode( FrameAnimationGraph graph, string internalId, FrameClip clip, string displayName) { var node = graph.Nodes.FirstOrDefault(item => item != null && item.InternalId == internalId); if (node != null) { return node; } node = new AnimationNode(clip.Id, displayName, internalId: internalId); graph.AddNode(node); return node; } private static void GetOrAddEdge( FrameAnimationGraph graph, string internalId, AnimationNode from, AnimationNode to) { if (graph.Edges.Any(edge => edge != null && edge.InternalId == internalId)) { return; } graph.AddEdge(new AnimationEdge(from.InternalId, to.InternalId, internalId)); } private static void GetOrAddFlow( FrameAnimationGraph graph, string id, string displayName, AnimationNode entry, Color color) { var flow = graph.Flows.FirstOrDefault(item => item != null && item.Id == id); if (flow == null && FrameAnimationAssetOperations.IsPlayableIdAvailable(graph, id)) { flow = new AnimationFlow(id, displayName, entry.InternalId); graph.AddFlow(flow); } if (flow != null) { graph.EditorData.GetOrCreateFlowData(flow.Id, color); } } private static void ExtendForWorkspaceSample(FrameAnimationGraph graph) { var imported = graph.Clips.First(clip => clip.ImportInfo?.SourceTagName == "待机"); var manual = ScriptableObject.CreateInstance(); manual.name = "ManualWorkspaceSample"; manual.Configure( "ManualWorkspaceSample", "Graph 内 Manual 样例", imported.Frames.Take(1).Select(frame => new FrameAnimationFrame( frame.Sprite, frame.DurationMs, string.Empty, -1)), 1f, FrameClipEndBehavior.HoldLastFrame); AssetDatabase.AddObjectToAsset(manual, graph); var clips = graph.Clips.Concat(new[] { manual }).ToList(); var external = AssetDatabase.LoadAssetAtPath( "Assets/GameContent/Test/FrameAnimation/Idle.asset"); if (external != null && clips.All(clip => clip.Id != external.Id)) { clips.Add(external); } var importedNode = new AnimationNode(imported.Id, "Imported 待机"); var terminalClip = external != null && clips.Contains(external) ? external : manual; var terminalNode = new AnimationNode( terminalClip.Id, "共享 Manual 终点", endBehaviorOverride: FrameClipEndBehavior.Loop); var edge = new AnimationEdge(importedNode.InternalId, terminalNode.InternalId); var flow = new AnimationFlow( "WorkspaceSampleFlow", "工作台引用定位样例", importedNode.InternalId); graph.Configure( graph.Id, graph.DisplayName, clips, new[] { importedNode, terminalNode }, new[] { edge }, new[] { flow }, flow.Id); } private static JObject CreateFrame(string name, int x, int duration) { return new JObject { ["filename"] = name, ["frame"] = new JObject { ["x"] = x, ["y"] = 0, ["w"] = 2, ["h"] = 2 }, ["rotated"] = false, ["trimmed"] = false, ["spriteSourceSize"] = new JObject { ["x"] = 0, ["y"] = 0, ["w"] = 2, ["h"] = 2 }, ["sourceSize"] = new JObject { ["w"] = 2, ["h"] = 2 }, ["duration"] = duration }; } private static string CreateObjectJson(string imageName, string firstTag, string secondTag) { var root = CreateRoot(imageName, firstTag, secondTag); root["frames"] = new JObject { ["中文帧_00"] = WithoutFilename(CreateFrame("中文帧_00", 0, 160)), ["中文帧_01"] = WithoutFilename(CreateFrame("中文帧_01", 2, 90)) }; return root.ToString(Formatting.Indented); } private static string CreateArrayJson(string imageName, string firstTag, string secondTag) { var root = CreateRoot(imageName, firstTag, secondTag); root["frames"] = new JArray( CreateFrame("自动帧_00", 0, 140), CreateFrame("自动帧_01", 2, 110)); return root.ToString(Formatting.Indented); } private static JObject CreateRoot(string imageName, string firstTag, string secondTag) { return new JObject { ["meta"] = new JObject { ["image"] = imageName, ["size"] = new JObject { ["w"] = 4, ["h"] = 2 }, ["frameTags"] = new JArray( new JObject { ["name"] = firstTag, ["from"] = 0, ["to"] = 0, ["direction"] = "forward" }, new JObject { ["name"] = secondTag, ["from"] = 0, ["to"] = 1, ["direction"] = "pingpong" }) } }; } private static JObject WithoutFilename(JObject frame) { frame.Remove("filename"); return frame; } private static void WriteTexture(string path) { var texture = new Texture2D(4, 2, TextureFormat.RGBA32, false); texture.SetPixels(new[] { Color.cyan, Color.cyan, Color.magenta, Color.magenta, Color.cyan, Color.cyan, Color.magenta, Color.magenta }); texture.Apply(); File.WriteAllBytes(path, texture.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(texture); } private static void WriteTextAsset(string path, string content) { File.WriteAllText(path, content, new UTF8Encoding(false)); } private static void EnsureFolder(string path) { var segments = path.Split('/'); var current = segments[0]; for (var index = 1; index < segments.Length; index++) { var next = current + "/" + segments[index]; if (!AssetDatabase.IsValidFolder(next)) { AssetDatabase.CreateFolder(current, segments[index]); } current = next; } } } }