I implemented a different humanoid model for my Player (the correct Avatar is placed). I don’t know how to get it to move properly. It doesn’t seem to respond to me changing the values in the Nav Mesh agent and the walking/running animations aren’t playing either. How should go about solving this problem? I also tried to upload a video here, but rejected it, not sure how to convert it to the right file either.
You can always post a video to YouTube and set it to “Anyone with a link” and provide the link here and I can see it.
I just want to make sure of the issue here:
- Is the character moving at all when you click a location?
- Was it moving when we were using capsules?
- Was it moving with the Character_Knight character?
- Is the character moving, but the animation wasn’t reflecting the movement?
- Is the idle animation working?
- Make sure that the spelling on the parameter in the Animator Blend Tree matches exactly with the name used in the Animator.SetFloat. Like C# itself, it is case sensitive, so speed != Speed
Ok, great: https://youtu.be/v5Bm0QAqkeA
The character moved normally when it had the Knight model. The character moves when I click a location, I also added WASD movement. The idle animation on the new model does seem to work. The only parameter I see is “forwardSpeed”, it looks the same in the blend tree and mover script.
Zip up your project and upload it to https://gdev.tv/projectupload and I’ll take a look and see what might be happening.
ok thanks, I sent it.
I probably should have asked “was the old character playing the walk animation” and asked for the Mover script.
As it turns out, the Mover script is incomplete, in that the Animator is never updated.
private void UpdateAnimator()
{
// Update animator based on movement speed or any custom logic (if needed)
}
We need to flesh this out a bit, from the lecture Match Animation To Movement:
private void UpdateAnimator()
{
Vector3 velocity = navMeshAgent.velocity;
Vector3 localVelocity = transform.InverseTransformDirection(velocity);
float speed = localVelocity.z;
GetComponent<Animator>().SetFloat("forwardSpeed", speed);
}
Additionally, we need to add UpdateAnimator(); to the end of the Update method.
No change on my end. Could there be something going on with the Animation clip that’s it’s not syncing with the different mesh model? Is something needing to be changed in model, rig or animation?
Sometimes, if there is an animator below the parent object, Unity gets confused and defers to that animator. Try deleting the animator that’s attached to the character (Not the one on the Player, but the one on the GameObject between the Player and the Rig.
The walk animation does work now since I removed that extra animator, thanks. I made some other adjustments like smoothing rotation with quaternion slerp and moving faster with a key press. However, I’m not sure that the running animation shows when I speed up. Also, when I try to walk forward/backward with W and S keys that doesn’t work very well. I tried adding that functionality like so:
private void HandleMovementInput()
{
// Get horizontal (A/D or Left/Right arrow keys) and vertical (W/S or Up/Down arrow keys) movement input
float horizontal = Input.GetAxis("Horizontal"); // A/D, Left/Right arrow keys
float vertical = Input.GetAxis("Vertical"); // W/S, Up/Down arrow keys
// Calculate the movement direction relative to the character's orientation
movementInput = (transform.forward * vertical + transform.right * horizontal).normalized * navMeshAgent.speed;
// Ensure no vertical movement on the Y axis (lock movement to 2D plane)
movementInput.y = 0f; // Prevent movement on the Y axis
}
I think the movement is a lot improved now, but maybe you have some suggestion to how I can further tweak it. https://www.youtube.com/watch?v=aaVQ1jrleHg
Handling manual movement with the NavMesh is a bit of a headscratcher…
A couple of things to consider:
As the code works right now, when you start using the Mover for Enemies, they’ll all move every time you press WASD… it’s better to handle WASD movement input in the PlayerController…
The NavMeshAgent really doesn’t like it when you move the character and it’s not the one what moved it. It really really doesn’t like it…
Use NavMeshAgent.Move(moveInput * speed * Time.deltaTime);
Now you’re NavMeshAgent.velocity will tell you the actual movement and you can use that for the animator parameter.
I removed the WASD function but now having an issue with getting the character with click to move again. I watched the Nav Mesh videos, but I didn’t see a way to expand the blue area/mesh in the scene within the Navigation tab. Maybe I missed something.
I added debugs to parts of the Mover and Player Controller script and got the following result the moment I press play:
[PlayerController] RaycastNavMesh failed.
UnityEngine.Debug:Log (object)
InventoryExample.Control.PlayerController:InteractWithMovement () (at Assets/Scripts/Control/PlayerController.cs:134)
InventoryExample.Control.PlayerController:Update () (at Assets/Scripts/Control/PlayerController.cs:36)
[Mover] Not moving. No path or already at destination.
UnityEngine.Debug:Log (object)
InventoryExample.Movement.Mover:Update () (at Assets/Scripts/Movement/Mover.cs:49)
I suppose I’ll share the entire scripts since they’ve had a number of changes and if it helps figure where I’m going wrong here.
Mover:
using GameDevTV.Saving;
using UnityEngine;
using UnityEngine.AI;
namespace InventoryExample.Movement
{
public class Mover : MonoBehaviour, IAction, ISaveable
{
[SerializeField] float moveSpeed = 3f;
[SerializeField] float rotationSpeed = 5f;
[SerializeField] float maxSpeed = 5f;
[SerializeField] float runSpeedMultiplier = 2f;
private NavMeshAgent navMeshAgent;
private Animator animator;
private void Awake()
{
navMeshAgent = GetComponent<NavMeshAgent>();
navMeshAgent.angularSpeed = rotationSpeed;
navMeshAgent.updateRotation = false; // We handle rotation manually
animator = GetComponent<Animator>();
}
private void Update()
{
if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
{
navMeshAgent.speed = moveSpeed * runSpeedMultiplier;
}
else
{
navMeshAgent.speed = Mathf.Min(moveSpeed, maxSpeed);
}
if (navMeshAgent.hasPath && navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance)
{
Debug.Log($"[Mover] Moving. Velocity: {navMeshAgent.desiredVelocity}");
Vector3 desiredVelocity = navMeshAgent.desiredVelocity.normalized * navMeshAgent.speed;
navMeshAgent.Move(desiredVelocity * Time.deltaTime);
Quaternion targetRotation = Quaternion.LookRotation(desiredVelocity);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
else
{
Debug.Log("[Mover] Not moving. No path or already at destination.");
}
UpdateAnimator();
}
public void MoveTo(Vector3 destination)
{
if (navMeshAgent.isOnNavMesh)
{
Debug.Log($"[Mover] Setting destination: {destination}");
navMeshAgent.isStopped = false;
navMeshAgent.SetDestination(destination);
}
else
{
Debug.LogWarning("[Mover] Not on NavMesh.");
}
}
public void StartMoveAction(Vector3 destination, float speedFraction)
{
Debug.Log($"[Mover] StartMoveAction called. Destination: {destination}");
GetComponent<ActionScheduler>().StartAction(this);
MoveTo(destination);
}
public bool CanMoveTo(Vector3 destination)
{
NavMeshPath path = new NavMeshPath();
NavMesh.CalculatePath(transform.position, destination, NavMesh.AllAreas, path);
return path.status == NavMeshPathStatus.PathComplete;
}
public void Cancel()
{
navMeshAgent.ResetPath();
}
private void UpdateAnimator()
{
float speed = navMeshAgent.velocity.magnitude;
animator.SetFloat("forwardSpeed", speed);
}
public object CaptureState()
{
return new SerializableVector3(transform.position);
}
public void RestoreState(object state)
{
SerializableVector3 position = (SerializableVector3)state;
Vector3 restoredPosition = position.ToVector();
restoredPosition.y = transform.position.y;
transform.position = restoredPosition;
GetComponent<ActionScheduler>().CancelCurrentAction();
}
private void OnDrawGizmos()
{
if (navMeshAgent != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position, transform.position + navMeshAgent.velocity);
}
}
}
}
PlayerController:
using UnityEngine;
using System;
using UnityEngine.EventSystems;
using UnityEngine.AI;
using GameDevTV.Inventories;
namespace InventoryExample.Control
{
public class PlayerController : MonoBehaviour
{
[System.Serializable]
public struct CursorMapping
{
public CursorType type;
public Texture2D texture;
public Vector2 hotspot;
}
[SerializeField] CursorMapping[] cursorMappings = null;
[SerializeField] float maxNavMeshProjectionDistance = 10f;
[SerializeField] float raycastRadius = 1f;
bool movementStarted = false;
private void Update()
{
CheckSpecialAbilityKeys();
if (Input.GetMouseButtonUp(0))
{
movementStarted = false;
}
if (InteractWithUI()) return;
if (InteractWithComponent()) return;
if (InteractWithMovement()) return;
SetCursor(CursorType.None);
}
private void CheckSpecialAbilityKeys()
{
var actionStore = GetComponent<ActionStore>();
if (Input.GetKeyDown(KeyCode.Alpha1))
{
actionStore.Use(0, gameObject);
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
actionStore.Use(1, gameObject);
}
if (Input.GetKeyDown(KeyCode.Alpha3))
{
actionStore.Use(2, gameObject);
}
if (Input.GetKeyDown(KeyCode.Alpha4))
{
actionStore.Use(3, gameObject);
}
if (Input.GetKeyDown(KeyCode.Alpha5))
{
actionStore.Use(4, gameObject);
}
if (Input.GetKeyDown(KeyCode.Alpha6))
{
actionStore.Use(5, gameObject);
}
}
private bool InteractWithUI()
{
if (EventSystem.current.IsPointerOverGameObject())
{
SetCursor(CursorType.UI);
return true;
}
return false;
}
private bool InteractWithComponent()
{
RaycastHit[] hits = RaycastAllSorted();
foreach (RaycastHit hit in hits)
{
IRaycastable[] raycastables = hit.transform.GetComponents<IRaycastable>();
foreach (IRaycastable raycastable in raycastables)
{
if (raycastable.HandleRaycast(this))
{
SetCursor(raycastable.GetCursorType());
return true;
}
}
}
return false;
}
RaycastHit[] RaycastAllSorted()
{
RaycastHit[] hits = Physics.SphereCastAll(GetMouseRay(), raycastRadius);
float[] distances = new float[hits.Length];
for (int i = 0; i < hits.Length; i++)
{
distances[i] = hits[i].distance;
}
Array.Sort(distances, hits);
return hits;
}
private bool InteractWithMovement()
{
Vector3 target;
bool hasHit = RaycastNavMesh(out target);
if (hasHit)
{
Debug.Log($"[PlayerController] Hit NavMesh at {target}");
if (!GetComponent<Mover>().CanMoveTo(target))
{
Debug.Log("[PlayerController] Can't move to that point.");
return false;
}
if (Input.GetMouseButtonDown(0))
{
Debug.Log("[PlayerController] Left click detected. Starting movement.");
GetComponent<Mover>().StartMoveAction(target, 1f);
}
SetCursor(CursorType.Movement);
return true;
}
Debug.Log("[PlayerController] RaycastNavMesh failed.");
return false;
}
private bool RaycastNavMesh(out Vector3 target)
{
target = new Vector3();
RaycastHit hit;
bool hasHit = Physics.Raycast(GetMouseRay(), out hit);
if (!hasHit) return false;
NavMeshHit navMeshHit;
bool hasCastToNavMesh = NavMesh.SamplePosition(
hit.point, out navMeshHit, maxNavMeshProjectionDistance, NavMesh.AllAreas);
if (!hasCastToNavMesh) return false;
target = navMeshHit.position;
return true;
}
private void SetCursor(CursorType type)
{
CursorMapping mapping = GetCursorMapping(type);
Cursor.SetCursor(mapping.texture, mapping.hotspot, CursorMode.Auto);
}
private CursorMapping GetCursorMapping(CursorType type)
{
foreach (CursorMapping mapping in cursorMappings)
{
if (mapping.type == type)
{
return mapping;
}
}
return cursorMappings[0];
}
private static Ray GetMouseRay()
{
return Camera.main.ScreenPointToRay(Input.mousePosition);
}
}
}
It looks like you need to rebake your NavMesh.
Head to the Bake tab and press the Bake button, and the NavMesh should expand to cover all walkable areas.
So, it wasn’t an issue with the nav mesh or the logic. I got the debug “Ray cast did NOT hit anything.” When I simply changed the main camera to look slightly downward on the character and the game world, it worked. The turning and stopping can definitely be fine-tuned, but glad it’s finally working.
