Unity输入与屏幕设置实战笔记:从旧版Input到Input System
我不太喜欢写那种截图铺满的入门教程这篇更偏向我自己反复会翻的备忘录。主题就两个一是Unity里的按键操作二是屏幕设置。搞游戏开发或者做工具类应用的人应该都有同感这两种需求几乎每个项目都会碰上但每次写都还要去查一下Input类的API或者记不清Screen.SetResolution的参数顺序。这篇笔记把这两块相关的细节、写法、坑点一次性理清楚新老项目都适用尤其适合正在做小游戏、AR展示、交互大屏这类需要频繁处理输入和显示适配的开发者参考。1. 按键操作第一步先把输入方案定下来很多人一上来就写Input.GetKey其实这是默认的旧版输入方案。Unity引擎本身已经自带了两套输入系统旧版Input类和新版Input System Package它们不是简单的升级关系而是底层架构上的两套方案选择哪一套会直接影响整个项目的按键处理方式。1.1 新旧两套输入系统的定位差异旧版Input类是引擎内置的从项目创建那一刻起就可以直接用不需要安装任何额外包。它的特点是简单粗暴靠Input.GetKey(KeyCode.W)、Input.GetMouseButton(0)这类静态方法轮询状态所有配置都集中在Project Settings里的Input Manager面板鼠标、键盘、触屏、加速度计、陀螺仪等输入被统一处理对小型项目来说完全够用。新版Input System是后出的包需要从包管理器单独安装。它的核心思路是抽象一层“Action”概念把一个动作比如“移动”“跳跃”和具体按键解耦键位绑定不再写死在代码里而是通过.inputactions资源配置。项目运行时可以用同一套逻辑适配键盘、手柄、触屏甚至多套键位方案这对需要支持多平台、多外设的正式项目来说很有吸引力。这里要提醒一下如果项目是纯2D小游戏、原型验证、或者团队里没有专门做工具链的人直接用旧版Input类会更省事。不要为了追新而强行引入Input System两套系统的坐标转换、输入时机、事件分发机制都不一样一旦混用很容易出现同一个按键触发两次、事件丢失这类莫名其妙的毛病。1.2 如何切换输入处理模式切换输入方案的地方在Project Settings的Player页面找到Active Input Handling选项。这里有三个值Input Manager (Old)、Input System Package (New)、Both。默认是旧版只有安装了Input System包之后这个选项才会出现New和Both两个选项。Both模式最容易踩坑。因为旧版和新版两套系统同时激活同一个键盘事件会被两个系统各自捕获一次。比如你在新版Input System里配置了Space作为跳跃键同时又用旧版Input.GetKeyDown(KeyCode.Space)写了另一段跳跃逻辑那么一次按键会触发两次跳跃。理论上Both模式是为了让团队分阶段迁移到新系统但实际使用中能感觉到明显的输入延迟和双触发风险我建议要么纯旧版要么切到New之后把旧代码全部清理不要长期开着Both。注意切换Active Input Handling之后编辑器会要求重启。如果你的项目准备从旧版迁到新版建议先在分支上做切换后逐场景检查所有绑定了输入脚本的对象因为新版Input System的事件默认不是每帧轮询而是基于消息回调时序习惯完全不同。2. 旧版Input类的按键检测与封装实践如果你是刚开始接触Unity键盘操作或者项目还在用旧版方案那么掌握GetKey系列方法就够用了。最核心的就是三个Input.GetKey、Input.GetKeyDown、Input.GetKeyUp。2.1 三个方法的使用语义与场景先直接给结论方法返回值时机典型用途Input.GetKey(KeyCode.W)按住期间每一帧都返回true持续移动、角色走动、相机跟随Input.GetKeyDown(KeyCode.Space)按下那一刻的当前帧返回true跳跃、射击、开门等一次性事件Input.GetKeyUp(KeyCode.LeftShift)松开那一刻的当前帧返回true蓄力释放、长按结束、取消操作这三者的区别必须刻在脑子里。很多人把GetKeyDown写成每帧判断然后发现跳跃像抽搐一样连续触发就是因为没有理解“只有按下那一帧才为true”这个语义。同理GetKeyUp适合处理“松开才生效”的场景比如蓄力攻击按得越久伤害越高你在Update里累计时间等GetKeyUp触发时结算伤害逻辑就非常顺。鼠标操作对应的是Input.GetMouseButton(0)、Input.GetMouseButtonDown(0)、Input.GetMouseButtonUp(0)参数0代表左键、1代表右键、2代表中键。鼠标位置用Input.mousePosition读取滚轮用Input.mouseScrollDelta。这些和键盘方法的使用逻辑完全一致不需要额外学。2.2 常用KeyCode枚举与键位冲突处理键盘上的每个物理键位都有对应的KeyCode枚举值日常基本就那几个字母键就是KeyCode.A到KeyCode.Z数字键是KeyCode.Alpha0到KeyCode.Alpha9功能键是KeyCode.F1到F12方向键是KeyCode.UpArrow、KeyCode.DownArrow、KeyCode.LeftArrow、KeyCode.RightArrow修饰键分左右LeftShift和RightShift是独立枚举LeftAlt、RightAlt、LeftControl、RightControl也都是分开的。键位冲突是个容易被忽视的问题。比如你用了WASD移动同时又监听UpArrow方向键如果两套键位都调用了同一段角色移动代码玩家按方向键时也会移动这在某些设计里是想要的但有些时候不想让方向键和WASD互相干扰那就要在封装时做一个统一的输入偏好设置把键位映射做成可配置项而不是散落在各个脚本里硬编码。我建议至少建一个静态输入配置类把键盘映射集中在里面用枚举定义自己的游戏动作再映射到具体KeyCode。这样方便统一排查冲突也方便以后改成自定义按键设置界面。自定义设置界面其实也不复杂只要监听一次GetKeyDown记录当前按下的KeyCode写回配置类就行。2.3 一个移动控制的封装示例写一个最常见的角色移动封装这里用旧版Input类直接展示核心结构using UnityEngine; public class PlayerController : MonoBehaviour { public float moveSpeed 5f; private Rigidbody rb; private Vector3 inputDirection; private void Awake() { rb GetComponentRigidbody(); } void Update() { float horizontal 0f; float vertical 0f; if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) horizontal - 1f; if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) horizontal 1f; if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) vertical - 1f; if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) vertical 1f; inputDirection new Vector3(horizontal, 0f, vertical).normalized; if (Input.GetKeyDown(KeyCode.Space)) { Jump(); } } void FixedUpdate() { rb.velocity new Vector3(inputDirection.x * moveSpeed, rb.velocity.y, inputDirection.z * moveSpeed); } private void Jump() { if (IsGrounded()) { rb.AddForce(Vector3.up * 5f, ForceMode.Impulse); } } }这里有个很重要的经验按键轮询放Update物理力、位移修改放FixedUpdate。因为Update的调用频率和渲染帧率挂钩FixedUpdate是固定的物理步长。如果在Update里直接修改Rigidbody可能会因为帧率波动导致移动速度不稳而在FixedUpdate里读取Update阶段写入的输入方向向量物理系统就能稳定地处理运动。输入方向先归一化是为了保证斜向移动和正向移动速度一致否则同时按W和D时合成向量的长度是1.414倍角色会明显跑得更快。3. 新版Input System的快速上手与键位自定义如果你的项目目标平台包含多个手柄、触屏、键盘混合输入或者你需要做自定义按键重映射、UI提示动态更新那新版Input System的优势就很明显了。3.1 从安装到生成InputActions资源首先在Package Manager里搜索Input System安装后Unity会提示需要重启编辑器并切换Active Input Handling。然后右键Project窗口选择Create - Input Actions创建一个.inputactions文件双击打开编辑窗口。编辑窗口里默认会有一个名为Player的Action Map我习惯按照游戏状态拆Map比如Gameplay、UI、Menu。在Gameplay这个Map下可以新建Action比如MoveAction Type选ValueControl Type选Vector2跳跃JumpAction Type选Button。每个Action右侧有一个绑定列表点击加号可以添加多种绑定比如Move可以同时绑定WASD的合成结果和手柄左摇杆。这里推荐一个小技巧把Action的按需启用做成常态。不要让所有Action Map同时启用而是根据当前游戏状态切换比如在UI界面时禁用Gameplay地图避免玩家点击按钮时角色还在乱动。用InputAction.SwitchCurrentActionMap或者Map.Enable()/Disable()来控制。3.2 C#脚本里如何响应Action使用生成器的方式最直接。勾选.inputactions资源的Generate C# Class选项Unity会生成一个同名的C#类比如InputActions.cs然后在脚本里创建实例并订阅事件using UnityEngine; using UnityEngine.InputSystem; public class NewInputController : MonoBehaviour { private InputActions inputActions; private Vector2 moveValue; private void Awake() { inputActions new InputActions(); inputActions.Gameplay.Move.performed OnMove; inputActions.Gameplay.Move.canceled OnMoveCanceled; inputActions.Gameplay.Jump.performed OnJump; } private void OnEnable() { inputActions.Enable(); } private void OnDisable() { inputActions.Disable(); } private void OnMove(InputAction.CallbackContext context) { moveValue context.ReadValueVector2(); } private void OnMoveCanceled(InputAction.CallbackContext context) { moveValue Vector2.zero; } private void OnJump(InputAction.CallbackContext context) { Debug.Log($跳跃触发按键是{context.control.displayName}); } private void FixedUpdate() { // 使用 moveValue 驱动物理 } }在Input System里同一帧内输入会以事件形式派发事件顺序和处理的时机跟旧版完全不同不能再像以前那样依赖Update里判断布尔值。performed事件在按下时触发canceled在松开时触发这样逻辑天然就是事件驱动的写UI状态切换时特别舒适。还有一个容易忽视的点新Input System里拿到的向量其Y轴在屏幕坐标系和世界坐标系里的含义不同。Move Action在配置触摸板或鼠标时如果Control Type是Vector2你读到的是屏幕坐标增量用在相机移动时要自行转换。建议把输入处理单独放到一个Controller类里不要直接散落在GameObject脚本中。3.3 什么时候留用旧版什么时候迁移我个人判断标准很简单如果只是PC端做演示、单个玩家用键盘鼠标操作旧版快且稳如果要做本地多人、手柄/键盘混战、触屏和键鼠共用一套逻辑就值得投入时间迁移到新系统。迁移成本主要在输入相关的旧代码必须全部重写无法百分之百机械替换。而且动画状态机里的Trigger、UI事件监听这些联动逻辑往往比纯粹的输入代码更影响工时要留出至少两天的回归测试时间。4. 屏幕设置分辨率、全屏与移动端方向按键之外屏幕设置也是被问得最多的一块。很多项目在PC编辑器里跑一切正常一打包到目标机器上就出现窗口尺寸不对、全屏黑屏、移动端画面被拉伸变形。4.1 Screen.SetResolution参数与运行逻辑窗口大小和全屏状态的设置集中在Screen类核心API是Screen.SetResolution(int width, int height, FullScreenMode fullScreenMode, int preferredRefreshRate)。参数顺序经常会搞混尤其是第一个和第二个先宽后高和像素坐标的x、y顺序保持了一致。FullScreenMode是一个枚举有四个常用值含义要分清FullScreenMode含义适用场景ExclusiveFullScreen独占全屏直接切换显示模式性能最好单显示器桌面游戏FullScreenWindow无边框全屏窗口分辨率跟随桌面多显示器、直播截屏、需要快速切窗口Windowed普通窗口模式可拖动调整大小调试、工具类应用MaximizedWindow最大化窗口不隐藏标题栏少见兼容老平台时使用在代码里切换时频率最高的错误是调用SetResolution但没指定FullScreenMode。旧版接口Screen.fullScreentrue/false还在但建议统一用Screen.fullScreenMode FullScreenMode.Windowed配合Screen.SetResolution来写让意图更明确。实际项目里我是这样组织设置代码的using UnityEngine; public class ScreenSettingManager : MonoBehaviour { public void ApplyWindowMode(int width, int height) { Screen.SetResolution(width, height, FullScreenMode.Windowed); } public void ApplyBorderlessFullscreen() { Screen.SetResolution(Screen.currentResolution.width, Screen.currentResolution.height, FullScreenMode.FullScreenWindow); } public void ApplyExclusiveFullscreen(int width, int height, int refreshRate) { Screen.SetResolution(width, height, FullScreenMode.ExclusiveFullScreen, refreshRate); } }有一点要特别说明Screen.fullScreenMode设置的是模式Screen.SetResolution内部也会隐式修改模式。如果你想在窗口模式下按一个特定尺寸显示应该先切Windowed再SetResolution大小顺序反了可能触发一次多余的屏幕分辨率切换。另一个细节是preferredRefreshRate参数传0时Unity会用当前显示器默认刷新率我在高刷新率显示器上通常直接传0因为手动指定60会莫名限制刷新率传0反而能拿到显示器当前的最优刷新率。4.2 运行时切换全屏窗口化的正确顺序运行时切换显示模式最怕黑屏和闪屏。独占全屏在Windows下切换时会请求显卡驱动切换显示模式这个过程显示器会黑屏一到两秒这是硬件层面的正常现象不是废了。但如果你在窗口模式下连续多次调用SetResolution并且每次间隔很短可能会让窗口管理器来不及处理最终导致窗口位置飘移或者任务栏闪烁。我建议在切换前先记录当前窗口位置切完再设置回来using UnityEngine; public class ScreenSwitcher : MonoBehaviour { private Rect windowRect; public void OnToggleFullscreen() { // 先记录窗口位置与大小 windowRect new Rect(Screen.width, Screen.height, 0, 0); Screen.fullScreenMode Screen.fullScreen ? FullScreenMode.Windowed : FullScreenMode.FullScreenWindow; // 等待一两帧后再调整避免切换过程中尺寸冲突 Invoke(nameof(RestoreWindowRect), 0.1f); } private void RestoreWindowRect() { if (Screen.fullScreenMode FullScreenMode.Windowed) { // 恢复窗口尺寸 Screen.SetResolution((int)windowRect.width, (int)windowRect.height, FullScreenMode.Windowed); } } }这里用Invoke延迟一帧是给系统一个缓冲期。当然如果你对窗口位置有更高要求需要原生窗口管理API那就要写平台相关的插件但绝大多数Unity项目用不到那么深。4.3 移动端横竖屏旋转与安全区适配移动端屏幕设置比PC更容易出问题因为涉及到屏幕方向、设备像素密度、安全区。方向控制用Screen.orientation属性常用值是Portrait竖屏、LandscapeLeft、LandscapeRight、AutoRotation。简单说想让游戏锁定竖屏就设Screen.orientation ScreenOrientation.Portrait还想在运行时根据重力感应自动旋转就要打开Screen.autorotateToPortrait、Screen.autorotateToLandscapeLeft、Screen.autorotateToLandscapeRight这些开关。同时要在Player Settings里把Default Orientation设置为对应的方向否则打包后第一次启动的画面方向可能不对。移动端有全面屏和刘海屏直接用Screen.width、Screen.height拿到的物理像素但内容不能顶到屏幕边缘。这时候要读取Screen.safeArea它会返回一个Rect代表系统认为安全的内容区域。处理安全区的做法是让UI根节点跟随safeArea并且避免把关键操作按钮放在安全区外面。一个常见做法是写一个Canvas适配组件在Start时根据safeArea把Panel的锚点缩到安全区域内。如果场景里UI根节点设置了CanvasScaler的UIScaleMode为ScaleWithScreenSize那在计算safeArea坐标时要对接Canvas的参考分辨率直接使用Rect单位很可能会错位。5. 常见问题与排查技巧实录把我在实际项目里踩过的、以及在社区里反复看到的问题整理成一张表方便你排查时直接对号入座。5.1 常见问题速查表现象可能原因解决思路按键盘没反应但鼠标正常Active Input Handling设置是New代码还在用Input.GetKey改成Both临时测试最终统一方案同一个按键触发两次逻辑Both模式下新旧输入同时接收事件只保留其中一套不要长期开Both按住方向键主角匀速移动但画面一卡一卡的Update里直接移动Rigidbody受帧率波动影响输入在Update采集物理移动放FixedUpdateSetResolution后窗口跑到屏幕外窗口位置没有被记住分辨率切换后Windows重排窗口切换前记录窗口矩形切换后恢复位置全屏黑屏很久无法恢复使用了ExclusiveFullScreen且分辨率超出显示器支持优先用FullScreenWindow切换前获取Screen.resolutions校验移动端横竖屏切换后UI错位safeArea未更新UI锚点没有跟随安全区在方向改变事件里重新计算safeArea并刷新布局手柄按键不响应新版Input System缺少手柄绑定在InputActions编辑器里给Action添加Gamepad绑定高分屏显示器下UI模糊窗口缩放和DPI缩放没有正确处理Player Settings里启用高DPI支持UI用CanvasScaler按比例缩放这张表里最值得展开的就是第一项和第四项。这两个问题几乎每周都有人问原因是旧代码迁移到新输入系统时Input类静态方法不会再返回任何值但代码不会报错只是没有任何反应这种“静默失败”特别难排查。我建议遇到按键失灵先区分是键盘问题还是输入系统问题在控制台里打印InputSystem.devices看设备是否被识别再看对应设备下的control状态。5.2 关于按键响应延迟与UI抢焦点按键响应出现延迟可能来自两个方向。一个是新版Input System的事件泵机制输入采样频率和渲染频率解耦如果你的游戏整体帧率很低Input System的输入事件仍然会以设备刷新率采集但因为主循环处理不过来事件会被堆积表现出来就是你按了键过了一两百毫秒才有反应。这种情况要优先解决性能瓶颈而不是责怪Input System。另一个是UI的EventSystem抢焦点。很多人在界面上放了一个InputField之后发现其他按键完全失效或者按了方向键结果游戏里没反应。原因是EventSystem会把键盘事件派发给当前选中的UI控件如果InputField处于选中状态它会消费掉所有按键事件Unity的旧版输入管理器里可以用EventSystem.current.IsPointerOverGameObject()来判断是否点在UI上再决定是否屏蔽游戏输入。新版Input System则有专门的UI输入模块处理方式更规范但也更容易出现“UI消费了输入”的新问题。5.3 关于多显示器与DPI缩放PC上接两个显示器的场景越来越常见但Unity自身默认创建的窗口只会在一个显示器上Screen.SetResolution只会改变主窗口在屏幕上的模式不会跨屏扩展。如果你需要多屏输出需要的是Display API可以把Game视图渲染到第二个显示器上这通常用于展示型应用或者赛车游戏的多屏拼接和普通的窗口全屏设置完全不是一回事。DPI缩放也是容易被忽视的坑。在Windows上如果启用了系统DPI缩放Unity创建的窗口在显示器缩放比例不同时会呈现模糊状态。建议在Player Settings里的Resolution and Presentation面板勾选支持高DPI同时在代码里读取Screen.dpi来动态调整UI缩放比例。如果你的UI是Screen Space - Overlay模式在高分屏上字体容易出现锯齿记得把CanvasScaler的Dynamic Pixels Per Unit调高。注意不要把所有屏幕适配逻辑都堆在Start里。移动设备在折叠、外接显示、分屏模式下屏幕参数在运行期是变化的正确做法是监听Screen.orientation变化事件以及再在OnRectTransformDimensionsChange里刷新UI。6. 最后补充两个我在项目中常用的收尾手段如果以上这些你都处理完了我再分享两个小习惯。第一做一个全局的Settings类把屏幕模式和按键映射全部统一管理不要在每个场景各写一份。哪怕是原型阶段的小项目这个习惯也能节省后面联调的大量时间。第二个习惯是保留一份纯手写的键位表记录每个动作默认按键、备用按键、对应手册里的UI提示名。键位表是给美术和策划看的不是给你的代码看的。很多时候按键冲突并不是程序代码写错而是策划方案里两个动作分配了同一个键先把表拉齐再回到代码排查效率会高很多。输入和屏幕设置看起来是Unity里最基础的操作但越基础的东西越值得认真对待。在后来的项目里我逐渐养成了把这两块独立封装、单独测试的习惯迭代频繁时确实省了很多事希望这份笔记对你也有同样的作用。