Files
aibis-dream/Assets/Scripts/FrameAnimation/Runtime/FrameAnimationResolver.cs
T

275 lines
10 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 ResolvedPlaybackPlan(string playableId, bool isFlow, IReadOnlyList<ResolvedPlaybackStep> steps)
{
PlayableId = playableId;
IsFlow = isFlow;
Steps = steps;
}
}
internal static class FrameAnimationResolver
{
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], playableId, options, out plan, out error);
}
return TryResolveFlow(graph, flowMatches[0], options, out plan, out error);
}
private static bool TryResolveClip(
FrameClip clip,
string playableId,
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(
playableId,
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);
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);
}
}
}