itch.io is community of indie game creators and players

Devlogs

HowTo: Creating Smooth 8-Directional Movement for a 3D Player in Unity

Not so Trash Panda
A downloadable game for Windows

Hey everyone! Its Shelby from the Submersion development team and I'm here to talk about a major process in our development cycle for our upcoming game, and that process is Player Movement!

I believe that player interaction is the most critical element of any game and player movement is a major part of how the user will interact with said game. That being said it's really easy to get right but it is also really easy to mess up! I'm here to help you all not mess it up!

So for starters, what is 8-Directional movement?

8-Directional movement as the name suggests is movement in 8 different directions. It's best to think of the movement directions as points on a compass.

A breakdown of the steps

However you decide to use the 8-Directional movement is up to you, for our game, we're using an orthographic camera angle that looks at the player from above, but this movement can be used on a perspective type character as well, and it can be convenient for a third person game. I'll also show you some additional polishing changes you may or may not wish to implement.

The Player Movement

Creating Player Inputs

Creating the Player

  • "PlayerPrefab" is an empty game object that I use to store both my player and the camera. Weird physics related issues occur when you simply make the camera a child of the player so this is a nice way to drag both into a scene already setup, without having to create and setup each time.
  • "Main Camera" is as the name suggests, a regular unity camera. Your scene should come default with one in it so you can just drag this into the empty game object so it becomes a child.
  • "CinemachineCamera" is a special camera object used for smoothing out the player's camera movement, you can ignore this unless you desire to have a more smooth camera that follows the player. Other wise you can just setup a simple code the make the regular unity camera follow the x and z of the player's position. I don't have too much knowledge on Cinemachine so I would recommend doing some outside research if you want to use this for your game.
  • "Player" is where I store all of my player scripts, collisions, rigidbodies, and any other components I might need.
  • Inside the Player Input component, drag your Action asset that we had previously created into the empty space next to "Actions", then set your "Default Map" to "Player" because that's what we named our Action Map.

    This should be all of the setup we need, so now we can actually start programming. Go ahead and open up the PlayerMovement.cs script you just created.

    Detecting Player Inputs

    There are a few quick changes we need to make the the script to set us up for success. For starters, at the top of the script where your libraries are, add the input system library. The formal code for this is:

    using UnityEngine.InputSystem;

    Now you could technically just call the moveAction.ReadValue<Vector2>(); code every time you need to get inputs, but this way we can just call the method GetPlayerInputs(); and it will look a bit cleaner. 

    Congratulations! You are now able to detect user inputs! If you want to test this out, you can Debug.Log() your Vector2 function in FixedUpdate() and then enter play mode in the editor and just press your keybinds to test it. You'll know you did it correctly if the console is returning something like (1, 0) or (0, 1), and so on. You can also test diagonal vectors by holding down a vertical input like W or S and a horizontal input like A or D at the same time, and you'll see a vector that looks like (1, 1) or (-1, 1), and so on. Next we are going to use these inputs to move our player.

    Moving the Player based on an Input

    Just to go into further detail on what some of these extra phrases and words are, SerializeField allows the user to view and change the value of the variable from the Unity editor. This is especially useful when you have multiple of the same component and want to drag and drop a specific one onto that variable from the editor such as two separate colliders. A Header is used for purely organizational purposes for viewing in the editor. It adds a header above the variables to allow the user of the game object to better separate different variables in that script. Note that in order to use the Header your variable either has to be public or has to contain the SerializeField attribute.

    Now that we have those variables made, we need to create a method that will be called from FixedUpdate() for actually moving the player. We are going to create a void type method with a Vector2 parameter. Your declaration formatting should look something like this:

    private void MovePlayer(Vector2 _direction) 
    { 
    }

    Since we now know the direction is not null, we can use it to move the player. we are going to use C#'s += functionality and translate the position of our player by doing transform.position += (how we implement it). Now our input is a Vector2 but our player moves on a Vector3 space, because we are using Unity 3D. We are going to create a new Vector3 that uses the x axis of our _direction variable for the x of the Vector3, and we are going to use the y axis of our _direction variable for z of the Vector3. Our y variable for the Vector3 can be set to zero as we do not want the movement of our player to also go up and down. We also need to then multiply this entire Vector3 by Time.deltaTime, so that it updates over time and not immediately. Now at this point you can stop coding if you really want to, however the player will be really slow. That is where our movement speed variables come in. Between the Vector3 and the Time.deltaTime, multiply by your movement speed. Your code should now look like this:

    transform.position += new Vector3(_direction.x, 0, _direction.y) * walkSpeed * Time.deltaTime;

    The last step you need to do is call the MovePlayer() method from your FixedUpdate() method. Because our method has a Vector2 parameter required for it to be called, we are going to call the previously created GetPlayerInputs() method inside of the MovePlayer() parentheses. This works because all the GetPlayerInputs() method does is return a Vector2, so it doesn't do any fancy background work that might conflict with our MovePlayer() method. Inside of FixedUpdate(), the call should look like this:

    MovePlayer(GetPlayerInputs());

    Congratulations! Now if you enter play mode from the editor you can see that using your movement keybinds will move your player around the world space. We can polish this to make it look much better by having the player model rotate to face the direction in which it is moving, and that is exactly what we are going to do next.

    Rotating the Player to face its Movement Direction

    We are going to start off by declaring another global variable. We need a declaration for our player model. Simply declare a GameObject type variable and name it something like "playerModel". If you want to make it easier on yourself you can give this variable the SerializeField attribute and just drag and drop the model onto it from the editor, but I am going to show you how to just grab it via code as well.

    private GameObject playerModel;

    Assuming you did not give it the SerializeField attribute, here is how you can define the game object. Game objects contain a component called a transform, which is used to update the position, rotation, and scale of the object. However, the Transform type also contains a child method called GetChild(). We can use this method to declare our game object as the game object at the desired index we insert into the function. Because our player only has one child object, which is the player model, the index will be 0. from there we will need to ensure we are getting the game object from that method. We can call all of this from our Start() method, and it should look like this:

    playerModel = gameObject.transform.GetChild(0).gameObject;

    Now we are going to declare another method for rotating our player, the format will be the exact same as our MovePlayer() function but instead we'll call it RotatePlayer(), and we will need that same parameter.

    private void RotatePlayer(Vector2 _direction) 
    { 
    }

    Again, I would strongly recommend making sure the _direction variable is a non null variable before using it. After you have done that, the code is really simple. As stated before, the Transform type allows us to access the position, rotation, and scale of the game object, so for this instance, we are going to use rotation. We are going to access the rotation of our game object by declaring it as playerModel.transform.rotation. then we are going to change the rotation using Unity's Quaternion type. The Quaternion type has a TON of child methods that are all really useful for all sorts of different rotational events. For our case, we are going to use the Quaternion.LookRotation() method to get our game object to look at the angle inside the parameter. We are going to be declaring a new Vector3 as the parameter for this method with our look direction being our _direction x value as our x value for the Vector3 and our _direction y value as the z value for the Vector3. Like for our movement we are going to leave the y value of the Vector3 as zero. Your RotatePlayer() method should look like this: 

    private void RotatePlayer(Vector2 _direction)
    {
        if (_direction != null)
        {
            playerModel.transform.rotation = Quaternion.LookRotation(new Vector3(_direction.x, 0, _direction.y);
        }
    }

    Now all you need to do is call this in the same way you called the MovePlayer() method inside of FixedUpdate(). You will need to use the same GetPlayerInputs() method as the parameter for your call. Your FixedUpdate() method should now look like this:

    void FixedUpdate()  
    {   
        MovePlayer(GetPlayerInputs());
        RotatePlayer(GetPlayerInputs());
    }

    At this point you can consider yourself done. There are two changes we can make to improve upon this. The first one is smoothing out the rotation a bit more since it looks really static right now.

    Smoothing the Player Rotation

    Now that we have that set up, locate your RotatePlayer() method. We need to declare a local angle variable inside of this method that uses the float type. We are going to make it so that the value of this angle variable is dependent on the Mathf.SmoothDampAngle() method.

    float angle = Mathf.SmoothDampAngle();
    

    Now we need to fill in the parameters for the Mathf.SmoothDampAngle() method. The first one is our current rotation. Because we are going to be rotating around the Y axis, we need to get the y of our player model's rotation. This can be done using Transform.eulerAngles. Which returns the angles of that transform in degrees. So we are going to use playerModel.transform.eulerAngles.y as our current rotation.

    float angle = Mathf.SmoothDampAngle(playerModel.transform.eulerAngles.y);
    

    The next parameter is the desired rotation. This requires a little more math as we do not know the desired rotation since it depends on which way the player is moving. Fortunately, the same Mathf struct has a lot of math related methods. We are going to be using two different Mathf methods to find our desired angle, Mathf.Rad2Deg and Mathf.Atan2Mathf.Rad2Deg converts a value in radians to a value in degrees. Because we are working with world space, we need the value in degrees. Mathf.Atan2 is an arctangent function that takes in a y and x value and returns an angle in radians (hence why we need to convert it to degrees using Mathf.Rad2Deg). Here is where it gets weird. Mathf.Atan2 takes in two parameters, one for a y float and one for an x float, and then it does the arctangent function as y/x, so you would think you would use _direction.y for the y float and  _direction.x for the x float, but if you do that and test it, it actually flips the facing direction, making it so that you face the opposite direction of where you are moving. So naturally we just flip the two _direction values and it works perfectly fine. You also need to multiply this Mathf.Atan2 by Mathf.Rad2Deg. The order isn't strictly necessary so you can multiply either by either and it works regardless.

    float angle = Mathf.SmoothDampAngle(playerModel.transform.eulerAngles.y, Mathf.Rad2Deg * Mathf.Atan2(_direction.x, _direction.y));

    The next parameter is the reference to a velocity. If you recall we said we did not want velocity to affect our rate of rotation, so we will just call our previously created arbitrary float value for this.

    float angle = Mathf.SmoothDampAngle(playerModel.transform.eulerAngles.y, Mathf.Rad2Deg * Mathf.Atan2(_direction.x, _direction.y), ref r);

    There is just one more thing we can do to improve upon this and that is making it so that upon release of the keybind, your player will continue to face the direction they were facing while moving. If you enter play mode now and move around then release, you'll find that the player returns to their original facing direction when no inputs are provided. This might not look bad to you which is totally fine but just in case I'll show you how to fix it!

    Keeping the Player facing their Move Direction

    You will now find that if you enter play mode and walk around and then release your input keybind, your player will continue to face the direction it was just facing.

    Congratulations! You have successfully implemented 8-Directional movement with smooth dynamic rotations and gravity! That wasn't so hard was it?

    If you have any questions for me relating to how I created this or any questions for the Submersion development team, feel free to ask them in the comments! I look forward to showing y'all our game when it releases and I hope you guys stick along for the ride!

    Download Not so Trash Panda
    Leave a comment