AI Defence Systems

OK now I’m just going overboard with combat, but honestly speaking… I’m enjoying these advancements, xD

So my idea here, is to get the AI to be able to defend themselves during combat, for both distance-type of attacks, which includes ranged and magic attacks, and nearby melee attacks. Again, I’m splitting this into two parts, the ranged part and the melee part (although I haven’t given the melee part much thought, but we’ll figure it out down the line). The entire thing will run by a boolean called ‘canDefend’ (i.e: some enemies in my game can defend themselves, and others can’t. It’ll depend on what type of creature you’re facing):

  1. For the Ranged Part, there’s an ‘OnTriggerEnter’ in ‘Projectile.cs’. We can use that to create a (somewhat) big sphere around the enemies, and when the projectile enters it, assuming you are the instigator’s LastAttacker, we can roll a random dice and if the number exceeds a specific threshold, get the enemy to roll, raise a shield (I haven’t worked out how to get enemies to wield shields as of yet) or do something to defend himself from whatever attack is coming his way

  2. For melee, you can use your sword, shield or whatever you got to try and defend yourself as well. I’ll find a way for this one in ‘Fighter.cs’ somehow

And when the defence is up, they can deflect an ‘x’ percentage of damage, depending on an integer for each weapon that’ll be there.

This sounds like another big topic, but I’m just writing this topic here for when I got any fun questions to ask.

OK so… First problem (and I’ll be trying to solve it as we speak), is that my sphere collider that is supposed to trigger a defence mechanic by the enemy is not working through code. This is my inspector’s setup:

The new script down there is meant to be called as a detection solution in code (I eventually deleted that, created a new tag, and used that instead (through a ‘CompareTag()’ function), but it still didn’t work

and I’m trying to call a debugger (which isn’t being called by the way) in ‘Projectile.OnTriggerEnter()’:

            if (other.GetComponent<IncomingRangedAttackDetector>())
            {
                Debug.Log($"Incoming Ranged Attack detected");
            }

(using the script for the time being) Any idea why that’s the case?

And before you ask, yes I went to the project settings and disabled any kind of interactions between projectiles, and this new element that I called ‘IncomingRangedDamageDetector’ in my LayerMasks and Tags

so… the enemy can take damage as usual, BUT… the trigger doesn’t detect incoming projectiles for some reason (and… I’m clueless. These functions just can’t work peacefully, can they? :stuck_out_tongue_winking_eye:)

Yeah, if you did this, the trigger won’t fire. Your post essentially says; “I disabled interaction. Why doesn’t it interact?”

OK you were right, it was a mistake from my side.

However, whilst it does need to be turned off (I just checked), but now because that collider is physically in the way, the enemy won’t get hit. Any ideas on how do I avoid the projectile from physically striking this instead of my enemy, who is in that trigger bubble?

It’s a trigger, the projectile won’t hit it. That’s the point of triggers. They act like colliders without the collision

except that this one does :sweat_smile: (and I have no idea why)

which is why I wanted to disable it in the project settings at the start

It doesn’t physically hit it. It won’t, it’s a trigger. You probably have code that checks for any collider and then destroy the projectile. While this is a trigger, it is still a collider. Fix the code. Add a tag or something and ignore it if it has the tag.

You have the right idea in terms of ignoring the collider. This is what you’ll do in normal circumstances, but in this case you don’t want to ignore it, but you also do. So you have to not ignore it in the physics, and add code to ignore it when you don’t want to know about it.
Perhaps you could look for that component. In Projectile you’d probably have something like

private void OnTriggerEnter(Collider other)
{
    if (other.TryGetComponent<IncomingRangedAttackDetector>(out var detector)
    {
        detector.NotifyIncomingProjectile(this);
        return;
    }
    // do normal projectile damage and destruction
}

Well that turned out to be much simpler than expected… Here’s what I found to work, in ‘Projectile.OnTriggerEnter()’ (I haven’t decided yet where exactly it should be, so for now it’s at the top of the function):

            if (other.CompareTag("IncomingRangedDamageDetector"))
            {
                Debug.Log($"Incoming Ranged Attack detected");
                return;
            }

Essentially, if the collider you just detected was the Incoming Ranged Damage Detector on the NPC, ignore it, by writing out a ‘return’ (i.e: Ignore having to damage yourself and try hurt that, because it’s pointless), and we can do all our required logic in here

and then after that, we can get the State Machine of the NPC and ask it to enter it’s own dodging state, or whatever they need to do to defend themselves


why do you have a detector variable on your NPC? :sweat_smile:

I don’t. You do.


Your code

Technically, it’s not the projectile’s job to decide if the target should defend so you really want the target to detect projectiles

ahh, my bad :sweat_smile: - I changed it to a simple tag detector (it takes me time sometimes to realize that things can be done in a much simpler way)

Anyway, new problem. Based on this block of code:

            if (other.CompareTag("IncomingRangedDamageDetector"))
            {
                Debug.Log($"Incoming Ranged Attack detected");
                var stateMachine = other.GetComponentInParent<EnemyStateMachine>();

                if (stateMachine != null)
                {
                    Debug.Log($"{stateMachine.gameObject.name} has been detected");
                }
                return;
            }

Initially I tried adding a checker to ensure that the instigator is not ‘other’, a way to get my NPC to avoid triggering this event if he’s the one who fired the arrow, by changing the if statement above to the following (I can see this becoming a serious problem down the line, if not dealt with now):

if (other.CompareTag("IncomingRangedDamageDetector") && other != instigator) 
{
// do something...
}

BUT… that failed. I’m still trying to investigate why

my idea is, if the projectile detects that layer, the parent of that layer (which the projectile just triggered), aka: the enemy himself, will do the dodging or blocking that’s needed, but the projectile is the one that should trigger the detector

Later on, I might swap the current detection system to an eyes-based one (I’m sure we messed with this idea before. I have a semi-working prototype somewhere in my code), to make this whole thing act a lot more realistically

After all this time, you still just say ‘it failed’ and then we have to spend 72 posts to try and figure out what exactly ‘it failed’ means.

Not the projectile’s job. The only job the projectile has is to fly where it should fly and to deal damage when it hits something.

‘it failed’ means that the instigator is triggering his own detector, essentially making him play the dodging state because he fired his own arrow, at someone else, and his own detector detected an arrow coming from somewhere (which is the instigator’s own arrow, which is meant to be ignored). The idea is to avoid that. In other words, don’t trigger your own incoming damage detector

I know what you are trying to do, but there are so many things that can go wrong here that “it failed” doesn’t mean anything.
Where does instigator come from? What does it look like in code?

other is the detector so unless the detector fired the arrow, instigator will never equal other

this is a bit complex, but bear with me:

Instigator is setup in ‘WeaponConfig.cs’, when the enemy/player fires a weapon, through a function called ‘LaunchProjectile’ (which, similar to my dodging state, is split into two parts. One is for freelook, where you fire a straight arrow, and the other is for targeting, firing an aimed-straight arrow), as follows:

    // Launching Projectile on a target in the targeting state:
    public void LaunchProjectile(Transform rightHand, Transform leftHand, Health target, GameObject instigator, float calculatedDamage) {

            // This function instantiates our Projectile (for our ranged weapons), 
            // sets its position (either from right or left hand), 
            // and Rotation (using Quaternion.Identity)

            // NOTE: GetMainHand() is out of course content. Original content function is 'GetTransform()' below:
            // Projectile projectileInstance = Instantiate(projectile, GetMainHand(rightHand, leftHand).position, Quaternion.identity);
            Projectile projectileInstance = Instantiate(projectile, GetTransform(rightHand, leftHand).position, Quaternion.identity);
        
        // Finally, we also want to set the target for our projectile to follow (our player/enemy), 
        // set the one who started the battle as the instigator, 
        // and give it damage to deal whoever this projectile is aiming at
        projectileInstance.SetTarget(target, instigator, calculatedDamage, GetSkill());

    }

    // for cases of firing a projectile when we don't have a target (freelook state):
    public void LaunchProjectile(Transform rightHandTransform, Transform leftHandTransform, GameObject instigator, float damage)
    {
        Transform correctTransform = GetTransform(rightHandTransform, leftHandTransform);
        Projectile projectileInstance = Instantiate(projectile, correctTransform.position, Quaternion.identity);
        projectileInstance.transform.forward = instigator.transform.forward;
        projectileInstance.SetupNoTarget(instigator, damage);
    }

and then it’s setup in ‘Projectile.cs’:

        public void SetTarget(Health target, GameObject instigator, float damage, Skill skill) {

            // This function calls 'SetTarget' below, so we can aim at our enemies using our abilities
            SetTarget(instigator, damage, target, Vector3.zero, skill);

        }

        public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage, Skill skill) {

            SetTarget(instigator, damage, null, targetPoint, skill);
}


        /// <summary>
        /// This function sets the target of our projectile. 1. It sets the target the projectile is aiming at, 2. it keeps track of him,
        /// 3. It sets the damage destined to the projectile target, 4. It keeps track of who is responsible for firing that projectile, and 5.
        /// it sets the skill that will get the XP for firing that hit
        /// </summary>
        /// <param name="instigator"></param>
        /// <param name="damage"></param>
        /// <param name="target"></param>
        /// <param name="targetPoint"></param>
        /// <param name="skill"></param>
        public void SetTarget(GameObject instigator, float damage, Health target = null, Vector3 targetPoint = default, Skill skill = Skill.Ranged) {

            this.target = target;
            // our Target is now the GameObject holding this script
            this.targetPoint = targetPoint;
            // keep track of the target we are firing our Projectile abilities at
            this.damage = damage;
            // The damage dealt by our Projectile = damage from SetTarget ('float damage' argument)
            this.instigator = instigator;
            // The instigator is set to be an instance of whoever holds this script
            this.skill = skill;
            // The skill to train when a projectile is fired
            Destroy(gameObject, maxLifeTime);   // destroys our Projectile after its maximum lifetime (so it doesn't take a toll on our computer)
        }

If needed, here’s also my ‘Projectile.OnTriggerEnter()’:

        // New Code, for Third Person projectiles:
        private void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("IncomingRangedDamageDetector"))
            {
                Debug.Log($"Incoming Ranged Attack detected");
                var stateMachine = other.GetComponentInParent<EnemyStateMachine>();

                if (stateMachine != null)
                {
                    Debug.Log($"{stateMachine.gameObject.name} has been detected");
                }
                return;
            }

            // This function tells the projectile holding this script what to do
            // when it hits something, depending on whether it's a target, or just 
            // something in the way

            // if he's dead, just keep going. Don't try to damage him again...:
            // if (other.GetComponent<Health>().IsDead()) return;

            if (other.gameObject == instigator) return; // don't hit yourself

            // if (other.TryGetComponent(out Health health)) health.TakeDamage(instigator, damage, instigator.GetComponent<Fighter>().GetCurrentWeaponConfig().GetSkill());

            // TEST (if failed, uncomment the line above)
            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());
            }

            if (other.TryGetComponent(out ForceReceiver forceReceiver)) forceReceiver.AddForce(transform.forward * damage * 0.5f, true); // multiplied by 0.5f so as to neutralize the knockback impact

            speed = 0;

            onHit.Invoke();

            if (hitEffect != null) Instantiate(hitEffect, transform.position, transform.rotation);

            foreach (GameObject toDestroy in destroyOnHit) Destroy(toDestroy);

            Destroy(gameObject, lifeAfterImpact);

            Debug.Log($"Instigator = {instigator.name}");
            Debug.Log($"Collider = {other.name}");
            Debug.Log($"Damage Dealt = {damage}");
        }

I’ll be gone for a bit now (apologies for this), and will be back in an hour or two, but I think I have an idea for this. My idea would be to check if the instigator is the parent of the detected detector, then we can cancel it out. I’ll tag you when I return :slight_smile: (again, I’m sorry for this)

The detector just needs to know which game object is used as its own instigator. Add a field and drag the root game object there (I am assuming it’s the one that’s being used as the instigator when firing the weapon). Either make it public, or make a getter. Then, when you check it, use that. I’m going to make this the state machine since you also use that

// in the detector script - which you now need
[field: SerializeField] public EnemyStateMachine StateMachine { get; private set; }

// In the projectile script - which is not the right place but whatever, I'm not the one maintaining this code
if (other.TryGetComponent<IncomingRangedAttackDetector>(out var detector))
{
    if (detector.StateMachine.gameObject == instigator)
    {
        Debug.Log("This is my projectile, ignore");
    }
    else
    {
        detector.StateMachine.SwitchState(new EnemyDodgeState(detector.Statemachine));
    }
    return;
}

the detector script goes on the detector itself, right? I’ll give this a test run now :slight_smile:

WAIT ONE SECOND… What’s the best place to have this script placed in then? I figured ‘Projectile.cs’ would be a good spot, because it has an ‘OnTriggerEnter’ and it’s the one doing the triggering

I just want to keep this as clean and error-free as possible. So if you foresee a problem that I don’t, please let me know

and your approach above worked. Thank you :slight_smile:

I ended up copy-pasting most of my player’s Targeting State’s dodging system for this one, and used the same blend tree I created. Let’s just say I’m laughing so hard at how much fun the game has suddenly become!

TOTALLY WORTH IT :rofl:

(and he rolls based on a random dice roll that randomizes which direction he’s supposed to go. I might actually want to make this formula a little stronger, as in find a way to block him from rolling in the direction of the arrow, but I have no idea how… Will this even be computationally expensive to maintain? Let’s see what Brian has to say about this when he’s back)

Yes

I told you; it’s not the projectile’s responsibility to trigger the dodge. It should rather be the detector that checks if projectiles have entered it and act appropriately

Of course it did. :sunglasses::yum:

Be sure not to make it too strong or your enemies will become impossible to kill.

1 Like

Showoff :stuck_out_tongue_winking_eye: (I’m joking, I’m joking)

I’m actually enjoying this, but I wanted to make it just a little stronger. In such a way that they’ll dodge in any direction EXCEPT the direction that the arrow is coming from, BUT WITHOUT BEING A MATHEMATICAL NIGHTMARE. Any idea how to make this happen?

do we just copy-paste the code from there to there, or will there be major modifications? I just thought the projectiles would be easier to work with

In the end, I’ll throw in a probability factor that determines the chances of the enemy dodging a hit, depending on who you’re dealing with

Privacy & Terms