Unity Top-Down Movement and Mouse Aiming (2D and 3D)

Illustration: a top-down ship rotating toward a target reticle with laser lines

Twin-stick shooters, action RPGs and survival games all share the same core: move in one direction while aiming in another. Getting it right means framerate-independent physics movement, aiming that works with both a mouse and a gamepad, and projectiles that go where the player expects. This guide to Unity top-down movement and mouse aiming builds all three in Unity 6 for both 2D (Rigidbody2D) and 3D (Rigidbody), plus the tweaks that make the controls feel tight.

Input Actions for Move and Aim

Create an Input Actions asset (or use Unity 6’s default InputSystem_Actions) with a Player map containing:

  • Move: Value, Vector2, WASD composite and Left Stick
  • Aim: Value, Vector2, Right Stick only
  • Point: Value, Vector2, bound to <Pointer>/position (mouse position in screen pixels)
  • Fire: Button, Left Mouse Button and Right Trigger

Mouse aim and stick aim are fundamentally different. The mouse gives a position on screen that you convert to a world point; the stick gives a direction directly. Keeping them as separate actions makes that easy to handle. If you haven’t used action assets yet, see our Input System beginner’s guide.

To decide which to use, remember the last device that produced aim input. If the right stick moved beyond a dead zone, use stick aim until the mouse moves again.

2D Movement With Rigidbody2D

Set up the player with a Rigidbody2D (Body Type Dynamic, Gravity Scale 0, Freeze Rotation Z ticked, Interpolate set to Interpolate) and a CircleCollider2D. Read input in Update, apply it in FixedUpdate:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody2D))]
public class TopDownMover2D : MonoBehaviour
{
    [SerializeField] InputActionReference moveAction;
    [SerializeField] float maxSpeed = 7f;
    [SerializeField] float acceleration = 60f;
    [SerializeField] float deceleration = 80f;

    Rigidbody2D rb;
    Vector2 input;

    void Awake() => rb = GetComponent<Rigidbody2D>();
    void OnEnable() => moveAction.action.Enable();

    void Update()
    {
        input = Vector2.ClampMagnitude(moveAction.action.ReadValue<Vector2>(), 1f);
    }

    void FixedUpdate()
    {
        Vector2 target = input * maxSpeed;
        float rate = input.sqrMagnitude > 0.01f ? acceleration : deceleration;
        rb.linearVelocity = Vector2.MoveTowards(rb.linearVelocity, target, rate * Time.fixedDeltaTime);
    }
}

Three details make this better than the usual transform.position += approach:

  • It moves through physics, so the player can’t tunnel through walls and slides along them properly.
  • ClampMagnitude stops diagonal keyboard input (1, 1) being 41% faster than straight movement, while keeping analogue stick values below 1 for slow walking.
  • Separate acceleration and deceleration lets you tune snappy stops independently of start-up. High values (60+) feel arcade-tight; low values feel icy.

Interpolation is what removes the subtle stutter you see when the camera follows a physics body. If you still see jitter, our Rigidbody jitter fix covers every cause.

2D Aiming at the Mouse or Right Stick

For mouse aim, convert the pointer’s screen position into world space and get the direction from the player. With an orthographic camera, ScreenToWorldPoint works directly. Then rotate the player (or just a weapon pivot) with Mathf.Atan2:

using UnityEngine;
using UnityEngine.InputSystem;

public class TopDownAim2D : MonoBehaviour
{
    [SerializeField] InputActionReference pointAction, aimAction;
    [SerializeField] Transform aimPivot;            // weapon or body
    [SerializeField] float stickDeadZone = 0.25f;

    Camera cam;
    bool usingStick;
    Vector2 lastPointer;
    public Vector2 AimDirection { get; private set; } = Vector2.right;

    void Awake() => cam = Camera.main;

    void OnEnable()
    {
        pointAction.action.Enable();
        aimAction.action.Enable();
    }

    void Update()
    {
        Vector2 stick = aimAction.action.ReadValue<Vector2>();
        Vector2 pointer = pointAction.action.ReadValue<Vector2>();

        if (stick.magnitude > stickDeadZone) usingStick = true;
        else if ((pointer - lastPointer).sqrMagnitude > 4f) usingStick = false;
        lastPointer = pointer;

        if (usingStick)
        {
            if (stick.magnitude > stickDeadZone) AimDirection = stick.normalized;
        }
        else
        {
            Vector3 world = cam.ScreenToWorldPoint(pointer);
            Vector2 dir = (Vector2)world - (Vector2)aimPivot.position;
            if (dir.sqrMagnitude > 0.0001f) AimDirection = dir.normalized;
        }

        float angle = Mathf.Atan2(AimDirection.y, AimDirection.x) * Mathf.Rad2Deg;
        aimPivot.rotation = Quaternion.Euler(0f, 0f, angle);
    }
}

This assumes the sprite faces right at zero rotation, which is Unity’s convention for transform.right. If your art faces up, subtract 90 from the angle. Keeping the last direction when the stick is released stops the character snapping back to the right.

A common choice is to rotate only a weapon pivot and flip the body sprite left or right based on AimDirection.x. That way pixel-art characters never appear upside down.

3D Movement and Aiming on a Ground Plane

In 3D, map input X/Y onto world X/Z. If the camera is rotated (say 45 degrees for an isometric look), convert input relative to the camera’s flattened forward so “up” on the stick always moves up the screen:

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody))]
public class TopDownController3D : MonoBehaviour
{
    [SerializeField] InputActionReference moveAction, pointAction, aimAction;
    [SerializeField] float maxSpeed = 7f, acceleration = 60f;
    [SerializeField] float turnSpeed = 1080f;   // degrees per second

    Rigidbody rb;
    Camera cam;
    Vector3 moveDir, aimDir = Vector3.forward;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        rb.constraints = RigidbodyConstraints.FreezeRotation;
        rb.interpolation = RigidbodyInterpolation.Interpolate;
        cam = Camera.main;
    }

    void OnEnable()
    {
        moveAction.action.Enable(); pointAction.action.Enable(); aimAction.action.Enable();
    }

    void Update()
    {
        Vector3 camForward = Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up).normalized;
        Vector3 camRight = Vector3.ProjectOnPlane(cam.transform.right, Vector3.up).normalized;

        Vector2 move = Vector2.ClampMagnitude(moveAction.action.ReadValue<Vector2>(), 1f);
        moveDir = camRight * move.x + camForward * move.y;

        Vector2 stick = aimAction.action.ReadValue<Vector2>();
        if (stick.magnitude > 0.25f)
        {
            aimDir = (camRight * stick.x + camForward * stick.y).normalized;
        }
        else
        {
            Ray ray = cam.ScreenPointToRay(pointAction.action.ReadValue<Vector2>());
            Plane ground = new Plane(Vector3.up, transform.position);
            if (ground.Raycast(ray, out float dist))
            {
                Vector3 flat = ray.GetPoint(dist) - transform.position;
                flat.y = 0f;
                if (flat.sqrMagnitude > 0.01f) aimDir = flat.normalized;
            }
        }
    }

    void FixedUpdate()
    {
        Vector3 target = moveDir * maxSpeed;
        Vector3 vel = rb.linearVelocity;
        Vector3 flatVel = Vector3.MoveTowards(new Vector3(vel.x, 0f, vel.z), target, acceleration * Time.fixedDeltaTime);
        rb.linearVelocity = new Vector3(flatVel.x, vel.y, flatVel.z);

        Quaternion look = Quaternion.LookRotation(aimDir, Vector3.up);
        rb.MoveRotation(Quaternion.RotateTowards(rb.rotation, look, turnSpeed * Time.fixedDeltaTime));
    }
}

The key trick is the mathematical plane at the player’s height. Raycasting against it is cheaper than a physics raycast and never misses because the cursor is over a gap or a wall top. It also aims correctly when the player stands on a raised platform. If you’d rather aim at whatever the cursor touches (for clicking enemies), use a physics raycast with a ground layer mask instead; see our raycast tutorial.

Keeping vertical velocity untouched (vel.y) means gravity still works on slopes and when falling off ledges.

Shooting Projectiles

Spawn bullets at a muzzle point and launch them along the aim direction. This 2D version works the same way in 3D with Rigidbody and Vector3:

using UnityEngine;
using UnityEngine.InputSystem;

public class TopDownGun2D : MonoBehaviour
{
    [SerializeField] InputActionReference fireAction;
    [SerializeField] TopDownAim2D aim;
    [SerializeField] Transform muzzle;
    [SerializeField] Rigidbody2D bulletPrefab;
    [SerializeField] float bulletSpeed = 20f;
    [SerializeField] float fireRate = 10f;          // shots per second
    [SerializeField] float spreadDegrees = 3f;

    float nextShot;

    void OnEnable() => fireAction.action.Enable();

    void Update()
    {
        if (!fireAction.action.IsPressed() || Time.time < nextShot) return;
        nextShot = Time.time + 1f / fireRate;

        float spread = Random.Range(-spreadDegrees, spreadDegrees);
        Vector2 dir = Quaternion.Euler(0f, 0f, spread) * aim.AimDirection;

        Rigidbody2D bullet = Instantiate(bulletPrefab, muzzle.position,
            Quaternion.Euler(0f, 0f, Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg));
        bullet.linearVelocity = dir * bulletSpeed;
    }
}

Give the bullet prefab a Rigidbody2D with Gravity Scale 0, Collision Detection set to Continuous (fast objects otherwise pass through thin walls), and a script that destroys it on hit or after a couple of seconds. Put bullets on their own layer and stop them colliding with the player in the collision matrix. For any game that fires more than a few shots a second, replace Instantiate and Destroy with a pool; our object pooling guide uses this exact case.

✨ Building a shooter on a deadline?

Bullet pattern tools, feedback packs and top-down art kits can get a prototype playable in an afternoon. Here are the Asset Store tools we think are worth it.

See our Asset Store picks →

Making It Feel Tight

  • High acceleration, higher deceleration. Players forgive a fraction of a second of wind-up, but sliding after letting go feels like ice.
  • Aim the muzzle, not the centre. Compute the mouse direction from the muzzle (or pivot) position, otherwise bullets miss the cursor when it’s close to the player.
  • Camera lead. Offset the camera slightly towards the aim direction (or halfway to the cursor) so the player sees more of where they’re shooting.
  • Feedback on every shot. A small recoil kick, a muzzle flash and a short sound do more for feel than any speed tweak.
  • Stick dead zones. Use a radial dead zone around 0.2 to 0.25 on the aim stick so the character doesn’t twitch when the stick rests slightly off-centre.
  • Don’t slow movement while firing unless it’s a deliberate design choice. Most twin-stick games keep full speed.

Troubleshooting

Aim is offset from the cursor in 3D

You’re using ScreenToWorldPoint with a perspective camera, which returns a point on the near clip plane. Use ScreenPointToRay and a ground plane instead.

The character spins or tips over when hitting walls

Rotation isn’t frozen. Freeze rotation on the Rigidbody (Z in 2D, all axes in 3D) and rotate through MoveRotation.

Diagonal movement is faster

Clamp the input magnitude to 1 with Vector2.ClampMagnitude, or set the WASD composite’s Mode to Digital Normalized.

Bullets go through walls

Set the bullet’s Collision Detection to Continuous, and make sure walls have colliders on a layer bullets can hit.

Gamepad aim snaps back to the right when released

Only update the aim direction when the stick is outside the dead zone, so the last direction is kept.

FAQ

Should top-down movement use Rigidbody or transform?

Rigidbody (setting linearVelocity) so collisions work properly. Moving the transform directly makes the player push into walls and jitter.

How do I make the player face the mouse in 2D?

Convert the pointer to world space with ScreenToWorldPoint, take the direction from the player, and set the rotation to Atan2 of that direction in degrees.

How do I support both mouse and gamepad aiming?

Read both, and switch to whichever produced input most recently: stick beyond its dead zone, or the mouse moving more than a few pixels.

Why use a Plane instead of Physics.Raycast for 3D aiming?

A Plane at player height never misses and aims at the right height. Use Physics.Raycast only when you need to know what the cursor is over.

What fire rate feels good for a twin-stick shooter?

Somewhere between 6 and 12 shots per second for a basic gun, with a little random spread so streams of bullets look alive.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *