This post is part of game designing guide, I recommend reading everything if you didn’t.

In the last article we imported character animations to Unity and created Animator transitions, it was our first interaction with Unity.

And now we will create a foundation of our game. We will write our first code that will:

  • Move our character.
  • Play animations when moving.
  • Rotate our character when turning.

According to pressed buttons.

But don’t worry if you’re just a beginner, as we will go also go through:

  • Default Unity code.
  • Unity basics.
  • C# basics.

There’s no way you will pick up coding after just one article, but it will work as solid start for you. So make sure to seek information and learn on your own in the meantime.

Free Unity Scripts

If you just want the scripts, and don’t actually want to learn, then this section was made specifically for you.

However…

This article is dedicated to explaining every line of our code.

And I strongly recommend you to read everything, otherwise you’ll never learn.

Character Movement:

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

public class CharacterMovement : MonoBehaviour
{
    public Camera camera;
    float speed = 3.0f;
    float jumpSpeed = 6.0f;
    float gravity = 20.0f;

    CharacterController characterController;
    Animator animator;
    Coroutine jump;

    Vector3 movement = Vector3.zero;
    Vector3 input = Vector3.zero;
    Vector2 adjustedInput = Vector2.zero;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        animator = gameObject.GetComponent<Animator>();
    }

    void Update()
    {
        if (characterController.isGrounded)
        {
            // Playing Running Animation
            animator.SetBool("Running", (Input.GetKey(KeyCode.RightArrow) ^ Input.GetKey(KeyCode.LeftArrow)) | (Input.GetKey(KeyCode.UpArrow) ^ Input.GetKey(KeyCode.DownArrow)));

            // Getting Input For Moving Horizontally
            input.x = (InputValue(KeyCode.RightArrow) - InputValue(KeyCode.LeftArrow));
            // Getting Input For Moving Vertically
            input.z = (InputValue(KeyCode.UpArrow) - InputValue(KeyCode.DownArrow));

            // Normalizing Input To Have Normal Diagonal Speed & Speeding
            adjustedInput = new Vector2(input.x, input.z).normalized * speed;
            movement.x = adjustedInput.x;
            movement.z = adjustedInput.y;

            // Starting Jump Coroutine
            if (Input.GetButton("Jump") && jump == null)
            {
                jump = StartCoroutine(Jump());
            }
        }
        else
        {
            movement.y -= gravity * Time.deltaTime;
        }

        // Finally Moving Character Depending On Previously Adjusted Input & Camera
        characterController.Move(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (movement * Time.deltaTime));
    }

    IEnumerator Jump()
    {
        // Starting Animation And Waiting Till Legs Bend
        animator.SetBool("Jumping", true);
        yield return new WaitForSeconds(0.25f);

        // Performing Jump And Waiting Till Animation Finishes
        movement.y = jumpSpeed;
        yield return new WaitForSeconds(0.75f);

        // Jump Finished, Changing Values Back So We Can Jump Again
        jump = null;
        animator.SetBool("Jumping", false);
    }

    float InputValue(KeyCode key)
    {
        if (Input.GetKey(key))
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }
}

It controls when to change position and play animation.

Character Rotation:

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

public class CharacterRotation : MonoBehaviour
{
    public Camera camera;

    Vector3 input = Vector3.zero;
    Quaternion influencedByCamera;

    void Update()
    {
        if (Input.GetAxis("Horizontal") >= 0.2 | Input.GetAxis("Horizontal") <= -0.2 | Input.GetAxis("Vertical") >= 0.2 | Input.GetAxis("Vertical") <= -0.2)
        {
            input = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));

            influencedByCamera = Quaternion.LookRotation(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (input * Time.deltaTime));

            transform.rotation = Quaternion.Slerp(transform.rotation, influencedByCamera, 0.1F);
        }
    }
}

Rotates the character slowly when we turn.

As you see, one script does one thing, and their names are self-explanatory. That’s very smart approach that will keep our project clean.

First Look At Coding

Time to write our first C# code. Don’t worry if you don’t understand it at all. I will try to explain every line in a way that even non-tech people can understand it.

Creating C# File

I told you before to make two folders. Now, you can enter the one called Scripts and create a new C# file:

But there’s actually better way to do this.

You see, when you create script this way, its not assigned to our object.

That’s why I don’t like to do it this way.

Better way is to select our Character in the Scene:

Or in Hierarchy.

Now press Add Component in Inspector on the right:

Select New Script and name it CharacterMovement.

It’s great to make every script does just one thing, it keeps the whole project very clean and pleasure to work with. And to make their names self-explanatory so we always know what’s doing what.

Default Unity Script Code

This is how the the default script looks:

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

public class CharacterMovement : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

I will break it down so its easier to understand.

But understand one thing, this guide is already big and there’s no way to include content as huge as C# book in it.

I will cover everything but if you need a deeper understanding of any line then you must find information in Google on your own.

Namespaces

Our first three lines:

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

These lines contain keyword using and then name of namespace.

First word using means literally import.

Second word (or rather combination of words) contains a namespace name that we import into our script (and just this one script). You can see there are dots between names. Every dot in this case means one layer deeper.

When we import a namespace (also called a package or library), we are doing it because we want to use predefined variables, methods and classes that someone was kind enough to write for us and include in a namespace.

This saves us lot of time because we otherwise would have to write it on our own, but also because these packages are often really hard to code so they’re essentially doing the hard work for us.

If we import Unity’s namespace and start using its code, it means we are using Unity’s API.

So what do you need these packages for?

For example you need UnityEngine namespace to use default Unity’s class MonoBehaviour.

Objects

Everything in Unity is an object.

Camera is an object.

Your character is an object.

Even the light is an object.

And every one of them can have a component attached. Component can also be a custom script – this is what we are coding now.

Classes

The next line contains a class:

public class PlayerMovement : MonoBehaviour

First three words create a class called exactly that. Class is used to give functionality to the objects we spawn.

For example we have an object Main Camera that has some default components (therefore scripts with code in it) and functionality added to it by Unity:

This is the equivalent to a script that we code on our own. It has variables that we can adjust to make the camera work in a way we want.

And we can code infinite amount of scripts and assign them to infinite amount of objects.

Every of our scripts will have a different class specified in it. And whatever code, methods and variables we write it in – this code will be ran by every object that had this script assigned to it.

So if you create a script with class called SpeedUpNOWWW and add a code for exactly that, then add the script as component to cars, animals, and even enemies, then they all will get a speed up boost accordingly to the code we made.

After the class name, theres a keyword MonoBehaviour after a colon. It means this is a parent of our class and we get some of its funtionality.

When you create a class, you always open squarebrackets “{“, and close them “}“, they will contain our code.

Access Level

First keyword defines access level, it can be either public, private, or protected. And they all work like computer firewall that blocks incoming connections.

I recommend using public at first everywhere, because this way we will be able to access our code from as many places as possible.

public class PlayerMovement : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

So this is how our class looks like.

In summary, you first specify access level, just make it public.

then you write keyword class.

Then the class name, must be equivalent to script file name.

Then you specify class parent name after a colon. That’s optional.

Unity does all this by default for us whenever you create a new script so you don’t have to worry.

Inside the class you will put variables and methods.

Methods

Inside you have two methods called Start() and Update(). You always add brackets () after method name so C# understand that its a method. You also add squarebrackets {} after to methods, and put methods’s code in them.

These methods are predefined by Unity. Start() is executed once at the start, so if we want to set our hair color to black before a player can see the character, then we will do it inside this method, because this specific code only needs to be executed once, and instantly.

In Update() you put code that needs to be constantly executed. For example you can assign camera’s position to player in this place, because player position will change a lot.

So its obvious that we also need to be constantly changing camera’s position, otherwise camera will stay in one place, and player will move, and we will lose him from our sight.

Keyword void before method’s name means that when we execute this method, it will not return the result of its calculations to the place where we called the method from.

Returning Value

If we put int instead of void then it will return a value of type integer so for example we create a code that adds 5 to our health that we will use when we drink health potion.

Such code will contain a variable with current player’s health, but it will also call a method to calculate how much it will be when we add 5 to our health. This code will also return the value.

By returning the value, I mean we will send the value first, and then obtain the calculated result at the place where we called our method.

So inside the code for drinking health potions.

Without returning it, we wouldn’t have access to calculated value. It would get lost.

The value will be of type integer because its a number and not a sentence for example.

The reason why these two methods made by Unity don’t return anything, is because they’re already being called by the engine and setting them to return a value will either result with error or not work correctly.

And also because their purpose is to execute our code, and not return anything. There are many predefined by Unity methods like these that are all designed for different things.

For now you don’t have to worry about returning value, because we aren’t doing it in this lesson. But I needed to explain why you add keyword void before your methods – because they’re just doing the work and are not sending the result of that work anywhere.

Moving Character

This is our movement code.

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

public class CharacterMovement : MonoBehaviour
{
    public Camera camera;
    float speed = 3.0f;
    float jumpSpeed = 6.0f;
    float gravity = 20.0f;

    CharacterController characterController;
    Animator animator;
    Coroutine jump;

    Vector3 movement = Vector3.zero;
    Vector3 input = Vector3.zero;
    Vector2 adjustedInput = Vector2.zero;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        animator = gameObject.GetComponent<Animator>();
    }

    void Update()
    {
        if (characterController.isGrounded)
        {
            // Playing Running Animation
            animator.SetBool("Running", (Input.GetKey(KeyCode.RightArrow) ^ Input.GetKey(KeyCode.LeftArrow)) | (Input.GetKey(KeyCode.UpArrow) ^ Input.GetKey(KeyCode.DownArrow)));

            // Getting Input For Moving Horizontally
            input.x = (InputValue(KeyCode.RightArrow) - InputValue(KeyCode.LeftArrow));
            // Getting Input For Moving Vertically
            input.z = (InputValue(KeyCode.UpArrow) - InputValue(KeyCode.DownArrow));

            // Normalizing Input To Have Normal Diagonal Speed & Speeding
            adjustedInput = new Vector2(input.x, input.z).normalized * speed;
            movement.x = adjustedInput.x;
            movement.z = adjustedInput.y;

            // Starting Jump Coroutine
            if (Input.GetButton("Jump") && jump == null)
            {
                jump = StartCoroutine(Jump());
            }
        }
        else
        {
            movement.y -= gravity * Time.deltaTime;
        }

        // Finally Moving Character Depending On Previously Adjusted Input & Camera
        characterController.Move(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (movement * Time.deltaTime));
    }

    IEnumerator Jump()
    {
        // Starting Animation And Waiting Till Legs Bend
        animator.SetBool("Jumping", true);
        yield return new WaitForSeconds(0.25f);

        // Performing Jump And Waiting Till Animation Finishes
        movement.y = jumpSpeed;
        yield return new WaitForSeconds(0.75f);

        // Jump Finished, Changing Values Back So We Can Jump Again
        jump = null;
        animator.SetBool("Jumping", false);
    }

    float InputValue(KeyCode key)
    {
        if (Input.GetKey(key))
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }
}

Comments pretty much say what is happening.

I’ll go through the lines that I didn’t explain yet.

Variables

Variables first.

CharacterController characterController;
Animator animator;

Here, you create variable called characterController of class CharacterController.

First words CharacterController and Animator (starting from big letter) are declaring variables classes (variable can be of either class or data type).

Second words starting from small letter are the names of variables that you just created. You can name it however you want, for example dog or variableOfClassCharacterController but these are bad choices.

There are simple naming rules that you should follow when coding so check it later.

Common practice that I also prefer is to give them their original name while making the first letter lowercase. You can’t make first letter uppercase in this case because this name is already reserved for CharacterController.

In C# letter case matters.

float speed = 2.0f;
float jumpSpeed = 10.0f;
float gravity = 20.0f;

These are variables of type float. Variable types are called data and you can also use them with methods.

Most used data types are integer, bool, float, double but there are more. There’s a very popular class called string that we use in same way that we use data types, but its not a data type, we can however, use it in the same manner.

For example:

string welcomeSentence = "Welcome on Gamedev Success!";
int visitorAge = 25;
float visitorFrustrationLevel = 1.3f;
double visitorHappinessLevel = 2.5;

Int is a whole number, boolean holds either true or false, float and double are for storing numbers with decimal points but double is more precise. String is for variables that contain text.

Vector3 is used to pass 3D directions and positions.

So what is next?

If Statements

Next is an if statement that checks if a condition is either true or false then performs code accordingly.

void Update()
{
    if (characterController.isGrounded)
    {
        // Playing Running Animation
        animator.SetBool("Running", (Input.GetKey(KeyCode.RightArrow) ^ Input.GetKey(KeyCode.LeftArrow)) | (Input.GetKey(KeyCode.UpArrow) ^ Input.GetKey(KeyCode.DownArrow)));

        // Getting Input For Moving Horizontally
        input.x = (InputValue(KeyCode.RightArrow) - InputValue(KeyCode.LeftArrow));
        // Getting Input For Moving Vertically
        input.z = (InputValue(KeyCode.UpArrow) - InputValue(KeyCode.DownArrow));

        // Normalizing Input To Have Normal Diagonal Speed & Speeding
        adjustedInput = new Vector2(input.x, input.z).normalized * speed;
        movement.x = adjustedInput.x;
        movement.z = adjustedInput.y;

        // Starting Jump Coroutine
        if (Input.GetButton("Jump") && jump == null)
        {
            jump = StartCoroutine(Jump());
        }
    }
    else
    {
        movement.y -= gravity * Time.deltaTime;
    }

    // Finally Moving Character Depending On Previously Adjusted Input & Camera
    characterController.Move(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (movement * Time.deltaTime));
}

About if statements you can read here.

if(characterController.isGrounded)

This checks if predefined Unity boolean variable isGrounded returns true or false. Then it performs code inside squarebrackets if it returned true.

We are checking it because you don’t want to be able to jump while you’re already in the air.

Component References

Next:

characterController = GetComponent<CharacterController>();
animator = gameObject.GetComponent<Animator>();

This gets you a reference to the component.

Without it, we can’t interact with the component that we added to our object.

In this case without it we can’t write code that will make use of Animator and CharacterController components that we added to our Character.

This is placed in Start() because we need to get the reference only once.

Input Variables & Animator Parameters

Next:

// Playing Running Animation
animator.SetBool("Running", (Input.GetKey(KeyCode.RightArrow) ^ Input.GetKey(KeyCode.LeftArrow)) | (Input.GetKey(KeyCode.UpArrow) ^ Input.GetKey(KeyCode.DownArrow)));

// Getting Input For Moving Horizontally
input.x = (InputValue(KeyCode.RightArrow) - InputValue(KeyCode.LeftArrow));
// Getting Input For Moving Vertically
input.z = (InputValue(KeyCode.UpArrow) - InputValue(KeyCode.DownArrow));

First line uses function SetBool of our animator variable (that holds reference to Animator component).

It plays animation depending on the first parameter. First parameter is the same one that you’ve previously set in Animator window.

Second parameter (after coma) is boolean condition. In our later code parts we created a function that returns 1 (that is equal to true) when any keyboard button is pressed, and returns 0 (that is equal to false) when no button is pressed.

And now we are calling that function. It returns the result which is either true or false so we can use it as our boolean condition.

^, also called XOR, is operator that will return true if only one of values is true. If both are true or none are true, then it returns false.

After that we again use this function to know what keys are pressed. Previously it was for animation but now it will be for moving. We are storing result  (which is either 1 or 0) in our Vector3 input.

Vector3 & Vector2

Vector3 has values for x, y and z and we can manually assign each of them.

How Vector3 looks like:

Vector3 exampleVector3 = new Vector3(20, 100, 900);

And in our case, the first number is a method. The second number is 0, and third number is again a method.

Normalizing Input

That:

// Normalizing Input To Have Normal Diagonal Speed & Speeding
adjustedInput = new Vector2(input.x, input.z).normalized * speed;
movement.x = adjustedInput.x;
movement.z = adjustedInput.y;

Normalizes the input. By default, we could move twice as fast if we press up and left arrow keys.

That’s obviously not what we want so we added normalized at the end of our Vector2 after dot. We will also multiply it by speed because we want to control how quickly our character moves.

This was done in Vector2 instead of Vector3 because the variable in the middle is y axis that is for jumping. We don’t want to slow down or speed up our jumping if jump was done in the same time as running was, so we normalized Vector2 instead.

IF Conditions

// Starting Jump Coroutine
if (Input.GetButton("Jump") && jump == null)
{
    jump = StartCoroutine(Jump());
}

That just checks if we are pressing button set for jumping in project settings and if Coroutine is currently playing.

Coroutine is function that contains code executed after certain time. If we want to execute our code after a second or two, we need to use Coroutine.

Finally we make a call to Character Controller method Move:

// Finally Moving Character Depending On Previously Adjusted Input & Camera
characterController.Move(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (movement * Time.deltaTime));

Second parameter is our (normalized and adjusted by speed) movement multiplied by Time.deltaTime. The reason for this is to make the movement not dependant on our frame rate (smaller on slow computers, bigger on faster).

By multiplying by Time.deltaTime we are setting a global movement speed that will work the same on every of your players computer and regardless whether they are running milion things and slowing computer or nothing at all.

IEnumerator / Coroutine Method

Our Coroutine method:

IEnumerator Jump()
{
    // Starting Animation And Waiting Till Legs Bend
    animator.SetBool("Jumping", true);
    yield return new WaitForSeconds(0.25f);

    // Performing Jump And Waiting Till Animation Finishes
    movement.y = jumpSpeed;
    yield return new WaitForSeconds(0.75f);

    // Jump Finished, Changing Values Back So We Can Jump Again
    jump = null;
    animator.SetBool("Jumping", false);
}

As you can see it’s unnatural if we jump immedietely without bending our legs.

So we have to start animation, wait for the legs to bend, then make our character jump and therefore change his position, then again wait till he finished jumping.

After that we are setting jump to null and Animator parameter to false so we can jump again.

Input Method

This is our input method I talked about previously:

float InputValue(KeyCode key)
{
    if (Input.GetKey(key))
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

First it creates a method called InputValue of type float. And it uses one parameter of type KeyCode. So whenever we call this function we have to specify which button we are checking, and its KeyCode name.

Then it checks if its pressed or not. Returns 1 if true, and 0 if false.

Rotating Model

Create script CharacterRotation in same way.

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

public class CharacterRotation : MonoBehaviour
{
    public Camera camera;

    Vector3 input = Vector3.zero;
    Quaternion influencedByCamera;

    void Update()
    {
        if (Input.GetAxis("Horizontal") >= 0.2 | Input.GetAxis("Horizontal") <= -0.2 | Input.GetAxis("Vertical") >= 0.2 | Input.GetAxis("Vertical") <= -0.2)
        {
            input = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));

            influencedByCamera = Quaternion.LookRotation(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (input * Time.deltaTime));

            transform.rotation = Quaternion.Slerp(transform.rotation, influencedByCamera, 0.1F);
        }
    }
}

So let’s quickly cover what this script does.

public Camera camera;

Vector3 input = Vector3.zero;
Quaternion influencedByCamera;

This just declares variables so we can later assign value to them every frame. You can also declare them in Update() but then they will be declared every frame and its not needed.

void Update()

This gets called every frame.

if (Input.GetAxis("Horizontal") >= 0.2 | Input.GetAxis("Horizontal") <= -0.2 | Input.GetAxis("Vertical") >= 0.2 | Input.GetAxis("Vertical") <= -0.2)

This checks input.

Whenever the GetAxis returns value other than 0, it means arrow key is pressed. It can return values ranging from -1 to 1.

The reason why we are not checking if value is bigger than 0, but bigger than 0.2 instead, is because we don’t want our character to rotate if we just barely push the button.

input = new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical"));

Storing our input.

Next:

influencedByCamera = Quaternion.LookRotation(Quaternion.Euler(0, camera.transform.rotation.eulerAngles.y, 0) * (input * Time.deltaTime));

This alters our input so when we press up arrow, it will always go forward according to the camera. If you press left, it will go left according to the camera, etc.

Without that, arrow keys would be absolute and depending on how we rotate camera, character could actually go back when you press forward, or left when you press right.

transform.rotation = Quaternion.Slerp(transform.rotation, influencedByCamera, 0.1F);

Quaternion.Slerp adds a smooth transition between current rotation and desired rotation so the turning is not instant.

We made our very basic character. But as you move, camera is not following.

In our next lesson we will be adding a neat orbiting and rotating third person camera script and explaining how does it work.

References:

1 Comment

  1. 2 things:
    1. shouldn’t the gravity be applied even when character is grounded?
    2. why not make the variables public so we can change them in inspector?

    apart from that great tutorial thx

Post A Comment