[I was gone because the laptop needed some rest, because I accidentally kept the power mode on max for a very long time ]
hello all, and @Brian_Trotter I’m just tagging you here because I’ll need you, or @bixarrio 's help for this one, so here’s what this topic is about:
How do I get the NPC to “follow” the player? Essentially, I have a function in my game that gets NPCs to join the player’s team through dialogue. Without too much context, it uses the DialogueTrigger.cs script from the RPG Course Series, and then does other things to ensure the NPC safely joins the player, but that’s not the point today
Where I handle this, I invoke a new event system I created, from a script called ‘AllyFollowPlayerManager.cs’ script, which is a simple event script that looks like this:
using System;
using UnityEngine;
namespace RPG.Combat {
public class AllyFollowPlayerManager : MonoBehaviour
{
public static event Action OnPlayerAllyAdded;
public static event Action OnPlayerAllyRemoved;
public static void NotifyPlayerAllyAdded()
{
OnPlayerAllyAdded?.Invoke();
}
public static void NotifyPlayerAllyRemoved()
{
OnPlayerAllyRemoved?.Invoke();
}
}
}
Now, I invite NPCs through an ‘AIAggroGroupSwitcher.cs’ script that I created, which is a little complex but fairly self-explanatory (all that matters is after the 9th debug in each of the inviting and expelling functions):
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;
[Tooltip("This is the conversation you would get if you want to invite an NPC to your AggroGroup (used in 'AIConversant.cs', setup in 'RespawnManager.cs')")]
[SerializeField] private Dialogue.Dialogue NPCOutOfPlayerGroupInviteDialogue;
[Tooltip("This is the conversation you would get if you want to kick out an NPC ally from your AggroGroup (used in 'AIConversant.cs', setup in 'RespawnManager.cs')")]
[SerializeField] private Dialogue.Dialogue NPCInPlayerGroupKickoutDialogue;
private void Awake()
{
playerAggroGroup = GameObject.Find("PlayerAggroGroup").GetComponent<AggroGroup>();
}
public void AddNPCToPlayerAggroGroup()
{
Debug.Log($"{gameObject.name} added to player AggroGroup - 1");
// 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
Debug.Log($"{gameObject.name} added to player AggroGroup - 2");
// First, Delete him from his own AggroGroup:
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>().RemoveFighterFromGroup(this.GetComponent<EnemyStateMachine>());
Debug.Log($"{gameObject.name} added to player AggroGroup - 3");
// And now, add him to the player's AggroGroup:
playerAggroGroup.AddFighterToGroup(this.GetComponent<EnemyStateMachine>());
Debug.Log($"{gameObject.name} added to player AggroGroup - 4");
// Set the RespawnManager's Dialogue accordingly:
GetComponentInParent<RespawnManager>().SetCurrentConversation(GetNPCInPlayerGroupKickOutDialogue());
Debug.Log($"{gameObject.name} added to player AggroGroup - 5");
// Switch the AggroGroup of the enemy holding this script, to the Player's AggroGroup:
GetComponentInParent<RespawnManager>().SetAggroGroup(playerAggroGroup);
Debug.Log($"{gameObject.name} added to player AggroGroup - 6");
// Set the AIConversant's Dialogue accordingly (this is the core value that determines who holds the conversation):
GetComponent<AIConversant>().SetDialogue(GetNPCInPlayerGroupKickOutDialogue());
Debug.Log($"{gameObject.name} added to player AggroGroup - 7");
// Set the 'AggroGroup' within the 'EnemyStateMachine' as well, otherwise you're getting a NullReferenceException:
GetComponent<EnemyStateMachine>().SetAggroGroup(playerAggroGroup);
Debug.Log($"{gameObject.name} added to player AggroGroup - 8");
// In 'EnemyStateMachine.cs', tick 'isPlayerAlly' (used in 'PlayerRangerAimingState.cs'). No tag resets, because that'll screw the Ranger Aiming State up (it's taken care of in 'PlayerRangerAimingState.cs' and 'PlayerRangerFiringState.cs')
GetComponent<EnemyStateMachine>().SetIsPlayerAlly(true);
// Notify the 'AllyFollowPlayerManager' added event, so enemies can follow the player around the game map, from 'EnemyStateMachine.cs'
Debug.Log($"{gameObject.name} added to player AggroGroup - 9");
AllyFollowPlayerManager.NotifyPlayerAllyAdded();
}
public void RemoveNPCFromPlayerAggroGroup()
{
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 1");
// 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:
// 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");
// First, Add the NPC back to his original Individual AggroGroup:
this.GetComponent<EnemyStateMachine>().GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>().AddFighterToGroup(this.GetComponent<EnemyStateMachine>());
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 3");
// Remove the NPC from the Player's AggroGroup:
playerAggroGroup.RemoveFighterFromGroup(this.GetComponent<EnemyStateMachine>());
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 4");
// Set the RespawnManager's Dialogue accordingly:
GetComponentInParent<RespawnManager>().SetCurrentConversation(GetNPCOutOfPlayerGroupInviteDialogue());
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 5");
// Switch the AggroGroup of the enemy holding this script, back to original AggroGroup:
GetComponentInParent<RespawnManager>().SetAggroGroup(GetComponentInParent<AggroGroup>());
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 6");
// Set the AIConversant's Dialogue accordingly (this is the core value that determines who holds the conversation):
GetComponent<AIConversant>().SetDialogue(GetNPCOutOfPlayerGroupInviteDialogue());
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 7");
// Set the 'AggroGroup' within the 'EnemyStateMachine' as well, otherwise you're getting a NullReferenceException:
GetComponent<EnemyStateMachine>().SetAggroGroup(GetComponentInParent<RespawnManager>().GetComponentInParent<AggroGroup>());
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 8");
// In 'EnemyStateMachine.cs', untick 'isPlayerAlly' (used in 'PlayerRangerAimingState.cs'). No tag resets, because that'll screw the Ranger Aiming State up (it's taken care of in 'PlayerRangerAimingState.cs' and 'PlayerRangerFiringState.cs')
GetComponent<EnemyStateMachine>().SetIsPlayerAlly(false);
// Notify the 'AllyFollowPlayerManager' removed event, so enemies can stop following the player around the game map, from 'EnemyStateMachine.cs'
Debug.Log($"{gameObject.name} kicked out of player AggroGroup - 9");
AllyFollowPlayerManager.NotifyPlayerAllyRemoved();
}
public Dialogue.Dialogue GetNPCOutOfPlayerGroupInviteDialogue()
{
return NPCOutOfPlayerGroupInviteDialogue;
}
public void SetNPCOutOfPlayerGroupInviteDialogue(Dialogue.Dialogue dialogue)
{
this.NPCOutOfPlayerGroupInviteDialogue = dialogue;
}
public Dialogue.Dialogue GetNPCInPlayerGroupKickOutDialogue()
{
return NPCInPlayerGroupKickoutDialogue;
}
public void SetNPCInPlayerGroupKickOutDialogue(Dialogue.Dialogue dialogue)
{
this.NPCInPlayerGroupKickoutDialogue = dialogue;
}
}
}
and the subscriptions for the events mentioned above are used in ‘EnemyStateMachine.cs’:
// in 'Start()':
// TEST - Follow the player if you're an ally, when the scene starts
if (isPlayerAlly) // boolean that indicates if the NPC is an ally with the player or not, switched through dialogue or set in the inspector, depending on your needs
{
StartFollowingPlayer();
}
else
{
StopFollowingPlayer();
}
// in 'OnEnable()' (the Unity Function):
// Get Player Ally NPCs to follow or unfollow the player, depending on 'isPlayerAlly'
AllyFollowPlayerManager.OnPlayerAllyAdded += StartFollowingPlayer;
AllyFollowPlayerManager.OnPlayerAllyRemoved += StopFollowingPlayer;
// in 'OnDisable()' (the Unity Function):
// Get Player Ally NPCs to follow or unfollow the player, depending on 'isPlayerAlly'
AllyFollowPlayerManager.OnPlayerAllyAdded -= StartFollowingPlayer;
AllyFollowPlayerManager.OnPlayerAllyRemoved -= StopFollowingPlayer;
and then I created the ‘StartFollowingPlayer’ and ‘StopFollowingPlayer’, again in ‘EnemyStateMachine.cs’:
private void StartFollowingPlayer()
{
Debug.Log($"{this.gameObject.name} will start following the player");
SwitchState(new EnemyFollowingPlayerState(this));
}
private void StopFollowingPlayer()
{
Debug.Log($"{this.gameObject.name} will not be following the player");
// SwitchState(new EnemyIdleState(this));
}
So, my question is, is this approach usable? And more importantly, what do I write in ‘EnemyFollowingPlayerState.cs’? I was thinking of copy-pasting my ‘EnemyChasingState.cs’ and then swapping the ‘EnemyAttackingState.cs’ to ‘EnemyIdleState.cs’ in the end, but I’m not sure.
The idea is simple:
- if the player left this NPC behind, beyond a specific distance, and they’re an ally, they will wait in idle state unless the player returns to them. I’ll also throw in a checkpoint where we check for patrol paths in ‘EnemyIdleState.cs’. If it’s an ally, you continue (not sure how to do that just yet)
- if they’re in the ‘specific distance’ to the player, they will follow the player (I think using the Chasing here would be a good idea) until they are close enough to him, and once they’re close, simply go to idle until the player does something
This is an additional idea from the top of my head. It’s not necessary though, it’s just from the top of my head:
- If the player runs, get the NPC to run as well, at a fraction of the player’s speed. I control running through ‘stateMachine.InputReader.IsSpeeding’ (it’s called ‘IsSpeeding’ because it’s a generic variable that’s expected to work for multiple operations)