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 osu.Framework.Allocation;
6using osu.Framework.Graphics.Containers;
7using osu.Framework.Timing;
8
9namespace osu.Framework.Graphics.Animations
10{
11 public abstract class AnimationClockComposite : CustomisableSizeCompositeDrawable, IAnimation
12 {
13 private readonly bool startAtCurrentTime;
14
15 private bool hasSeeked;
16
17 private readonly ManualClock manualClock = new ManualClock();
18
19 /// <summary>
20 /// Construct a new animation.
21 /// </summary>
22 /// <param name="startAtCurrentTime">Whether the current clock time should be assumed as the 0th animation frame.</param>
23 protected AnimationClockComposite(bool startAtCurrentTime = true)
24 {
25 this.startAtCurrentTime = startAtCurrentTime;
26 }
27
28 [BackgroundDependencyLoader]
29 private void load()
30 {
31 base.AddInternal(new Container
32 {
33 RelativeSizeAxes = Axes.Both,
34 Clock = new FramedClock(manualClock),
35 Child = CreateContent()
36 });
37 }
38
39 public override IFrameBasedClock Clock
40 {
41 get => base.Clock;
42 set
43 {
44 base.Clock = value;
45 consumeClockTime();
46 }
47 }
48
49 protected override void LoadComplete()
50 {
51 base.LoadComplete();
52
53 // always consume to zero out elapsed for update loop.
54 double elapsed = consumeClockTime();
55
56 if (!startAtCurrentTime && !hasSeeked)
57 manualClock.CurrentTime += elapsed;
58 }
59
60 protected internal override void AddInternal(Drawable drawable) => throw new InvalidOperationException($"Use {nameof(CreateContent)} instead.");
61
62 /// <summary>
63 /// Create the content container for animation display.
64 /// </summary>
65 /// <returns>The container providing the content to be added into this <see cref="AnimationClockComposite"/>'s hierarchy.</returns>
66 public abstract Drawable CreateContent();
67
68 private double lastConsumedTime;
69
70 protected override void Update()
71 {
72 base.Update();
73
74 double consumedTime = consumeClockTime();
75 if (IsPlaying)
76 manualClock.CurrentTime += consumedTime;
77 }
78
79 /// <summary>
80 /// The current playback position of the animation, in milliseconds.
81 /// </summary>
82 public double PlaybackPosition
83 {
84 get
85 {
86 double current = manualClock.CurrentTime;
87
88 if (Loop) current %= Duration;
89
90 return Math.Clamp(current, 0, Duration);
91 }
92 set
93 {
94 hasSeeked = true;
95 manualClock.CurrentTime = value;
96
97 // consume current clock to avoid additional jumps on top of the seek due to time naturally elapsing.
98 // there's no need to do this before we're loaded - LoadComplete() will also consume clock initially,
99 // and Time might not even be initialised yet during load
100 if (IsLoaded)
101 consumeClockTime();
102 }
103 }
104
105 public double Duration { get; protected set; }
106
107 public bool IsPlaying { get; set; } = true;
108
109 public virtual bool Loop { get; set; }
110
111 public void Seek(double time) => PlaybackPosition = time;
112
113 private double consumeClockTime()
114 {
115 double elapsed = Time.Current - lastConsumedTime;
116 lastConsumedTime = Time.Current;
117 return elapsed;
118 }
119 }
120}