Building a Custom MouseController in Unity — Step-by-StepA robust MouseController gives you precise, responsive mouse input handling tailored to your game. This guide walks through building a reusable MouseController in Unity (C#), from basics to advanced features: cursor locking, smoothing, customizable sensitivity, drag detection, raycasting for object interaction, and editor-friendly configuration. Code samples are provided and explained step‑by‑step so you can adapt the controller to first‑person, top‑down, or UI-driven games.
What this article covers
- Project setup and design goals
- Core MouseController features and architecture
- Implementation: stepwise C# scripts with explanations
- Common use cases (FPS camera, object selection, drag & drop)
- Advanced topics: smoothing, input buffering, multi-monitor/capture issues
- Editor integration and testing tips
Design goals and considerations
A good MouseController should be:
- Modular — usable across different scenes and projects.
- Configurable — sensitivity, smoothing, axis inversion, filtering.
- Performant — minimal allocations, using Update vs. FixedUpdate appropriately.
- Accurate — correct handling of cursor lock, raw delta, and DPI differences.
- User-friendly — clear API and inspector settings for designers.
We’ll implement a MouseController class focused on pointer delta handling, cursor state management, raycasting utilities, and events for other systems to subscribe to.
Project setup
- Unity version: recommended 2020.3 LTS or newer (Input APIs used here work on later versions; adjust if using the new Input System).
- Create a new 3D project.
- Create folders: Scripts, Prefabs, Materials.
- In Scripts, create MouseController.cs and example scripts (MouseLook.cs, MousePicker.cs).
Core architecture
We’ll split responsibilities:
- MouseController: exposes raw/smoothed delta, cursor lock state, sensitivity, and events (OnMove, OnClick, OnDrag).
- MouseLook: subscribes to MouseController and applies rotation to camera/character.
- MousePicker: performs raycasts using the MouseController pointer and raises selection events.
This separation keeps input handling decoupled from game logic.
MouseController API design
Public properties and events (example):
- float Sensitivity { get; set; }
- float Smoothing { get; set; }
- bool InvertY { get; set; }
- Vector2 RawDelta { get; }
- Vector2 SmoothedDelta { get; }
- bool IsCursorLocked { get; set; }
- event Action
OnMove; - event Action
OnClick; - event Action
OnDrag;
Pointer data structs:
public struct PointerClickData { public Vector2 screenPosition; public int button; // 0 left, 1 right, 2 middle public float time; } public struct PointerDragData { public Vector2 startScreenPosition; public Vector2 currentScreenPosition; public int button; public float duration; }
Implementation: MouseController.cs
Below is a step‑by‑step implementation suitable for the built‑in Input system (Input.GetAxis / GetAxisRaw / GetMouseButton). If you use the new Input System, mapping is similar but use InputAction callbacks.
using System; using UnityEngine; public class MouseController : MonoBehaviour { [Header("General")] public float sensitivity = 1.0f; [Tooltip("Higher values = smoother (more lag). 0 = raw input")] [Range(0f, 1f)] public float smoothing = 0.1f; public bool invertY = false; [Header("Cursor")] public bool lockCursor = true; public bool showCursorWhenUnlocked = true; public Vector2 RawDelta { get; private set; } public Vector2 SmoothedDelta { get; private set; } public bool IsCursorLocked => Cursor.lockState == CursorLockMode.Locked; public event Action<Vector2> OnMove; public event Action<PointerClickData> OnClick; public event Action<PointerDragData> OnDrag; // Internal Vector2 velocity; Vector2 dragStartPos; bool dragging; int dragButton; void Start() { ApplyCursorState(); } void Update() { ReadDelta(); HandleCursorToggle(); HandleClicksAndDrags(); OnMove?.Invoke(SmoothedDelta); } void ReadDelta() { // Use GetAxisRaw for unfiltered deltas; multiply by sensitivity and Time.deltaTime when applying to rotations. float dx = Input.GetAxisRaw("Mouse X"); float dy = Input.GetAxisRaw("Mouse Y"); RawDelta = new Vector2(dx, dy); // Optionally invert Y if (invertY) RawDelta.y = -RawDelta.y; // Apply sensitivity Vector2 scaled = RawDelta * sensitivity; // Smooth: simple exponential smoothing (lerp towards new delta) SmoothedDelta = Vector2.SmoothDamp(SmoothedDelta, scaled, ref velocity, smoothing); } void HandleCursorToggle() { if (Input.GetKeyDown(KeyCode.Escape)) { lockCursor = false; ApplyCursorState(); } if (lockCursor && !IsCursorLocked) ApplyCursorState(); } void ApplyCursorState() { if (lockCursor) { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } else { Cursor.lockState = CursorLockMode.None; Cursor.visible = showCursorWhenUnlocked; } } void HandleClicksAndDrags() { for (int b = 0; b < 3; b++) { if (Input.GetMouseButtonDown(b)) { var cd = new PointerClickData { screenPosition = Input.mousePosition, button = b, time = Time.time }; OnClick?.Invoke(cd); // Begin potential drag dragging = true; dragButton = b; dragStartPos = Input.mousePosition; } if (Input.GetMouseButtonUp(b)) { if (dragging && dragButton == b) { dragging = false; } } } if (dragging) { var dd = new PointerDragData { startScreenPosition = dragStartPos, currentScreenPosition = Input.mousePosition, button = dragButton, duration = Time.time - Time.time // placeholder if tracking start time separately }; OnDrag?.Invoke(dd); } } }
Notes:
- Smoothing implementation uses SmoothDamp to reduce jerky motion; set smoothing to 0 for raw deltas.
- Sensitivity scales raw delta — multiply by Time.deltaTime when applying to rotation to keep frame-rate independence.
- For FPS controllers, use GetAxisRaw for responsiveness; for UI, Input.mousePosition is typically used.
MouseLook example (first‑person camera)
This script subscribes to MouseController.OnMove and applies rotation:
using UnityEngine; [RequireComponent(typeof(MouseController))] public class MouseLook : MonoBehaviour { public Transform cameraTransform; public float verticalLimit = 89f; MouseController mouse; float pitch = 0f; // x rotation float yaw = 0f; // y rotation void Awake() { mouse = GetComponent<MouseController>(); if (cameraTransform == null) cameraTransform = Camera.main.transform; yaw = transform.eulerAngles.y; pitch = cameraTransform.localEulerAngles.x; } void OnEnable() { mouse.OnMove += HandleMouseMove; } void OnDisable() { mouse.OnMove -= HandleMouseMove; } void HandleMouseMove(Vector2 delta) { // delta is in 'mouse units' per frame - multiply by Time.deltaTime if you want time-based motion yaw += delta.x; pitch += -delta.y; // invert handled earlier if desired pitch = Mathf.Clamp(pitch, -verticalLimit, verticalLimit); transform.rotation = Quaternion.Euler(0f, yaw, 0f); cameraTransform.localRotation = Quaternion.Euler(pitch, 0f, 0f); } }
Tip: If using sensitivity that already accounts for frame time, don’t multiply by Time.deltaTime here; otherwise multiply delta by Time.deltaTime to maintain consistent rotation across frame rates.
MousePicker example (raycasting / object selection)
A simple picker that casts rays from screen position:
using UnityEngine; using System; [RequireComponent(typeof(MouseController))] public class MousePicker : MonoBehaviour { public LayerMask pickMask = ~0; public float maxDistance = 100f; MouseController mouse; public event Action<GameObject> OnHover; public event Action<GameObject> OnClickObject; void Awake() { mouse = GetComponent<MouseController>(); } void Update() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, pickMask)) { OnHover?.Invoke(hit.collider.gameObject); if (Input.GetMouseButtonDown(0)) { OnClickObject?.Invoke(hit.collider.gameObject); } } else { OnHover?.Invoke(null); } } }
Drag detection improvements
The simple drag code above is minimal. For production:
- Track button start time and position separately per button.
- Implement a deadzone threshold (e.g., 5–10 pixels) before reporting a drag to avoid false positives.
- Support gesture cancellation (e.g., when cursor locks or window loses focus).
- For world-space dragging, translate screen delta into world movement with Camera.ScreenToWorldPoint or ray-plane intersection.
Handling DPI and high-resolution mice
- Windows and other OSes can scale cursor movement. To get the most accurate raw deltas, the new Input System or native plugins may be needed.
- For most games, using GetAxisRaw combined with a well-chosen sensitivity gives acceptable results. If you target professional mice (high DPI) consider exposing sensitivity in both in-game units and DPI-aware multipliers.
Multimonitor and capture behavior
- When cursor locks, Unity captures movement even if the OS cursor is outside the window. This is desirable for FPS.
- On focus loss, pause input or release cursor to prevent runaway behavior. Use OnApplicationFocus and OnApplicationPause to handle those cases.
Performance notes
- Keep per-frame allocations to zero: avoid creating new structs/objects inside Update for frequent events if subscribers are many. Reuse objects or use pooled event data if necessary.
- Use LayerMask and distance checks to reduce raycast overhead.
- For UI interactions, prefer Unity UI event system with GraphicRaycaster instead of Physics.Raycast where applicable.
Editor-friendly configuration
- Expose sensitivity, smoothing, invertY, and cursor settings in the inspector with helpful tooltips.
- Add a custom editor (Editor folder) to preview smoothed delta in play mode and test sensitivity live.
- Provide prefabs with the MouseController and example MouseLook and MousePicker components preset for common setups.
Testing checklist
- Test on different frame rates (30, 60, 144+ FPS).
- Test with vsync on/off and windowed/fullscreen.
- Test cursor lock/unlock, alt‑tab behavior, and multiple monitors.
- Test with different input devices (trackpad, mouse, tablet) to ensure parameters are sensible.
Extensions and next steps
- Integrate with Unity’s new Input System for better device handling and remapping.
- Add gesture recognition (double‑click, flick, two‑finger drag on touchpads) if needed.
- Add network-safe input abstraction for multiplayer (authoritative server-side validation).
- Implement an InputDebugger overlay to visualize raw vs. smoothed deltas and event timings.
Example project structure
- Scripts/
- MouseController.cs
- MouseLook.cs
- MousePicker.cs
- InputDebugger.cs
- Prefabs/
- MouseControllerPrefab (configured for FPS)
- Scenes/
- Demo_FPS.unity
- Demo_TopDown.unity
Building a custom MouseController gives you control over input fidelity and user experience. The code here is a practical, extensible foundation you can adapt to many interaction models in Unity.
Leave a Reply