Writing a MoveTowards that works for NavMesh

sdhains

New member
I'm writing unity software where there are many different objects in space that the AI engages with. Each object has its own offset/rotation sweet spot that the AI needs to position itself in to use the objects. The MoveTowards/StartAbilityLocation workflow would have really helped me out with this but I'm disappointed to find it doesn't work with Behavior Designer/NavMesh/AI agents.


I don't imagine it would be too crazy to implement. Does anyone have any tips on doing this? Why does the MoveTowards function not work for NavMesh? How might I approach Implementing a MoveTowards that was NavMesh friendly?
 
Oh question for you @schaderDev - what did you do to get the NavMeshAgentMovement ability active again after the interaction has finished? The Automatic trigger (with manual stop) causes the Interact to happen constantly. So it seems some intervention is necessary.

I created a 5 second cooldown on the interactable target to prevent Interact from continuously triggering. But this is broken for longer animations because NavMesh keeps on moving (and NavMesh might reach its destination by the time the cooldown is over).

Im going to try to do something with the InteractCompleteEvent tomorrow. Maybe trigger a MoveAway ability?

To be honest I'm feeling a little stuck. Did you solve this?
 
Last edited:
Attach the script below to your interactable object (the object with the AnimatedInteractable.cs).

You also need to change the variable "m_SingleInteract" in the AnimatedInteractable.cs script (line 19) from protected to public.

With that script your agent will do the interaction only once when he enters the trigger. Leaving the trigger will reset the interaction.
This should prevent the agent to constantly try to interact with the object if he is inside the trigger.

C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Opsive.UltimateCharacterController.Objects.CharacterAssist;
using Opsive.UltimateCharacterController.Utility;
using Opsive.UltimateCharacterController.Character;

public class SetSingleInteract : MonoBehaviour
{
    private AnimatedInteractable animInteractable;

    // Start is called before the first frame update
    void Start()
    {
        animInteractable = gameObject.GetComponent<AnimatedInteractable>();
    }

    private void OnTriggerEnter(Collider other)
    {
        var layerManager = other.gameObject.GetCachedParentComponent<CharacterLayerManager>();

        if (!MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            return;
        }

        if (MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            // Interact only once
            animInteractable.m_SingleInteract = true;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        var layerManager = other.gameObject.GetCachedParentComponent<CharacterLayerManager>();

        if (!MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            return;
        }


        if (MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            // On trigger exit reset the interactable object
            animInteractable.m_SingleInteract = false;
            animInteractable.ResetInteract();
        }
    }
}
 
THANK YOU SO MUCH!!

I created a StaticInteractable class that is compatible with a modified version of this script (see below). In case anyone wants to do it on a component without an Animator. Note that for some reason this file needs to be in the same folder as AnimatorInteractable to work. The audio manager doesn't work without this file placement. If anyone could explain why this would be great!

C#:
using UnityEngine;
using Opsive.UltimateCharacterController.Traits;
using Opsive.UltimateCharacterController.Audio;

namespace Opsive.UltimateCharacterController.Objects.CharacterAssist
{
    public class StaticInteractable : MonoBehaviour, IInteractableTarget, IInteractableMessage
    {
        [Tooltip("Can the interactable be interacted with only once?")] [SerializeField]
        public bool m_SingleInteract;

        [Tooltip("The bool parameter name that should be changed when interacted with. Can be empty.")] [SerializeField]
        protected string m_BoolParameter;

        [Tooltip("The value to set the bool to when interacted with. Only used if the bool parameter is not empty.")]
        [SerializeField]
        protected bool m_BoolInteractValue = true;

        [Tooltip("The UI message that should be displayed when the bool is enabled.")] [SerializeField]
        protected string m_BoolEnabledMessage;

        [Tooltip("The UI message that should be displayed when the bool is disabled.")] [SerializeField]
        protected string m_BoolDisabledMessage;

        [Tooltip("Should the bool interact value be toggled after an interact?")] [SerializeField]
        protected bool m_ToggleBoolInteractValue = true;

        [Tooltip("An array of audio clips that can be played when the interaction starts.")] [SerializeField]
        protected AudioClip[] m_InteractAudioClips;

        private GameObject m_GameObject;
        private StaticInteractable[] m_StaticInteractables;
        private bool m_HasInteracted;
        private int m_BoolParameterHash;
        private int m_AudioClipIndex = -1;
        private bool m_ActiveBoolInteractable;

        public bool ActiveBoolInteractable
        {
            get { return m_ActiveBoolInteractable; }
        }

        /// <summary>
        /// Initialize the default values.
        /// </summary>
        private void Awake()
        {
            m_GameObject = gameObject;
            if (!string.IsNullOrEmpty(m_BoolParameter))
            {
                m_BoolParameterHash = Animator.StringToHash(m_BoolParameter);
            }

            var staticInteractables = GetComponents<StaticInteractable>();
            if (staticInteractables.Length > 1)
            {
                m_StaticInteractables = new StaticInteractable[staticInteractables.Length - 1];
                var count = 0;
                for (int i = 0; i < staticInteractables.Length; ++i)
                {
                    if (staticInteractables[i] == this)
                    {
                        continue;
                    }

                    m_StaticInteractables[count] = staticInteractables[i];
                }
            }

        }

        /// <summary>
        /// Can the target be interacted with?
        /// </summary>
        /// <param name="character">The character that wants to interactact with the target.</param>
        /// <returns>True if the target can be interacted with.</returns>
        public bool CanInteract(GameObject character)
        {
            return !m_HasInteracted || !m_SingleInteract;
        }

        /// <summary>
        /// Interact with the target.
        /// </summary>
        /// <param name="character">The character that wants to interactact with the target.</param>
        public void Interact(GameObject character)
        {
            if (m_BoolParameterHash != 0)
            {
                // If the bool value can be toggled then there's a chance that another AnimatedInteractable is currently active. In that case the original
                // AnimatedInteractable should respond to the interact event.
                if (m_StaticInteractables != null)
                {
                    for (int i = 0; i < m_StaticInteractables.Length; ++i)
                    {
                        if (m_StaticInteractables[i].ActiveBoolInteractable)
                        {
                            m_StaticInteractables[i].Interact(character);
                            return;
                        }
                    }
                }

                if (m_ToggleBoolInteractValue)
                {
                    m_BoolInteractValue = !m_BoolInteractValue;
                    m_ActiveBoolInteractable = !m_ActiveBoolInteractable;
                }
            }

            if (m_InteractAudioClips.Length > 0)
            {
                // Sequentually switch between audio clips.
                m_AudioClipIndex = (m_AudioClipIndex + 1) % m_InteractAudioClips.Length;
                AudioManager.Play(m_GameObject, m_InteractAudioClips[m_AudioClipIndex]);
            }

            m_HasInteracted = true;
        }

        /// <summary>
        /// Resets the interact variables.
        /// </summary>
        public void ResetInteract()
        {
            m_HasInteracted = false;
            m_AudioClipIndex = -1;
        }

        /// <summary>
        /// Returns the message that should be displayed when the object can be interacted with.
        /// </summary>
        /// <returns>The message that should be displayed when the object can be interacted with.</returns>
        public string AbilityMessage()
        {
            if (m_BoolParameterHash != 0)
            {
                // If the bool value can be toggled then there's a chance that another AnimatedInteractable is currently active. In that case the original
                // AnimatedInteractable should respond to the message.
                if (m_StaticInteractables != null)
                {
                    for (int i = 0; i < m_StaticInteractables.Length; ++i)
                    {
                        if (m_StaticInteractables[i].ActiveBoolInteractable)
                        {
                            return m_StaticInteractables[i].AbilityMessage();
                        }
                    }
                }

                return m_BoolInteractValue ? m_BoolEnabledMessage : m_BoolDisabledMessage;
            }

            return string.Empty;
        }
    }
}

and this is a SetSingleInteract that works with the StaticInteractable:


C#:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Opsive.UltimateCharacterController.Objects.CharacterAssist;
using Opsive.UltimateCharacterController.Utility;
using Opsive.UltimateCharacterController.Character;

public class SetSingleInteract : MonoBehaviour
{
    private StaticInteractable m_StaticInteractable;

    // Start is called before the first frame update
    void Start()
    {
        m_StaticInteractable = gameObject.GetComponent<StaticInteractable>();
    }

    private void OnTriggerEnter(Collider other)
    {
        var layerManager = other.gameObject.GetCachedParentComponent<CharacterLayerManager>();

        if (!MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            return;
        }

        if (MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            // Interact only once
            m_StaticInteractable.m_SingleInteract = true;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        var layerManager = other.gameObject.GetCachedParentComponent<CharacterLayerManager>();

        if (!MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            return;
        }


        if (MathUtility.InLayerMask(other.gameObject.layer, layerManager.CharacterLayer))
        {
            // On trigger exit reset the interactable object
            m_StaticInteractable.m_SingleInteract = false;
            m_StaticInteractable.ResetInteract();
        }
    }
}
 
Top