Creating Your First Unity Game – Rainy Knifes Part 3: Player Movement Script

Table of Contents

Creating Your First Unity Game – Rainy Knifes Part 3: Player Movement Script

Reading Time: 14 minutes
Level: Beginner
Version: 2020.3.1 LTS

Help Others Learn Game Development

Share on facebook
Share on twitter
Share on reddit
Share on linkedin

In part 2 of this tutorial series we talked about sprites, rendering order, colliders and rigid bodies.

We also prepared our Player by setting up all the necessary components to make the Player a solider physics game object.

In this part of the tutorial, we are going to create the Player movement script and make the Player move in the level.

Player Movement Script

Inside your project root folder(Assets) Right Click -> Create -> Folder, give it a name Scripts and inside of that folder create a new folder and name it Player Scripts.

Inside the Player Scripts folder, Right Click -> Create -> C# Script and give it a name PlayerMovement.

Attach the script on the Player game object in the Hierarchy tab. You can do this in two ways.

One is clicking on the Add Component button and searching for the script name in the search bar, another way is to drag the script from its folder on the Player game object in the Hierarchy:

Img 27

Choose whatever way works for you, but from now on, when I say attach a script to a specific game object, you are going to do it in one of the two mentioned ways.

Now double click the PlayerMovement script, this will open it in Visual Studio.

Private, Public And SerializeField

In the lecture titled C# Programming With Unity: Extending The Functionality Of A Class we introduced a concept called data encapsulation.

We talked about private and public variables and we gave examples for both. Now in Unity, having public or private variables affects another thing and that is the variable visibility in the Inspector tab.
 
To explain better what I mean, let’s take a look at an example. In our PlayerMovement script, one of the variables that we will declare is the moveSpeed:
 
				
					public float moveSpeed = 5f;
				
			

You can declare this variable below the class declaration, or above the Start() function because this is going to be a global class variable.

Since we declared this variable to be public, this variable will be visible in the Inspector tab.

You can verify this by selecting the Player game object in the Hierarchy and taking a look at his script component in the Inspector tab:

Img 28 - 85%

Let us first explain what is the purpose of having the variable exposed in the Inspector tab.

Having the variable visible in the Inspector tab allows us to change the value of that variable without the need to go back in the script.

For example, if we set the value of moveSpeed inside the script to be equal to 5, and then in the Inspector tab we change that value to 10, the value that Unity will use for that variable when the game starts is going to be 10.

So basically when you change the value of the variable in the Inspector tab, the value you declared in the script will be ignored and the new value in the Inspector tab will be used.

Memorize this concept because we will use this a lot in our development.

Now if you change the declaration of the moveSpeed variable from public to private like this:

				
					private float moveSpeed = 5f;
				
			

The variable will not be visible in the Inspector tab anymore which you can verify by selecting the Player game object again:

Img 29 - 85%

Going back to the lecture about data encapsulation and that it is better to set your variables to private, if you want to make your variable private but still be able to access it in the Inspector tab and change its value, you can add the SerializeField keyword in the variable declaration:

				
					[SerializeField]
    private float moveSpeed = 5f;
				
			
The SerializeField keyword goes in between the [] and it is put above the private variable you wish to make visible in the Inspector tab.
 
If you select the Player game object, you will see that the moveSpeed variable is once more visible in the Inspector tab:
Img 28 - 85%

Moving The Player

We are going to use the Transform component and its Position property to move the player.

Inside the Update() function, add the following lines of code:

				
					private void Update()
    {
        float moveX = Input.GetAxisRaw("Horizontal");

        Vector2 tempPos = transform.position;

        tempPos.x += moveX * moveSpeed * Time.deltaTime;

        transform.position = tempPos;
    }
				
			

Let’s break down what is happening in the code:

The line of code: Input.GetAxisRaw(“Horizontal”) will get the input when the user presses one of the following keys: Left Arrow, Right Arrow, A, or D and the function will return a float value.

The value will be -1 if the user presses the Left Arrow or the A key on the keyboard.

The value will be 1 if the user presses the Right Arrow or the D key on the keyboard.

And the value will be 0 if the user is not pressing any keys on the keyboard.

That is the reason why we are using a variable float moveX to store the value that will be returned by the GetAxisRaw static function.

You can test all of this out by using the following line of code:

				
					Debug.Log("The value of horizontal input is: " + Input.GetAxisRaw("Horizontal"));
				
			

You can add the above line of code in the Update function and test the output while you are pressing the specified keys I mentioned above.

The line of code: Vector2 tempPos = transform.position; is used to edit the position property of the transform.

Vector2 represents a vector in space with x and y coordinates.

We saw that the transform.position property has x, y and z coordinates, but this is a 2D game and we don’t need to access the z axis that’s why we created a Vector2 variable.

But even if we used Vector3 instead of Vector2, it will not affect our game at all because we will only edit x and y coordinates for that variable.

Now the reason why we first need to create a Vector2 tempPos variable is because in C#, we can’t edit the position property by typing something like:

				
					transform.position.x += moveX * moveSpeed * Time.deltaTime;
				
			

If you try to do this, you will get an error like this:

So to modify the position property we first need to store the current value of the position in a temporary variable, hence the Vector2 tempPos = transform.position; and now we can edit the values of the temporary variable.

Since we are moving horizontally e.g. along the X axis, we are editing the X value of the tempPos.

The line of code: tempPos.x += moveX * moveSpeed * Time.deltaTime; is adding to the current value of the X axis of the position the following calculation:

  • moveX will have the value of -1, 0, or 1 depending the input we get from the user
  • moveSpeed is set to be 5 by default, but we can always change that value to make the Player move faster or slower
  • Time.deltaTime is the interval in seconds from the last frame to the current one e.g. the time between two frames, which is a very small number
Time.deltaTime is always used to smooth out the movement when you are moving a game object using its transform property inside the Update function.
 
The “+=” is a shortcut for addition. Instead of writing:
 
				
					tempPos.x = tempPos.x + moveX * moveSpeed * Time.deltaTime;
				
			

We can write:

				
					tempPos.x += moveX * moveSpeed * Time.deltaTime;
				
			

And these two lines of code have the exact same outcome. To make this even more clear, I will use another example:

				
					int a = 1;

// Calculation 1
a += 1;

// Calculation 2
a = a + 1;
				
			

The two calculations above have the exact same outcome, just the Calculation 1 is a shortcut so that we don’t have to write a longer line of code.

Finally, the line of code transform.position = tempPos; will assign back the new position value to the transform.position property e.g. it will assign back the edited position back to the original property.

Again, this is because we can’t edit transform.position directly, instead we need to store it in a temporary value.

Now we can test the game out and see the outcome:

Try pressing A, D, Left Arrow and Right Arrow keys and you will see how the Player is moving like in the example above.

You can always change the moveSpeed value from the Inspector tab and make the Player move slower and faster, and I encourage you to do that right now just to get a grasp of how Unity game development works.

One thing to note is, you can also change the moveSpeed in the Inspector tab during gameplay testing, and any other variable that is exposed in the Inspector.

However, when you stop the game, the value will return back to the original value you had before you pressed the Play button:

This is something to keep in mind in case you want to change the value of the variable in the Inspector tab and you messed with it during gameplay, just remember the value you are satisfied with, and when you stop testing your game, set the value for that variable.

Rigidbody VS Transform

Another thing that you might be wondering is why did we attach a Rigidbody 2D on the Player game object and we are using its Transform component to move him.
 
The reason for that is because it’s more performance heavy if we move a game object that has a collider but doesn’t have a rigidbody using its Transform component.
 
If you remove the Rigidbody 2D from the Player the movement code will work as it did so far, but as I said, then the Player will only have a collider attached on him and we are moving him using Transform, so that can be performance heavy especially on mobile.
 
Because of that, even though we don’t use the Rigidbody 2D to move the Player, we still need to attach it on him.
 
We will demonstrate how to use rigidbodies to move game objects as well in other examples that we will see in the future.
 

Player's Facing Direction - Facing The Movement Direction

While the Player is moving he is not facing the movement direction.

The Player is by default turned to face the right side because that’s how the sprite was drawn, but when we move to the left side in the game, the Player should face the left side.

There are two ways how to fix this.

The first way is using the scale of the Player. If you remember, the Transform component has three properties: position, rotation and scale.

We can use the scale to change the facing direction of the game object. Just change the value to the negative number of the axis for which you want to change the direction:

As you can see, when we set the scale X to a negative value the Player changes the direction he is facing. This also works on any axis, but for our purpose we only need the X axis.

The second way how we can fix this issue is by using the Sprite Renderers Flip property:

Depending on the situation you might use one way over the other. In our current game, we can use any of the two and they will serve the purpose to change the facing direction.

I am going to use the Sprite Renderer component, and later in other games we will use the Scale property as well.

In the PlayerMovement script add the following lines of code:

				
					private SpriteRenderer sr;

    private void Awake()
    {
        sr = GetComponent<SpriteRenderer>();
    }
    
    private void Start()
    {
        
    }
				
			

When you plan to use a component of a game object often it is a good idea to declare it as a global variable inside the class hence the private SpriteRenderer sr declaration.

The Awake function is one of the initialization functions, and inside of it we get a reference to the SpriteRenderer by using GetComponent function.

If you don’t know what is GetComponent, then you should check out the lecture Object References And GetComponent.

Now that we have the reference to the SpriteRenderer component, we can modify the movement code to make the Player face the direction he is moving:

				
					private void Update()
    {
        float moveX = Input.GetAxisRaw("Horizontal");

        Vector2 tempPos = transform.position;

        tempPos.x += moveX * moveSpeed * Time.deltaTime;

        transform.position = tempPos;

        // change the facing direction
        if (moveX > 0)
            sr.flipX = false;
        else if (moveX < 0)
            sr.flipX = true;
    }
				
			

When we test the game now, the Player will face the direction he is moving to:

As you can see, when we move to the right side the Player is facing the right side, when we move to the left side, the Player is facing the left side.
 
You will also notice that the flipX property of the Sprite Renderer is a bool variable. When we need to face the right side we set its value to false, because the initial facing of the sprite is the right side.
 
But when we want to face the left side we set its value to true.
 

Unitys Coordinate System

We use the moveX variable to test if we should flip the sprite.
 
If you remember the Input.GetAxisRaw will return -1 if we press the Left Arrow or the A key, and it will return 1 if we press the Right Arrow or the D key.
 
That is the reason why we are testing if moveX > 0 or if moveX < 0, because in Unity, the left side is the negative side, and the right side is the positive side. For the vertical axis, up is the positive side and down is the negative side.
 
And this how we can determine if the Player is moving to the left or if he is moving to the right side.
 
This is how the coordinate system looks like in an image:
 
Img 31

So every time we have a negative value for the calculation of the position, the Player will move to the left side if the movement is horizontal and down if the movement is vertical.

And if we have a positive value for the calculation of the position, the Player will move to the right side for the horizontal movement and up for the vertical movement.

You will also notice that the if statement doesn’t have curly brackets {}. The reason for that is if you have only one line of code after the if statement, you can omit the curly brackets:

				
					// BOTH CODE VERSIONS HAVE THE SAME OUTCOME

// with curly brackets
if (moveX > 0)
{
    sr.flipX = false;
}
else if (moveX < 0)
{
    sr.flipX = true;
}

// without curly brackets
// possible only if you have ONE
// line of code after the if or else statement
if (moveX > 0)
    sr.flipX = false;
else if (moveX < 0)
    sr.flipX = true;
				
			

Initialization Functions In Unity

A new function that we introduced in this tutorial is the Awake function. We used it to get a reference to the Sprite Renderer component.

So far we saw the Start function and we were using it to initialize everything for our game. But there are two other initialization functions that we can use: one is Awake and the other is OnEnable.

Now if all three functions are used for initialization, what is the difference between them?

First the execution order. One function is called before the other and we can easily test that by using Debug.Log:

				
					private void Awake()
    {
        Debug.Log("Awake: Initialized");
    }

    private void OnEnable()
    {
        Debug.Log("OnEnable: Initialized");
    }

    private void Start()
    {
        Debug.Log("Start: Initialized");
    }
				
			

If you run the game you will see this in the Console:

As you can see, the first function that is called is Awake, then OnEnable, then Start. This is the order by which these functions are called.
 
Another difference between them is that OnEnable is called every time a game object is activated after being deactivated, we will see examples of this in other lectures.
 
I mainly use the Awake function for initializations where we GetComponents or get references to game objects in the Scene, that’s why I put the code to get the Sprite Renderer component in the Awake.
 
When we progress more in other lectures we will take a look at multiple examples of initialization using the functions mentioned above and I will show examples where you will use one over the other and explain the reasons why.
 

Player's Bounds

While everything is ok with the player’s movement, we didn’t set up any bounds where the Player can move.

With this I mean that the Player can get out of screen if you walk to far away on the left or the right side.

Because of that, we need a way to strict his movement only within the bounds of the level:

Img 33

There are multiple ways how we can fix this issue, but one of the most simplest ways is to take the Player game object, move him up to the bound where you want to strict him and take a look at the current X position(because we are setting horizontal bounds), that will be the bound up to where the Player can go:

As you can see from the example above, you simply move the Player up to the edge of the bound, and then take that X position as the bound value and for the bound on the left side, just use the negative value.

Now we need to create two variables that will represent those bounds in the code. So right below the SpriteRenderer sr declaration add the following:

				
					[SerializeField]
    private float min_X = -2.28f;

    [SerializeField]
    private float max_X = -2.28f;
				
			
One neat trick that we can use when we are declaring two or more variables of the same type is the following:
 
				
					[SerializeField]
    private float min_X = -2.28f, max_X = 2.28f;
				
			
The code we wrote above and the code in the example before it do the exact same thing – create two float variables, only the code above is shorter.
 
Just by reading the names of our variables you can assume that the max_X variable is used for the bound on the right side, and min_X variable is used for the bound on the left side.
 
In the lecture about variables, we talked about the importance of giving your variables meaningful names.
 
The reason for that is it will make your code more readable and you will understand what is the purpose of a specific variable just by reading its name.
 
Let us now strict the Player’s movement via code:
 
				
					private void Update()
    {
        float moveX = Input.GetAxisRaw("Horizontal");

        Vector2 tempPos = transform.position;

        tempPos.x += moveX * moveSpeed * Time.deltaTime;

        // code for restricting player's movement
        if (tempPos.x > max_X)
            tempPos.x = max_X;
        else if (tempPos.x < min_X)
            tempPos.x = min_X;

        transform.position = tempPos;

        // change the facing direction
        if (moveX > 0)
            sr.flipX = false;
        else if (moveX < 0)
            sr.flipX = true;
    }
				
			
On line 9 we are testing if the tempPos.x is greater than max_X, if that is the case then we will set the tempPos.x to be equal to max_X.
 
This way we don’t allow the tempPos.x to go above the max_X value and hence restricting the Player’s movement on the right side.
 
For the left side we test if tempPos.x is less than min_X, and if that is the case we set the value of tempPos.x to be equal to min_X and this will restrict the Player’s movement on the left side.
 
You can test this out by running the game:
 

Writing Better Code

The last thing that we will do in this lecture is format and optimize our code.

First there is one thing that I need to explain in regards to the Update function.

The Update function is called every frame, which means if we have sixty frames in one second, the Update function is called 60 times in one second.

That is a lot of code that needs to be processed in a small amount of time. That’s why it’s important that you optimize the code you put in the Update otherwise your game can slow down.

In regards to what we did so far, here is what we can change. The two variables that we are creating:

				
					private void Update()
    {
        float moveX = Input.GetAxisRaw("Horizontal");

        Vector2 tempPos = transform.position;
    }
				
			
Instead of creating the float moveX and Vector2 tempPos every frame, we can declare them as variables and use them like variables:
 
				
					// These variables are declared above
// the Awake function
private float moveX;

private Vector2 tempPos;
				
			

Don’t forget to change the code in the Update as well:

				
					private void Update()
    {
        moveX = Input.GetAxisRaw("Horizontal");

        tempPos = transform.position;
    }
				
			

Now the code above is not creating new floats and Vector2 variables every frame, instead it is using the ones we already declared and this is much more efficient for our game.

Of course, even if we leave the first version of the code it will not affect our game performance, but this is a small game with not that many lines of code, if you are creating a bigger game and you are not paying attention to these things, then your game can slow down.

I also want to group the lines of code in a function, because that will make it more readable and it will be easier to maintain.

This is what we talked about in the lecture about functions, we group our code into functions and then use the function name to call the code.

I will leave the re-written code below and you can inspect it and compare it to the first version of the code that we had and see how neatly everything is grouped now:
 
				
					using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    [SerializeField]
    private float moveSpeed = 5f;

    private float moveX;

    private Vector2 tempPos;

    private SpriteRenderer sr;

    [SerializeField]
    private float min_X = -2.28f, max_X = 2.28f;

    private void Awake()
    {
        sr = GetComponent<SpriteRenderer>();
    }

    private void Update()
    {
        HandleMovement();
        HandleFacingDirection();
    }

    // function responsible for moving the player
    void HandleMovement()
    {
        moveX = Input.GetAxisRaw("Horizontal");

        tempPos = transform.position;

        tempPos.x += moveX * moveSpeed * Time.deltaTime;

        // call the handle bounds function before
        // you re-assign the position back to
        // transform.position
        HandleBoundsRestriction();

        transform.position = tempPos;
    }

    // function for handling the bounds restriction
    void HandleBoundsRestriction()
    {
        if (tempPos.x > max_X)
            tempPos.x = max_X;
        else if (tempPos.x < min_X)
            tempPos.x = min_X;
    }

    // function for handling the facing direction
    void HandleFacingDirection()
    {
        if (moveX > 0)
            sr.flipX = false;
        else if (moveX < 0)
            sr.flipX = true;
    }

} // class
				
			

Where To Go From Here

In this lecture we covered the code side of making the Player move along with making him face the direction of his movement and setting up the bounds where the Player can move.

Next, we will move on to the fourth part of this tutorial titled Knifes, Prefabs, And Spawning Obstacles In The Game where we will learn how to save game objects and how to spawn them in the game from our code.

Leave a Comment