Waypoint System Example
The way this waypoint system works is very simple and adaptable to any new project. The base is originally from Dr. Penny Bly’s Udemy course “The Beginner’s Guide to Artificial Intelligence in Unity” (very helpful course with a great teacher!) and then it’s adapted with various unit behaviors according to what’s desired in the given project.
For Safety Protocol, I ended up placing three waypoint managers on the moving unit, to allow for different behaviors depending on which waypoint manager is currently active. This allows for a more dynamic behavior, which gives a more interesting player experience.
In this specific instance, the roomba roams randomly between the waypoints when using the first and third set of waypoints. When the player approaches, the roomba goes to the second manager’s waypoints and follows them consecutively until the end. It then moves on to the next waypoint manager’s waypoints to continue roaming there. The sequence is repeated whenever the player gets close.
using UnityEngine;
public class RoombaMovement : WaypointFollow
{
[SerializeField][Tooltip("Waypoints made by this Waypoint Manager will be used for Roomba's second behavior.")]
private WaypointManager _secondWPManager = default;
[SerializeField][Tooltip("Waypoints made by this Waypoint Manager will be used for Roomba's third behavior.")]
private WaypointManager _thirdWPManager = default;
[SerializeField][Tooltip("How fast the roomba should be during its second behavior.")]
private float _accelerateSpeed = 150f;
public AudioSource movementSound;
public AudioSource voiceSound;
public AudioSource runAwaySound;
[HideInInspector]
public bool playerHasArrived = false;
private bool _isInVentRoom = false;
private int _previousWP = 0;
private WaypointManager _currentWpManager = default;
protected override void Start()
{
base.Start();
SetCurrentWPManager(firstWPManager);
}
protected override void LateUpdate()
{
if (_secondWPManager.waypoints.Count == 0 || _secondWPManager.waypoints == null)
{
return;
}
if (_thirdWPManager.waypoints.Count == 0 || _thirdWPManager.waypoints == null)
{
return;
}
base.LateUpdate();
}
protected override void SetLookAtGoal()
{
// lookAtGoal is set by currentWPManager
lookAtGoal = _currentWpManager.waypoints[currentWP];
}
protected override void SetUnitBehavior()
{
if (playerHasArrived == false)
{
UseDefaultSpeed();
if (direction.magnitude < wpProximity)
{
RandomizeCurrentWP();
//Avoid going to the same waypoint twice in a row
while (currentWP == _previousWP)
{
RandomizeCurrentWP();
}
}
}
else
{
UseAccelerateSpeed();
if (direction.magnitude < wpProximity)
{
// increse/decrese currentWP depending on if the roomba is
//currently in the big vent room or not
// to make sure the waypoints of the second manager get followed
//in a consecutive order from either direction
if (_isInVentRoom)
{
currentWP = currentWP - 1;
}
else
{
currentWP = currentWP + 1;
}
// when roomba has reached the max index of the second waypoint manager.
//-> has entered the big vent room
if (currentWP > _secondWPManager.waypoints.Count - 1)
{
SetCurrentWPManager(_thirdWPManager);
currentWP = 0;
playerHasArrived = false;
_isInVentRoom = true;
PlayRoamingSound();
}
// when roomba has reached index 0 of second waypoint manager.
//-> has left the big vent room
if (currentWP < 0)
{
SetCurrentWPManager(firstWPManager);
currentWP = 0;
playerHasArrived = false;
_isInVentRoom = false;
PlayRoamingSound();
}
}
}
}
private void SetCurrentWPManager(WaypointManager waypointManager)
{
_currentWpManager = waypointManager;
}
private int RandomizeCurrentWP()
{
SetPreviousWP();
return currentWP = Random.Range(0, _currentWpManager.waypoints.Count);
}
private void SetPreviousWP()
{
_previousWP = currentWP;
}
private void UseDefaultSpeed()
{
speed = _defaultSpeed;
}
private void UseAccelerateSpeed()
{
speed = _accelerateSpeed;
}
private void PlayRunAwaySound()
{
voiceSound.Stop();
movementSound.Stop();
runAwaySound.Play();
AudioManager.instance.Play("roombaRun");
}
private void PlayRoamingSound()
{
voiceSound.Play();
movementSound.Play();
runAwaySound.Stop();
AudioManager.instance.Stop("roombaRun");
}
private void OnTriggerEnter(Collider other)
{
PlayerController playerController = other.GetComponent();
if (playerController != null)
{
playerHasArrived = true;
SetCurrentWPManager(_secondWPManager);
SetSecondManagerCurrentWP();
PlayRunAwaySound();
}
}
private void SetSecondManagerCurrentWP()
{
if (_isInVentRoom)
{
currentWP = _currentWpManager.waypoints.Count - 1;
}
else
{
currentWP = 0;
}
}
}
using UnityEngine;
public class WaypointFollow : MonoBehaviour
{
[SerializeField, Tooltip("The main WaypointManager for the unit's default movement.")]
protected WaypointManager firstWPManager = default;
[SerializeField, Tooltip("How close the unit needs to be to target location before going towards new target.")]
protected float wpProximity = 0.3f;
[SerializeField, Tooltip("Unit movement speed. Note: Using \"SimpleMove()\".")]
protected float speed = 100.0f;
[SerializeField, Tooltip("Unit turning/rotation speed. Set with caution; wrong values may cause bugs when turning.")]
private float _rotationSpeed = 3.0f;
[HideInInspector]
public int currentWP = 0;
protected float _defaultSpeed = 0f;
protected Vector3 direction = default;
protected Vector3 lookAtGoal = default;
private bool _goingBackToStart = false;
private CharacterController _controller = default;
void Awake()
{
_controller = GetComponent();
}
protected virtual void Start()
{
_defaultSpeed = speed;
}
protected virtual void LateUpdate()
{
if (firstWPManager.waypoints.Count == 0 || firstWPManager.waypoints == null)
{
return;
}
Move();
}
protected virtual void Move()
{
SetLookAtGoal();
SetDirection();
SetRotation();
SetUnitBehavior();
_controller.SimpleMove(direction.normalized * (speed * Time.deltaTime));
}
protected virtual void SetLookAtGoal()
{
lookAtGoal = firstWPManager.waypoints[currentWP];
}
protected virtual void SetUnitBehavior()
{
if (direction.magnitude > wpProximity)
{
return;
}
if (direction.magnitude < wpProximity)
{
if (firstWPManager.circularWaypointSystem == true)
{
currentWP++;
if (currentWP >= firstWPManager.waypoints.Count)
{
currentWP = 0;
}
}
else
{
if (currentWP >= firstWPManager.waypoints.Count - 1)
{
currentWP = firstWPManager.waypoints.Count - 1;
_goingBackToStart = true;
}
else if (currentWP <= 0)
{
_goingBackToStart = false;
currentWP = 0;
}
if (_goingBackToStart)
{
currentWP--;
}
else
{
currentWP++;
}
}
}
}
private void SetDirection()
{
lookAtGoal.y = transform.position.y;
direction = lookAtGoal - transform.position;
}
private void SetRotation()
{
transform.rotation = Quaternion.Slerp(transform.rotation,
Quaternion.LookRotation(direction),
Time.deltaTime * _rotationSpeed);
}
}