Skip to content

Commit 75399c7

Browse files
committed
Added StepAnimator to facilitate multi-step, timed, and optionally looping "animations" in the form of event callbacks
1 parent d69370b commit 75399c7

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ MLEM uses [semantic versioning](https://semver.org/). Potentially breaking chang
44
## 8.1.0 (In Development)
55

66
### MLEM
7+
Additions
8+
- Added StepAnimator to facilitate multi-step, timed, and optionally looping "animations" in the form of event callbacks
9+
710
Fixes
811
- Fixed sprite animation groups not advancing to their first frame immediately
912
- Fixed GetRandomWeightedEntry throwing an exception when the random value is very close to 1

MLEM/Animations/StepAnimator.cs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using MLEM.Maths;
5+
using MLEM.Misc;
6+
7+
namespace MLEM.Animations {
8+
/// <summary>
9+
/// A step animator is a simple class to facilitate multi-step, timed, and optionally looping "animations" in the form of event callbacks.
10+
/// A step animator supports multiple <see cref="Steps"/> which are executed in order in <see cref="Update"/>.
11+
/// </summary>
12+
/// <typeparam name="TObject">The object to animate, which is passed to all relevant events.</typeparam>
13+
public class StepAnimator<TObject> : GenericDataHolder {
14+
15+
/// <summary>
16+
/// This step animator's steps.
17+
/// </summary>
18+
public readonly ReadOnlyCollection<Step> Steps;
19+
/// <summary>
20+
/// Whether this step animator is looping.
21+
/// </summary>
22+
public readonly bool Loop;
23+
24+
/// <summary>
25+
/// The index of the <see cref="CurrentStep"/> in the <see cref="Steps"/> collection.
26+
/// This is -1 if the animator has not started yet or is finished.
27+
/// </summary>
28+
public int CurrentStepIndex { get; private set; }
29+
/// <summary>
30+
/// The currently executed step, or <see langword="null"/> if the animator has not started yet or is finished.
31+
/// </summary>
32+
public Step CurrentStep => this.CurrentStepIndex >= 0 && this.CurrentStepIndex < this.Steps.Count ? this.Steps[this.CurrentStepIndex] : null;
33+
/// <summary>
34+
/// The time into the <see cref="CurrentStep"/>.
35+
/// </summary>
36+
public TimeSpan CurrentStepTime { get; private set; }
37+
/// <summary>
38+
/// The total time that the <see cref="CurrentStep"/> should play for.
39+
/// </summary>
40+
public TimeSpan CurrentStepTotalTime { get; private set; }
41+
42+
private readonly Random random;
43+
44+
/// <summary>
45+
/// Creates a new step animator with the given settings.
46+
/// </summary>
47+
/// <param name="random">The random instance to use for determining the time for each <see cref="Step"/> with differing <see cref="Step.MinTime"/> and <see cref="Step.MaxTime"/> to play for.</param>
48+
/// <param name="steps">The steps to play.</param>
49+
/// <param name="loop">Whether the <paramref name="steps"/> should loop.</param>
50+
public StepAnimator(Random random, IList<Step> steps, bool loop = false) {
51+
this.random = random;
52+
this.Steps = new ReadOnlyCollection<Step>(steps);
53+
this.Loop = loop;
54+
this.CurrentStepIndex = -1;
55+
}
56+
57+
/// <summary>
58+
/// Creates a new step animator with the given settings.
59+
/// </summary>
60+
/// <param name="steps">The steps to play.</param>
61+
/// <param name="loop">Whether the <paramref name="steps"/> should loop.</param>
62+
public StepAnimator(IList<Step> steps, bool loop = false) : this(new Random(), steps, loop) {}
63+
64+
/// <summary>
65+
/// Creates a new step animator with the given settings.
66+
/// </summary>
67+
/// <param name="steps">The steps to play.</param>
68+
public StepAnimator(params Step[] steps) : this(steps, false) {}
69+
70+
/// <summary>
71+
/// Updates this step animator, starting, updating and finishing its <see cref="Steps"/>.
72+
/// If this method returns <see langword="true"/>, this step animator has finished playing (which will never occur if there are any steps and <see cref="Loop"/> is <see langword="true"/>).
73+
/// </summary>
74+
/// <param name="obj">The object to animate, which is passed to all relevant events.</param>
75+
/// <param name="passed">The amount of time that has passed since the last time this method was called.</param>
76+
/// <returns>Whether this step animator has finished playing.</returns>
77+
public bool Update(TObject obj, TimeSpan passed) {
78+
if (this.CurrentStepIndex >= this.Steps.Count)
79+
return true;
80+
81+
if (this.CurrentStepIndex < 0)
82+
this.Advance(obj);
83+
84+
this.CurrentStepTime += passed;
85+
86+
var timePercentage = Math.Min(1, this.CurrentStepTime.Ticks / (float) this.CurrentStepTotalTime.Ticks);
87+
this.CurrentStep?.Animation?.Invoke(this, this.CurrentStep, obj, timePercentage);
88+
89+
return timePercentage >= 1 && this.Advance(obj);
90+
}
91+
92+
/// <summary>
93+
/// Resets this step animator, returning back to the first <see cref="Step"/> in its <see cref="Steps"/> collection.
94+
/// </summary>
95+
/// <param name="obj">The object to animate, which is passed to all relevant events.</param>
96+
public void Reset(TObject obj) {
97+
this.CurrentStep?.Finished?.Invoke(this, this.CurrentStep, obj);
98+
this.CurrentStepIndex = -1;
99+
}
100+
101+
private bool Advance(TObject obj) {
102+
this.CurrentStep?.Finished?.Invoke(this, this.CurrentStep, obj);
103+
104+
this.CurrentStepIndex++;
105+
if (this.CurrentStepIndex >= this.Steps.Count) {
106+
if (!this.Loop)
107+
return true;
108+
this.CurrentStepIndex = 0;
109+
}
110+
111+
this.CurrentStepTime = TimeSpan.Zero;
112+
this.CurrentStepTotalTime = this.CurrentStep == null ? TimeSpan.Zero :
113+
this.CurrentStep.MinTime == this.CurrentStep.MaxTime ? this.CurrentStep.MinTime :
114+
TimeSpan.FromTicks((long) this.random.NextSingle(this.CurrentStep.MinTime.Ticks, this.CurrentStep.MaxTime.Ticks));
115+
116+
this.CurrentStep?.Started?.Invoke(this, this.CurrentStep, obj);
117+
118+
return false;
119+
}
120+
121+
/// <summary>
122+
/// A step in a <see cref="StepAnimator{TObject}"/>.
123+
/// </summary>
124+
public class Step : GenericDataHolder {
125+
126+
/// <summary>
127+
/// The minimum time that this step should be able to play for.
128+
/// If this is different from <see cref="MaxTime"/>, a random time between the two is selected each time the step is played.
129+
/// </summary>
130+
public readonly TimeSpan MinTime;
131+
/// <summary>
132+
/// The maximum time that this step should be able to play for.
133+
/// If this is different from <see cref="MinTime"/>, a random time between the two is selected each time the step is played.
134+
/// </summary>
135+
public readonly TimeSpan MaxTime;
136+
137+
/// <summary>
138+
/// The animation function that is called every <see cref="Update"/> while this step is active.
139+
/// </summary>
140+
public AnimationFunction Animation;
141+
/// <summary>
142+
/// A function that is called when this step starts, before the first time <see cref="Animation"/> is called.
143+
/// </summary>
144+
public Action<StepAnimator<TObject>, Step, TObject> Started;
145+
/// <summary>
146+
/// A function that is called when this step finishes, after the last time <see cref="Animation"/> is called, or when this step is currently playing while the <see cref="StepAnimator{TObject}"/> is <see cref="Reset"/>.
147+
/// </summary>
148+
public Action<StepAnimator<TObject>, Step, TObject> Finished;
149+
150+
/// <summary>
151+
/// Creates a new animator step with the given settings.
152+
/// </summary>
153+
/// <param name="time">The time that this step should play for.</param>
154+
/// <param name="varianceFactor">An optional variance factor, which is used to determine a <see cref="MinTime"/> and <see cref="MaxTime"/> by multiplying the <paramref name="time"/> with it and subtracting the result from the <see cref="MinTime"/> and adding the result to the <see cref="MaxTime"/>.</param>
155+
/// <param name="animation">The animation function that is called every <see cref="Update"/> while this step is active.</param>
156+
public Step(TimeSpan time, float varianceFactor, AnimationFunction animation = null) : this(
157+
time - TimeSpan.FromTicks((long) (time.Ticks * varianceFactor)),
158+
time + TimeSpan.FromTicks((long) (time.Ticks * varianceFactor)),
159+
animation) {}
160+
161+
/// <summary>
162+
/// Creates a new animator step with the given settings.
163+
/// </summary>
164+
/// <param name="time">The time that this step should play for.</param>
165+
/// <param name="animation">The animation function that is called every <see cref="Update"/> while this step is active.</param>
166+
public Step(TimeSpan time, AnimationFunction animation = null) : this(time, time, animation) {}
167+
168+
/// <summary>
169+
/// Creates a new animator step with the given settings.
170+
/// </summary>
171+
/// <param name="minTime">The minimum time that this step should be able to play for. If this is different from <paramref name="maxTime"/>, a random time between the two is selected each time the step is played.</param>
172+
/// <param name="maxTime">The maximum time that this step should be able to play for. If this is different from <paramref name="minTime"/>, a random time between the two is selected each time the step is played.</param>
173+
/// <param name="animation">The animation function that is called every <see cref="Update"/> while this step is active.</param>
174+
public Step(TimeSpan minTime, TimeSpan maxTime, AnimationFunction animation = null) {
175+
this.MinTime = minTime;
176+
this.MaxTime = maxTime;
177+
this.Animation = animation;
178+
}
179+
180+
}
181+
182+
/// <summary>
183+
/// An animation function used by <see cref="Step"/>.
184+
/// The animation function that is called every <see cref="Update"/> while a <see cref="Step"/> is active.
185+
/// </summary>
186+
public delegate void AnimationFunction(StepAnimator<TObject> animator, Step step, TObject obj, float timePercentage);
187+
188+
}
189+
}

0 commit comments

Comments
 (0)