Optimizing Performance for Your MouseController in Game Development

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

  1. Unity version: recommended 2020.3 LTS or newer (Input APIs used here work on later versions; adjust if using the new Input System).
  2. Create a new 3D project.
  3. Create folders: Scripts, Prefabs, Materials.
  4. 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.

Comments

Leave a Reply

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