[Quick question Brian. Can we develop a âCaptureAsJTokenâ and âRestoreFromJTokenâ function (I canât test the final part without saving the game) to store the lists of AggroGroup members, the âenemiesâ list in the comment right above this one? Literally, all I need is a save and load solution for my saving system, so I know how the dialogue acts when the player goes and comes from the game. PLEASEEEEE]
Another Saving System I might need is one for the âAIAggroGroupSwitcher.csâ script below, so that the game knows which dialogue to play based on whether the NPC holding this script is part of the playerâs AggroGroup or not
OK so⌠to keep things clean, Iâm attempting this on my own, and hereâs what Iâm learning so far:
- Create an âAIAggroGroupSwitcher.csâ script. This script is responsible for switching the AggroGroup of whoever holds it. In this case, itâs the NPC with the Dialogue thatâs holding it, so he shall be called when this operation happens
In there, there will be two functions, âAddNPCToPlayerAggroGroupâ and âRemoveNPCFromPlayerAggroGroupâ, which will be responsible for, as the name says it, adding and removing the NPCs from your Playerâs AggroGroup
And, needless to say, donât forget in Awake to specify your âPlayerAggroGroupâ
-
Create a new Dialogue, and in there create a path for your speech to follow. Once you get there, call the function âAddNPCToPlayerAggroGroupâ, and also create a path for removing it. I havenât gotten this far yet, but weâre walking it throughâŚ
-
On the NPC holding the script, include a âDialogueTrigger.csâ script, so you can actually connect the Dialogue to the function thatâs calling it
-
Hereâs my current âAIAggroGroupSwitcher.csâ script, finalized ONLY TO ADD NPCs to the Playerâs AggroGroup:
using UnityEngine;
using RPG.States.Enemies;
using RPG.Respawnables;
namespace RPG.Combat {
public class AIAggroGroupSwitcher : MonoBehaviour
{
// This script is placed on NPCs, and is essentially responsible for
// allowing them to enter and exit the player's AggroGroup, through
// dialogue:
private AggroGroup playerAggroGroup;
private AggroGroup currentAggroGroup;
void Awake()
{
playerAggroGroup = GameObject.Find("PlayerAggroGroup").GetComponent<AggroGroup>();
}
public void AddNPCToPlayerAggroGroup()
{
Debug.Log($"{gameObject.name} added to player AggroGroup");
// This function essentially deletes the Enemy
// that calls it from his own AggroGroup, and adds him to
// the Player's AggroGroup, and is meant to be called
// at the end of the dialogues that lead to that action:
if (playerAggroGroup.GetGroupMembers().Contains(this.GetComponent<EnemyStateMachine>())) return; // if this script holder is part of the Player's AggroGroup, don't add him again
// First, Delete him from his own AggroGroup
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>().RemoveFighterFromGroup(this.GetComponent<EnemyStateMachine>());
// And now, add him to the player's AggroGroup
playerAggroGroup.AddFighterToGroup(this.GetComponent<EnemyStateMachine>());
// Next, update the state of 'currentAggroGroup', a variable to keep track of which team the NPC is on now
currentAggroGroup = playerAggroGroup;
// Finally, let 'RespawnManager.cs' know which AggroGroup the Script Holder is on, so it knows how to treat the situationa accordingly
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().SetAggroGroup(currentAggroGroup);
}
public void RemoveNPCFromPlayerAggroGroup()
{
// Similar to how 'AddNPCToPlayerAggroGroup()' adds the script holder to
// the player's AggroGroup, this function is supposed to delete the script
// holder from the Player's AggroGroup
}
}
}
-
Throw in a second Dialogue Trigger for âRemoveNPCFromPlayerAggroGroup()â on the NPC holding the script, and again connect that accordingly
-
In âAIConversant.csâ, create a setter for the dialogue of âAIConversant.csâ:
// This setter will set the dialogue, based on the state of the conversant.
// (For now it's used to change the dialogue for NPCs that join and leave the player's
// AggroGroup, so we can kick and invite them as we please)
public void SetDialogue(Dialogue dialogue)
{
this.dialogue = dialogue;
}
The reason I have a setter is because since I donât have a predicate for my Dialogue when the enemy is in or out of the team, I decided to just change the entire dialogue and change it in âAIConversant.csâ through code instead. Bear with me, itâll make more sense right about now
- in âAIAggroGroupSwitcher.csâ, introduce two new Dialogues to switch between in âAIConversant.csâ, so when youâre in the team you got a dialogue, and when youâre about to be invited in the team, you got an entirely different Dialogue, as follows:
[SerializeField] private RPG.Dialogue.Dialogue NPCOutOfPlayerGroupInviteDialogue;
[SerializeField] private RPG.Dialogue.Dialogue NPCInPlayerGroupKickOutDialogue;
Obviously Iâd appreciate if @Brian_Trotter helped me create a Predicate for that instead (to make my life significantly easier down the line), but for the time being Iâm working with what I know
- Update your âAddNPCToPlayerAggroGroupâ to include the setter, so you can set the dialogue accordingly:
// Swap the dialogue out for the one where the NPC can be potentially be kicked out of the Player's AggroGroup now:
conversant.SetDialogue(NPCInPlayerGroupKickOutDialogue);
Iâll upload the full script below when itâs ready (Currently Iâm stuck with a funky problem. When the game starts, it uses the RespawnManager to spawn the character. The problem is, it doesnât know which Dialogue to use, the kick-out or invite⌠so Iâll need the saving system requested at the very top of this comment to get that). So far, hereâs my new âAIAggroGroupSwitcher.csâ script:
using UnityEngine;
using RPG.States.Enemies;
using RPG.Respawnables;
using RPG.Dialogue;
namespace RPG.Combat {
public class AIAggroGroupSwitcher : MonoBehaviour
{
// This script is placed on NPCs, and is essentially responsible for
// allowing them to enter and exit the player's AggroGroup, through
// dialogue:
private AggroGroup playerAggroGroup;
private AggroGroup currentAggroGroup;
private AIConversant conversant;
private Dialogue.Dialogue currentConversation;
[SerializeField] private RPG.Dialogue.Dialogue NPCOutOfPlayerGroupInviteDialogue;
[SerializeField] private RPG.Dialogue.Dialogue NPCInPlayerGroupKickOutDialogue;
void Awake()
{
playerAggroGroup = GameObject.Find("PlayerAggroGroup").GetComponent<AggroGroup>();
conversant = GetComponent<AIConversant>();
if (currentConversation == null) currentConversation = NPCOutOfPlayerGroupInviteDialogue;
}
public void AddNPCToPlayerAggroGroup()
{
Debug.Log($"{gameObject.name} added to player AggroGroup");
// This function essentially deletes the Enemy
// that calls it from his own AggroGroup, and adds him to
// the Player's AggroGroup, and is meant to be called
// at the end of the dialogues that lead to that action:
if (playerAggroGroup.GetGroupMembers().Contains(this.GetComponent<EnemyStateMachine>())) return; // if this script holder is part of the Player's AggroGroup, don't add him again
// First, Delete him from his own AggroGroup
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>().RemoveFighterFromGroup(this.GetComponent<EnemyStateMachine>());
// And now, add him to the player's AggroGroup
playerAggroGroup.AddFighterToGroup(this.GetComponent<EnemyStateMachine>());
// Next, update the state of 'currentAggroGroup', a variable to keep track of which team the NPC is on now
currentAggroGroup = playerAggroGroup;
// Finally, let 'RespawnManager.cs' know which AggroGroup the Script Holder is on, so it knows how to treat the situationa accordingly
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().SetAggroGroup(currentAggroGroup);
// Swap the dialogue out for the one where the NPC can be potentially be kicked out of the Player's AggroGroup now:
conversant.SetDialogue(NPCInPlayerGroupKickOutDialogue);
// Set the Current Conversation, so the Respawn Manager knows what to use when respawning
currentConversation = NPCInPlayerGroupKickOutDialogue;
}
public void RemoveNPCFromPlayerAggroGroup()
{
Debug.Log($"{gameObject.name} kicked out of player AggroGroup");
// Similar to how 'AddNPCToPlayerAggroGroup()' adds the script holder to
// the player's AggroGroup, this function is supposed to delete the script
// holder from the Player's AggroGroup:
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
// First, Add the NPC back to his original Individual AggroGroup
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>().AddFighterToGroup(this.GetComponent<EnemyStateMachine>());
// Remove the NPC from the Player's AggroGroup
playerAggroGroup.RemoveFighterFromGroup(this.GetComponent<EnemyStateMachine>());
// Update the current AggroGroup, for the RespawnManager
currentAggroGroup = this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>();
// let 'RespawnManager.cs' know which AggroGroup the Script holder is on, so it treats the situation accordingly
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().SetAggroGroup(currentAggroGroup);
// Swap the dialogue out for the one to invite the NPC to the Player's AggroGroup
conversant.SetDialogue(NPCOutOfPlayerGroupInviteDialogue);
// Set the Current Conversation, so the Respawn Manager knows what to use when respawning
currentConversation = NPCOutOfPlayerGroupInviteDialogue;
}
public Dialogue.Dialogue GetCurrentConversation()
{
return currentConversation;
}
}
}
and my âAggroGroup.csâ (again):
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;
using RPG.States.Player;
namespace RPG.Combat
{
public class AggroGroup : MonoBehaviour
{
[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
}
}
}
and my âRespawnManager.csâ, if needed:
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;
using RPG.Dialogue;
namespace RPG.Respawnables
{
[RequireComponent(typeof(JSONSaveableEntity))]
public class RespawnManager : MonoBehaviour, IJsonSaveable
{
[SerializeField] EnemyStateMachine spawnableEnemy;
[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 != null)
{
// If you found an enemy as a child of this script holder, delete his 'OnDeath' listener
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
}
foreach (Transform child in transform)
{
// Start the Respawn by deleting any existing gameObjects
Destroy(child.gameObject);
}
spawnedEnemy = Instantiate(spawnableEnemy, transform);
// 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)
{
Debug.Log($"Assigning Patrol Path {patrolPath} to {spawnedEnemy.name}");
spawnedEnemy.AssignPatrolPath(patrolPath);
// set the enemy's initial hostility here instead of 'EnemyStateMachine.Start()', otherwise the override will mess with your head for days
// (I made that mistake, hence why I'm writing about it here):
// if the spawned Enemy is part of the Player's AggroGroup, turn off his hostility on respawn against the Player:
if (spawnedEnemy != null && spawnedEnemy.GetAggroGroup() != null && spawnedEnemy.GetAggroGroup().GetPlayer()) spawnedEnemy.SetHostile(false);
else spawnedEnemy.SetHostile(spawnedEnemy.GetInitialHostility);
spawnedEnemy.SwitchState(new EnemyIdleState(spawnedEnemy));
}
else
{
Debug.Log($"No Patrol Path to assign");
}
// For Dialogue-Powered NPCs, you need to find a way to get the proper conversation for their state, based on whether they're on your team or not:
if (GetComponentInChildren<AIConversant>() != null)
{
GetComponentInChildren<AIConversant>().SetDialogue(GetComponentInChildren<AIAggroGroupSwitcher>().GetCurrentConversation());
}
// --------------------------- 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 (spawnedEnemy != null && spawnedEnemy.GetAggroGroup().GetPlayer()) spawnedEnemy.SetHostile(false);
// a friend of the NPC that just respawned is under attack, so that just-respawned NPC should go and fight for them:
else if (allyEnemy != null && 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);
// Find a way to set the hostility of respawned player allies here to false:
spawnedEnemy.SetHostile(true, allyEnemy.GetOpponent());
Debug.Log($"{spawnedEnemy.gameObject.name} 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);
}
}
// ---------------------------------------------------------------------------------------------------------
}
public void SetAggroGroup(AggroGroup aggroGroup)
{
// This setter is used in 'AIAggroGroupSwitcher.cs', essentially to
// let this Respawn Manager know which AggroGroup to load the Respawned
// enemy to
this.aggroGroup = aggroGroup;
}
private IEnumerator HideCharacter()
{
Debug.Log($"HideCharacter in progress");
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();
if (spawnedEnemy != null)
{
spawnedEnemy.Health.onDie.RemoveListener(OnDeath);
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))
{
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;
}
}
}
All I want to do here is find a way to connect the correct dialogue, with the correct status. So if the NPC is part of the playerâs AggroGroup, when the player returns to the game, get the conversation that can potentially kick the NPC out of the Playerâs AggroGroup.
And if the NPC is NOT part of the playerâs AggroGroup, when the player returns to the game and loads it up, it should fire the dialogue that essentially invites the NPC to the Playerâs AggroGroup
The same case should go for Respawning. This one is a major problem for me, because I am clueless as to how to get the correct âcurrentConversationâ status from âAIAggroGroupSwitcher.csâ for that
And more importantly, be able to save and load the âenemiesâ List on my âAggroGroup.csâ script, so we know who is where
And before I forget. Just to give you an idea of my hierarchy of any NPC in the game, itâs split to 3 parts:
-
The uppermost empty gameObject is the âAggroGroup.csâ. The plan is to have every character in the game have an AggroGroup.cs script as a header, and that can be empty or full, depending on who is on the team
-
The second layer is the âRespawnManager.csâ, this is responsible for respawning the player at the start of the game, and when he dies
-
The third layer, only instantiated when the game runs, is the character and all of his data beneath him
Edit 2: Iâm stuck with a new unforeseen problem. When the enemy dies and resets, itâll use âcurrentConversationâ based on what AIAggroGroupSwitcher.Awake() says (which is the âinviteâ dialogue, regardless of whether heâs already on the team or not⌠I need to find a way to update that as well), which is expected, but I want to update the conversation based on whether he was part of the playerâs AggroGroup or not⌠(and unfortunately when he dies, he has to be deleted off the list for performance reasons, so this really makes things harder for me to program)
This just keeps getting harder the more I try. Please send help