feat(frameAnima): 动画系统前四个阶段
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user