"Mercy Factor" (Extra Code Review Please)

Hello there good people. It’s nice to see you all again :slight_smile:

Recently I was developing my AI Fighting System, something to get my enemies to fight against one another, and part of my system is something that I call as the “Mercy Factor”.

In simple terms, if you and your allies are in a fight, and let’s say you (THE PLAYER… Doing the same for enemies is a bit horrifying :sweat_smile:) accidentally hit them with your sword or something else, they will increment their ‘accidentHitCounter’ variable, until a ‘maximumNumberOfAccidents’

Theoretically, what I want to do, is once you hit that number (which is 3 hits in my case), whoever you just hit 3 times, from your allies, is expected to drop the fight with their enemy and focus on you, because you are becoming a nuisance to them now… It’s like a “Hey buddy, watch your sword. I won’t be so nice to you if you keep making mistakes” kinda thing. Through the way I set this up, I made sure normal NPCs don’t go through the same process

Realistically, though, it’s not working as I expect it to work…

What I expect it to do, is after 3 hits, you set the LastAttacker variable of yours to be the player (which it does), and then also set your opponent to be the player as well (which, it doesn’t do)

Worse, after the fourth hit, it sets that enemy’s LastAttacker back to whoever it was before you hit them, and it’s really getting weird…

Can someone please have a look at my function and help me fix it? The logic was split into two, a part in my ‘SetLastAttacker’ function (which was a dangerous move for me to do), and another half in my ‘OnTakenHit’ function, because it wouldn’t work otherwise. Here they are:

The ‘OnTakenHit’ half, which deals with the opponent:

                // (MERCY FACTOR) If you were a teammate of the player, and the player 
                // hit you, and you were initially fighting someone else from a different AggroGroup,
                // focus on the aggroGroup of whoever you're fighting, as it was probably an accident hit
                // by the player (if 3 or more hits are an 'accident', the ally will drop the fight 
                // and fight the player, unless they are provoked by someone else):
                if (this.GetAggroGroup() != null && this.GetAggroGroup().GetPlayer() != null && this.GetOpponent() != null)
                {
                EnemyStateMachine opponentEnemy = this.GetOpponent().GetComponent<EnemyStateMachine>();
                if (opponentEnemy != null && opponentEnemy.GetAggroGroup() != this.GetAggroGroup())
                {
                    Debug.Log($"{this.gameObject.name} will ignore the player's attack and focus on the other team");
                    EnemyStateMachine attackerBeforePlayer = opponentEnemy.GetComponent<EnemyStateMachine>();
                    if (attackerBeforePlayer != null) 
                    {
                        Debug.Log($"attacker before player is: {attackerBeforePlayer.gameObject.name}");
                        // EnemyStateMachine nearestEnemy = GetClosestEnemyInEnemyAggroGroup(this, attackerBeforePlayer.GetComponent<EnemyStateMachine>().GetAggroGroup());
                        EnemyStateMachine nearestEnemy = attackerBeforePlayer;
                        if (nearestEnemy != null)
                        {
                            Debug.Log($"AccidentHitCounter: {GetAccidentHitCounter()}");
                            // If you don't want the 'AccidentHitCounter', delete the if statement 
                            // below, and keep whatever is inside the 'else' only (and delete the
                            // 'IncrementAccidentHitCounter()' function inside that 'else' as well):
                            if (GetAccidentHitCounter() >= maximumNumberOfAccidents)
                            {
                                Debug.Log($"{this.gameObject.name}: Counter has reached the maximum number of accidents");
                                SetHostile(true, instigator.gameObject);
                                SetHasOpponent(true);
                                SetOpponent(instigator.gameObject);
                                // ResetAccidentHitCounter(); // The reset is done in 'SetLastAttacker()', otherwise you'll have a glitch in hunting the player down TWICE
                            }
                            else
                            {
                                Debug.Log($"{this.gameObject.name}: Incrementing AccidentHitCounter");
                                // Get the Nearest Opponent to you, which is most likely going to be the target you were aiming for anyway:
                                SetHostile(true, nearestEnemy.gameObject);
                                SetHasOpponent(true);
                                SetOpponent(nearestEnemy.gameObject);
                                // The incrementing of the 'AccidentHitCounter()' is done here, as 'SetLastAttacker()' is called in multiple
                                // areas of this script, for various reasons, combined with in 'Health.OnTakenHit += OnDamageInstigator',
                                // running the risk of being called twice eventually screwing this system up. To avoid that
                                IncrementAccidentHitCounter();
                                Debug.Log($"nearest enemy for {this.gameObject.name} is: {nearestEnemy.gameObject.name}");
                            }
                            // I have also copy-pasted this block into 'SetLastAttacker()' and 
                            // modified the output to ensure it gets the appropriate output, based
                            // on whether this scenario is encountered or not (because ultimately,
                            // the NPC Enemy/Ally will follow the value of 'LastAttacker').

                            // There's also a line in 
                            // 'SetLastAttacker', to ensure that enemies respond properly with the new 'MERCY FACTOR'
                            // System, that goes as follows:
                            // if (LastAttacker == null && instigator != null) { LastAttacker = instigator; return; }

                        }
                    }
                }
            }
            else Debug.Log($"Condition for mercy factor not met");

and the half from ‘SetLastAttacker’:

            // (TEST - 11/5/2024):
            // Instead of automatically setting the Last Attacker as the instigator,
            // we have a special case need. If you were a teammate of the player, 
            // and the player hit you, and you were initially fighting someone else from 
            // a different AggroGroup, focus on the aggroGroup of whoever you're fighting, 
            // as it was probably an accident hit from the player
            // (P.S: anything more than 'maximumNumberOfAccidents' is no longer an accident hit, it's
            // on purpose, so you go wild on the player now):
            if (this.GetAggroGroup() != null && this.GetAggroGroup().GetPlayer() != null && this.GetOpponent() != null)
            {
                Debug.Log($"{this.gameObject.name} is part of the player's AggroGroup, and has an enemy");
                EnemyStateMachine opponentEnemy = this.GetOpponent().GetComponent<EnemyStateMachine>();
                if (opponentEnemy != null && opponentEnemy.GetAggroGroup() != this.GetAggroGroup())
                {
                    Debug.Log($"SetLastAttacker's {this.gameObject.name} will ignore the player's attack and focus on the other team");
                    EnemyStateMachine attackerBeforePlayer = opponentEnemy.GetComponent<EnemyStateMachine>();
                    if (attackerBeforePlayer != null)
                    {
                        Debug.Log($"{this.gameObject.name} SetLastAttacker's attacker before player is: {attackerBeforePlayer.gameObject.name}");
                        // EnemyStateMachine nearestEnemy = GetClosestEnemyInEnemyAggroGroup(this, attackerBeforePlayer.GetComponent<EnemyStateMachine>().GetAggroGroup());
                        EnemyStateMachine nearestEnemy = attackerBeforePlayer;
                        if (nearestEnemy != null)
                        {
                            // if you don't want the 'AccidentHitCounter' delete the if-else statement
                            // below, and only keep the 'LastAttacker = nearestEnemy.gameObject' line
                            if (GetAccidentHitCounter() >= maximumNumberOfAccidents)
                            {
                                Debug.Log($"{this.gameObject.name}  says Enough Buddy, you are under attack!...!");
                                LastAttacker = instigator;
                                ResetAccidentHitCounter(); // the resetting of the 'AccidentHitCounter' is done here, to avoid the glitch of hunting the player down twice
                            }
                            else
                            {
                                Debug.Log($"{this.gameObject.name} says: Buddy, watch it...!");
                                LastAttacker = nearestEnemy.gameObject;
                                // Incrementing the 'AccidentHitCounter' is done in the 'OnTakenHit()' 
                                // function, to avoid doing it twice here (since 'SetLastAttacker()'
                                // is called in both 'Health.onDie' in 'Start()', and in 
                                // various functions in this script, we run the risk of calling it 
                                // twice by accident. To avoid that, we did the incrementing in 
                                // 'OnTakenHit()')
                            }
                        }
                    }
                }
            }
            else
            {
                // If you got to this 'else' statement, it means you were either not a part of
                // the Player's AggroGroup, or you were... but you weren't part of a fight with someone
                // else. This means that the player was just bothering you, so now you can safely fight
                // him to death, unless triggered by someone else:
                Debug.Log($"Natural ending for 'LastAttacker = instigator'");
                LastAttacker = instigator;
            }

The code is pretty much copy-pasted between both, with just the innermost variables between them changing. My problem is in this part, the innermost part of the loop, from ‘OnTakenHit’:

// If you don't want the 'AccidentHitCounter', delete the if statement 
                            // below, and keep whatever is inside the 'else' only (and delete the
                            // 'IncrementAccidentHitCounter()' function inside that 'else' as well):
                            if (GetAccidentHitCounter() >= maximumNumberOfAccidents)
                            {
                                Debug.Log($"{this.gameObject.name}: Counter has reached the maximum number of accidents");
                                SetHostile(true, instigator.gameObject);
                                SetHasOpponent(true);
                                SetOpponent(instigator.gameObject);
                                // ResetAccidentHitCounter(); // The reset is done in 'SetLastAttacker()', otherwise you'll have a glitch in hunting the player down TWICE
                            }
                            else
                            {
                                Debug.Log($"{this.gameObject.name}: Incrementing AccidentHitCounter");
                                // Get the Nearest Opponent to you, which is most likely going to be the target you were aiming for anyway:
                                SetHostile(true, nearestEnemy.gameObject);
                                SetHasOpponent(true);
                                SetOpponent(nearestEnemy.gameObject);
                                // The incrementing of the 'AccidentHitCounter()' is done here, as 'SetLastAttacker()' is called in multiple
                                // areas of this script, for various reasons, combined with in 'Health.OnTakenHit += OnDamageInstigator',
                                // running the risk of being called twice eventually screwing this system up. To avoid that
                                IncrementAccidentHitCounter();
                                Debug.Log($"nearest enemy for {this.gameObject.name} is: {nearestEnemy.gameObject.name}");
                            }

For some reason, this if-else statement NEVER, EVER goes into the ‘if’ part, although the counter does get reset (which means that the opponent is supposed to be reset there!). It always goes into the else, and I have absolutely no idea why, and it’s been bothering me for a while now

I did the debugging in the opponent getter as well, and it says that when the counter resets, it gets called from the else from the opponent as well

(Note: if it helps, anywhere it says ‘GetAggroGroup().GetPlayer()’ means it’s the player’s AggroGroup. The player’s AggroGroup and every other AggroGroup in the game is seperated by one thing: there’s only one team in the game that has a player variable in their AggroGroup)

This is all custom code that I wrote, and is not part of any course. Just me trying to develop my own game beyond what the courses teach, xD.

Please help me fix it. Thank you :slight_smile:

I ended up solving the problem. Apparently it was a sequence of order of execution issue of some sort, so I went around it in a funky way. Here’s what I ended up with:

in ‘OnTakenHit’ (I cleaned up the comment mess that I had a little bit):

            if (this.GetAggroGroup() != null && this.GetAggroGroup().GetPlayer() != null && this.GetOpponent() != null) 
            {
                EnemyStateMachine opponentEnemy = this.GetOpponent().GetComponent<EnemyStateMachine>();

                if (opponentEnemy != null && opponentEnemy.GetAggroGroup() != this.GetAggroGroup()) 
                {
                    EnemyStateMachine nearestEnemy = opponentEnemy;

                    if (GetAccidentHitCounter() >= maximumNumberOfAccidents) 
                    {
                        // Do nothing here. The Opponent and LastAttacker logic setup is done in 'SetLastAttacker'
                    }
                    else 
                    {
                        Debug.Log($"{this.gameObject.name}: Incrementing AccidentHitCounter");
                        // The Opponent and LastAttacker setup is done in 'SetLastAttacker', because of sequence of order of execution problems
                        IncrementAccidentHitCounter();
                    }
                }
            }    
            else Debug.Log($"Condition for mercy factor not met");

and then in ‘SetLastAttacker()’:

            // --------------------- (TEST) Rewrite of the Mercy Factor (i.e: if the player hits his ally 3 times in a row whilst that ally is busy in a fight, the ally's target now is the player): ------------------------------
            if (this.GetAggroGroup() != null && this.GetAggroGroup().GetPlayer() != null && this.GetOpponent() != null)
            {
                EnemyStateMachine opponentEnemy = this.GetOpponent().GetComponent<EnemyStateMachine>();

                if (opponentEnemy != null && opponentEnemy.GetAggroGroup() != this.GetAggroGroup())
                {
                    EnemyStateMachine nearestEnemy = opponentEnemy;

                    if (GetAccidentHitCounter() >= maximumNumberOfAccidents)
                    {
                        Debug.Log($"{this.gameObject.name}  says Enough Buddy, you are under attack!...!");
                        LastAttacker = instigator;
                        SetOpponent(instigator);
                        SetHasOpponent(true);
                        ResetAccidentHitCounter(); // Resetting the counter here
                    }
                    /* else
                    {
                        Debug.Log($"{this.gameObject.name} says: Buddy, watch it...!");
                        LastAttacker = nearestEnemy.gameObject;
                    } */
                }
            }
            else
            {
                Debug.Log($"Natural ending for 'LastAttacker = instigator'");
                LastAttacker = instigator;
            }
            // ------------------------------- END OF TEST -------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Since I realized that ‘SetLastAttacker’ runs first for some reason, I figured I can just set the opponent up there and get it to work, and to no one’s surprise, it actually did

And this time, from testing, it actually works perfectly!

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms