Support Forum

Local Space AI Path, LocalSpaceRichAI equivalent for 2D

I just bought A* pathfinding today and I’m still getting to grips with it.

I’m working on a project in 2D and I would like to employ the equivalent of the “Moving” demo, which uses the LocalSpaceRichAI. I notice from a comment in the AIBaseEditor class that Rich AI is not compatible with 2D and is exclusively designed for XZ traversal.

I’ve therefore been trying to get an equivalent set up for AIPath i.e LocalSpaceAIPath, the documentation for LocalSpaceRichAI suggests it should be relatively straightforward to convert the logic and I basically understand what is being done behind the scenes - perform pathfinding at a graph at zero space and inverse transform the path at runtime, but for the life of me I can’t seem to get it working.

I was hoping somebody could shed some light on how the LocalSpaceRichAI actually does local space pathfinding in a bit more detail and give any pointers towards replicating that logic.

Hi

You can make a LocalSpaceAIPath by copy-pasting that script into a new one and changing the base class from RichAI to AIPath. However you need a few extensions to the AIPath script.

You need to add a GraphTransform field to the PathInterpolator class and for all Vector3 parameters that functions get you need to call transform.InverseTransform(value) and for all Vector3 values that are returned from methods and properties in that class you need to call transform.Transform(value).

Then in the LocalSpaceAIPath you need to replace the

richPath.transform = graph.transformation;

line with

interpolator.transform = graph.transformation;

I haven’t tried this myself, but I think this should work.

EDIT: I solved my issue by some more research, read my answer below how.


Hey @aron_granberg,

I hope you can help me with the same issue Jack had a couple of months back. As I read from a previous post I changed the following things:

In the get functions of position, endPoint, and tangent, I return the values with transform.inverseTransform. For example:

Instead of (tangent):

return path[segmentIndex + 1] - path[segmentIndex]

I do:

return transform.InverseTransform(path[segmentIndex + 1] - path[segmentIndex]);

Then as you discussed in the referenced forum post, I changed the position and circleCenter3D in the functions MoveToClosestPoint, MoveToLocallyClosestPoint, and MoveToCircleIntersection2D. In the beginning of the functions I write:

circleCenter3D = transform.Transform(circleCenter3D);

Sadly this still doesn’t work. It creates a weird behavior where the agent is moving back to the center of the graph.

I really hope you’re able to help me, as this is the latest piece in the puzzle. If anyone else has any helpful ideas, let me know.

(I couldn’t add more screenshots, as I’m a new user.)

Thanks a lot!

1 Like

I re-read my code and I found out that Aron said “…from graph space to world space” and then realized I was accidentally doing the opposite. It works like a charm right now!

I suggest everyone who is interested in a local space AI path to read the second forum post with this answer.

Make sure to use transform.Transform(); in the get functions of position, endPoint, and tangent. And visa versa. I set the point and circleCenter3D after checking the path like this:

point = transform.InverseTransform(point);

I also changed the transform name to graphTransform, as there’s already a transform being used in the MoveToCircleIntersection2D function.

@aron_granberg would it be possible for me to give you my code, so that you can add it to the moving example? I think it would save some people a slight headache! :smile:

3 Likes

@aron_granberg I have implemented the above as described to try to get AIPath working on a rotating spherical world (see post: Falling through spherical world sometimes and navmesh rotation) and it’s mostly working. There are a couple of issues:

  1. The AI’s jerk every time the path is recalculated.
  2. To get this to work I made copies of the following scripts (and renamed them) in the project so that I could modify them (because they are now in the package manager):

AIPath -> AIPathPapaJ
PathInterpolateor -> PathInterpolateorPapaJ
AIPathAlignedToSurface -> AIPathAlignedToSurfacePapaJ

The problem I am having is that the renamed named scripts are crashing the inspector:

ArgumentException: acceleration
Pathfinding.EditorBase.FindProperty (System.String name) (at Library/PackageCache/com.arongranberg.astar@4.3.33/PackageTools/Editor/EditorBase.cs:124)
Pathfinding.EditorBase.PropertyField (System.String propertyPath, System.String label, System.String tooltip) (at Library/PackageCache/com.arongranberg.astar@4.3.33/PackageTools/Editor/EditorBase.cs:144)
Pathfinding.EditorBase.FloatField (System.String propertyPath, System.String label, System.String tooltip, System.Single min, System.Single max) (at Library/PackageCache/com.arongranberg.astar@4.3.33/PackageTools/Editor/EditorBase.cs:134)
Pathfinding.BaseAIEditor.Inspector () (at Library/PackageCache/com.arongranberg.astar@4.3.33/Editor/AIBaseEditor.cs:84)
Pathfinding.EditorBase.OnInspectorGUI () (at Library/PackageCache/com.arongranberg.astar@4.3.33/PackageTools/Editor/EditorBase.cs:94)
UnityEngine.Debug:LogException(Exception, Object)
Pathfinding.EditorBase:OnInspectorGUI() (at Library/PackageCache/com.arongranberg.astar@4.3.33/PackageTools/Editor/EditorBase.cs:96)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

Here is what I have going on is my script:

using UnityEngine;
using System.Collections.Generic;

namespace Pathfinding
{
    /// <summary>
    /// Movement script for curved worlds.
    /// This script inherits from AIPath, but adjusts its movement plane every frame using the ground normal.
    /// </summary>
    [HelpURL("http://arongranberg.com/astar/docs/class_pathfinding_1_1_a_i_path_aligned_to_surface.php")]
    public class AIPathAlignedToSurfacePapaJ : AIPathPapaJ
    {

        /// <summary>Root of the object we are moving on</summary>
        public LocalSpaceGraph graph;

        protected override void Start()
        {
            GameObject go = GameObject.Find("Earth");
            if (go == null)
                return;
            graph = go.GetComponent<LocalSpaceGraph>();

            base.Start();
            movementPlane = new Util.SimpleMovementPlane(rotation);
        }

        protected override void OnUpdate(float dt)
        {
            base.OnUpdate(dt);
            UpdateMovementPlane();
        }

        protected override void ApplyGravity(float deltaTime)
        {
            // Apply gravity
            if (usingGravity)
            {
                // Gravity is relative to the current surface.
                // Only the normal direction is well defined however so x and z are ignored.
                verticalVelocity += float.IsNaN(gravity.x) ? Physics.gravity.y : gravity.y;
            }
            else
            {
                verticalVelocity = 0;
            }
        }

        protected override Vector3 ClampToNavmesh(Vector3 position, out bool positionChanged)
        {
            // This code clamps the AI to the sphere of radius 5.0f
            positionChanged = false;
            // Normalize position then scale to radius of sphere
            Vector3 newPosition = Vector3.Normalize(position) * 5.0f;
            if (newPosition != position) positionChanged = true; // not sure what this does
            //Debug.Log(position.ToString());
            return newPosition;
        }

        /// <summary>Find the world position of the ground below the character</summary>
        protected override void UpdateMovementPlane()
        {
            // Construct a new movement plane which has new normal
            // but is otherwise as similar to the previous plane as possible
            var normal = Vector3.Normalize(transform.position);

            var fwd = Vector3.Cross(movementPlane.rotation * Vector3.right, normal);

            if (fwd != Vector3.zero)
            {
                movementPlane = new Util.SimpleMovementPlane(Quaternion.LookRotation(fwd, normal));
                rvoController.movementPlane = movementPlane;
            }
            else
            {
                Debug.Log("APPathAlignedToSurfacePapaJ UpdateMovementPlane fwd Vector3.zero");
            }
        }
        void RefreshTransform()
        {
            graph.Refresh();
            interpolator.graphTransform = graph.transformation;
            movementPlane = graph.transformation.ToSimpleMovementPlane();
        }

        protected override void CalculatePathRequestEndpoints(out Vector3 start, out Vector3 end)
        {
            RefreshTransform();
            base.CalculatePathRequestEndpoints(out start, out end);
            start = graph.transformation.InverseTransform(start);
            end = graph.transformation.InverseTransform(end);
        }
    }
}

Any insight on where to look to fix the inspector would be helpful. I think I can figure out the jerking on my own.

Thanks,

Jacob

Resolved the inspector crashing by creating child classes based on AIPath and PathInterpolator that basically just copy all the code from the parent class. The inspector see them for what it wants it to be yet it is different.

Jacob

@aron_granberg I changed my mind, I stumped on the jerking when the path is recalculated. I am speculating that it has to do with the mesh moving and there being a disconnect between the local space and world space that is corrected on the next update.

Here is some video of the issue:

Jacob

Hi

I think the AIPath script might try to set the movement plane when the path is calculated. I think you need to override a function to prevent that from happening. I don’t have any code with me right now, but try to search the AIPath and AIBase scripts for where it sets the movement plane.

@aron_granberg Ahhh, brilliant!!! That worked. I haven’t quite wrapped my head around what the movement plane is all about or why this causes the agents to jump. Here is what I did before you pointed this out (that was mostly working):

After making the last video I noticed that if I lowered the path recalculation rate that the Navemesh wasn’t rotating with the sphere. This made me realize that the Navmesh was not being refreshed in the in the OnUpdate(). So I changed OnUpdate() to this:

        protected override void OnUpdate(float dt)
        {
            RefreshTransform();
            base.OnUpdate(dt);
            UpdateMovementPlane();
        }

But this caused all the boats orientate to y.up. So I modified it again to this:

        protected override void OnUpdate(float dt)
        {
            graph.Refresh();
            interpolator.graphTransform = graph.transformation;
            base.OnUpdate(dt);
            UpdateMovementPlane();
        }

Which worked great, and I just turned off automatic path recalculation and just called SearchPath() when setting a new waypoint. This worked great but the agents still hopped when selecting a new way point. By commenting out the updating of the movement plane in RefreshTransform() like this:

        void RefreshTransform()
        {
            graph.Refresh();
            interpolator.graphTransform = graph.transformation;
            //movementPlane = graph.transformation.ToSimpleMovementPlane();
        }

The boats no longer hop when selecting a new way point and I can probably turn path recalculation back on (if needed).

Thanks so much for your help, we are loving A* Pro, more so now that its doing what we want it to do.

Jacob

1 Like

Thanks guys! This thread was super helpful. Aron instructors were clear and @ patrickscheper notes were able to reduce the time to implement this to a matter of minutes.

Being a solo dev, I do really appreciate having such a great community of folks creating a strong knowledge base.

1 Like