338 lines
12 KiB
C#
338 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace AibisDream.FrameAnimation
|
|
{
|
|
internal sealed class ResolvedPlaybackStep
|
|
{
|
|
public FrameClip Clip { get; }
|
|
public string NodeId { get; }
|
|
public float PlaybackSpeed { get; }
|
|
public FrameClipEndBehavior TerminalEndBehavior { get; }
|
|
|
|
public ResolvedPlaybackStep(
|
|
FrameClip clip,
|
|
string nodeId,
|
|
float playbackSpeed,
|
|
FrameClipEndBehavior terminalEndBehavior)
|
|
{
|
|
Clip = clip;
|
|
NodeId = nodeId ?? string.Empty;
|
|
PlaybackSpeed = playbackSpeed;
|
|
TerminalEndBehavior = terminalEndBehavior;
|
|
}
|
|
}
|
|
|
|
internal sealed class ResolvedPlaybackPlan
|
|
{
|
|
public string PlayableId { get; }
|
|
public bool IsFlow { get; }
|
|
public IReadOnlyList<ResolvedPlaybackStep> Steps { get; }
|
|
public AnimationFlowAsyncCompletionMode AsyncCompletionMode { get; }
|
|
|
|
public ResolvedPlaybackPlan(
|
|
string playableId,
|
|
bool isFlow,
|
|
IReadOnlyList<ResolvedPlaybackStep> steps,
|
|
AnimationFlowAsyncCompletionMode asyncCompletionMode =
|
|
AnimationFlowAsyncCompletionMode.WaitForTerminalLoopFirstCycle)
|
|
{
|
|
PlayableId = playableId;
|
|
IsFlow = isFlow;
|
|
Steps = steps;
|
|
AsyncCompletionMode = asyncCompletionMode;
|
|
}
|
|
}
|
|
|
|
internal static class FrameAnimationResolver
|
|
{
|
|
internal static bool TryResolveNode(
|
|
FrameAnimationGraph graph,
|
|
AnimationNode node,
|
|
out ResolvedPlaybackPlan plan,
|
|
out FrameAnimationPlaybackError error)
|
|
{
|
|
plan = null;
|
|
error = FrameAnimationPlaybackError.None;
|
|
if (graph == null)
|
|
{
|
|
error = Error(FrameAnimationPlaybackErrorCode.GraphMissing, "FrameAnimationGraph 为空。");
|
|
return false;
|
|
}
|
|
if (node == null || !graph.Nodes.Contains(node) || node.Type != AnimationNodeType.Clip)
|
|
{
|
|
error = Error(FrameAnimationPlaybackErrorCode.InvalidPlayableData, "AnimationNode 不属于当前 Graph 或类型无效。");
|
|
return false;
|
|
}
|
|
|
|
var clips = graph.Clips.Where(clip => clip != null && clip.Id == node.ClipId).ToArray();
|
|
if (clips.Length != 1)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Node '{node.InternalId}' 的 clipId '{node.ClipId}' 不存在或不唯一。");
|
|
return false;
|
|
}
|
|
var clip = clips[0];
|
|
if (!TryValidateClip(clip, out error))
|
|
{
|
|
return false;
|
|
}
|
|
var speed = node.SpeedOverride ?? clip.Speed;
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(speed))
|
|
{
|
|
error = Error(FrameAnimationPlaybackErrorCode.InvalidSpeed, $"Node '{node.InternalId}' 的有效速度无效。");
|
|
return false;
|
|
}
|
|
|
|
plan = new ResolvedPlaybackPlan(
|
|
node.InternalId,
|
|
false,
|
|
new[]
|
|
{
|
|
new ResolvedPlaybackStep(
|
|
clip,
|
|
node.InternalId,
|
|
speed,
|
|
node.EndBehaviorOverride ?? clip.DefaultEndBehavior)
|
|
});
|
|
return true;
|
|
}
|
|
|
|
public static bool TryResolve(
|
|
FrameAnimationGraph graph,
|
|
string playableId,
|
|
FrameAnimationPlayOptions options,
|
|
out ResolvedPlaybackPlan plan,
|
|
out FrameAnimationPlaybackError error)
|
|
{
|
|
plan = null;
|
|
error = FrameAnimationPlaybackError.None;
|
|
|
|
if (graph == null)
|
|
{
|
|
error = Error(FrameAnimationPlaybackErrorCode.GraphMissing, "FrameAnimationGraph 为空。");
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(playableId))
|
|
{
|
|
error = Error(FrameAnimationPlaybackErrorCode.PlayableIdEmpty, "playableId 不能为空。");
|
|
return false;
|
|
}
|
|
|
|
var clipMatches = graph.Clips.Where(clip => clip != null && clip.Id == playableId).ToList();
|
|
var flowMatches = graph.Flows.Where(flow => flow != null && flow.Id == playableId).ToList();
|
|
var matchCount = clipMatches.Count + flowMatches.Count;
|
|
if (matchCount == 0)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.PlayableNotFound,
|
|
$"Graph '{graph.Id}' 中不存在 playable '{playableId}'。");
|
|
return false;
|
|
}
|
|
|
|
if (matchCount > 1)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"playableId '{playableId}' 在 Clip / Flow 统一命名空间中不唯一。");
|
|
return false;
|
|
}
|
|
|
|
if (clipMatches.Count == 1)
|
|
{
|
|
return TryResolveClip(clipMatches[0], options, out plan, out error);
|
|
}
|
|
|
|
return TryResolveFlow(graph, flowMatches[0], options, out plan, out error);
|
|
}
|
|
|
|
internal static bool TryResolveClip(
|
|
FrameClip clip,
|
|
FrameAnimationPlayOptions options,
|
|
out ResolvedPlaybackPlan plan,
|
|
out FrameAnimationPlaybackError error)
|
|
{
|
|
plan = null;
|
|
if (!TryValidateClip(clip, out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var endBehavior = options.EndBehaviorOverride ?? clip.DefaultEndBehavior;
|
|
plan = new ResolvedPlaybackPlan(
|
|
clip != null ? clip.Id : string.Empty,
|
|
false,
|
|
new[] { new ResolvedPlaybackStep(clip, string.Empty, clip.Speed, endBehavior) });
|
|
return true;
|
|
}
|
|
|
|
private static bool TryResolveFlow(
|
|
FrameAnimationGraph graph,
|
|
AnimationFlow flow,
|
|
FrameAnimationPlayOptions options,
|
|
out ResolvedPlaybackPlan plan,
|
|
out FrameAnimationPlaybackError error)
|
|
{
|
|
plan = null;
|
|
error = FrameAnimationPlaybackError.None;
|
|
var steps = new List<ResolvedPlaybackStep>();
|
|
var visited = new HashSet<string>();
|
|
var currentNodeId = flow.EntryNodeId;
|
|
var topology = new FrameAnimationGraphTopology(graph);
|
|
|
|
while (true)
|
|
{
|
|
if (!FrameAnimationValueUtility.IsValidInternalId(currentNodeId))
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Flow '{flow.Id}' 的入口或后继 Node internalId 无效。");
|
|
return false;
|
|
}
|
|
|
|
if (!visited.Add(currentNodeId))
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Flow '{flow.Id}' 的可达路径形成了环路。");
|
|
return false;
|
|
}
|
|
|
|
var nodeMatches = topology.FindNodes(currentNodeId);
|
|
if (nodeMatches.Count != 1)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Flow '{flow.Id}' 的 Node '{currentNodeId}' 不存在或不唯一。");
|
|
return false;
|
|
}
|
|
|
|
var node = nodeMatches[0];
|
|
if (node.Type != AnimationNodeType.Clip)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Node '{node.InternalId}' 使用了第一版不支持的节点类型。");
|
|
return false;
|
|
}
|
|
|
|
var clipMatches = graph.Clips.Where(clip => clip != null && clip.Id == node.ClipId).ToList();
|
|
if (clipMatches.Count != 1)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Node '{node.InternalId}' 的 clipId '{node.ClipId}' 不存在或不唯一。");
|
|
return false;
|
|
}
|
|
|
|
var clip = clipMatches[0];
|
|
if (!TryValidateClip(clip, out error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var nodeSpeed = node.SpeedOverride ?? clip.Speed;
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(nodeSpeed))
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidSpeed,
|
|
$"Node '{node.InternalId}' 的有效播放速度无效。");
|
|
return false;
|
|
}
|
|
|
|
var outgoing = topology.GetOutgoing(currentNodeId);
|
|
if (outgoing.Count > 1)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Node '{node.InternalId}' 第一版最多只能有一个后继 Edge。");
|
|
return false;
|
|
}
|
|
|
|
if (node.EndBehaviorOverride.HasValue && outgoing.Count > 0)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Node '{node.InternalId}' 已设置结束行为,不能同时拥有后继 Edge。");
|
|
return false;
|
|
}
|
|
|
|
var isTerminal = outgoing.Count == 0;
|
|
var terminalBehavior = node.EndBehaviorOverride ??
|
|
options.EndBehaviorOverride ??
|
|
flow.EndBehaviorOverride ??
|
|
clip.DefaultEndBehavior;
|
|
steps.Add(new ResolvedPlaybackStep(clip, node.InternalId, nodeSpeed, terminalBehavior));
|
|
|
|
if (isTerminal)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var edge = outgoing[0];
|
|
if (!FrameAnimationValueUtility.IsValidInternalId(edge.InternalId) ||
|
|
edge.Condition != AnimationEdgeCondition.Always ||
|
|
edge.FromNodeId == edge.ToNodeId)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Node '{node.InternalId}' 的后继 Edge 无效。");
|
|
return false;
|
|
}
|
|
|
|
currentNodeId = edge.ToNodeId;
|
|
}
|
|
|
|
plan = new ResolvedPlaybackPlan(
|
|
flow.Id,
|
|
true,
|
|
steps,
|
|
flow.AsyncCompletionMode);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryValidateClip(FrameClip clip, out FrameAnimationPlaybackError error)
|
|
{
|
|
if (clip == null || string.IsNullOrWhiteSpace(clip.Id) || clip.FrameCount == 0)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
"FrameClip 为空、id 为空或没有 Frame。");
|
|
return false;
|
|
}
|
|
|
|
if (!FrameAnimationValueUtility.IsValidSpeed(clip.Speed))
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidSpeed,
|
|
$"Clip '{clip.Id}' 的 speed 无效。");
|
|
return false;
|
|
}
|
|
|
|
for (var index = 0; index < clip.Frames.Count; index++)
|
|
{
|
|
var frame = clip.Frames[index];
|
|
if (frame == null || frame.DurationMs <= 0)
|
|
{
|
|
error = Error(
|
|
FrameAnimationPlaybackErrorCode.InvalidPlayableData,
|
|
$"Clip '{clip.Id}' 的第 {index} 帧为空或 durationMs 无效。");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
error = FrameAnimationPlaybackError.None;
|
|
return true;
|
|
}
|
|
|
|
private static FrameAnimationPlaybackError Error(
|
|
FrameAnimationPlaybackErrorCode code,
|
|
string message)
|
|
{
|
|
return new FrameAnimationPlaybackError(code, message);
|
|
}
|
|
}
|
|
}
|