forked from stride3d/stride
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAnimationComponent.cs
More file actions
216 lines (189 loc) · 8.5 KB
/
AnimationComponent.cs
File metadata and controls
216 lines (189 loc) · 8.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;
using Stride.Animations;
using Stride.Core;
using Stride.Core.Collections;
using Stride.Engine.Design;
namespace Stride.Engine
{
/// <summary>
/// Add animation capabilities to an <see cref="Entity"/>. It will usually apply to <see cref="ModelComponent.Skeleton"/>
/// </summary>
/// <remarks>
/// Data is stored as in http://altdevblogaday.com/2011/10/23/low-level-animation-part-2/.
/// </remarks>
[DataContract("AnimationComponent")]
[DefaultEntityComponentProcessor(typeof(AnimationProcessor), ExecutionMode = ExecutionMode.Runtime | ExecutionMode.Thumbnail | ExecutionMode.Preview)]
[Display("Animations", Expand = ExpandRule.Once)]
[ComponentOrder(2000)]
[ComponentCategory("Animation")]
public sealed class AnimationComponent : EntityComponent
{
/// <summary>
/// Gets the animations associated to the component.
/// </summary>
/// <userdoc>The list of the animation associated to the entity.</userdoc>
public Dictionary<string, AnimationClip> Animations { get; } = new();
/// <summary>
/// Gets list of active animations. Use this to customize startup animations.
/// </summary>
[DataMemberIgnore]
public TrackingCollection<PlayingAnimation> PlayingAnimations { get; } = new();
[DataMemberIgnore]
public AnimationBlender Blender { get; internal set; } = new();
[DataMemberIgnore]
public IBlendTreeBuilder BlendTreeBuilder { get; set; }
public AnimationComponent()
{
PlayingAnimations.CollectionChanged += PlayingAnimations_CollectionChanged;
}
private void PlayingAnimations_CollectionChanged(object sender, TrackingCollectionChangedEventArgs e)
{
var item = (PlayingAnimation)e.Item;
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
{
var evaluator = item.Evaluator;
if (evaluator != null)
{
Blender.ReleaseEvaluator(evaluator);
item.Evaluator = null;
}
item.EndedTCS?.TrySetResult(true);
item.EndedTCS = null;
break;
}
default:
break;
}
}
/// <summary>
/// Plays right away the animation with the specified name, instantly removing all other blended animations.
/// </summary>
/// <param name="name">The animation name.</param>
public PlayingAnimation Play(string name)
{
PlayingAnimations.Clear();
var playingAnimation = new PlayingAnimation(name, Animations[name]) { CurrentTime = TimeSpan.Zero, Weight = 1.0f };
PlayingAnimations.Add(playingAnimation);
return playingAnimation;
}
/// <summary>
/// Returns <c>true</c> if the specified animation is in the list of currently playing animations
/// </summary>
/// <param name="name">The name of the animation to check</param>
/// <returns><c>true</c> if the animation is playing, <c>false</c> otherwise</returns>
public bool IsPlaying(string name)
{
foreach (var playingAnimation in PlayingAnimations)
{
if (playingAnimation.Name.Equals(name))
return true;
}
return false;
}
/// <summary>
/// Adds a new playing animation at the end of the list. It doesn't alter currently playing animations.
/// </summary>
/// <param name="clip">Animation clip to add to the list of playing animations</param>
/// <param name="timeScale">Speed at which the animation should play</param>
/// <param name="weight">Weight of the animation, in regard to all other playing animations.</param>
/// <param name="startTime">Time, in seconds, at which the animation starts playing</param>
/// <param name="blend">Blend mode - linear or additive</param>
/// <param name="repeatMode">Repeat mode - play once or loop indefinitely</param>
/// <returns>The added playing animation</returns>
public PlayingAnimation Add(AnimationClip clip, double startTime = 0, AnimationBlendOperation blend = AnimationBlendOperation.LinearBlend,
float timeScale = 1f, float weight = 1f, AnimationRepeatMode? repeatMode = null)
{
var playingAnimation = new PlayingAnimation(string.Empty, clip)
{
TimeFactor = timeScale,
Weight = weight,
CurrentTime = TimeSpan.FromSeconds(startTime),
BlendOperation = blend,
RepeatMode = repeatMode ?? clip.RepeatMode,
};
PlayingAnimations.Add(playingAnimation);
return playingAnimation;
}
/// <summary>
/// Crossfades to a new animation.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="fadeTimeSpan">The fade time span.</param>
/// <exception cref="ArgumentException">name</exception>
public PlayingAnimation Crossfade(string name, TimeSpan fadeTimeSpan)
{
if (!Animations.ContainsKey(name))
throw new ArgumentException(nameof(name));
// Fade all animations
foreach (var otherPlayingAnimation in PlayingAnimations)
{
otherPlayingAnimation.WeightTarget = 0.0f;
otherPlayingAnimation.CrossfadeRemainingTime = fadeTimeSpan;
}
// Blend to new animation
return Blend(name, 1.0f, fadeTimeSpan);
}
/// <summary>
/// Blends progressively a new animation.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="desiredWeight">The desired weight.</param>
/// <param name="fadeTimeSpan">The fade time span.</param>
/// <exception cref="ArgumentException">name</exception>
public PlayingAnimation Blend(string name, float desiredWeight, TimeSpan fadeTimeSpan)
{
if (!Animations.ContainsKey(name))
throw new ArgumentException("name");
var playingAnimation = new PlayingAnimation(name, Animations[name]) { CurrentTime = TimeSpan.Zero, Weight = 0.0f };
PlayingAnimations.Add(playingAnimation);
if (fadeTimeSpan > TimeSpan.Zero)
{
playingAnimation.WeightTarget = desiredWeight;
playingAnimation.CrossfadeRemainingTime = fadeTimeSpan;
}
else
{
playingAnimation.Weight = desiredWeight;
}
return playingAnimation;
}
public PlayingAnimation NewPlayingAnimation(string name)
{
return new PlayingAnimation(name, Animations[name]);
}
/// <summary>
/// Returns an awaitable object that will be completed when the animation is removed from the PlayingAnimation list.
/// </summary>
/// <remarks>
/// This happens when:
/// <list type="bullet">
/// <item>RepeatMode is PlayOnce and animation reached end</item>
/// <item>Animation faded out completely (due to blend to 0.0 or crossfade out)</item>
/// <item>Animation was manually removed from AnimationComponent.PlayingAnimations</item>
/// </list>
/// </remarks>
/// <returns></returns>
public Task Ended(PlayingAnimation animation)
{
if (!PlayingAnimations.Contains(animation))
throw new InvalidOperationException("Trying to await end of an animation which is not playing");
if (animation.EndedTCS == null)
{
Interlocked.CompareExchange(ref animation.EndedTCS, new TaskCompletionSource<bool>(), null);
}
return animation.EndedTCS.Task;
}
}
public interface IBlendTreeBuilder
{
void BuildBlendTree(List<AnimationOperation> animationList);
}
}