Understanding Basic Trigonometry to Calculate Movement for Server & Clients

Published by

on

One thing I probably should have done before I even started working on networking movement was to really understand and replicate how movement even works in my own code. I should warn the reader(s) that I actually am TERRIBLE at math. So even calculating the most basic things are usually quite a challenge for me. This post will dive into how I got movement working because I really didn’t understand it at first.

Starting from the Basics

As a fellow idiot who doesn’t know math, I’ll try to explain this with images and code because if you are like me, you can not read math symbols. Let’s keep it to 2D because it turns out that’s all I need for basic movement.

First, we need to know where our character is in space. On game start we set the location to X: 0, Y: 0, Z: 0 just to make things simple. We also need to know the rotation of our client, which is represented by Yaw, Pitch and Roll. Since we are not flying or swimming (yet!) we don’t care about roll, so let’s just focus on Yaw and Pitch:
Aileron_yaw.gifAileron_pitch.gif
Yaw is moving left to right here, and pitch is up and down. For walking/running movement however, we only actually care about Yaw. With Yaw we only need to care about two axes, the X and Y axis. In Unreal, using the TRotator we think of rotations in terms of degrees.

What complicates everything is that we need to move along the X/Y axis depending on what the camera is looking at, as this will change our angle of movement in world space. We also need to know whether or not the player is pressing the forward or backward keys + left/right keys at the same time, as this will further influence our movement angle.

We have to capture the keys pressed, yaw, and pitch and then calculate the velocity of the character DEPENDING on the direction (rotation) of the character. What does this even look like in code?

It took me a while to figure out what to do, it turns out we need to employ some trigonometry.

Before Velocity, We Need Direction

OK we have angles, but how do we translate that into direction? We need to apply the rotations while we are calculating the players velocity. To do that we need to use the sin and cos functions. How does sin and cos help us?
912x3s6n8dq61.gif
Image taken from reddit post explaining sine, cosine and tangent.

The main thing to note is cos(0) = x:0, y:1, and sin(0) = x:0, y:0. Since my brain still didn’t see how this applied, I ended up using a graphing calculator to figure out what these functions would look like.

Except I forgot one major fundamental aspect of working with sine/cosine, they work with radians NOT degrees. Here’s what my game movement looked like when I applied the functions using degrees by accident, note the keys I was pressing were not moving my character in the right direction either, then the character flipped on its head when I changed pitch!

Right so sin and cos need radians. Luckily those are simple, you just calculate them like this:

auto YawAngleInRadians = YawDegrees * 3.14 / 180; // 3.14 being PI

In my case, I’m using the lovely cglm library, so I just call glm_rad instead. To understand this better, let’s apply some different rotations to see what it looks like on a graph. Let’s say the character is facing the Y axis (default in Unreal Engine). Pretend the below plot is a top down view where our character is standing on the origin (0,0) point. We need to move this character forward and to the right by 45 degrees (basically diagonally), that would turn out to be:

X = sin(glm_rad(45));
Y = cos(glm_rad(45));

Pasted image 20240210182541.png
Look at that! The intersection of sin/cos of a 45 degree angle in radians is forward and to the right, just as we want. Let’s say we are going to the left:
Pasted image 20240210182736.png
How about backwards?
Pasted image 20240210182844.png

Hopefully you get the point (pun intended). All we need to do is use the sin/cos and apply it to our X/Y axes to get the direction. To get velocity, we calculate a constant acceleration value and multiply it by our delta time (of the frame tick):

Rotation.vec4[1] = PitchsToProcess.at(i); // Pitch
Rotation.vec4[2] = YawsToProcess.at(i); // Yaw

// add/sub an additional 45 degrees to the angle if we are pressing left/right keys
float YawAngle = Rotation.vec4[2];
if (Input.IsPressed(input::InputType::Left))
{
	YawAngle -= 45;
}
else if (Input.IsPressed(input::InputType::Right))
{
	YawAngle += 45;
}

YawAngle = glm_rad(YawAngle); // convert to radians
float Acceleration = Movement.BaseAcceleration * It.delta_time();
Log->Info("UpdateVelocity: YawAngle {} Acceleration: {}", YawAngle, Acceleration);

if (Input.IsPressed(input::InputType::Forward))
{
	Velocity.vec3[0] = cos(YawAngle) * Acceleration;
	Velocity.vec3[1] = sin(YawAngle) * Acceleration;
	Log->Info("UpdateVelocity: Adding forward X Acceleration {} Y: {}", Velocity.vec3[0], Velocity.vec3[1]);
} 
else if (Input.IsPressed(input::InputType::Backward))
{
	Velocity.vec3[0] = cos(YawAngle) * -Acceleration;
	Velocity.vec3[1] = sin(YawAngle) * -Acceleration;
	Log->Info("UpdateVelocity: Adding backward X Acceleration {} Y: {}", Velocity.vec3[0], Velocity.vec3[1]);
}

There you have it, we (somewhat) have our velocity!

Updating positions with our velocity

The above code was calculating the Velocity of our player given the inputs, but we also have to apply that Velocity to our character, we do that by adding to our character’s world location vector. This calculation is done in in our flec’s “Movement” system:

 // Moves any entity with a Position and Velocity
GameWorld.system<units::Position, units::Velocity>("Movement")
	.each([=](flecs::iter& It, size_t Index, units::Position &Position, units::Velocity &Velocity)
	{
		Position.vec3[0] += Velocity.vec3[0] * It.delta_time();
		Position.vec3[1] += Velocity.vec3[1] * It.delta_time();
		Position.vec3[2] += Velocity.vec3[2] * It.delta_time();
		auto Entity = It.entity(Index);
		Log->Info("Entity {} Now in position (x={},y={},z={})", Entity.id(), Position.vec3[0], Position.vec3[1], Position.vec3[2]);
	});

Here’s the result:

It kind of works, but on initial input we actually have a sudden jump when we start new inputs. What we need is to smoothly lerp between the current position and the next position. Since all movement needs to be server authoritative, that means we need to update our Movement system with the lerp and NOT do any of this in the Unreal client. Luckily cglm comes to our rescue again!

// Moves any entity with a Position and Velocity
GameWorld.system<units::Position, units::Velocity>("Movement")
	.each([=](flecs::iter& It, size_t Index, units::Position &Position, units::Velocity &Velocity)
	{
		auto NewPositionX = Position.vec3[0] + Velocity.vec3[0];
		auto NewPositionY = Position.vec3[1] + Velocity.vec3[1];
		auto NewPositionZ = Position.vec3[2] + Velocity.vec3[2];
		Position.vec3[0] = glm_lerp(Position.vec3[0], NewPositionX, It.delta_time());
		Position.vec3[1] = glm_lerp(Position.vec3[1], NewPositionY, It.delta_time());
		Position.vec3[2] = glm_lerp(Position.vec3[2], NewPositionZ, It.delta_time());
	  
		auto Entity = It.entity(Index);
		Log->Info("Entity {} Now in position (x={},y={},z={})", Entity.id(), Position.vec3[0], Position.vec3[1], Position.vec3[2]);
	});

Now we can smoothly move between our positions… but we have another problem, our character ‘slides’ for ever after we start moving. This is because we never apply any friction to counter it when we stop moving. Let’s add that in to our velocity calculations, if the user isn’t pressing forward/backward or being a dummy and pressing forward and backward at the same time:

YawAngle = glm_rad(YawAngle); // convert to radians
float Acceleration = Movement.BaseAcceleration * It.delta_time();
float Friction = Movement.BreakingFriction * It.delta_time();
Log->Info("UpdateVelocity: YawAngle {} Acceleration: {} AngularAcceleration: {}", YawAngle, Acceleration);

// Funny guy check
if (Input.IsPressed(input::InputType::Forward) && Input.IsPressed(input::InputType::Backward))
{
	// Apply friction because we are technically not moving
	ApplyFriction(Velocity, Friction);
	break;
}

if (Input.IsPressed(input::InputType::Forward))
{
	Velocity.vec3[0] = cos(YawAngle) * Acceleration;
	Log->Info("UpdateVelocity: Adding forward X Acceleration {}", Velocity.vec3[0]);
	Velocity.vec3[1] = sin(YawAngle) * Acceleration;
	Log->Info("UpdateVelocity: Adding forward Y Acceleration {}", Velocity.vec3[1]);
} 
else if (Input.IsPressed(input::InputType::Backward))
{
	Velocity.vec3[0] = cos(YawAngle) * -Acceleration;
	Log->Info("UpdateVelocity: Adding backward X Acceleration {}", Velocity.vec3[0]);
	Velocity.vec3[1] = sin(YawAngle) * -Acceleration;
	Log->Info("UpdateVelocity: Adding backward Y Acceleration {}", Velocity.vec3[1]);
}
else if (!Input.IsPressed(input::InputType::Forward) && !Input.IsPressed(input::InputType::Backward))
{
	// Apply friction either +/- depending on which direction we are going
	ApplyFriction(Velocity, Friction);
	Log->Info("UpdateVelocity: Adding friction X {} Y: {}", Velocity.vec3[0], Velocity.vec3[1]);
}

Where the ApplyFriction function is as follows:

/**
 * @brief ApplyFriction to the input velocity and the value for how much
 * friction to apply. Updates Velocity in place
 * 
 * @param Velocity [in/out]
 * @param FrictionValue How much Friction to apply
 */
 void ApplyFriction(units::Velocity &Velocity, const float FrictionValue)
{
	 // Apply friction either +/- depending on which direction we are going
	if (Velocity.vec3[0] > 0)
	{
		Velocity.vec3[0] = glm_max(0, Velocity.vec3[0] - FrictionValue);
	}
	else if (Velocity.vec3[0] < 0)
	{
		Velocity.vec3[0] = glm_min(0, Velocity.vec3[0] + FrictionValue);
	}
	
	if (Velocity.vec3[1] > 0)
	{
		Velocity.vec3[1] = glm_max(0, Velocity.vec3[1] - FrictionValue);
	}
	else if (Velocity.vec3[1] < 0)
	{
		Velocity.vec3[1] = glm_min(0, Velocity.vec3[1] + FrictionValue);
	}
}

Note that depending on the current direction we are going, we need to force either min/max values compared to the new value. I think this logic could be condensed/written better but I haven’t given it much thought.

Now our character can both smoothly start and come to a stop if we aren’t pressing forward/backward:

Unfortunately, I have this strange artifact now where the character bumps up and down. I’m not entirely sure where this is coming from. I suspect either it’s trying to trigger an animation as I am calling SetActorLocationAndRotation, it might not like being teleported like this…?

// Called every frame
void UPMOMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (!PMOSystem)
	{
		return;
	}
	auto Player = Cast<APMOClientCharacter>(this->GetOwner());

	if (!Player)
	{
		UE_LOG(LogTemp, Warning, TEXT("Failed to get Player from MovementComponent"));
		return;
	}
    // Get the location & rotation from flecs / PMO Subsystem.
	auto Location = PMOSystem->GetPlayerLocation();
	auto Rotation = PMOSystem->GetPlayerRotation();

	UE_LOG(LogTemp, Warning, TEXT("Got PLayer Location %s and Rotation: %s"), *Location.ToString(), *Rotation.ToString());

	Player->SetActorLocationAndRotation(Location, Rotation);
}

I will leave this as a problem to solve in the next post! I will also need to figure how to re-wire my movement component/system into Unreal’s animation system so we can actually look like we are walking or running.