A Player Character with Several States that is Mouse Controllable and Does Not Walk Through Walls

Introduction

This project involves creating the player states, and therefore creating the player’s ability to move and interact with the game world. The player game object has three states: Idle state, run state, attack state. Each state was built beginning from a player idle state. Essentially, as I built each state, the player and the game itself gained complexity. Each of the states discussed below correspond to an animation in the player game object’s animator. In a previous project, An Animated Character with Independent States and Transitions, I explained how I created the animation controller for the player game object and I used buttons to transition between each animation. The table below explains each state in order to better understand the player game object’s state transitions. These states, when combined, create a player character that will navigate the game world, while trying to stay alive by fending off enemy attacks.

Idle State

The default state that all animations transition to is the idle state. When the player is not running, attacking, taking damage, or is dead, the player is idle. As it is the default animation state, no parameters need to be called to ensure that the player is idle. From the idle state, the player can transition to a walk state, take damage state, and a death state. If the player game object had more complex states that required parameters that were Boolean values, then I would consider building an idle state. The idle state would reset all Boolean values in the animator controller. Since my player game object’s animator controller parameters are mostly triggers, I do not think it would be necessary to script an idle state in the player controller script. By default, the player is idling.

Walk State

The walk state involves moving the player from one point to another. In the last project, Moving a Nav-mesh Agent Between Points and to a Mouse Click, I explained how the player moves from one point to another through lerping. Whenever the player is lerping, a Boolean value, isRunning, is set in the player animator controller. As the player nears the end of the lerp, the isRunning bool is set to false.

Attack State

The attack state is the most complicated state for the palyer game object. The attack state involves several timers, several attack animations, a capsule collider around the player game object.

The attack state begins when there is input from the player (right mouse click) and a sufficient amount of time has passed since the last attack. When the player wishes to attack, the first step in the script is to check if a sufficient amount of time has passed since the last attack. I added a timer that calculates the time between attacks for two reasons. The first is so that an attack animation can complete its animation cycle before the player attacks again. This avoids a situation where a player “button smashes” and right clicks to attack several times. If this timer were not in place, the attacks would cycle the amount of times the player clicked. For example, if a player “button smashed” to attack 8 times, the player would cycle through the attack state 8 times. The other reason that I added a timer between attacks is to add a challenge for the player. By not allowing the player to attack in succession, the player is forced to choose the right moment to attack the enemy.

If the above conditions are true, then an attack method is called. When the attack occurs, an animator trigger is set to “isAttacking.” This allows the transition from the idle animation to the attack animation. When the attack animation is complete, the attack animation transitions to the idle animation and the player returns to the idle state. To add variety to the game, I chose to randomize the attack animation every time the player attacks. The four animations are: right kick, left kick, right punch, and left punch. I labeled each attack trigger “Attack {Number}” where {number} is an integer from 1 to 4. When the player game object is in the attack state, a string value holds the string “Attack ” concatenated with a random integer from 1 to 4. The script then passes this string to the player animator, and the corresponding trigger is set.

Triggering a Random Attack Animation:

string attackState = "Attack " + Random.Range((int) 1, (int) 4);
        playerAnimator.SetTrigger(attackState);

Every frame, the game also handles controlling the attack collider by calling a method AttackColliderHandler(). The Attack() method sets a timer to 0 and sets a Boolean value of isAttacking to true. The AttackColliderHandler() method checks has a conditional statement that checks these two conditions and begins a chain of timers that enabled and disable a capsule collider. In other words, when the player is in the attack state, a capsule collider is enabled. This collider is a trigger that when it intersects with an enemy game object, that enemy game object is destroyed. This collider is set as a trigger in Unity.

When a collider attached to the enemy game object collides with the player’s attack collider, a method called OnTriggerEnter() is called. This method is in a script attached to all enemy game objects. When the enemy collides with a trigger collider, this method is called. The method attached to the enemy game object then checks if the trigger collider is the player’s attack collider, and if it is, the enemy game object is destroyed.

OnTriggerEnter() method:

     public void OnTriggerEnter (Collider col) {
        if (col.gameObject.tag == "PlayerCollider") {
            Destroy (gameObject);
        }
    } 

In a previous project, I wrote about how triggering methods from the animation itself was not possible since the assets that I used for this project were set as read-only. Ideally, I would have had the animation call a method at a certain point (say, just as the player game object started a punch). That method would turn on the collider. When the animation was near complete, I would call another method that would turn the collider off.

However, since the character asset was set as read only I had to come up with a solution that would work just the same. How I eventually mimicked the above behaviour was by creating a series of timers. When the player attacks, there is a slight delay before the timer is enabled. Afterwards, the collider is enabled and another timer begins. This timer tracks the amount of time the collider will be enabled. When that timer is complete, the collider is disabled.
I do admit that although this is a quick fix for the issue of having read-only assets, this is not ideal since Time.deltaTime increases independently from framerate. In other words, playing this game on a machine with a faster or slower frame refresh rate will create variations in when the capsule collider is enabled or disabled.

Attack Section of Player Controller Script

    //Objects 
    private GameObject player;
    private Rigidbody rb;
    private Animator playerAnimator;

    //attack collider (for kicks and punches)
    public CapsuleCollider attackCollider;
    private float timeColliderOn = 0.2f;
    private float colliderTimer;
    private float waitToCollider;
    private float timeToWaitForCollider = 0.4f;
    private bool isAttacking = false;
    private float ColliderDegradeTimer = 0.1f;
    private bool ColliderTimerEnabled = false;

    void Update () {
        HandleInput();
        AttackColliderHandler();
    }    
    void HandleInput () {
        if (Input.GetMouseButtonDown (1) && !isLerping && timeSinceLastAttack <= 0) {             Attack ();         }     }     void Attack () {    //CALLED FROM INPUT         string attackState = "Attack " + Random.Range((int) 1, (int) 4);         playerAnimator.SetTrigger(attackState);         timeSinceLastAttack = timeBetweenAttacks;         isAttacking = true;         waitToCollider = 0;     }     void AttackColliderHandler() { //CALLED EACH FRAME         //When attacking this adds delay of when collider turns on         if (waitToCollider >= timeToWaitForCollider && isAttacking) { 
        //After delay, we increase another timer that will turn collider on
            colliderTimer = timeColliderOn;                              
        }

        //if the collider timer is bigger than 0 (because above if statement)
        //Enable the collider
        if (colliderTimer >= 0) {                                        
            attackCollider.enabled = true;
        //A collider Timer is enabled                                 
            ColliderTimerEnabled = true;                                
        } 

        //If Countdown Timer For collider is on (Since this is checked each frame)
        if (ColliderTimerEnabled) {                
        //Start this countdown which calculates how long until collider degrades    
            ColliderDegradeTimer -= Time.deltaTime;    
        }

        //If the above if statement ran (and timer is done counting down)
        if (ColliderDegradeTimer <= 0 && ColliderTimerEnabled) {
        //Turn off the attack collider    
            attackCollider.enabled = false;        
        //We are done attacking
            isAttacking = false;
        //We don't need to keep counting down                
            ColliderTimerEnabled = false;
        //We reset the timer.             
            ColliderDegradeTimer = 0.1f;        
        }
    } 
    
//TIMERS//
    public void Timers () { //CALLED EACHFRAME
        timeSinceLastAttack -= Time.deltaTime;
        waitToCollider += Time.deltaTime;
        colliderTimer -= Time.deltaTime;

    }
}

Take Damaged State

When a player takes damage from an enemy, the player temporarily enters into a take damage state. In the take damage state, the isDamaged trigger parameter in the player animator controller is triggered. Any state can transition to the take damage state in the animator controller, and the take damage state transitions back to the idle default state. When the enemy is in the attack state and throws an attack towards the player, the TakeDamage() method is called from script that controls the enemy. When the player receives damage, the player’s total health is reduced by a certain amount, and the player’s animator is set to play the damage animation. If the player’s health falls to 0, the player is dead, and a Death() method is called. This method begins the player game object’s death state.

Take Damage section of Player Health Script:

    public int startingHealth = 100;
    public int currentHealth
    Animator anim;

    void Awake (){
        anim = GetComponent  ();
        currentHealth = startingHealth;
    }

    public void TakeDamage (int amount) {
        currentHealth -= amount;
        if (currentHealth <= 0 && !isDead) {
            Death ();
        } else {
            anim.SetTrigger("isDamaged");
        }
    }

Death State

When the player game object’s health reaches 0, a Death() method is called from the player health script and the player enters the death state. When the player is dead, a Boolean value to indicate that the player is dead is set to true, and the “isDead” trigger is triggered in the animator. In the animator, any state can transition to the death state, however there are no transitions from this state to any other animation state, since the player will not revive itself or come back to life in some form. When building the death state, had to ensure that the player’s health remains 0 when the player. This is to avoid a case where perhaps more than one enemy is attacking the player and the player dies while an enemy is mid-attack, and the player health will become a negative integer. If the player is dead, the player health will remain 0 every frame due to a conditional statement that checks if the player is dead, found in the Update() method. When the isDead Boolean value is set to true, the ability to enter the walk state is disabled, and the look at mouse script is disabled. This ensures that the player no longer moves while in the death state (it can no longer lerp). This also ensures that the player game object doesn’t rotate around when in the death state, since the look at mouse script controls the player’s transform rotation along with its child camera game object.

Death section of Player Health Script:

    public bool isDead;   

    void Update () {
        if (isDead) {   
            currentHealth = 0;
        }
    }

    //DEATH STATE
    void Death () {
        isDead = true;
        anim.SetBool ("isDead");
    }       
} 

Mouse Controls

The player game object is controlled by input received from the player’s mouse. The left click of the mouse triggers a transition from the idle state to the walk state, and the right mouse click triggers a transition from the idle state to the attack state. The input from the user is nested in a HandleInput() method, that is called every frame. In this method there are two conditions. Each condition ensures that there is a mouse click from the user (either left or right), and also ensures that the player is not moving by checking if isLerping false (thus signalling that the player is not in the walk state).

Handle Input method:

    void HandleInput () {
        if (Input.GetMouseButtonDown (0) && !isLerping) {
            StartLerping ();
        }
        if (Input.GetMouseButtonDown (1) && !isLerping && timeSinceLastAttack <= 0) {
            Attack ();
        }
    }

Does Not Walk Through Walls

Creating a player that does not walk through walls is quite simple. I simply added a capsule collider component around the player game object.

There are colliders set around the playable area. These colliders act as walls and prevent the player game object from traversing through the collider.

The attack collider does not interfere with the walls, since this collider is set as a trigger. When a collider is set as a trigger, another collider can pass freely through it. Setting up colliders around a playable area ensures that a game object stays within the boundary of the game.

Conclusion

Each of the states triggers some form of action that adds to the mechanics of the game. The player can now make decisions in game that will affect how the game is played. As the player masters the attack function and can destroy enemies, the player will have to take decisions in game to survive and (as I will explain in the next project) score points.
By creating a player character with several states that is mouse controllable and does not walk through walls, I am able to add to the game’s aesthetics by adding challenging but understandable controls. The player is able to move through the game space, attack, take damage, stay idle, or die. These states are crucial for creating the mechanics of a simple survival game. Below are the two scripts that handle the player’s states and health. Both these scripts were used to create the five independent states of the player game object.

Player Controller Script (Handles the Walk and Attack States):

using UnityEngine;
using System.Collections;

public class PlayerController : MonoBehaviour {

	//Get Objects
	private GameObject player;
	private Rigidbody rb;
	private Animator playerAnimator;

	//Vars for Lerping
	private Vector3 currentPosition;
	private Vector3 targetPosition;
	private Vector3 targetPositionGrounded;
	public float timeTakenDuringLerp = 1f; 	//Time taken to move from start to finish positions
	private bool isLerping; //whether we are interpolating or not
	private float timeStartedLerping; //Time.time value when we started interpolation

	//used for attacking
	public float timeBetweenAttacks;
	private float timeSinceLastAttack;
	private bool isDead;

	//attack collider (for kicks and punches)
	public CapsuleCollider attackCollider;
	private float timeColliderOn = 0.2f;
	private float colliderTimer;
	private float waitToCollider;
	private float timeToWaitForCollider = 0.4f;
	private bool isAttacking = false;
	private float ColliderDegradeTimer = 0.1f;
	private bool ColliderTimerEnabled = false;

	//To transition when player is dead
	private float deathTimer = 0;
	public float timeToStayDead = 4f;

	void Start() {
		player = gameObject;
		rb = GetComponent();
		playerAnimator = GetComponent();
		timeSinceLastAttack = 0;
		colliderTimer = 0;
	}

	void Update () {
		currentPosition = player.transform.position;
		Timers();
		HandleInput();
		AttackColliderHandler();
	}

	void FixedUpdate () {
		if (!gameObject.GetComponent ().isDead) {
			HandleLerping ();
		} else {
			deathTimer += Time.deltaTime;
			if (deathTimer > timeToStayDead) {
				Application.LoadLevel(0); //reload if dead
			}
		}
	} 	

	//Handling Input
	void HandleInput () {
		if (Input.GetMouseButtonDown (0) && !isLerping) {
			StartLerping ();
		}
		if (Input.GetMouseButtonDown (1) && !isLerping && timeSinceLastAttack = 0.9f) {
				isLerping = false;
				playerAnimator.SetBool("isRunning", false);
			}

		}
	}


	//ATTACK STATE//

	void Attack () {	//CALLED FROM INPUT
		string attackState = "Attack " + Random.Range((int) 1, (int) 4);
		playerAnimator.SetTrigger(attackState);
		timeSinceLastAttack = timeBetweenAttacks;
		isAttacking = true;
		waitToCollider = 0;
	}

	void AttackColliderHandler() { //CALLED EACH FRAME
		//When attacking this adds delay of when collider turns on
		if (waitToCollider >= timeToWaitForCollider && isAttacking) { 
		//After delay, we increase another timer that will turn collider on
			colliderTimer = timeColliderOn;							  
		}

		//if the collider timer is bigger than 0 (because above if statement)
		//Enable the collider
		if (colliderTimer >= 0) {										
			attackCollider.enabled = true;
		//A collider Timer is enabled 								
			ColliderTimerEnabled = true;								
		} 

		//If Countdown Timer For collider is on (Since this is checked each frame)
		if (ColliderTimerEnabled) {				
		//Start this countdown which calculates how long until collider degrades	
			ColliderDegradeTimer -= Time.deltaTime;	
		}

		//If the above if statement ran (and timer is done counting down)
		if (ColliderDegradeTimer <= 0 && ColliderTimerEnabled) {
		//Turn off the attack collider	
			attackCollider.enabled = false;		
		//We are done attacking
			isAttacking = false;
		//We don't need to have the above if statement running (we don't need to keep counting down)				
			ColliderTimerEnabled = false;
		//We reset the timer. 			
			ColliderDegradeTimer = 0.1f;		
		}
	}

	//TIMERS//

	public void Timers () { 
	//CALLED EACHFRAME
		timeSinceLastAttack -= Time.deltaTime;
		waitToCollider += Time.deltaTime;
		colliderTimer -= Time.deltaTime;

	}
}

Player Health Script (Handles the Take Damage and Death States):

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class PlayerHealth : MonoBehaviour
{
    public int startingHealth = 100;  // The amount of health the player starts the game with.
    public int currentHealth;         // The current health the player has.
    Animator anim;                    // Reference to the Animator component.
    public bool isDead;               // Whether the player is dead.


    void Awake (){
        anim = GetComponent  ();
        currentHealth = startingHealth;
    }

    void Update () {
        if (isDead) {     //ensures that dead (if health pickup shows up at place of death, for example)
            currentHealth = 0;
        }
    }


    //TAKING DAMAGE OR ADDING HEALTH
    public void TakeDamage (int amount) {
        currentHealth -= amount;
        if (currentHealth <= 0 && !isDead) {
            Death ();
        } else {
            anim.SetTrigger("isDamaged");
        }
    }

    public void AddHealth (int amount) {
        currentHealth += amount;
    }


    //DEATH STATE
    void Death () {
        isDead = true;
        anim.SetTrigger ("isDead");
    }       
} 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s