
我面试Unity岗位的时候特别爱问一个问题Input.GetKeyDown(KeyCode.Space)这一行代码到底是“每帧去查一次按键状态”还是“引擎主动通知你按下了一次”十个人里有六七个答不上来。剩下几个能答出“轮询”的再往下问一句“那为什么不能在 FixedUpdate 里读”又开始含糊。这篇就把 Unity 的 Input 输入这件事从头到尾捋一遍。输入这件事看着基础但它决定了游戏手感的底层逻辑——移动、跳跃、视角、UI 交互、跨平台适配全要从这里出发。内容会覆盖旧版输入管理器Input Manager、新版输入系统Input System、UI 输入限制、鼠标坐标换算、滑动条、触摸输入、串口/PLC 这类外部输入设备的接入思路以及一堆经典排查坑。适合刚接触 Unity 的初学者也适合想从旧输入系统迁到新输入系统、或者一直被“按键没反应”“UI 点击穿透”折磨的开发者。1. 管好 Input 类你写的每个 GetKey背后都是轮询1.1 Input 类到底提供哪几类输入 APIUnity 的旧输入系统核心就一个Input静态类。它的 API 看起来杂实际可以分成三类键盘鼠标类、输入轴类、触摸类。键盘鼠标类最常见就是GetKey、GetKeyDown、GetKeyUp和GetMouseButton、GetMouseButtonDown、GetMouseButtonUp。看名字就能猜出来带 Down 的是“按下那一瞬间”触发一次带 Up 的是“松开那一瞬间”触发一次不带后缀的是“按住期间每帧都返回 true”。触摸类主要是Input.touchCount和Input.GetTouch(index)。移动端项目绕不开这两兄弟后面聊到平台差异时会详细展开。输入轴类是很多人一开始没搞明白的也就是GetAxis(Horizontal)这种写法。它不是直接查某个键而是查一条“虚拟轴”的当前值返回的是 -1 到 1 之间的浮点数。这个设计很巧妙后面单独说。先看一段典型代码把这些 API 放一起感受一下void Update() { // 按下空格键的瞬间触发一次 if (Input.GetKeyDown(KeyCode.Space)) { Debug.Log(空格按下); } // 鼠标左键按下瞬间触发 if (Input.GetMouseButtonDown(0)) { Debug.Log(左键点击); } // 第一根手指触摸屏幕的瞬间 if (Input.touchCount 0 Input.GetTouch(0).phase TouchPhase.Began) { Debug.Log(第一根手指落下); } // 读取水平输入轴返回 -1 到 1 float h Input.GetAxis(Horizontal); transform.Translate(new Vector3(h * Time.deltaTime * speed, 0, 0)); }1.2 为什么 GetKeyDown 必须放在 Update 里这是很多人踩过的坑把Input.GetKeyDown写进FixedUpdate结果发现按键偶尔失灵特别在帧率不稳的时候。原因是这样的。旧输入系统本质是“状态查询”引擎在每一帧渲染之前把当前所有输入设备的状态采样并缓存好。你在 Update 里读到的是这一帧开始时的快照。GetKeyDown判断的是“这一帧按键状态是按下上一帧是抬起”也就是一个边沿检测。FixedUpdate的执行频率是固定的默认每秒 50 次和帧率并不同步。如果某一帧渲染的间隔里夹了好几次物理更新或者物理更新之间隔了两帧渲染那么按键按下的那一帧可能根本没有对应的FixedUpdate被执行边沿就被跳过了。反过来把输入逻辑放在 Update 里每一帧渲染前都会检查一次状态几乎不会漏。这也是为什么 Unity 官方推荐所有输入读取都放 Update物理相关计算才放 FixedUpdate。下面这个对比很直观如果你的项目目标是 30 帧但物理更新是 50Hz那么 FixedUpdate 和 Update 的真实执行顺序是完全错开的。你永远无法保证“按下”和“物理更新”恰好对上。1.3 输入轴把键盘和摇杆统一成一个 -1 到 1 的值默认情况下Project Settings 的 Input Manager 里已经配好了几条虚拟轴最常用的是 Horizontal 和 Vertical。Horizontal 默认绑定的是键盘 A/D 和左右方向键。按 D 或右方向键轴值往正方向走最大到 1按 A 或左方向键轴值往负方向走最小到 -1。不按键时回到 0。你代码里写Input.GetAxis(Horizontal)拿到的就是这个合成后的结果。它不等于某个物理按键的状态而是“所有绑定到这个轴上的输入源”的综合结果。这个设计的价值在于同一套移动代码接键盘、接手柄、接触摸摇杆都不用改。GetAxis和GetAxisRaw的差别也在这里。GetAxis带平滑过渡按下按键后数值不是瞬间跳到 1而是按配置的灵敏度慢慢走过去松开后也按重力慢慢归零。GetAxisRaw不做任何平滑按下立刻 1松开立刻 0。做像素级移动、菜单选择这类需要精确响应的场景用 GetAxisRaw 更合适做角色行走这类需要起停过渡的效果用 GetAxis 手感更好。2. 旧版 Input Manager虚拟轴的参数帮你把手感调顺2.1 找到并读懂 Input Manager 面板在 Unity 里打开 Edit Project Settings找到 Input Manager点进去会看到两个标签页一个是“输入管理器旧”一个是“输入系统包”。如果你还没装新输入系统只会看到旧的这个装了之后才会两个并存。旧标签页里最核心的是 Axes 列表里面列着所有虚拟轴包括 Horizontal、Vertical、Fire1、Fire2、Mouse X、Mouse Y、Mouse ScrollWheel 这些。点击任意一个轴下方会显示它的全部参数。很多新手看到这一大串参数直接头皮发麻其实真正影响日常开发的也就几个。2.2 轴参数逐项说明参数作用说明Name轴名称代码里GetAxis(名字)用的就是它改名会导致所有引用失效Negative Button负方向按键比如 Horizontal 的 A、LeftArrowPositive Button正方向按键比如 Horizontal 的 D、RightArrowAlt Negative Button备用负方向按键给同一根轴加第二套按键比如手柄十字键Alt Positive Button备用正方向按键同上Gravity归零速度松开按键后数值以这个速度每秒回到 0Dead死区输入绝对值小于这个值时直接按 0 处理Sensitivity灵敏度按下按键后数值以这个速度每秒靠近目标值Snap吸附归零勾选后反方向按键生效前先把当前值归零防止键位冲突Invert反转把轴输出值取反Type输入类型Key or Mouse Button / Mouse Movement / Joystick Axis 等Axis设备轴当 Type 是 Joystick Axis 时指定读取摇杆的 X 轴或 Y 轴重点说三个参数Gravity、Sensitivity、Dead。Gravity 决定的是松开键后的“余韵”。数值调大松开后立刻归零数值调小角色会往前滑一小段。做动作游戏时这个参数直接决定“站得住”和“滑冰感”的区别。Sensitivity 决定的是按下键后达到满值需要多久。调大了响应快但方向变化会非常生硬调小了手感柔和但角色转弯会显得慢半拍。Dead 则是给摇杆用的摇杆用久了中心点漂移或者你希望手柄轻微推动不响应就调大 Dead。2.3 键盘和摇杆共用一套轴时的参数推荐这里直接给一套我在项目里用过的参数你可以拿去当起点再微调。如果是纯键盘项目Horizontal 轴推荐参数推荐值理由Gravity3松开按键后快速停住键盘玩家不接受滑行Sensitivity3键盘输入就是 0/1 开关3 的过渡够自然又不拖沓Snaptrue防止同时按 A/D 时两个方向打架如果是纯摇杆项目比如手柄控制角色移动推荐单独建一条轴Type 设为 Joystick AxisAxis 根据需要选 X axis 或 Y axis参数推荐值理由Dead0.1过滤摇杆漂移和轻微误触Sensitivity1摇杆本身就是模拟量不需要额外加速Gravity0摇杆回中是物理行为不需要软件归零Snaptrue从右转到左时直接反向不经过中间 0如果你要在同一个项目里同时支持键盘和手柄不要图省事把两类设备绑在同一个默认轴上很容易遇到“手柄插上后键盘方向也飘”的问题。我的习惯是键盘用默认的 Horizontal/Vertical手柄单独建一套MoveX_Gamepad、MoveY_Gamepad轴代码里二选一读取。这样设备之间永远不会互相污染。2.4 旧输入系统里最常见的两个坑按键粘滞和轴冲突第一个坑是按键粘滞。Windows 系统本身就带按键重复功能按住一个键不松系统会连续发出多次“按下事件”。Unity 的旧输入系统在默认配置下GetKeyDown会被系统这些重复事件再次触发导致你明明只按了一下空格角色却连续跳了两次。解决思路有两个要么在逻辑层做冷却要么用time参数区分真实按压和系统重复。一般来说做功能时给跳跃、攻击这类操作加一个“不可再触发”的冷却区间就够了。第二个坑是轴冲突。常见现象是角色站在原地不动但数值显示 Horizontal 一直在 0.5 左右跳。排查思路是打开 Input Manager检查是否有两条轴用了同一个 Positive Button或者一条轴的 Alt Positive Button 和另一条轴的 Positive Button 绑到了同一个键上。还有一种隐蔽情况默认的 Horizontal 轴同时绑定了 A/D 和方向键如果你在代码里又读取了另一个自定义轴而这个自定义轴正好也绑定了方向键就会出现两个轴同时响应。3. 换到新输入系统时这篇基础文能帮你少走弯路3.1 装包、切 Active Input Handling、换 UI 组件新输入系统Input System已经成了 Unity 的默认推荐方案新项目直接用它老项目迁移可能费点劲。安装步骤打开 Window Package Manager在搜索框输入 Input System点 Install。装完以后 Unity 会弹窗问你要不要启用新系统、重启编辑器同时 Player Settings 里会多出一个 Active Input Handling 选项有三个值Input Manager (Old)、Input System Package (New)、Both。老项目建议选 Both让新旧 API 同时可用代码不用一次性改完。但 Both 不是长久之计因为同一个按键可能被新旧两套系统同时读到逻辑万一混在一起排查起来非常酸爽。新项目强烈建议直接选 New从一开始就别碰旧 API。还有 UI 部分装了新输入系统之后场景里旧的 EventSystem 组件会报一个警告要求你把 Standalone Input Module 替换成 Input System UI Input Module。Unity 在 EventSystem 的 Inspector 面板上提供了 Replace 按钮点一下自动换不用手动删了重建。这一步没做的话Canvas 上的按钮就全部没反应了。3.2 InputAction 资产的三种用法哪种适合你新输入系统的核心概念是 InputAction。右键菜单 Create Input Actions创建一个 .inputactions 资产双击打开后可以编辑 Action Map、Action、Binding。Action Map 是一组动作的集合比如“玩家操作”是一个 Map里面包含 Move、Jump、Fire 等动作UI 操作又是一个 Map包含导航、确认、取消。每个 Action 可以绑定到一个或多个具体的物理输入上比如 Jump 绑定 Space 键也同时绑定手柄 A 键。这个资产有三种典型用法我分别说明。第一种是直接轮询适合移动这类每帧都要读状态的操作using UnityEngine.InputSystem; void Update() { // 直接读取键盘当前状态 if (Keyboard.current.spaceKey.wasPressedThisFrame) { Debug.Log(空格按下); } // 读取 A/D 轴的输出 float move Keyboard.current.dKey.ReadValuefloat() - Keyboard.current.aKey.ReadValuefloat(); }第二种是生成 C# 类适合追求类型安全、希望代码里调用清晰的项目。在 .inputactions 资产的 Inspector 面板里勾选 Generate C# ClassUnity 会生成一个包装类然后在代码里订阅事件public class PlayerJump : MonoBehaviour { private PlayerInputActions inputActions; private void Awake() { inputActions new PlayerInputActions(); inputActions.Player.Jump.performed _ Jump(); } private void OnEnable() { inputActions.Player.Enable(); } private void OnDisable() { inputActions.Player.Disable(); } private void Jump() { // 跳跃逻辑 } }第三种是 Inspector 拖拽绑定适合原型阶段或者给策划配置用。在场景里挂一个 PlayerInput 组件把 .inputactions 资产拖上去然后在它的 Action Events 里把具体事件拖到目标对象的方法上完全不用写事件订阅代码。三种方式的取舍很清晰原型阶段用拖拽最快正式项目我推荐生成 C# 类 事件订阅直接轮询则适合某些极端简单的读取场景。3.3 Both 模式下的残留问题事件穿透和 API 失效用 Both 模式最常见的问题是“按键事件触发两次”。原因很简单一段逻辑用Input.GetKeyDown读到空格另一段逻辑用新输入系统Keyboard.current.spaceKey.wasPressedThisFrame也读到了空格两段代码各自跑了一遍。这个问题一旦产生排查成本很高因为看起来像是“逻辑被调用了多次”实际上根因在输入层。我建议的迁移顺序是先把旧的输入读取代码全部集中到一个静态工具类里把这个工具类改成内部用新系统 API 实现然后全局替换调用入口。也就是只保留一个输入出口新旧 API 不混用。如果在某个脚本里用了Input.GetAxis而 Active Input Handling 又切成 New控制台会直接报异常提示旧输入 API 在当前构建目标下不可用。这个坑很直白迁移时用全局搜索把Input.全部找出来一个一个改。3.4 新输入系统的几个概念先入个门Action Map、Action、Binding这铁三角是最基础的概念。再往后还有 Composite比如把 WASD 绑定到一个 2D Vector 动作上、Processor输入后处理比如把鼠标灵敏度做成曲线、Interaction区分 Tap、Hold、Drag 这些手势。初学阶段不建议一上来就啃 Composite 和自定义 Processor先用最朴素的 Single Button 和 Axis 把移动、跳跃跑通再慢慢往复杂手势延伸。很多人劝退新输入系统就是因为一上来被资产编辑器里那一大堆字段吓住了其实日常用到的就那么几块。4. UI 输入不熟比按键不熟更致命InputField 限制、点击穿透与点击范围4.1 InputField 只允许数字和字母别只用 Content Type很多人在 UI 上要限制用户只能输入数字或字母第一反应是改 InputField 的 Content Type。预设选项里确实有 Integer Number、Decimal Number、Alphanumeric 这些但它的控制粒度很粗整数模式就完全不能输小数字母模式就挡不住标点符号。更灵活的做法是用onValidateInput自定义校验。下面这段代码让 InputField 只接受字母和数字其他字符直接丢弃using UnityEngine; using UnityEngine.UI; public class InputFilter : MonoBehaviour { public InputField inputField; private void Start() { inputField.onValidateInput ValidateChar; } private char ValidateChar(string text, int charIndex, char addedChar) { if (char.IsLetterOrDigit(addedChar)) { return addedChar; } return \0; } }注意onValidateInput也有限制在中文输入法、手机自带键盘这类 IME 组合输入场景下回调拿到的addedChar可能不是最终输入的内容所以这只适合做“输入过程中的即时过滤”。最终提交时建议再对inputField.text做一次正则或字符遍历校验防止过滤不彻底。4.2 int.TryParse从输入文本到安全数值的转换UI 上用户输入的数字最终都要转成数值类型。很多人直接写int.Parse(inputField.text)平时跑得好好的一旦用户不小心输了个空格、字母或空字符串就直接抛异常。异常在 Unity 里的表现就是控制台刷红某些情况下还会打断当前一帧的执行流程体验很差。正确姿势是int.TryParsepublic void OnSubmitButtonClick() { string inputText inputField.text.Trim(); if (int.TryParse(inputText, out int value)) { Debug.Log(解析成功: value); // 继续业务逻辑 } else { Debug.LogWarning(输入不合法无法转换为整数); // 给用户提示比如让 InputField 标题变红 } }Trim()很重要它去掉首尾空白避免用户手滑打了空格导致解析失败。TryParse无论输入多离谱都不会抛异常而是返回 false你可以安全地处理错误分支。实际上不只是 intUnity 项目里所有“从 UI 文本到数值”的转换我都建议用 Try 系列。4.3 按钮点击范围扩大用透明 Image 和命中阈值项目里经常遇到这种需求按钮的视觉部分很小但希望玩家点击时容错率高一点也就是扩大点击范围。很多人直接去拉按钮 RectTransform 的大小结果发现图形也跟着变形了。正确做法是给按钮加一个透明但可接收射线的子物体把子物体的 RectTransform 拉大到目标范围让整个大区域都算按钮的点击热区。最关键的一步是这个子物体上要挂一个 Image 组件并且把 Raycast Target 勾上这样 UGUI 的射线检测才会命中它。子物体的 Color 设成全透明都行因为它只是用来做命中测试不参与显示。这里牵扯到 Image 组件上的一个隐藏参数Alpha Hit Test Minimum Threshold。默认值是 0表示不做透明度检测全透明区域也能接收点击。如果你把它调成大于 0 的值比如 0.5那么最终渲染出来 alpha 值低于 0.5 的像素就不会响应点击。这个功能主要用来做不规则形状按钮的精确点击但也会让“透明扩展区”失效。所以经验就是想扩大点击范围保持Alpha Hit Test Minimum Threshold为 0用透明的子物体撑大热区别依赖图片本身的 alpha。4.4 UI 点击穿透与 World UI 无遮挡的处理点击 UI 上的按钮结果 3D 角色也跟着被点击了这是典型的 UI 点击穿透。最直接的处理是判断当前指针是否在 UI 上using UnityEngine; using UnityEngine.EventSystems; public class ClickBlocker : MonoBehaviour { public bool IsPointerOverUI() { if (EventSystem.current null) { return false; } return EventSystem.current.IsPointerOverGameObject(); } }这个方法在 PC 上很可靠。但到了移动端IsPointerOverGameObject()不带参数的重载经常失灵因为触摸不再像鼠标那样只有一个“指针”。正确用法是public bool IsPointerOverUI(int fingerId) { return EventSystem.current.IsPointerOverGameObject(fingerId); }fingerId从哪来从Input.GetTouch(index).fingerId拿。如果不确定可以在点击时遍历所有触摸把每根手指的 fingerId 都传进去判断一遍。World UI 无遮挡的问题也经常见。World Space 类型的 Canvas 在场景里漂浮着点击它的按钮时经常被 3D 物体挡住或者反过来遮挡 3D 物体。排查两个地方一是 Canvas 组件的 Event Camera 有没有指向正在渲染它的相机Event Camera 没设置UGUI 射线无法生成二是 Canvas 的 Sorting Order 和 3D 物体的位置关系Sorting Order 越高越靠后渲染通常 UI 都应该设置在比较高的值。5. 把输入变成画面鼠标坐标、摄像机与一个滑动条5.1 ScreenToWorldPoint 的 Z 值坑把鼠标点击的屏幕坐标转换成世界坐标是很多交互功能的第一步。经典写法是这样Vector3 mouse Input.mousePosition; mouse.z 10f; Vector3 world Camera.main.ScreenToWorldPoint(mouse);这里最容易被忽略的就是那个mouse.z。屏幕坐标是一个二维概念只有 X 和 Y但ScreenToWorldPoint需要输入一个三维坐标其中 Z 表示“这个点在相机前方多远”。如果你忘了设置 Z或者直接传Input.mousePositionZ 默认是屏幕深度通常很小或者为 0转换出来的世界坐标永远落在相机位置附近物体就会莫名其妙地吸附到相机上。正确的理解是ScreenToWorldPoint返回的是“从相机位置出发经过屏幕点的那条射线在距离相机 Z 处的位置”。Z 越大点越远。想做点击放置物体一般取一个固定距离比如 10 或者和地面平面的高度相关。如果是平面交互更稳的做法是射线检测Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { transform.position hit.point; }这种做法自动处理了“点在哪个深度”的问题不用再手动调 Z。5.2 摄像机跟随与鼠标/触摸输入的组合摄像机跟随角色是最常见的输入应用场景。基础版本只有位置跟随public Transform target; public Vector3 offset; private void LateUpdate() { transform.position target.position offset; }要加鼠标控制视角的话用鼠标轴private float yaw; private float pitch; private void LateUpdate() { yaw Input.GetAxis(Mouse X) * sensitivityX; pitch - Input.GetAxis(Mouse Y) * sensitivityY; pitch Mathf.Clamp(pitch, -80f, 80f); Quaternion rotation Quaternion.Euler(pitch, yaw, 0f); transform.position target.position rotation * offset; transform.rotation rotation; }注意这里用了 LateUpdate因为要等角色这一帧移动完相机再去跟随否则画面会出现抖动。这算是摄像机跟随的一个铁律。触摸端的思路类似只不过鼠标轴的来源变成了 Touch 的位置差。简单实现private Vector2 lastTouchPosition; private void Update() { if (Input.touchCount 1) { Touch touch Input.GetTouch(0); if (touch.phase TouchPhase.Began) { lastTouchPosition touch.position; } else if (touch.phase TouchPhase.Moved) { Vector2 delta touch.position - lastTouchPosition; yaw delta.x * touchSpeed; pitch - delta.y * touchSpeed; lastTouchPosition touch.position; } } }核心逻辑就是“当前帧触摸位置减去上一帧触摸位置”得到一个位移增量再把这个增量映射到旋转角上。不管是鼠标还是触摸本质都一样拿位移转角度。5.3 用 Input.GetAxis 驱动一个 Slider快速理解输入范围和归一化热搜词里有个“unity 做一个滑动条”其实和 Slider 组件有关但很多新手不知道 Slider 不仅能被鼠标拖动也能被代码控制实现“输入映射到 UI”的效果。先创建一个 Slider在 Canvas 下右键 UI Slider 即可它会自动生成 Background、Fill Area、Handle Slide Area 三部分。Slider 的值范围默认是 0 到 1也可以自己在 Inspector 里改 Min Value 和 Max Value。然后写一段很直观的验证代码把 Horizontal 轴的值映射到 Slider 上public Slider slider; private void Update() { // GetAxis 返回 -1 到 1先把范围变成 0 到 1 float normalized (Input.GetAxis(Horizontal) 1f) * 0.5f; slider.value normalized; }这段代码跑起来之后按 A 键滑块往左走按 D 键滑块往右走松开键滑块平滑回到中间。它把“输入轴范围”和“UI 数值范围”之间的联系展示得非常直观。如果你想限制 Slider 的值在某个区间比如音量最小 0.2、最大 0.8有两种方式一是直接在 Slider 组件上设 Min/Max二是代码里用Mathf.Clamp卡住赋值slider.value Mathf.Clamp(normalized, 0.2f, 0.8f);Mathf.Clamp和Mathf.Clamp01是我在 UI 交互里用得最频繁的两个函数。任何“来自用户的原始输入”都可能是超范围的进入 UI 之前先 clamp 一次能避免大量边界 bug。6. 输入不生效排查链路以及容易被误判成输入问题的平台坑6.1 一个从现象到原因的排查顺序表每日在群里看到最多的求助就是“Unity 输入没反应”。但“没反应”这个表述实在太模糊了按键没反应和点击没反应排查路径完全不同。我整理了一个顺序表照着排查能省一半时间。现象优先检查项说明键盘按键没反应Active Input Handling 是否还是 Old代码是否在 Update 里物体是否 SetActive(false)装了新输入系统后如果切到 New旧 API 直接失效鼠标点击 UI 按钮没反应EventSystem 是否存在InputSystemUIInputModule 是否已替换Image 的 Raycast Target 是否勾选Canvas 的 Event Camera 是否设置UI 点击走的是射线检测任何一个环节断了都不行PC 上正常手机上没反应是否依赖了鼠标 API是否忘了处理多指触摸手机没有鼠标滚轮也没有鼠标移动轴点击 UI 同时触发 3D 交互是否漏了IsPointerOverGameObject判断两条射线管线是独立的UI 挡住了不代表 3D 点击被拦截输入偶尔失灵是否在 FixedUpdate 里读了 GetKeyDown输入读取不要放 FixedUpdate按住键后逻辑反复触发是否没有做冷却或者没有处理按键重复系统层面的 Key Repeat 会持续产生按下事件排查的心法就一句话先把“输入值到底有没有拿到”验证清楚再谈逻辑。我见过太多人改了半天业务代码最后发现是某个 Image 的 Raycast Target 没勾。用 Debug.Log 把输入值打出来问题定位立刻清晰很多。6.2 PC 正常、手机上失灵触摸输入与鼠标输入的本质区别PC 上Input.GetMouseButtonDown(0)很好用但拿到手机上它只能处理最基础的单指点击而且有些 Android 机型对鼠标 API 的模拟行为很迷。真正做移动端交互还是要回到触摸 API 本身。几个关键点Input.touchCount表示当前有几根手指在屏幕上。Input.GetTouch(i)返回第 i 根手指的状态包含 position、deltaPosition、phase。TouchPhase.Began是手指落下Moved是手指移动Ended是手指抬起。多指手势双指缩放、旋转需要跟踪每根手指的fingerId因为它不固定是索引 0 或 1手指抬起后索引会变。一个简单的双指缩放思路private float lastDistance; private void Update() { if (Input.touchCount 2) { Touch touch1 Input.GetTouch(0); Touch touch2 Input.GetTouch(1); float currentDistance Vector2.Distance(touch1.position, touch2.position); if (touch1.phase TouchPhase.Began || touch2.phase TouchPhase.Began) { lastDistance currentDistance; } else { float scaleFactor currentDistance / lastDistance; transform.localScale * scaleFactor; lastDistance currentDistance; } } }这段代码的核心是记录上一帧两根手指的距离和当前帧距离做除法得到的比值就是缩放倍数。它完全不依赖鼠标 API所以真机上跑起来基本不会出问题。6.3 WebGL/串口/PLC 这类“特殊输入”到底怎么接入先提醒一个搜索热词陷阱。很多人搜“unity 发布 webgl 使用 idbfs 写入失败”搜出来的报错文本里带了input之类的关键词就以为是输入系统的问题其实完全不是一回事。IDBFS 是浏览器 IndexedDB 文件系统写入失败通常和浏览器存储空间、隐私模式、跨域配置有关跟 Input API 没关系。还有一个常见报错长这样failed to deserialize the json body into the target type: input: missing field。这个也经常被人贴上“Unity 输入出错”的标签。实际上这是服务端接口反序列化 JSON 失败提示请求体里缺字段跟 Unity 的 Input 类八竿子打不着。遇到这类报错先看全日志确认是客户端还是服务端再看是不是真的和输入有关。再说外部设备输入。Unity 项目接串口设备、PLC、传感器这类“非标准输入”常见做法是用System.IO.Ports.SerialPort读取数据然后在 Update 里消费。一个关键原则是串口数据接收是在后台线程触发的不能直接在回调里改 Unity 的 Transform、调用 API否则会出现线程安全问题。正确的做法是收到数据后先放进一个线程安全的队列或者用一个临时变量存最新的数据帧然后在 Update 主线程里取出来使用。PLC 通信如果是走 Modbus RTU/TCP本质也是串口或网络数据收发。接入流程一样先打通物理链路再把协议解析出来的值包装成统一的“自定义输入源”最后映射到游戏逻辑里。你可以把这些外部数据源抽象成和键盘鼠标同级的输入事件甚至接到新输入系统的自定义设备上但基础版先用一个静态类存最新值就够了。我在实际项目中的习惯是所有输入不管是键盘、触摸还是串口数据都先归一化成“事件”或“状态”两类然后统一供给业务层。状态类比如移动轴、摇杆值每帧读取事件类比如跳跃、确认、急停只有触发时才广播。这样后面加设备、加平台支持改动都在输入层业务代码一行都不用动。这算是我踩过无数坑之后最想分享的经验。最后再补一个调试技巧在开发阶段不管做哪个平台先把输入值显示在屏幕上。比如在 OnGUI 里把Input.GetAxis(Horizontal)、Input.touchCount、当前 Slider 值全部打印出来跑起来就能肉眼确认“输入到底到没到”。很多时候你以为的“逻辑 bug”其实是“输入根本没进来”这一步能帮你省下大量时间。