What is Lerper?
Most of the times that we want to do an interpolation, we have to go through the same overwhelming process of implementing interpolation, handling delta time, updating values, etc…
By creating this helper class, any interpolation could be done using this class.
There are two lerper classes available: Normal Lerper & Dynamic Lerper
Lerper implements a Lerper_Type which defines the behavior of the lerper: Default, PingPong
1
2
3
4
5
enum class Lerper_Type : uint8_t
{
Default = 0,
PingPong = 1
};
Lerper
Lerper uses a main object ticker that is alive in the game, until termination.
The main structure of lerper is based on:
updateFunc
delegateHandle
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
/**
* @brief One-time interpolation without dynamic changes after instantiation with self-handled destruction
* @tparam T Interpolation on Type
* @tparam L Interpolation Type
*/
template <typename T, Lerper_Type L = Lerper_Type::Default>
class Lerper final
{
bool done = false;
const T a, b, diff;
const float multiplier;
std::function<void(T)> updateFunc;
std::function<void()> onReachedTarget;
float alpha = 0;
FDelegateHandle delegateHandle;
Lerper(const T& A, const T& B, std::function<void(T)> UpdateFunc, const float Multiplier, std::function<void()> OnReachedTarget = nullptr) : a(A), b(B), diff(B - A), multiplier(Multiplier), updateFunc(UpdateFunc), onReachedTarget(OnReachedTarget)
{
delegateHandle = UACBGMLerpTicker::BindTickFunction(this, &Lerper::Tick);
}
~Lerper()
{
}
}
typename T will determine the target type that interpolation is going to be done on.
Lerper_Type L will determine the type of interpolation.
updateFunc is the function that will be called on each update.
onReachedTarget is the function that will be called when interpolation finishes.
delegateHandle is necessary because when our interpolation is done, we will need to remove this function from our multicast delegate. As soon as the interpolation is done, the object will be destructed.
Lerper Tick
Specialization plays a big role in implementation of our Tick
function, as each interpolation type is going to have a different implementation.
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
template <Lerper_Type LL = L, std::enable_if_t<LL == Lerper_Type::Default, bool> = false>
void Tick(const float DeltaTime)
{
if (alpha < 1)
{
updateFunc(a + (diff * alpha));
alpha += multiplier * DeltaTime;
}
else
{
FinalizeLerp<L>();
}
}
template <Lerper_Type LL = L, std::enable_if_t<LL == Lerper_Type::PingPong, bool> = false>
void Tick(const float DeltaTime)
{
if (alpha < 1)
{
updateFunc(a + (diff * alpha));
alpha += multiplier * DeltaTime;
}
else if (alpha < 2)
{
updateFunc(b - (diff * (alpha - 1)));
alpha += multiplier * DeltaTime;
}
else
{
FinalizeLerp<L>();
}
}
The Tick
function has been specialized here. We’re going to have different behaviors for each Lerper_Type
.
On Default
, we’re going to do the interpolation until we get to the final point/result b
.
On PingPong
, if alpha
is less than 1, meaning that the we have not yet reached our destination, we’re going to interpolate towards b
. If we’re less than 2, then we’ve reached b
and now we have to interpolate towards a
.
Specialization has been done using std::enable_if
Lerper Public Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Halt(const bool TeleportToDestination = false, const bool CallOnReachedTarget = false)
{
FinalizeLerp<L>(TeleportToDestination, CallOnReachedTarget);
}
void SetAlpha(const float Alpha)
{
alpha = Alpha;
}
float GetAlpha() const
{
return alpha;
}
Lerper Finalization
Lerper finalization does also utilize Specialization to avoid run-time Lerper_Type
checks.
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
template <Lerper_Type LL = L, std::enable_if_t<LL == Lerper_Type::Default, bool> = false>
void FinalizeLerp()
{
if (!done)
{
done = true;
UACBGMLerpTicker::UnbindTickFunction(delegateHandle);
updateFunc(b);
if (onReachedTarget)
onReachedTarget();
delete this;
}
}
template <Lerper_Type LL = L, std::enable_if_t<LL == Lerper_Type::PingPong, bool> = false>
void FinalizeLerp()
{
if (!done)
{
done = true;
UACBGMLerpTicker::UnbindTickFunction(delegateHandle);
updateFunc(a);
if (onReachedTarget)
onReachedTarget();
delete this;
}
}
First override of FinalizeLerp
has no parameters.
To avoid any calls more than once,
done
has been declared to handle that.
To unbind the function from the global ticker, UACBGMLerpTicker
::UnbindTickFunction
has been used.
Based on
Lerper_Type
, to finalize our lerper, we either go tob
or we go back toa
. To avoid run-time checks forLerper_Type
, specialization has been used.
If
onReachedTarget
is valid, then it will be called.
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
template <Lerper_Type LL = L, std::enable_if_t<LL == Lerper_Type::Default, bool> = false>
void FinalizeLerp(const bool TeleportToDestination, const bool CallOnReachedTarget)
{
if (!done)
{
done = true;
UACBGMLerpTicker::UnbindTickFunction(delegateHandle);
if (TeleportToDestination)
{
updateFunc(b);
}
if (onReachedTarget && CallOnReachedTarget)
{
onReachedTarget();
}
delete this;
}
}
template <Lerper_Type LL = L, std::enable_if_t<LL == Lerper_Type::PingPong, bool> = false>
void FinalizeLerp(const bool TeleportToDestination, const bool CallOnReachedTarget)
{
if (!done)
{
done = true;
UACBGMLerpTicker::UnbindTickFunction(delegateHandle);
if (TeleportToDestination)
{
updateFunc(a);
}
if (onReachedTarget && CallOnReachedTarget)
{
onReachedTarget();
}
delete this;
}
}
Second override of FinalizeLerp
has two parameters, TeleportToDestination
and CallOnReachedTarget
.
Based on the two parameters,
updatedFunc
andonReachedTarget
are going to be called respectively.
Instantiation
A helper function has been declared to check whether the two provided values are equal or not. If so, then the construction will not happen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static Lerper* Instantiate(const T& A, const T& B, std::function<void(T)> UpdateFunc, const float Multiplier, std::function<void()> OnReachedTarget = nullptr)
{
//TODO: static_assert(Static_Assert_Checks::Contains_Operators_V<T>, "Target typename should contain the following operators: [+, -, *]");
if (A == B)
{
if (OnReachedTarget)
OnReachedTarget();
return nullptr;
}
auto lerper = new Lerper(A, B, UpdateFunc, Multiplier, OnReachedTarget);
return lerper;
}
TODO: Static checks will be implemented to give clearer errors for the types that do not contain the respective
+
,-
and*
operators.