Avoidance AI Behavior

Hello all, I hope everyone has had a nice holiday season, and beginning to their year! I’ve been pretty busy trying to keep up with my game projects just as much as trying to keep up with my friends who’ve also been on break! But amongst all this I’ve actually been working on a ton of code for “Project RAT”, as my friends and I have been calling it. And specifically, over the last 2 days I’ve been working on a behavior for one of our ranged enemy types, who is meant to avoid the player, and avoid collisions with walls and pitfalls.

How I did this wasn’t terribly difficult, but it definitely proved very fun to write as it did take a fair amount of effort! It operates on a List which stores all Walls and Players OnTriggerEnter() and removes them OnTriggerExit(). From there I’m just taking the Vector of the object in my list, normalizing it, and multiplying it by the reverse distance from the transform of the enemy. Oh! and then we made that shit negative. Easy enough! We go through all elements in the List with that same operation, adding the resulting Vector2 to a field which we then use Vector2.MoveTo()to send our enemy in that direction.

This Behavior is complete, but still requires some polishing! I would like to make the movement of the AI smoother and less exact, while also creating a better catch for when the AI is backed into a corner (as of right now it just stops moving). The Bot has 2 states. The idle state is obviously just a place holder which has the bot pacing back and forth. The Avoid behavior is the real highlight here! But without further ado, here’s some code:

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

//The abstract keyword enables you to create classes and class members that are incomplete and must be implemented in a derived class.
public class Avoidance: MonoBehaviour
{
    //Variables for the walk speed, and the duration of the pace back and forth
    public FloatVariable walkSpeed;
    public FloatVariable paceDuration;
    public FloatVariable visionDistance;
    public StringVariable wallsTag;
    public SphereCollider aiVision;


    //Arr to store all obstacles, Vector for avoid, bool for is avoiding, and then moveRight for idle
    public List<GameObject> Radius = new List<GameObject>();
    public Vector2 avoidVector = new Vector2();
    public bool isAvoiding;
    public bool moveRight;
    private bool playerIn;

    //Timer used for pacing back and forth
    private float timer = 0.0f;


    private void Start()
    {
        aiVision.radius = visionDistance;
    }

    // Update is called once per frame
    void Update()
    {
        //As long as not avoiding
        if (!isAvoiding)
        {
            //Will proceed with idle
            idleMovements();
        }
        else
        {
            AvoidMovement();
        }

    }

    //The default for the enemy with no walls or players around
    private void idleMovements()
    {

        //Will AI move to the right
        if (moveRight)
        {
            //moves AI right
            transform.Translate(2 * Time.deltaTime * walkSpeed, 0, 0);

            //flips AI
            transform.localScale = new Vector2(transform.localScale.x, transform.localScale.y);
        }
        else
        {
            //moves AI left
            transform.Translate(-2 * Time.deltaTime * walkSpeed, 0, 0);

            //overrides the central motherboard of the player, installing 15 terabytes of My Little Pony episodes
            transform.localScale = new Vector2(-transform.localScale.x, transform.localScale.y);
        }

        //adds to timer
        timer += Time.deltaTime;

        //once timer is over the duration
        if(timer > paceDuration)
        {

            //move left
            if (moveRight) moveRight = false;
            else moveRight = true;

            //reset
            timer = timer - paceDuration;
        }       
    }


    //Controls avoidance behaviors
    private void AvoidMovement()
    {

        transform.Translate(Vector2.MoveTowards(transform.position, avoidVector, 1) * Time.deltaTime);
        
    }


    //Recalculates the avoid vecotr for avoidance behavior
    private void RecalculateAvoid()
    {
        //clears vector2 and instantiates a temp for reverse distance
        float tempDistance = 0f;
        Vector2 tempVec;

        //If the player isnt present or if the player is present and there is only 1 wall
        if (!playerIn || playerIn && Radius.Count < 3) { 

            //Iterates through array of Positions
            for (int rep = 0; rep < Radius.Count; rep++)
            {
                tempVec = new Vector2(Radius[rep].transform.position.x, Radius[rep].transform.position.y);
                ClearAvoid();


                //Gets the reverse distance from the AI transform.
                tempDistance = (visionDistance - (Vector2.Distance(tempVec, transform.position)));

                //Normalized and multiplies by reverse distance
                tempVec.Normalize();
                tempVec *= tempDistance;

                //Adds to the "avoid" vector
                avoidVector += tempVec;
            }

            //Flips the sign of the AvoidVector
            avoidVector = avoidVector * -1;
        }
        
        else if (playerIn)
        {
            ClearAvoid();
        }

    }

    //Resets vector field
    private void ClearAvoid()
    {
        avoidVector = Vector2.zero;
    }

    //On player or wall enter into collider
    private void OnTriggerEnter(Collider other)
    {

        Debug.Log("Triggered");
    

        //If Game Object is wall
        if(other.gameObject.tag == wallsTag)
        {
            //Set temp
            GameObject temp = other.gameObject;

            //add and recalculate
            Radius.Add(temp);
            RecalculateAvoid();
            isAvoiding = true;
        }

        //If Game Object is player
        else if(other.gameObject.tag == "Player")
        {
            //Set temp
            GameObject temp = other.gameObject;

            //Add and recalculate
            Radius.Add(temp);
            RecalculateAvoid();
            isAvoiding = true;
            playerIn = true;

            Debug.Log("Gonna recalculate!");
        }
    }

    //When a tracked object leaves the zone
    private void OnTriggerExit(Collider other)
    {

        //If player exits
        if (other.gameObject.tag == "Player")
        {

            //Iterate through List
            for(int rep = 0; rep < Radius.Count; rep++)
            {

                //If the selected tag is player
                if(Radius[rep].gameObject.tag == "Player")
                {

                    //Remove player, recalculates avoid vector, and break
                    Radius.RemoveAt(rep);
                    RecalculateAvoid();
                    playerIn = false;
                    break;
                }
            }
        }

        //If it's a wall
        else if(other.gameObject.tag == wallsTag)
        {

            //Iterate through list
            for(int rep = 0; rep < Radius.Count; rep++)
            {
                //If the position of the selected matches that which exited
                if(Radius[rep].gameObject.transform.position == other.gameObject.transform.position)
                {

                    //Remove, recalculate avoid vector, and break
                    Radius.RemoveAt(rep);
                    RecalculateAvoid();
                    break;
                }
            }
        }

        //If the List is empty
        if (Radius.Count == 0)
        {
            //Reset all
            isAvoiding = false;
            ClearAvoid();
        }
    }
}
pics make the site look cleaner 😉

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 )

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