• Jaime Tous

Refactoring an Undergrad Game II: The Character Controller Part 1

Updated: Jan 23

There are currently input commands in five different scripts. Undergrad me got the job (somewhat) done, but this is bad for a number of reasons:


1. If I make any change to my inputs--such as changing the name--I have to go through several different files to make sure all the input checks still work. I'd rather not do that, because it's time consuming and it's easy to miss something as the project becomes more complex.


2. Scripts should handle as few things as possible (e.g. the audio manager should not also be handling inputs). While this does mean I'll end up with far more scripts, reducing their complexity on the individual level reduces the chance of any changes I make affecting other parts of the game. Additionally, it makes those scripts easier to read/comprehend, since there's less stuff you need to follow.


3. Right now I do not have the ability for the player to rebind their keys, and it would be very difficult to do so with the way it's structured. Consolidating all my commands with an input manager makes this far more feasible. I won't touch this one just yet, but I'll get back to this in a future post.


We've got good reasons to refactor our input, so let's get started. Of course, we first remove the extra input checks, but where do we start after that? I like starting with the character controller, since it and the input manager are closely linked by virtue of the inputs giving the controller the data to do the interesting stuff. Why separate them? Because we want to be able to use this for more than just the player. We want to be able to use this character controller for AI. Having keyboard commands tied to these functions will make things messy.


When it came to basic top-down locomotion, I had the following:

private void FixedUpdate()

{

float h = Input.GetAxisRaw("Horizontal");

float v = Input.GetAxisRaw("Vertical");


Move(h, v);

Turning();

}


private void Move(float horizontal, float vertical)

{

// Set the movement vector based on the axis input.

Vector3 movement = new Vector3(horizontal, 0f, vertical);


// Normalize the movement vector and make it proportional to the speed per second.

movement = movement.normalized * speed * Time.deltaTime;


// Move the player to it's current position plus the movement.

characterRigidbody.MovePosition(transform.position + movement);

}

The method took in values of -1 to 1 from the horizontal and vertical axes and created a directional vector. This value was normalized-so that combined vertical and lateral movement didn't make you run faster-and then multiplied by the character speed and the frame time. Finally, this new vector value was added to the character's current transform position.


The keyboard-based movement method worked in conjunction with the mouse-based turning method:

private void Turning()

{

// Create a ray from the mouse cursor on screen in the direction of the camera

Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);


// Perform the raycast and if it hits something on the floor layer

if (Physics.Raycast(camRay, out RaycastHit floorHit, camRayLength, floorMask))

{

// Create vector from player to point on floor raycast from mouse hit.

Vector3 playerToMouse = floorHit.point - transform.position;


// Create a quaternion based on the vector from the player to the mouse.

Quaternion newRotation = Quaternion.LookRotation(playerToMouse);


// Set the player's rotation to this new rotation.

playerRigidbody.MoveRotation(newRotation);

}

}

A ray was cast directly from the mouse position. If the ray hit anything anything on the "Floor" layer, the player's position was subtracted from the raycast hit point to get the direction vector. This direction vector was then used to create a quaternion rotation to rotate the player towards the hit point.


This was functional, in that you could move and turn, but it led to the situation that your character would move in one direction while they faced another. This isn't ideal. Not only does it look awful, but it's unintuitive, as the vertical axis is generally used for forward movement in most games.


This is an easy fix. Here's the new code:

public void MoveCharacter(float vertical, float horizontal)

{

Vector3 movement = (transform.forward * vertical) + (transform.right * horizontal);

movement = movement.normalized * _speed * Time.deltaTime;

characterRigidbody.MovePosition(transform.position + movement);

}

There's only one difference in this snippet; we're calculating the movement direction based on the character's orientation rather than using an absolute orientation. This means when the character faces down and the player presses forward, the character moves down. With that changed, we can touch up the TurnCharacter method.


Since the player's forward motion is based on the character's direction, and currently the character is made to look at the mouse position, the character will snap instantly to a new mouse location. That's an easy fix, though. All we need to do is insert a lerp between the character's current location and where the mouse is located, add a factor for speed, and we're good to go. It looks like this:

characterRigidbody.MoveRotation(Quaternion.Lerp(

transform.rotation,

targetRotation,

rotationSpeed * Time.deltaTime)

This is it for this entry, as it's gotten a bit long. Next entry we'll tie our modified player controller to our external input manager.

©2020 by Jaime Tous