Merging the Third Person Controller Course with the RPG Course

Keep in mind that I am not as far along in the merge as you. I have been doing other things and I want to take my time with this. I did not intend to imply that there was more coming. I just did the enemy attacking state last night. We kind of have a tortoise and hare situation going on here. :laughing:

1 Like

lol… more like a Tortoise and a Ferrari 812 SF :stuck_out_tongue: (it’s not my dream car, just the first one that came on to mind. I’m a big car guy by all means) - talk about an extremely ambitious guy who wants to dive into the ocean ASAP

By all means, I’m low-key still waiting for the banking remake, xD (OK I’ll be quiet) - I re-read that article and went “OK I’ll be patient… I have no idea how to swap that out!”

Edit: Hoping Brian is doing well… :sweat_smile:

1 Like

Not supporting the Swimming state, and definitely not boat states…

Exactly like we’re selecting them in the Complex Blend Trees. In this case, immediately before calling the death animation state with your blend tree of deaths, use a float parameter dedicated to choosing that animation, and roll a Random.Range(0, NumberOfAnimations) and SetFloat that Parameter.

it’s not as hard as you think, trust me :stuck_out_tongue: - it’s just a fun way of introducing a new dimension to the game, xD

sounds simple. I could see how this can be used in countless ways for enemies. For idle states, random death animations, big boss fights where transitioning at specific times is essential, etc… Once the bank is off the charts (I’m reading the same article for the thousandth time as we speak, trying to figure out how in the world do you make this work for third person… my mind ain’t mind-ing though :sweat_smile:), I’ll go give this a go. Hopefully it ain’t too hard though

Edit: I created a GameObject and stored the bank there on the banker himself. Next, so far I managed to get the player to enter the banking state machine, and I ended up calling the ‘OpenBank’ function I created for ‘OtherInventory.cs’… Now I’m looking for a safe way to get the bank and open it. For the most part, I think all the major functions are (ALMOST) fully taken care of for that one

and… I’m stuck with this weird error now:

NullReferenceException: Object reference not set to an instance of an object
GameDevTV.Inventories.OtherInventory.<OpenBank>b__3_0 () (at Assets/GameDev.tv Assets/Scripts/Inventories/OtherInventory.cs:49)
RPG.Bank.BankAction.Update () (at Assets/Project Backup/Scripts/Bank/BankAction.cs:44)

which leads to this function that I created, which comes from ‘PlayerBankingState.Enter()’:


        [SerializeField] GameObject bankUIToOpen;

        public void OpenBank(PlayerStateMachine player) 
        {
            Debug.Log("OpenBank Function Called");
            player.GetComponent<BankAction>().StartBankAction(transform, () => 
            {showHideUI.ShowOtherInventory(bankUIToOpen);}); // <-- this is where the error is coming from
        }

which leads to this:

    public void StartBankAction(Transform bankTarget, System.Action callback) {

        if (bankTarget == null) return;
        this.callback = callback;
        target = bankTarget;
        GetComponent<ActionSchedular>().StartAction(this);

    }

Which I’m a little clueless on because… well… I’m confused on what ‘System.Action callbacks’ are (usually when we enter this code, I know it’s advanced territory… so I’ll just quietly copy paste). Please… Help @Brian_Trotter :slight_smile: - I tried just enabling the UI, but I can’t transfer anything in and out of the inventory if I do just that… I think something is missing

(I’m trying to copy-paste and tune whatever the point-and-click solution had, but damn this is quite hard tbh…)

And then it hit me that ‘System.Action Callback’ is literally nothing but giving a function a call to another function as a parameter. Please correct me if I’m wrong, but if true, then WOW… What a way to go around it :stuck_out_tongue:


Edit 3: A few hours of debugging later, I realize that the Inventory Slots of the bank ALWAYS return the NRE error if we hover over them, with this function for ‘OpenBank’:

        public void OpenBank(PlayerStateMachine player)
        {
            bankUIToOpen.SetActive(true);
            // figure out why in the world wouldn't the Inventory Slot UI refresh for the bank, although it refreshes for the player's Inventory
            bankUIToOpen.GetComponentInChildren<InventoryUI>().Setup(bankUIToOpen);
        }

and why is that the case? Because this function in ‘InventoryUI.cs’ always returns a false:

        public bool Setup(GameObject user) {

            if (user.TryGetComponent(out selectedInventory)) {

                // if the object has an inventory (our bank in this case), set the Selected Inventory Up, 
                // Subscribe to the Redraw() changes, and then redraw the inventory:
                selectedInventory.inventoryUpdated += Redraw;
                Title.text = selectedInventory.name;
                Redraw();
                return true;

            }
            return false;

        }

How do we fix this? :sweat_smile: (I’m really sorry to drag you into another topic… I’m trying my best not to bother you by asking for anymore help, as you’ve helped me a lot (and I’m grateful for that!), but I’ve been trying for a few more hours after writing the previous code, and I still can’t figure it out. So… Please Help me get the bank to work again :slight_smile:)

I’ll see what I can do. The project doesn’t have the bank in it, so I have to recreate the bank system first within the project, and then port it to the 3rd person interface. It shouldn’t be too complex, just that it’s not in the project, and as such, can cause meyhem and confusion when I add it.

1 Like

THANK YOU SO SO SO MUCH for shedding light on this topic again Brian. I’ve been trying weird stuff for hours now, with little to no avail. I know it’s out of context, but with the current Architecture of the project, it’s essential as we speak

Once again, I sincerely apologize for bringing this up now, but it matters to me :sweat_smile:

Please also do consider the Click-to-Withdraw/Click-to-Deposit later on, using the 3-way state machine we discussed here (if that still matters), just to keep it simple down the line :smiley: - if there’s anything missing down the line, we will discuss it when the time is right

Edit: I deleted the entire banking state entrance from the FreeLook state, because somehow, it was messing with my Construction System. I’ll just wait for your update :grinning_face_with_smiling_eyes:

Edit 2: Something completely off topic, but I fixed a problem with the animals that’s been bothering me for a week… It’s one of those little things that genuinely don’t matter, but I was a bit too focused on the little details :sweat_smile:

hello again @Brian_Trotter - quick question on the side, as I wait for any updates about the bank (I’m testing other systems in the meanwhile). Is there any chance I can, for example, disable the NavMeshAgent underwater for my game? I have a bug with my animals that some of them walk around the world (don’t ask me why) into the water, as if it’s a normal agent. They’re not expected to swim (those that are expected have that taken care of), so they just… walk into the water

Any chance you know of a script or something that deactivates the NavMeshAgent under the Water Layer Mask? I have to mention as well, the Terrain is one large chunk of land. It’s not small parts, hence why I’m asking about it :slight_smile:

Alternatively, how do you install the NavMesh Modifier again in Unity 2021 LTS? (Edit: Figured it out, but that failed drastically as well…)

Your water should likely have a plane or other mesh that represents the water itself.
Add a NavMeshModifier to the plane or mesh (if you’re using the Legacy NavMesh, a NavMeshObstacle), and set the modifier’s status to Not Walkable. Then rebake your NavMesh Surface.

Not sure if I’m following correctly or not, but… the Water is just an empty gameObject with a box collider on it, and it collides with the land, as you can see from the picture above

Unfortunately, even with the NavMeshModifier (I re-installed the new NavMesh by Unity’s GitHub link. For the NavMeshModifier, I activated ‘Override Area’, and then ‘Area Type’ was changed to ‘Non-Walkable’)/NavMeshObstacle (that one was a total fail tbh…), the NavMesh still completely ignores the point where the empty water gameObject (with a Box Collider) collides with the water, hence it doesn’t make the bottom part of the water unwalkable

Am I following this right…? Any other solutions you may be able to suggest?

I’d use RayCasts, but this will be computationally expensive if there’s a lot of birds down the line…

Edit: And then I tried something funny, like placing a Plane under the water and placing the NavMeshModifier there, but to no avail. For the time being, I asked the water asset creator about it, so I don’t disturb you further about this ((I asked for a bank system assistance) although I might… but that’s like a solution after all else fails :sweat_smile:)

This is how I did it on my first sandbox level. It worked with no issues at all.

  1. Position the plane where the water is.

  2. Add the navmesh modifier and set to unwalkable.

  3. Make the water asset a child object of the plane.

  4. Disable the mesh renderer on the plane.

  1. Disable the Mesh Collider on the plane so you’re not walking on water :slight_smile:

Edit: and… by pure accident, I realized why my enemy was unable to walk on some areas, when experimenting with the NavMeshAgent. Apparently although my angle was maxed out at 60 degrees, my step height was a bit too low, and I completely forgot this even exists… No more NavMesh issues at least, but still… there’s a “I can walk into the water like it’s no big deal” problem

Edit 2: For this one, I got the NavMeshAgent area underwater to be non-walkable, but… the Raven still walks on it. For anyone using Unity 2021 LTS like me and is confused down the line, here’s what I did:

  1. ‘Window → Package Manager’ → ‘+ sign’ → ‘Add Package from git URL’ → Input ‘com.unity.ai.navigation’

  2. On my Terrain, I placed the “NavMeshSurface” component, and completely deleted the old “AI → Navigation” thingy… This was mind-bogglingly confusing for me, until I realized it’s meant to be REPLACED

  3. I added an Empty GameObject on my Water profile, called it “NavMeshWaterBlocker”, and set its dimensions accordingly, and then on my “NavMeshModifierVolume” component over there I set it to Non-Walkable, and then on the ‘NavMeshSurface’ I re-baked it and it seems to have done the right thing

  4. I learned that modifications to the “NavMeshSurface” can now be done under the ‘Agent’ of ‘Windows → AI → Navigation’, and the NavMeshSurface will automatically accustom itself to it

Anyway, our enemy agents beautifully avoid the water now, but my Raven ain’t budging, because his NavMeshAgent is overridden by another script, called ‘AIControl.cs’. He’ll get me no matter what (that’s a problem to ask MalberS for help with, not here)

Can I ask another question for the time being? How do you get the enemies to attack each other? I found a function in ‘Fighter.cs’ called ‘OnTakenHit’ that is responsible for what happens to the enemy when he’s hit. However, I can’t find out the code that gets them to fight one another… At least when I try this:

target = null;
Attack(instigator);

they get really confused and don’t even move, because… well… they don’t know who to attack, which has me guessing that the player is still on their record

All I want is “You hit me, fine I’ll fight you till death once and then whoever wins will go back to trying to attack the player (assuming we were doing that earlier). If you respawn, you don’t remember our fight” kinda thing (I’ve been trying to get this to work for a while today. Time to ask questions :stuck_out_tongue:)

If I knew the flow of logic we currently have for how enemies attack the player, I wouldn’t have asked this question…

Where is this code? I hope you don’t mean in Fighter, because Fighter doesn’t manage targets with the third person anymore, remember?

You’ll need to keep a separate variable in your EnemyStateMachine for the last character that hit it.

First, in Health, you need to pass along the Instigator in an event… In this case, I added a new event

public event System.Action<GameObject> OnDamageInstigator;

And of course in TakeDamage, add to the list of damage events called

OnDamageInstigator?.Invoke(instigator);

Then in EnemyStateMachine, add a reference to LastAttacker

publicHealth LastAttacker {get; private set;}

And in Start, add this to your Health subscriptions:

            Health.OnDamageInstigator += SetLastAttacker;

And add these two methods:

        void SetLastAttacker(GameObject instigator)
        {
            if (LastAttacker != null)
            {
                LastAttacker.onDie.RemoveListener(ClearLastAttacker);
            }

            LastAttacker = instigator.GetComponent<Health>();
            LastAttacker.onDie.AddListener(ClearLastAttacker);

        }

        void ClearLastAttacker()
        {
            LastAttacker = null;
        }

Now, wherever you are making decistions to attack the Player, first check to see if LastAttacker is null or has a value.

First, let’s look at EnemyBaseState

        protected bool IsInChaseRange()
        {
            if (stateMachine.LastAttacker != null && !stateMachine.LastAttacker.IsDead()) return true;
            if (stateMachine.Player.GetComponent<Health>().IsDead()) return false;
            return Vector3.SqrMagnitude(stateMachine.transform.position - stateMachine.Player.transform.position) <= stateMachine.PlayerChasingRangedSquared;
        }

        protected bool IsAggrevated() => stateMachine.CooldownTokenManager.HasCooldown("Aggro") || stateMachine.LastAttacker!=null;
        

Then an adjustment to the first line of MoveToPlayer in EnemyChasingState()

        private void MoveToPlayer(float deltaTime)
        {
            if (stateMachine.Agent.enabled)
            {
                stateMachine.Agent.destination = stateMachine.LastAttacker!=null? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position;
                Vector3 desiredVelocity = stateMachine.Agent.desiredVelocity.normalized;
                Move(desiredVelocity * stateMachine.MovementSpeed, deltaTime);
                stateMachine.Agent.velocity = stateMachine.CharacterController.velocity;
                stateMachine.Agent.nextPosition = stateMachine.transform.position;
            }
            else
            {
                Move(deltaTime);
            }
        }

And an adjustment to the call to FaceTarget in EnemyAttackingState.Tick()

            FaceTarget(stateMachine.LastAttacker!=null? stateMachine.LastAttacker.transform.position : stateMachine.Player.transform.position, deltaTime);
            Move(deltaTime);

Now when an enemy is attacked by another enemy, he’ll go aggro on that enemy just like if you were playing classic Doom (a game from the 90s). However, even if that character is attacking another enemy, once YOU attack, the character should attack YOU (as you’ll become the LastAttacker).

oh that was something I created for automatic attacking back in the Point-and-click days. When we got to third-person, it became an empty function.

this was just a rough attempt of mine, right before I asked the question here… (I honestly thought it would be simple)

I read the response and literally went “isn’t it like 6AM there? How did you figure this out in a few minutes…?!” :stuck_out_tongue:

Jokes aside, I’ll give this a go in a bit (I have to head down to buy food to break my fast for now. Time will be due in an hour) and update you in a bit :smiley:

I’m working on my code computer, and I know the general steps already.

This hasn’t been fully tested, though, so you’ll likely need to tweak a thing or two (I may have missed something that targets the player somewhere, for example, and so some divergent behavior could occur… Additionally, as I glance back over this code, I think if you hit the enemy, then another enemy hits the enemy and so on, the poor guy will just start switching back and forth between you in confusion. That’s where an Aggro percentage value would come in. That’s far beyond the scope of this, though. Off to work I go.

that’s… what was happening before I asked the question. I’ll test it as soon as I’m back :slight_smile:

not a single clue what this is, but I trust your judgement :smiley:

[FEEL FREE TO IGNORE, I THINK I SOLVED EVERYTHING MENTIONED HERE]

[NOTE: Apart from Problem Number 1, of the Blocking State (I’ll debug this later, I have to head to Dubai now), I think the other 3 were solved. Kindly just review and let me know if anything can be changed or done better]

I know Doom. Believe it or not, I once programmed a Reinforcement Learning Algorithm (following the instructions of another teacher from Google, on another Udemy course) back in 2016 to play Doom… Like any other broke programmer, I never executed the code due to hardware limitations (I was 18 back then. I’m 26 and ultra broke now :stuck_out_tongue:)

anyway, I think the system works… I managed to see them fight one another in glimpses here and there, but sometimes they’ll be spinning around like crazy (and it’s a little hard to get their focus off me). I think they don’t know who their next target was… However:

Problem #1: my blocking state approach was a bit lousy, so whenever I block, they can’t even hit each other. I’m guessing it has something to do with what I once added in ‘Fighter.TryHit()’, right before taking the damage and the applied force:

                    /* if (other.tag == "Player") // (blocking state) or you can try get the SkillStore, either works...!
                    {
                        bool inVulnerable = other.GetComponent<Health>().GetInvulnerable();
                        if (inVulnerable) return; // add more information in here, to accept up to 50% original damage (use Random.Range for that), and impact state for example (the better the shield, the less likely it is to allow damage through)
                    } */

which leads to this, in ‘Health.cs’:

    private bool isInvulnerable = false;

    public void SetInvulnerable(bool isInvulnerable)
    {
        this.isInvulnerable = isInvulnerable;
    }

    public bool GetInvulnerable()
    {
        return isInvulnerable;
    }

which… is called from the script I created for ‘PlayerBlockingState.cs’:

using RPG.Combat;
using UnityEngine;

namespace RPG.States.Player {

public class PlayerBlockingState : PlayerBaseState
{

    public PlayerBlockingState(PlayerStateMachine stateMachine) : base(stateMachine) {}

    public override void Enter()
    {
        stateMachine.Health.SetInvulnerable(true);
        stateMachine.Animator.CrossFadeInFixedTime(GetBlockHashName(), stateMachine.CrossFadeDuration);
    }

    public override void Tick(float deltaTime)
    {
        Move(deltaTime);

        // If the player wasn't blocking, return to targeting:
        if (!stateMachine.InputReader.IsBlocking) 
        {
            stateMachine.SwitchState(new PlayerTargetingState(stateMachine));
            return;
        }

        // If the target is dead, return to freeLook state:
        if (stateMachine.Targeter.CurrentTarget == null) 
        {
            stateMachine.SwitchState(new PlayerFreeLookState(stateMachine));
            return;
        }
    }

    public override void Exit()
    {
        stateMachine.Health.SetInvulnerable(false);
    }

        public string GetBlockHashName()
        {
            string description = "";
            var playerFighter = GameObject.FindGameObjectWithTag("Player").GetComponent<Fighter>();

            if (playerFighter.GetCurrentWeaponConfig() != null && !playerFighter.GetCurrentWeaponConfig().IsTwoHanded && playerFighter.GetCurrentShieldConfig() == null)
            {
                Debug.Log("You have a weapon, single-handed, and no shield, so 1-Handed Block");
                description = "1HBlock";
                return description;
            }

            if (playerFighter.GetCurrentShieldConfig())
            {
                Debug.Log("You have a shield in-hand");
                description = "ShieldBlock";
            }

            if (playerFighter.GetCurrentWeaponConfig().IsTwoHanded)
            {
                Debug.Log("You have a two-handed weapon");
                description = "2HLongBlock";
            }


            return description;
        }

    }

}

[ATTEMPTED TO SOLVE BELOW, PLEASE CHECK]
Problem #2: And their chase ranges are now insanely long, so they’ll follow me for really lengthy distances (ONLY WHEN THEY ARE TWO GUYS FOLLOWING ME… If it’s just one guy, he won’t chase me that long), it’s getting a bit creepy… Is that normal?! (and the rangers’ chase range and Aggro timer is pretty much infinity as we speak… No joke!)

And problem #3: When I respawn, and let’s say they both did fight each other before my death, when I get back to them, they’re not reset to their patrolling positions. They just look like two kids who were doing a crime, and the teacher caught them, and now they gotta hide it up by smiling their ways to your heart, otherwise you’ll catch them…

[ATTEMPTED TO SOLVE BELOW, PLEASE CHECK]
And… problem #4: if the last attacker of one enemy is another enemy, he won’t hit the other enemy unless I am close to him (and I’m here like “huh…?! Why are you relying on my range? You’re supposed to rely on the range to whoever last attacked you, not always mine…”). I’m not even going to lie, I enjoyed laughing at this one… I set my 2H enemy to be close to the Ranger, and the ranger kept firing at him until he killed him. But now… I need to fix it :slight_smile:


Edit 1: For problem #4, I managed to get the enemy to attack the NPC that hit him when I’m way out of range, by making a small change in ‘IsInAttackingRange’. Changing it from this:

        private bool IsInAttackingRange() 
        {
            return Vector3.SqrMagnitude(stateMachine.Player.transform.position - stateMachine.transform.position) <= attackingRange;
        }

to this:

        private bool IsInAttackingRange() 
        {
            return Vector3.SqrMagnitude(stateMachine.LastAttacker.transform.position - stateMachine.transform.position) <= attackingRange;
        }

All I did was change ‘stateMachine.Player.transform.position’ to ‘stateMachine.LastAttacker.transform.position’

BUT… Now when I’m in his range, he won’t move to me, because he’s not been instructed on who to follow if there’s no ‘LastAttacker’, so I updated my ‘IsInAttackingRange()’ a little further. Now it looks like this:

        private bool IsInAttackingRange() 
        {
            if (stateMachine.LastAttacker == null) 
            {
                return Vector3.SqrMagnitude(stateMachine.Player.transform.position - stateMachine.transform.position) <= attackingRange;
            }

            else return Vector3.SqrMagnitude(stateMachine.LastAttacker.transform.position - stateMachine.transform.position) <= attackingRange;
        }

@Brian_Trotter whenever you have a moment to spare, please review these changes :slight_smile: - they don’t seem to be causing any issues, BUT… You never know what happens down the line because of something silly I did


Edit 2: For the second problem, with the enemy having incredibly long pursuit times, I figured it’s because of how long he has been aggrevated for. I go check the “EnemyBaseState.IsAggrevated()”, and I find this:

    protected bool IsAggrevated() {
        // only aggrevate the enemy either if he's "Aggro" (i.e: he is temporarily mad), or someone attacked him:
        return stateMachine.CooldownTokenManager.HasCooldown("Aggro") || stateMachine.LastAttacker != null;
    }

which means, he’s either got a target, OR he is temporarily mad at the NPC (so if the player ain’t dead, regardless of the timer, he will pursue him forever, which completely obliterates the need for a cooldown timer here… I needed to resolve this)

to combat this, I changed it to this:

    protected bool IsAggrevated() {
        // only aggrevate the enemy either if he's "Aggro" (i.e: he is temporarily mad), or someone attacked him:
        return stateMachine.CooldownTokenManager.HasCooldown("Aggro") && stateMachine.LastAttacker != null;
    }

so now he must have a target AND he must be angry (which means if his anger is out, stop trying to attack the instigator/player). That does seem to solve the problem, or so I think… (for the ranger, it does. For the melee warriors, it does not. If you hit a melee warrior, he will chase you pretty much forever)

so to fix the problem with the melee warriors, I updated ‘IsInChaseRange()’, from this:

to this:

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.CompareTag("Player") && stateMachine.LastAttacker != null && !stateMachine.LastAttacker.IsDead()) return true;
        
        // If you have no target, and the player is still alive, AND HE'S CLOSE, chase the player down:
        return Vector3.SqrMagnitude(stateMachine.transform.position - stateMachine.Player.transform.position) <= stateMachine.PlayerChasingRangedSquared; // temporarily replaced by the test below
    }

it’s a minor update, basically only permanently chasing the NPCs forever (until one of them dies), but the player is still under the ‘Aggro’ timer, but it makes a big difference

However, if one enemy kills another and then I escape their chase range, they don’t know how to return to Idle (they will turn to statues until I get close to them again)… I need to find a place to tell them “if your LastAttacker is null, and you’re not hostile, switch to Idle” or something


Edit 3: To fix the problem I just mentioned above, about enemies turning to statues, from experimenting I realized that these guys just don’t know what to do for the rest of their “Aggro” period of the Cooldown timer. It’s when I noticed that at low health and quick death, they will return to patrolling as usual, but at high health they won’t (and that’s when I noticed they have hidden aggro times)

so to fix it, I went to ‘EnemyStateMachine.ClearLastAttacker()’, this one:

and added an extra line to compensate for the Aggro time:

    // TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
    void ClearLastAttacker() 
    {
        LastAttacker = null;
        CooldownTokenManager.SetCooldown("Aggro", 0f);
        SwitchState(new EnemyIdleState(this));
    }

Finally, to get the Projectile enemy to stop firing his arrows at you whilst he’s fighting someone else, I made a few changes to ‘EnemyStateMachine.GetTarget()’. From just ‘return Player;’ to this:

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

I’m just documenting here for 2 reasons:

  1. If I mess things up, I know how to reverse the changes
  2. Brian gets to make sure he knows what I’m doing. In case I do anything stupid… :slight_smile:

Once the blocking state issue is resolved, I’ll turn this mechanic off for enemies in the same aggro group. So now different creatures in different Aggro Groups will be able to fight different creatures, and now I don’t need any wild classes, or so I think…


Edit 4: For the blocking state, I recalled something extremely important all of a sudden from what you have taught me a few days ago when we were trying to fix the two-colliders-on-player issue, regarding the continue statement in ‘foreach’ loops. If I recall correctly, you mentioned that ‘continue’ breaks out of the foreach cycle, and goes on to the next ‘foreach’ cycle, and that’s when it hit me that the ‘if (other.tag == “Player”)’ was not wrong, it just needed to ‘continue’ instead of return (in other words, if there’s a block, ignore whoever is blocking, and just ‘continue’ on to damaging anyone else who was not blocking, and was within the Sword’s damage radius). So now, if the player is blocked, ignore him and hurt anyone else who was hurt by the sword. In the end, my final loop has changed from this:

if (other.tag == "Player") // (blocking state) or you can try get the SkillStore, either works...!
                    {
                        bool inVulnerable = other.GetComponent<Health>().GetInvulnerable();
                        if (inVulnerable) return; // add more information in here, to accept up to 50% original damage (use Random.Range for that), and impact state for example (the better the shield, the less likely it is to allow damage through)
                    }

to this:

                    if (other.CompareTag("Player") && other.GetComponent<Health>().GetInvulnerable()) continue;

The final step will be to find a way to only activate enemies to fight one another only if the enemy is not part of the AggroGroup. So any SOLO enemies or other AggroGroup enemies will fight one another :slight_smile: (heck, I may even make them hostile to enemies of other groups, but that’s just diving into deep territory for now. I’ll delay this until after the bank)

And… before I almost forget, I changed this line in ‘EnemyChasingState.Tick()’ from this:

FaceTarget(stateMachine.Player.transform.position, deltaTime);

to this:

                    // TEST (SUCCESS): get the enemies to point at each other, instead of the player, if they're fighting one another:
                    if (stateMachine.LastAttacker == null) stateMachine.SwitchState(new EnemyIdleState(stateMachine)); // ensures the enemy knows what to do if the enemy is dead (if this isn't here, NREs will be unleashed like bombs...!)
                    else FaceTarget(stateMachine.LastAttacker.transform.position, deltaTime);

so the enemy knows how to deal with what to do once his target is killed

and considering the new changes in ‘GetTarget()’ of ‘EnemyStateMachine.cs’, this means that projectiles need to also be blocked from being fired by an NPC if their target is dead (otherwise it’ll keep striking you when it enters the trigger of touching you, and now you’re constantly dying over and over and over again, endlessly). To do that, change this ‘if’ statement in ‘Fighter.Shoot’ from this:

            if (targetObject != null)
            {
                // use the 'LaunchProjectile' function with 5 arguments (as we do have a target)
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, targetObject.GetComponent<Health>(), gameObject, damage);
            }
            else
            {
                // use the 'LaunchProjectile' function with 4 arguments (as we don't have a target)
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, gameObject, damage);
            }

to this:

            if (targetObject != null && !targetObject.GetComponent<Health>().IsDead())
            {
                // use the 'LaunchProjectile' function with 5 arguments (as we do have a target)
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, targetObject.GetComponent<Health>(), gameObject, damage);
            }
            else
            {
                // use the 'LaunchProjectile' function with 4 arguments (as we don't have a target)
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, gameObject, damage);
            }

it’s a tiny change, but now you won’t have arrows firing against dead players, and now you’re not consistently being killed over and over again into weird bugs

Good news @Brian_Trotter - I solved all (or so I think…) issues I mentioned regarding the tweaks needed to get the code working, so you can just ignore the previous insanely long comment. For now, we can go back to trying to implement the bank. Hope to get some nearby updates on that one soon (I still can’t solve the banking system issue, since the ‘ShowHideUI.cs’ script was deleted in one of the tutorials…, even if my life depended on it :confused:)

In the meanwhile, I’ll investigate and see if somehow I can make enemies in the same AggroGroup (the script below) ignore accidental hits from teammates and only attack enemies who accidentally hit them from outside the AggroGroup. That way, I have the cheap version of enemy groups that can fight one another :slight_smile:

AggroGroup.cs (if necessary):

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

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)

        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:
            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
            enemies.Remove(enemy);
            enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
        }

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

    }

}

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

Privacy & Terms