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();
}
}
}
