Action-Adventure Player Controls

Introduction

The right player controls are a key-element for your game. If the controls are bad your game cannot be good either. In this blog post, I will introduce the player controls to you we used in our action-adventure game Kordex. This blog is the part of a bigger tutorial series concerning Player Controls, Camera Controls and Camera Collision. In this first part of the series we will cover Action-Adventure like Player Controls.

Download Files

Player Controls Concept

The concept we present here is derived from previous action-adventure games like “The Legend of Zelda: The Wind Waker” or “Super Mario 64”.

The basic idea is that if the player moves forward (The “W” key in a WASD controls setup) he always runs away from the camera. So the forward vector of the player is the vector from the camera to the player. The other three directions (left, right and back) are defined according to the forward direction.

Player Controller

Movement

The forward direction of the player is always facing away from the camera. Consequently when moving right the player circles the camera clockwise.

Player Controls - Player Movement

Grounding

In order to place the player on the ground we shoot a ray downwards (negative y) from the center of the player and check if it hits the ground. If the ray does not hit or the distance between the hit and the center of the player is greater than half the height of the character, the player is mid-air and a gravity vector is applied to the character’s velocity. Otherwise the player is counted as grounded and the velocity in y-direction is set to zero. Additionally, to prevent the player going to mid-air mode even if he just falls from a little step or walks down a slope we additionally add a little epsilon to the ray. In this region the player is theoretically not touching the ground but counted as grounded.

Player Controls - Player Raycast

Player Controls Implementation

In this section we will show you how to implement the concepts explained above into your own Unity project. You can also download the project using the download button above.

First of all, start a new Unity project and setup a basic scene with a level where the player should walk around later.

Player Controls - Setup

The next step is to import the Unity standard package “Characters”. To do so, go to Assets > Import Package > Characters. On the opening window click Import.

After importing drag the character model “Ethan” from the imported package into the Hierarchy and rename it to “Player”.

Player Controls - Player Ethan

Add a Rigidbody and a Capsule Collider component to the player. Afterwards in the Rigidbody settings, untick Use Gravity and Freeze Rotation in all three axes. Adjust the Capsule Collider size to match the player body but leave some space at the bottom so the collider is not touching the floor (also not when standing on a slope) so it does not interfere with our PlayerController-raycasting later on.

Player Controls - Rigid Body + Collider

Animator Controller

Before we begin to implement the player, we setup a basic animator controller in order to get a better feeling over the controls of the player. First of all create a new animator controller by choosing Assets > Create > Animator Controller and name it “PlayerAnimatorController”. In order to assign this new animator controller to our character just drag and drop the “PlayerAnimatorController” on the “Player” in the Hierarchy. If you double-click on the “PlayerAnimatorController” now, the animator window should pop up. Here you can create animation states for each animation our player should perform.

Animator States

This tutorial just covers the idle and the run animation provided from the Standard Assets. Drag both the “HumanoidIdle” and the “HumanoidRun” into the animator.

Player Controls - Animator Controller Start

Make sure that “HumanoidIdle” is the default state (marked as orange and “Entry” has a transition to “HumanoidIdle”). If not, right click on “HumanoidIdle” and click Set as Layer Default State. After that, rename “HumanoidIdle” to “Idle” and “HumanoidRun” to “Run” for simplification.

Animator Transisitons

Then right click on “Idle” and select Make Transition. Next, click on “Run” to make a transition from “Idle to “Run”. Select this newly created transition and uncheck Has Exit Time in the Inspector to enable instant transition between those two states. Additionally create a transition from “Run” to “Idle” with Has Exit Time unchecked.

Player Controls - Animator Transitions

The next step is to define when the transition between “Idle” and “Run” should occur. For this, we create a new parameter of the type Float in the Animator.

Player Controls - Animator Parameter

Name this one “MoveSpeed”. Afterwards select the transition from “Idle” to “Run” and add the condition “MoveSpeed Greater 0.1” in the Inspector.

Player Controls - Animator Controller Conditions 1

Then select the other transition and add the condition “MoveSpeed Less 0.1”

Player Controls - Animator Controller Conditions 2

That’s it so far for the Animator Controller. If you hit play now you should see the player doing its idle animation. Note that this is just very basic. Only the basic animations are setup here to get a general feeling of the player movement. Animations like a “Jump” can be added of course.

Player Controller

After completing the animation controller, it’s now time to implement the script responsible for player movement. Additionally we will also create a simple script for camera control. First of all, create two C# scripts “PlayerController” and “SimpleCameraController” and drag them onto the “Player” and the “Main Camera” respectively.

We will begin with writing the PlayerController. Open this script in your favorite editor and delete the Start() and the Update() method to get the following empty class:

using UnityEngine;

// Script for controlling the main character
public class PlayerController : MonoBehaviour
{

Attributes

Next we will add two nested classes MoveSettings and CharacterInput. MoveSettings contains different parameters concerning speed and falling which are configurable in the inspector later, while CharacterInput stores the keyboard and mouse input concerning the player:

// Class wrapping up settings concerning the character movement
[System.Serializable]
public class MoveSettings
{
    // How fast the player moves
    public float forwardVel = 4;
    // How fast the player turns
    public float rotateVel = 1000;
    // How high the player jumps
    public float jumpVel = 6;
        
    // The distance to the ground to count the player as fully grounded
    public float distToGround = 0.0f;
    // The minimal distance to the ground to count the player falling (distToGround + eps)
    public float minFallingDistToGround = 0.1f;
    // The offset from the transform position to cast the down ray from
    public Vector3 pivotOffset = new Vector3(0.0f, 0.5f, 0.0f);
    // The mask defines what objects are counted as ground
    public LayerMask ground;
}

// Class containing all input fields
class CharacterInput
{
    // The input in the forward direction
    public float forward = 0;
    // The input in the sideward direction
    public float sideward = 0;
    // The input for jumping
    public float jump = 0;
}

The first three attributes of the MoveSettings class define the respective velocities of the player. The distance to the ground defines how high the player should stand above the ground measured from the player’s pivot to the ground object. The minFallingDistToGround realizes the concept explained above so that the player does not always fall instantly when walking down slopes. The raycast in the downwards direction for ground detection will be cast from the 3D model pivot + the pivotOffset. The purpose for the offset is not to miss the ground if the player extends a bit into the 3D model geometry of the floor. The LayerMask ground allows you to specify what’s ground and what is not.

The CharacterInput class just stores the input values for forward-/sideward-walking and jumping.

Next we will define the attributes of our PlayerController:

// Instance of move settings
public MoveSettings moveSettings = new MoveSettings();

// Gravity acceleration
public float downAccel = 18.0f;

// Used for storing velocity before applying to the rigid body
Vector3 velocity = Vector3.zero;

// Store if the player is grounded
bool grounded = false;

// Stores the raycast hit to the ground
RaycastHit groundHit = new RaycastHit();

// Is set if the player is during a jump
bool onJump = false;

// Instance of character input
CharacterInput input = new CharacterInput();

// Reference to camera
Camera playerCamera;

// Rigidbody component
Rigidbody rigidBody = null;

// Reference for animator component
Animator animator = null;

We have an instance of MoveSettings and a down acceleration which defines how fast the player falls. Both can be configured in the Unity Inspector later. Furthermore there are several private fields. We will always store the velocity of the player which is applied each frame. Additionally, we will check every frame if the player is grounded and set the grounded attribute appropriately. The potential hit with the ground will be stored in groundHit. Another possible state of the player is jumping, for which the onJump attribute stands. The rest of the attributes are just references to the player components and the camera.

Methods

The Unity methods Start(), Update() and FixedUpdate() are defined as followed:

// Unity Methods
void Start()
{
    // Grab components
    playerCamera = Camera.main;
    rigidBody = GetComponent();
    animator = GetComponent();
}

void Update()
{
    // Get player input
    GetInput();
    // Process player turning
    Turn();
}

void FixedUpdate()
{
    // Check Grounded once per FixedUpdate()
    CheckGrounded();

    // Process player running and jumping
    Run();
    Jump();

    // Player rotation (on XZ-plane)
    Quaternion playerRotation = transform.rotation;

    // Adjust movement to move along the surface normal of the ground (reduces falling when walking on slopes)
    if (grounded)
    {
        playerRotation = Quaternion.FromToRotation(Vector3.up, groundHit.normal) * transform.rotation;
    }

    // Set velocity in player look direction (rotation)
    rigidBody.velocity = playerRotation * new Vector3(0.0f, 0.0f, velocity.z)
                        + new Vector3(0.0f, velocity.y, 0.0f);
}

In the Start() function, all the necessary components are collected. The Update() function checks for relevant key or mouse input and calls the Turn() function for player turning. For all physically based operations the FixedUpdate() is used. Because the CheckGrounded() method will use ray casting and the Run() and Jump() methods are working with velocities they are called here. Finally the forward velocity of the player is applied based on the player rotation. If the player is standing on the ground the move direction is additionally directed along the tangent of the surface. The gravity is always applied in the same direction regardless of the player rotation.

The GetInput() is:

// Gets control input
void GetInput()
{
    input.forward = Input.GetAxis("Vertical");
    input.sideward = Input.GetAxis("Horizontal");

    // Keep jump value at 1.0f till it gets recognized by the Jump() function
    if (input.jump == 0.0f)
        input.jump = Input.GetAxis("Jump");
}

The predefined axis "Vertical" and "Horizontal" are used for player input. The jump button is held to 1.0 if recognized because the Jump() function call happens in the FixedUpdate() and not in the normal Update().

The movement functions have the following code:

// Method processing the running according to the forward (and sideward) input
void Run()
{
    // Player can only move forward in his look direction -> take length of both input movement parameters to get speed
    // Adjusting the look direction is done in the Turn() function
    float velInput = Mathf.Clamp((new Vector2(input.forward, input.sideward)).magnitude, 0.0f, 1.0f);

    velocity.z = velInput * moveSettings.forwardVel;

    // Update animator
    animator.SetFloat("MoveSpeed", velocity.z);
}

// Method processing the player turning
void Turn()
{
    // Forward direction: camPos -> playerPos
    Vector3 cameraVector = new Vector3(transform.position.x - playerCamera.transform.position.x, 0.0f, 
                                               transform.position.z - playerCamera.transform.position.z);

    // Calculate the look direction of the player based on the input and the cameraVector
    Vector3 playerLookDirection = Quaternion.LookRotation(cameraVector) * new Vector3(input.sideward, 0.0f, input.forward);

    if (playerLookDirection != Vector3.zero)
    {
        // Calculate the specific player rotation to match the player look direction
        Quaternion destRot = Quaternion.LookRotation(playerLookDirection);
        // Perform smoothing from current player look direction to new look direction
        transform.rotation = Quaternion.RotateTowards(transform.rotation, destRot, moveSettings.rotateVel * Time.deltaTime);
    }
}

// Method for processing the jump action
void Jump()
{
    // Check if player is standing on ground or is mid air
    if (grounded)
    {
        // Check for jump input and if the player is currently not jumping
        if (input.jump > 0 && !onJump)
        {
            // Set y-velocity accordingly
            velocity.y = moveSettings.jumpVel;
            onJump = true;
        }
        else if (grounded)
        {
            // Zero out y-velocity if player is standing on the ground and not jumping
            velocity.y = 0;
        }
    }
    else
    {
        // Falling -> Apply gravity
        velocity.y -= downAccel * Time.fixedDeltaTime;
    }

    // Reset jump input parameter
    input.jump = 0.0f;
}

Running is only possible in the viewing direction. Turning is done based on the input vector and the camera vector. Jumping adjusts the y-velocity and sets the onJump attribute of the class.

Finally the ground check looks like the following code:

    // Checks if the player is standing on a ground
    void CheckGrounded()
    {
        bool lastGrounded = grounded;

        // Cast a ray downwards in y-axis with a max distance of pivotOffset.y + minFallingDistToGround to see if it hit the ground layer
        bool hit = Physics.Raycast(transform.position + moveSettings.pivotOffset, 
                        Vector3.down, out groundHit, moveSettings.pivotOffset.y + moveSettings.minFallingDistToGround, moveSettings.ground);
        // Subtract the pivotOffset.y from the distance passed
        groundHit.distance -= moveSettings.pivotOffset.y;

        // The player is counted as grounded if 
        // his distance to the ground is smaller than the set distance to ground 
        // or
        // his distance to the ground smaller is smaller than the minimum falling distance set and
        // he was standing on the ground the last time checked and he is not during a jump
        if (hit && (groundHit.distance < moveSettings.distToGround || (lastGrounded && !onJump))) { // Player is grounded -> place him exactly on the ground with a distance of distToGround
            transform.position = new Vector3(transform.position.x, groundHit.point.y + moveSettings.distToGround, transform.position.z);
            grounded = true;

            // Set jump as finished
            onJump = false;
        }
        else
        {
            // Not grounded
            grounded = false;
        }
    }
}

First a ray is cast down from the player pivot + offset. If this ray hits, the player has a distance of minFallingDistToGround or less to the ground. The next if-branch serves the purpose of not cancelling an open jump by placing the player exactly over the ground.

Simple Follow Camera

For testing purposes we will introduce a very simple follow camera. The camera just keeps a certain distance in the xz-plane to the player and always looks towards him. There is no controlling, smoothing or collision detection yet. These topics will be discussed in the following blogposts.

using UnityEngine;

// Simple camera following player with a certain distance
public class SimpleCameraController : MonoBehaviour
{
    // The target to follow
    public Transform target;
    // The distance to keep
    public float distance = 4.0f;

    void Update()
    {
        // Move camera to XZ-plane of target
        transform.position = new Vector3(transform.position.x, target.position.y, transform.position.z);
        // Adjust distance from player while preserving camera look angle
        transform.position = target.position + (transform.position - target.position).normalized * distance;
        // Face player
        transform.LookAt(target);
    }
}

Final Steps

Now it’s time to setup our PlayerController and SimpleCameraController we just wrote.

Camera Setup

First of all, we have to select a target for our SimpleCameraController. You could just drag the Player directly into the Target field but then the camera would look exactly at the pivot of the Player which are usually the feet. A better way is to create an empty GameObject as child of the player and name it “CameraLookAt” for example, so you can adjust its position directly in the Unity Scene view.

Player Controls - Camera Look At

Drag this one into the the target field of the CameraController.

Player Camera Assign

Player Collision

Next we will define a collision layer which will be used for the player ground. Go to Edit > Project Settings >Tags and Layers. Here you can see all defined layers for now and also have the possibility to add your own layers. Just select the first free User Layer and enter “Ground” in the text field next to it.

Player Controls - Add Layer

Now we have a new layer which we can assign to our world.

Player Controls - Assign Layer

When asked if you want to set the layer for all child objects click Yes, change children. Now set the layer in the PlayerController for “Ground”.

Player Controls - Ground Player Assign

Now you can configure the PlayerController as desired.

Currently if walking against a wall, the sphere collider of the Player hits the wall and sticks to it because the Unity physics system applies friction to colliding objects. Hence we create a custom physics material with zero friction and assign this to the Player collider. Navigate to Assets > Create > Physic Material. Name it “ZeroFrictionPhysicMaterial” and change the values in the inspector to following:

Player Controls - Zero Friction Material

Then assign this material to the player collider:

Player Controls - Physic Material Assign

Concluding

That’s it so far for the action-adventure player controls. Hopefully we could give you a basic overview of some character movement concepts. The provided scripts can of course be extended by many ways like the ability to focus targets and a better jump with restricted player movement which will be explained in following tutorials. You can also add your own ideas to the controllers to suit your needs. In further tutorials we will also discuss the implementation of a controllable camera and a suitable camera collision in order to prevent the camera from moving through objects.

Thank you…

…for reading this article. We hope you enjoyed it 🙂

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *

*