Forum rules - please read before posting.

How to make NPC avoid the player and other NPC

I could not find a clear answer anywhere to the question: what exactly is required for NPC to move around player and other NPC in the 3D world and Unity navigation?

The pathfinding based on baked NavMeshes works fine. But whatever I do, my NPCs do not see the player at all when they plan their trips. They want to walk right through him. I tried attaching NavMeshAgents with NavMeshAgentIntegration, but it does seem to make any difference. The result is the same: NPC does not avoid the player.

"Pathfinding update time" in Settings Manager is set to non-zero. Do I need to set something up?

There is one solution that actually works: when I attach NavMeshObstacle with "Carve" on to the Player prefab, it creates a hole in the mesh around the player. And all NPCs walk around the player like they should.

However, it creates another problem: the pathfinding of the player himself gets messed up. Due to the fact that it first needs to get out of the hole (and it directs the player anywhere it likes) and then find the actual way.

Am I missing something here? Shouldn't NavMeshAgents do the job? Or the AC itself?
Anyone?

Comments

  • edited November 2018

    There may well be a native AC solution to the problem (I'm sure Chris will tell you if there is), but when I was last working on a 3D AC game, the only surefire way I found of getting NPCs to properly respond to the player when they were following a path was to write my own solution

    Basically that used Unity's OnCollisionEnter to detect whether they had collided with the player. I then stopped them from moving, and used OnCollisionExit to determine when the player had moved away, at which point I set them moving on their path again (AC's ResumeLastPath function)

    That seemed to work ok for my own needs, but of course it wasn't the same as proper pathfinding avoidance, it was just "Stop moving when you hit something and then resume moving a bit later"

  • edited November 2018

    You'd think NavMeshAgent would do it, but alas - AFAIK even Unity's own navigation system struggles with natively allowing dynamic pathfinding around agents - even more so when both characters are moving.

    One rule to keep in mind is that a GameObject should never have both a NavMeshAgent and a NavMeshObstacle attached and enabled at the same time.

    On the AC side of things, having a non-zero Pathfind update time (s) will cause characters to recalculate their path every X seconds (though this is ignored when a character uses a NavMeshAgent component). Recalculating the path will only have an effect if the NavMesh itself is changing - since the path calculation is handled by Unity.

    One quick-and-dirty trick to get dynamic avoidance, therefore, is to remove your NavMeshAgent / NavMeshAgentIntegration components and just keep your NavMeshObstacle with both Carve and Carve Only Stationary checked. With a small pathding update time (e.g. 0.1), a moving character will avoid a stationary character, and a stationary character will recalculate their own path once they begin moving and their own carve is removed.

    If you were to stick with the Agent, you could still also have an Obstacle - but you'd need to add a script that ensures only one is enabled at a time, depending on the character's movement state:

    using UnityEngine;
    using System.Collections;
    using AC;
    
    public class DynamicObstacle : MonoBehaviour
    {
    
        private NavMeshObstacle navMeshObstacle;
        private NavMeshAgent navMeshAgent;
        private Char myCharacter;
    
        private void Awake ()
        {
            navMeshObstacle = GetComponent <NavMeshObstacle>();
            navMeshAgent = GetComponent <NavMeshAgent>();
            myCharacter = GetComponent <Char>();
        }
    
    
        private void Update ()
        {
            navMeshObstacle.enabled = (myCharacter.charState == CharState.Idle);
            navMeshAgent.enabled = (myCharacter.charState != CharState.Idle);
        }
    
    }
    
  • There is a slight problem with NavMeshObstacle. According to Unity's documentation enabling and disabling carving has a 1 frame delay. Who knows why.

    I tested it, there is a delay are right. That makes everything way too complicated.

    The solution with NavMeshObstacle with carving is far the best so far. NPC's and the player avoid each other nicely, and not when they collide, but when they plan their path.

    I don't really know what NavMeshAgent does, because no matter what I do, it does seem to be doing anything at all. Pathfinding for NPC just ignores the player with NavMeshAgent as he wasn't there. NPC tries to walk right through him.

    So I think the best solution is to stick with NavMeshObstacle attached to player and every NPC.

    However, there is still that annoying problem with pathfinding, if the player is surrounded by a NavMesh hole created by the NavMeshObstacle. The first waypoint seems to be completely random.

    Here's the situation:

    The player is a red dot and the destination point is X. Let's say that the player is always outside of NavMesh, because of the NavMeshObstable attached.

    In the perfect world pathfinding algorithm should find the first point on the edge on NavMesh, where it is on the left picture. But it's not that simple.

    The pathfinding algorithm in AC is in GetPointsArray() in NavigationEngine_UnityNavigation.cs. It is actually pretty smart: it calls Unity's NavMesh.CalculatePath() function, but in our case that function fails every time.

    But the algorithm does not give up. Instead, it tries to find a better starting point for pathfinding, starting from the old one and searching in wider and wider circles, until it finds something that sits on the NavMesh.

    The problem is that the newly found starting point can anywhere on the edge of the hole created by NavMeshObstacle. Which makes the player make a few steps in a totally crazy direction before he starts walking to his destination.

    So I found a slightly better solution. Let the hunt for the new starting point start not from the player's position, but somewhere closer to the destination.

    It's still not perfect, but works better. Now when I make the player walk in the straight line with no obstacles (most of the cases), he does exactly that. If there are lots of turns on his way, well, at least his first steps are in a generally correct direction. It's not a perfect world, but acceptable.

    So if you want to make NPC avoid each other and the player in 3D world, enable Unity Navigation and give them all a NavMeshObstacle component with carving on.

    Then, create a custom navigation based NavigationEngine_UnityNavigation.cs.
    Modify the GetPointsArray() adding the following code right after:

    if (!NavMesh.CalculatePath (startPosition, targetPosition, -1, _path)) {

    The code:

    NavMeshObstacle ob = _char.GetComponent ();
    if (ob) {
    float radius;
    if (ob.shape == NavMeshObstacleShape.Capsule)
    radius = ob.radius;
    else
    radius = Mathf.Max(ob.size.x, ob.size.z) / 2;
    Vector3 trip = targetPosition - startPosition;
    Vector3 v = Vector3.Normalize (trip) * radius;
    startPosition += v;
    }

    I hope someone finds it useful.

    Also, I can think of two ways to further improve that.

    First, after finding a new starting point outside of your NavMeshObstacle hole, and calling NavMesh.CalculatePath(), how about calling it again?

    But this time, find your starting point by drawing a line from starting position to the first point the NavMesh.CalculatePath() returned. It will bring you much closer to the perfect world situation, especialy if you repeat the procedure two or three times.

    And the second improvement would be to completely ignore the first waypoint, the one on the edge of the hole.

  • Yes, good points.

    I, too, had encountered the "starting from edge" behaviour when looking into this myself. This did seem mitigated somewhat, however, by the use of the Pathfinding update time (s) field, since this can be used to force a recalculation of the path before the character has time to go the wrong way - if low enough.

    I agree that it would be more efficient to simply re-run CalculatePath 1-frame after doing so the first time, however. Fortunately, AC provides an OnCharacterSetPath custom event that you can hook into - see the tutorial here.

    Try the following script. In practice, it seems a short delay of 0.1s is more consistent than waiting a single frame:

    using UnityEngine;
    using System.Collections;
    using AC;
    
    public class CalcPathTwice : MonoBehaviour
    {
    
        private Char character;
        private bool canRecalculate = true;
    
        private void OnEnable ()
        {
            character = GetComponent <Char>();
            EventManager.OnCharacterSetPath += OnSetPath;
            EventManager.OnCharacterEndPath += OnEndPath;
        }
    
        private void OnDisable ()
        {
            EventManager.OnCharacterSetPath -= OnSetPath;
        }
    
        private void OnSetPath (Char character, Paths path)
        {
            if (this.character == character && canRecalculate)
            {
                canRecalculate = false;
    
                if (path.nodes.Count > 1)
                {
                    Vector3 target = path.nodes [path.nodes.Count-1];
                    StartCoroutine (MoveToPoint (target));
                }
            }
        }
    
        private void OnEndPath (Char character, Paths path)
        {
            if (this.character == character)
            {
                canRecalculate = true;
            }
        }
    
        private IEnumerator MoveToPoint (Vector3 point)
        {
            yield return new WaitForSeconds (0.1f);
            character.MoveToPoint (point, character.isRunning, true);
        }
    
    }
    
  • Ah, I missed OnCharacterSetPath event. Yes, that should do the trick!

    So, if I understand correctly, the perfect pathfinding solution would be something like this:
    1. User clicks and starts pathfinding.
    2. OnCharacterSetPath is triggered.
    3. Script cancels the path (how? character.EndPath?)
    4. Script temporarily disables the NavMeshObstacle's carving for the character.
    5. Script waits a moment (NavMeshObstacle's delay in Unity)
    6. Script calls character.MoveToPoint(previewsly_found_endpoint) to find the correct path this time
    7. Script enables NavMeshObstacle back so NPCs can avoid the player nicely

    But my question is: won't calling "character.MoveToPoint()" trigger OnCharacterSetPath and start the entire process again?

    Nevertheless it's definitely worth trying!

  • edited November 2018

    won't calling "character.MoveToPoint()" trigger OnCharacterSetPath and start the entire process again?

    Yes - hence the inclusion of the "canRecalculate" flag in there as well. The OnCharacterEndPath event is used to set this to true again when they stop - so while the event will fire again, it shouldn't have an effect.

  • Ok, it's all clear now.
    I'll try to implement and test it somewhere in the not too distant future.

Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.
Do NOT follow this link or you will be banned from the site!