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