Merging the Third Person Controller Course with the RPG Course

I still haven’t had a chance to go back through the banking and introduce it to the third person project.

I’m glad I could ignore that insanely long post because I was well and truly lost scrolling through it.

It’s just there for documentation purposes, so if I know I have something gone terribly wrong, I’ll know what the source is… When you guys don’t respond for a while, I just go and try solve my own problems alone if I can (not the case with banking. That one is too advanced for me… I easily got lost multiple times with that one). Usually it’s either I solve them, and this turns into an insanely long or heavily edited post (like above), or you get to see the problem before I solve it, and I manage to get professional help :slight_smile:

I honestly see this as a win situation, as it gives me a chance to REALLY investigate my own code and get better at this skill :grinning_face_with_smiling_eyes:


@Brian_Trotter another question for me to crack on for today. If I want to get groups of enemies to fight against one another, so let’s say I get one of them to attack one guy from a different AggroGroup, and now that entire AggroGroup wants to hurt the other guy who attacked their friend, and now his friends want to fight that other group (to defend their friend), and now it turns into clan wars… how do you recommend we approach this one? I honestly want to see this happen (I want to keep it optional as well, so it will need to be a boolean)

So the idea is, whilst I do love the entire ‘Enemies fighting each other approach’, I want to limit this to be between AggroGroups, so enemies in the same group can safely ignore accidents by their friends, but they won’t ignore it from enemies who are not in the same ‘AggroGroup’ as them (or enemies not in an AggroGroup at all), and now everyone in the AggroGroup of the enemy you just hit will attempt to chase you down

and… that was my plan on getting species to fight one another. I don’t want them to ignore the hit (if enemies are too strong, this can actually be a good way to win the fight), just ignore the fact that you’ve been hit if he’s in the same ‘AggroGroup’ as you

Again, optional, so it must have a boolean of some sort (probably in ‘AggroGroup.cs’, something like ‘membersHateEachOther’ or something)

My personal approach would be as follows:

  1. Create a List that fills up with every enemy that hit the current target.
  2. If they are in the same AggroGroup (I already found a way to get the AggroGroup on the enemy. I’ll leave it below, if needed), then ignore the first one.
  3. If that enemy hits his AggroGroup friend again, the ‘AggroGroup friend’ will attack that enemy to death this time.
  4. If they’re not in the same AggroGroup, attack them the first time they attack you
  5. Everyone who is on the list, and is killed, gets their data deleted off the list of the killer

This way the enemy can take care of all accidents done by friends or foes (but I’m not so sure how to code that…)

Here is how I got the AggroGroup on the Enemy:

// in 'EnemyStateMachine.cs':

    // TEST - AggroGroup for enemies (so that enemies in the same AggroGroup can safely ignore accidents by teammates)
    [field: Tooltip("the AggroGroup this enemy is in, if he's in one (it's automatically updated in 'AggroGroup.cs', using the 'SetAggroGroup()' function here, so don't touch it!)")]
    [field: SerializeField] public AggroGroup AggroGroup {get; private set;}

    public void SetAggroGroup(AggroGroup aggroGroup)
    {
        this.AggroGroup = aggroGroup;
    }

    public AggroGroup GetAggroGroup()
    {
        if (AggroGroup == null) return null;
        return AggroGroup;
    }

// in 'AggroGroup.cs':

// new 'AddFighterToGroup()' and 'RemoveFighterFromGroup()', with the AggroGroup assignments:
        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:

            // TEST:
            // for NPC fights, we don't want enemies to be reactive to hits from enemies in the same 'AggroGroup', and the first step is to tell them who is with them in the same group:
            enemy.SetAggroGroup(this);

            enemies.Add(enemy);
            enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
        }

        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

            // TEST:
            // cleaning up dead enemies from the 'AggroGroup' (this will be useful later, when we have enemies that drop out of a players' AggroGroup or something (friendly and enemy NPCs), as an example):
            enemy.SetAggroGroup(null);

            enemies.Remove(enemy);
            enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
        }

// new 'CheckForMember()' to help identify who is on the list:
        // Check for members on the list of enemies:
        public bool CheckForMember(EnemyStateMachine enemy) 
        {
            return enemies.Contains(enemy);
        }
1 Like

Looks like you’re on the right track.

You didn’t list your TakeDamage in Aggrogroup, but here’s the broad swath of what it needs to do:

  • Check to see if the damageDealer that hit you is within your Aggrogroup. If it is, do nothing.
  • If the damageDealer is NOT in the aggrogroup, then you need to aggrevate, but you need to set each member of the Aggrogroup’s LastAttacker with enemy.SetLastAttacker. It doesn’t matter if it’s the player or not because the player being the last attacker will have the same effect as the “Aggro” countdown token, it will make the members mad at the player.

Before we start, I’ll admit… I probably broke the ‘Aggro’ cooldown timer somewhere, because right now it only works when you’re out of range…

[SOLVED (at least the basic one): Read Comment Below this one]

@Brian_Trotter OK I’m a little bit struggling to comprehend this… Can you please give me a hint or elaborate a little further? I’m genuinely confused here :sweat_smile: (and I truly have absolutely no clue where in the world do I start coding for this functionality to work. Like literally, I’m incredibly confused… I can’t find where on earth do I go to write the code to get the enemy to ignore the instigator, if he’s part of the same aggroGroup as the NPC that just got hit. That’s where I’m struggling the most)

I think this is taken care of by a variable I have known as ‘groupHatesPlayer’, which I might swap it’s name out for ‘groupHatesInstigator’ (since now we are dealing with Player or potential NPC enemies)

My idea is simple. Each AggroGroup has a list of enemies, if one enemy from one group fires at an enemy from another group, each enemy from the triggered group will aim to go for an enemy from the list of the instigator members’ group, by index (so first index enemy aims for their first index enemy), and we shall ignore the enemy that is already in the fight

If there are leftover enemies, they can aim to help any of their friends at random

If the player is involved, the enemies will aim to kill their enemies first, and the leftovers will aim at the player when they have a chance

If this sounds too confusing, let’s just stick to whatever I was initially aiming for. I just want an optimal solution to get these enemies fighting each other in groups, or them ganging up against an outsider who did one of them wrong (and a way to deal with groups, should there be a bunch of enemies involved from each/either of the groups in war, which is what I was trying to convey in the paragraphs above, right below the quote to your comment)… that would really make the AI a whole lot more fun :slight_smile:


[Read this part IF YOU WISH, but feel free to ignore… I’m just documenting how I fixed another bunch of bugs]

I also had a major problem with my enemies not being able to fire proper arrows at the player, from Patrolling mode, especially the hostile ones, and whilst it took me hours to fix it, I think I finally solved the riddle. Here’s what I did:

  1. In ‘Projectile.cs’, I added a few conditions for null statements (I don’t fully understand why ‘GetTarget()’ in ‘EnemyStateMachine’ would release null statements, but it does (and when it does, the enemies fire no arrows from their hands, so I had to compensate for that… you’ll see the code below)… so I did a few hot-fixes for it. First, this is my current ‘EnemyStateMachine.GetTarget()’:
    public GameObject GetTarget()
    {
        // TEST: If the last attacker is not the player, then that's the target:
        if (LastAttacker != GameObject.FindWithTag("Player")) return LastAttacker;
        // the target for the enemy launching a projectile (which is basically the player in this case)
        return Player;
    }

so if you’re not the Player, your target will be the enemy

and then I had to make some compensations in ‘Shoot()’, as follows, because the enemies sometimes just don’t know who to fire at, so their arrows will literally fly above or beyond the real target. Like I said, it completely misses the y-axis. So I added this in ‘Shoot()’, before the if statements for the players’ targeting conditions:

If you didn’t read this, at least please tell me how else can I get rid of having to use the null?

            // this 'null' is really troubling me, because other functions needing it have to use it this way too... any other way to go around it?
            if (targetObject == null && GetComponent<EnemyStateMachine>()) 
            {
                targetObject = GameObject.FindWithTag("Player");
            }

and then I realized I have a real problem with hostility, because the ranger would literally track me down for miles, even if I was out of his chase range. To fix this, I changed a single line in ‘EnemyBaseState.ShouldPursue()’. From this:

if (stateMachine.IsAggro()) return true;

to this:

if (stateMachine.IsAggro() && IsInChaseRange()) return true;
if (stateMachine.IsHostile && IsInChaseRange()) return true;

I spent my entire day today just fixing bugs that came out because of those NPC fights :slight_smile: - anyway, back to investigating other bugs and fixing them, I shall go

NOTE: All of this is done after setting up the link to the AggroGroup, as shown in the following quote:

Apparently after breaking fast, you may or may not be able to think properly, and suddenly get the AggroGroup integrated with the NPC fights (so now NPCs in the same group will defend themselves, and their friends, from outside attacks). Here’s what took me 2 days to find:

// in 'AggroGroup.cs', modify 'OnTakenHit':

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

            foreach (EnemyStateMachine enemy in enemies) 
            {
                if (enemy != null && enemy.GetAggroGroup() == this) enemy.SetHostile(true, instigator);
            }
        }

so basically, if there are enemies in the ‘AggroGroup’, and they’re in the same AggroGroup, then get the enemies hostile against the instigator

BUT… This will require a little bit of tuning in ‘EnemyStateMachine.SetHostile()’. First of all, you will need an ‘attacker’ (or ‘instigator’ for consistency) input. So basically, if someone attacked whoever was attacked in the AggroGroup, we shall set him up as the last Attacker, as follows (I kept this as an optional parameter, so as to avoid messing it up with other calls currently running):

    public void SetHostile(bool IsHostile, GameObject attacker = null) 
    {
        this.IsHostile = IsHostile;
        if (attacker != null) SetLastAttacker(attacker);
    }

So now, we got a link from the AggroGroup to setting up the group against the attacker, but there’s a small glitch that we have:

If the instigator was, by accident, the attacker himself, now that instigator will keep attacking and punishing himself, because he accidentally attacked his teammate. We don’t want this mechanic. So… I added this at the top of ‘SetLastAttacker()’:

        if (instigator == gameObject) return;

Now, he won’t attack himself any longer

The next mechanic I will want is a way to ignore accidental hits from teammates, or I may not want that… I’m still debating on it (I wasn’t expecting to figure this out today tbh)

I still haven’t figured this one out though. I figured the second one out, combined with some miraculous bug-fixing, but I can’t figure the first one out… Please help (and then we can return to the bank, as I’ll probably be gone for a day or two to fix other bugs :slight_smile:)

and… I have a second death bug. In other words, when the player/enemy targeted is dead, they can be killed by arrows twice and drop the same loot twice. I need to find a way to get the arrows to pass over the player as if they don’t exist when they’re dead

There are LOTS of ways to achieve this… It appears, however, that you already have a CheckForMember() method…

BEFORE damaging a Health, check to see if it is Dead… if(otherHealth.IsDead) return;
It’s…
really…
that…
simple…

It should have already been in both Fighter.TryHit() and Projectile.OnTriggerEnter() (at least I’m pretty sure I put it in the course project).

[NO PROBLEMS IN THIS POST, THE NEW SET OF PROBLEMS IS IN THE NEXT COMMENT]

I went through your lesson #40 again, and… I don’t think you placed any death condition checks there, under ‘Projectile.OnTriggerEnter()’. Just a heads-up :slight_smile: (I can take a wild guess what goes where though, so there’s that. I’ll try it out and update this comment)

And… yup, adding this line:

                if (other.GetComponent<Health>().IsDead()) return;

at the top of this block:

            if (other.TryGetComponent(out Health health) && !alreadyHit.Contains(health))
            {
                if (other.GetComponent<Health>().IsDead()) return;
                alreadyHit.Add(health);
                health.TakeDamage(instigator, damage, instigator.GetComponent<Fighter>().GetCurrentWeaponConfig().GetSkill());
            }

fixed the problem :slight_smile:

yes, I do, but… where do you put that call is my big question. I tried countless spots yesterday but I still can’t figure it out… I literally created that function just for that purpose, but never got to use it yet (because I don’t know where it goes…!)

Once this is done, we can go back to the bank problem so we can get this out of the way as soon as possible (this is essential for me. Meanwhile I’ll be trying to make an AggroGroup diversion for the player, so he can have friends that can help him in his fights. It’s good that we have enemy groups that’ll fight anyone who hurts a friend of theirs, but I personally also want a way to get NPCs to befriend the player as well, for those intense boss fights that I personally want to make quite hard for just the player alone)


Edit: I think I figured it out… @Brian_Trotter please review if this is fine, or it can be done in a better way/can cause future issues. Here’s all I had to do (after some thinking). In ‘EnemyAttackingState.cs’, I went and placed this in Tick(), right below getting the normalized time, before we check for the combo attacks:

            // TEST - if the instigator (an enemy) and the victim are from the same AggroGroup, ignore the hit from the instigator:
            if (stateMachine.LastAttacker != null && stateMachine.LastAttacker.TryGetComponent(out EnemyStateMachine attackerStateMachine) && attackerStateMachine.GetAggroGroup() == stateMachine.GetAggroGroup())
            {
                stateMachine.ClearLastAttacker();
                return;
            }

and then I tuned the ‘OnTakenHit’ in ‘AggroGroup.cs’:

void OnTakenHit(GameObject instigator)
        {
            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && instigator != null && instigator.TryGetComponent(out EnemyStateMachine instigatorStateMachine) && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() == this)
                {
                    Debug.Log("Accidental attack from within the group. Ignore");
                }
                else if (enemy != null && enemy.GetAggroGroup() == this)
                {
                    Debug.Log("Attack from outside, fight back");
                    enemy.SetHostile(true, instigator);
                }
            }
        }

and now, enemies can safely ignore any attacks coming from the same AggroGroup (I’ll still keep the ‘CheckForMembers()’ function in ‘AggroGroup.cs’. You never know when it can come in handy). Now, let me ensure that the accidental hits are limited, and anything beyond 2 accidents will start a fight (I solved that too. The way it currently works, is if the enemy has been hit by a different enemy or has killed his target, the accidentHitCounter (the recorder of how many hits have happened) is reset (the checker number is hard-coded though. I don’t even know if I want this in or not, but for the time being it’s there))

Anyway, now we can finally have a look at the bank :slight_smile: (my big guess is, the deletion of ‘ShowHideUI.cs’ is what’s causing the mayhem for me… I still want to get that bank back though, this thing is a dealbreaker for me)

Question, though… So far, I have successfully got my enemies to get into clan wars like I always wanted, but… I realized that EVERYONE on the team goes for the guy who instigated the fight, instead of spreading out and actually covering up against other allies of the opposing team, which really makes combat boring and predictable, and probably easier than needed for the enemies

This is my goal: in ‘AggroGroup.cs’, I want to change ‘OnTakenHit()’ a little bit.

I installed a new variable in ‘EnemyStateMachine.cs’, called ‘HasOpponent’, that is called in ‘EnemyStateMachine.SetHostile’, which currently looks like this:

    public void SetHostile(bool IsHostile, GameObject attacker = null) 
    {
        this.IsHostile = IsHostile;
        if (LastAttacker != null) ClearLastAttacker();

        if (attacker != null) {
            SetLastAttacker(attacker);
            // TEST
            attacker.GetComponent<EnemyStateMachine>().SetHasOpponent(true);
        }
    }

and there’s also a Getter and Setter for ‘HasOpponent()’ in ‘EnemyStateMachine.cs’:

    public bool GetHasOpponent()
    {
        return HasOpponent;
    }

    public void SetHasOpponent(bool hasOpponent)
    {
        this.HasOpponent = hasOpponent;
    }

so that everyone knows that this enemy has been occupied by someone else, and that they can go search for someone else to fight

As for the AggroGroup, I tried changing ‘OnTakenHit()’ up to search for enemies accordingly. This is the previous one (the original one, which works perfectly fine for my needs for now, BEFORE being able to spread the enemies out):

void OnTakenHit(GameObject instigator) // WORKS - DO NOT DELETE!
        {
            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && instigator != null && instigator.TryGetComponent(out EnemyStateMachine instigatorStateMachine) && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() == this)
                {
                    Debug.Log("Accidental attack from within the group. Ignore");
                }
                else if (enemy != null && enemy.GetAggroGroup() == this)
                {
                    Debug.Log("Attack from outside, fight back");
                    enemy.SetHostile(true, instigator);
                }
            }
        }

and this is the new one (which doesn’t work to get the enemies spread out. In fact, it introduces some serious bugs relevant to the health):

void OnTakenHit(GameObject instigator) 
        {
            foreach (EnemyStateMachine enemy in enemies) 
            {
                if (enemy != null && enemy.GetAggroGroup() == this && instigator != null && instigator.TryGetComponent(out EnemyStateMachine instigatorStateMachine) && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() == this) 
                {
                    Debug.Log("Accidental attack from within the group. Ignore");
                }
                else if (enemy != null && enemy.GetAggroGroup() == this) 
                {
                    Debug.Log("Attack from outside, fight back!");
                    if (!instigator.GetComponent<EnemyStateMachine>().GetHasOpponent()) enemy.SetHostile(true, instigator);
                    else 
                    {
                        // if the instigator has an opponent, search the list for another member without an opponent and aim for the closest one of these members
                        var availableEnemies = enemies.Where(enemy => !enemy.GetHasOpponent()).ToList();
                        if (availableEnemies.Count > 0) 
                        {
                            // Sort available enemies by distance from the instigator:
                            var nearestEnemy = availableEnemies.OrderBy(enemy => Vector3.Distance(enemy.transform.position, instigator.transform.position)).FirstOrDefault();
                            enemy.SetHostile(true, nearestEnemy.gameObject);
                        }
                        else {
                            // if you can't find any, aim for the nearest target
                            var nearestEnemy = enemies.OrderBy(enemy => Vector3.Distance(enemy.transform.position, instigator.transform.position)).FirstOrDefault();
                            enemy.SetHostile(true, nearestEnemy.gameObject);
                        }
                    }
                }
            }
        }

The logic I believe I have achieved is as follows:

  1. When this enemy, which is a part of this AggroGroup, has taken a hit, determine if it’s from a group member or not. If so, ignore, unless they fire again (that’s when you get into a fight)

  2. If that group member does not have an opponent, and the enemy is from another AggroGroup, aim for the enemy without an opponent

  3. If that enemy has an opponent, find another one

  4. If you can’t find an empty opponent, aim for the nearest one, even if it’s occupied

the more accurate logic though, at least what I believe I achieved, can be seen in the comments within the code itself, in the block above

BUT… When I run it, the arrows go through the enemies that seem to be unkillable, even after their health is zero (I wanted to take the RangeFinder approach at first, on my enemies, but this sounded like overengineering and overcomplicating a problem, so I reversed that attempt)

Can you please have a quick glimpse and see if you can spot any mistakes, as to why the enemies no longer die and this script isn’t working? Would really appreciate some help getting this to work (I’m trying it on my own by all means, and if I find something I’ll update this comment)


All I’m trying to do is, although the AggroGroup goal will be common for all enemies in the opposing AggroGroup (once you attack one of them), I want to assign enemies to kill (from the opposing AggroGroup) individually, based on who is the closest, and is free to fight (if they’re not free to fight, fight the closest one, even if he’s occupied. If the player is involved, the closest enemies to him (in ‘PlayerChasingRange’, who are not part of his AggroGroup, will attack him)

But because it’s all done in a foreach loop, this is a bit of a headache… I can REALLY use some suggestions for alternative approaches or code snippets here

Edit (14 Hours later): For a moment, I got it to work… now it’s giving me NREs that got the whole thing sliding downwards… Since the last time I updated this comment, there have been some drastic changes in my code to make it happen (OK I kinda give up, this is a bit too hard for my slow brain… Any ideas on how to solve this?!)

AI is a tricky thing, and what you’re aiming to do is quite complicated (and you keep changing the specificaions, which starts to leave massive corner cases that slow things down or lead to serious bugs).

Lots of your issues are because of exactly that… code snippets rather than the whole picture. I’m trying to help where I can but I don’t have YOUR code in my project. With so many side projects and 500 post long megathreads, and edits of edits in the post keeping up is impossible.

Most of your issues have stemmed (I think) from using Respawnables, and wanting Respawnables to easily interact with AggroGroups… On top of that, adding additional functionality into those aggrogroups far beyond the course, it’s leading to bugs and unintended side efffects. This is also one of those cases that would benefit from being under source control. I often, for example, will make major changes to the code, realize it fails my tests or that the feature is not improving the game, and then simply roll the code back to a stable point where the code was working.

I suspect this is a something that will need a complete rethink on how to achieve the goal.

[I SOLVED EVERYTHING MENTIONED HERE. Please Feel free to skip if you wish, or read if you wish… it’s up to you, but I’ll move on to the final step before trying something new, xD]

believe it or not, I got to a point where they actually are able to identify the enemies on the opposing team (which took me an entire day to figure it out), and actually go and fight them… However, when they die, all goes down in this part of the system, because they no longer care about who is getting attacked when they respawn, and the instigators never bother to refresh the important data for some reason… The solution, though, is like really… REALLY… REALLYREALLY… long

I can’t blame anyone but myself tbh, but I want this to work on the long run :slight_smile: - so I can make combat entertaining (which is the whole point of this project)

If it helps though, to post my entire code regarding this problem, by all means I’m up for it, if you (or anyone proficient with this project and it’s ins and outs) willing to have a look. If not, completely understandable :slight_smile:

regarding that, just a side note… if I edit something, it’s not necessary for you to go investigate it (unless you want to). I will always keep the most important information out there for you to see, because if I need your help, I want to make it as easy for you as possible to see what the problem is, which is why my answers are always quite detailed… The edits are usually just mistakes or fixed problems, nothing more

Anyway, if you’re interested in having a look (and that’s entirely optional. I can’t ask you to prioritize me over your real students… I’ll eventually have to fix this myself if nobody else can help), I’ll leave the new code I integrated (and trying to fix) in my ‘AggroGroup.cs’ and ‘EnemyStateMachine.cs’:

here’s AggroGroup.cs (I also created two helper functions to help me get what I need, the closest unassigned enemies (the priority for the AI) and the closest enemies (these will be what the AI aims for, if all other enemies are occupied):

        public List<EnemyStateMachine> GetGroupMembers() 
        {
            if (enemies == null) return null;
            else return enemies;
        }

void OnTakenHit(GameObject instigator)
        {
            if (instigator.GetComponent<PlayerStateMachine>())
            {
                foreach (EnemyStateMachine enemy in enemies)
                {
                    if (enemy != null && enemy.GetAggroGroup() == this && enemy.GetInitialHostility)
                    {
                        enemy.SetHostile(true, instigator);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards the player.");
                    }
                }
                return;
            }

            List<EnemyStateMachine> unassignedEnemies = enemies.Where(enemy => enemy != null && !enemy.HasOpponent).ToList();

            AggroGroup attackerAggroGroup = null;
            if (instigator.GetComponent<EnemyStateMachine>() != null)
            {
                attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>().GetAggroGroup();
            }

            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent) // you're part of the victim group, with nobody to fight:
                {
                    if (attackerAggroGroup != null)
                    {
                        EnemyStateMachine nearestUnassignedEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
                        if (nearestUnassignedEnemy != null)
                        {
                            enemy.SetHostile(true, nearestUnassignedEnemy.gameObject);
                            enemy.SetOpponent(nearestUnassignedEnemy.gameObject);
                            enemy.SetHasOpponent(true);
                            nearestUnassignedEnemy.SetHostile(true, enemy.gameObject);
                            nearestUnassignedEnemy.SetOpponent(enemy.gameObject);
                            nearestUnassignedEnemy.SetHasOpponent(true);
                            Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestUnassignedEnemy.gameObject.name} in the attacker's group.");
                            continue;
                        }
                    }

                    EnemyStateMachine nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies);
                    if (nearestEnemy != null)
                    {
                        enemy.SetHostile(true, nearestEnemy.gameObject);
                        enemy.SetOpponent(nearestEnemy.gameObject);
                        enemy.SetHasOpponent(true);
                        nearestEnemy.SetHostile(true, enemy.gameObject);
                        nearestEnemy.SetOpponent(enemy.gameObject);
                        nearestEnemy.SetHasOpponent(true);
                        unassignedEnemies.Remove(nearestEnemy);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name} in the victim group.");
                        continue;
                    }

                    EnemyStateMachine closestEnemy = GetClosestEnemy(enemy);
                    if (closestEnemy != null)
                    {
                        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.");
                    }
                }
            }
        }

        EnemyStateMachine GetNearestUnassignedEnemy(EnemyStateMachine enemy, List<EnemyStateMachine> unassignedEnemies)
        {
            EnemyStateMachine nearestUnassignedEnemy = null;
            float nearestDistance = Mathf.Infinity;

            foreach (EnemyStateMachine unassignedEnemy in unassignedEnemies) 
            {
                if (unassignedEnemy != null && !unassignedEnemy.GetOpponent() && unassignedEnemy.GetAggroGroup() != this) 
                {
                    float distance = Vector3.Distance(enemy.transform.position, unassignedEnemy.transform.position);
                    if (distance < nearestDistance) 
                    {
                        nearestDistance = distance;
                        nearestUnassignedEnemy = unassignedEnemy;
                    }
                }
            }
            return nearestUnassignedEnemy;
        }

        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;
        }
 

And I also created a solution to get the aggroGroup of the enemy that did the damage, and a new way to define whether an enemy is occupied or not, using ‘Opponents’. Here is the current ‘EnemyStateMachine.cs’ script as we speak:

using RPG.Attributes;
using RPG.Combat;
using RPG.Control;
using RPG.Movement;
using RPG.Stats;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
using System.Linq;
using RPG.States.Player;

namespace RPG.States.Enemies {

public class EnemyStateMachine : StateMachine, ITargetProvider
{
    [field: SerializeField] public Animator Animator {get; private set;}
    [field: SerializeField] public CharacterController CharacterController {get; private set;}
    [field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
    [field: SerializeField] public NavMeshAgent Agent {get; private set;}
    [field: SerializeField] public PatrolPath PatrolPath {get; private set;}
    [field: SerializeField] public Fighter Fighter {get; private set;}
    [field: SerializeField] public BaseStats BaseStats {get; private set;}
    [field: SerializeField] public Health Health {get; private set;}
    [field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}


    [field: SerializeField] public float FieldOfViewAngle {get; private set;} = 90.0f;
    [field: SerializeField] public float MovementSpeed {get; private set;} = 4.0f;
    [field: SerializeField] public float RotationSpeed {get; private set;} = 45f;
    [field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.1f;
    [field: SerializeField] public float AnimatorDampTime {get; private set;} = 0.1f;

    [field: Tooltip("When the player dies, this is where he respawns to")]
    [field: SerializeField] public Vector3 ResetPosition {get; private set;}

    // TEST - Aggrevating others in the 'PlayerChaseRange' range:
    [field: Tooltip("Regardless of whether the enemy is part of an AggroGroup or not, activating this means every enemy in 'PlayerChasingRange', surrounding the attacked enemy, will aggrevate towards the player for 'CooldownTimer' seconds the moment the enemy is hit, and then they'll be quiet if the player doesn't fight them back. If the player attacks the enemies though, the longer the fight goes on for, the longer they will aggrevate towards him (because CooldownTimers' append is set to true))")]
    [field: SerializeField] public bool AggrevateOthers { get; private set; } = false;

    // TEST - Testing for Aggrevated Enemy toggling (called in 'OnTakenHit' below, 'EnemyPatrolState.cs', 'EnemyDwellState.cs' and 'EnemyIdleState.cs'):
    [field: Tooltip("Is this Enemy supposed to hate and want to kill the player?")]
    [field: SerializeField] public bool IsHostile {get; private set;} // for enemies that have hatred towards the player
    [field: Tooltip("For how long further will the enemy be mad and try to attack the player?")]
    [field: SerializeField] public float CooldownTimer {get; private set;} = 3.0f; // the timer for which the enemies will be angry for
    
    // TEST - Who is the Last Atacker on this enemy? (NPC Fights):
    [field: Tooltip("The last person who attacked this enemy, works with 'OnDamageInstigator' in 'Health.TakeDamage()'")]
    public GameObject LastAttacker {get; private set;}

    // TEST - How many accidental hits can the AggroGroup member take in, before fighting back:
    [field: Tooltip("How many accidental hits can this enemy take, from a friend in the same AggroGroup, before eventually fighting back? (hard-coded limit to 1 accidental hit. Second one triggers a fight). Handled in 'EnemyAttackingState.cs'\n(Will reset if another NPC/Player attack this enemy, or the LastAttacker is dead\n(DO NOT TOUCH, IT AUTOMATICALLY UPDATES ITSELF!))")]
    [field: SerializeField] public int accidentHitCounter {get; private set;}

    // Function:
    public bool IsAggro => CooldownTokenManager.HasCooldown("Aggro"); // enemies are aggrevated through a timer-check. In other words, they can only be angry for some time

    public float PlayerChasingRangedSquared {get; private set;}
    public GameObject Player {get; private set;}

    public Blackboard Blackboard = new Blackboard();

    [field: Tooltip("When the game starts, is this enemy, by default nature, angry towards the player?")]
    [field: SerializeField] public bool InitiallyHostile {get; private set;}

    [field: Header("Don't bother trying to change this variable anymore.\nWhen the game starts, It's controlled by either 80% of the Target Range of the Enemies' Weapon,\nor 100% of the weapon Range of the Enemies' Weapon, depending on who is higher.\nAll changes are done in 'EnemyStateMachine.HandleWeaponChanged,\nand subscribed to 'Fighter.OnWeaponChanged' in 'EnemyStateMachine.Awake'")]
    [field: SerializeField] public float PlayerChasingRange { get; private set; } = 10.0f;

    [field: Header("Do NOT touch this variable, it is here for debugging and is automatically updating itself!")]
    [field: SerializeField] public AggroGroup AggroGroup { get; private set; }

    [field: Header("Does this enemy have an opponent, or should an aggrevated NPC hunting enemies in the aggroGroup of this enemy hunt him down?")]
    [field: SerializeField] public bool HasOpponent {get; private set;}
    [field: Header("Who is the opponent this NPC is facing?")]
    [field: SerializeField] public GameObject Opponent {get; private set;}

    [field: Tooltip("AUTOMATICALLY UPDATES... This variable tells the developer who the enemy aggroGroup is, based on who this enemy is attacking, so the rest can follow lead (based on this variable from the first enemy on the list)")]
    [field: SerializeField] public AggroGroup EnemyAggroGroup {get; private set;}

        private void OnValidate()
    {
        if (!Animator) Animator = GetComponentInChildren<Animator>();
        if (!CharacterController) CharacterController = GetComponent<CharacterController>();
        if (!ForceReceiver) ForceReceiver = GetComponent<ForceReceiver>();
        if (!Agent) Agent = GetComponent<NavMeshAgent>();
        if (!Fighter) Fighter = GetComponent<Fighter>();
        if (!BaseStats) BaseStats = GetComponent<BaseStats>();
        if (!Health) Health = GetComponent<Health>();
        if (!CooldownTokenManager) CooldownTokenManager = GetComponent<CooldownTokenManager>();
    }

    private void Awake() 
    {
        if (Fighter) Fighter.OnWeaponChanged += HandleWeaponChanged;
    }

    private void Start() 
    {
        // Initially, enemy hostility = enemy initialHostility
        IsHostile = InitiallyHostile;

        Agent.updatePosition = false;
        Agent.updateRotation = false;

        ResetPosition = transform.position;

        PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
        Player = GameObject.FindGameObjectWithTag("Player");
        Blackboard["Level"] = BaseStats.GetLevel(); // immediately stores the enemies' combat level in the blackboard (useful for determining the chances of a combo, based on the enemies' attack level, in 'EnemyAttackingState.cs', for now...)
        
        // The following check ensures that if we kill an enemy, save the game, quit and then return later, he is indeed dead
        // (the reason it's here is because 'RestoreState()' happens between 'Awake()' and 'Start()', so to counter for the delay, we do it in start too)
        if (Health.IsDead()) SwitchState(new EnemyDeathState(this));
        else SwitchState(new EnemyIdleState(this));

        Health.onDie.AddListener(() => 
        {
            SwitchState(new EnemyDeathState(this));
        });

        Health.onResurrection.AddListener(() => {
            SwitchState(new EnemyIdleState(this));
        });

        Health.OnDamageTaken += () => 
        {
            // TEST (if statement and its contents only - the else statement has nothing to do with this):
            if (AggrevateOthers)
            {
            foreach (Collider collider in Physics.OverlapSphere(transform.position, PlayerChasingRange).Where(collider => collider.TryGetComponent(out EnemyStateMachine enemyStateMachine)))
            {
                collider.GetComponent<EnemyStateMachine>().CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
            }
            }
            CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
        };

        // The enemy will aim next to whoever attacked him last (if it's the player, aim for him. If it's another enemy, aim for him too)
        Health.OnDamageInstigator += SetLastAttacker;

        ForceReceiver.OnForceApplied += HandleForceApplied;

    }

    public void AssignPatrolPath(PatrolPath newPatrolPath)
    {
        PatrolPath = newPatrolPath;
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, PlayerChasingRange);
    }

    private void OnEnable()
    {
        Health.OnTakenHit += OnTakenHit;
    }

    private void OnDisable()
    {
        Health.OnTakenHit -= OnTakenHit;
    }

    private void OnTakenHit(GameObject instigator)
    {
        CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true); // 'true' in the end basically allows the enemy to add up to his total anger time
    }

    // public bool IsAggresive => CooldownTokenManager.HasCooldown("Aggro");

    private void HandleForceApplied(Vector3 force) 
    {
        if (Health.IsDead()) return;
        // Disable comments below if you want impact states to be random for the enemy:
        // float forceAmount = Random.Range(0f, force.sqrMagnitude);
        // if (forceAmount > Random.Range(0f, BaseStats.GetLevel())) {
        SwitchState(new EnemyImpactState(this));
        // }
    }

    public GameObject GetTarget()
    {
        // TEST: If the last attacker is not the player, then that's the target:
        if (LastAttacker != GameObject.FindWithTag("Player")) return LastAttacker;
        // the target for the enemy launching a projectile (which is basically the player in this case)
        return Player;
    }

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

    /// <summary>
    /// Who is this NPC hostile towards, and which AggroGroup do they belong to...?!
    /// </summary>
    /// <param name="IsHostile"></param>
    /// <param name="attacker"></param>
    public void SetHostile(bool IsHostile, GameObject attacker = null)
    {
        this.IsHostile = IsHostile;
        if (LastAttacker != null) ClearLastAttacker();
        
        if (attacker != null)
        {
        SetLastAttacker(attacker);
        // (TEST) get the enemyAggroGroup, so that we can easily access the enemies and get each individual enemy in an AggroGroup to hunt a single guy:
        // if the attacker is the player, or an enemy with no aggroGroup, return 'EnemyAggroGroup' = null:
        if (attacker.GetComponent<PlayerStateMachine>() || attacker.GetComponent<EnemyStateMachine>().GetAggroGroup() == null || attacker.GetComponent<Health>().IsDead()) EnemyAggroGroup = null;
        // if it's an enemy with an AggroGroup, get that AggroGroup:
        else EnemyAggroGroup = attacker.GetComponent<EnemyStateMachine>().GetAggroGroup();
        }
    }

    public bool GetInitialHostility => InitiallyHostile;

    public void _ResetPosition() 
        {
        Agent.enabled = false;
        CharacterController.enabled = false;
        transform.position = ResetPosition;
        Agent.enabled = true;
        CharacterController.enabled = true;
        }

    // The code below changes the 'PlayerChasingRange' based on the weapon in-hand. I want the 'PlayerChasingRange' to be what I want, so I commented it out:
    private void HandleWeaponChanged(WeaponConfig weaponConfig) 
    {
        if (weaponConfig) 
        {
            PlayerChasingRange = Mathf.Max(weaponConfig.GetTargetRange() * 0.8f, weaponConfig.GetWeaponRange()); // keep the range of pursuit of the enemy below the players' targeting range
            PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
        }
    }

    // TEST: Who was the last attacker against the enemy?
    public void SetLastAttacker(GameObject instigator) 
    {
        // if the caller is 'SetHostile', there's a chance it's calling it on itself, because the AggroGroup
        if (instigator == gameObject) return;

        // if whoever the enemy was aiming for is dead, get them off the 'onDie' event listener:
        if (LastAttacker != null) LastAttacker.GetComponent<Health>().onDie.RemoveListener(ClearLastAttacker);

        // if the last attacker is not dead, add them to the list, and then prepare them to be deleted off the list when they die:
        LastAttacker = instigator;
        LastAttacker.GetComponent<Health>().onDie.AddListener(ClearLastAttacker);
    }

    // TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
    public void ClearLastAttacker()
    {
        // if the enemy is dead, clear him off the 'LastAttacker', clear the cooldown timer, return to idle state, and ensure no accident hit counter on record (he will forget about what that guy did...):
        LastAttacker = null;
        CooldownTokenManager.SetCooldown("Aggro", 0f);
        SwitchState(new EnemyIdleState(this));
        ClearAccidentHitCounter();
    }

    public AggroGroup GetAggroGroup() 
    {
        return AggroGroup;
    }

    public void SetAggroGroup(AggroGroup aggroGroup)
    {
        this.AggroGroup = aggroGroup;
    }

    // Called in 'EnemyAttackingState.Tick()', to ensure enemies don't accidentally hit one another for too long
    public int AddToAccidentHitCounter() 
    {
        return accidentHitCounter++;
    }

    public int ClearAccidentHitCounter() 
    {
        return accidentHitCounter = 0;
    }

    public bool GetOpponent() 
    {
        return HasOpponent;
    }

    public void SetHasOpponent(bool hasOpponent)
    {
        this.HasOpponent = hasOpponent;
    }

    public void ClearOpponent() 
    {
        this.Opponent = null;
    }

    /// <summary>
    /// Who is the opponent of this NPC?
    /// </summary>
    /// <param name="HasOpponent"></param>
    /// <param name="attacker"></param>
    public void SetOpponent(GameObject attacker = null)
    {
        Opponent = attacker;
    }

    }
}

I’m still refining it though :slight_smile:

(the big problem I have right now, is that these enemies do not refresh their ‘hasOpponent’ and ‘Opponent’ variables for some reason, which means that they won’t be able to seek another fight… and they’ll just start attacking me, even if they are naturally non-hostile. When the game starts running at first, they will all act exactly as expected, until someone dies… that’s when refreshing will be extremely important, otherwise they won’t be able to seek out other fights. That, and the fact that getting them to find nearby occupied enemies when all are occupied, are both a complete failure)

In simple terms, what I’m trying to do, is if one group member hit someone from another group, get both groups to go to war against each other, and when they clean each other out, I want the enemies to find other unoccupied enemies and try attack them. If they can’t, just get the nearest enemy to you and go for him (which means, I need to access death of these enemies somehow from ‘OnTakenHit’ or what not… not sure tbh, I just want to update the enemies to find other opponents when their primary target is dead)

If it’s a deal breaker though, later on I may make the AggroGroup able to accept enemies as well, so we have friend NPCs that can help us down the line (and that’s pretty much the point of trying to do this, along with other systems like… idk… Police and gangs maybe? Don’t ask me what’s going on here :stuck_out_tongue:)

Again, completely optional. If you’re interested, that would be awesome (because I desperately need help… I have a hard time processing information, as a person). If not, I’m trying it solo anyway :smiley:


Edit 1: The insanely complex function above works, but it needed a little bit of help… Again, when you’re fasting, you can’t think straight (to any muslim or non-muslim reading this, Eid Mubarak! :slight_smile:). I found the solution I was desperately seeking all day, to clean up my ‘Opponent’ and ‘HasOpponent’ variables, and to a huge extent, the system works significantly better now, it’s almost perfect. Here’s what I did, all in ‘EnemyStateMachine.cs’:

// in 'EnemyStateMachine.Start()':
Health.onDie.AddListener(OnDie);

    /// <summary>
    /// in NPC fights between AggroGroups, when the enemy holding this script dies, this function is executed
    /// </summary>
    void OnDie() 
    {
        if (LastAttacker != null) 
        {
            ClearOpponent();
            SetHasOpponent(false);
            ClearEnemyAggroGroup();
            SetHostile(GetInitialHostility, LastAttacker);
            LastAttacker.GetComponent<EnemyStateMachine>().ClearOpponent();
            LastAttacker.GetComponent<EnemyStateMachine>().SetHasOpponent(false);
            LastAttacker.GetComponent<EnemyStateMachine>().ClearEnemyAggroGroup();
            LastAttacker.GetComponent<EnemyStateMachine>().SetHostile(GetInitialHostility, LastAttacker);
        }
    }

and that’s one giant leap for mankind in my books… I still have the issue of enemies that don’t have a target staying in Idle/Patrolling State, when they are supposed to be seeking a target. Once I fix the bugs on this one, I’ll investigate why it’s unreachable

(but first, I need to investigate why in the world that when they kill an enemy, regardless of their hostility, and I’m in their range, they have to attack me…?! (I fixed that… it was a problem with calling ‘SetHostility’, where my second argument was null if it wasn’t fed anything. I’m learning the hard way the importance of checking for nulls here)


Edit 2: OK so, the last problem I have, which I want to fix, is that whilst enemies do get their opponents as expected, some enemies are just chilling and won’t help their friends who are in a fight. The way I want this to work, is for those enemies that are doing nothing, I want them to get the closest enemies and fight with them, even if they are occupied by other enemies… (and if someone respawns, they will leave the over-occupied enemy and focus on the respawned enemy)

But because of a ‘continue’ that is essential to have it all work together, some of that code never saw daylight. Can you please investigate this part of ‘OnTakenHit’ and help me identify how to get the two bottom blocks of the code below to work? I have a feeling like it’s just a sequence of execution thing, but yes I do need help figuring it out (and then I got the AI to work as expected):

foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent) // you're part of the victim group, with nobody to fight:
                {
                    if (attackerAggroGroup != null)
                    {
                        EnemyStateMachine nearestUnassignedEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
                        if (nearestUnassignedEnemy != null)
                        {
                            enemy.SetHostile(true, nearestUnassignedEnemy.gameObject);
                            enemy.SetOpponent(nearestUnassignedEnemy.gameObject);
                            enemy.SetHasOpponent(true);
                            nearestUnassignedEnemy.SetHostile(true, enemy.gameObject);
                            nearestUnassignedEnemy.SetOpponent(enemy.gameObject);
                            nearestUnassignedEnemy.SetHasOpponent(true);
                            Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestUnassignedEnemy.gameObject.name} in the attacker's group.");
                            continue;
                        }
                    }

                    // below here never sees daylight, and it's troubling me... (the rest of this code can be found above, the first code snippet):

                    EnemyStateMachine nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies); // you didn't find an aggroGroup for the attacker, so just find an empty nearby enemy
                    if (nearestEnemy != null)
                    {
                        enemy.SetHostile(true, nearestEnemy.gameObject);
                        enemy.SetOpponent(nearestEnemy.gameObject);
                        enemy.SetHasOpponent(true);
                        nearestEnemy.SetHostile(true, enemy.gameObject);
                        nearestEnemy.SetOpponent(enemy.gameObject);
                        nearestEnemy.SetHasOpponent(true);
                        unassignedEnemies.Remove(nearestEnemy);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name} in the victim group.");
                        continue;
                    }

                    EnemyStateMachine closestEnemy = GetClosestEnemy(enemy); // all enemies are occupied, so now you're getting the closest enemy you can find
                    if (closestEnemy != null)
                    {
                        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.");
                    }
                }
            }

I solved it all. If you want the final ‘AggroGroup.OnTakenHit()’, along with the two helper functions ‘GetUnassignedEnemies’ and ‘GetClosestEnemies’, here you go (this took me 30+ hours to develop):

void OnTakenHit(GameObject instigator)
        {
            if (instigator.GetComponent<PlayerStateMachine>())
            {
                foreach (EnemyStateMachine enemy in enemies)
                {
                    if (enemy != null && enemy.GetAggroGroup() == this && enemy.GetInitialHostility)
                    {
                        enemy.SetHostile(true, instigator);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards the player.");
                    }
                }
                return;
            }

            List<EnemyStateMachine> unassignedEnemies = enemies.Where(enemy => enemy != null && !enemy.HasOpponent).ToList();

            AggroGroup attackerAggroGroup = null;
            if (instigator.GetComponent<EnemyStateMachine>() != null)
            {
                attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>().GetAggroGroup();
            }

            // delete anything relevant to 'foundOpponent' if test failed:
            bool foundOpponent = false;

            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent) // you're part of the victim group, with nobody to fight:
                {
                    if (attackerAggroGroup != null)
                    {
                        EnemyStateMachine nearestUnassignedEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
                        if (nearestUnassignedEnemy != null)
                        {
                            enemy.SetHostile(true, nearestUnassignedEnemy.gameObject);
                            enemy.SetOpponent(nearestUnassignedEnemy.gameObject);
                            enemy.SetHasOpponent(true);
                            nearestUnassignedEnemy.SetHostile(true, enemy.gameObject);
                            nearestUnassignedEnemy.SetOpponent(enemy.gameObject);
                            nearestUnassignedEnemy.SetHasOpponent(true);
                            Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestUnassignedEnemy.gameObject.name} in the attacker's group.");
                            foundOpponent = true;
                            continue;
                        }
                    }

                    EnemyStateMachine nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies); // you didn't find an aggroGroup for the attacker, so just find an empty nearby enemy
                    if (nearestEnemy != null)
                    {
                        enemy.SetHostile(true, nearestEnemy.gameObject);
                        enemy.SetOpponent(nearestEnemy.gameObject);
                        enemy.SetHasOpponent(true);
                        nearestEnemy.SetHostile(true, enemy.gameObject);
                        nearestEnemy.SetOpponent(enemy.gameObject);
                        nearestEnemy.SetHasOpponent(true);
                        unassignedEnemies.Remove(nearestEnemy);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name} in the victim group.");
                        continue;
                    }

                    EnemyStateMachine closestEnemy = GetClosestEnemy(enemy); // all enemies are occupied, so now you're getting the closest enemy you can find
                    if (closestEnemy != null)
                    {
                        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.");
                    }
                }
            }

            if (!foundOpponent) 
            {
                foreach (EnemyStateMachine enemy in enemies) 
                {
                    if (enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent) 
                    {
                        EnemyStateMachine nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies);
                        if (nearestEnemy != null) 
                        {
                            enemy.SetHostile(true, nearestEnemy.gameObject);
                            enemy.SetOpponent(nearestEnemy.gameObject);
                            enemy.SetHasOpponent(true);
                            nearestEnemy.SetHostile(true, enemy.gameObject);
                            nearestEnemy.SetOpponent(enemy.gameObject);
                            nearestEnemy.SetHasOpponent(true);
                            unassignedEnemies.Remove(nearestEnemy);
                            Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name} in the victim group");
                            continue;
                        }

                        EnemyStateMachine closestEnemy = GetClosestEnemy(enemy);
                        if (closestEnemy != null) 
                        {
                            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");
                        }
                    }
                }
            }
        }

        EnemyStateMachine GetNearestUnassignedEnemy(EnemyStateMachine enemy, List<EnemyStateMachine> unassignedEnemies)
        {
            EnemyStateMachine nearestUnassignedEnemy = null;
            float nearestDistance = Mathf.Infinity;

            foreach (EnemyStateMachine unassignedEnemy in unassignedEnemies) 
            {
                if (unassignedEnemy != null && !unassignedEnemy.GetOpponent() && unassignedEnemy.GetAggroGroup() != this) 
                {
                    float distance = Vector3.Distance(enemy.transform.position, unassignedEnemy.transform.position);
                    if (distance < nearestDistance) 
                    {
                        nearestDistance = distance;
                        nearestUnassignedEnemy = unassignedEnemy;
                    }
                }
            }
            return nearestUnassignedEnemy;
        }

        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;
        }
 

@Brian_Trotter heads up, I solved ALL the problems mentioned above (for the Edit 2 problem, I created a boolean and expanded the code that I said “never sees daylight”, and duplicated it into the boolean check if statement, and it works well for all groups that have enemies with teammates under attack, and some teammates that are doing nothing about it (so now everyone will be occupied with the fight). It’s a smart system now :smiley:), and my system works 95% of the way as we speak. The last thing I need to do, is develop an ‘OnStrikePerformed’ event for health, which is similar to ‘OnTakenHit’, because with my current system, the enemy allignment only works with the team that got hit. To keep it equal, the team that got struck as well needs to perform the same operation to make it flawless

I’ll keep you updated on that soon :slight_smile:


@Brian_Trotter and… I am clueless when it comes to creating events. Can you please help me design a function that aims to update a striker when he causes damage to a victim, something similar to ‘OnTakenHit’ (let’s call it ‘OnStrikerHit’ for example… not a single clue, just a wild guess)? I know it may sound simple to you, but I can’t wrap my head around it, and the lack of one is causing me some serious logic problems for the next part :slight_smile: (especially if the striker group is smaller than the victim group)

basically, just like how ‘OnTakenHit’ gets the victim to react when he takes a hit, ‘OnStrikerHit’ will be the reaction code for the instigator, when the instigator scores a hit (Please take your time with this one, I’m off for the day. It’s Eid here, and I’d love to spend it with my family (at least just today), xD)


If you check my last edit (please don’t do that, if you don’t want to confuse yourself, xD), I was seeking a solution to get the last attacker on the player, so I can clean up his data when the player dies. I recently developed the ‘LastAttacker’ detector on the player, so anyone who was aiming for the player before his death will forget it ever existed when he’s dead, which will enable these enemies to search for opponents again down the line. Now, it works regardless of whether it was an NPC opponent or the player who had a fight with the NPC who is supposed to forget the instigator

But I also want to develop an event system for when the player runs out of their range, so they can wipe him off the list again if he runs away, and start hunting for other enemies if they’re in a battle. Can you kindly help me with that? (there’s a lot of reasons why I want to do this…!). Right now, as we speak, I’m doing it in ‘OnTakenHit()’, which really is not the best solution out there…!

I admit, I don’t fully understand how events work, and I’m still baffled on how we even get the last attacker. If you can explain this to me a little further, that would be amazing


So, to keep it simple, my current goals are as follows:

  1. An ‘OnStrikerHit’ event, which allows the instigator to react when he hits an enemy, through both melee and projectiles, so I can get the AI to do the searching more accurately (this one actually matters A LOT to me and this system, if I want to make it work, otherwise finding enemies is honestly going to be a half-baked nightmare)

  2. Find a way to call ‘IsInPlayerChaseRange’ through Events, rather than ‘OnTakenHit’ (which is my temporary solution), so I can wipe out information for the enemies, so they can go search for other enemies or get back to patrolling, when the player/attacker is out of range… depending on what’s going on (if their enemy gets away for example). I don’t know if doing that in Update is the best solution or not, so I’m asking here, because Updates are expensives operations in my opinion…

If I can find a solution to clear enemies off aggrssors/responders to the player once the player is out of their range, which is what I seek in question 2, I will solve a serious bug

  1. Get a better understanding of exactly how events work, because I’ll be needing them a lot soon (and the more I use them, the more confused I get) :slight_smile: … sometimes you have adding listeners (for Unity Events), and sometimes you have Lambda Expressions (for System.Action events), and I’m here like “PICK A SIDE LAH! (Malaysian Slang kicked in)”

  2. (I haven’t thought of this one yet, I’m literally just throwing it out there for now, and I have a funny feeling that it’s an extremely easy one) Suppose I want to integrate a new system, one in which the player only gets loot if he has dealt more than half the damage, otherwise the NPC won’t drop anything… How can we accomplish this?

  3. Not sure where I went completely wrong, but for some reason, as of recently, some enemies will live during their ‘HideCharacter’ time and go and effectively attack enemies, which will completely mess with their Opponent setup system and cause another corner bug that needs to be fixed. At the moment, I did a little bit of patching, and the problem now only occurs when too many arrows are coming from many areas before the enemy is hidden to respawn (i.e: The system has evolved and needs more robust solutions now). Can you please have a look at the code below, and see if you can identify the reason and how to fix it?

Speaking of events, I have a serious question, what’s the big difference between using UnityEvents and System.Action Events? Why do we keep tossing between them?

Edit: OK this is the part where I admit that my attempt was a complete mess, and I need help re-designing (or erasing) the system… I left it at a (partially) stable point (as long as enemies die when they’re supposed to die, and don’t live their life through the Hide Time, I count that as stable… I’m not touching this again!), and I am not touching it again unless I can get some help out of this mess :slight_smile:

If it helps in anyway, here’s my ‘AggroGroup.cs’ and ‘EnemyStateMachine.cs’. I have not touched anything but these two:
AggroGroup.cs:

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

namespace RPG.Combat
{

    public class AggroGroup : MonoBehaviour
    {
        [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")]

        [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)

        [SerializeField] List<EnemyStateMachine> unassignedEnemies;

        private Health playerHealth;

        /// <summary>
        /// At the start of the game
        /// </summary>
        private void Start()
        {
            // Ensures guards are not active to fight you, if you didn't trigger them:
            if (groupHatesPlayer) Activate(true);
            else foreach (EnemyStateMachine enemy in enemies) 
            {
                    enemy.SetHostile(enemy.GetInitialHostility);
            }
        }

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

        /// <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 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; // NEW LINE
        }

        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;
        }

        /* void OnTakenHit(GameObject instigator) // basic functionality, to get all enemies in the AggroGroup to hunt down the player when he's nearby
        {
            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this) enemy.SetHostile(true, instigator);
            }
        } */

        // ----------------- // Use this function if you don't want Enemies to intelligently find out other group members to fight with, and all of them just aim for one guy ------------------

        /* void OnTakenHit(GameObject instigator) 
        {
            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && instigator != null && instigator.TryGetComponent(out EnemyStateMachine instigatorStateMachine) && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() == this)
                {
                    // if the victim is in the same group as the instigator, which is an enemy state machine, 
                    // this area runs
                    Debug.Log("Accidental attack from within the group. Ignore");
                }
                else if (enemy != null && enemy.GetAggroGroup() == this)
                {
                    // if the victim is from a different group, but the Player/another foreign NPC attacks, you run this
                    Debug.Log("Attack from outside, entire group fights back");
                    enemy.SetHostile(true, instigator);
                }
            }
        } */

        // ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        // ---------------------------------- Use this function if you want Superior Intelligence, where each enemy can intelligently identify enemies in the opposite fighting group and attack them accordingly (still in the works) ----------------------

        void OnTakenHit(GameObject instigator)
        {
            // THE LOGIC FOR DEALING WITH THE PLAYER IN THIS FUNCTION ASSUMES HE'S A SOLO PLAYER
            // IT WILL DEFINITELY CHANGE WHEN THE PLAYER HAS HIS OWN GROUP OF SUPPORT NPCs.
            // FOR NOW, LET'S JUST MAKE SURE IT ALL WORKS...

            // Individually dealing with the player, and getting the entire team to just run up against him:
            if (instigator.GetComponent<PlayerStateMachine>())
            {
                foreach (EnemyStateMachine enemy in enemies)
                {
                    // enemy must exist, be in AggroGroup of victim, and is in Player's Chase Range to make him a target:
                    if (enemy != null && enemy.GetAggroGroup() == this && enemy.IsInPlayerChasingRange())
                    {
                        enemy.SetHostile(true, instigator.gameObject);
                        enemy.SetOpponent(instigator.gameObject);
                        enemy.SetHasOpponent(true);
                        // ONLY ACTIVATE THE NEXT LINE IF YOU WANT THE PLAYER TO BE HUNTED DOWN BY EVERYONE 
                        // IN THE AGGROGROUP, REGARDLESS OF DISTANCE 
                        // (DEACTIVATE IF YOU WANT DISTANCE TO BE A FACTOR, IT'LL WORK JUST FINE...!):
                        // enemy.CooldownTokenManager.SetCooldown("Aggro", enemy.CooldownTimer, true);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards the player.");
                    }
                }
                return;
            }

            // Dealing with the enemies now:
            unassignedEnemies = enemies.Where(enemy => enemy != null && !enemy.HasOpponent).ToList();

            AggroGroup attackerAggroGroup = null;
            if (instigator.GetComponent<EnemyStateMachine>() != null && instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != null)
            {
                attackerAggroGroup = instigator.GetComponent<EnemyStateMachine>().GetAggroGroup();
            }

            foreach (EnemyStateMachine enemy in enemies)
            {
                if (enemy != null && enemy.GetAggroGroup() == this && !enemy.HasOpponent) // you're part of the victim group, with nobody to fight:
                {
                    if (attackerAggroGroup != null && attackerAggroGroup != this)
                    {
                        EnemyStateMachine nearestUnassignedEnemy = GetNearestUnassignedEnemy(enemy, attackerAggroGroup.GetGroupMembers());
                        if (nearestUnassignedEnemy != null)
                        {
                            enemy.SetHostile(true, nearestUnassignedEnemy.gameObject);
                            enemy.SetOpponent(nearestUnassignedEnemy.gameObject);
                            enemy.SetHasOpponent(true);
                            nearestUnassignedEnemy.SetHostile(true, enemy.gameObject);
                            nearestUnassignedEnemy.SetOpponent(enemy.gameObject);
                            nearestUnassignedEnemy.SetHasOpponent(true);
                            unassignedEnemies.Remove(nearestUnassignedEnemy);
                            Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestUnassignedEnemy.gameObject.name} in the attacker's group.");
                            continue;
                        }
                    }

                    EnemyStateMachine nearestEnemy = GetNearestUnassignedEnemy(enemy, unassignedEnemies); // you didn't find an aggroGroup for the attacker, so just find an empty nearby enemy
                    if (nearestEnemy != null)
                    {
                        enemy.SetHostile(true, nearestEnemy.gameObject);
                        enemy.SetOpponent(nearestEnemy.gameObject);
                        enemy.SetHasOpponent(true);
                        nearestEnemy.SetHostile(true, enemy.gameObject);
                        nearestEnemy.SetOpponent(enemy.gameObject);
                        nearestEnemy.SetHasOpponent(true);
                        unassignedEnemies.Remove(nearestEnemy);
                        Debug.Log($"{enemy.gameObject.name} is now hostile towards {nearestEnemy.gameObject.name} in the victim group.");
                        continue;
                    }

                    EnemyStateMachine closestEnemy = GetClosestEnemy(enemy); // all enemies are occupied, so now you're getting the closest enemy you can find
                    if (closestEnemy != null)
                    {
                        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.");
                    }
                }
            }
        }

        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;
        }

        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;
        }
        // --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    }
}

EnemyStateMachine.cs:

using RPG.Attributes;
using RPG.Combat;
using RPG.Control;
using RPG.Movement;
using RPG.Stats;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
using System.Linq;
using RPG.States.Player;

namespace RPG.States.Enemies {

public class EnemyStateMachine : StateMachine, ITargetProvider
{
    [field: SerializeField] public Animator Animator {get; private set;}
    [field: SerializeField] public CharacterController CharacterController {get; private set;}
    [field: SerializeField] public ForceReceiver ForceReceiver {get; private set;}
    [field: SerializeField] public NavMeshAgent Agent {get; private set;}
    [field: SerializeField] public PatrolPath PatrolPath {get; private set;}
    [field: SerializeField] public Fighter Fighter {get; private set;}
    [field: SerializeField] public BaseStats BaseStats {get; private set;}
    [field: SerializeField] public Health Health {get; private set;}
    [field: SerializeField] public CooldownTokenManager CooldownTokenManager {get; private set;}


    [field: SerializeField] public float FieldOfViewAngle {get; private set;} = 90.0f;
    [field: SerializeField] public float MovementSpeed {get; private set;} = 4.0f;
    [field: SerializeField] public float RotationSpeed {get; private set;} = 45f;
    [field: SerializeField] public float CrossFadeDuration {get; private set;} = 0.1f;
    [field: SerializeField] public float AnimatorDampTime {get; private set;} = 0.1f;

    [field: Tooltip("When the player dies, this is where he respawns to")]
    [field: SerializeField] public Vector3 ResetPosition {get; private set;}

    // TEST - Aggrevating others in the 'PlayerChaseRange' range:
    [field: Tooltip("Regardless of whether the enemy is part of an AggroGroup or not, activating this means every enemy in 'PlayerChasingRange', surrounding the attacked enemy, will aggrevate towards the player for 'CooldownTimer' seconds the moment the enemy is hit, and then they'll be quiet if the player doesn't fight them back. If the player attacks the enemies though, the longer the fight goes on for, the longer they will aggrevate towards him (because CooldownTimers' append is set to true))")]
    [field: SerializeField] public bool AggrevateOthers { get; private set; } = false;

    // TEST - Testing for Aggrevated Enemy toggling (called in 'OnTakenHit' below, 'EnemyPatrolState.cs', 'EnemyDwellState.cs' and 'EnemyIdleState.cs'):
    [field: Tooltip("Is this Enemy supposed to hate and want to kill the player?")]
    [field: SerializeField] public bool IsHostile {get; private set;} // for enemies that have hatred towards the player
    [field: Tooltip("For how long further will the enemy be mad and try to attack the player?")]
    [field: SerializeField] public float CooldownTimer {get; private set;} = 3.0f; // the timer for which the enemies will be angry for
    
    // TEST - Who is the Last Atacker on this enemy? (NPC Fights):
    [field: Tooltip("The last person who attacked this enemy, works with 'OnDamageInstigator' in 'Health.TakeDamage()'")]
    public GameObject LastAttacker {get; private set;}

    // TEST - How many accidental hits can the AggroGroup member take in, before fighting back:
    [field: Tooltip("How many accidental hits can this enemy take, from a friend in the same AggroGroup, before eventually fighting back? (hard-coded limit to 1 accidental hit. Second one triggers a fight). Handled in 'EnemyAttackingState.cs'\n(Will reset if another NPC/Player attack this enemy, or the LastAttacker is dead\n(DO NOT TOUCH, IT AUTOMATICALLY UPDATES ITSELF!))")]
    [field: SerializeField] public int accidentHitCounter {get; private set;}

    // Function:
    public bool IsAggro => CooldownTokenManager.HasCooldown("Aggro"); // enemies are aggrevated through a timer-check. In other words, they can only be angry for some time

    public float PlayerChasingRangedSquared {get; private set;}
    public GameObject Player {get; private set;}

    public Blackboard Blackboard = new Blackboard();

    [field: Tooltip("When the game starts, is this enemy, by default nature, angry towards the player?")]
    [field: SerializeField] public bool InitiallyHostile {get; private set;}

    [field: Header("Don't bother trying to change this variable anymore.\nWhen the game starts, It's controlled by either 80% of the Target Range of the Enemies' Weapon,\nor 100% of the weapon Range of the Enemies' Weapon, depending on who is higher.\nAll changes are done in 'EnemyStateMachine.HandleWeaponChanged,\nand subscribed to 'Fighter.OnWeaponChanged' in 'EnemyStateMachine.Awake'")]
    [field: SerializeField] public float PlayerChasingRange { get; private set; } = 10.0f;

    [field: Header("Do NOT touch this variable, it is here for debugging and is automatically updating itself!")]
    [field: SerializeField] public AggroGroup AggroGroup { get; private set; }

    [field: Header("Does this enemy have an opponent, or should an aggrevated NPC hunting enemies in the aggroGroup of this enemy hunt him down?")]
    [field: SerializeField] public bool HasOpponent {get; private set;}
    [field: Header("Who is the opponent this NPC is facing?")]
    [field: SerializeField] public GameObject Opponent {get; private set;}

    [field: Tooltip("AUTOMATICALLY UPDATES... This variable tells the developer who the enemy aggroGroup is, based on who this enemy is attacking, so the rest can follow lead (based on this variable from the first enemy on the list)")]
    [field: SerializeField] public AggroGroup EnemyAggroGroup {get; private set;}

        private void OnValidate()
    {
        if (!Animator) Animator = GetComponentInChildren<Animator>();
        if (!CharacterController) CharacterController = GetComponent<CharacterController>();
        if (!ForceReceiver) ForceReceiver = GetComponent<ForceReceiver>();
        if (!Agent) Agent = GetComponent<NavMeshAgent>();
        if (!Fighter) Fighter = GetComponent<Fighter>();
        if (!BaseStats) BaseStats = GetComponent<BaseStats>();
        if (!Health) Health = GetComponent<Health>();
        if (!CooldownTokenManager) CooldownTokenManager = GetComponent<CooldownTokenManager>();
    }

    private void Awake() 
    {
        if (Fighter) Fighter.OnWeaponChanged += HandleWeaponChanged;
    }

    private void Start() 
    {
        // Initially, enemy hostility = enemy initialHostility
        IsHostile = InitiallyHostile;

        Agent.updatePosition = false;
        Agent.updateRotation = false;

        ResetPosition = transform.position;

        PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
        Player = GameObject.FindGameObjectWithTag("Player");
        Blackboard["Level"] = BaseStats.GetLevel(); // immediately stores the enemies' combat level in the blackboard (useful for determining the chances of a combo, based on the enemies' attack level, in 'EnemyAttackingState.cs', for now...)
        
        // The following check ensures that if we kill an enemy, save the game, quit and then return later, he is indeed dead
        // (the reason it's here is because 'RestoreState()' happens between 'Awake()' and 'Start()', so to counter for the delay, we do it in start too)
        if (Health.IsDead()) SwitchState(new EnemyDeathState(this));
        else SwitchState(new EnemyIdleState(this));

        // When the script holder is killed by another NPC, from another AggroGroup, this event is called:
        Health.onDie.AddListener(OnDie);

        Health.onDie.AddListener(() => 
        {
            SwitchState(new EnemyDeathState(this));
        });

        Health.onResurrection.AddListener(() => {
            SwitchState(new EnemyIdleState(this));
        });

        Health.OnDamageTaken += () => 
        {
            // TEST (if statement and its contents only - the else statement has nothing to do with this):
            if (AggrevateOthers)
            {
            foreach (Collider collider in Physics.OverlapSphere(transform.position, PlayerChasingRange).Where(collider => collider.TryGetComponent(out EnemyStateMachine enemyStateMachine)))
            {
                collider.GetComponent<EnemyStateMachine>().CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
            }
            }
            // CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true);
        };

        if (AggroGroup == null) GetAggroGroup();

        // The enemy will aim next to whoever attacked him last (if it's the player, aim for him. If it's another enemy, aim for him too)
        Health.OnDamageInstigator += SetLastAttacker;
        ForceReceiver.OnForceApplied += HandleForceApplied;
    }

    /// <summary>
    /// in NPC fights between AggroGroups, when one enemy dies, this function is executed
    /// (so that the NPC can start seeking other opponents)
    /// </summary>
    public void OnDie()
    {
        if (LastAttacker != null && LastAttacker.GetComponent<EnemyStateMachine>())
        {
            LastAttacker.GetComponent<EnemyStateMachine>().ClearOpponent();
            LastAttacker.GetComponent<EnemyStateMachine>().SetHasOpponent(false);
            LastAttacker.GetComponent<EnemyStateMachine>().ClearEnemyAggroGroup();
            LastAttacker.GetComponent<EnemyStateMachine>().SetHostile(LastAttacker.GetComponent<EnemyStateMachine>().GetInitialHostility, gameObject);
            LastAttacker.GetComponent<EnemyStateMachine>().SwitchState(new EnemyIdleState(LastAttacker.GetComponent<EnemyStateMachine>()));
        }
    }

    AggroGroup ClearEnemyAggroGroup()
    {
        return EnemyAggroGroup = null;
    }

    public void AssignPatrolPath(PatrolPath newPatrolPath)
    {
        PatrolPath = newPatrolPath;
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, PlayerChasingRange);
    }

    private void OnEnable()
    {
        Health.OnTakenHit += OnTakenHit;
    }

    private void OnDisable()
    {
        Health.OnTakenHit -= OnTakenHit;
    }

    private void OnTakenHit(GameObject instigator)
    {
        if (GetOpponent() != instigator)
        {
            // wipe the data against the last attacker, preparing for the next one:
            SetHostile(false, LastAttacker);
            SetOpponent(null);
            SetHasOpponent(false);
            ClearEnemyAggroGroup();

            if (instigator != null && instigator.GetComponent<EnemyStateMachine>()) 
            {
                instigator.GetComponent<EnemyStateMachine>().SetHostile(true, this.gameObject);
                instigator.GetComponent<EnemyStateMachine>().SetOpponent(this.gameObject);
                instigator.GetComponent<EnemyStateMachine>().SetHasOpponent(true);
                
                if (instigator.GetComponent<EnemyStateMachine>().GetAggroGroup() != null) 
                EnemyAggroGroup = instigator.GetComponent<EnemyStateMachine>().GetAggroGroup();
                else EnemyAggroGroup = null;
            }
        }

        CooldownTokenManager.SetCooldown("Aggro", CooldownTimer, true); // 'true' in the end basically allows the enemy to add up to his total anger time
        if (HasOpponent == false) SetHasOpponent(true);
        if (Opponent == null) SetOpponent(instigator.gameObject);
    }

    // public bool IsAggresive => CooldownTokenManager.HasCooldown("Aggro");

    private void HandleForceApplied(Vector3 force) 
    {
        if (Health.IsDead()) return;
        // Disable comments below if you want impact states to be random for the enemy:
        // float forceAmount = Random.Range(0f, force.sqrMagnitude);
        // if (forceAmount > Random.Range(0f, BaseStats.GetLevel())) {
        SwitchState(new EnemyImpactState(this));
        // }
    }

    public GameObject GetTarget()
    {
        // TEST: If the last attacker is not the player, then that's the target:
        if (LastAttacker != GameObject.FindWithTag("Player")) return LastAttacker;
        // the target for the enemy launching a projectile (which is basically the player in this case)
        return Player;
    }

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

    /// <summary>
    /// Who is this NPC hostile towards, and which AggroGroup do they belong to...?!
    /// </summary>
    /// <param name="IsHostile"></param>
    /// <param name="attacker"></param>
    public void SetHostile(bool IsHostile, GameObject attacker = null)
    {
        this.IsHostile = IsHostile;
        if (LastAttacker != null) ClearLastAttacker();
        
        if (attacker != null)
        {
        SetLastAttacker(attacker);
        // (TEST) get the enemyAggroGroup, so that we can easily access the enemies and get each individual enemy in an AggroGroup to hunt a single guy:
        // if the attacker is the player, or an enemy with no aggroGroup, return 'EnemyAggroGroup' = null:
        if (attacker.GetComponent<PlayerStateMachine>() || attacker.GetComponent<EnemyStateMachine>().GetAggroGroup() == null || attacker.GetComponent<Health>().IsDead()) EnemyAggroGroup = null;
        // if it's an enemy with an AggroGroup, get that AggroGroup:
        else EnemyAggroGroup = attacker.GetComponent<EnemyStateMachine>().GetAggroGroup();
        }
    }

    public bool GetInitialHostility => InitiallyHostile;

    public void _ResetPosition() 
        {
        Agent.enabled = false;
        CharacterController.enabled = false;
        transform.position = ResetPosition;
        Agent.enabled = true;
        CharacterController.enabled = true;
        }

    // The code below changes the 'PlayerChasingRange' based on the weapon in-hand. I want the 'PlayerChasingRange' to be what I want, so I commented it out:
    private void HandleWeaponChanged(WeaponConfig weaponConfig) 
    {
        if (weaponConfig) 
        {
            PlayerChasingRange = Mathf.Max(weaponConfig.GetTargetRange() * 0.8f, weaponConfig.GetWeaponRange()); // keep the range of pursuit of the enemy below the players' targeting range
            PlayerChasingRangedSquared = PlayerChasingRange * PlayerChasingRange;
        }
    }

    // TEST: Who was the last attacker against the enemy?
    public void SetLastAttacker(GameObject instigator) 
    {
        // if the caller is 'SetHostile', there's a chance it's calling it on itself, because the AggroGroup
        if (instigator == gameObject) return;

        // if whoever the enemy was aiming for is dead, get them off the 'onDie' event listener:
        if (LastAttacker != null) LastAttacker.GetComponent<Health>().onDie.RemoveListener(ClearLastAttacker);

        // if the last attacker is not dead, add them to the list, and then prepare them to be deleted off the list when they die:
        LastAttacker = instigator;
        LastAttacker.GetComponent<Health>().onDie.AddListener(ClearLastAttacker);
    }

    // TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
    public void ClearLastAttacker()
    {
        // if the enemy is dead, clear him off the 'LastAttacker', clear the cooldown timer, return to idle state, and ensure no accident hit counter on record (he will forget about what that guy did...):
        LastAttacker = null;
        CooldownTokenManager.SetCooldown("Aggro", 0f);
        SwitchState(new EnemyIdleState(this));
        ClearAccidentHitCounter();
    }

    public AggroGroup GetAggroGroup() 
    {
        if (AggroGroup == null) return null;
        return AggroGroup;
    }

    public void SetAggroGroup(AggroGroup aggroGroup)
    {
        this.AggroGroup = aggroGroup;
    }

    public AggroGroup ClearAggroGroup() 
    {
        return AggroGroup = null;
    }

    // Called in 'EnemyAttackingState.Tick()', to ensure enemies don't accidentally hit one another for too long
    public int AddToAccidentHitCounter() 
    {
        return accidentHitCounter++;
    }

    public int ClearAccidentHitCounter() 
    {
        return accidentHitCounter = 0;
    }

    public bool GetHasOpponent() 
    {
        return HasOpponent;
    }

    public GameObject GetOpponent() 
    {
        return Opponent;
    }

    public void SetHasOpponent(bool hasOpponent)
    {
        this.HasOpponent = hasOpponent;
    }

    public void ClearOpponent() 
    {
        this.Opponent = null;
    }

    /// <summary>
    /// Special Function, created for when the player is in Chase Range of AggroGroup enemies (to avoid enemies far away from chasing the player down)
    /// (ONLY USED IN AggroGroup.OnTakenHit() so far)
    /// </summary>
    /// <returns></returns>
    public bool IsInPlayerChasingRange() 
    {
        return Vector3.SqrMagnitude(transform.position - Player.transform.position) <= PlayerChasingRangedSquared;
    }

    /// <summary>
    /// Who is the opponent of this NPC?
    /// </summary>
    /// <param name="HasOpponent"></param>
    /// <param name="attacker"></param>
    public void SetOpponent(GameObject attacker = null)
    {
        Opponent = attacker;
    }

    public AggroGroup GetEnemyAggroGroup()
    {
        return EnemyAggroGroup;
    }

    }
}

I’m really really really really sorry for the emotional trainwreck. I only wanted this to work, but tbh I don’t care anymore… (I don’t know what to do, and I’m very confused). Let’s just get this project back to a clean state

For now, let’s just get the 5 questions in the last edit answered (Question 2 is extremely important to me. I have an extremely harsh bug that relies on the answer to that one, as it will solve a corner case)

1 Like

Ever since I wrote the comment above, I have fixed a lot, and I mean A LOT of code… from rewriting some functions to fixing race conditions (I had some code race to set different results, so I had to rewrite that too) and other accidents I had… This system is quite clean now, and hopefully it’ll stay this way because… well… I’ll probably explode if it messes up again (but I still didn’t finish one part, where first I need to find unassigned enemies before going for nearby enemies, but I can fix that later. For now, he just goes for the nearest enemy… I need to be off now!). It’s not perfect, but meh… it does the job for the time being

Moving forward, I just need your help please @Brian_Trotter to help me develop an event for when the player gets out of chase range of an enemy - I only seek the basic steps (and preferably an explanation of what’s going on… I admit, I’m struggling to understand events), and I’ll take care of whatever code runs in there, as soon as we get to the point of having a working “OnPlayerOutOfChaseRange()” function

After that, I’ll go develop the next steps of my system (blending the player to have his own AggroGroup, but that’s another task for me for later), and I’d let you investigate the bank whenever you have a chance (please, it would really mean a lot to me because frankly speaking, I didn’t fully understand that system)

Once again, I truly apologize for the mess I have caused so far… None of this was easy for me, and I am sure not for you either (you know… each time there’s a heavily edited comment for example. I’m sorry for that. I’m just always seeking help whenever something is out of my control) :sweat_smile:

So the trickiest part about doing an OnPlayerOutOfChaseRange() event with our third Person crossover is that we’re using States that can’t easily be subscribed to. This means we need to make the StateMachine a proxy.
So that’s where we start, with an event in EnemyStateMachine

//event keyword helps prevent just any old class from calling the event, only the StateMachine can call it
//System.Action<EnemyStateMachine> means that all subscribers methods must take in an EnemyStateMachine as a parameter.  
//OnPlayerOutOfChaseRange is the name of our event, what classes will subscribe to
public event System.Action<EnemyStateMachine> OnPlayerOutOfChaseRange();

Since we’ll be determining that we’ve left Chace range in a state, we need a passthrough method to invoke the event.

public Trigger_OnPlayerOutOfChaseRange() => OnPlayerOutOfChaseRange.Invoke(this);

Finally, in PlayerChasingState, we need to test to see if the Player is in range within the Tick() method, and if the Player is NOT, then we call

stateMachine.Trigger_OnPlayerOutOfChaseRange();

It’s really that simple.

Good day Brian, thank you for the guidance. Just to be a little more clear though, can we revise the rules of the events together, just to make sure I’m on the right page? So far based on my understanding, here’s what an event needs:

  1. A declaration, through either a “public event System.Action (there’s a “<” here, since writing it out here will cause it to disappear)ClassTypeHere> EventNameHere”, or a “UnityEvent” that can be declared like any variable (serious question though, why do we keep jumping between them?)
  2. Subscribed and Unsubscribed to events points (and I read somewhere on one of your forums when I was hunting down for information that you did mention that every subscribed event needs a point of unsubscription), so when it’s invoked, the specific function subscribing to the event will be called/Invoked
  3. A spot to invoke the event from

Seriously though, why do we unsubscribe to events? I can’t find an easier way to explain my point of confusion here… :sweat_smile:

Am I correct? Did I miss anything out?

Why don’t we have any function incrementations here, out of curiosity? The whole “+=” thingy… (if this approach is allowed here, how would we do it in this case as an example?)

As much as I dislike events (because they confuse me to program), I agree with you that having updates as pulses (which is the whole point of events, as far as my understanding goes) is significantly better than programming everything in an Update function

As for the testing, I’ll most likely test it again on Monday (I have an exam to start revising for… and I’m guessing we can invoke this in “ShouldPursue”, the function we created in “EnemyBaseState.cs” to determine if we should pursue someone or not (I haven’t touched that function ever since we were getting IsInChaseRange to work), am I correct? We can continue this conversation on Monday :slight_smile:)

I don’t have that kind of state in my State Machine. I only have this class in ‘EnemyBaseState.cs’:

    protected bool IsInChaseRange()
    {
        // (NEW) TEST: If you have a last attacker (who is not the player), and he ain't dead, chase that target down
        // (now it's no longer necessarily just the player. The enemy can be a last attacker as well... it all boils down to who last hit the enemy holding the 'EnemyStateMachine.cs' script):
        if (stateMachine.LastAttacker != null && !stateMachine.LastAttacker.CompareTag("Player") && !stateMachine.LastAttacker.GetComponent<Health>().IsDead()) return true;

        // If you're not attacked by another NPC, just get the chase range so we can continue acting accordingly...:
        return Vector3.SqrMagnitude(stateMachine.transform.position - stateMachine.Player.transform.position) <= stateMachine.PlayerChasingRangedSquared; // temporarily replaced by the test below
    }

Can we invoke it here instead, or should we go everywhere we call this function and Invoke it? (Edit: Ignore that for the time being, Ed’s comment below makes it all make sense for me to test on Monday)

I think he means EnemyChasingState.

1 Like

that makes a whole lot more sense, if true :slight_smile:

You can use a single ` at the beginning and end of any text to format it inline. The <ClassDeclaration> disappears because the system thinks you are trying to use an HTML tag. So
`public event System.Action<ClassTypeHere>` becomes public event System.Action<ClassTypeHere>

An event is a list of delegates that will be called when the event is invoked. If you subscribe to an event, but then do not unsubscribe when the method is no longer valid, when the event is invoked, a null reference will occur. Worse, these sorts of null references are next to impossible to track down as you know that it was an error with the event but you have no idea what method unsubscribed when it shouldn’t have.

Simple: If you want to be able to add an event in the inspector, you use a UnityEvent. In code, you use AddListener and RemoveListener. Otherwise, if you don’t need that functionality in the inspector, you use an event and subscribe to it with += and -=

Because C# knows that this is an event, it simply adds the delegate to the list of delegates (and removes the delegate with -=). This is a form of syntactic sugar.

I meant to type in EnemyChasingState

why does this have to be such a major headache? I remember I once read a “joke” about a programmer’s kid who asked his dad “dad, if computers know we missed a Semicolon, why don’t they just put it for us?”

Same question here, “If the computer knows the NRE is because of a missing unsubscription, why not just integrate it and notify us about it? Why does it always have to make me spend 20 hours hunting it down…?!”

Or, in Professor Macgonagall’s words (from Harry Potter): “Why is it that when something bad happens, it is always you three…?!”:

You won’t convince me that we have a silly bot about to replace 80% of all jobs worldwide, and we still can’t do a task as simple as this one…

:stuck_out_tongue:

let me guess, this one is more universal, so it’s recommended… right?

now I’m even more confused… When does it know and when doesn’t it know, and when should I or should I not use the whole += and -= thingy…?!

the memes you throw in here just never fail to make me laugh… :stuck_out_tongue:

hello again Brian, can you please have a look at this ‘Respawn()’ function in ‘RespawnManager.cs’?:

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

            // 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);
                spawnedEnemy.SwitchState(new EnemyIdleState(spawnedEnemy));
            }
            else
            {
                Debug.Log($"No Patrol Path to assign");
            }
            // --------------------------- 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 (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);
                            spawnedEnemy.SetHostile(true, allyEnemy.GetOpponent());
                            Debug.Log("Enemy Respawned and is supposed to fight...");
                            break;
                        }
                    }
                }

                // -------------------------------------- END OF TEST AREA --------------------------------------------------------------------------------------------------------

                if (spawnedEnemy.TryGetComponent(out DialogueAggro dialogueAggro)) //aggrogroup is at this point valid
                {
                    dialogueAggro.SetAggroGroup(aggroGroup);
                }
            }
            // ---------------------------------------------------------------------------------------------------------
        }

The problem I have is that ‘enemy.SetHostile(true);’ never sets the hostility of the enemy to true, for some reason I don’t know of… The debug gets called, the opponent setup is done perfectly fine, but this specific line just never happens for some reason when my enemy respawns… (I tried toggling it to true, and it does exactly what it’s supposed to do. The problem is getting the code to toggle it to true, which is what matters more to me)

I have a funny feeling that it gets deactivated from somewhere else first, but I genuinely have zero idea where from…

If you need any extra information, please let me know

I truly wish it were that simple. If it were possible to detect during compile time, this might be possible, but if the GameObject that houses the delegate is destroyed, so is the reference to the script that subscribed to the event. By the time that happens, Unity has overwritten the memory with other things. I do agree, though, that it would be nice if the compiler would just fix those annoying ; errors.

It knows because very very clever engineers at Microsoft wrote a lot of switch case statements to cover all the possible uses of the -= statement.
You should use the +=/-= when dealing with events and Actions, you should use the AddListener and RemoveListener when dealing with UnityEvents. UnityEvents are true events in the way using System; thinks of events, so the compiler doesn’t know how to replace += with AddListener.

Neither do I, as I’m not seeing that end of the code… Look for code that sets an enemy’s hostile to false.

Privacy & Terms