Home Lerper
Post
Cancel

Lerper

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 to b or we go back to a. To avoid run-time checks for Lerper_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 and onReachedTarget 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.

This post is licensed under CC BY 4.0 by the author.