First post, and first off - want to say how much i appreciate this course. Sam and Rick have done a wonderful job presenting the material - and i greatly appreciate all the source being made available - including for commercial use. And the feedback on the forums has been super helpful.
I’m through the RPG core and Inventory courses - next onto Dialogue and Quests. Was going back through code to comment and tidy - and wanted to work through some of the Mob aggro mechanics - where it seems Mobs could get caught into a feedback loop of retaining aggro. Below is my code with comments and some debugging lines that describe my solution. In a nutshell, introducing three separate timers for timeSinceHitByPlayer, timeSinceSawPlayer, timeSinceHeardShout - and only allowing a Shout (aggravate neighbors) if the AI is hit or sees the player. That is, a shout does not cause another shout. This seems to break the loop.
Comments/Feedback welcome.
Cheers!
using UnityEngine;
using RPG.Combat;
using RPG.Core;
using RPG.Movement;
using RPG.Attributes;
using GameDevTV.Utils;
using System;
namespace RPG.Control {
public class AIController : MonoBehaviour {
[SerializeField] float chaseDistance = 5f; // aggro radius
[SerializeField] float suspicionTime = 3f; // Ager Aggro ends, there is a suspiscion period
[SerializeField] float aggroCooldownTime = 5f; // if > chaseDistance for this time, -> suspicion mode
[SerializeField] PatrolPath patrolPath = null;
[SerializeField] float waypointTolerance = 1f;
[SerializeField] float wayPointMoveAndDwellTime = 2f; // time stopped per waypoint
[Range(0,1)] // require next variable to be in this range
[SerializeField] float patrolSpeedFraction = 0.2f;
[SerializeField] float shoutRadius = 5f; // bring your friends...
// three aggro events: HitByPlayer (Unity Action), SawPlayer, HeardShout
// each has its own timer: timeSinceHitByPlayer, timeSinceSawPlayer, timeSinceHeardShout
// will be in an aggro state during a HitByPlayerCD, SawPlayerCD, JoinThePartyCD
// each aggro event sets each timer to zero
// a check of any timer within CD window yields an aggrevated state
// Shout is caused by being hit or seeing a player, not by another shout, this cuts the shout feedback loop in the pack
Fighter fighter;
Health health;
Mover mover;
GameObject player;
LazyValue<Vector3> guardPosition_lv;
float timeSinceArrivedAtLastWaypoint = Mathf.Infinity;
// replace timeSinceAggrevated counter with separate counters
float timeSinceHeardShout = Mathf.Infinity;
float timeSinceLastSawPlayer = Mathf.Infinity;
float timeSinceHitByPlayer = Mathf.Infinity;
int currentWaypointIndex = 0;
private void Awake() {
fighter = GetComponent<Fighter>();
health = GetComponent<Health>();
mover = GetComponent<Mover>();
player = GameObject.FindWithTag("Player");
guardPosition_lv = new LazyValue<Vector3>(GetGuardPosition);
}
private Vector3 GetGuardPosition() {
return transform.position;
}
private void Start() {
guardPosition_lv.ForceInit();
}
private void Update() {
if (health.IsDead()) return;
if ( ( IsAggrevated() || PlayerSeen() ) && fighter.CanAttack(player)) {
AttackBehaviour();
}
else if (timeSinceLastSawPlayer < (aggroCooldownTime + suspicionTime)) {
SuspicionBehaviour();
}
else {
PatrolBehaviour();
}
UpdateTimers();
}
// note: this is also called by a Unity event on the GameObject
public void PlayerHitMe() {
print($"{gameObject.name} : you hit me!");
AggrevateNearbyEnemies(); // shout
timeSinceHitByPlayer = 0;
}
public void HeardShout() {
print($"{gameObject.name} : heard a shout!");
// hearing a shout doesn't make you shout, breaks the loop
timeSinceHeardShout = 0;
}
public bool PlayerSeen() {
float distanceToPlayer = Vector3.Distance(transform.position, player.transform.position);
if (distanceToPlayer < chaseDistance) {
print($"{gameObject.name} : i see you!");
AggrevateNearbyEnemies(); // shout
timeSinceLastSawPlayer = 0;
return true;
}
return false;
}
private void UpdateTimers() {
timeSinceArrivedAtLastWaypoint += Time.deltaTime;
timeSinceHeardShout += Time.deltaTime;
timeSinceLastSawPlayer += Time.deltaTime;
timeSinceHitByPlayer += Time.deltaTime;
}
private void PatrolBehaviour() {
Vector3 nextPosition = guardPosition_lv.value;
if ( patrolPath != null) {
if (AtWaypoint()) {
CycleWaypoint();
}
nextPosition = getCurrentWaypoint();
}
if (timeSinceArrivedAtLastWaypoint > wayPointMoveAndDwellTime) {
mover.StartMoveAction(nextPosition, patrolSpeedFraction); // stops fighter action
timeSinceArrivedAtLastWaypoint = 0;
}
}
private Vector3 getCurrentWaypoint() {
return patrolPath.GetWayPoint(currentWaypointIndex);
}
private void CycleWaypoint() {
currentWaypointIndex = patrolPath.GetNextIndex(currentWaypointIndex);
}
private bool AtWaypoint() {
float distanceToWaypoint = Vector3.Distance(transform.position, getCurrentWaypoint());
return distanceToWaypoint < waypointTolerance;
}
private void SuspicionBehaviour() {
GetComponent<ActionScheduler>().CancelCurrentAction();
}
private void AttackBehaviour() {
fighter.Attack(player);
}
private void AggrevateNearbyEnemies() { // shout
// just centering a sphere of r shoutRadius on the current enemy
// it is not traveling - so, any direction with max distance 0
// find all colliders that hit
RaycastHit[] hits = Physics.SphereCastAll(transform.position, shoutRadius, Vector3.up, 0);
foreach (RaycastHit hit in hits) {
AIController ai = hit.collider.GetComponent<AIController>();
if (ai == null || ai == this) continue; // ignore self-agro as well
ai.HeardShout();
}
}
private bool IsAggrevated() {
bool check = false;
// has it been a while since i've been hit:
if (timeSinceHitByPlayer < aggroCooldownTime) check = true;
// has it been a while since i've heard a shout
if (timeSinceHeardShout < aggroCooldownTime) check = true;
// has it been a while since i've seen the player
if (timeSinceLastSawPlayer < aggroCooldownTime) check = true;
// else, i'm chill
print($"{gameObject.name} aggrevated state is {check}");
return check;
}
// add some gizmos which show up in the Scene
// called by Unity
private void OnDrawGizmosSelected() {
Gizmos.color = Color.blue;
// the chase distance of an enemy
Gizmos.DrawWireSphere(transform.position, chaseDistance);
}
}
}```