OK so… I found out where the problem above is coming from, and it’s from this line:
// The following line is correct, but for the system to fully work as expected, the Saving and Restoring System for 'AggroGroup.cs' must be developed!
if (!playerAggroGroup.GetGroupMembers().Contains(this.GetComponent<EnemyStateMachine>())) return; // if this script holder is no longer part of the Player's AggroGroup, don't try deleting him again
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 2");
It’s a safety check I created in ‘AIAggroGroupSwitcher.cs’ in both the adder and remover of the NPC from the player’s AggroGroup, which was blocking the entire thing from proceeding
and the only natural solution to solve this problem, moving forward, is the inevitable fact that now I have to create a saving and restoring system for my ‘AggroGroup.cs’ script, essentially saving and restoring 4 different variables:
- A list of ‘enemies’. This is the most important one
- A boolean called ‘groupHatesPlayer’, something to aggrevate the entire team if the player touches one of them (I don’t even know why this is here anymore, considering I replaced it in ‘OnTakenHit’, but… yeah, let’s keep it for now)
- ‘resetIfPlayerDies’, to reset everyone when the player dies (again, no clue why it’s here, as ‘PlayerStateMachine.OnDie’ and ‘Respawner.ResetEnemies’ both overtake that job)
- ‘player’ of type ‘PlayerStateMachine.cs’, so we know which group has the player (it’s my way of identifying the player’s AggroGroup)
Whenever you see this, Please help me code it @Brian_Trotter (because I genuinely have no clue with this one…)
I tried copy pasting, and then fine-tuning the ‘QuestStatus.cs’ attempt, but the compiler complained that I dont have a ‘CaptureAsJToken()’ function in my ‘EnemyStateMachine.cs’ script, so I assumed I must’ve missed out something
This is the absolutely final step for this topic, then I’ll move on to something else
Edit 1: I asked ChatGPT for a little bit of help with this one, because I truly am clueless on how to code a Saving System, and here’s what it came up with (which didn’t work, by the way, but it’s a starting point), so as to reduce the amount of time we spend fixing this problem:
public JToken CaptureAsJToken()
{
var state = new JObject();
// Capture enemies as their unique IDs (Instance IDs):
JArray enemiesArray = new JArray();
foreach (var enemy in enemies)
{
if (enemy != null)
{
enemiesArray.Add(enemy.GetInstanceID());
}
}
state["enemies"] = enemiesArray;
state["groupHatesPlayer"] = groupHatesPlayer;
state["resetIfPlayerDies"] = resetIfPlayerDies;
return state;
}
public void RestoreFromJToken(JToken state)
{
// Restore enemies from their Unique IDs (Instance IDs):
var enemiesArray = state["enemies"].ToObject<List<int>>();
enemies.Clear();
foreach (var id in enemiesArray)
{
var enemy = FindObjectsOfType<EnemyStateMachine>().FirstOrDefault(enemy => enemy.GetInstanceID() == id);
if (enemy != null)
{
enemies.Add(enemy);
}
}
groupHatesPlayer = state["groupHatesPlayer"].ToObject<bool>();
resetIfPlayerDies = state["resetIfPlayerDies"].ToObject<bool>();
}
That’s how it decided to save my ‘AggroGroup’ list of enemies, leading to the final script to be as follows:
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;
using RPG.States.Player;
using GameDevTV.Saving;
using Newtonsoft.Json.Linq;
namespace RPG.Combat
{
[RequireComponent(typeof(JSONSaveableEntity))]
public class AggroGroup : MonoBehaviour, IJsonSaveable
{
[Header("The Player's Individual AggroGroup MUST BE A PARENT OF THE PLAYER,\notherwise the first if statement in the first Foreach loop\nin the first if statement in 'OnTakenHit' will act up!")]
[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")]
[Tooltip("The list of enemies this AggroGroup has")]
[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)
[Header("IF THE PLAYER IS NOT SUPPOSED TO BE NULL FOR THIS GROUP, THEN\nKEEP THIS AS A PARENT OF THE PLAYER, OTHERWISE\nIT WILL MESS EVERYTHING UP!")]
[Tooltip("Is the player a part of this aggroGroup? If so, it will be placed here through code")]
[SerializeField] PlayerStateMachine player;
private Health playerHealth;
private AggroGroup playerAggroGroup;
void Awake()
{
// Getting the player to be in his individual AggroGroup, which is the group parenting the player:
if (this.GetComponentInChildren<PlayerStateMachine>()) player = GameObject.FindWithTag("Player").GetComponent<PlayerStateMachine>();
// Getting the Player's AggroGroup:
playerAggroGroup = GameObject.FindWithTag("Player").GetComponentInParent<AggroGroup>();
}
/// <summary>
/// At the start of the game
/// </summary>
private void Start()
{
// If the entire group hates the player, they're all set to hostile,
// otherwise everyone is at their initial hostility:
if (groupHatesPlayer) Activate(true);
else foreach (EnemyStateMachine enemy in enemies) // this can't go to 'EnemyStateMachine.cs'. Doing so will conflict with 'RespawnManager.cs''s hostility. It stays here
{
// if the enemy is part of the same AggroGroup as the player, deactivate Hostility (and do that when Respawned as well)
// otherwise set Hostility to default hostility setting, if that enemy is not in the same AggroGroup as the player
if (enemy.GetAggroGroup().GetPlayer() != null) enemy.SetHostile(false);
else 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>
/// Is this enemy within the same AggroGroup as the player?
/// </summary>
/// <returns>Player GameObject</returns>
public PlayerStateMachine GetPlayer()
{
return player;
}
/// <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;
}
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;
}
private bool isDead;
public bool GetIsDead()
{
return isDead;
}
public void SetIsDead(bool isDead)
{
this.isDead = isDead;
}
/// <summary>
/// This Update function forces the enemy to delete any 'Missing'
/// Opponent gameObjects they might have, ensuring that they return
/// to their normal action if an enemy went missing, or is dead (for those stupid walking corpses,
/// I need to deal with that later...)
/// </summary>
void Update()
{
// 'IsDead' is a boolean flag. The reason it's here is to ensure this function is called
// ONCE, AND ONCE ONLY, when an enemy is dead, to get everyone to forget about him. If you don't
// do that, it'll call repetitively and cause some performance issues:
// IsDead is called from 'EnemyStateMachine.OnDie()', when the enemy dies, and it's turned back off
// here after the data of the dead enemies' hunters is wiped out:
if (isDead)
{
foreach (EnemyStateMachine enemy in FindObjectsOfType<EnemyStateMachine>())
{ // just to make sure we don't accidentally stalk ghost enemies...
if (enemy.GetOpponent() != null && enemy.GetOpponent().GetComponent<Health>().IsDead())
{
// if you're in the Player's AggroGroup, you have special treatment for hostility,
// as you can't be hostile against the player, who is your Ally
if (enemy.GetAggroGroup() != null && enemy.GetAggroGroup().GetPlayer())
{
Debug.Log($"{enemy.gameObject.name} is getting hostility set to false");
enemy.SetHostile(false, null);
enemy.SetOpponent(null);
enemy.SetHasOpponent(false);
isDead = false;
}
// if you're not in the Player's AggroGroup, just be normal
else
{
Debug.Log($"{enemy.gameObject.name} is getting hostility set to initial hostility");
enemy.SetHostile(enemy.GetInitialHostility);
enemy.SetOpponent(null);
enemy.SetHasOpponent(false);
isDead = false;
}
}
}
return;
}
}
// -------------------------------- Want Superior AI Intelligence? THIS IS IT! -----------------
// The Final Version of 'OnTakenHit()'. This version does what the third version did, but can also deal
// with cases where the player is part of an AggroGroup, and needs to call his allies when he's under attack ---------------------------
/// <summary>
/// This is the smartest version of 'OnTakenHit'. Essentially, it initially checks for if the Player was the one
/// who instigated the damage. If true, the player's allies will aim for the nearest enemies of the player's
/// Opponent's AggroGroup team they can find.
/// If the player is solo, all enemies in the enemy's 'PlayerChaseRadius' radius will attempt to attack
/// the Player. If the instigator is an enemy, get the nearest unassigned enemy you can find
/// in the Enemy's AggroGroup, and aim for that. If you can't find any,
/// get the closest enemy you can find
/// </summary>
/// <param name="instigator"></param>
void OnTakenHit(GameObject instigator)
{
// Check if the instigator is the player, or anyone in the player's team:
if (instigator.GetComponent<PlayerStateMachine>())
{
// If the instigator is in player chasing range, then all enemies in this victim's 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))
{
Debug.Log("We have entered the foreach loop");
// if the victim of the player is not part of his AggroGroup (i.e: "GetPlayer() == null"),
// enemies in the players' AggroGroup shall aim
// for the victim's team members, and this shall turn into a big fight:
if (GetPlayer() == null)
{
Debug.Log($"PlayerAggroGroup: {playerAggroGroup.gameObject.name}");
// Player hit someone from outside:
if (playerAggroGroup != null && playerAggroGroup != this)
{
Debug.Log("Not part of the Player's AggroGroup");
foreach (EnemyStateMachine playerAllyEnemy in playerAggroGroup.GetGroupMembers().Where(playerAllyEnemy => playerAllyEnemy != null))
{
Debug.Log("PlayerAggroGroup members accessed");
// All available enemies in PlayerAggroGroup should get ready to fight:
if (playerAllyEnemy != null && playerAllyEnemy.GetOpponent() == null && !playerAllyEnemy.GetHasOpponent())
{
EnemyStateMachine closestEnemy = GetClosestEnemyFromPlayerOpponentAggroGroup(playerAllyEnemy);
Debug.Log("Closest Enemy for the Player's Ally Enemy has been accumulated");
// FIGHT (turn on enemy-checking here, so that enemies who are unavailable are eliminated):
if (closestEnemy != null /* && closestEnemy.GetOpponent() == null */)
{
// Player's ally targets the closest enemy in the Victim's AggroGroup:
playerAllyEnemy.SetHasOpponent(true);
playerAllyEnemy.SetOpponent(closestEnemy.gameObject);
playerAllyEnemy.SetHostile(true, closestEnemy.gameObject);
// the Closest Enemy in the Victim's AggroGroup should aim for the Player's ally as well:
closestEnemy.SetHasOpponent(true);
closestEnemy.SetOpponent(playerAllyEnemy.gameObject);
closestEnemy.SetHostile(true, playerAllyEnemy.gameObject);
}
Debug.Log($"{playerAllyEnemy.gameObject.name} is now hostile towards {enemy.gameObject.name}, as they are not part of our AggroGroup");
Debug.Log($"{enemy.gameObject.name} is now hostile towards {playerAllyEnemy.gameObject.name}, as they are not part of the AggroGroup");
}
}
}
else {Debug.Log($"Player's AggroGroup either not found, or is the same as this one");}
}
// For cases where I have no opponent, and the player within my chase range has hit my ally, I shall aim for the player
// (and surprisingly, it works beautifully for both the player allies and enemies, so we can essentially
// get player allies to flip on him if he tries to flip on one of them, and they're not fighting anyone else
// (kind of like a 'penalty for trying to betray the team' system)):
if (enemy.GetOpponent() == null && !enemy.GetHasOpponent() && enemy.IsInPlayerChasingRange())
{
// 'EnemyStateMachine.ClearPlayer()' takes care of individually clearing up enemies when the player gets out of Chase Range
// and is called in 'EnemyChasingState.cs'
enemy.SetHostile(true, instigator.gameObject);
enemy.SetOpponent(instigator.gameObject);
enemy.SetHasOpponent(true);
Debug.Log($"{enemy.gameObject.name} is now hostile towards the player In Chase Range.");
}
}
return;
}
// BELOW IS THE CASE FOR WHEN THE INSTIGATOR WAS ANOTHER NPC, NOT THE PLAYER:
// Get the (instigator) attacker's aggro group, who is NOT the player:
AggroGroup attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>()?.GetAggroGroup();
foreach (EnemyStateMachine enemy in this.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");
}
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);
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.");
}
}
}
}
// --------------------------------------------------------------------------------------------------------------------------------
/// <summary>
/// From the list of enemies that have not been assigned an opponent, who is the closest
/// enemy that this enemy can go and attack?
/// </summary>
/// <param name="enemy"></param>
/// <param name="unassignedEnemies"></param>
/// <returns>nearestUnassignedEnemy</returns>
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;
}
/// <summary>
/// This function gets the closest enemy to your player,
/// regardless of whether that enemy is available for a fight or not
/// (used as a backup in case no enemies from opposing AggroGroup are
/// available for a fight)
/// </summary>
/// <param name="enemy"></param>
/// <returns></returns>
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;
}
// -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
/// <summary>
/// This function first finds the AggroGroup of the player, and foreach enemy in there, they will get the Player's Current Opponent.
/// Each member of the Player's AggroGroup will search for the closest unoccupied enemy to fight, from the team of the Player's Rival.
/// If it finds one, they'll go for it and fight till death. If it fails, just get the closest enemy it can find from the Player's Opponent's
/// AggroGroup, and aim for that
/// </summary>
private EnemyStateMachine GetClosestEnemyFromPlayerOpponentAggroGroup(EnemyStateMachine playerAllyEnemy)
{
EnemyStateMachine closestEnemy = null; // Initialize closest enemy to null
float closestDistance = Mathf.Infinity; // Initialize closest distance to infinity
// Player Ally is part of the Player's AggroGroup:
if (playerAllyEnemy != null && playerAllyEnemy.GetAggroGroup() != null)
{
// Get the player's Current Opponent (IT'S NOT USING 'player'. INSTEAD, WE CREATED A NEW ONE FROM SCRATCH. That one is for other types of checks):
PlayerStateMachine player = FindObjectOfType<PlayerStateMachine>();
GameObject playerOpponent = player.GetOpponent();
// Player has an Opponent:
if (playerOpponent != null)
{
// Get the Player Opponent's AggroGroup:
AggroGroup playerOpponentAggroGroup = playerOpponent.GetComponent<EnemyStateMachine>().GetAggroGroup();
if (playerOpponentAggroGroup != null)
{
// If there's only one enemy left in the player's Opponent's AggroGroup, everyone in the group should aim for him:
if (playerOpponentAggroGroup.GetGroupMembers().Count(enemy => enemy != null) == 1)
{
// Aim for that one last enemy:
closestEnemy = playerOpponentAggroGroup.GetGroupMembers().FirstOrDefault(enemy => enemy != null);
Debug.Log($"{playerAllyEnemy.gameObject.name} is aiming for the last remaining enemy, {closestEnemy.gameObject.name}");
}
// If more than one enemy is in the group, then allies should each get someone to focus on:
else foreach (EnemyStateMachine enemy in playerOpponentAggroGroup.GetGroupMembers())
{
// Find an unoccupied enemy
if (enemy != null && !enemy.GetHasOpponent() && enemy.GetOpponent() == null)
{
// Calculate distance between playerAllyEnemy and current enemy
float distance = Vector3.Distance(playerAllyEnemy.transform.position, enemy.transform.position);
// Check if this enemy is closer than the previously closest enemy
if (distance < closestDistance)
{
// Update closest enemy and closest distance
closestEnemy = enemy;
closestDistance = distance;
Debug.Log($"{playerAllyEnemy.gameObject.name} is aiming for unoccupied enemy {closestEnemy.gameObject.name}");
}
}
}
// If no unoccupied enemy was found, aim for the closest enemy in the player's opponent's AggroGroup
if (closestEnemy == null)
{
foreach (EnemyStateMachine enemy in playerOpponent.GetComponent<EnemyStateMachine>().GetAggroGroup().GetGroupMembers())
{
if (enemy != null)
{
float distance = Vector3.Distance(playerAllyEnemy.transform.position, enemy.transform.position);
if (distance < closestDistance)
{
closestEnemy = enemy;
closestDistance = distance;
Debug.Log($"{playerAllyEnemy.gameObject.name} is aiming for closest enemy {closestEnemy.gameObject.name}, regardless of occupation");
}
}
}
}
}
}
}
return closestEnemy; // Return the closest enemy found
}
public JToken CaptureAsJToken()
{
var state = new JObject();
// Capture enemies as their unique IDs (Instance IDs):
JArray enemiesArray = new JArray();
foreach (var enemy in enemies)
{
if (enemy != null)
{
enemiesArray.Add(enemy.GetInstanceID());
}
}
state["enemies"] = enemiesArray;
state["groupHatesPlayer"] = groupHatesPlayer;
state["resetIfPlayerDies"] = resetIfPlayerDies;
return state;
}
public void RestoreFromJToken(JToken state)
{
// Restore enemies from their Unique IDs (Instance IDs):
var enemiesArray = state["enemies"].ToObject<List<int>>();
enemies.Clear();
foreach (var id in enemiesArray)
{
var enemy = FindObjectsOfType<EnemyStateMachine>().FirstOrDefault(enemy => enemy.GetInstanceID() == id);
if (enemy != null)
{
enemies.Add(enemy);
}
}
groupHatesPlayer = state["groupHatesPlayer"].ToObject<bool>();
resetIfPlayerDies = state["resetIfPlayerDies"].ToObject<bool>();
}
}
}
it did exclude the PlayerStateMachine variable called ‘player’ though. It would be amazing if we integrate that, so the game knows the player’s team