Click To Move (Sample)

Provided as a part of the Samples

Namespace: MegaCrush.NavmeshBaker.Sample


Purpose

A simple player controller that moves an agent to the clicked point on the ground — but only after the navmesh is ready. This prevents errors like:

“Resume can only be called on an active agent that has been placed on a NavMesh.”


How it works

  • Subscribes to BakerEvents.OnBakeCompleted.

  • On the first bake:

    • Enables the NavMeshAgent.

    • Warps the agent to a valid NavMesh point near its current position.

    • Unlocks input handling.

  • On each left mouse click:

    • Casts a ray into the scene.

    • Uses NavMesh.SamplePosition() to find the nearest walkable point.

    • Calls SetDestination() on the agent.


Inspector

  • Ray Distance (float) Max distance for the raycast from the camera.

  • Ray Mask (LayerMask) Which layers are valid for the click raycast.

  • Sample Radius (float) Radius around the click point to look for a valid NavMesh.

  • Verbose (bool) If enabled, prints debug logs when snapping and moving.


Example usage

  1. Ensure your scene has a BakerCoordinator with a NavMeshBakeProfile and Center Target.

  2. Add one or more DynamicNavMeshSurface components to define navigable regions.

  3. Create a capsule or character GameObject.

  4. Add a NavMeshAgent and this ClickToMoveController script.

  5. Play — after the first runtime bake, click anywhere on the ground to move the agent.


Why it’s useful

  • Ensures agents only start moving once the navmesh is valid.

  • Removes startup warnings/errors from spawning before a bake.

  • Demonstrates how to use BakerEvents.OnBakeCompleted to gate gameplay logic.


Script

using UnityEngine;
using UnityEngine.AI;
using MegaCrush.RuntimeNavmeshBaker; // BakerEvents

namespace MegaCrush.NavmeshBaker.Sample
{
    [RequireComponent(typeof(NavMeshAgent))]
    public class ClickToMoveController : MonoBehaviour
    {
        [Header("Raycast")]
        [SerializeField] private float rayDistance = 1000f;
        [SerializeField] private LayerMask rayMask = ~0;

        [Header("NavMesh")]
        [SerializeField, Min(0f)] private float sampleRadius = 4f;

        [Header("Debug")]
        [SerializeField] private bool verbose = false;

        private NavMeshAgent agent;
        private Camera mainCam;
        private bool inputEnabled;

        private void Awake()
        {
            agent = GetComponent<NavMeshAgent>();
            mainCam = Camera.main;

            // Leave disabled until we can snap it to the navmesh
            agent.enabled = false;
        }

        private void OnEnable()
        {
            BakerEvents.OnBakeCompleted += HandleBakeCompleted;

            // Try immediately — scene may already have a navmesh
            if (!agent.enabled) TryEnableOnNavMesh();
        }

        private void OnDisable()
        {
            BakerEvents.OnBakeCompleted -= HandleBakeCompleted;
        }

        private void Update()
        {
            if (!mainCam) mainCam = Camera.main;
            if (!mainCam || !agent.enabled || !inputEnabled || !agent.isOnNavMesh) return;

            if (Input.GetMouseButtonDown(0))
            {
                var ray = mainCam.ScreenPointToRay(Input.mousePosition);
                if (Physics.Raycast(ray, out var hit, rayDistance, rayMask, QueryTriggerInteraction.Ignore))
                {
                    if (NavMesh.SamplePosition(hit.point, out var nmHit, sampleRadius, NavMesh.AllAreas))
                    {
                        if (verbose) Debug.Log($"[ClickToMove] SetDestination {nmHit.position}");
                        agent.isStopped = false;
                        agent.SetDestination(nmHit.position);
                    }
                    else if (verbose)
                    {
                        Debug.Log("[ClickToMove] No navmesh near click.");
                    }
                }
            }
        }

        private void HandleBakeCompleted(DynamicNavMeshSurface s, Bounds b, float dur)
        {
            if (TryEnableOnNavMesh())
            {
                // Only needed once; stop listening
                BakerEvents.OnBakeCompleted -= HandleBakeCompleted;
                if (verbose) Debug.Log("[ClickToMove] First bake detected; input enabled.");
            }
        }

        /// <summary>
        /// If there’s a valid navmesh near our position, enable the agent and warp it.
        /// </summary>
        private bool TryEnableOnNavMesh()
        {
            if (agent.enabled) return true;

            if (NavMesh.SamplePosition(transform.position, out var hit, sampleRadius, NavMesh.AllAreas))
            {
                agent.enabled = true;
                agent.Warp(hit.position);
                inputEnabled = true;

                if (verbose) Debug.Log($"[ClickToMove] Snapped agent to navmesh at {hit.position}");
                return true;
            }

            return false;
        }
    }
}

Last updated