using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; namespace AibisDream.FrameAnimation.Editor { public static class FrameAnimationRuntimeSampleBuilder { private const string SourceTexturePath = "Assets/GameContent/Test/AnimatorRaw/手接树叶特写.png"; private const string SampleFolder = "Assets/GameContent/Test/FrameAnimation"; private const string IntroClipPath = SampleFolder + "/Intro.asset"; private const string IdleClipPath = SampleFolder + "/Idle.asset"; private const string FlowGraphPath = SampleFolder + "/FlowSampleGraph.asset"; private const string DirectGraphPath = SampleFolder + "/DirectSampleGraph.asset"; private const string ScenePath = "Assets/Scenes/FrameAnimationRuntimeTest.unity"; [MenuItem("Tools/Frame Animation/Rebuild Runtime Sample")] private static void BuildFromMenu() { if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { return; } BuildFromCommandLine(); } public static void BuildFromCommandLine() { EnsureFolder(SampleFolder); var sprites = AssetDatabase.LoadAllAssetsAtPath(SourceTexturePath) .OfType() .OrderBy(sprite => ExtractFrameNumber(sprite.name)) .ToArray(); if (sprites.Length < 21) { throw new InvalidOperationException( $"样例源 Texture 需要至少 21 个 Sprite,当前只找到 {sprites.Length} 个。"); } var introClip = CreateOrLoadAsset(IntroClipPath); introClip.Configure( "Intro", "Intro", CreateFrames(sprites, 0, 5), 1f, FrameClipEndBehavior.HoldLastFrame); EditorUtility.SetDirty(introClip); var idleClip = CreateOrLoadAsset(IdleClipPath); idleClip.Configure( "Idle", "Idle", CreateFrames(sprites, 5, 16), 1f, FrameClipEndBehavior.Loop); EditorUtility.SetDirty(idleClip); var flowGraph = CreateOrLoadAsset(FlowGraphPath); var introNodeId = FindExistingNodeId(flowGraph, introClip.Id); var idleNodeId = FindExistingNodeId(flowGraph, idleClip.Id); var introNode = new AnimationNode(introClip.Id, "Intro", internalId: introNodeId); var idleNode = new AnimationNode( idleClip.Id, "Idle Loop", FrameClipEndBehavior.Loop, internalId: idleNodeId); var flow = new AnimationFlow("IntroToIdle", "Intro To Idle", introNode.InternalId); flowGraph.Configure( "FlowSampleGraph", "Flow Sample Graph", new[] { introClip, idleClip }, new[] { introNode, idleNode }, new[] { new AnimationEdge(introNode.InternalId, idleNode.InternalId) }, new[] { flow }, flow.Id); EditorUtility.SetDirty(flowGraph); var directGraph = CreateOrLoadAsset(DirectGraphPath); directGraph.Configure( "DirectSampleGraph", "Direct Sample Graph", new[] { introClip, idleClip }, Array.Empty(), Array.Empty(), Array.Empty(), idleClip.Id); EditorUtility.SetDirty(directGraph); AssetDatabase.SaveAssets(); BuildScene(flowGraph, directGraph); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"Frame Animation runtime sample rebuilt: {ScenePath}"); } private static IEnumerable CreateFrames( IReadOnlyList sprites, int startIndex, int count) { for (var offset = 0; offset < count; offset++) { var sourceIndex = startIndex + offset; var sprite = sprites[sourceIndex]; yield return new FrameAnimationFrame(sprite, 250, sprite.name, sourceIndex); } } private static void BuildScene( FrameAnimationGraph flowGraph, FrameAnimationGraph directGraph) { var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); var cameraObject = new GameObject("Main Camera", typeof(Camera)); cameraObject.tag = "MainCamera"; cameraObject.transform.position = new Vector3(0f, 0f, -10f); var camera = cameraObject.GetComponent(); camera.orthographic = true; camera.orthographicSize = 6f; camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f); var spriteObject = new GameObject("Flow Sample - SpriteRenderer", typeof(SpriteRenderer)); spriteObject.transform.position = new Vector3(-3.5f, 0f, 0f); spriteObject.transform.localScale = Vector3.one * 0.45f; var spritePlayer = spriteObject.AddComponent(); spritePlayer.ConfigureForAuthoring(flowGraph, true, 1f); var canvasObject = new GameObject( "Direct Clip Sample Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); var canvas = canvasObject.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; var scaler = canvasObject.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1280f, 720f); var imageObject = new GameObject( "Direct Idle - Image", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); imageObject.transform.SetParent(canvasObject.transform, false); var rectTransform = imageObject.GetComponent(); rectTransform.sizeDelta = new Vector2(384f, 216f); rectTransform.anchoredPosition = new Vector2(300f, 0f); var image = imageObject.GetComponent(); image.preserveAspect = true; var imagePlayer = imageObject.AddComponent(); imagePlayer.ConfigureForAuthoring(directGraph, true, 1f); EditorSceneManager.MarkSceneDirty(scene); if (!EditorSceneManager.SaveScene(scene, ScenePath)) { throw new InvalidOperationException($"无法保存样例场景:{ScenePath}"); } } private static T CreateOrLoadAsset(string path) where T : ScriptableObject { var asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) { return asset; } asset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(asset, path); return asset; } private static string FindExistingNodeId(FrameAnimationGraph graph, string clipId) { var existing = graph.Nodes.FirstOrDefault(node => node != null && node.ClipId == clipId); return existing != null && FrameAnimationValueUtility.IsValidInternalId(existing.InternalId) ? existing.InternalId : null; } private static int ExtractFrameNumber(string spriteName) { if (string.IsNullOrEmpty(spriteName)) { return int.MaxValue; } var spaceIndex = spriteName.LastIndexOf(' '); var dotIndex = spriteName.LastIndexOf('.'); if (spaceIndex >= 0 && dotIndex > spaceIndex && int.TryParse(spriteName.Substring(spaceIndex + 1, dotIndex - spaceIndex - 1), out var number)) { return number; } return int.MaxValue; } 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; } } } }