Trouble getting RVO to avoid collision between agents efficiently

Hi!

I am integrating both your A* algorithm with a Grid navigation system and RVO together with a custom character controller (see https://assetstore.unity.com/packages/tools/physics/kinematic-character-controller-99131).

I have been through all the tutorials, the examples, looked into the AIPath/AIBase code base, and checked about 20 forum threads or so, but haven’t been able to completely eliminate my issue.

Here’s the breakdown of the algorithm:

1 - The NPCs request from the Seeker the path to reach their destination (eg. a player they want to attack).
2 - The result is turned into an input direction (normalized) and fed into the kinematic character controller. (this work fine)
3 - When close to their target (a few meters away), the A* directions are superseded by local navigation which simply takes a straight line towards the player. This is to avoid glitchy movement when close to the target destination.
4 - The kinematic character controller calculates the actual velocity (this works fine as well).
5 - The velocity is fed into the RVO Controller.
6 - The RVO’s calculated velocity is injected back into the character controller.

The characters are moving but the local avoidance quality is extremely poor. Characters bump into each other instead and try to walk through each other instead of walking around.

The agents are all using physics with a capsule collider, but no rigid body (the kinematic character controller replaces it). Physics is disabled between NPCs as suggested in the tutorials.

Here’s a video exhibiting the behaviour. I had a little trouble aligning the two goblins because the inputs do not match my keyboard layout, but you can see that they are having trouble walking around each other. Sometimes they’ll just stop moving altogether when another agent is placed right in-between themselves and the player.

Here are the relevant parts of code:

The RVO calculation (step 4 and 5)

    protected override Vector3 VerifyVelocity(Vector3 velocity, float deltaTime)
    {
      _rvoController.SetTarget(_destination, velocity.magnitude, MaxStableMoveSpeed);

      _ = _rvoController.CalculateMovementDelta(motor.TransientPosition, deltaTime);

      var newVelocity = _rvoController.velocity;
      newVelocity.y = velocity.y;

      return newVelocity;
    }

The A* path calculation (step 1 and 2)

    protected void MoveTo(Vector3 position, float minDistance)
    {
      _seeker.StartPath(transform.position, position, OnPathComplete);
      _destination = position;
      _minDistance = minDistance;
    }

    protected abstract Vector3 UpdateDestination();

    protected override void Update()
    {
      base.Update();
      
      // We have no path to follow yet, so don't do anything
      if (_path == null)
        return;
      
      var targetDistance = Vector3.Distance(_destination, motor.TransientPosition);

      if (targetDistance < _minDistance + 0.05)
      {
        OnDestinationReached();
        _path = null;

        return;
      }
      
      if (Time.time > _lastRepath + repathRate && _seeker.IsDone())
      {
        _lastRepath  = Time.time;
        _destination = UpdateDestination();

        // Start a new path to the targetPosition, call the the OnPathComplete function
        // when the path has been calculated (which may take a few frames depending on the complexity)
        MoveTo(_destination, _minDistance);
      
        // We have no path to follow, so don't do anything
        if (_path == null)
          return;
      }

      // Check in a loop if we are close enough to the current waypoint to switch to the next one.
      // We do this in a loop because many waypoints might be close to each other and we may reach
      // several of them in the same frame.

      // The distance to the next waypoint in the path
      float distanceToWaypoint;
      bool  reachedEndOfPath = false;

      while (true)
      {
        // If you want maximum performance you can check the squared distance instead to get rid of a
        // square root calculation. But that is outside the scope of this tutorial.
        distanceToWaypoint = Vector3.Distance(transform.position, _path.vectorPath[_currentWaypoint]);

        if (distanceToWaypoint < nextWaypointSeekMinDistance)
        {
          // Check if there is another waypoint or if we have reached the end of the path
          if (_currentWaypoint + 1 < _path.vectorPath.Count)
          {
            _currentWaypoint++;
          }
          else
          {
            // Set a status variable to indicate that the agent has reached the end of the path.
            // You can use this to trigger some special code if your game requires that.
            reachedEndOfPath = true;
            break;
          }
        }
        else
        {
          break;
        }
      }

      // Slow down smoothly upon approaching the end of the path
      // This value will smoothly go from 1 to 0 as the agent approaches the last waypoint in the path.
      float speedFactor = 1;

      if (targetDistance < _minDistance + nextWaypointSeekMinDistance)
        speedFactor = Mathf.Min(1, Mathf.Sqrt((targetDistance - _minDistance) / nextWaypointSeekMinDistance) + .3f);
      
      // var speedFactor    = reachedEndOfPath ? Mathf.Sqrt(distanceToWaypoint / nextWaypointSeekMinDistance) : 1f;

      // Direction to the next waypoint
      // Normalize it so that it has a length of 1 world unit
      var lookDir = (_path.vectorPath[_currentWaypoint] - motor.TransientPosition).normalized;
      var moveDir = lookDir;

      moveDir.y = 0;

      _inputs.LookVector = GetLookAt(lookDir);
      _inputs.MoveVector = moveDir * speedFactor;

      SetInputs(ref _inputs);

      if (pathDebugObject != null)
        pathDebugObject.transform.position = _path.vectorPath[_currentWaypoint];

      // Switch to local navigation
      if (reachedEndOfPath && targetDistance > _minDistance + 0.05)
      {
        moveDir            = (_destination - motor.TransientPosition).normalized;
        _inputs.MoveVector = moveDir * speedFactor;
        
        SetInputs(ref _inputs);
      }
    }

Any help appreciated!

Thanks,
Alexis