AggroGroup for third person

and… nope, didn’t work :sweat_smile: - if it helps, this is the updated script:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using RPG.Movement;
using Unity.VisualScripting;
using RPG.Attributes;

namespace RPG.Combat
{

    public class AggroGroup : MonoBehaviour
    {

        [SerializeField] List<Fighter> fighters = new List<Fighter>();    // fighters to aggregate when our player takes a wrong dialogue turn (and pisses everyone off)
        [SerializeField] bool activateOnStart = false;
        [SerializeField] bool resetIfPlayerDies = true; // 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)

        private bool isActivated;   // Activates/deactivates the 'aggroGroup'
        private bool playerActivated;   // this boolean ensures that the group does not give up on trying to fight the player, if the main character dies, ensuring that they still fight him, even without the leader
        private Health playerHealth;


        private void Start()
        {

            // Ensures guards are not active to fight you, if you didn't trigger them:
            Activate(activateOnStart);

        }

        private void OnEnable()
        {

            if (resetIfPlayerDies)
            {

                playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);

            }

        }

        public void OnDisable()
        {

            if (resetIfPlayerDies)
            {

                Health playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.RemoveListener(ResetGroupMembers);

            }

        }

        public void ResetGroupMembers()
        {
            Activate(false);
        }

        public void Activate(bool shouldActivate) 
        {
            fighters.RemoveAll(fighter => fighter == null || fighter.IsDestroyed());

            foreach (Fighter fighter in fighters) 
            {
                ActivateFighter(fighter, shouldActivate);
            }

            isActivated = shouldActivate;

        }

        private void ActivateFighter(Fighter fighter, bool shouldActivate) 
        {

            CombatTarget target = fighter.GetComponent<CombatTarget>();
            if (target != null) 
            {
                target.enabled = shouldActivate;
            }

            fighter.enabled = shouldActivate;

        }

        public void AddFighterToGroup(Fighter fighter)
        {

            // If you got the fighter you want to add on your list, return:
            if (fighters.Contains(fighter)) return;
            // For other fighters on the list, add them lah:
            fighters.Add(fighter);
            ActivateFighter(fighter, isActivated);
        }

        public void RemoveFighterFromGroup(Fighter fighter)
        {

            // Remove fighters from the list
            if (fighter == null) return;
            ActivateFighter(fighter, false);
            fighters.Remove(fighter);

        }

    }

}

It won’t work, hence the ‘edit’ note.

like… zero chance? :sweat_smile:

(At least the bug of invincibility of the player after death is gone with that reverse…)

Anyway, I’m back to square one with the AggroGroup… all I need is a way for the other NPCs to chase me down if I attack one of their friends

Yes. Like @Brian_Trotter said:

so you need to stop using it

fair enough. I’ll reverse to his solution then when he returns… :slight_smile: (Sorry I was incredibly tired, so I went into deep sleep and literally just woke up)

oh well… I re-followed Brian’s instructions as best as I could, and I’m back to “my enemies are frozen statues” state again, when they’re under the AggroGroup. If any of you both would be kind enough to help me investigate what went wrong, because there’s literally no debuggers coming out of this one, that would be awesome. Here’s my current code:

// AggroGroup.cs:

using System.Collections.Generic;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;

namespace RPG.Combat
{

    public class AggroGroup : MonoBehaviour
    {

        [SerializeField] List<EnemyStateMachine> enemies = new List<EnemyStateMachine>();
        [SerializeField] bool activateOnStart = false;
        [SerializeField] bool resetIfPlayerDies = true; // 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)

        private bool isActivated;   // Activates/deactivates the 'aggroGroup'
        private Health playerHealth;


        private void Start()
        {

            // Ensures guards are not active to fight you, if you didn't trigger them:
            Activate(activateOnStart);

        }

        private void OnEnable()
        {

            if (resetIfPlayerDies)
            {

                playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);

            }

        }

        public void OnDisable()
        {

            if (resetIfPlayerDies)
            {

                Health playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.RemoveListener(ResetGroupMembers);

            }

        }

        public void ResetGroupMembers()
        {
            Activate(false);
        }

        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 lah:
            enemies.Add(enemy);
            enemy.enabled = isActivated;
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
            if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
        }

        public void RemoveFighterFromGroup(EnemyStateMachine enemy)
        {
            // Remove fighters from the list
            if (enemy == null) return;
            enemies.Remove(enemy);
        }

        void OnTakenHit(GameObject instigator) 
        {
            Activate(true);
        }

    }

}

RespawnManager.cs, after the changes:

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);
            } // write an else here to wear the "Unarmed" (i.e: default boxing hands) if it's null, to avoid weird glitches

            // 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); // Swapped to state machine on next line
                spawnedEnemy.AssignPatrolPath(patrolPath);
                spawnedEnemy.SwitchState(new EnemyPatrolState(spawnedEnemy));
            }
            else
            {
                Debug.Log($"No Patrol Path to assign");
            }
            // --------------------------- Extra Functionality: Setting up Aggro Group + Adding Fighters ---------------
            if (aggroGroup != null)
            {
                aggroGroup.AddFighterToGroup(spawnedEnemy);
                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);

            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 the new “SetHostile()” function in ‘EnemyStateMachine.cs’:

    public void SetHostile(bool IsHostile) 
    {
        this.IsHostile = IsHostile;
    }

Yeah, your code can’t be the same anymore. You are now disabling the enemy’s state machine, so there’s no way they’ll be doing anything

OK I’m not sure how you figured that out, but can you please tell me which part went wrong…? I’m really baffled as on where do I need to make changes to fix this :sweat_smile:

and yes you’re right, the State Machines were deactivated, but with how things are flowing, I’m struggling to find the source of the state machine deactivation

Edit: Not sure if that’s the ideal way to go around it, but this seems to solve the state machine activation issue (I commented “flipped this one” beside both modified lines):

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 lah:
            enemies.Add(enemy);
            enemy.enabled = !isActivated; // flipped this one...
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
            if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
        }

        public void RemoveFighterFromGroup(EnemyStateMachine enemy)
        {
            // Remove fighters from the list
            if (enemy == null) return;
            enemies.Remove(enemy);
            enemy.enabled = isActivated; // flipped this one as well
            enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
        }

Again, I’m not 100% sure of the flow of why this happens, but for now all works… (for the state Machines)

However, the enemy won’t get out of his patrolling path to want to attack you yet, if you bother one of his friends… the only thing that currently happens through code, is that “IsHostile” is forced to be true, regardless of the initial state of the enemy, when he’s part of the AggroGroup (so if you get close enough, he’ll chase you down, otherwise he’ll ignore you… still not getting out of his way to want to defend his friend :sweat_smile:)

Edit: I solved it… made the “Player Chasing Range” on that enemy larger, and now he will hunt down the player only if he bothers that enemy or one of his friends in the AggroGroup. Not to mention though, ‘ActivateOnStart’ MUST BE SET TO FALSE, unless you want that group to be naturally aggressive towards you

and… I have a funny little bug which I plan to keep. Apparently the swing the enemy does can hit other enemies if they’re in the way. I’m not deleting that one, it’s too much fun, and can be helpful for those incredibly hard fights with multiple enemies :stuck_out_tongue:

By the way, is there anyway we can write comments in the inspector? I have a few notes I don’t want to forget, apart from the tooltip


If anyone needs it, here’s the final renewed “AggroGroup.cs” script

using System.Collections.Generic;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;

namespace RPG.Combat
{

    public class AggroGroup : MonoBehaviour
    {

        [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 activateOnStart = 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("ONLY HOVER OVER TO SEE THE COMMENT")]
        [Tooltip("")]
        [SerializeField] string comment;

        private bool isActivated;   // Activates/deactivates the 'aggroGroup'
        private Health playerHealth;


        private void Start()
        {
            // Ensures guards are not active to fight you, if you didn't trigger them:
            Activate(activateOnStart);
        }

        private void OnEnable()
        {
            if (resetIfPlayerDies)
            {
                playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);
            }
        }

        public void OnDisable()
        {
            if (resetIfPlayerDies)
            {
                Health playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
                if (playerHealth != null) playerHealth.onDie.RemoveListener(ResetGroupMembers);
            }
        }

        public void ResetGroupMembers()
        {
            Activate(false);
        }

        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 lah:
            enemies.Add(enemy);
            enemy.enabled = !isActivated;
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
            if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
        }

        public void RemoveFighterFromGroup(EnemyStateMachine enemy)
        {
            // Remove fighters from the list
            if (enemy == null) return;
            enemies.Remove(enemy);
            enemy.enabled = isActivated;
            enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
        }

        void OnTakenHit(GameObject instigator) 
        {
            Activate(true);
        }

    }

}

(although you’ll want to make a few changes in “RespawnManager.cs” as well, just deleting “.Fighter” everytime you call “spawnedEnemy” in “AddFighterToGroup” or “RemoveFighterFromGroup”):

and this function in “EnemyStateMachine.cs”:

[field: SerializeField] public bool IsHostile {get; private set;} = true; // for enemies that have hatred towards the player
    
    public void SetHostile(bool IsHostile) 
    {
        this.IsHostile = IsHostile;
    }

which links to another thread regarding aggressive enemies (I can’t remember the link for it…)

and you’ll also want to do what Brian says must be done here :slight_smile: :

Thanks to both of you @bixarrio and @Brian_Trotter for standing by my side :smiley:

Look at the attack section of the merge tutorial. There are a few places where Brian added a header that shows up in the Inspector.

1 Like

I did a funny workaround around it. I created a header that says “DO NOT MODIFY THIS VARIABLE, JUST HOVER OVER IT TO SEE WHAT’S IMPORTANT”, and then assigned a string variable named “comment”. I won’t touch that, just hover over it to see what I need to see

Edit: then I deleted the variable and left the tooltip, but as a header now

        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 lah:
            enemies.Add(enemy);
            enemy.enabled = isActivated; // <-- HERE
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
            if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
        }

You can’t do this either, because you can’t be disabling the state machines. Flipping it just means that now they will become statues when they leave the group

Good job

Tooltip or Header is what you have. Unless you want to either create your own editor control, or import some third party package that can do it for you

Maybe this. It was adapted from NaughtyAttributes

// Somewhere _outside_ an 'Editor' folder
public class CommentAttribute : PropertyAttribute
{
    public string Comment { get;  set; }
    public CommentAttribute(string comment) => Comment = comment;
}

// Somewhere _inside_ an 'Edit' folder
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(CommentAttribute))]
public class CommentPropertyDrawer : DecoratorDrawer
{
    public override float GetHeight() => GetBoxHeight();

    public override void OnGUI(Rect rect)
    {
        var attr = attribute as CommentAttribute;
        if (attr != null && !string.IsNullOrWhiteSpace(attr.Comment))
        {
            var indentRect = EditorGUI.IndentedRect(rect);
            var indent = indentRect.x - rect.x;
            var boxRect = new Rect(rect.x + indent, rect.y, rect.width - indent, GetBoxHeight());
            EditorGUI.HelpBox(boxRect, attr.Comment, MessageType.Info);
        }
    }

    private float GetBoxHeight()
    {
        var attr = (CommentAttribute)attribute;
        var minHeight = EditorGUIUtility.singleLineHeight * 2.0f;
        var desiredHeight = GUI.skin.box.CalcHeight(new GUIContent(attr.Comment), EditorGUIUtility.currentViewWidth);
        var height = Mathf.Max(minHeight, desiredHeight);

        return height;
    }
}

// Usage
[SerializeField, Comment("Can we move this camera?")] bool _canMoveCamera = true;

image

so how do we fix that then…? Yes I did notice they’ll be flying statues until I get closer for some reason… :sweat_smile:

Don’t enable = anything. Remove the line. You used to enable/disable the fighter because the fighter is what made the enemies attack you. Now you do that with SetHostile() because you are no longer working with the fighter. So, you also don’t want to be enabling/disabling the state machines.

that turns them into frozen statues until we get to their chasing range…

Then, if you removed the line correctly (there are 2 places), you have a different issue somewhere else

I turned the functions as follows, using the new Hostile modifications:

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 lah:
            enemies.Add(enemy);
            // enemy.enabled = !isActivated;
            enemy.IsHostile = isActivated;
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
            // if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
        }

        public void RemoveFighterFromGroup(EnemyStateMachine enemy)
        {
            // Remove fighters from the list
            if (enemy == null) return;
            enemies.Remove(enemy);
            // enemy.enabled = isActivated;
            enemy.IsHostile = !isActivated;
            enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
        }

        

but this also meant I have to adjust the property “IsHostile()”, in the State Machine itself:

[field: SerializeField] public bool IsHostile {get; set;} = true; // for enemies that have hatred towards the player
    

is this correct?

I just noticed that… when the player dies and respawns, they literally never patrol ever again, and somehow… that goes for all enemies. Any idea where I can start debugging that from?

and… the reason they never patrol after my respawning is because their NavMeshAgents get deactivated somewhere through code…

Why?

Ok, progress. And you know this doesn’t happen in the AggroGroup because there’s no code here that does that

because if the setter is private, the value can’t be adjusted…? :sweat_smile:

I just noticed that as well, because my enemy outside of the group did not patrol either… I think I have an idea where to investigate

Edit: OK I’m not sure where it’s originating from, but if it helps in anyway, I found this in ‘EnemyPatrolState.cs’, something me and Brian added in sometime down the line:

public void MoveToPlayer(float deltaTime) 
        {
            if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true; // turn on the NavMeshAgent, otherwise the enemy won't be able to chase you down...
            stateMachine.Agent.destination = stateMachine.Player.transform.position;
            Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized;
            Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime);
            stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
            stateMachine.Agent.nextPosition = stateMachine.transform.position;
        }

The first line is what we both added

and a little while later, I found this:

        void MoveToWayPoint(float deltaTime) 
        {
            Vector3 direction = stateMachine.Agent.desiredVelocity.normalized;
            Move(direction * movementSpeed, deltaTime);
            stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
            stateMachine.Agent.nextPosition = stateMachine.transform.position;
        }

I’m guessing I can borrow the first line of the “EnemyChasingState.MoveToPlayer” to “EnemyPatrolState.MoveToNextWayPoint”. I’ll give that a go

Edit: Nope, that did not work. Any ideas @bixarrio ? (I’ll be back in 30 minutes). My next guess is there’s something going on in ‘Respawner.cs’

Edit: I’m back. What else can we change? How about we try changing this function in ‘Respawner.cs’? It looks like something we can work on (I tried, but… the screen stays stuck in fading out):

private void ResetEnemies() {

            // Resetting our Enemies, so they don't keep trying to kill us each time we respawn:
            foreach (AIController enemyControllers in FindObjectsOfType<AIController>())
            {

                // If he ain't dead, reboost part of his health:
                Health health = enemyControllers.GetComponent<Health>();

                // Only heal and reset enemies that are NOT dead (dead ones are handled by 'RespawnManager.cs')
                if (health && !health.IsDead())
                {

                    // Reset the Enemy AI Controller (Warp Position + AI Variables):
                    enemyControllers.Reset();
                    // Heal the enemies' health, which beat the player:
                    health.Heal(health.GetMaxHealthPoints() * enemyHealthRegenPercentage / 100);

                }

            }

        }

@Brian_Trotter any idea where we can investigate why the NavMeshAgent of the enemies gets deactivated when the player respawns? We’re almost done with this topic

I ran out of edits, but I did fix my Patrolling NavMeshAgent issue. I added this line in ‘Respawner.ResetEnemies()’, at the very bottom of the function:

                if (!enemyControllers.GetComponent<NavMeshAgent>().enabled) enemyControllers.GetComponent<NavMeshAgent>().enabled = true;

so now it looks like this:

        private void ResetEnemies() {

            // Resetting our Enemies, so they don't keep trying to kill us each time we respawn:
            foreach (AIController enemyControllers in FindObjectsOfType<AIController>())
            {

                // If he ain't dead, reboost part of his health:
                Health health = enemyControllers.GetComponent<Health>();

                // Only heal and reset enemies that are NOT dead (dead ones are handled by 'RespawnManager.cs')
                if (health && !health.IsDead())
                {

                    // Reset the Enemy AI Controller (Warp Position + AI Variables):
                    enemyControllers.Reset();
                    // Heal the enemies' health, which beat the player:
                    health.Heal(health.GetMaxHealthPoints() * enemyHealthRegenPercentage / 100);

                }

                if (!enemyControllers.GetComponent<NavMeshAgent>().enabled) enemyControllers.GetComponent<NavMeshAgent>().enabled = true;

            }

        }

YAY… Anyway, moving on, I have to mention a minor bug that has been irritating me for a while now:

Have you guys tried pausing the game when your enemies are moving around and watching the animation? For some reason, when I unpause my game, after I paused my game and my enemy was in moving state of patrolling (moving, not dwelling), he will stop playing the animation and keep on sliding (basically, my guess is that the float parameter responsible for playing the movement animation will be set to zero), until I quit the game and return to the scene again.

What seems to be the cause here, and how do we fix it?

Privacy & Terms