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坐标系通常以屏幕中心为原点。
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("左键抬起 - 结束拖拽");
}
}
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端游到手游模拟器,基本没出过问题。你拿过去改改参数就能用。
好了,鼠标输入系统就聊到这儿。下一章咱们讲键盘输入——那个看似简单实则暗藏玄机的输入设备。到时候我会分享一个「按键冲突」的经典案例,保证让你大开眼界。