A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.Linq;
7using osu.Framework.Caching;
8
9namespace osu.Framework.Graphics.Animations
10{
11 /// <summary>
12 /// Represents a generic, frame-based animation. Inherit this class if you need custom animations.
13 /// </summary>
14 /// <typeparam name="T">The type of content in the frames of the animation.</typeparam>
15 public abstract class Animation<T> : AnimationClockComposite, IFramedAnimation
16 {
17 /// <summary>
18 /// The duration in milliseconds of a newly added frame, if no duration is explicitly specified when adding the frame.
19 /// Defaults to 60fps.
20 /// </summary>
21 public double DefaultFrameLength = 1000.0 / 60.0;
22
23 private readonly List<FrameData<T>> frameData;
24
25 /// <summary>
26 /// The number of frames this animation has.
27 /// </summary>
28 public int FrameCount => frameData.Count;
29
30 public int CurrentFrameIndex { get; private set; }
31
32 public T CurrentFrame => frameData[CurrentFrameIndex].Content;
33
34 private readonly Cached currentFrameCache = new Cached();
35
36 /// <summary>
37 /// Construct a new animation which loops by default.
38 /// </summary>
39 /// <param name="startAtCurrentTime">Whether the current clock time should be assumed as the 0th animation frame.</param>
40 protected Animation(bool startAtCurrentTime = true)
41 : base(startAtCurrentTime)
42 {
43 frameData = new List<FrameData<T>>();
44 Loop = true;
45 }
46
47 /// <summary>
48 /// Displays the frame with the given zero-based frame index.
49 /// </summary>
50 /// <param name="frameIndex">The zero-based index of the frame to display.</param>
51 public void GotoFrame(int frameIndex)
52 {
53 Seek(frameData[Math.Clamp(frameIndex, 0, frameData.Count)].DisplayStartTime);
54 }
55
56 /// <summary>
57 /// Adds a new frame with the given content and display duration (in milliseconds) to this animation.
58 /// </summary>
59 /// <param name="content">The content of the new frame.</param>
60 /// <param name="displayDuration">The duration the new frame should be displayed for.</param>
61 public void AddFrame(T content, double? displayDuration = null)
62 {
63 AddFrame(new FrameData<T>
64 {
65 Duration = displayDuration ?? DefaultFrameLength, // 60 fps by default
66 Content = content,
67 });
68 }
69
70 public void AddFrame(FrameData<T> frame)
71 {
72 var lastFrame = frameData.LastOrDefault();
73
74 frame.DisplayStartTime = lastFrame.DisplayEndTime;
75 Duration += frame.Duration;
76
77 frameData.Add(frame);
78
79 OnFrameAdded(frame.Content, frame.Duration);
80 }
81
82 /// <summary>
83 /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for 1/60th of a second.
84 /// </summary>
85 /// <param name="contents">The contents to use for creating new frames.</param>
86 public void AddFrames(IEnumerable<T> contents)
87 {
88 foreach (var t in contents)
89 AddFrame(t);
90 }
91
92 /// <summary>
93 /// Adds a new frame for each element in the given enumerable. Every frame will be displayed for the given number of milliseconds.
94 /// </summary>
95 /// <param name="frames">The contents and display durations to use for creating new frames.</param>
96 public void AddFrames(IEnumerable<FrameData<T>> frames)
97 {
98 foreach (var t in frames)
99 AddFrame(t.Content, t.Duration);
100 }
101
102 /// <summary>
103 /// Removes all frames from this animation.
104 /// </summary>
105 public void ClearFrames()
106 {
107 frameData.Clear();
108
109 Duration = 0;
110 CurrentFrameIndex = 0;
111 }
112
113 /// <summary>
114 /// Displays the given contents.
115 /// </summary>
116 /// <remarks>This method will only be called after <see cref="OnFrameAdded(T, double)"/> has been called at least once.</remarks>
117 /// <param name="content">The content that will be displayed.</param>
118 protected abstract void DisplayFrame(T content);
119
120 /// <summary>
121 /// Called whenever a new frame was added to this animation.
122 /// </summary>
123 /// <param name="content">The content of the new frame.</param>
124 /// <param name="displayDuration">The display duration of the new frame.</param>
125 protected virtual void OnFrameAdded(T content, double displayDuration)
126 {
127 }
128
129 protected override void Update()
130 {
131 base.Update();
132
133 if (frameData.Count == 0) return;
134
135 updateFrameIndex();
136
137 if (!currentFrameCache.IsValid)
138 updateCurrentFrame();
139 }
140
141 private void updateFrameIndex()
142 {
143 switch (PlaybackPosition.CompareTo(frameData[CurrentFrameIndex].DisplayStartTime))
144 {
145 case -1:
146 while (CurrentFrameIndex > 0 && PlaybackPosition < frameData[CurrentFrameIndex].DisplayStartTime)
147 {
148 CurrentFrameIndex--;
149 currentFrameCache.Invalidate();
150 }
151
152 break;
153
154 case 1:
155 while (CurrentFrameIndex < frameData.Count - 1 && PlaybackPosition >= frameData[CurrentFrameIndex].DisplayEndTime)
156 {
157 CurrentFrameIndex++;
158 currentFrameCache.Invalidate();
159 }
160
161 break;
162 }
163 }
164
165 private void updateCurrentFrame()
166 {
167 DisplayFrame(CurrentFrame);
168
169 UpdateSizing();
170
171 currentFrameCache.Validate();
172 }
173 }
174}