Navigation on Walls - Any tips or resources?

Hi there,

I’m trying to evaluate different navigation assets and path finding to see what is suitable for AI that can climb walls and ceilings. I’ve had a look around the forums and somewhat of a quick look through the documentation and tutorials to see if there were many resources on the topic but unfortunately couldn’t find much.

What I did find is linked here:

arongranberg.com/astar/documentation/dev_4_3_41_5623d52b/spherical.html

In my prototype scene I’m using a navmesh graph and tried to base it on the spherical example scene. The AI agent is using the AI Path Aligned To Surface. I realise that the edges are probably too sharp as it’s not recommended according to the spherical documentation. However I can see that my path does get calculated but the AI does not move along the path vertically and goes to the target through the obstacle or wall. Is this a limitation of the current included movement scripts and I need to look at creating a custom movement script for navigation of vertical paths or should I be looking at trying something else. I did see the example with link nodes and such but I need this to work for quite a number of AI as a swarm. Not sure if there would be issues with link nodes to queue and I want the AI to avoid each other as best as possible.

image below shows the agent path being calculated correctly but navigation not working.

https://i.imgur.com/rzYehTL.png

Any help or point in the right direction would be greatly appreciated.

Hi

Here’s a recent thread by another user with almost the same use case: Question About Different Gravities on Any Surfaces - #4 by ryanflees

Is this a limitation of the current included movement scripts and I need to look at creating a custom movement script for navigation of vertical paths or should I be looking at trying something else. I did see the example with link nodes and such but I need this to work for quite a number of AI as a swarm. Not sure if there would be issues with link nodes to queue and I want the AI to avoid each other as best as possible.

Yes. The path will get generated correctly, but the AIPathAlignedToSurface movement script only support moving over smooth-ish surfaces. A 90 degree wall will confuse it.

Hi Aron,

Thanks for your fast reply and pointing me in the right direction! If I manage to implement agent movement behaviour with vertical and ceiling movement would you be okay with me sharing it here? I think it’s something that people will continue to ask about and I don’t mind sharing my implementation. I’m just unsure about leaking any source code if someone has not paid for a license. Otherwise I could do a write up without sharing code.

1 Like

Of course! Please share!

Just wanted to share my current progress.

Here is a video:

I’m using AIPath as the base and have overridden UpdateMovementPlane and set gravity to none. It seems there is something else that is still fighting against the alignment, is there any other method I should look at? Perhaps something in MovementInternal?

Made some more progress and managed to stop the rotation stutter from happening

Currently I’m just overriding the UpdateMovementPlane but it still needs some massaging in some places. I’m going to take a look at MovementInternal next which I think will help reduce any weird inconsistencies with the movement transitions up walls etc.

I have something working, might not be the best solution but it seems work a bit better than what I had before. The only issue I’m running into is that if I place a destination that is at a similar height to where the AI agent is it won’t go along the path. You can see it in this video below. Can you think of anything that might cause this? I’ve linked the script for reference. I use the same setting in the inspector that you can see in the video.

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>
	public class AIWallCrawler : AIPath {
        
        public float smoothness = 5f;
        public int raysNb = 8;
        public float raysEccentricity = 0.2f;
        public float outerRaysOffset = 2f;
        public float innerRaysOffset = 25f;

        private Vector3 vel;
        private Vector3 lastVelocity;
        private Vector3 lastPosition;
        private Vector3 forward;
        private Vector3 upward;
        private Quaternion lastRot;
        private Vector3[] pn;

		protected override void Start () {
			base.Start();
			//movementPlane = new Util.SimpleMovementPlane(rotation);

            vel = new Vector3();
            forward = transform.forward;
            upward = transform.up;
            lastRot = transform.rotation;
		}

		protected override void OnUpdate (float dt) {
			base.OnUpdate(dt);
			UpdateMovementPlane();
        }
        static Vector3[] GetClosestPoint(Vector3 point, Vector3 forward, Vector3 up, float halfRange, float eccentricity, float offset1, float offset2, int rayAmount)
        {
            Vector3[] res = new Vector3[2] { point, up };
            Vector3 right = Vector3.Cross(up, forward);
            float normalAmount = 1f;
            float positionAmount = 1f;

            Vector3[] dirs = new Vector3[rayAmount];
            float angularStep = 2f * Mathf.PI / (float)rayAmount;
            float currentAngle = angularStep / 2f;
            for(int i = 0; i < rayAmount; ++i)
            {
                dirs[i] = -up + (right * Mathf.Cos(currentAngle) + forward * Mathf.Sin(currentAngle)) * eccentricity;
                currentAngle += angularStep;
            }

            foreach (Vector3 dir in dirs)
            {
                RaycastHit hit;
                Vector3 largener = Vector3.ProjectOnPlane(dir, up);
                Ray ray = new Ray(point - (dir + largener) * halfRange + largener.normalized * offset1 / 100f, dir);
                Debug.DrawRay(ray.origin, ray.direction);
                if (Physics.SphereCast(ray, 0.01f, out hit, 2f * halfRange))
                {
                    res[0] += hit.point;
                    res[1] += hit.normal;
                    normalAmount += 1;
                    positionAmount += 1;
                }
                ray = new Ray(point - (dir + largener) * halfRange + largener.normalized * offset2 / 100f, dir);
                Debug.DrawRay(ray.origin, ray.direction, Color.green);
                if (Physics.SphereCast(ray, 0.01f, out hit, 2f * halfRange))
                {
                    res[0] += hit.point;
                    res[1] += hit.normal;
                    normalAmount   += 1;
                    positionAmount += 1;
                }
            }
            res[0] /= positionAmount;
            res[1] /= normalAmount;
            return res;
        }


		/// <summary>Find the world position of the ground below the character</summary>
		protected override void UpdateMovementPlane () {
            vel = (smoothness * velocity + (transform.position - lastPosition)) / (1f + smoothness);
            if (vel.magnitude < 0.00025f)
                vel = lastVelocity;
            lastPosition = transform.position;
            lastVelocity = vel;
            
            if (!reachedDestination)
            {
                pn = GetClosestPoint(transform.position, transform.forward, transform.up, 0.5f, 0.1f, 30, -30, 4);
                upward = pn[1];
                Vector3[] pos = GetClosestPoint(transform.position, transform.forward, transform.up, 0.5f, raysEccentricity, innerRaysOffset, outerRaysOffset, raysNb);
                transform.position = Vector3.Lerp(lastPosition, pos[0], 1f / (1f + smoothness));
                forward = vel.normalized;
                Quaternion q = Quaternion.LookRotation(forward, upward);
                transform.rotation = Quaternion.Lerp(lastRot, q, 1f / (1f + smoothness));
                lastRot = transform.rotation;
                movementPlane = new Util.SimpleMovementPlane(lastRot);
                if (rvoController != null) rvoController.movementPlane = movementPlane;
            }
		}
	}
}

1 Like

Hmm, it’s hard to tell. It looks like it is hitting some trigonometric function wraparound (i.e. going from 0 to 360 degrees instantly), but I cannot see anything that would cause that in your code.

It’s been a couple of days since I last worked on this challenge. Still can’t quite figure out what causes it to not want to travel to the new destination when it’s at the same or similar height. I’ve been considering just starting with AIBase and pulling some things from AIPath and see what happens. Hoping someone smarter than me might come along and work it out lol. If I get anywhere I’ll report back with the solution.

1 Like

Sorry, it’s been a hot minute since I posted some updates but I’ve been busy with work/life and decided to work on some other things in my project but came back to this problem recently.

I modified the script to be much simpler using two spherecasts which works much better than using eccentricity, there is a little bit of improvement that can be done using this to interpolate between the two normals but works fine in testing for now. I have some basic custom gravity setup as well just to keep the agent close to the ground but within some distance.

However I’m still running into that issue where if the target is at a similar height the agent gets stuck recalculating the path and will loop up and down. Any suggestions as to why this might happen?

Here is the new code for anyone interested.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Pathfinding;
using Raycasting;

public partial class WallCrawler : AIPath
{
    [Header("Grounding")]
    public CapsuleCollider capsuleCollider;
    [Range(1, 10)]
    public float gravityMultiplier;
    public LayerMask walkableLayer;

    public float gravityOffDistance;

    [Header("Ray Adjustments")]
    [Range(0.0f, 1.0f)]
    public float forwardRayLength;
    [Range(0.0f, 1.0f)]
    public float downRayLength;
    [Range(0.1f, 1.0f)]
    public float forwardRaySize = 0.66f;
    [Range(0.1f, 1.0f)]
    public float downRaySize = 0.9f;
    
    public float downRayRadius;
    public float forwardRayRadius;

    private Vector3 GroundNormal;

    private SphereCast downRay, forwardRay;
    private RaycastHit hitInfo;



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

    protected override void UpdateMovementPlane () {
        if (Physics.SphereCast(transform.TransformPoint(capsuleCollider.center), downRayRadius, -transform.up, out hitInfo, downRayLength, walkableLayer))
        {
            Debug.Log("Bottom Hit");
            GroundNormal = hitInfo.normal.normalized;
        }

        if (Physics.SphereCast(transform.TransformPoint(capsuleCollider.center), forwardRayRadius, transform.forward, out hitInfo, forwardRayLength, walkableLayer))
        {
            GroundNormal = hitInfo.normal.normalized;
        }

        if (GroundNormal != Vector3.zero)
        {
            var fwd = Vector3.Cross(movementPlane.rotation * Vector3.right, GroundNormal);
            movementPlane = new Pathfinding.Util.SimpleMovementPlane(Quaternion.LookRotation(fwd, GroundNormal));
        }

    }

    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.
            if (Physics.Raycast(transform.TransformPoint(capsuleCollider.center), -transform.up, out hitInfo, Mathf.Infinity, walkableLayer))
            {
                if (hitInfo.distance > gravityOffDistance)
                {
                    Debug.Log("Threshold on Gravity Distance Met");
                    verticalVelocity -= gravityMultiplier;
                }
            }
        } else {
            verticalVelocity = 0;
        }
    }
}

1 Like

Think I’ve actually fixed the issue by setting the Pick Next Waypoint Dist to a much lower value. I guess next is to just polish and smooth out the rotations, probably implement a better gravity method.

3 Likes

Thanks man, a very useful and simple script;
I took it as a basis for myself, hope you don’t mind.

I’ve cleaned it up a bit from unnecessary legacy code and inactive variables.
I’ll post it here; maybe someone will find it useful.

using UnityEngine;

namespace Pathfinding {
    public class WallCrawler : AIPathAlignedToSurface {
        [Header("Grounding")]
        [SerializeField]
        private CapsuleCollider capsuleCollider;
        [SerializeField]
        private LayerMask walkableLayer;

        [Header("Ray Adjustments")]
        [Range(0.0f, 10.0f)]
        [SerializeField]
        private float forwardRayLength;
        [Range(0.0f, 10.0f)]
        [SerializeField]
        private float forwardRayRadius;
        [Range(0.0f, 10.0f)]
        [SerializeField]
        private float downRayLength;
        [Range(0.0f, 10.0f)]
        [SerializeField]
        private float downRayRadius;

        private Vector3 GroundNormal;
        private RaycastHit HitInfo;
        
        protected override void OnUpdate(float dt) {
            base.OnUpdate(dt);
            UpdateMovementPlane();
        }

        protected override void UpdateMovementPlane() {
            if (Physics.SphereCast(transform.TransformPoint(capsuleCollider.center), downRayRadius, -transform.up, out HitInfo, downRayLength, walkableLayer))
            {
                Debug.Log("Bottom Hit");
                GroundNormal = HitInfo.normal.normalized;
            }

            if (Physics.SphereCast(transform.TransformPoint(capsuleCollider.center), forwardRayRadius, transform.forward, out HitInfo, forwardRayLength, walkableLayer))
            {
                Debug.Log("Forward Hit");
                GroundNormal = HitInfo.normal.normalized;
            }

            if (GroundNormal != Vector3.zero)
            {
                var fwd = Vector3.Cross(movementPlane.rotation * Vector3.right, GroundNormal);
                movementPlane = new Pathfinding.Util.SimpleMovementPlane(Quaternion.LookRotation(fwd, GroundNormal));
            }
        }
    }
}