AggroGroup for third person

OK so since the transition to third person, I think my AggroGroup broke. What currently happens is if I assign any of my enemies to the AggroGroup, they won’t patrol, and… even if they’re nearby and I get into a fight with a member of the group, they won’t fight the player to try and defend their buddies. How do I fix this? As of last update, about 4 months ago, this was what my AggroGroup system looked like (I haven’t tested triggering the aggroGroup through dialogue as of yet, but for the moment I just want the basics to work):

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)
        [SerializeField] internal bool hasWeaponVisibleBeforeFirstTimeAggregation;

        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, hasWeaponVisibleBeforeFirstTimeAggregation);
            
        }

        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, hasWeaponVisibleBeforeFirstTimeAggregation);
        }

        /* public void HandleAttack(Fighter fighter) {

            if (!isActivated) {
                Activate(true);
                fighters.Remove(fighter);
            }
        } */

        public void Activate(bool shouldActivate, bool hasWeaponRendered = false)
        {

            // First step is to clean up the list, because if you don't, other fighters won't get involved in the fight:
            fighters.RemoveAll(fighter => fighter == null || fighter.IsDestroyed());
            
            isActivated = shouldActivate;
            foreach (Fighter fighter in fighters)
            {

                if (fighter == null) RemoveFighterFromGroup(fighter);

                else {
                    if (hasWeaponRendered) {
                    WeaponConfig enemyWeaponConfig = fighter.currentWeaponConfig;
                    fighter.AttachWeapon(enemyWeaponConfig);
                }
                }
                
                // If you don't have a fighter script, stay out of this fight:
                if (!fighter) continue;
                
                fighter.enabled = shouldActivate;

                if (fighter.TryGetComponent(out CombatTarget target)) target.enabled = shouldActivate;

                // ----------------------------- TEST FUNCTION (Delete if failed): Setting destination of enemies to rush to attack the player: ----------------------                
                if (fighter.TryGetComponent(out NavMeshAgent navMeshAgent))
                {
                    navMeshAgent.enabled = shouldActivate;

                    if (shouldActivate)
                    {
                        Transform playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
                        navMeshAgent.SetDestination(playerTransform.position);
                        navMeshAgent.isStopped = false;
                        fighter.GetComponent<Mover>().MoveTo(playerTransform.position, 1.0f);

                    }

                }
                // --------------------------- END OF TEST FUNCTION --------------------------------------------------------------------------------------------------
            }

        }

        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);
            fighter.enabled = isActivated;
            if (fighter.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;

        }

        public void RemoveFighterFromGroup(Fighter fighter) {

            // Remove fighters from the list
            fighters.Remove(fighter);

        }

    }

}

Another day, another fallen major system that needs addressing… :sweat_smile: (or at least are we addressing this later on?). If it helps, I also know the AggroGroup is somehow connected to my ‘RespawnManager.cs’ and ‘DialogueAggro.cs’. Should I provide these two scripts as well?

It’s not something I’m ready for right now, but it’s down the list… With upcoming life events, I can’t say when right now.

fair enough. Let’s ask @bixarrio though, he might be interested to help out :slightly_smiling_face:
(I’m trying to figure it out in the backscenes anyway, but I’m struggling to get my mind into this)

and why is there a cake beside my name? :stuck_out_tongue:

Hover over the Cake and see

ooo happy birthday to me :stuck_out_tongue: (hope to see one on December 4th as well :laughing:)

by the way, on a side note, do you know of a way where we can compare the health of the enemy between last frame and the current frame? I’m trying to get my “Activate()” function to work based on comparing the health of the enemy between frames :slight_smile: (so if it falls, we know the enemy was attacked, and that’ll activate the AggroGroup, if he’s in an AggroGroup)

Add this event to Health

public event System.Action OnDamageTaken;

Subscribe to it where you want to activate the AggroGroup

In the TakeDamage() method, look for the if/else, one is Die(), the other invokes the takeDamage UnityEvent, which was poorly set up for subscribing in code.
Add

OnDamageTaken?.Invoke();

to that block (in addition to the takeDamage call)

I already have all of that setup I believe, but under the name of ‘OnTakenHit’ instead of ‘OnDamageTaken’, almost exactly as you described it. However, what I’m trying to do here is check for damage taken in an if statement, as follows (I’m re-writing my “Activate” function from above), in the very last line of the function below:

public void Activate(bool shouldActivate)
        {
            // Remove nullified or destroyed fighters:
            fighters.RemoveAll(fighter => fighter == null || fighter.IsDestroyed());

            foreach (Fighter fighter in fighters) 
            {
                if (fighter == null) RemoveFighterFromGroup(fighter);

                else
                {
                    WeaponConfig enemyWeaponConfig = fighter.currentWeaponConfig;
                    fighter.AttachWeapon(enemyWeaponConfig);
                }

                // if the fighter is an enemy, and his recent health took a hit, aggregate others in the group:
                if (fighter == GameObject.FindWithTag("Enemy") && /* trying to check for changes in health heree */)

            }

        }

That’s what I’m trying to fill up here, and events can still easily confuse me as we speak…

Once the enemy is confirmed to be damaged by the player, then I’ll attempt to find everyone in the group and get them to set their destination to be the player, through the “EnemyChaseState.cs” (which should take care of the attacking after that). I’ll do my best to take care of most of that, I just need to get the enemy to know that he’s been hit

Checking every frame is a massive code drag… events are far more efficient.

Make a variable
lastHealth
Compare it to the currentHealth
If it’s different, return true and set lastHealth to currentHealth
Complain about your lowered framerate

I won’t argue against that, I know that not everything has to be done in ‘Update’ (learned that the hard way). All I’m asking is, is there a simpler solution to check for the event in that if statement?

That method has other issues… GameObject.FindWithTag(“Enemy”) will find the first enemy (and only the FIRST enemy) IN THE SCENE (ONLY THE FIRST ENEMY)…
I’m in the middle of something else… I’ll try to make some sense of this later… did you mean CompareTag???

if I’m being brutally honest, I don’t know the big difference between “FindWithTag”, “FindGameObjectWithTag” and “CompareTag”, so… I will just go with “yes, I meant CompareTag” :stuck_out_tongue: (although Unity doesn’t like that…)

Naming-wise, to me they all seem to be doing the same thing

CompareTag is different than FindWithTag… I suggest checking the docs on those

1 Like

Ok, here’s how I understand Aggrogroup context, from the Dialogues and Quests course…

In that, the AggroGroup usually starts out not aggro, but then, if you say the wrong thing, a DialogueTrigger calls Activate(true);

That generally activates or deactivates the Fighter Component, and that’s about it

This method… Ok, you’re removing destroyed fighters, but then you’re checking them for null again in the foreach they will never be null, as they were removed in the RemoveAll() method
And otherwise, it re-equip’s the weapon? Why??

The Fighter component being active or not is irrelevant to the Third Person course, so we need a completely different approach anyways…

The most sensible approach is to have an IsHostile variable on the EnemyStateMachine… we discussed this in another thread.

So first, I would change things to be List<EnemyStateMachine> stateMachines = new(); instead of tracking Fighters. Fighters don’t Update anymore and aren’t in the aggro equation anymore.

public void Activate(bool shouldActivate)
{
    stateMachines.RemoveAll(stateMachine=>stateMachine==null || stateMachine.IsDestroyed());
    foreach(EnemyStateMachine machine in stateMachines)
    {
         machine.SetHostile(shouldActivate);
    }
}

Now, before you remind me about your respawnables, you’ll have to change the RespawnManager to assign the enemy’s EnemyStateMachine to the stateMachines rather than the Fighter to the fighters. You can do this, I have confidence in you.

Now… if you want the aggroGroup to go aggro if the Player hits them, all you have to do, once again in AggroGroup is subscribe to each enemy’s Health’s OnTakenHit, either in Awake() or when your RespawnManager adds the enemy to the Aggrogroup… Add this method:

void OnTakenHit()
{
    Activate(true);
}

No need to unsubscribe because of the direction of the AggroGroup → Health relationship (the AggroGroup never dies, the Health does die)

lol I’m lowkey now scared to tell you that I was slowly finding my way around it. I literally just finished coding a properly working function that debugs when the enemy is hit, and I was going to introduce the mechanics in that to get the other enemies in the group to start chasing the player down

By all means, I’ll go through your solution. If you’re interested in mine though, here’s what I came up with so far (and I used ChatGPT for help. I know it’s wrong, but I would’ve struggled terribly otherwise (and it refreshed my memory on what a lambda expression is, so that’s a bonus :stuck_out_tongue:)):

Here is my ‘Activate()’ so far:

        public void Activate(bool shouldActivate)
        {
            // Remove nullified or destroyed fighters:
            fighters.RemoveAll(fighter => fighter == null || fighter.IsDestroyed());

            foreach (Fighter fighter in fighters) 
            {
                if (fighter == null) RemoveFighterFromGroup(fighter);

                else
                {
                    WeaponConfig enemyWeaponConfig = fighter.currentWeaponConfig;
                    fighter.AttachWeapon(enemyWeaponConfig);
                }

                // if the fighter is an enemy, and his recent health took a hit, aggregate others in the group:
                // NOTE: ONLY USE "CompareTag" HERE, BECAUSE FindWithTag 
                // WILL ONLY RETURN THE FIRST ENEMY, NOT EVERYONE INVOLVED, WHICH IS NOT WHAT WE WANT!
                if (fighter.CompareTag("Enemy")) 
                {
                    Health fighterHealth = fighter.GetComponent<Health>();

                    if (fighterHealth != null) 
                    {
                        fighterHealth.OnTakenHit += OnTakenHitEventHandler;
                        // when the enemy dies, or his health is nullified, unsubscribe from the event:
                        fighterHealth.onDie.AddListener((/* no parameters for this lambda expression, just the content function of this lambda expression after the arrow */) => OnFighterDeath(fighterHealth));
                    }
                }
            }
        }

and here are my new two functions that subscribed to the event handlers:

        void OnTakenHitEventHandler(GameObject instigator)
        {
            Debug.Log("Enemy was hit by: " + instigator.name); // so far, this one works
        }

        void OnFighterDeath(Health health) 
        {
            health.OnTakenHit -= OnTakenHitEventHandler;
        }

Anyway, I’ll go through your solution as well and decide on the next step :slight_smile:

That’s an interesting question… I don’t know what weird bug do I have in my project, but if I don’t do that, my enemy’s weapon will never ever show until he swings his first swing against me, and boom all of a sudden the player just discovered the enemy had an invisible weapon. Surprises would be nice, but that’s not the type of surprise that I want

In fact, the exact same problem happens when the enemy respawns, so I added this (Incomplete) block as well under “RespawnManager.Respawn()”, right after we re-instantiate the enemy:

            // (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

This fixes my invisible sword problem

good memory :laughing:

Anyway, I’ll go ahead and probably delete what I was doing in favor of yours :stuck_out_tongue:

Update: Something is COMPLETELY WRONG… The enemies don’t patrol after the update, they don’t fight back, they can’t be pushed around… they’re basically statues again :sweat_smile:

No debugs, here’s the changes I did:

// Fighters to StateMachines:
        // We're Dealing with State Machines Now:
        [SerializeField] List<EnemyStateMachine> enemies = new List<EnemyStateMachine>();

// Complete overhaul of my 'Activate' function:

        public void Activate(bool shouldActivate)
        {
            // Remove nullified or destroyed fighters:
            enemies.RemoveAll(enemy => enemy == null || enemy.IsDestroyed());

            foreach (EnemyStateMachine enemy in enemies) 
            {
                enemy.SetHostile(shouldActivate);
            }
        }

// AddFighterToGroup:

        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;

        }

// OnTakenHit:

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

and in ‘EnemyStateMachine.cs’:

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

and then in ‘RespawnManager.cs’, I changed it all from Fighter to just the state machine

In the meanwhile I’ll go give my previous solution a full attempt and see what I can come up with and update this comment accordingly :slight_smile:

Not a clue… none of this should affect patrolling at all…

IsHostile does need to be utilized, of course, to decide whether to chase or not. We discussed that in the other thread.

I must off to bed be.

no worries, let’s continue this tomorrow. I’m still prototyping with my own solution as we speak :slight_smile: - let’s see if I can pull this off, xD

Edit: It got significantly worse… somehow, the “AggroGroup.cs” logic, I’m not sure where I went terribly wrong, gets the player to be invincible to each enemy in the AggroGroup once they killed him ONCE. So when he respawns, and he meets the enemy that killed him, he’s invincible to that enemy now:

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

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; // make the group aggressive when the game starts
        [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() {

            foreach (Fighter fighter in fighters) 
            {
                if (fighter != null && fighter.GetComponent<Health>() != null) 
                {
                    fighter.GetComponent<Health>().OnTakenHit -= OnTakenHitEventHandler;
                }
            }

            Activate(false);
            Debug.Log("AggroGroup Deactivated");
        }

        public void Activate(bool shouldActivate)
        {
            // Remove nullified or destroyed fighters:
            fighters.RemoveAll(fighter => fighter == null || fighter.IsDestroyed());

            foreach (Fighter fighter in fighters)
            {

                WeaponConfig enemyWeaponConfig = fighter.currentWeaponConfig;
                fighter.AttachWeapon(enemyWeaponConfig);

                // if the fighter is an enemy, and his recent health took a hit, aggregate others in the group:
                // NOTE: ONLY USE "CompareTag" HERE, BECAUSE FindWithTag 
                // WILL ONLY RETURN THE FIRST ENEMY, NOT EVERYONE INVOLVED, WHICH IS NOT WHAT WE WANT!
                if (fighter.CompareTag("Enemy"))
                {
                    Health fighterHealth = fighter.GetComponent<Health>();

                    if (fighterHealth != null)
                    {
                        fighterHealth.OnTakenHit += OnTakenHitEventHandler;
                        // when the enemy dies, or his health is nullified, unsubscribe from the event:
                        fighterHealth.onDie.AddListener((/* no parameters for this lambda expression, just the content function of this lambda expression after the arrow */) => OnFighterDeath(fighterHealth));
                    }
                Debug.Log("AggroGroup Activated");
                }
            }
        }

        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);
            fighter.enabled = isActivated;
            fighter.GetComponent<Health>().OnTakenHit += OnTakenHitEventHandler; // NEW LINE
            if (fighter.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;

        }

        public void RemoveFighterFromGroup(Fighter fighter) {

            // Remove fighters from the list
            fighters.Remove(fighter);

        }

        void OnTakenHitEventHandler(GameObject instigator)
        {
            Debug.Log("Enemy was hit by: " + instigator.name); // so far, this one works

            // Hit registered, now get everyone in the group to chase the player down:
            if (instigator.CompareTag("Player")) 
            {
                foreach (Fighter fighter in fighters) 
                {
                    if (fighter.CompareTag("Enemy")) 
                    {
                        EnemyStateMachine stateMachine = fighter.GetComponent<EnemyStateMachine>();
                        stateMachine.SwitchState(new EnemyChasingState(stateMachine));
                    }
                }
            }
        }

        void OnFighterDeath(Health health)
        {
            health.OnTakenHit -= OnTakenHitEventHandler;
        }

    }

}

And… the AggroGroup members still won’t chase him down if he hit one of their friends :sweat_smile:

I’ll just leave this here (I know it’s from this script because the glitch is good as gone for anyone not in an AggroGroup), in case you or @bixarrio would like to have a look - this is what I can come up with before my brain got fried…

Edit
Nevermind any of this, there’s been a lot happening since I started writing this post and much of it is now pointless


You’re disabling the navMesh when enemies should not attack, so they won’t patrol.


The AggroGroup from the course should work just fine. Not sure why you have so much other stuff here. Looks like the only things you have extra is the ability to add enemies at runtime, the ability to stay aggro through player respawns and something about showing the weapon…

To simplify adding an enemy at runtime, I’d just move the code from (the original) Activate() into a ‘single enemy’ function. You’d also keep track of the current active state (as you’ve done)

public void Activate(bool shouldActivate)
{
    fighters.RemoveAll(f => f == null);

    foreach (Fighter fighter in fighters)
    {
        ActivateFighter(fighter, shouldActivate);
    }
    isActive = shouldActivate; // keep track of current state
}

private void ActivateFighter(Fighter fighter, bool shouldActivate)
{
    CombatTarget target = fighter.GetComponent<CombatTarget>();
    if (target != null)
    {
        target.enabled = shouldActivate;
    }
    fighter.enabled = shouldActivate;
}

Then, when you add an enemy, just activate them with the new function

public void AddFighter(Fighter fighter)
{
    if (fighters.Contains(fighter)) return;
    fighters.Add(fighter);
    ActivateFighter(fighter, isActive);
}

And when you remove a fighter, you’d probably need to deactivate that fighter

public void RemoveFighter(Fighter fighter)
{
    // Can't properly deactivate a 'null' fighter
    if (fighter == null) return;
    ActivateFighter(fighter, false);
    fighters.Remove(fighter);
}

I reversed literally everything and decided to start from scratch, just to follow along with what you’re saying :slight_smile: - I’ll test it and comment after that the update

Privacy & Terms