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
Ensure your scene has a BakerCoordinator with a NavMeshBakeProfile and Center Target.
Add one or more DynamicNavMeshSurface components to define navigable regions.
Create a capsule or character GameObject.
Add a
NavMeshAgent
and this ClickToMoveController script.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