Unity 2D - 4 way movement system

13. jan. 2021

The tutorial is written in a step by step process. If you're familiar with Unity or you're only interested in the code part, you can skip to the end, where you can find the snippet of the entire code.

ome parts of this tutorial are easier to follow through video, so make sure to check out this tutorial in my Youtube video.

As we've discussed in the previous post, Unity is a very popular and capable game engine. In order to use it, you have to download it first on their website; you can do that here. Select the correct version for your PC and follow the installation process (it's very easy and similar to any software install)


How to start a project

After you start Unity Hub, simply click on "new" and select the type of project you wish to create. We'll start with a 2D project since it's a bit simpler if you're a solo developer and completely new to game development. If you'd like to start a 3D project, you can select that here. Then name your project and select where you want it to be saved. After that, click "create" and wait a bit for the project to load.

Quick overview of the interface

1) A menu of all game objects in your current scene. Here is also the place to create new ones.

2) Menus for animation and grid manipulation (drawing, erasing etc.).

3) Project folder, where you can create scripts, prefabs, scenes, materials and organise them in folders.

4) Scene manager - grab game objects, draw Tilemaps, get an overview, and most of the work gets done here.

5) Inspector - Game object settings, you can add components and then set them to the values you want.

Unity interface overview BuckySide

Don't worry, the interface will feel more familiar as soon as you start working with some examples.

Creating a player

The first thing you need to create a player is a game object. In the Hierarchy area, right-click and select "Create empty". Now you can name it "Player", so you'll always know which game object is the Player game object. Then go to the "Inspector" on the right and select "Player" for the tag. You will soon find out why this is useful.

Right now you still can't see the player, so let's change that. Click add component in the "Inspector" and type "Sprite Renderer" in the search area and confirm it. This component allows us to use an image to represent the game object. If you have your own art, you can use that, if not, you can download my art here. Now let's go to the "Project" area and let's right-click and create a new folder, name it "Art". Then simply drag the art into Unity in the Art folder. Now click on the "tutorial-player-walk" image you've just imported, and in the Inspector, select:

- Sprite mode: Multiple

- Pixels per Unit: 16

- Filter mode: Point (no filter)

- Compression: None

Then click "Apply", and after it's done, click on the "Sprite Editor". Now select the slice, grid by cell size and enter 16x32 - that's correct for the pixel art you've downloaded in this post. If you have different art, slice it based on how it was created. Then click "Finish", and we're done with importing art for our player. Now let's once again click on the Player game object and select the little knob in the Sprite Renderer component (Sprite section) in the "Inspector". Now select a sprite. I usually just go with a front-facing character, don't worry, we'll add animations later, and it won't be important.

The next thing we want to do is add more components to the Player game object to make him move and interact with the game world. Let's add components (the same way we did the Sprite Renderer), "BoxCollider2D" and "RigidBody2D". The "RigidBody2D" component will be used to move our character, and the "BoxCollider2D" component will be used for our character to interact with the game world. For our example, we'll use a 4-way directional movement, which is used in top-down games. In order to do that you should go to the Inspector area and inside the "RigidBody2D" component set "Gravity Scale" to 0. This way, gravity won't pull out the player down. If you want for example a platformer, you can use gravity since you have 2-way directional movement with jumps for going up and gravity to pull you back down.

Making the player move

Now it's time for us to create our first script! Let's head to the project area, and the same way we've created and Art folder before, we now create a "Scripts" folder. I usually create another folder called "Player" and put all of the player scripts in here, just to keep things organised. Then inside the folder right click and create a new C# script. You can name it "PlayerMovement". Open the script. There are two methods already created, a Start method and an Update method. A start method is executed only once, and that's at the start when the game object becomes active. The Update method starts after the Start method and is continuously executed. There are a lot of other methods, which all function differently and can be used in different scenarios. We'll learn two other methods in this tutorial post.

The first is a method called "Awake". It's similar to the start method, but it's executed before the Start method. I like using the Awake method when I'm setting variables, getting components and other similar tasks. However, before the methods, let's create variables, one will be a float called "speed", and the other will be named "playerRigidbody", allowing us to manipulate the RigidBody2D inside our code. Variables can be "private" or "public" (same with methods or voids). If it's private, it means it can only be used in this script and won't be visible in the Inspector. If it's public, it can be referenced and used in other methods/voids, and it will be visible in the Inspector. For speed we can set it to private and set the variable to [SerializeField]. This setting makes the variable visible in the Unity Inspector.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    [SerializeField]
    private float speed;

    private Rigidbody2D playerRigidBody;
    private Vector3 moveDirection;

After the variables are completed, let's create the Awake method and assign the RigidBody2D component.

    private void Awake()
    {
        playerRigidBody = GetComponent<Rigidbody2D>();
    }

Now that our code can access the Rigidbody2D component of the out Player game object, we want to add velocity to the component. For that, we're going to use the "FixedUpdate" method. The difference between the Fixed update method and the Update method is that the Update method runs once per frame, while the Fixed update can run zero times, once or many times per frame. That's because it considers your frame rate. Now that we're assigning velocity to a game object, it's better to do it in a Fixed update so that players with lower FPS won't have slower moving characters than players with a higher FPS.

To apply velocity to the Ridigdbody, we'll use a Vector3 and movedirection variable we've set and multiplied the X-axis and Y-axis values with speed. We don't have to worry about the Z-axis as we're in a 2D space.

    private void FixedUpdate()
    {
        playerRigidBody.velocity = new Vector3(moveDirection.x * speed, moveDirection.y * speed);
    }

The last thing we need to do is get players to input in our script. This part we can do in the Update method as the things we've talked about before don't apply here. We'll make two variables, a moveX and a moveY which will receive the player's input. We are using Unity's Input manager for this, where the axis input is set to "WASD" or arrow keys when using a keyboard. You can change the bidings here if you'd like something else.

When we get the input, we just have to set the moveDirection variable to the correct Vector3 using the moveX and moveY. In the end, we're going to add ".normalize" to ensure the player doesn't move faster when going diagonally.

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

        moveDirection = new Vector3(moveX, moveY, 0f).normalized;
    }

And that's all the code we need to make our player move! Now select your player and add the script as a component in the Inspector. You can now set the speed to anything you want, play around with it and see what feels right. I've set mine to 5.

Building the game world

Now that we can move the player, we don't want to walk around in space as we currently are. So let's make a game world around the player!

In the Hierarchy area, right-click and go to 2D Object -> Tilemap -> Rectangular. We can rename the first Tilemap to "Ground". Now I suggest you duplicate it, and you do that by selecting it, then press "Ctrl+D", and you can rename it to "Collision". You can also do that later, but there can be a collision issue that can be fixed by erasing blank Collision tiles that can get placed. But we're avoiding all of that by simply creating both right now. For the Collision tilemap, you want to add a component in the Inspector, called "Tilemap Collider 2D". You can leave all the settings in the component to the default values.

Now import the second art (tutorial-overworld.png) to Unity the same way you've imported the character, and them all of the same settings, except when you're slicing it Grid by cell size, set that to be 16x16. Now navigate in Unity to the bottom left side of the program and select the "Tile Palette" tab. Click on "Create New Palette", name it what you want, and leave the rest of the settings default. When selecting where to save in the Windows Explorer, I usually navigate to the "Assets folder" (the main one) and create a new folder and name it "Tile Palettes". Again this is just for organisation, and it's really important once your project gets bigger. Then grab the art in the Art folder (inside Unity) and drag it to the Tile map.

Above the Tile map you have tools to paint and erase sprites. Make sure you're currently painting on the "Ground" Tile map.

You can probably no longer see the player, just the game world. That's because currently, we have both the player and the Ground Tile Map on the same sorting layer and defined in the same order in the layer. To fix this, select the Player game object and on the component Sprite renderer in the Inspector, set "Order in layer" to 1. This will make sure the player is always drawn above the game world.

Moving the camera

If you try and play your game, you'll quickly realize you want the camera to move with the player so you can explore more of the world. We'll create another script to do just that. I like to create another folder called "GeneralGame", for example, in the Scripts folder and create a new C# script there. I'll name it CameraMovement.

First, open the script, and you can delete both the Start and Update methods as we won't be using them. Now let's create a variable that will tell our script where our target -> Player is. We're going to reference the Transform of the player since that's the component that holds the positional values of the game object. I named it "target".

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMovement : MonoBehaviour
{
    private Transform target;

Now we want to set a target, and we'll do that in the awake method. It's very similar to what we did with Rigidbody2D, only here we'll find it by using a find function that utilises tags. Remember we've tagged the Player game object as "Player"? That's going to come in handy now.

    private void Awake()
    {
        target = GameObject.FindGameObjectWithTag("Player").transform;
    }

Great, the camera now knows where the player is. All that there is left is to check if the position of player is not the same as the position of the camera and if that's the case move the camera to the player position. We'll once again use the FixedUpdate method for the same reasons as before.

    private void FixedUpdate()
    {
        if (transform.position != target.position)
        {
            // We're going to use the position of the Z axis of the camera, since the camera has a value of -10 and the player
            // has 0. The camera needs the negative value so it can capture the game world and objects.
            Vector3 targetPosition = new Vector3(target.position.x, target.position.y, transform.position.z);
            transform.position = Vector3.Lerp(transform.position, targetPosition, 1.0f)
        }
    }

All that's left to do is to apply the CameraMovement script to the camera, and you can now see that the camera follows the player.

Collisions and edge of map

As you've probably noticed in your testing playthroughs, you can now walk outside of the drawn game world. To fix that, we will draw the collision tilemap around the edges. We've already created a Collision Tile map and added a Tile map Collider 2D. So now, instead of drawing on the Ground Tile map, select the Collision Tile map and use the forest sprites to draw the borders. At this point, we should address one more thing. Select the player and in the Rigidbody2D component, go to Constraints and check the box for "Freeze Rotation" for the Z-axis. If you want, you can leave it unchecked and see the results.

Now we want to limit the camera so it doesn't show the game world outside of the drawn world since it looks funny that we can see beyond the playable area. To do that, we're going to add a couple of things to our CameraMovement script. We're going to define two Vector2 variables, one for a maximum position and one for a minimum position. Now we can use a Vector2 since we're only using the X and Y-axis.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMovement : MonoBehaviour
{
    private Transform target;
    [SerializeField]
    private Vector2 maxPosition;
    [SerializeField]
    private Vector2 maxPosition;

Then in the FixedUpdate method, we want to limit the target Position variable we've set before with the new variables, and we'll do that by using "Mathf.Clamp" function.

private void FixedUpdate()
    {
        if (transform.position != target.position)
        {
            // We're going to use the position of the Z axis of the camera, since the camera has a value of -10 and the player
            // has 0. The camera needs the negative value so it can capture the game world and objects.
            Vector3 targetPosition = new Vector3(target.position.x, target.position.y, transform.position.z);
            targetPosition.x = Mathf.Clamp(targetPosition.x, minPosition.x, maxPosition.x);
            targetPosition.y = Mathf.Clamp(targetPosition.y, minPosition.y, maxPosition.y);

            transform.position = Vector3.Lerp(transform.position, targetPosition, 1.0f)
        }
    }

Now select the Main Camera game object and set the correct minPosition and maxPosition values for the camera. You can do that by selecting the camera, moving it around to the edges, and using the values you see in the camera's transform.

Player animation and camera settings

How to move the camera and how to create animations is a process you'll best see in a video. This post is long enough, and putting the process to create animation into words would be too much. I suggest you check out my tutorial on Youtube so you can see how to do these things.

If you enjoyed and maybe learned something reading this, follow me on Twitter, so you'll be notified when the next post is uploaded.

You have probably noticed, this tutorial was taken from my stream on Twitch where you can come and join me. We have a lot of fun creating games and learning more about game development. And if you're interested in game development, check out my YouTube channel where you can find more videos about game development.

Final code

PlayerMovement script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    [SerializeField]
    private float speed;

    private Rigidbody2D playerRigidBody;
    private Vector3 moveDirection;

    private void Awake()
    {
        playerRigidBody = GetComponent>Rigidbody2D<();
    }

    private void FixedUpdate()
    {
        playerRigidBody.velocity = new Vector3(moveDirection.x * speed, moveDirection.y * speed);
    }

    // Update is called once per frame
    void Update()
    {
        float moveX = Input.GetAxisRaw("Horizontal");
        float moveY = Input.GetAxisRaw("Vertical");

        moveDirection = new Vector3(moveX, moveY, 0f).normalized;
    }
}

CameraMovement script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMovement : MonoBehaviour
{
    private Transform target;
    [SerializeField]
    private Vector2 maxPosition;
    [SerializeField]
    private Vector2 maxPosition;

    private void Awake()
    {
        target = GameObject.FindGameObjectWithTag("Player").transform;
    }

    private void FixedUpdate()
    {
        if (transform.position != target.position)
        {
            // We're going to use the position of the Z axis of the camera, since the camera has a value of -10 and the player
            // has 0. The camera needs the negative value so it can capture the game world and objects.
            Vector3 targetPosition = new Vector3(target.position.x, target.position.y, transform.position.z);
            targetPosition.x = Mathf.Clamp(targetPosition.x, minPosition.x, maxPosition.x);
            targetPosition.y = Mathf.Clamp(targetPosition.y, minPosition.y, maxPosition.y);

            transform.position = Vector3.Lerp(transform.position, targetPosition, 1.0f)
        }
    }
}