How to implement gravity for Unity NPC movement system

I’m working on a city building game where NPCs walk around randomly. Right now my citizens move to random spots, pause for a couple seconds, then pick new destinations. The problem is they keep floating in the air instead of staying on the ground.

I tried adding these gravity lines but strange bugs happen like NPCs jumping up when they hit objects or terrain:

npcController = GetComponent<CharacterController>();
movementDir = Vector3.zero;
movementDir.y -= gravityForce * Time.deltaTime;
npcController.Move(movementDir * Time.deltaTime);

The NPCs teleport upward whenever they touch anything including the ground surface. How can I fix this? I just want them to walk normally on slopes and stay grounded instead of floating.

Here’s my complete NPC script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Citizen : MonoBehaviour
{
private Vector3 targetPos;
private Quaternion targetRot;
private int walkSpeed;
private Vector3 movementDir = Vector3.zero;
private bool allowRotation = true;

Vector3 moveVec;
CharacterController npcController;

void Start()
{
    transform.rotation *= Quaternion.Euler(-90, 0, 0);
    npcController = GetComponent<CharacterController>();
    walkSpeed = 5;
    PickNewDestination();
    StartCoroutine(WaitRoutine());
}

void Update()
{
    int gravityForce = 20;
    movementDir = Vector3.zero;
    
    movementDir.y -= gravityForce * Time.deltaTime;
    npcController.Move(movementDir * Time.deltaTime);
    
    float stepSize = walkSpeed * Time.deltaTime;
    transform.position = Vector3.MoveTowards(transform.position, targetPos, stepSize);
    
    if (allowRotation)
    {
        transform.LookAt(targetPos);
        transform.rotation *= Quaternion.Euler(-90, 90, 0);
        allowRotation = false;
    }
    
    if (transform.position == targetPos)
    {
        StartCoroutine(WaitRoutine());
    }
}

void PickNewDestination()
{
    targetPos = new Vector3(Random.Range(transform.position.x - 10f, transform.position.x + 10f), transform.position.y, Random.Range(transform.position.z - 10f, transform.position.z + 10f));
    allowRotation = true;
}

IEnumerator WaitRoutine()
{
    yield return new WaitForSeconds(2);
    
    if (transform.position == targetPos)
    {
        PickNewDestination();
    }
}
}

Your gravity’s broken because you’re zeroing out movementDir every frame before applying gravity. Gravity can’t build up that way. The real problem is mixing Vector3.MoveTowards on the transform with CharacterController.Move. They fight each other - MoveTowards completely bypasses the CharacterController’s collision detection. Try this instead: csharp private float yVelocity = 0f; void Update() { Vector3 horizontalMovement = Vector3.zero; if (Vector3.Distance(transform.position, targetPos) > 0.1f) { Vector3 direction = (targetPos - transform.position).normalized; direction.y = 0; horizontalMovement = direction * walkSpeed; } if (npcController.isGrounded && yVelocity < 0) yVelocity = 0f; else yVelocity -= 20f * Time.deltaTime; Vector3 movement = horizontalMovement + Vector3.up * yVelocity; npcController.Move(movement * Time.deltaTime); } This keeps the vertical velocity persistent and lets the CharacterController handle all collisions. Your NPCs should stick to the ground and move smoothly without teleporting around.

Your code has a major conflict - you’re calling npcController.Move() and then immediately overriding it with Vector3.MoveTowards() on the transform. The CharacterController handles physics-based movement, but then you’re directly changing the transform position, which completely bypasses collision detection.

That’s why you’re getting the jumping. The CharacterController detects collisions and tries to fix them, but MoveTowards just forces the position change anyway.

Ditch the MoveTowards line completely. Handle everything through the CharacterController instead. Calculate your horizontal direction to the target and combine it with gravity in one movement vector.

Also, stop resetting movementDir to zero every update - that’s killing your gravity. Keep a separate vertical velocity variable that builds up over time when you’re not grounded. This’ll give you proper ground detection and smooth slope movement without the teleporting mess.

The problem is you’re mixing Vector3.MoveTowards with CharacterController.Move - they’re fighting each other.

MoveTowards directly sets transform position and ignores physics. CharacterController.Move handles physics movement. You can’t use both.

Here’s what happens: CharacterController applies gravity, then MoveTowards teleports the NPC somewhere else, causing that jumping.

Ditch MoveTowards. Calculate direction to target and stick with CharacterController.Move:

void Update()
{
    Vector3 direction = (targetPos - transform.position).normalized;
    direction.y = 0; // Keep horizontal movement separate
    
    movementDir = direction * walkSpeed;
    movementDir.y -= gravityForce * Time.deltaTime;
    
    npcController.Move(movementDir * Time.deltaTime);
}

Also accumulate gravity velocity instead of resetting it:

private float verticalVelocity = 0f;

void Update()
{
    if (npcController.isGrounded)
        verticalVelocity = 0f;
    else
        verticalVelocity -= gravityForce * Time.deltaTime;
        
    movementDir.y = verticalVelocity;
}

Honestly though, managing NPC behavior gets messy quick. I’ve hit this exact issue on multiple projects. What works way better is automating the whole NPC system externally.

I use Latenode to handle pathfinding decisions, destination picking, and behavior states. It sends commands to Unity through webhooks. Much cleaner than managing everything in Update loops.