Creating my own path

Is there a way of creating a path with my own world positions?
I have an array of vector 3 and simulates a zigzag movement , and I want that the path will move across them

Hi songerk,

just did this myself the past week. Yes, there is. You can use the AIPath and AIBase as inspiration. I am doing this for a 2D game so I altered the implementation in my “derived” (not in terms of inheritance) class quite a lot. But maybe this can help you out as a starting point. I have two classed NavigationAction (this is the integration point with my BT implementation) and a derived class FollowPath that does exactly what you are asking for (following a set of self-defined positions in the world).

NavigationAction

Code
public abstract class NavigationAction : Action
    {
        protected Path Path { get; set; }
        private PathInterpolator Interpolator { get; set; } = new PathInterpolator();
        private Seeker Seeker;
        /// <summary>Only when the previous path has been calculated should the script consider searching for a new path</summary>
		private bool WaitingForPathCalculation { get; set; } = false;

        /// <summary>
		/// Called when the agent recalculates its path.
		/// This is called both for automatic path recalculations (see <see cref="canSearch)"/> and manual ones (see <see cref="SearchPath)"/>.
		///
		/// See: Take a look at the <see cref="Pathfinding.AIDestinationSetter"/> source code for an example of how it can be used.
		/// </summary>
        protected System.Action OnSearchPath { get; set; }

        protected PathCalculationDefinition PathCalculationDefinition { get; set; }

        /// <summary>
		/// True if the ai has reached the <see cref="destination"/>.
		/// This is a best effort calculation to see if the <see cref="destination"/> has been reached.
		/// For the AIPath/RichAI scripts, this is when the character is within <see cref="AIPath.endReachedDistance"/> world units from the <see cref="destination"/>.
		/// For the AILerp script it is when the character is at the destination (±a very small margin).
		///
		/// This value will be updated immediately when the <see cref="destination"/> is changed (in contrast to <see cref="reachedEndOfPath)"/>, however since path requests are asynchronous
		/// it will use an approximation until it sees the real path result. What this property does is to check the distance to the end of the current path, and add to that the distance
		/// from the end of the path to the <see cref="destination"/> (i.e. is assumes it is possible to move in a straight line between the end of the current path to the destination) and then checks if that total
		/// distance is less than <see cref="endReachedDistance"/>. This property is therefore only a best effort, but it will work well for almost all use cases.
		///
		/// Furthermore it will not report that the destination is reached if the destination is above the head of the character or more than half the <see cref="height"/> of the character below its feet
		/// (so if you have a multilevel building, it is important that you configure the <see cref="height"/> of the character correctly).
		///
		/// The cases which could be problematic are if an agent is standing next to a very thin wall and the destination suddenly changes to the other side of that thin wall.
		/// During the time that it takes for the path to be calculated the agent may see itself as alredy having reached the destination because the destination only moved a very small distance (the wall was thin),
		/// even though it may actually be quite a long way around the wall to the other side.
		///
		/// In contrast to <see cref="reachedEndOfPath"/>, this property is immediately updated when the <see cref="destination"/> is changed.
		///
		/// <code>
		/// IEnumerator Start () {
		///     ai.destination = somePoint;
		///     // Start to search for a path to the destination immediately
		///     ai.SearchPath();
		///     // Wait until the agent has reached the destination
		///     while (!ai.reachedDestination) {
		///         yield return null;
		///     }
		///     // The agent has reached the destination now
		/// }
		/// </code>
		///
		/// See: <see cref="AIPath.endReachedDistance"/>
		/// See: <see cref="remainingDistance"/>
		/// See: <see cref="reachedEndOfPath"/>
		/// </summary>
        public bool ReachedDestination
        {
            get
            {
                if (!this.ReachedEndOfPath)
                {
                    return false;
                }
                if (!this.Interpolator.valid || this.RemainingDistance + (this.Destination - (Vector2)this.Interpolator.endPoint).magnitude > this._switchOverDistance)
                {
                    return false;
                }

                return true;
            }
        }

        /// <summary>
		/// Position in the world that this agent should move to.
		///
		/// If no destination has been set yet, then (+infinity, +infinity, +infinity) will be returned.
		///
		/// Note that setting this property does not immediately cause the agent to recalculate its path.
		/// So it may take some time before the agent starts to move towards this point.
		/// Most movement scripts have a repathRate field which indicates how often the agent looks
		/// for a new path. You can also call the <see cref="SearchPath"/> method to immediately
		/// start to search for a new path. Paths are calculated asynchronously so when an agent starts to
		/// search for path it may take a few frames (usually 1 or 2) until the result is available.
		/// During this time the <see cref="pathPending"/> property will return true.
		///
		/// If you are setting a destination and then want to know when the agent has reached that destination
		/// then you could either use <see cref="reachedDestination"/> (recommended) or check both <see cref="pathPending"/> and <see cref="reachedEndOfPath"/>.
		/// Check the documentation for the respective fields to learn about their differences.
		///
		/// <code>
		/// IEnumerator Start () {
		///     ai.destination = somePoint;
		///     // Start to search for a path to the destination immediately
		///     ai.SearchPath();
		///     // Wait until the agent has reached the destination
		///     while (!ai.reachedDestination) {
		///         yield return null;
		///     }
		///     // The agent has reached the destination now
		/// }
		/// </code>
		/// <code>
		/// IEnumerator Start () {
		///     ai.destination = somePoint;
		///     // Start to search for a path to the destination immediately
		///     // Note that the result may not become available until after a few frames
		///     // ai.pathPending will be true while the path is being calculated
		///     ai.SearchPath();
		///     // Wait until we know for sure that the agent has calculated a path to the destination we set above
		///     while (ai.pathPending || !ai.reachedEndOfPath) {
		///         yield return null;
		///     }
		///     // The agent has reached the destination now
		/// }
		/// </code>
		/// </summary>
		protected Vector2 Destination { get; set; }

        /// <summary>
		/// Current desired velocity of the agent (does not include local avoidance and physics).
		/// Lies in the movement plane.
		/// </summary>
		private Vector2 Velocity2D;

        public CloseToDestinationMode WhenCloseToDestinationMode { get; set; } = CloseToDestinationMode.Stop;

        private float _switchOverDistance = 0f;
        private float _lookAheadDistance = 1f;

        /// <summary>
		/// Distance to the end point to consider the end of path to be reached.
		/// When the end is within this distance then <see cref="OnTargetReached"/> will be called and <see cref="reachedEndOfPath"/> will return true.
		/// </summary>
		public float EndReachedDistance = 0.25F;

        [HideInInspector]
        public bool StartHasRun = false;

        /// <summary>
		/// The end of the path has been reached.
		/// If you want custom logic for when the AI has reached it's destination add it here. You can
		/// also create a new script which inherits from this one and override the function in that script.
		///
		/// This method will be called again if a new path is calculated as the destination may have changed.
		/// So when the agent is close to the destination this method will typically be called every <see cref="repathRate"/> seconds.
		/// </summary>
		public virtual void OnTargetReached()
        {
        }

        public abstract float SwitchOverDistance();
        public abstract float LookAheadDistance();

        public override void OnStart()
        {
            this._switchOverDistance = this.SwitchOverDistance();
            this._lookAheadDistance = this.LookAheadDistance();

            this.PathCalculationDefinition = this.Agent.GetComponent<PathCalculationSetting>().Definition;
            this.Seeker = this.Agent.GetComponent<Seeker>();
            this.Seeker.pathCallback += this.OnPathComplete;

            this.StartHasRun = true;
            this.Init();
        }

        public override void OnStop()
        {
            if (this.Seeker != null)
            {
                this.Seeker.pathCallback -= this.OnPathComplete;
            }
            
            // Release current path so that it can be pooled
            if (this.Path != null)
            {
                this.Path.Release(this);
            }
            this.Path = null;
            this.Interpolator.SetPath(null);
            this.ReachedEndOfPath = false;
        }

        protected Vector3 AgentPosition
        {
            get => this.Agent.Transform.position;
        }

        protected float AgentRadius
        {
            get => this.Agent.Radius;
        }

        protected MovementSetting AgentMovementSettings
        {
            get => this.Agent.MovementSettings;
        }

        protected AutoRepathPolicy AutoRepath
        {
            get => this.PathCalculationDefinition.RepathPolicy;
        }

        protected virtual void OnPathComplete(Path newPath)
        {
            var p = newPath as ABPath;

            if (p == null) throw new System.Exception("This function only handles ABPaths, do not use special path types");

            this.WaitingForPathCalculation = false;

            // Increase the reference count on the new path.
            // This is used for object pooling to reduce allocations.
            p.Claim(this);

            // Path couldn't be calculated of some reason.
            // More info in p.errorLog (debug string)
            if (p.error)
            {
                p.Release(this);
                SetPath(null);
                return;
            }

            // Release the previous path.
            if (this.Path != null)
            {
                this.Path.Release(this);
            }

            // Replace the old path
            this.Path = p;

            // Make sure the path contains at least 2 points
            if (this.Path.vectorPath.Count == 1) this.Path.vectorPath.Add(this.Path.vectorPath[0]);
            this.Interpolator.SetPath(this.Path.vectorPath);

            this.ReachedEndOfPath = false;

            // Simulate movement from the point where the path was requested
            // to where we are right now. This reduces the risk that the agent
            // gets confused because the first point in the path is far away
            // from the current position (possibly behind it which could cause
            // the agent to turn around, and that looks pretty bad).
            this.Interpolator.MoveToLocallyClosestPoint((this.AgentPosition + p.originalStartPoint) * 0.5f);
            this.Interpolator.MoveToLocallyClosestPoint(this.AgentPosition);

            var graph = AstarPath.active.data.graphs[0] as ITransformedGraph;

            // Update which point we are moving towards.
            // Note that we need to do this here because otherwise the remainingDistance field might be incorrect for 1 frame.
            // (due to interpolator.remainingDistance being incorrect).
            this.Interpolator.MoveToCircleIntersection2D(this.AgentPosition, this._lookAheadDistance, graph.transform);

            var distanceToEnd = this.RemainingDistance;
            if (distanceToEnd <= this.EndReachedDistance)
            {
                this.ReachedEndOfPath = true;
                OnTargetReached();
            }
        }

        void Init()
        {
            if (this.StartHasRun)
            {
                this.AutoRepath.Reset();
                if (this.ShouldRecalculatePath)
                {
                    this.SearchPath();
                }
            }
        }

        public float RemainingDistance
        {
            get
            {
                return this.Interpolator.valid ? this.Interpolator.remainingDistance + (this.Interpolator.position - this.AgentPosition).magnitude : float.PositiveInfinity;
            }
        }

        public bool PathPending
        {
            get
            {
                return this.WaitingForPathCalculation;
            }
        }

        /// <summary>True if the path should be automatically recalculated as soon as possible</summary>
		protected virtual bool ShouldRecalculatePath
        {
            get
            {
                return !this.WaitingForPathCalculation && this.AutoRepath.ShouldRecalculatePath(this.AgentPosition, this.AgentRadius, this.Destination);
            }
        }

        /// <summary>
		/// Point on the path which the agent is currently moving towards.
		/// This is usually a point a small distance ahead of the agent
		/// or the end of the path.
		///
		/// If the agent does not have a path at the moment, then the agent's current position will be returned.
		/// </summary>
        public Vector3 SteeringTarget
        {
            get
            {
                return this.Interpolator.valid ? this.Interpolator.position : this.AgentPosition;
            }
        }

        public void MovementUpdate(float deltaTime, out Vector2 nextPosition)
        {
            var currentAcceleration = this.AgentMovementSettings.MaxAcceleration;

            // If negative, calculate the acceleration from the max speed
            if (currentAcceleration < 0) currentAcceleration *= -this.AgentMovementSettings.MaxVelocity;

            var currentPosition = this.AgentPosition;

            // Update which point we are moving towards
            var graph = AstarPath.active.data.graphs[0] as ITransformedGraph;
            this.Interpolator.MoveToCircleIntersection2D(currentPosition, this._lookAheadDistance, graph.transform);
            var dir = this.SteeringTarget - currentPosition;

            // Calculate the distance to the end of the path
            var distanceToEnd = dir.magnitude + Mathf.Max(0, this.Interpolator.remainingDistance);

            // Check if we have reached the target
            var prevTargetReached = this.ReachedEndOfPath;
            this.ReachedEndOfPath = distanceToEnd <= this._switchOverDistance && this.Interpolator.valid;
            if (!prevTargetReached && this.ReachedEndOfPath)
            {
                this.OnTargetReached();
            }
            float slowdown;

            // Normalized direction of where the agent is looking
            var forwards = (Vector2)Vector3.right;

            var maxSpeed = this.AgentMovementSettings.MaxVelocity;

            // Check if we have a valid path to follow and some other script has not stopped the character
            var stopped = this.ReachedDestination && this.WhenCloseToDestinationMode == CloseToDestinationMode.Stop;
            if (this.Interpolator.valid && !stopped)
            {
                // How fast to move depending on the distance to the destination.
                // Move slower as the character gets closer to the destination.
                // This is always a value between 0 and 1.
                var slowdownDistance = this.AgentMovementSettings.SlowDownDistance;
                slowdown = distanceToEnd < slowdownDistance ? Mathf.Sqrt(distanceToEnd / slowdownDistance) : 1;

                if (this.ReachedEndOfPath && this.WhenCloseToDestinationMode == CloseToDestinationMode.Stop)
                {
                    // Slow down as quickly as possible
                    this.Velocity2D -= Vector2.ClampMagnitude(this.Velocity2D, currentAcceleration * deltaTime);
                }
                else
                {

                    this.Velocity2D += MovementUtilities.CalculateAccelerationToReachPoint(dir, dir.normalized * maxSpeed, this.Velocity2D, currentAcceleration, this.AgentMovementSettings.MaxAngularVelocity, maxSpeed, forwards) * deltaTime;
                }
            }
            else
            {
                slowdown = 1;
                // Slow down as quickly as possible
                this.Velocity2D -= Vector2.ClampMagnitude(this.Velocity2D, currentAcceleration * deltaTime);
            }

            this.Velocity2D = MovementUtilities.ClampVelocity(this.Velocity2D, maxSpeed, slowdown, false, forwards);

            // Set how much the agent wants to move during this frame
            //nextPosition = Vector2.ClampMagnitude(this.Velocity2D * deltaTime, distanceToEnd);
            nextPosition = this.Velocity2D * deltaTime;
        }

        public override State OnUpdate()
        {
            var data = AstarPath.active.data;
            if (data != null && data.graphs.Length > 0)
            {
                if (this.ShouldRecalculatePath)
                {
                    this.SearchPath();
                }

                this.MovementUpdate(Time.deltaTime, out var nextPosition);
                this.Agent.Body.AddForce(nextPosition);
            }
            return State.Running;
        }

        public virtual void SearchPath()
        {
            var data = AstarPath.active.data;
            if (data == null || data.graphs.Length == 0)
            {
                return;
            }

            if (float.IsPositiveInfinity(this.Destination.x))
            {
                return;
            }
            if (this.OnSearchPath != null)
            {
                this.OnSearchPath();
            }

            this.CalculatePathRequestEndpoints(out var start, out var end);

            // Request a path to be calculated from our current position to the destination
            var p = ABPath.Construct(start, end, this.OnPathCalculated);
                        
            // p.traversalProvider = this.PathCalculationDefinition.ProviderConfiguration.TraversalProviderFactory();

            this.SetPath(p, false);
        }

        protected virtual void OnPathCalculated(Path p)
        {
            
        }

        public void SetPath(Path path, bool updateDestinationFromPath = true)
        {
            if (updateDestinationFromPath && path is ABPath abPath)
            {
                this.Destination = abPath.originalEndPoint;
            }

            if (path == null)
            {
                CancelCurrentPathRequest();
                ClearPath();
            }
            else if (path.PipelineState == PathState.Created)
            {
                // Path has not started calculation yet
                this.WaitingForPathCalculation = true;
                this.Seeker.CancelCurrentPathRequest();
                this.Seeker.StartPath(path);
                this.AutoRepath.DidRecalculatePath(this.Destination);
            }
            else if (path.PipelineState == PathState.Returned)
            {
                // Path has already been calculated

                // We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it.
                if (this.Seeker.GetCurrentPath() != path)
                {
                    this.Seeker.CancelCurrentPathRequest();
                }
                else throw new System.ArgumentException("If you calculate the path using seeker.StartPath then this script will pick up the calculated path anyway as it listens for all paths the Seeker finishes calculating. You should not call SetPath in that case.");

                this.OnPathComplete(path);
            }
            else
            {
                // Path calculation has been started, but it is not yet complete. Cannot really handle this.
                throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished.");
            }
        }

        /// <summary>
		/// Outputs the start point and end point of the next automatic path request.
		/// This is a separate method to make it easy for subclasses to swap out the endpoints
		/// of path requests. For example the <see cref="LocalSpaceRichAI"/> script which requires the endpoints
		/// to be transformed to graph space first.
		/// </summary>
		protected virtual void CalculatePathRequestEndpoints(out Vector3 start, out Vector3 end)
        {
            start = this.AgentPosition;
            end = this.Destination;
        }

        /// <summary>
		/// True if the agent has reached the end of the current path.
		///
		/// Note that setting the <see cref="destination"/> does not immediately update the path, nor is there any guarantee that the
		/// AI will actually be able to reach the destination that you set. The AI will try to get as close as possible.
		/// Often you want to use <see cref="reachedDestination"/> instead which is easier to work with.
		///
		/// It is very hard to provide a method for detecting if the AI has reached the <see cref="destination"/> that works across all different games
		/// because the destination may not even lie on the navmesh and how that is handled differs from game to game (see also the code snippet in the docs for <see cref="destination)"/>.
		///
		/// See: <see cref="remainingDistance"/>
		/// See: <see cref="reachedDestination"/>
		/// </summary>
        public bool ReachedEndOfPath { get; protected set; }

        /// <summary>
        /// Fills buffer with the remaining path.
        ///
        /// <code>
        /// var buffer = new List<Vector3>();
        ///
        /// ai.GetRemainingPath(buffer, out bool stale);
        /// for (int i = 0; i < buffer.Count - 1; i++) {
        ///     Debug.DrawLine(buffer[i], buffer[i+1], Color.red);
        /// }
        /// </code>
        /// [Open online documentation to see images]
        /// </summary>
        /// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param>
        /// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or (for the RichAI script only) if the agent has detected that some nodes in the path have been destroyed.</param>
        public void GetRemainingPath(List<Vector3> buffer, out bool stale)
        {
            buffer.Clear();
            buffer.Add(this.AgentPosition);
            if (!this.Interpolator.valid)
            {
                stale = true;
                return;
            }

            stale = false;
            this.Interpolator.GetRemainingPath(buffer);
        }

        /// <summary>True if this agent currently has a path that it follows</summary>
        public bool HasPath
        {
            get
            {
                return this.Interpolator.valid;
            }
        }

        protected void ClearPath()
        {
            CancelCurrentPathRequest();
            if (this.Path != null) this.Path.Release(this);
            this.Path = null;
            this.Interpolator.SetPath(null);
            this.ReachedEndOfPath = false;
        }



        protected void CancelCurrentPathRequest()
        {
            this.WaitingForPathCalculation = false;
            // Abort calculation of the current path
            if (this.Seeker != null) this.Seeker.CancelCurrentPathRequest();
            Debug.Log("CancelCurrentPathRequest");
        }
    }

FollowPath

Code
public class FollowPath : NavigationAction, IBehaviours
    {
        [Tooltip("Which path to use - in case there are multiple (otherwise can be empty)")]
        public string UsePathId = string.Empty;

        private PathSetting _pathSettings;
        private Random _random = new Random();
        private State _state = State.Running;
        private int _currentPathPosition = 0;

        public override void OnStart()
        {
            this._pathSettings = this.GetPathSetting();
            this.Destination = this._pathSettings.Points[this.GetStartPoint()];

            base.OnStart();
        }

        public override void OnStop()
        {
            base.OnStop();
        }

        public override void OnTargetReached()
        {
            // end of astar path reached, check for next point on patrol path
            var pointBefore = this._currentPathPosition;
            var point = this.GetPointToPersue();
            if (this._currentPathPosition == pointBefore)
            {
                this._state = State.Success;
                return;
            }

            this.Destination = point;
            this.SearchPath();
        }

        protected override void OnPathCalculated(Path p)
        {
            base.OnPathCalculated(p);
        }

        public override State OnUpdate()
        {
            base.OnUpdate();
            return this._state;
        }

        private Vector2 GetPointToPersue()
        {
            switch (this._pathSettings.Mode)
            {
                case PathSetting.PatrolMode.Random:
                    this._currentPathPosition = this._random.Next(this._pathSettings.Points.Count);
                    break;
                case PathSetting.PatrolMode.Loop:
                    var count = this._pathSettings.Points.Count;
                    this._currentPathPosition = ((this._currentPathPosition + (int)this._pathSettings.PathFollowOrder) + count) % count;
                    break;
                case PathSetting.PatrolMode.BackAndForth:
                    if (this._currentPathPosition <= 0 || this._currentPathPosition >= this._pathSettings.Points.Count)
                    {
                        if (this._pathSettings.PathFollowOrder == PathSetting.FollowOrder.Ascending) this._pathSettings.PathFollowOrder = PathSetting.FollowOrder.Descending;
                        if (this._pathSettings.PathFollowOrder == PathSetting.FollowOrder.Descending) this._pathSettings.PathFollowOrder = PathSetting.FollowOrder.Ascending;
                    }
                    this._currentPathPosition = (this._currentPathPosition + (int)this._pathSettings.PathFollowOrder) % this._pathSettings.Points.Count;
                    break;
                case PathSetting.PatrolMode.Once:
                    if (this._currentPathPosition <= 0 || this._currentPathPosition >= this._pathSettings.Points.Count)
                    {
                        break;
                    }
                    this._currentPathPosition = (this._currentPathPosition + (int)this._pathSettings.PathFollowOrder);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            return this._pathSettings.Points[this._currentPathPosition];
        }

        private int GetStartPoint()
        {
            return this._pathSettings.SelectionMethod switch
            {
                PathSetting.StartPointSelectionMethod.Closest => this.GetClosestPoint(),
                PathSetting.StartPointSelectionMethod.Random => this._random.Next(this._pathSettings.Points.Count),
                _ => 0
            };
        }

        private static float Distance(Vector3 point, IAiAgent agent)
        {
            var agentPos = agent.Transform.position;
            return (point - agentPos).magnitude;
        }

        private int GetClosestPoint()
        {
            var minIx = 0;
            var minDist = Distance(this._pathSettings.Points[minIx], this.Agent);

            for (var i = 1; i < this._pathSettings.Points.Count; i++)
            {
                var current = this._pathSettings.Points[i];
                var dist = Distance(current, this.Agent);
                if (dist < minDist)
                {
                    minIx = i;
                    minDist = dist;
                }
            }

            return minIx;
        }

        private PathSetting GetPathSetting()
        {
            var paths = this.Agent.GetComponents<PathSetting>();
            if (paths == null || !paths.Any())
            {
                throw new Exception($"There is no path with id {this.UsePathId} at agent {this.Agent.GetComponent<Entity>().name}");
            }

            if (!string.IsNullOrEmpty(this.UsePathId))
            {
                paths = paths.Where(x => x.PathId == this.UsePathId).ToArray();
            }

            if (paths.Length != 1)
            {
                throw new Exception($"There are {paths.Length} paths available for path id {this.UsePathId}");
            }

            return paths[0];
        }

        public override float SwitchOverDistance()
        {
            return this._pathSettings.SwitchOverDistance;
        }

        public override float LookAheadDistance()
        {
            return this.AgentMovementSettings.LookAheadDistance;
        }
    }

What this does, is that FollowPath selects a specific point from the PathSettings and follows them in a defined order (Loop, Random, BackAndForth, and Once => see for reference Vector2 GetPointToPersue()) whenever the end of a path is reached. What is important here, is that in FollowPath the Destination attribute is set, which is than used by NavigationAction to do the actual path finding.NevigationAction is more or less just AIPath → You should be able to use AIPath and a likewise script to achieve the same with less custom implementation.

Be aware that this might not be usable for your scenario 1:1 because it is heavily altered for my specific needs but maybe it can help you.

Best :slight_smile:

Hi

If you want to create a custom path, you can use ABPath.FakePath. See ABPath - A* Pathfinding Project

Thank you, it helped !