[BUG LOCATION FOUND, YET TO BE SOLVED, But please check next comment for a theoretical question that I have]
I promise you, if I knew the source of it, I wouldn’t have asked the question…
This is the part where I apologize in advance, and kindly seek your help to help me fix any false code here, because… well… I’m trying my best, but I genuinely have no idea how else to make this happen
I apologize for this, but after 10 hours since I wrote this post, I genuinely still can’t identify where the issue is. I have my suspicions on 3 different scripts, ‘EnemyStateMachine.cs’, ‘AggroGroup.cs’ and ‘RespawnManager.cs’. Whenever you have the time @Brian_Trotter please just have a glimpse and see if you can help me set the hostility of non-hostile respawnable enemies to true, when they respawn (if, and only if, one of the group members is under attack, so they can go and defend their buddies):
‘AggroGroup.cs’ (nearly everything important for this issue is in ‘OnTakenHit’ - BE WARNED THOUGH, THE LAST ‘else-if’ STATEMENT OF ‘OnTakenHit’ IS UNREACHABLE CODE FOR SOME REASON…!):
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;
using RPG.States.Player;
using System;
namespace RPG.Combat
{
public class AggroGroup : MonoBehaviour
{
[Header("This script aggrevates a group of enemies,\n if you attack one of their friends.\n The player must be in their 'PlayerChasingRange' though\n otherwise they won't try attacking you")]
[SerializeField] List<EnemyStateMachine> enemies = new List<EnemyStateMachine>();
[Tooltip("Set this to true only for groups of enemies that you want naturally aggressive towards the player")]
[SerializeField] bool groupHatesPlayer = false;
[Tooltip("Set to true only if you want enemies to forget any problems they had with the player when he dies")]
[SerializeField] bool resetIfPlayerDies = false; // this is left as a boolean because it allows us to choose which aggroGroup members reset their settings on players' death, and which groups may not reset themselves on death (depending on your games' difficulty)
[SerializeField] List<EnemyStateMachine> unassignedEnemies;
private Health playerHealth;
/// <summary>
/// At the start of the game
/// </summary>
private void Start()
{
// Ensures guards are not active to fight you, if you didn't trigger them:
if (groupHatesPlayer) Activate(true);
else foreach (EnemyStateMachine enemy in enemies)
{
enemy.SetHostile(enemy.GetInitialHostility);
}
}
public void OnEnable()
{
if (resetIfPlayerDies)
{
playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);
}
}
public void OnDisable()
{
playerHealth.onDie.RemoveListener(ResetGroupMembers);
}
/// <summary>
/// Called when the player dies, and everyone is reset to their original state (resetIfPlayerDies must be set to true)
/// </summary>
public void ResetGroupMembers()
{
if (groupHatesPlayer) Activate(true);
else if (playerHealth != null)
{
foreach (EnemyStateMachine enemy in enemies)
{
enemy.SetHostile(enemy.GetInitialHostility);
}
}
}
/// <summary>
/// Used when you're sure the WHOLE GROUP either likes you, or hates you (otherwise use 'EnemyStateMachine.SetHostile' for individuals)
/// </summary>
/// <param name="shouldActivate"></param>
public void Activate(bool shouldActivate)
{
enemies.RemoveAll(enemy => enemy == null || enemy.IsDestroyed());
foreach (EnemyStateMachine enemy in enemies)
{
enemy.SetHostile(shouldActivate);
}
}
public void AddFighterToGroup(EnemyStateMachine enemy)
{
// If you got the fighter you want to add on your list, return:
if (enemies.Contains(enemy)) return;
// For other fighters on the list, add them:
enemy.SetAggroGroup(this);
enemies.Add(enemy);
enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
}
public void RemoveFighterFromGroup(EnemyStateMachine enemy)
{
// if the enemy is gone, don't try removing him again:
if (enemy == null) return;
// else Remove fighters from the list
enemy.SetAggroGroup(null);
enemies.Remove(enemy);
enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
}
public bool HasMember(EnemyStateMachine enemy)
{
return enemies.Contains(enemy);
}
public List<EnemyStateMachine> GetGroupMembers()
{
if (enemies == null) return null;
else return enemies;
}
/* void OnTakenHit(GameObject instigator) // basic functionality, to get all enemies in the AggroGroup to hunt down the player when he's nearby
{
foreach (EnemyStateMachine enemy in enemies)
{
if (enemy != null && enemy.GetAggroGroup() == this) enemy.SetHostile(true, instigator);
}
} */
// ----------------- // Use this function if you don't want Enemies to intelligently find out other group members to fight with, and all of them just aim for one guy ------------------
/* void OnTakenHit(GameObject instigator)
{
foreach (EnemyStateMachine enemy in enemies)
{
if (enemy != null && enemy.GetAggroGroup() == this && instigator != null && instigator.TryGetComponent(out EnemyStateMachine instigatorStateMachine) && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() == this)
{
// if the victim is in the same group as the instigator, which is an enemy state machine,
// this area runs
Debug.Log("Accidental attack from within the group. Ignore");
}
else if (enemy != null && enemy.GetAggroGroup() == this)
{
// if the victim is from a different group, but the Player/another foreign NPC attacks, you run this
Debug.Log("Attack from outside, entire group fights back");
enemy.SetHostile(true, instigator);
}
}
} */
// ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
// ---------------------------------- Use this function if you want Superior Intelligence, where each enemy can intelligently identify enemies in the opposite fighting group and attack them accordingly (still in the works) ----------------------
void OnTakenHit(GameObject instigator)
{
// Check if the instigator is the player
if (instigator.GetComponent<PlayerStateMachine>())
{
// If the instigator is in player chasing range, then all enemies in this AggroGroup, within Player Chasing Range, with no opponent, shall aim for the player (regardless of Hostility):
foreach (EnemyStateMachine enemy in enemies.Where(enemy => enemy != null && enemy.GetAggroGroup() == this && enemy.IsInPlayerChasingRange() && enemy.GetOpponent() == null))
{
// Enemy must be free, and within range:
if (enemy.GetOpponent() == null && !enemy.GetHasOpponent() && enemy.IsInPlayerChasingRange()) {
// First, delete the invaded-by-player-group's data, and then get them to focus on the player:
enemy.SetHostile(true);
enemy.SetOpponent(instigator.gameObject);
enemy.SetHasOpponent(true);
Debug.Log($"{enemy.gameObject.name} is now hostile towards the player.");
}
}
return;
}
// if the hit is from an intruder, and two enemies are fighting it out, get them both to cut the fight
// and the closest one to the intruder shall focus on fighting the intruder (and if the further one is still in 'PlayerChasingRange'
// of the intruder, he can fight the instigator as well):
// Get the (instigator) attacker's aggro group
AggroGroup attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>()?.GetAggroGroup();
// Find unassigned enemies in this aggroGroup:
List<EnemyStateMachine> unassignedEnemies = enemies.Where(enemy => enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent).ToList();
foreach (EnemyStateMachine enemy in enemies)
{
// Find the nearest enemy from the attacker's group
EnemyStateMachine nearestEnemy = null;
if (attackerAggroGroup != null && attackerAggroGroup != this)
{
nearestEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
Debug.Log($"nearestEnemy is called from unassigned members of opposing group");
}
else
{
nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies);
Debug.Log($"nearestEnemy is called through unassignedEnemies of this group");
}
if (nearestEnemy != null)
{
// Set both enemies as hostile towards each other
enemy.SetHostile(true, nearestEnemy.gameObject);
enemy.SetOpponent(nearestEnemy.gameObject);
enemy.SetHasOpponent(true);
nearestEnemy.SetHostile(true, enemy.gameObject);
nearestEnemy.SetOpponent(enemy.gameObject);
nearestEnemy.SetHasOpponent(true);
// unassignedEnemies.Remove(nearestEnemy);
Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name}, as the nearest unassigned enemy.");
}
else
{
// If no enemy found, set closest enemy as opponent
EnemyStateMachine closestEnemy = GetClosestEnemy(enemy);
if (closestEnemy != null)
{
// Set both enemies as hostile towards each other
enemy.SetHostile(true, closestEnemy.gameObject);
enemy.SetOpponent(closestEnemy.gameObject);
enemy.SetHasOpponent(true);
closestEnemy.SetHostile(true, enemy.gameObject);
closestEnemy.SetOpponent(enemy.gameObject);
closestEnemy.SetHasOpponent(true);
Debug.Log($"{enemy.gameObject.name} is now hostile towards {closestEnemy.gameObject.name} as the closest enemy.");
}
}
}
}
EnemyStateMachine GetNearestUnassignedEnemy(EnemyStateMachine enemy, List<EnemyStateMachine> unassignedEnemies)
{
EnemyStateMachine nearestUnassignedEnemy = null;
float nearestDistance = Mathf.Infinity;
foreach (EnemyStateMachine unassignedEnemy in unassignedEnemies)
{
if (unassignedEnemy != null && !unassignedEnemy.GetHasOpponent() && unassignedEnemy.GetAggroGroup() != this)
{
float distance = Vector3.Distance(enemy.transform.position, unassignedEnemy.transform.position);
if (distance < nearestDistance)
{
nearestDistance = distance;
nearestUnassignedEnemy = unassignedEnemy;
}
}
}
return nearestUnassignedEnemy;
}
EnemyStateMachine GetClosestEnemy(EnemyStateMachine enemy)
{
EnemyStateMachine closestEnemy = null;
float closestDistance = Mathf.Infinity;
foreach (EnemyStateMachine otherEnemy in enemies)
{
if (otherEnemy != null && otherEnemy != enemy && otherEnemy.GetAggroGroup() != this)
{
float distance = Vector3.Distance(enemy.transform.position, otherEnemy.transform.position);
if (distance < closestDistance)
{
closestEnemy = otherEnemy;
closestDistance = distance;
}
}
}
return closestEnemy;
}
// --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
}
}
‘RespawnManager.cs’ (this is the script where I want to make the changes, within the ‘Test Area’ in ‘Respawn()’, under the assignment of Patrol Paths. All I’m trying to do, is get the hostility of any non-Initially Hostile enemies to be hostile, if one of their group members is under attack, and they are not assigned to be fighting anyone):
using System.Collections.Generic;
using GameDevTV.Saving;
using RPG.Control;
using UnityEngine;
using RPG.Combat;
using Newtonsoft.Json.Linq;
using RPG.States.Enemies;
using System.Collections;
using System;
namespace RPG.Respawnables
{
[RequireComponent(typeof(JSONSaveableEntity))]
public class RespawnManager : MonoBehaviour, IJsonSaveable
{
[SerializeField] EnemyStateMachine spawnableEnemy; // prefab of the enemy (was 'AIController.cs' type previously)
[SerializeField] private float hideTime; // time before hiding our dead character
[SerializeField] private float respawnTime; // time before respawning our hidden dead character, as another alive, respawned character
[SerializeField] PatrolPath patrolPath; // the path our character will follow, from his spawn point
[SerializeField] AggroGroup aggroGroup; // aggrevated group of guards, based on wrong dialogue player has said
[SerializeField] bool hasBeenRestored; // checks if the enemy has been restored before fading in from the main menu, or a load scene, or not
// private AIController spawnedEnemy; // changed to EnemyStateMachine type below
// private EnemyStateMachine spawnedEnemy; // in-game instance of the enemy
// TEST (TimeKeeper)
private double destroyedTime;
private TimeKeeper timeKeeper;
// --------------------------- NOTE: RestoreState() occurs BEFORE Start(), hence we need to change Start() to Awake() --------------
private void Awake()
{
// TEST
timeKeeper = TimeKeeper.GetTimeKeeper();
// Check if the Enemy has been restored first or not, prior to Respawning him (ensuring RestoreState(), which occurs first, works properly)
if (!hasBeenRestored) Respawn();
}
// --------------------------------------------------------------------------------------------------------------------------------
private void Respawn()
{
var spawnedEnemy = GetSpawnedEnemy();
if (spawnedEnemy)
{
// Dude is not dead no longer, so delete his previous 'onDeath' record after he's respawned
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
}
foreach (Transform child in transform)
{
// Start the Respawn by deleting any existing gameObjects
Destroy(child.gameObject);
}
// Respawn the enemy, and parent the enemy to our respawnManagers' transform
spawnedEnemy = Instantiate(spawnableEnemy, transform);
// (TEST BLOCK BELOW, ADDED BY BAHAA INDIVIDUALLY - SUCCESS):
// If the enemy has a weapon that's supposed to be in his hand, make him wear it:
if (spawnedEnemy.GetComponent<Fighter>().GetCurrentWeaponConfig() != null)
{
WeaponConfig enemyWeaponConfig = spawnedEnemy.GetComponent<Fighter>().currentWeaponConfig;
spawnedEnemy.GetComponent<Fighter>().AttachWeapon(enemyWeaponConfig);
}
// Get the spawned/respawned enemies' health, and listen for death notifications
spawnedEnemy.Health.onDie.AddListener(OnDeath);
if (patrolPath != null)
{
// THE PROBLEM OF HOSTILITY IS COMING FROM HERE, ACCESSING THE PATROLLING PATH BEFORE DEALING WITH AGGROGROUPS!!!
// (SO FIX THE IF-STATEMENT SURROUNDING THIS CODE...!)
Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
spawnedEnemy.AssignPatrolPath(patrolPath);
spawnedEnemy.SwitchState(new EnemyIdleState(spawnedEnemy));
}
else
{
Debug.Log($"No Patrol Path to assign");
}
// --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------
if (aggroGroup != null)
{
aggroGroup.AddFighterToGroup(spawnedEnemy);
// -------------------------------- TEST AREA ------------------------------------------------------------------------------------------------------------------------
// get the AggroGroup on Respawn, so when the enemy returns to life, he can go through the list of allies, and if any of them are under attack, he can try fight with them
spawnedEnemy.SetAggroGroup(spawnedEnemy.GetAggroGroup());
if (spawnedEnemy.GetAggroGroup() != null)
{
foreach (EnemyStateMachine allyEnemy in spawnedEnemy.GetAggroGroup().GetGroupMembers())
{
if (allyEnemy != spawnedEnemy && allyEnemy.GetOpponent() != null && allyEnemy.GetOpponent().GetComponent<EnemyStateMachine>().GetAggroGroup() != spawnedEnemy.GetAggroGroup())
{
// aim for whoever is attacking your allies, and then break after finding the first one:
spawnedEnemy.SetOpponent(allyEnemy.GetOpponent());
spawnedEnemy.SetHasOpponent(true);
spawnedEnemy.SetHostile(true, allyEnemy.GetOpponent());
Debug.Log($"Enemy Respawned and is supposed to fight... SetHostile set to {spawnedEnemy.IsHostile}");
break;
}
}
}
// -------------------------------------- END OF TEST AREA -----------------------------------------------------------------------------------------------------------
if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
{
dialogueAggro.SetAggroGroup(aggroGroup);
}
}
// ---------------------------------------------------------------------------------------------------------
}
private IEnumerator HideCharacter()
{
var spawnedEnemy = GetSpawnedEnemy();
if (spawnedEnemy == null) yield break;
spawnedEnemy.transform.SetParent(null);
yield return new WaitForSecondsRealtime(hideTime);
Destroy(spawnedEnemy.gameObject);
}
void OnDeath()
{
var spawnedEnemy = GetSpawnedEnemy();
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
spawnedEnemy.SwitchState(new EnemyDeathState(spawnedEnemy)); // THIS LINE IS A TEST (Brutally forcing enemyDeathState if the enemy is dead)
StartCoroutine(HideCharacter());
destroyedTime = timeKeeper.GetGlobalTime();
StartCoroutine(WaitAndRespawn());
if (aggroGroup != null)
{
aggroGroup.RemoveFighterFromGroup(spawnedEnemy);
}
}
private IEnumerator WaitAndRespawn()
{
var elapsedTime = (float)(timeKeeper.GetGlobalTime() - destroyedTime);
yield return new WaitForSecondsRealtime(respawnTime - elapsedTime);
Respawn();
}
private bool IsDead()
{
var spawnedEnemy = GetSpawnedEnemy();
return spawnedEnemy == null || spawnedEnemy.Health.IsDead();
}
private EnemyStateMachine GetSpawnedEnemy()
{
return GetComponentInChildren<EnemyStateMachine>();
}
public JToken CaptureAsJToken()
{
JObject state = new JObject();
IDictionary<string, JToken> stateDict = state;
// TEST (Adding data to the JObject Dictionary):
var isDead = IsDead();
var data = new RespawnData(destroyedTime, isDead);
stateDict["RespawnData"] = JToken.FromObject(data);
// we only care about data of alive enemies
if (!isDead)
{
var spawnedEnemy = GetSpawnedEnemy();
foreach (IJsonSaveable JSONSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
{
JToken token = JSONSaveable.CaptureAsJToken();
string component = JSONSaveable.GetType().ToString();
Debug.Log($"{name} Capture {component} = {token.ToString()}");
stateDict[component] = token;
}
}
return state;
}
public void RestoreFromJToken(JToken s)
{
JObject state = s.ToObject<JObject>();
IDictionary<string, JToken> stateDict = state;
var data = default(RespawnData);
if (stateDict.TryGetValue("RespawnData", out var dataToken))
{
data = dataToken.ToObject<RespawnData>();
}
var isDead = data.IsDead;
destroyedTime = data.DestroyedTime;
// Should be dead
if (isDead && !IsDead())
{
Debug.Log("Should be dead, but isn't...");
var spawnedEnemy = GetSpawnedEnemy();
Debug.Log($"Listeners before: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
Debug.Log($"Listeners after: {spawnedEnemy.Health.onDie.GetPersistentEventCount()}");
Debug.Log($"Health Before: {spawnedEnemy.Health.GetHealthPoints()}");
spawnedEnemy.Health.Kill();
Debug.Log($"Health After: {spawnedEnemy.Health.GetHealthPoints()}");
StartCoroutine(WaitAndRespawn());
if (aggroGroup != null)
{
aggroGroup.RemoveFighterFromGroup(spawnedEnemy);
}
StartCoroutine(HideCharacter());
// HideCharacter();
Debug.Log($"Spawned Enemy: {GetSpawnedEnemy()}");
}
else if (isDead && IsDead())
{
Debug.Log("Should be dead, and is indeed dead...");
StopAllCoroutines();
StartCoroutine(WaitAndRespawn());
StartCoroutine(HideCharacter());
// HideCharacter();
}
// Should be alive
else if (!isDead && IsDead())
{
Debug.Log("Shouldn't be dead, but is dead...");
Respawn();
LoadEnemyState(stateDict);
}
else
{
Debug.Log("Shouldn't be dead, and isn't dead...");
LoadEnemyState(stateDict);
}
}
private void LoadEnemyState(IDictionary<string, JToken> stateDict)
{
var spawnedEnemy = GetSpawnedEnemy();
foreach (IJsonSaveable jsonSaveable in spawnedEnemy.GetComponents<IJsonSaveable>())
{
string component = jsonSaveable.GetType().ToString();
if (stateDict.ContainsKey(component))
{
// NORMAL CODE (Don't delete if test failed):
Debug.Log($"{name} Restore {component} => {stateDict[component].ToString()}");
jsonSaveable.RestoreFromJToken(stateDict[component]);
}
}
}
}
[Serializable]
public struct RespawnData
{
public bool IsDead;
public double DestroyedTime;
public RespawnData(double destroyedTime, bool isDead)
{
IsDead = isDead;
DestroyedTime = destroyedTime;
}
}
}
and ‘EnemyStateMachine.cs’ (this one will be quite important in countless aspects, so I placed the entire script here):
using RPG.Attributes;
using RPG.Combat;
using RPG.Control;
using RPG.Movement;
using RPG.Stats;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
using System.Linq;
using RPG.States.Player;
using Unity.VisualScripting.Dependencies.Sqlite;
namespace RPG.States.Enemies {
public class EnemyStateMachine : StateMachine, ITargetProvider
{
[field: SerializeField] public Animator Animator {get; private set;}
[field: SerializeField] public CharacterController CharacterController {get; private set;}
[field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
[field: SerializeField] public NavMeshAgent Agent {get; private set;}
[field: SerializeField] public PatrolPath PatrolPath {get; private set;}
[field: SerializeField] public Fighter Fighter {get; private set;}
[field: SerializeField] public BaseStats BaseStats {get; private set;}
[field: SerializeField] public Health Health {get; private set;}
[field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}
[field: SerializeField] public float FieldOfViewAngle {get; private set;} = 90.0f;
[field: SerializeField] public float MovementSpeed {get; private set;} = 4.0f;
[field: SerializeField] public float RotationSpeed {get; private set;} = 45f;
[field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.1f;
[field: SerializeField] public float AnimatorDampTime {get; private set;} = 0.1f;
[field: Tooltip("When the player dies, this is where he respawns to")]
[field: SerializeField] public Vector3 ResetPosition {get; private set;}
// TEST - Aggrevating others in the 'PlayerChaseRange' range:
[field: Tooltip("Regardless of whether the enemy is part of an AggroGroup or not, activating this means every enemy in 'PlayerChasingRange', surrounding the attacked enemy, will aggrevate towards the player for 'CooldownTimer' seconds the moment the enemy is hit, and then they'll be quiet if the player doesn't fight them back. If the player attacks the enemies though, the longer the fight goes on for, the longer they will aggrevate towards him (because CooldownTimers' append is set to true))")]
[field: SerializeField] public bool AggrevateOthers { get; private set; } = false;
// TEST - Testing for Aggrevated Enemy toggling (called in 'OnTakenHit' below, 'EnemyPatrolState.cs', 'EnemyDwellState.cs' and 'EnemyIdleState.cs'):
[field: Tooltip("Is this Enemy supposed to hate and want to kill the player?")]
[field: SerializeField] public bool IsHostile {get; private set;} // for enemies that have hatred towards the player
[field: Tooltip("For how long further will the enemy be mad and try to attack the player?")]
[field: SerializeField] public float CooldownTimer {get; private set;} = 3.0f; // the timer for which the enemies will be angry for
// TEST - Who is the Last Atacker on this enemy? (NPC Fights):
[field: Tooltip("The last person who attacked this enemy, works with 'OnDamageInstigator' in 'Health.TakeDamage()'")]
public GameObject LastAttacker {get; private set;}
// TEST - How many accidental hits can the AggroGroup member take in, before fighting back:
[field: Tooltip("How many accidental hits can this enemy take, from any friend in the same AggroGroup, before eventually fighting back? (hard-coded limit to 1 accidental hit. Second one triggers a fight). Handled in 'EnemyAttackingState.cs'\n(Will reset if another NPC/Player attack this enemy, or the LastAttacker is dead\n(DO NOT TOUCH, IT AUTOMATICALLY UPDATES ITSELF!))")]
[field: SerializeField] public int accidentHitCounter {get; private set;}
// Function:
public bool IsAggro => CooldownTokenManager.HasCooldown("Aggro"); // enemies are aggrevated through a timer-check. In other words, they can only be angry for some time
public float PlayerChasingRangedSquared {get; private set;}
public GameObject Player {get; private set;}
public Blackboard Blackboard = new Blackboard();
[field: Tooltip("When the game starts, is this enemy, by default nature, angry towards the player?")]
[field: SerializeField] public bool InitiallyHostile {get; private set;}
[field: Header("Don't bother trying to change this variable anymore.\nWhen the game starts, It's controlled by either 80% of the Target Range of the Enemies' Weapon,\nor 100% of the weapon Range of the Enemies' Weapon, depending on who is higher.\nAll changes are done in 'EnemyStateMachine.HandleWeaponChanged,\nand subscribed to 'Fighter.OnWeaponChanged' in 'EnemyStateMachine.Awake'")]
[field: SerializeField] public float PlayerChasingRange { get; private set; } = 10.0f;
[field: Header("Do NOT touch this variable, it is here for debugging and is automatically updating itself!")]
[field: SerializeField] public AggroGroup AggroGroup { get; private set; }
[field: Header("Does this enemy have an opponent, or should an aggrevated NPC hunting enemies in the aggroGroup of this enemy hunt him down?")]
[field: SerializeField] public bool HasOpponent {get; private set;}
[field: Header("Who is the opponent this NPC is facing?")]
[field: SerializeField] public GameObject Opponent {get; private set;}
[field: Tooltip("AUTOMATICALLY UPDATES... This variable tells the developer who the enemy aggroGroup is, based on who this enemy is attacking, so the rest can follow lead (based on this variable from the first enemy on the list)")]
[field: SerializeField] public AggroGroup EnemyAggroGroup {get; private set;}
// This event is triggered in 'EnemyChasingState.cs', when the player leaves the enemies' Chasing Range, so he can go hunt down other enemies:
public event System.Action<EnemyStateMachine> OnPlayerOutOfChaseRange;
private void OnValidate()
{
if (!Animator) Animator = GetComponentInChildren<Animator>();
if (!CharacterController) CharacterController = GetComponent<CharacterController>();
if (!ForceReceiver) ForceReceiver = GetComponent<ForceReceiver>();
if (!Agent) Agent = GetComponent<NavMeshAgent>();
if (!Fighter) Fighter = GetComponent<Fighter>();
if (!BaseStats) BaseStats = GetComponent<BaseStats>();
if (!Health) Health = GetComponent<Health>();
if (!CooldownTokenManager) CooldownTokenManager = GetComponent<CooldownTokenManager>();
}
private void Awake()
{
if (Fighter) Fighter.OnWeaponChanged += HandleWeaponChanged;
}
private void Start()
{
// Initially, enemy hostility = enemy initialHostility
IsHostile = InitiallyHostile;
Agent.updatePosition = false;
Agent.updateRotation = false;
ResetPosition = transform.position;
PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
Player = GameObject.FindGameObjectWithTag("Player");
Blackboard["Level"] = BaseStats.GetLevel(); // immediately stores the enemies' combat level in the blackboard (useful for determining the chances of a combo, based on the enemies' attack level, in 'EnemyAttackingState.cs', for now...)
// The following check ensures that if we kill an enemy, save the game, quit and then return later, he is indeed dead
// (the reason it's here is because 'RestoreState()' happens between 'Awake()' and 'Start()', so to counter for the delay, we do it in start too)
if (Health.IsDead()) SwitchState(new EnemyDeathState(this));
else SwitchState(new EnemyIdleState(this));
// When the script holder is killed by another NPC, from another AggroGroup, this event is called:
Health.onDie.AddListener(OnDie);
Health.onDie.AddListener(() =>
{
SwitchState(new EnemyDeathState(this));
});
/* Health.onResurrection.AddListener(() => {
Debug.Log("onResurrection Invoked");
SwitchState(new EnemyIdleState(this));
}); */
Health.OnDamageTaken += () =>
{
// TEST (if statement and its contents only - the else statement has nothing to do with this):
if (AggrevateOthers)
{
foreach (Collider collider in Physics.OverlapSphere(transform.position, PlayerChasingRange).Where(collider => collider.TryGetComponent(out EnemyStateMachine enemyStateMachine)))
{
collider.GetComponent<EnemyStateMachine>().CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
}
}
// CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
};
if (AggroGroup == null) GetAggroGroup();
// The enemy will aim next to whoever attacked him last (if it's the player, aim for him. If it's another enemy, aim for him too)
Health.OnDamageInstigator += SetLastAttacker;
ForceReceiver.OnForceApplied += HandleForceApplied;
}
public void TriggerOnPlayerOutOfChaseRange()
{
OnPlayerOutOfChaseRange?.Invoke(this);
Debug.Log("OnPlayerOutOfChaseRange event invoked");
// if the opponent was the player before we got out of range,
// reset the data so the enemy can go find someone else to fight
// (Something is wrong here... Redo this part when given the chance):
/* if (Opponent != null && Opponent.GetComponent<PlayerStateMachine>())
{
SetHostile(GetInitialHostility);
SetOpponent(null);
SetHasOpponent(false);
} */
}
/// <summary>
/// in NPC fights between AggroGroups, when one enemy dies, this function is executed
/// (so that the NPC can start seeking other opponents)
/// </summary>
public void OnDie()
{
// all enemies who had this guy set to an opponent should erase the data, so they can go targeting again after his death:
foreach (EnemyStateMachine enemy in FindObjectsOfType<EnemyStateMachine>())
{
if (enemy.GetOpponent() == gameObject) {
if (enemy.GetInitialHostility) {enemy.SetHostile(true);} else {enemy.SetHostile(false);}
enemy.SetHasOpponent(false);
enemy.SetOpponent(null);
}
}
// solve the following problem: When this object dies, the LastAttacker will get the EnemyAggroGroup, and search for other enemies to attack:
// if the Last Attacker is still alive, but I am dead, then he can get my AggroGroup:
if (LastAttacker != null && LastAttacker.GetComponent<EnemyStateMachine>())
{
LastAttacker.GetComponent<EnemyStateMachine>().EnemyAggroGroup = GetAggroGroup();
// When you got the AggroGroup, check for the LastAttacker. When you found the LastAttacker that killed me, assuming he's alive and he's my enemy, he can get the next enemy to kill:
foreach (EnemyStateMachine enemy in FindObjectsOfType<EnemyStateMachine>())
{
if (enemy == LastAttacker.GetComponent<EnemyStateMachine>() && !enemy.GetComponent<Health>().IsDead() && enemy.GetAggroGroup() != GetAggroGroup())
{
// if the enemy that killed me, from the opposing AggroGroup, is still alive after my death,
// he shall find the closest enemy from my team, and aim for him:
enemy.SetHostile(true, GetClosestEnemyInEnemyAggroGroup(enemy).gameObject);
}
}
}
// if my killer is still alive, he shall erase my data after my death:
if (LastAttacker != null && LastAttacker.GetComponent<EnemyStateMachine>())
{
LastAttacker.GetComponent<EnemyStateMachine>().ClearOpponent();
LastAttacker.GetComponent<EnemyStateMachine>().SetHasOpponent(false);
LastAttacker.GetComponent<EnemyStateMachine>().ClearEnemyAggroGroup();
LastAttacker.GetComponent<EnemyStateMachine>().SetHostile(LastAttacker.GetComponent<EnemyStateMachine>().GetInitialHostility, gameObject);
LastAttacker.GetComponent<EnemyStateMachine>().SwitchState(new EnemyIdleState(LastAttacker.GetComponent<EnemyStateMachine>()));
}
}
public EnemyStateMachine GetClosestEnemyInEnemyAggroGroup(EnemyStateMachine enemy)
{
EnemyStateMachine closestEnemy = null;
float closestDistance = Mathf.Infinity;
foreach (EnemyStateMachine otherEnemy in FindObjectsOfType<EnemyStateMachine>())
{
if (otherEnemy != null && otherEnemy != enemy && otherEnemy.GetAggroGroup() != enemy.GetAggroGroup())
{
float distance = Vector3.Distance(enemy.transform.position, otherEnemy.transform.position);
if (distance < closestDistance)
{
closestEnemy = otherEnemy;
closestDistance = distance;
}
}
}
return closestEnemy;
}
EnemyStateMachine GetNearestUnassignedEnemyInEnemyAggroGroup(EnemyStateMachine enemy)
{
EnemyStateMachine nearestUnassignedEnemyInEnemyAggroGroup = null;
float nearestDistance = Mathf.Infinity;
foreach (EnemyStateMachine otherEnemy in FindObjectsOfType<EnemyStateMachine>())
{
if (otherEnemy != null && otherEnemy != enemy && otherEnemy.GetAggroGroup() != enemy.GetAggroGroup())
{
float distance = Vector3.Distance(enemy.transform.position, otherEnemy.transform.position);
if (distance < nearestDistance)
{
nearestUnassignedEnemyInEnemyAggroGroup = otherEnemy;
nearestDistance = distance;
}
}
}
return nearestUnassignedEnemyInEnemyAggroGroup;
}
AggroGroup ClearEnemyAggroGroup()
{
return EnemyAggroGroup = null;
}
public void AssignPatrolPath(PatrolPath newPatrolPath)
{
PatrolPath = newPatrolPath;
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, PlayerChasingRange);
}
private void OnEnable()
{
Health.OnTakenHit += OnTakenHit;
}
private void OnDisable()
{
Health.OnTakenHit -= OnTakenHit;
}
private void OnTakenHit(GameObject instigator)
{
if (instigator == null) return; // nobody touched you... you can't take a hit that way!
if (GetOpponent() != instigator && !instigator.GetComponent<PlayerStateMachine>())
{
// if you got hit by a new instigator (who is not the player), wipe out the old data and prepare for the new one:
SetHostile(false);
SetOpponent(null);
SetHasOpponent(false);
ClearEnemyAggroGroup();
// when hit, both of you should aim for one another:
if (instigator != null && instigator.GetComponent<EnemyStateMachine>())
{
SetHostile(true, instigator.gameObject);
SetOpponent(instigator.gameObject);
SetHasOpponent(true);
instigator.GetComponent<EnemyStateMachine>().SetHostile(true, this.gameObject);
instigator.GetComponent<EnemyStateMachine>().SetOpponent(this.gameObject);
instigator.GetComponent<EnemyStateMachine>().SetHasOpponent(true);
// if the instigator is part of an AggroGroup, get the data of that group (otherwise return null):
/* if (instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != null)
EnemyAggroGroup = instigator.GetComponent<EnemyStateMachine>().GetAggroGroup();
else EnemyAggroGroup = null; */
}
}
CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true); // 'true' in the end basically allows the enemy to add up to his total anger time
if (!HasOpponent) SetHasOpponent(true);
if (Opponent == null) SetOpponent(instigator.gameObject);
}
// public bool IsAggresive => CooldownTokenManager.HasCooldown("Aggro");
private void HandleForceApplied(Vector3 force)
{
if (Health.IsDead()) return;
// Disable comments below if you want impact states to be random for the enemy:
// float forceAmount = Random.Range(0f, force.sqrMagnitude);
// if (forceAmount > Random.Range(0f, BaseStats.GetLevel())) {
SwitchState(new EnemyImpactState(this));
// }
}
public GameObject GetTarget()
{
// TEST: If the last attacker is not the player, then that's the target:
if (LastAttacker != GameObject.FindWithTag("Player")) return LastAttacker;
// the target for the enemy launching a projectile (which is basically the player in this case)
return Player;
}
/* public void SetHostile(bool IsHostile)
{
this.IsHostile = IsHostile;
} */
/// <summary>
/// Who is this NPC hostile towards, and which AggroGroup do they belong to...?!
/// </summary>
/// <param name="IsHostile"></param>
/// <param name="attacker"></param>
public void SetHostile(bool IsHostile, GameObject attacker = null)
{
this.IsHostile = IsHostile;
if (LastAttacker != null) ClearLastAttacker();
if (attacker != null)
{
SetLastAttacker(attacker);
// (TEST) get the enemyAggroGroup, so that we can easily access the enemies and get each individual enemy in an AggroGroup to hunt a single guy:
// if the attacker is the player, or an enemy with no aggroGroup, or a dead instigator (enemy/player), return 'EnemyAggroGroup' = null:
if (attacker.GetComponent<PlayerStateMachine>() || attacker.GetComponent<EnemyStateMachine>().GetAggroGroup() == null || attacker.GetComponent<Health>().IsDead()) EnemyAggroGroup = null;
// if it's an enemy with an AggroGroup, get that AggroGroup:
else EnemyAggroGroup = attacker.GetComponent<EnemyStateMachine>().GetAggroGroup();
}
}
public bool GetInitialHostility => InitiallyHostile;
public void _ResetPosition()
{
Agent.enabled = false;
CharacterController.enabled = false;
transform.position = ResetPosition;
Agent.enabled = true;
CharacterController.enabled = true;
}
// The code below changes the 'PlayerChasingRange' based on the weapon in-hand. I want the 'PlayerChasingRange' to be what I want, so I commented it out:
private void HandleWeaponChanged(WeaponConfig weaponConfig)
{
if (weaponConfig)
{
PlayerChasingRange = Mathf.Max(weaponConfig.GetTargetRange() * 0.8f, weaponConfig.GetWeaponRange()); // keep the range of pursuit of the enemy below the players' targeting range
PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
}
}
// TEST: Who was the last attacker against the enemy?
public void SetLastAttacker(GameObject instigator)
{
// if the caller is 'SetHostile', there's a chance it's calling it on itself, because the AggroGroup
if (instigator == gameObject) return;
// if whoever the enemy was aiming for is dead, get them off the 'onDie' event listener:
if (LastAttacker != null) {LastAttacker.GetComponent<Health>().onDie.RemoveListener(ClearLastAttacker);}
// if the last attacker is not dead, add them to the list, and then prepare them to be deleted off the list when they die:
LastAttacker = instigator;
LastAttacker.GetComponent<Health>().onDie.AddListener(ClearLastAttacker);
}
// TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
public void ClearLastAttacker()
{
// if the enemy is dead, clear him off the 'LastAttacker', clear the cooldown timer, return to idle state, and ensure no accident hit counter on record (he will forget about what that guy did...):
LastAttacker = null;
CooldownTokenManager.SetCooldown("Aggro", 0f);
SwitchState(new EnemyIdleState(this));
ClearAccidentHitCounter();
}
public AggroGroup GetAggroGroup()
{
if (AggroGroup == null) return null;
return AggroGroup;
}
public void SetAggroGroup(AggroGroup aggroGroup)
{
this.AggroGroup = aggroGroup;
}
public AggroGroup ClearAggroGroup()
{
return AggroGroup = null;
}
// Called in 'EnemyAttackingState.Tick()', to ensure enemies don't accidentally hit one another for too long
public int AddToAccidentHitCounter()
{
return accidentHitCounter++;
}
public int ClearAccidentHitCounter()
{
return accidentHitCounter = 0;
}
public bool GetHasOpponent()
{
return HasOpponent;
}
public GameObject GetOpponent()
{
return Opponent;
}
public void SetHasOpponent(bool hasOpponent)
{
Debug.Log("SetHasOpponent Called");
this.HasOpponent = hasOpponent;
}
public void ClearOpponent()
{
this.Opponent = null;
}
/// <summary>
/// Special Function, created for when the player is in Chase Range of AggroGroup enemies (to avoid enemies far away from chasing the player down)
/// (ONLY USED IN AggroGroup.OnTakenHit() so far)
/// </summary>
/// <returns></returns>
public bool IsInPlayerChasingRange()
{
return Vector3.SqrMagnitude(transform.position - Player.transform.position) <= PlayerChasingRangedSquared;
}
/// <summary>
/// Who is the opponent of this NPC?
/// </summary>
/// <param name="HasOpponent"></param>
/// <param name="attacker"></param>
public void SetOpponent(GameObject attacker = null)
{
Debug.Log("SetOpponent called");
Opponent = attacker;
}
public AggroGroup GetEnemyAggroGroup()
{
return EnemyAggroGroup;
}
}
}
Again, I apologize for dragging you into this, I truly am sorry, but there’s nothing in my capabilities that I can do about it right now… PLEASE HELP ME OUT
Just to make your life a little easier as well, there’s only two spots in my entire code that sets the enemy hostility to false (which I also tried tuning, but it did not work). Both are in ‘EnemyStateMachine.cs’:
// 1. in 'OnDie':
if (enemy.GetInitialHostility) {enemy.SetHostile(true);} else {enemy.SetHostile(false);}
// 2. At the top of 'OnTakenHit':
SetHostile(false);
So far, the only way for me to get an enemy to be hostile, is to actually turn it on in the Inspector itself, and then he reacts… (but even that is flawed, because occasionally, my hostile enemy would also just go and attack members of the opponent group out of thin air for no apparent reason)
If you spot any other flaws in my code (I’m sure there’s a few), please PLEASE let me know about it and how to fix it. I’d very much appreciate that
(I already know this system probably introduced a death glitch somewhere, because occasionally my enemies will resurrect during their time before hiding the character, and rarely I would be dead when I respawn as well… I want to address this next. For now, let’s just take it step by step)
Edit (a few hours later): Something else I did notice can potentially solve the problem, is editing ‘IsHostile’ in ‘EnemyIdleState.Enter’ itself. In other words, “if you can’t find where to fix it, override it at the gate”. So here is my ‘EnemyIdleState.cs’, unmodified:
using RPG.States.Enemies;
public class EnemyIdleState : EnemyBaseState
{
public EnemyIdleState(EnemyStateMachine stateMachine) : base(stateMachine) {}
private bool idleSpeedReached;
public override void Enter()
{
if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;
if (stateMachine.PatrolPath) // if the enemy has a patrol state, switch to that
{
stateMachine.SwitchState(new EnemyPatrolState(stateMachine));
return;
}
stateMachine.Animator.CrossFadeInFixedTime(FreeLookBlendTreeHash, stateMachine.CrossFadeDuration);
}
public override void Tick(float deltaTime)
{
Move(deltaTime);
/* if (IsInChaseRange()) // && IsAggro()) // if you want to consider the optional aggregation, add "&& IsAggro()" to the if statement of this line
{
// if you want the vision system to work, uncomment the if statement below:
// if (CanSeePlayer()) {
stateMachine.SwitchState(new EnemyChasingState(stateMachine));
return;
// }
} */
if (ShouldPursue() || IsAggrevated())
{
stateMachine.SwitchState(new EnemyChasingState(stateMachine));
}
if (!idleSpeedReached)
{
stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f, stateMachine.AnimatorDampTime, deltaTime);
// Due to a bug in 'SetFloat()' that can twist values above and beyond what we need,
// the following 'if' statement is crucial:
if (stateMachine.Animator.GetFloat(FreeLookSpeedHash) < 0.05f)
{
stateMachine.Animator.CrossFadeInFixedTime(IdleHash, stateMachine.CrossFadeDuration);
idleSpeedReached = true;
}
}
}
public override void Exit() {}
}
My idea right now is that since Hostility is somehow set to false from somewhere in the code, and I cant figure out where from, then I will override it to true within ‘EnemyIdleState.cs’, for any respawned enemy, if any of his group members are under attack, UNDER VERY VERY VERY SPECIFIC Rules that can only be met by Respawned Enemies in the same AggroGroup… I’m just struggling to translate that to code
and… this was my final attempt of tuning ‘EnemyIdleState.Enter()’ to try and fix the problem, and at this point in time, I desperately need help, because I tried out EVERYTHING I can think of, but it didn’t work out well for me… The following Code block goes into the very very top of ‘EnemyIdleState.Enter()’:
// to set non-hostile enemies to be true, if a group member is under attack,
// first make sure this enemy is part of an AggroGroup, and make sure it's not empty:
if (stateMachine.GetAggroGroup() != null && stateMachine.GetAggroGroup().GetGroupMembers().Count > 1 && stateMachine.GetComponentInParent<RespawnManager>().HasJustRespawned())
{
// if this enemy has an AggroGroup, go through all the enemies in the group
// (and if one of them is under attack, get the nearest enemy you can find, and attack them):
foreach (EnemyStateMachine enemy in stateMachine.GetAggroGroup().GetGroupMembers())
{
if (enemy.GetOpponent() && enemy.GetHasOpponent()) continue;
stateMachine.SetHasOpponent(true);
stateMachine.SetOpponent(stateMachine.GetClosestEnemyInEnemyAggroGroup(stateMachine).gameObject);
stateMachine.SetHostile(true, stateMachine.GetClosestEnemyInEnemyAggroGroup(stateMachine).gameObject);
}
}
To limit everyone from just having ‘SetHostile(true)’ here, I tried introducing new rules for the ‘RespawnManager.cs’, so that only respawned enemies can go through this verification… In simple terms, what I tried to do was ensure that only Respawned Enemies can go through this, and if their friends are under attack and are hostile, and these respawned enemies have no opponent, set the nearest opponent they can find to be their enemy, and then fire away against them, and there’s that
(P.S: “SetHasOpponent()” and “SetOpponent” are what I call as “Dummy variables”. They’re not used in the game itself, but they give me an idea of who is aiming for who, so I understand their behaviour, to have an idea of what still needs to be fixed)
However, it failed… This is where I kindly ask for help because I truly am clueless of how else to flick that ‘SetHostile()’ variable on through code, at the right time (i.e: when you respawned, and your friends are under attack from opposing enemies)
AND… I FOUND WHERE THE BUG WAS COMING FROM, AND I NEVER WOULD HAVE THOUGHT OF IT FOR A THOUSAND YEARS AHEAD IF IT WEREN’T FOR A DEBUGGER IN ‘ENEMYIDLESTATE.CS’
Apparently I forgot to re-assign this line in ‘EnemyStateMachine.Start()’, at the very top of the function:
I DID NOT KNOW THAT THIS SCRIPT WAS BEING EXECUTED EVERY SINGLE TIME OUR ENEMY RESPAWNS, AND THIS LEFT ME CONFUSED FOR A VERY, VERY, VERY, VERY LONG TIME! (I LITERALLY THOUGHT IT ONLY RUNS WHEN THE GAME STARTS…!)