336 lines
15 KiB
C#
336 lines
15 KiB
C#
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<FrameAnimationGraph>(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<Texture2D>(ReadOnlyTexturePath);
|
|
var objectJson = AssetDatabase.LoadAssetAtPath<TextAsset>(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<FrameAnimationGraph>();
|
|
graph.Configure(
|
|
"ImportSampleGraph",
|
|
"Aseprite 导入与稳定刷新样例",
|
|
Array.Empty<FrameClip>(),
|
|
Array.Empty<AnimationNode>(),
|
|
Array.Empty<AnimationEdge>(),
|
|
Array.Empty<AnimationFlow>(),
|
|
string.Empty);
|
|
graph.AddImportSource(new FrameAnimationImportSource(
|
|
"Object / 只读切图",
|
|
AssetDatabase.LoadAssetAtPath<Texture2D>(ReadOnlyTexturePath),
|
|
AssetDatabase.LoadAssetAtPath<TextAsset>(ObjectJsonPath),
|
|
new Vector2(0.5f, 0.5f),
|
|
false));
|
|
graph.AddImportSource(new FrameAnimationImportSource(
|
|
"Array / 自动切图",
|
|
AssetDatabase.LoadAssetAtPath<Texture2D>(WritableTexturePath),
|
|
AssetDatabase.LoadAssetAtPath<TextAsset>(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<FrameAnimationGraph>(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<FrameClip>(
|
|
"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<FrameClip>();
|
|
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<FrameClip>(
|
|
"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;
|
|
}
|
|
}
|
|
}
|
|
}
|