Code/BehaviorTree/Task/NavMoveTo.cs
using System;
using Sandbox;

namespace NPBehave
{
    public class NavMoveTo : Task
    {
        private const float DestinationChangeThreshold = 0.0001f;
        private const uint DestinationChangeMaxChecks = 100;
        
        private NavMeshAgent _agent;
        private string _blackboardKey;
        private float _tolerance;
        private bool _stopOnTolerance;
        private float _updateFrequency;
        private float _updateVariance;

        private Vector3 _lastDestination;
        private float _lastDistance;
        private uint _failedChecks;

        /// CAUTION: EXPERIMENTAL !!!!
        /// <param name="agent">target to move</param>
        /// <param name="blackboardKey">blackboard key containing either a Transform or a Vector.</param>
        /// <param name="tolerance">acceptable tolerance</param>
        /// <param name="stopOnTolerance">should stop when in tolerance</param>
        /// <param name="updateFrequency">frequency to check for changes of reaching the destination or a Transform's location</param>
        /// <param name="updateVariance">random variance for updateFrequency</param>

#if UNITY_5_3 || UNITY_5_4
        public NavMoveTo(NavMeshAgent agent, string blackboardKey, float tolerance = 1.0f, bool stopOnTolerance = false, float updateFrequency = 0.1f, float updateVariance = 0.025f) : base("NavMoveTo")
#else
        public NavMoveTo(NavMeshAgent agent, string blackboardKey, float tolerance = 1.0f, bool stopOnTolerance = false, float updateFrequency = 0.1f, float updateVariance = 0.025f) : base("NavMoveTo")
#endif
        {
            _agent = agent;
            _blackboardKey = blackboardKey;
            _tolerance = tolerance;
            _stopOnTolerance = stopOnTolerance;
            _updateFrequency = updateFrequency;
            _updateVariance = updateVariance;
        }

        protected override void DoStart()
        {
            _lastDestination = Vector3.Zero;
            _lastDistance = 99999999.0f;
            _failedChecks = 0;

            Blackboard.AddObserver(_blackboardKey, OnBlackboardValueChanged);
            Clock.AddTimer(_updateFrequency, _updateVariance, -1, OnUpdateTimer);

            MoveToBlackboardKey();
        }

        protected override void DoStop()
        {
            StopAndCleanUp(false);
        }

        private void OnBlackboardValueChanged(Blackboard.Type type, object newValue)
        {
            MoveToBlackboardKey();
        }

        private void OnUpdateTimer()
        {
            MoveToBlackboardKey();
        }

        private void MoveToBlackboardKey()
        {
            object target = Blackboard.Get(_blackboardKey);
            if (target == null)
            {
                StopAndCleanUp(false);
                return;
            }

            // get target location
            Vector3 destination = Vector3.Zero;
            if (target is Transform transform)
            {
                if (_updateFrequency >= 0.0f)
                {
                    destination = transform.Position;
                }
            }
            else if (target is Vector3 vector3)
            {
                destination = vector3;
            }
            else if (target is GameObject gameObject)
            {
	            destination = gameObject.Transform.Position;
            }
            else
            {
                Log.Warning(
	                $"NavMoveTo: Blackboard Key '{_blackboardKey}' contained unsupported type '{target.GetType()}" );
                StopAndCleanUp(false);
                return;
            }

            // set new destination
            if(_agent.TargetPosition == null || !_agent.TargetPosition.Value.AlmostEqual( destination ) )
	            _agent.MoveTo(destination);

            float sqrDistLeft = (destination - _agent.AgentPosition).LengthSquared;
            
            bool destinationChanged = (_agent.TargetPosition ?? Vector3.Zero - _lastDestination).LengthSquared > (DestinationChangeThreshold * DestinationChangeThreshold); //(destination - agent.destination).sqrMagnitude > (DESTINATION_CHANGE_THRESHOLD * DESTINATION_CHANGE_THRESHOLD);
            bool distanceChanged = MathF.Abs(sqrDistLeft) > DestinationChangeThreshold;

            // check if we are already at our goal and stop the task
            if (_lastDistance < _tolerance)
            {
                if (_stopOnTolerance || (!destinationChanged && !distanceChanged))
                {
                    // reached the goal
                    StopAndCleanUp(true);
                    return;
                }
            }
            else if (!destinationChanged && !distanceChanged)
            {
                if (_failedChecks++ > DestinationChangeMaxChecks)
                {
                    // could not reach the goal for whatever reason
                    StopAndCleanUp(false);
                    return;
                }
            }
            else
            {
                _failedChecks = 0;
            }

            _lastDestination = _agent.TargetPosition ?? Vector3.Zero; 

            // Workaround for lastDistance set to 0 https://github.com/meniku/NPBehave/issues/33
            if (_agent.TargetPosition == null)
            {
                _lastDistance = 99999999.0f;
            }
            else
            {
                _lastDistance = sqrDistLeft;
            }
        }

        private void StopAndCleanUp(bool result)
        {
            _agent.Stop();
            Blackboard.RemoveObserver(_blackboardKey, OnBlackboardValueChanged);
            Clock.RemoveTimer(OnUpdateTimer);
            Stopped(result);
        }
    }
}