Need some guidance on combining AIPathAlignedToSurface and LocalSpaceRichAI

Hello, I recently purchased the asset in hopes of getting my spherical worlds with the type of pathfinding I want. Out of the box its very close to being there, but the two components I need are on different AI scripts and I’m having difficulty understanding how to combine them.

I have globe worlds that will be moving through space, and the characters need to be correctly oriented on the surface, as well as have the graph update as it moves through space.

AIPathAlignedToSurface aligns correctly, and LocalSpaceRichAI applies correctly as well, separately. Reading through the documentation it seems like the one that should be my base is the RichAI one, as I’m using navmeshes. So I need to put the AIPathAlignedToSurface inside the LocalSpaceRichAI. I managed to do that easily enough, but one method clearly overwrites the other, whichever is called last on the OnUpdate method.

My assumption is that I have to make the UpdateMovementPlane method work with the RefreshTransform one, rather than overwrite it? I’m just now sure how to do that.

I assume the change would be in this line. Any help would be greatly appreciated.

Hi

The RichAI will not easy work on a spherical surface. There’s a lot of internal logic that assumes a XZ movement plane.
However, you might have some luck with the AIPath script instead. Here’s a thread which you might find interesting: Local Space AI Path, LocalSpaceRichAI equivalent for 2D - #9 by Jacob_Christ

Ah, thanks so much! I did find various threads talking about this but at the time I couldn’t find a post that was at the tail ends of discussion. I followed that whole thread, updated the PathInterpolator class with the GraphTransform field and the modifications to the Vector3 parameters and gets. I looked at the code paste in that thread with the class AIPathAlignedToSurfacePapaJ, but I had to further modify it to get it to work. That individual’s code took out the InterpolateNormal method from the UpdateMovementPlane() override, I put that back in. And there was an override for ClampToNavmesh which I took out.

It works absolutely perfectly! Perfectly smooth movement toward the target destination while allowing me to move and rotate the globe its on.

Here is the final code for the class for anyone in the future:

	public class AIPathLocalSpaceAlignedToSurface : AIPath {
        /// <summary>Root of the object we are moving on</summary>
        public LocalSpaceGraph graph;

        protected override void Start()
        {
            graph = GetComponentInParent<LocalSpaceGraph>();

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

        protected override void OnUpdate(float dt)
        {
            graph.Refresh();
            interpolator.graphTransform = graph.transformation;
            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;
            }
        }

        /// <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 = InterpolateNormal(lastRaycastHit);

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

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

        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);
        }

        Mesh cachedMesh;
        List<Vector3> cachedNormals = new List<Vector3>();
        List<int> cachedTriangles = new List<int>();
        Vector3 InterpolateNormal(RaycastHit hit)
        {
            MeshCollider meshCollider = hit.collider as MeshCollider;

            if (meshCollider == null || meshCollider.sharedMesh == null)
                return hit.normal;

            Mesh mesh = meshCollider.sharedMesh;

            // For performance, cache the triangles and normals from the last frame
            if (mesh != cachedMesh)
            {
                if (!mesh.isReadable) return hit.normal;
                cachedMesh = mesh;
                mesh.GetNormals(cachedNormals);
                if (mesh.subMeshCount == 1)
                {
                    mesh.GetTriangles(cachedTriangles, 0);
                }
                else
                {
                    List<int> buffer = Pathfinding.Util.ListPool<int>.Claim();
                    // Absolutely horrible, but there doesn't seem to be another way to do this without allocating a ton of memory each time
                    for (int i = 0; i < mesh.subMeshCount; i++)
                    {
                        mesh.GetTriangles(buffer, i);
                        cachedTriangles.AddRange(buffer);
                    }
                    Pathfinding.Util.ListPool<int>.Release(ref buffer);
                }
            }

            var normals = cachedNormals;
            var triangles = cachedTriangles;
            Vector3 n0 = normals[triangles[hit.triangleIndex * 3 + 0]];
            Vector3 n1 = normals[triangles[hit.triangleIndex * 3 + 1]];
            Vector3 n2 = normals[triangles[hit.triangleIndex * 3 + 2]];
            Vector3 baryCenter = hit.barycentricCoordinate;
            Vector3 interpolatedNormal = n0 * baryCenter.x + n1 * baryCenter.y + n2 * baryCenter.z;
            interpolatedNormal = interpolatedNormal.normalized;
            Transform hitTransform = hit.collider.transform;
            interpolatedNormal = hitTransform.TransformDirection(interpolatedNormal);
            return interpolatedNormal;
        }
    }
2 Likes