4、鼠标输入系统:鼠标移动、点击、滚轮事件与光标追踪

鼠标,可以说是游戏交互里最「顺手」的设备了。你想想看,从第一人称射击到策略经营,从点击冒险到MOBA,几乎每个游戏类型都离不开它。但说实话,很多新手设计师对鼠标事件的理解,还停留在「点一下按钮」的层面。

我个人习惯把鼠标交互拆成三个维度:位置(光标在哪)、动作(按没按、滚没滚)、状态(持续还是瞬发)。今天咱们就把这三个维度彻底聊透。

4.1 鼠标移动事件:不只是「跟手」那么简单

鼠标移动,听起来最简单对吧?光标跟着鼠标走就行了。但我在项目中遇到过一个问题:玩家快速甩鼠标时,光标会「跳帧」。为什么?因为鼠标移动事件不是每帧都触发的,它取决于系统轮询频率。

来看一段基础代码:

// Unity C# 示例:鼠标移动追踪
void Update()
{
    // 获取鼠标在屏幕上的位置(像素坐标)
    Vector3 mousePos = Input.mousePosition;
    
    // 屏幕坐标转世界坐标(常用于3D游戏)
    Ray ray = Camera.main.ScreenPointToRay(mousePos);
    RaycastHit hit;
    
    if (Physics.Raycast(ray, out hit))
    {
        // 光标悬停到某个物体上
        Debug.Log("光标指向: " + hit.collider.name);
    }
}

这里有个坑:Input.mousePosition 返回的是屏幕像素坐标,左下角是(0,0),右上角是(Screen.width, Screen.height)。如果你直接拿这个坐标去驱动UI,会发现位置对不上——因为UI坐标系通常以屏幕中心为原点。

我的小技巧:做UI悬停效果时,我习惯用 RectTransformUtility.ScreenPointToLocalPointInRectangle 把屏幕坐标转成UI局部坐标。这样不管Canvas怎么缩放,光标位置都准。

4.2 鼠标点击事件:按下、抬起与「点击」的区别

很多新手分不清「按下」和「点击」。我刚开始做游戏时也犯过这个错——把点击逻辑写在 OnMouseDown 里,结果玩家按住鼠标拖拽时也触发了。嗯,这里要注意:

  • 按下(Down):鼠标按键被按下的那一帧触发,只触发一次
  • 抬起(Up):鼠标按键被松开的那一帧触发,只触发一次
  • 点击(Click):按下+抬起,且两次事件之间没有明显移动

看代码:

void Update()
{
    // 左键按下
    if (Input.GetMouseButtonDown(0))
    {
        Debug.Log("左键按下 - 开始拖拽或射击");
    }
    
    // 左键按住不放(持续触发)
    if (Input.GetMouseButton(0))
    {
        Debug.Log("左键按住中 - 持续拖拽");
    }
    
    // 左键抬起
    if (Input.GetMouseButtonUp(0))
    {
        Debug.Log("左键抬起 - 结束拖拽");
    }
}
我曾经踩过的坑:在手机移植PC游戏时,我直接用 Input.GetMouseButtonDown(0) 处理所有点击。结果发现触摸屏上「点击」和「长按」很难区分。后来我加了一个时间阈值:按下超过0.2秒就算长按,否则算点击。这才解决了误触问题。

4.3 滚轮事件:不只是「缩放」

滚轮,很多人只用来做缩放。但你想过没有?滚轮其实是一个「增量输入」设备。它不告诉你「现在滚到第几格」,而是告诉你「这次滚了多少距离」。

滚轮属性 说明 典型值范围
Input.GetAxis("Mouse ScrollWheel") 返回滚轮增量值 -0.1 到 0.1(每格)
滚动速度 取决于系统设置 可自定义乘数

代码示例:

void Update()
{
    float scroll = Input.GetAxis("Mouse ScrollWheel");
    
    if (scroll != 0)
    {
        // 滚轮缩放相机
        Camera.main.fieldOfView -= scroll * 10;
        Camera.main.fieldOfView = Mathf.Clamp(Camera.main.fieldOfView, 20, 100);
        
        // 或者切换武器(滚轮每格切换一次)
        if (scroll > 0) SwitchWeapon(1);   // 向上滚
        if (scroll < 0) SwitchWeapon(-1);  // 向下滚
    }
}

这里有个细节:GetAxis 返回的是浮点数,不是整数。你滚一格,它可能返回0.1或0.05,取决于系统设置。所以我一般会加一个死区判断:

if (Mathf.Abs(scroll) > 0.01f)  // 忽略微小抖动
{
    // 处理滚轮事件
}

4.4 光标追踪:隐藏、锁定与自定义

光标追踪不只是「知道光标在哪」,还包括「怎么显示光标」。FPS游戏里通常要隐藏光标并锁定到屏幕中心,而策略游戏则需要显示光标并允许自由移动。

看这几种模式:

  • Cursor.lockState = CursorLockMode.None:光标自由移动(默认)
  • Cursor.lockState = CursorLockMode.Locked:光标锁定到屏幕中心,适合FPS
  • Cursor.lockState = CursorLockMode.Confined:光标限制在窗口内,适合全屏游戏

代码示例:

void Start()
{
    // 进入FPS模式:隐藏光标并锁定
    Cursor.visible = false;
    Cursor.lockState = CursorLockMode.Locked;
}

void Update()
{
    // 按ESC解锁光标
    if (Input.GetKeyDown(KeyCode.Escape))
    {
        Cursor.visible = true;
        Cursor.lockState = CursorLockMode.None;
    }
    
    // 获取鼠标移动增量(用于视角旋转)
    float mouseX = Input.GetAxis("Mouse X");
    float mouseY = Input.GetAxis("Mouse Y");
    
    // 旋转相机
    transform.Rotate(-mouseY * sensitivity, mouseX * sensitivity, 0);
}
核心要点:光标锁定模式下,Input.mousePosition 不会变化(始终在屏幕中心)。此时要用 Input.GetAxis("Mouse X/Y") 获取移动增量。这个坑我见过太多人踩了。

4.5 实战:一个完整的鼠标交互系统

最后,我把上面所有知识点整合成一个完整的鼠标输入管理器。你可以直接拿去用:

public class MouseInputManager : MonoBehaviour
{
    [Header("鼠标设置")]
    public float scrollSensitivity = 10f;
    public float mouseSensitivity = 2f;
    
    [Header("光标状态")]
    public bool cursorLocked = true;
    
    void Update()
    {
        // 1. 光标锁定切换
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            cursorLocked = !cursorLocked;
            Cursor.visible = !cursorLocked;
            Cursor.lockState = cursorLocked ? 
                CursorLockMode.Locked : CursorLockMode.None;
        }
        
        // 2. 鼠标点击事件
        if (Input.GetMouseButtonDown(0))
            OnLeftClick();
        if (Input.GetMouseButtonDown(1))
            OnRightClick();
            
        // 3. 滚轮事件
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if (Mathf.Abs(scroll) > 0.01f)
            OnScroll(scroll);
            
        // 4. 鼠标移动(仅锁定模式下)
        if (cursorLocked)
        {
            float dx = Input.GetAxis("Mouse X") * mouseSensitivity;
            float dy = Input.GetAxis("Mouse Y") * mouseSensitivity;
            OnMouseMove(dx, dy);
        }
    }
    
    void OnLeftClick()
    {
        // 发射子弹、选择单位等
        Debug.Log("左键点击");
    }
    
    void OnRightClick()
    {
        // 移动指令、取消操作等
        Debug.Log("右键点击");
    }
    
    void OnScroll(float delta)
    {
        // 缩放、切换武器等
        Debug.Log("滚轮: " + delta);
    }
    
    void OnMouseMove(float dx, float dy)
    {
        // 旋转视角
        transform.Rotate(-dy, dx, 0);
    }
}

这个系统我用了好几年,从PC端游到手游模拟器,基本没出过问题。你拿过去改改参数就能用。

好了,鼠标输入系统就聊到这儿。下一章咱们讲键盘输入——那个看似简单实则暗藏玄机的输入设备。到时候我会分享一个「按键冲突」的经典案例,保证让你大开眼界。