Vector arithmetic to change NPC states and other game objects’ properties depending on player proximity

Introduction

For this project, I have built an enemy state machine that controls three separate states: Patrolling State, Approach Player State, and Attacking State. This final part of my game project builds the enemy’s artificial intelligence (AI). The enemy transitions to different states depending on proximity to the player (as long as the player is alive). The default state for the enemy is the patrol state. If the player is dead, the enemy will remain in the patrol state. If the player is in sight of the enemy, the enemy will approach the player. When the enemy is in range of the player, the enemy will attack the player. When the player is within the enemy’s distance, a skull game object will appear over the enemy’s head to indicate to the player that the enemy is about to attack. This contributes to the dynamics of the game, since the player is likely to react to an enemy approaching or attacking. By having a skull game object hang over the enemy, the player is more likely to know that the enemy is about to attack and will respond accordingly.

Using vector arithmetic to change NPC states and other game objects’ properties depending on player proximity adds to the mechanics of the game by creating obstacles that either the player must avoid or destroy. By adding these obstacles, the game becomes more challenging. Also, adding an enemy AI contributes to the game’s internal economy, since when the enemy game object is destroyed, the player’s score increases. However, if the enemy lands an attack on the player, the enemy’s health decreases. This creates tension in the game and actually gives the player purpose while playing. The player has to stay alive by destroying or avoiding enemies.

In my project, Moving a Nav-mesh Agent Between Points and to a Mouse Click, I described how I built the patrol movement of the player using the transform of empty game objects to create waypoints. For this project, I will describe how I used vector arithmetic to calculate if the player is in sight and if the player is within distance.

Handle State Method (Called Every Frame):

    void HandleStates () {
        if (!PlayerIsAlive()) {
            PatrolState();
            skull.SetActive(false);

        } else if (PlayerIsInSight()) {
            ApproachPlayerState ();
            skull.SetActive(true);

        } else if (PlayerIsWithinDistance()) {
            AttackState();
            skull.SetActive(true);
        } else {
            PatrolState();
            skull.SetActive(false);

        }
    }

 

The approach player state uses vector arithmetic to calculate the distance of the player. The distance between the enemy and the player is calculated every frame using Vector3.Distance() and passing the transform of the enemy and the transform of the player to the method.

Calculating Distance:

    void CalculateDistance () {
        //Find the distance between agent and the player
        currentPosition = transform.position;
        distance = Vector3.Distance (currentPosition, goal.position);
    }

The script tells the enemy to stop within a distance of the player before entering the attack state (indicated below by the float value distanceFromGoalToStop). The player is considered in sight when the distance is larger than distanceFromGoalToStop (as in, from right beside the player) and the distance is larger than the enemy sight range. In other words, the enemy can only “see” a certain distance, and the player is in sight when it’s far away (but not too far away) and not already standing beside it. The player is not considered in sight when it is within attacking distance, otherwise the enemy would be in the attack state.

Checking if Player Is In Sight:

    private bool PlayerIsInSight () {
        if (distance > distanceFromGoalToStop && distance < enemySightRange && !PlayerIsWithinDistance()) {
            return true;
        } else {
            return false;
        }
    }

The HandleStates() method checks whether or not the player is in sight, and if it is, then the enemy will approach the player and the skull game object is enabled. The enemy approaches the player. This is done by setting the enemy’s nav mesh agent’s destination to the player’s transform. A walk animation is triggered. This is done by setting a float value in the enemy’s animator controller. When set to a value greater than 0, the enemy transitions to a walk state. The enemy then approaches the player.

Approach Player State Script:
    void ApproachPlayerState() {
        //If distance is large enough and within the sight range, Go to goal (player)
        agent.Resume();
        animator.SetFloat ("Speed", 0.2f); 
        agent.destination = goal.transform.position;
    }

Attack State (When Player is in Distance)

When the player is within a certain distance, the enemy transitions into the attack state. In the attack state, the enemy first waits a few seconds before throwing the first attack. This is to ensure that the player doesn’t get swarmed by enemies and to give the player a “fighting chance.” This is done by checking if the enemy has thrown a first punch. By default, this Boolean value is set to false. If the player hasn’t thrown a first punch, an attack timer is set to 0, and the Boolean value is then set to true. This creates a delay between states, and sets the attack timer to 0. The attack timer increases over time.

In the attack state, the animator float that controls the walking animation is set to 0, and the animation transitions to an idle animation. When the timer is larger than a value that indicates the time between attacks, and if the player is still alive and within distance, the player then attacks. To ensure that the player is in distance for the attack, the PlayerIsWithinDistance() method calculates the distance from the enemy to the distance to stop in front of the player. If the distance from the enemy is less or equal to the the distance to stop in front of the player, the attack can occur.

During the attack, an attack animation plays. Just after the attack animation plays, there is a slight delay, then the player’s health decreases. The attack timer is then reset to 0. The enemy remains in the idle state until it is time to attack again, and then the attack repeats.

Attack State Method:

    void AttackState () {

        if (!hasThrownFirstPunch) {
            attackTimer = 0;
            hasThrownFirstPunch = true;
        }

        agent.Stop ();
        animator.SetFloat ("Speed", 0f);

        if (attackTimer > timeBetweenAttacks && PlayerIsAlive () && PlayerIsWithinDistance ()) {
            animator.SetTrigger ("isPunching");
            WaitForSeconds (0.25f);
            DoDamage ();
            attackTimer = 0;
        } 
    }

    void DoDamage () {
        playerHealth.TakeDamage(attackDamage);
    }

    IEnumerator WaitForSeconds (float seconds) {
        yield return new WaitForSeconds(seconds);
    }

    private bool PlayerIsWithinDistance () {
        if (distance <= distanceFromGoalToStop) {
            return true;
        } else {
            return false;
        }
    }

Bonus: Adding Health Pack Pickups

Since the enemy’s states ultimately affect the player’s health and consequently affect the internal economy of the game, I decided to add a catch-up mechanic by adding health packs that spawn at random locations. This ultimately affects the dynamics of the game, since players with low health may explore the game area to seek out health packs. In order to save on development time, I altered the enemy spawn script to spawn health pack prefabs. The health packs that spawn have a collider component that is a trigger. When the player interacts with the object, the player’s health is increased, and the health pack is destroyed. The script that spawns the health packs keeps track of all the health packs currently in the game, so that number is decreased by one when the health pack is destroyed.

 

Health Pack Script

using UnityEngine;
using System.Collections;

public class HealthPack : MonoBehaviour {

    private GameObject player;
    private PlayerHealth playerhealth;
    private GameObject spawnHealthPacks;
    private EnemyManager enemyManager;
    // Use this for initialization
    void Start () {
        player = GameObject.Find("Player");
         playerhealth = player.GetComponent();
        spawnHealthPacks = GameObject.Find("SpawnHealthPacks");
        enemyManager = spawnHealthPacks.GetComponent();
    }
    
    // Update is called once per frame
    void Update () {
    
    }

    void OnTriggerEnter(Collider other) { 
         playerhealth.currentHealth += 10; 
         Destroy (gameObject); 
         enemyManager.currentNumberOfHealthPickups--;

   }
}

Conclusion

Programming an enemy’s AI based on vector arithmetic contributes to an interactable space in the game. Adding mechanics such as enemies that attack the player and bonus health packs affect the player’s behavior in the game, therefore affecting the game’s dynamics.

While scripting this project, I have thought of the mechanics, dynamics, and aesthetics that ultimately create a game. As part of the dynamics, the internal economy of the game, health and score, motivate the players to take action by destroying or avoiding enemies and seeking out health packs when the player’s health is low. Although my game is quite simple, the player must constantly manage their health while scoring points in the game.

 

Enemy Movement Script:

using UnityEngine;
using System.Collections;

public class EnemyMovement : MonoBehaviour {

     //for initialization
    private NavMeshAgent agent; 
    private GameObject playerGameObject;
    private Animator animator;

    [Header("Distance")]
    public float distanceFromGoalToStop;
    private Vector3 currentPosition;
    private float distance;


    [Header("Enemy Sight")]
    public float enemySightRange;
    private Transform goal;

    [Header("Patrolling")]
    public Transform[] patrolWayPoints;
    public float timeToNextWaypoint;
    private float patrolTimer = 0;
    private int wayPointIndex;
    private bool isPatrolling;
    private int counter = 0;


    [Header("Attacking")]
    public float timeBetweenAttacks;
    public int attackDamage;
    private float attackTimer;
    private GameObject player; 
    private bool playerInRange;
    private PlayerHealth playerHealth;
    private bool hasThrownFirstPunch = false;
    private PlayerScore score;
    public GameObject skull;
    //For score:


    void Awake () {
         skull.SetActive(false);
        player = GameObject.FindGameObjectWithTag("Player");
        playerHealth = player.GetComponent();
        agent = GetComponent ();
        animator = GetComponent (); 
        score = player.GetComponent();
        //Set an initial patrol waypoint
        wayPointIndex = Random.Range(0, (patrolWayPoints.Length - 1));
        attackTimer = timeBetweenAttacks;
    } 
 

    void Update () {
        //Set the goal is the player's transform
        playerGameObject = GameObject.FindWithTag ("Player");
        goal = playerGameObject.transform;
        attackTimer += Time.deltaTime;
        patrolTimer += Time.deltaTime;
        CalculateDistance();
        HandleStates();
    }


    void CalculateDistance () {
        //Find the distance between agent and the player
        currentPosition = transform.position;
        distance = Vector3.Distance (currentPosition, goal.position);
    }

    void HandleStates () {
        if (!PlayerIsAlive()) {
            PatrolState();
            skull.SetActive(false);

        } else if (PlayerIsInSight()) {
            ApproachPlayerState ();
            skull.SetActive(true);

        } else if (PlayerIsWithinDistance()) {
            AttackState();
            skull.SetActive(true);
        } else {
            PatrolState();
            skull.SetActive(false);

        }
    }

    void ApproachPlayerState() {
        //If distance is large enough and within the sight range, Go to goal (player)
        agent.Resume();
        animator.SetFloat ("Speed", 0.2f); 
        agent.destination = goal.transform.position;

    }

     void PatrolState () {
        patrolTimer += Time.deltaTime;
        if (patrolTimer > timeToNextWaypoint) {
            if (wayPointIndex == patrolWayPoints.Length - 1) {
                wayPointIndex = 0;
            } else {
                wayPointIndex++;
            }
                patrolTimer = 0;
            }

        agent.Resume ();
        animator.SetFloat ("Speed", 0.2f);
        agent.destination = patrolWayPoints [wayPointIndex].position;

    }

    void AttackState () {

        if (!hasThrownFirstPunch) {
            attackTimer = 0;
            hasThrownFirstPunch = true;
        }

        agent.Stop ();
        animator.SetFloat ("Speed", 0f);

        if (attackTimer > timeBetweenAttacks && PlayerIsAlive () && PlayerIsWithinDistance ()) {
            animator.SetTrigger ("isPunching");
            WaitForSeconds (0.25f);
            DoDamage ();
            attackTimer = 0;
        } 
    }

    void DoDamage () {
        playerHealth.TakeDamage(attackDamage);
    }

    IEnumerator WaitForSeconds (float seconds) {
        yield return new WaitForSeconds(seconds);
    }

    private bool PlayerIsWithinDistance () {
        if (distance <= distanceFromGoalToStop) {             return true;         } else {             return false;         }     }     private bool PlayerIsInSight () {         if (distance > distanceFromGoalToStop && distance < enemySightRange && !PlayerIsWithinDistance()) {
            return true;
        } else {
            return false;
        }
    }

    private bool PlayerIsAlive () {
        if (playerHealth.currentHealth <= 0) {
            return false;
        } else {
            return true;
        }
    }
} 

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 )

Connecting to %s