feat: 帧动画编辑器及相关文档
This commit is contained in:
@@ -702,16 +702,50 @@
|
||||
.fa-preview-controls {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-height: 27px;
|
||||
}
|
||||
|
||||
.fa-selection-preview-panel .fa-preview-controls .unity-toolbar-button {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.fa-selection-preview-panel .fa-preview-controls .fa-preview-button {
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
max-width: 24px;
|
||||
height: 24px;
|
||||
min-height: 24px;
|
||||
max-height: 24px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fa-preview-button__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fa-preview-timeline {
|
||||
min-width: 28px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fa-preview-time {
|
||||
width: 82px;
|
||||
min-width: 82px;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
-unity-text-align: middle-right;
|
||||
}
|
||||
|
||||
.fa-preview-settings {
|
||||
|
||||
@@ -69,6 +69,8 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
private readonly List<Slider> previewTimelineSliders = new List<Slider>();
|
||||
private readonly List<Label> previewTimeLabels = new List<Label>();
|
||||
private readonly List<Button> previewPlayButtons = new List<Button>();
|
||||
private readonly Dictionary<Button, Image> previewButtonImages = new Dictionary<Button, Image>();
|
||||
private static readonly HashSet<string> MissingPreviewIconWarnings = new HashSet<string>();
|
||||
private readonly List<Label> previewTargetLabels = new List<Label>();
|
||||
private VisualElement clipPreviewPanel;
|
||||
private FrameAnimationPreviewElement clipPreviewElement;
|
||||
@@ -364,34 +366,78 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
mainHost.Add(main);
|
||||
}
|
||||
|
||||
private VisualElement BuildPreviewTransport()
|
||||
private Toolbar BuildPreviewTransport()
|
||||
{
|
||||
var controls = new VisualElement();
|
||||
var controls = new Toolbar { name = "preview-transport" };
|
||||
controls.AddToClassList("fa-preview-controls");
|
||||
controls.Add(new ToolbarButton(previewCoordinator.Restart) { text = "↶", tooltip = "Restart" });
|
||||
controls.Add(new ToolbarButton(() => previewCoordinator.Step(-1)) { text = "◀", tooltip = "Previous Frame" });
|
||||
var play = new ToolbarButton(PreviewPlayPause) { text = "▶", tooltip = "Play / Pause" };
|
||||
controls.Add(CreatePreviewButton("preview-restart", previewCoordinator.Restart, "Restart",
|
||||
"d_Refresh", "Refresh"));
|
||||
controls.Add(CreatePreviewButton("preview-previous-frame", () => previewCoordinator.Step(-1),
|
||||
"Previous Frame", "Animation.PrevKey"));
|
||||
var play = CreatePreviewButton("preview-play-pause", PreviewPlayPause, "Play", "PlayButton");
|
||||
previewPlayButtons.Add(play);
|
||||
controls.Add(play);
|
||||
controls.Add(new ToolbarButton(() => previewCoordinator.Step(1)) { text = "▶|", tooltip = "Next Frame" });
|
||||
controls.Add(new ToolbarButton(previewCoordinator.Stop) { text = "■", tooltip = "Stop" });
|
||||
controls.Add(CreatePreviewButton("preview-next-frame", () => previewCoordinator.Step(1),
|
||||
"Next Frame", "Animation.NextKey"));
|
||||
controls.Add(CreatePreviewButton("preview-stop", previewCoordinator.Stop, "Stop",
|
||||
"Assets/Editor/FrameAnimation/Icons/PreviewStop.png"));
|
||||
|
||||
var timeline = new Slider(0f, 1f)
|
||||
{
|
||||
style = { minWidth = 90f, maxWidth = 240f, flexGrow = 1f }
|
||||
name = "preview-timeline"
|
||||
};
|
||||
timeline.AddToClassList("fa-preview-timeline");
|
||||
timeline.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (!updatingPreviewUi) previewCoordinator.Seek(evt.newValue);
|
||||
});
|
||||
previewTimelineSliders.Add(timeline);
|
||||
controls.Add(timeline);
|
||||
var time = new Label("0 / 0 ms") { style = { minWidth = 82f } };
|
||||
var time = new Label("0 / 0 ms") { name = "preview-time" };
|
||||
time.AddToClassList("fa-preview-time");
|
||||
previewTimeLabels.Add(time);
|
||||
controls.Add(time);
|
||||
return controls;
|
||||
}
|
||||
|
||||
private ToolbarButton CreatePreviewButton(
|
||||
string name,
|
||||
Action action,
|
||||
string tooltip,
|
||||
params string[] iconSources)
|
||||
{
|
||||
var button = new ToolbarButton(action) { name = name, tooltip = tooltip };
|
||||
button.AddToClassList("fa-preview-button");
|
||||
var image = new Image { pickingMode = PickingMode.Ignore };
|
||||
image.AddToClassList("fa-preview-button__icon");
|
||||
button.Add(image);
|
||||
previewButtonImages[button] = image;
|
||||
SetPreviewButtonIcon(button, tooltip, iconSources);
|
||||
return button;
|
||||
}
|
||||
|
||||
private void SetPreviewButtonIcon(Button button, string tooltip, params string[] iconSources)
|
||||
{
|
||||
button.tooltip = tooltip;
|
||||
if (!previewButtonImages.TryGetValue(button, out var image)) return;
|
||||
image.image = null;
|
||||
foreach (var iconSource in iconSources)
|
||||
{
|
||||
var icon = iconSource.StartsWith("Assets/", StringComparison.Ordinal)
|
||||
? AssetDatabase.LoadAssetAtPath<Texture2D>(iconSource)
|
||||
: EditorGUIUtility.IconContent(iconSource)?.image;
|
||||
if (icon == null) continue;
|
||||
image.image = icon;
|
||||
return;
|
||||
}
|
||||
if (MissingPreviewIconWarnings.Add(tooltip))
|
||||
{
|
||||
var attempted = iconSources.Length > 0 ? $" Tried: {string.Join(", ", iconSources)}." : string.Empty;
|
||||
Debug.LogWarning($"Frame Animation Preview: no reliable Unity editor icon is available for " +
|
||||
$"'{tooltip}'.{attempted}");
|
||||
}
|
||||
}
|
||||
|
||||
private VisualElement BuildSelectionPreviewPanel()
|
||||
{
|
||||
var panel = new VisualElement { name = "selection-preview-panel" };
|
||||
@@ -564,7 +610,9 @@ namespace AibisDream.FrameAnimation.Editor
|
||||
}
|
||||
foreach (var button in previewPlayButtons)
|
||||
{
|
||||
button.text = previewCoordinator.State == FrameAnimationPreviewState.Playing ? "❚❚" : "▶";
|
||||
var isPlaying = previewCoordinator.State == FrameAnimationPreviewState.Playing;
|
||||
SetPreviewButtonIcon(button, isPlaying ? "Pause" : "Play",
|
||||
isPlaying ? "PauseButton" : "PlayButton");
|
||||
button.SetEnabled(previewCoordinator.TargetKind != FrameAnimationPreviewTargetKind.None);
|
||||
}
|
||||
var targetText = PreviewTargetName();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ad5208284c226e4297926dd64539442
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,166 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a870adb61eef3b4bb54dd6a93d043ef
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 12
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 0
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 1
|
||||
wrapV: 1
|
||||
wrapW: 1
|
||||
nPOTScale: 0
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 1
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 1
|
||||
spriteTessellationDetail: -1
|
||||
textureType: 8
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 3
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: iPhone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Android
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 3
|
||||
buildTarget: Server
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID: 5e97eb03825dee720800000000000000
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -26,6 +26,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u6263\u5934\u706F\u6CE1"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-8307372092058666549
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -76,6 +84,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u4F38\u624B\u8868\u60C5idle"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-5742946164860628285
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -126,6 +142,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u6342\u5934\u8868\u60C5idle"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-2521262061966760486
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -152,6 +176,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u6263\u5934 \uFF1F"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-1981100710209546618
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -178,6 +210,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u4F38\u624B \u77F3\u5316\u8868\u60C5"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-1764538467543073397
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -224,6 +264,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u4F38\u624B\u5207\u5C4F\u7279\u6548 "
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-1158850888323987525
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -250,6 +298,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u62FF\u4F4F\u5E3D\u5B50"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &-757131795367400242
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -296,6 +352,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u6263\u5934\u5207\u5C4F\u7279\u6548"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -388,6 +452,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u5BF9\u624B\u6307\u5207\u5C4F\u7279\u6548"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &7171718461941095968
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -438,6 +510,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u6263\u5934\u8868\u60C5idle"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &8692045413535011072
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -464,6 +544,14 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u6458\u5E3D"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
--- !u!114 &8885103194751219695
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -514,3 +602,11 @@ MonoBehaviour:
|
||||
importSourceId: d329feaf92a945f885e419c05824cd56
|
||||
sourceTagName: "\u5BF9\u624B\u6307\u8868\u60C5idle"
|
||||
isMissingFromSource: 0
|
||||
hasStandaloneImportSource: 0
|
||||
standaloneImportSource:
|
||||
texture: {fileID: 0}
|
||||
asepriteJson: {fileID: 0}
|
||||
pivot: {x: 0.5, y: 0.5}
|
||||
manageSpriteSlicing: 0
|
||||
lastSourceHash:
|
||||
lastImportedTagName:
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Reflection;
|
||||
using AibisDream.FrameAnimation.Editor;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
@@ -305,10 +306,28 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
||||
Is.Empty);
|
||||
Assert.That(window.rootVisualElement.Query<IMGUIContainer>().ToList(), Is.Empty);
|
||||
Assert.That(window.rootVisualElement.Q<VisualElement>("fa-workbench"), Is.Not.Null);
|
||||
Assert.That(window.rootVisualElement.Q<Button>("resource-tab-clips")
|
||||
.ClassListContains("fa-tab--active"), Is.True);
|
||||
Assert.That(window.rootVisualElement.Q<Button>("bottom-tab-import-diff")
|
||||
.ClassListContains("fa-tab--active"), Is.True);
|
||||
var clipsTab = window.rootVisualElement.Q<Button>("resource-tab-clips");
|
||||
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetResourceTab",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window,
|
||||
new[] { clipsTab.userData, (object)false });
|
||||
Assert.That(clipsTab.ClassListContains("fa-tab--active"), Is.True);
|
||||
var importDiffTab = window.rootVisualElement.Q<Button>("bottom-tab-import-diff");
|
||||
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetBottomTab",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window,
|
||||
new[] { importDiffTab.userData });
|
||||
Assert.That(importDiffTab.ClassListContains("fa-tab--active"), Is.True);
|
||||
|
||||
var transport = window.rootVisualElement.Q<Toolbar>("preview-transport");
|
||||
Assert.That(transport, Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-restart"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-previous-frame"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-play-pause"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-next-frame"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-stop"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-stop").Q<Image>().image, Is.Not.Null);
|
||||
Assert.That(transport.Q<Slider>("preview-timeline"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Label>("preview-time"), Is.Not.Null);
|
||||
Assert.That(transport.Q<Button>("preview-play-pause").enabledSelf, Is.False);
|
||||
|
||||
var flowsTab = window.rootVisualElement.Q<Button>("resource-tab-flows");
|
||||
typeof(FrameAnimationGraphEditorWindow).GetMethod("SetResourceTab",
|
||||
@@ -335,6 +354,18 @@ namespace AibisDream.FrameAnimation.Tests.EditMode
|
||||
false
|
||||
});
|
||||
AssertSelectionPreview(window, "Clip Preview");
|
||||
var playButton = transport.Q<Button>("preview-play-pause");
|
||||
Assert.That(playButton.enabledSelf, Is.True);
|
||||
Assert.That(playButton.text, Is.Empty);
|
||||
Assert.That(playButton.tooltip, Is.EqualTo("Play"));
|
||||
Assert.That(playButton.Q<Image>(), Is.Not.Null);
|
||||
Assert.That(playButton.Q<Image>().image, Is.Not.Null);
|
||||
|
||||
typeof(FrameAnimationGraphEditorWindow).GetMethod("PreviewPlayPause",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(window, null);
|
||||
Assert.That(playButton.text, Is.Empty);
|
||||
Assert.That(playButton.tooltip, Is.EqualTo("Pause"));
|
||||
Assert.That(playButton.Q<Image>().image, Is.Not.Null);
|
||||
|
||||
var node = sample.Nodes.First(item => item != null);
|
||||
setSelection?.Invoke(window, new object[]
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# 帧动画角色配置指南(策划版)
|
||||
|
||||
本文说明帧动画角色的 **Flow 配置**与 **Yarn 调用**。Clip 的切图、导入和帧时长配置不在本文展开。
|
||||
|
||||
示例资源:`Assets/GameContent/Huoshan/Actor/火山Graph.asset`
|
||||
|
||||
> 当前的 `火山Graph` 已有 Clip,但还没有配置 Flow。下文的 Flow 名称是教学示例,不代表资源中已经存在。
|
||||
|
||||
## 1. 先理解 Clip 和 Flow
|
||||
|
||||
- **Clip**:一段独立帧动画,例如 `摘帽`、`扣头切屏特效`、`伸手表情idle`。
|
||||
- **Flow**:把多个 Clip 按顺序串成一次完整表演,例如 `摘帽 → 伸手表情idle`。
|
||||
- Yarn 调用时,Clip ID 和 Flow ID 的写法完全相同,系统会自动查找对应内容。
|
||||
|
||||
适合直接调用 Clip 的情况:只播放一个动作或切换一个循环表情。
|
||||
|
||||
适合配置 Flow 的情况:动作需要连续播放多个阶段,并且希望 Yarn 只写一条命令。
|
||||
|
||||
## 2. 打开火山 Graph
|
||||
|
||||
1. 在 Project 窗口选中 `Assets/GameContent/Huoshan/Actor/火山Graph.asset`。
|
||||
2. 点击 Inspector 中的 **Open Frame Animation Graph Editor**。
|
||||
3. 也可以从 Unity 菜单打开:**Window > Aibis Dream > Frame Animation Graph Editor**,再选择 `火山Graph`。
|
||||
|
||||
编辑器左侧是 Clip / Flow 列表,中间是节点画布,右侧是当前选中内容的属性。
|
||||
|
||||
## 3. 配置一个 Flow
|
||||
|
||||
下面以新建 `摘帽到伸手_Flow` 为例,预期顺序为:
|
||||
|
||||
```text
|
||||
摘帽 → 伸手表情idle(循环)
|
||||
```
|
||||
|
||||
### 第一步:把 Clip 放到画布
|
||||
|
||||
1. 在左侧选择 **Clips**。
|
||||
2. 把 `摘帽` 从左侧拖到中间画布,生成一个 Node。
|
||||
3. 再把 `伸手表情idle` 拖到画布,生成第二个 Node。
|
||||
|
||||
同一个 Clip 可以在画布中生成多个 Node。Node 只是 Flow 中对 Clip 的一次引用,不会复制或修改原 Clip。
|
||||
|
||||
### 第二步:连接播放顺序
|
||||
|
||||
从 `摘帽` Node 右侧的输出点拖线,连接到 `伸手表情idle` Node 左侧的输入点。
|
||||
|
||||
当前 Flow 只支持单线顺序播放:
|
||||
|
||||
- 一个 Node 最多只能连接一个后继 Node;
|
||||
- 不支持分支;
|
||||
- 不支持把路径连成环;
|
||||
- 播放顺序由连线决定,不由节点在画布上的左右位置决定。
|
||||
|
||||
### 第三步:创建 Flow 并指定入口
|
||||
|
||||
1. 选中 `摘帽` Node。
|
||||
2. 右键该 Node,选择 **Create Flow From Node**;也可用画布上方 **Canvas > Create Flow From Selection**。
|
||||
3. 将 Flow ID 填为 `摘帽到伸手_Flow`。
|
||||
4. 确认入口是 `摘帽` Node。入口 Node 会显示 **E** 标记。
|
||||
|
||||
Flow ID 就是 Yarn 中填写的动画名。建议使用有明确含义且不易重复的名称,例如 `摘帽到伸手_Flow`。
|
||||
|
||||
> Clip ID 和 Flow ID 共用同一套命名空间,不能重名。修改 ID 后,已有 Yarn 文本不会自动更新,必须同步搜索并修改调用。
|
||||
|
||||
### 第四步:设置结尾行为
|
||||
|
||||
选中最后一个 Node,在右侧设置 **Override End Behavior**。常用选项:
|
||||
|
||||
| 选项 | 播放结束后的表现 | 常见用途 |
|
||||
| --- | --- | --- |
|
||||
| `Loop` | 从头循环最后一个 Clip | idle、持续表情 |
|
||||
| `HoldLastFrame` | 停在最后一帧 | 一次动作的定格结尾 |
|
||||
| `Clear` | 清空当前 Sprite | 动画结束后不显示图片 |
|
||||
| `HideTarget` | 隐藏渲染目标 | 动画结束后隐藏角色 |
|
||||
|
||||
本例最后的 `伸手表情idle` 应使用 `Loop`。
|
||||
|
||||
注意:只有终点 Node 才能设置结束行为。一个 Node 如果设置了结束行为,就不能再连接后继 Node。中间 Node 播完后会自动进入下一个 Node,不需要设置结束行为。
|
||||
|
||||
如需单独调整某个 Node 的速度,可勾选 **Override Speed**:`1` 为原速,`2` 为两倍速,`0.5` 为半速。速度必须大于 `0`。
|
||||
|
||||
### 第五步:预览、校验和保存
|
||||
|
||||
1. 在左侧选择刚创建的 Flow,点击右侧 **Focus Flow On Canvas**。
|
||||
2. 使用预览区的播放按钮检查顺序和循环结果。
|
||||
3. 点击顶部 **Validate**,底部 **Validation** 中不能有 Error。
|
||||
4. 点击顶部 **Save** 保存。
|
||||
|
||||
## 4. Yarn 调用
|
||||
|
||||
### 初始化角色
|
||||
|
||||
帧动画角色首次出现时,先初始化:
|
||||
|
||||
```yarn
|
||||
<<init_actor 火山 clinic FrameAnimation>>
|
||||
```
|
||||
|
||||
参数依次为:
|
||||
|
||||
```text
|
||||
角色名 槽位名 角色类型
|
||||
```
|
||||
|
||||
`火山` 会加载 Addressable 地址为 `FrameAnimation/火山` 的 Graph。`init_actor` 会等待角色 Prefab 和 Graph 加载完成,因此下一行可以直接切动画。
|
||||
|
||||
同一段角色出场流程中只需初始化一次,不要在每次换动画前重复初始化。
|
||||
|
||||
> 初始化完成后不会自动播放 Graph 中的 Default Playable,需要再调用一次 Clip 或 Flow。
|
||||
|
||||
### 调用一个 Clip
|
||||
|
||||
把 **Clip ID** 直接写在命令的第一个参数中:
|
||||
|
||||
```yarn
|
||||
<<change_actor_state 捂头表情idle 火山>>
|
||||
```
|
||||
|
||||
这条命令会直接播放 `火山Graph` 中的 `捂头表情idle` Clip。Clip 播完后的表现由该 Clip 的 **End Behavior** 决定:
|
||||
|
||||
- `Loop`:持续循环,直到被下一次状态切换替换;
|
||||
- `HoldLastFrame`:播放一次并停在最后一帧;
|
||||
- `Clear`:播放一次后清空图片;
|
||||
- `HideTarget`:播放一次后隐藏角色渲染目标。
|
||||
|
||||
#### Clip:播放后立刻继续 Yarn
|
||||
|
||||
```yarn
|
||||
<<change_actor_state "伸手 石化表情" 火山>>
|
||||
hs: 我太伤心了。
|
||||
```
|
||||
|
||||
`change_actor_state` 只负责开始播放,Yarn 不会等 Clip 播完,会立即执行下一行。适用于:
|
||||
|
||||
- 切换 idle 或持续循环表情;
|
||||
- Clip 需要和对白同时播放;
|
||||
- 后续时机由策划自己用 `wait` 控制。
|
||||
|
||||
#### Clip:播放完成后再继续 Yarn
|
||||
|
||||
```yarn
|
||||
<<change_actor_state_async 扣头切屏特效 火山>>
|
||||
<<change_actor_state 扣头表情idle 火山>>
|
||||
```
|
||||
|
||||
`change_actor_state_async` 会等待 Clip:
|
||||
|
||||
- 非循环 Clip:等待整段播放结束;
|
||||
- `Loop` Clip:等待第一轮播放结束,然后继续执行 Yarn;动画本身仍会循环。
|
||||
|
||||
因此,一次性动作之后要准确切换 idle 时,推荐使用上面的“异步动作 Clip → 循环 idle Clip”写法,不需要猜测 `wait` 秒数。
|
||||
|
||||
### 调用一个 Flow
|
||||
|
||||
Flow 的调用格式与 Clip 完全相同,只需把第一个参数换成 **Flow ID**。例如已配置 `摘帽到伸手_Flow`:
|
||||
|
||||
#### Flow:播放后立刻继续 Yarn
|
||||
|
||||
```yarn
|
||||
<<change_actor_state 摘帽到伸手_Flow 火山>>
|
||||
hs: 戴上“实实”牌帽子,给你的头顶添件宝!
|
||||
```
|
||||
|
||||
Yarn 会立即继续对白,Flow 则在后台按照连线依次播放。适合整段表演与对白同时发生的情况。
|
||||
|
||||
#### Flow:播放到终点首轮后再继续 Yarn
|
||||
|
||||
```yarn
|
||||
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
||||
// Flow 的前置动作和终点 Loop 首轮播放完后,才执行这里
|
||||
```
|
||||
|
||||
`change_actor_state_async` 会从入口开始等待整条 Flow:
|
||||
|
||||
- 终点为非循环 Clip:等待所有节点自然播放结束;
|
||||
- `前置动作 → 终点 Loop` 的 Flow:等待前置动作和终点 Loop 的第一轮全部结束。
|
||||
|
||||
如果终点是 `Loop`,命令返回后终点 Clip 仍会继续循环,直到被下一次状态切换替换。
|
||||
|
||||
### 快速选择命令
|
||||
|
||||
| 要播放的内容 | 希望 Yarn 是否等待 | 写法 |
|
||||
| --- | --- | --- |
|
||||
| 单个 Clip | 不等待 | `<<change_actor_state ClipID 角色名>>` |
|
||||
| 单个 Clip | 等完整动画;Loop 等第一轮 | `<<change_actor_state_async ClipID 角色名>>` |
|
||||
| 一整条 Flow | 不等待 | `<<change_actor_state FlowID 角色名>>` |
|
||||
| 一整条 Flow | 等到终点首轮完成 | `<<change_actor_state_async FlowID 角色名>>` |
|
||||
|
||||
命令本身不需要标明目标是 Clip 还是 Flow。系统会用 ID 在当前角色的 Graph 中查找;因此 Clip ID 和 Flow ID 不能重名。
|
||||
|
||||
### 名称中有空格时
|
||||
|
||||
Clip ID、Flow ID、角色名或槽位名中包含空格时,必须加英文双引号:
|
||||
|
||||
```yarn
|
||||
<<change_actor_state "伸手 石化表情" 火山>>
|
||||
<<change_actor_state_async "摘帽 到 伸手_Flow" 火山>>
|
||||
```
|
||||
|
||||
不含空格时可以不加;为减少出错,也可以统一加英文双引号。
|
||||
|
||||
## 5. 火山的完整示例
|
||||
|
||||
下面同时演示 Clip 和 Flow 调用,并假设已按前文创建 `摘帽到伸手_Flow`:
|
||||
|
||||
```yarn
|
||||
<<init_actor 火山 clinic FrameAnimation>>
|
||||
|
||||
// 调用 Loop Clip:立即继续 Yarn,idle 在对白期间持续循环
|
||||
<<change_actor_state 捂头表情idle 火山>>
|
||||
<<fade_in_actor 火山>>
|
||||
|
||||
hs: 医——生——救——我——!
|
||||
|
||||
// 调用 Flow:等“摘帽”播完,再等终点“伸手表情idle”完成第一轮
|
||||
<<change_actor_state_async 摘帽到伸手_Flow 火山>>
|
||||
|
||||
// Flow 返回后,终点的伸手 idle 仍在循环
|
||||
hs: 戴上“实实”牌帽子,给你的头顶添件宝!
|
||||
|
||||
// 调用一次性 Clip,并等待它完整播完
|
||||
<<change_actor_state_async "扣头切屏特效" 火山>>
|
||||
|
||||
// 再调用另一个 Loop Clip,替换当前状态并继续对白
|
||||
<<change_actor_state "扣头表情idle" 火山>>
|
||||
hs: 我……没有活干了。
|
||||
```
|
||||
|
||||
## 6. 提交前检查
|
||||
|
||||
- Flow 的入口是否是第一个动作,而不是终点 idle?
|
||||
- 节点是否按预期连线,且没有分支或环?
|
||||
- 只有最后一个 Node 设置了结束行为吗?
|
||||
- 需要持续显示的 idle 是否设置为 `Loop`?
|
||||
- Flow ID 是否与已有 Clip / Flow 重名?
|
||||
- Yarn 中的 ID 与 Graph 完全一致,包括空格和大小写吗?
|
||||
- 需要等动画时是否用了 `change_actor_state_async`?
|
||||
- 顶部 **Validate** 是否无 Error?
|
||||
- 是否点击 **Save**?
|
||||
|
||||
## 7. 常见问题
|
||||
|
||||
**调用后没有动画**
|
||||
|
||||
先检查角色是否用 `FrameAnimation` 类型初始化,再检查 Graph 的 Addressable 地址是否为 `FrameAnimation/角色名`。火山应为 `FrameAnimation/火山`。
|
||||
|
||||
**提示 playable 找不到**
|
||||
|
||||
Yarn 中传入的是 Clip ID 或 Flow ID,不是资源文件名、Graph 名或 Node 的 Display Name。检查字符、空格和大小写是否完全一致。
|
||||
|
||||
**动画刚开始就被打断**
|
||||
|
||||
后面很可能紧跟了另一条 `change_actor_state`。普通命令不会等待;需要等当前动画完成时改用 `change_actor_state_async`。
|
||||
|
||||
**循环动画导致剧情无法继续**
|
||||
|
||||
使用 `change_actor_state_async` 等待 Loop 时,只会等待第一轮,不会无限阻塞。如果仍未继续,先运行 **Validate** 检查 Flow 路径和结尾配置。
|
||||
|
||||
**想让 Flow 中途停住**
|
||||
|
||||
当前 Flow 是顺序播放,不支持分支或中途等待 Yarn。应拆成两个 Clip / Flow,在 Yarn 中分两次调用。
|
||||
Reference in New Issue
Block a user