Enemies moving around Obstacles

As a rule of thumb, obstacle avoidance with NavMesh is more a matter of level design than added code.

I think I see a code problem, however…
The Move() statement needs to come before the updating of the Agent.velocity and Agent.nextPosition.

I’ll test the changes, but a matter of fact, I do have a severe problem in this script. This block, at the top of ‘EnemyChasingState.Tick()’:

            if (!IsInChaseRange() && !IsAggrevated())
            {
                // stateMachine.TriggerOnPlayerOutOfChaseRange(); // <-- find another way for this, because having it here nullifies the player as the NPC last attacker, causing nothing but trouble
                stateMachine.SwitchState(new EnemyIdleState(stateMachine));
                return;
            }

(when I wrote it as a ‘!ShouldPursue()’, I was still testing stuff around, but this is the original form)

always, somehow, gets the enemy to cancel the player being his Last Attacker after his first hit, and then it acts normal. I can’t delete this, unfortunately either, because if I do it means they’ll chase me through infinite distance

It’s been driving me nuts yesterday for 6 hours as of why, and I am clueless. Here’s the relevant functions, if you’d like to have a look:

IsInChaseRange():

    /// <summary>
    /// Is the instigator within chase range? If there's no instigator, is the player in chase range? (Assuming this enemy is hostile)
    /// </summary>
    /// <returns></returns>
    protected bool IsInChaseRange()
    {
        // if your Last Attacker is not the player, and he's alive, find your way towards him
        if (stateMachine.LastAttacker != null && !stateMachine.LastAttacker.CompareTag("Player") && stateMachine.LastAttacker.GetComponent<Health>() != null && !stateMachine.LastAttacker.GetComponent<Health>().IsDead()) return true;
        // If you got this far, it means you got attacked by the player, so use that
        return Vector3.SqrMagnitude(stateMachine.transform.position - stateMachine.Player.transform.position) <= stateMachine.PlayerChasingRangedSquared; // <-- THIS IS SUPPOSED TO BE TRUE INITIALLY AS WELL, AND SOMEHOW IT RETURNS A FALSE ALTHOUGH THE PLAYER IS VERY NEARBY...?!
    }

IsAggrevated():

    /// <summary>
    /// Are you currently Aggressive (based on Aggro CooldownTimer), and did you recently get hit?
    /// </summary>
    /// <returns></returns>
    protected bool IsAggrevated() 
    {
        // only aggrevate the enemy either if he's "Aggro" (i.e: he is temporarily mad), AND his attacker isn't dead:
        return stateMachine.CooldownTokenManager.HasCooldown("Aggro") && stateMachine.LastAttacker != null; // <-- THIS IS ALSO SUPPOSED TO RETURN TRUE, BUT SOMEHOW AT FIRST IT ALSO RETURNS FALSE...!
    }

‘TriggerOnPlayerOutOfChaseRange()’:

    /// <summary>
    /// This function is called when the player is out of Chase Range of whoever has him as a target,
    /// in 'EnemyChasingState.cs'
    /// </summary>
    public void TriggerOnPlayerOutOfChaseRange()
    {
        ClearPlayer(this);
    }
    
    /// <summary>
    /// If the player is out of Chase Range of a specific enemy who used to hate him, that enemy
    /// must forget that the player was their enemy, when the player quits their chase range
    /// </summary>
    /// <param name="enemy"></param>
    void ClearPlayer(EnemyStateMachine enemy)
    {
        if (enemy != null && enemy.GetAggroGroup() != null && enemy.GetAggroGroup().GetPlayer() != null)
        {
            // if you're part of the Player's AggroGroup, do this:
            // if (LastAttacker != null) SetLastAttacker(null);
            enemy.SetHasOpponent(false);
            enemy.SetOpponent(null);
            enemy.SetHostile(false);
        }
        else
        {
            // if you're NOT part of the Player's AggroGroup, do this:
            // if (LastAttacker != null) SetLastAttacker(null);
            enemy.SetHasOpponent(false);
            enemy.SetOpponent(null);
            // enemy.SetHostile(GetInitialHostility);
            enemy.ResetHostility(); // TEST - 16/5/2024
        }
    }

and it’s driving me nuts why the first one, somehow, has these two actually respond as a false:

            if (!IsInChaseRange() && !IsAggrevated())

any chance that you know how to find where the code flows towards, starting from a debug point? Similar to how System.Diagnostics.StackTrace() gets the function caller, I seek something that tells me where it goes to NEXT, after that debug is called. I can really use that to understand where the logic is going wrong

After a seriously lengthy debugging session, and turning off my music (if I turn off my music, it means it’s a severe bug, xD), I found out where I made the mistake. Apparently I was clearing the cooldown timer ALONG WITH clearing the Last Attacker, IN THE SAME FUNCTION (i.e: Not OOP anymore), and for some reason this was acting up and causing unnatural behaviour

So what I did instead, was call the cooldown timer clearing function in ‘OnDie’ for the enemies who were aiming for the script holder

That way, no unusual behaviour is done. At this point in time, I’m just rolling with whatever gets the code to run safely, especially for my ally system, without introducing unwanted behaviour

Edit: After even further tracking down, I noticed that it was because the natural response of ‘AggroGroup.cs’ when an NPC has taken a hit, was to assign everyone, from the team of whoever I just hit, TO A TEAM MEMBER OF THE PLAYER, which includes the NPC the Player has just hit. I wrote that function on my own, and I deserve what happened to me

Naturally, ‘SetLastAttacker’ had to erase that first, through ‘ClearLastAttacker’, which also cleared the Aggro Cooldown timer, hence the weird behaviour I mentioned in the previous post, where my NPC would give up quickly (because he’s no longer Aggro, since his cooldown timer was cleared off). This was a big mistake from my side :sweat_smile:

Long story short, change this:

    // TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
    public void ClearLastAttacker()
    {
        Debug.Log($"{this.gameObject.name} has called Clear Last Attacker from {new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name}");
        // first of all, make sure the script caller is not dead 
        // (otherwise you'll have enemies jumping out of death 
        // to attack you through the Idle State call, and that's dumb):
        if (!this.Health.IsDead()) {
        // 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));
        }
    }

to this:

    // TEST: Use this function when whoever the enemy was aiming for, is killed, or got killed by (in 'SetLastAttacker' above):
    public void ClearLastAttacker()
    {
        Debug.Log($"{this.gameObject.name} has called Clear Last Attacker from {new System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name}");
        // first of all, make sure the script caller is not dead 
        // (otherwise you'll have enemies jumping out of death 
        // to attack you through the Idle State call, and that's dumb):
        if (!this.Health.IsDead()) {
        // 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;
        // Introducing a Cooldown Timer Clearer here will result in severe bugs. Instead, it's done in 'OnDie' for each enemy who had this guy as his target, individually (that's probably going to be the only time where we need this anyway)
        SwitchState(new EnemyIdleState(this));
        }
    }

and in ‘OnDie’ (that’s usually where ‘ClearLastAttacker’ gets called from), add this in the foreach loop, which inspects all enemies who were aiming for the script holder, who just died:

                enemy.CooldownTokenManager.ClearCooldown("Aggro"); // TEST - 16/5/2024

It took me an insanely long time that getting the enemy to cooldown does not only involve clearing the Last Attacker, but also clearing their Aggro Cooldown Timer. I hope someone finds this message and saves themselves months of try and error

Anyway, back to our topic:

@Brian_Trotter how about dynamically dropped pickups? I see my NPCs struggle the most when it comes to navigating around them, and unfortunately they’re not static in the game

I’ll try adding a NavMeshObstacle to them and update you about it

Edit: That worked, but for rough surfaces on my game that need to be burned to the ground, I’ll make some modifications to the environment itself. So it’s not a code change as much as it is about making a playable environment right now

PROBLEM: It worked, but I don’t think those NPCs are re-drawing a path to avoid them… Anyway we can integrate a path redraw to fix this?

If it’s placed in the scene at runtime, it needs a NavMeshObstacle and the NavMeshSurface needs to be rebaked.
Your placement routine needs a reference to the NavMeshSurface, then you can call

surface.BuildNavMesh();

On most environments, this is so fast it’s not noticeable.

OK so… I made these changes in ‘RandomDropper.cs’, and the reason I chose this script is because it’s where the item dropping happens:

        // PRIVATE
        private NavMeshSurface surface;

        void Awake() 
        {
            // Cache the NavMeshSurface Terrain, so we can redraw the terrain with the drops
            // for AI Navigation to work as expected
            surface = FindObjectOfType<NavMeshSurface>();
        }

        public void RandomDrop() {

            var baseStats = GetComponent<BaseStats>();

                // This function is responsible for how our enemies drop loot when they die
                var drops = dropLibrary.GetRandomDrops(baseStats.GetLevel());
                
                foreach (var drop in drops) {

                DropItem(drop.item, drop.number);  // drops 'numberOfDrops' number of items for our player to drop it

            }


                // Redraw the NavMeshSurface around the dropped item
                surface.BuildNavMesh();

        }

This should work, right? Although I may want to change FindObjectOfType to FindObjectsOfType when I have many biomes. But generally speaking, this is fine, right?

Edit: This is not fast at all. It gives me some severe frame drops as hiccups. The game would freeze terribly as a result for a few seconds each time someone dies. You can guess what kind of disasters this would cause on the long run (and before you ask, yes I updated my NavMesh system to the new one, and soon I might even try to update my game to URP, not sure how just yet, but it’s on my mind)

On a side note, any updates about Object Pooling? Performance still struggles there :sweat_smile:

I was thinking you were having issues with navigating around your constructed items…
Pickups should be easy, if the collider is a trigger they should just walk right through them.
If, on the other hand, the Pickup’s collider is not a trigger… then yeah, hilarity will ensue.

for the constructed items, that’s something we will get to when I get to the environment design stage. For now, I’m just trying to work out the problems I have in my little game scene :slight_smile:

but you’ll still need a NavMeshObstacle on these pickups for the NPCs to move around them, otherwise they get stuck pretty badly (they still do as we speak, but it’s not as bad as what they were at before installing a NavMesh Obstacle on the pickups. At this point in time, unless someone else got a better suggestion, it’s out of my control)

I fixed like 6 minor bugs since we last spoke, I’m just waiting on the Object Pooling before I go into more advanced topics (like giving the animals health and other stuff, so they can be interacted with as well. This is one of the 3 remaining big topics that I want to tackle)

One more suggestion… Put the Pickups on a new layer. Make sure that layer intersects with the Pickup Finder. In your NavMeshSurface, exclude that layer from the NavMesh generation.

And of course, make sure that the colliders on the Pickups are set to Trigger.

Oh dear god, I have 28 layers already, this is about to get hard to stabilize… :sweat_smile: (I’ll need to re-check how useful each one is down the line) - for now, can we skip this step? I might need the last few layers soon for Parkour when I get to that topic (I’ll try not get you involved in this. I already have a working prototype for it, but it’s independent… it’ll take some time to integrate it though)


For now though, I have another problem (it’s one problem that’s driving me nuts, again) I’d like to fix before moving to other wild topics. I’ll go to the Dynamic Spawner topic and write about it there :slight_smile:

I’ve provided several solutions, all of which inexplicably don’t work for just you… I KNOW this step works. It’s how I keep the characters themselves from punching holes in the NavMesh and being unable to move.

you absolutely sure it works? Like, 1000% sure? :stuck_out_tongue: (nah just kidding. Overall though, they all seem to be playing just fine. For the time being, let 'em be. If I see that issue again, I’ll update my systems). I just can’t afford to recklessly use Layers at the moment, that’s it :slight_smile:

Whenever you’re available @Brian_Trotter - we can try Object pooling. In the meanwhile, I’ll be testing with another bug in the Dynamic NPC Spawning system (which I introduced to my own systems because of a not-so-well tested variable)

Privacy & Terms