A* Pathfinding Project

Incorporate 2D Local Avoidance into custom movement?


#1

Hi there!

I’m trying to mix in local avoidance into my own custom move logic. Is this possible?

Currently this is what I have:

private void Update()
	{
		Move();
	}

	private void Move()
	{
		if (recalculatePathTick > 0.0f)
		{
			internalTimer -= Time.deltaTime;

			if (internalTimer <= 0.0f)
			{
				internalTimer = recalculatePathTick;
				CalculatePath();
			}
		}

		if (currentPath != null && currentPath.CompleteState == PathCompleteState.Complete)
		{
			if (currentWaypoint >= currentPath.vectorPath.Count || TargetInsideRadius(target.position, stoppingDistance))
			{
				currentDirection = Vector3.zero;
				return;
			}

			currentDirection = (currentPath.vectorPath[currentWaypoint] - transform.position).normalized;

			if (TargetInsideRadius(currentPath.vectorPath[currentWaypoint], nextWaypointDistance))
			{
				currentWaypoint++;
			}
		}

		var moveDelta = rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
		transform.position += currentDirection.normalized * speed * Time.deltaTime;
	}

	private void CalculatePath()
	{
		pathSeeker.StartPath(transform.position, target.position, OnPathCalculated);
	}

	private void OnPathCalculated(Path newPath)
	{
		ABPath p = newPath as ABPath;

		if (p == null || p.error)
		{
			return;
		}

		currentPath?.Release(this);

		currentPath = p;
		currentPath.Claim(this);

		currentWaypoint = 0;
		rvoController.SetTarget(target.position, speed, maxSpeed);
	}

	public bool TargetInsideRadius(Vector2 targetPosition, float range)
	{
		return ((Vector3)targetPosition - transform.position).sqrMagnitude <= range * range;
	}

But I’m unsure how to use that delta value from the RVOController correctly?
I can see from the gizmos in the editor that it is calculating the avoidance correctly, but currently ignoring it (because I’m not using the delta).

Any help is greatly appreciated!

Thanks.


#2

Hi

During the Update function you’ll want to do this:

rvoController.SetTarget(currentPath.vectorPath[currentWaypoint], speed, maxSpeed);
var moveDelta = rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
transform.position += moveDelta;

#3

Hey thanks, I tried your solution but it seems to ignore the avoidance all together.

Code now looks like this in Update:

        if (currentPath != null)
        {
            rvoController.SetTarget(currentPath.vectorPath[currentWaypoint], speed, maxSpeed);
        }

        var moveDelta = rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
        transform.position += moveDelta;

#4

Hi

And you are sure you do not have your previous movement code still there?
Where are the yellow debug lines coming from?


#5

Hey!

You can see the full script here:

using Pathfinding;
using Pathfinding.RVO;
using System;
using UnityEngine;

public enum Orientation
{
    Right = 0,
    Up = 1,
    Down = 2,
    Left = 3,
}

[RequireComponent(typeof(RVOController))]
public class AIMovementLocalAvoidance : MonoBehaviour
{
    [SerializeField]
    private Transform target;

    [SerializeField]
    private float speed = 10;

    [SerializeField]
    private float maxSpeed = 12;

    [SerializeField]
    private float recalculatePathTick = 0.5f;

    [SerializeField]
    private float nextWaypointDistance = 0.25f;

    [SerializeField]
    private float stoppingDistance = 0.5f;

    private Seeker pathSeeker;
    private ABPath currentPath;

    private int currentWaypoint;
    private float internalTimer;

    private Vector3 currentDirection;
    private Vector3 velocity;

    private Animator anim;
    private RVOController rvoController;
    private Orientation currentOrientation;

    private void Awake()
    {
        pathSeeker = GetComponent<Seeker>();
        rvoController = GetComponent<RVOController>();

        anim = GetComponentInChildren<Animator>();
    }

    private void Start()
    {
        CalculatePath();

        if (recalculatePathTick > 0.0f)
        {
            internalTimer = recalculatePathTick;
        }
    }

    private void Update()
    {
        Move();
        HandleAnimation(currentDirection);
        //Debug.DrawRay(transform.position, currentDirection.normalized, Color.white);
    }

    private void Move()
    {
        if (recalculatePathTick > 0.0f)
        {
            internalTimer -= Time.deltaTime;

            if (internalTimer <= 0.0f)
            {
                internalTimer = recalculatePathTick;
                CalculatePath();
            }
        }

        if (currentPath != null && currentPath.CompleteState == PathCompleteState.Complete)
        {
            if (currentWaypoint >= currentPath.vectorPath.Count || TargetInsideRadius(target.position, stoppingDistance))
            {
                currentDirection = Vector3.zero;
                return;
            }

            currentDirection = (currentPath.vectorPath[currentWaypoint] - transform.position).normalized;

            if (TargetInsideRadius(currentPath.vectorPath[currentWaypoint], nextWaypointDistance))
            {
                currentWaypoint++;
            }
        }

        if (currentPath != null)
        {
            rvoController.SetTarget(currentPath.vectorPath[currentWaypoint], speed, maxSpeed);
        }

        var moveDelta = rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
        transform.position += moveDelta;

        Debug.DrawRay(transform.position, moveDelta.normalized, Color.yellow);

        //currentDirection = ((transform.position + moveDelta) - transform.position).normalized;
        //transform.position = transform.position + moveDelta;

        //var currentPosition = transform.position + moveDelta;
        //currentPosition = ClampToNavMesh(currentPosition, out bool posChanged);
        //transform.position += moveDelta;
    }

    private Vector2 ClampToNavMesh(Vector3 position, out bool positionChanged)
    {
        NNConstraint constrain = NNConstraint.Default;

        constrain.tags = pathSeeker.traversableTags;
        constrain.graphMask = pathSeeker.graphMask;
        constrain.distanceXZ = true;

        var clampedPosition = AstarPath.active.GetNearest(position, constrain).position;

        var difference = clampedPosition - position;
        float sqrDifference = difference.sqrMagnitude;

        if (sqrDifference > 0.001f * 0.001f)
        {
            rvoController.SetCollisionNormal(difference);
            positionChanged = true;
        }

        positionChanged = false;
        return position + difference;
    }

    private void CalculatePath()
    {
        pathSeeker.StartPath(transform.position, target.position, OnPathCalculated);
    }

    private void OnPathCalculated(Path newPath)
    {
        ABPath p = newPath as ABPath;

        if (p == null || p.error)
        {
            return;
        }

        currentPath?.Release(this);

        currentPath = p;
        currentPath.Claim(this);

        currentWaypoint = 0;

        //Check to see wether the enemy agent is already ahead of the new path, when it has been calculated. If so, increment to the next waypoint.
        //Tries to remove jitter, when a path is recalculated.
        for (int i = 0; i < currentPath.vectorPath.Count; i++)
        {
            if (TargetInsideRadius(currentPath.vectorPath[i], nextWaypointDistance))
            {
                currentWaypoint += i + 1;
                break;
            }
        }
    }

    private void HandleAnimation(Vector3 velocity)
    {
        Orientation rotation = GetOrientation(velocity.normalized);

        if (velocity.magnitude > 0.2f)
        {
            anim.Play("Move");
        }
        else
        {
            anim.Play("Idle");
        }

        anim.SetFloat("Facing", (int)rotation);
    }

    public Orientation GetOrientation(Vector2 direction)
    {
        if (direction.x == 0.0f && direction.y == 0.0f)
        {
            //Return old direction
            return currentOrientation;
        }

        if (Mathf.Abs(direction.y) > Mathf.Abs(direction.x))
        {
            float dot = Vector2.Dot(Vector2.up, direction.normalized);
            currentOrientation = dot > 0 ? Orientation.Up : Orientation.Down;
        }
        else
        {
            currentOrientation = direction.x > 0 ? Orientation.Right : Orientation.Left;
        }

        return currentOrientation;
    }

    public bool TargetInsideRadius(Vector2 targetPosition, float range)
    {
        return ((Vector3)targetPosition - transform.position).sqrMagnitude <= range * range;
    }
}

#6

Hi

Is your agent time horizon high enough?
Your script works well for me with agent time horizon = 0.6.


#7

Hmm, interesting.

I tried playing around with those settings (default was 2) and the radius. It seems I can get something looking okay nice, with low values such as:
Radius: 0.2
Time Horizon: 0.6

But if I change their radius, I can’t find a Time Horizon which works properly for some reason. It goes back to just sliding into each other.


#8

First off, the only reason why I’m doing this custom movement myself and not using the built-in components such as AIPath, is because I want to
A: Extract the current direction of the pathfinding (with local avoidance), to then use as “fake input” for my AIs, so basically feed the direction as if it was a thumbstick input device.
B: I want snappy movement. Our game is a 2D topdown pixel art game, so we don’t want acceleration and deceleration. We want constant movement. But I can’t seem to achieve that with the built-in components. It often leads to sliding around corners etc.

Do you see any other way around this?


#9

Haha, sorry for the tripple post but I actually managed to get it working (with a hack).

I tried using your components (AIPath and RVOController) to extract the direction from the RVOController.velocity. It gave me the correct direction I needed for the fake input.
But I had to go into AIBase and comment out code wherever you actually set the transform or rigidbody position.

So with that, I still get the path and avoidance calculations, but not the actual movement.
Now this works, but feels very hacky and it will break whenever we update the asset.
Is there any way to get your components to do the actual calculations, without applying the movement itself?


#10

Hello! I just have a task to do so that the bots can bypass each other and not get to the same position together. I looked - your topic is what I need. Unfortunately, I have problems understanding the code from the example in the asset and documentation. If this is possible, maybe you could make some empty project with the implementation of this and give me a look? I just have the same problem with the fact that I want to control the character from my own script.


#11

I can’t really send you a project but I can tell you what I did and show you my script.

First off, it is a hack so I wouldn’t really recommend it but… Go into AIBase.cs and comment out the lines: 627, 628, 629. This makes sure that the AIPath script won’t actually set the position of the agent.

And then I’m using the following script to extract the velocity as a direction, and creating a fake input for it.

using Pathfinding;
using Pathfinding.RVO;
using UnityEngine;

public class AIPathExtractor : MonoBehaviour
{
    [SerializeField]
    private float speed = 2.0f;

    [SerializeField]
    private float animationTick = 0.1f;

    [SerializeField]
    private float inputSmoothing = 8.0f;

    private float internalTimer;

    private RVOController rvoController;
    private AIPath aiPathMover;
    private Animator anim;

    private Vector2 input;

    private void Awake()
    {
        rvoController = GetComponent<RVOController>();
        aiPathMover = GetComponent<AIPath>();
        anim = GetComponentInChildren<Animator>();
    }

    private void LateUpdate()
    {
        if (aiPathMover.reachedDestination || aiPathMover.reachedEndOfPath)
        {
            input = Vector2.zero;
            UpdateAnimations(input);
            return;
        }

        UpdateInput();
        UpdateMovement();

        if (Time.time >= internalTimer)
        {
            internalTimer = Time.time + animationTick;
            UpdateAnimations(input);
        }
    }

    private void UpdateInput()
    {
        input = Vector2.Lerp(input, rvoController.velocity.normalized, Time.deltaTime * inputSmoothing);
        Debug.DrawRay(transform.position, input, Color.red);
    }

    private void UpdateMovement()
    {
        transform.position += (Vector3)input.normalized * speed * Time.deltaTime;
    }

    private void UpdateAnimations(Vector3 velocity)
    {
        Orientation rotation = GetOrientation(velocity.normalized);

        if (velocity.magnitude > 0.2f)
        {
            anim.Play("Move");
        }
        else
        {
            anim.Play("Idle");
        }

        anim.SetFloat("Facing", (int)rotation);
    }

    public Orientation GetOrientation(Vector2 direction)
    {
        Orientation orientation = Orientation.Down;

        if (direction.x == 0.0f && direction.y == 0.0f)
        {
            //Return old direction
            return orientation;
        }

        if (Mathf.Abs(direction.y) > Mathf.Abs(direction.x))
        {
            float dot = Vector2.Dot(Vector2.up, direction.normalized);
            orientation = dot > 0 ? Orientation.Up : Orientation.Down;
        }
        else
        {
            orientation = direction.x > 0 ? Orientation.Right : Orientation.Left;
        }

        return orientation;
    }
}

Hope this helps!


#12

You can set ai.updatePosition = false and ai.updateRotation = false to make the AI not move the transform itself.

See https://arongranberg.com/astar/docs/aibase.html#updatePosition

Alternatively you could call ai.MovementUpdate manually.
See https://arongranberg.com/astar/docs/aibase.html#MovementUpdate


#13

Just what I was looking for! Perfect. Thank you Aron.


#14

Thank you, this is useful to me!