〇它是干什么的
先花 30 秒建立直觉,再动手写。
RevInput.Bind("Jump", RevKey.Space, RevKey.JoystickButton0); // ① 绑一次(键盘和手柄一起绑) RevInput.OnPressed("Jump", Jump, owner: this); // ② 订阅事件:按下时框架叫你 RevInput.OnAxis("MoveX", v => _moveX = v, owner: this); // ③ 订阅轴:数值变了才叫你
键位 / 触屏 / 手柄的差异、按下与抬起的判定、手势识别,框架全包 —— 业务不需要在 Update 里问"按了没"。
想让"按住持续做的事"也不用 Update:SetRepeat 配好节拍 + OnRepeat 订阅,框架按节拍推给你。
需要"此刻的状态"时,轮询 API 也都在:RevInput.Held("Fire") / RevInput.Axis("MoveX"),两种写法可混用。
输入一发生,它主动通知你,你在回调里干活就行;键位写在绑定表里,业务代码里一个键位字面量都没有。
| 它替你解决的问题 | 怎么做的 |
|---|---|
| 键位写死在业务里,改键要改代码 | 动作名 + 绑定表:业务只写 "Jump",键位在绑定表里 |
| 键盘/鼠标/触屏/手柄各写一套 | 三套输入压成同一份快照,动作判定只有一份逻辑 |
| 切后台回来"按键卡住"(角色一直跑) | 隐藏宿主在失焦/切后台时自动复位(抬起事件丢了也不会卡) |
| 弹窗打开时手一滑就操作了角色 | RevInput.OpenScope() + scope.Block() —— 只挡世界输入,ESC 照常可用 |
| 触屏手势要自己算(点击/长按/滑动/双指) | 内置 7 种手势,八向滑动带速度、双指捏合/旋转都有 |
| "提前 0.15 秒按的跳跃"白按了 | RevInput.PressedBuffered("Jump") —— 输入缓冲窗口(手感补偿) |
| 输入出问题只能猜 | 失败带原因码(RevInput.Failed)+ RevInput.Dump() 自检 + VerboseLog 开关 |
一3 分钟跑起来(可粘贴)
不用初始化、不用挂脚本。推荐写法 A:事件驱动 —— 业务里连 Update 都不用写。
写法 A:事件驱动(推荐)
sealed class PlayerInput : RevInputListenerBase // ① 继承基类:只重写你关心的方法 { float _moveX, _moveY; void OnEnable() { RevInput.Bind("Jump", RevKey.Space, RevKey.JoystickButton0); RevInput.Bind("Fire", RevMouseButton.Left); RevInput.BindAxis("MoveX", RevKey.A, RevKey.D); RevInput.BindAxis("MoveY", RevKey.S, RevKey.W); RevInput.SetRepeat("Fire", 0.35f, 0.08f); // 想让"按住自动连发"也走事件,就先配节拍 RevInput.AddListener(this, owner: this); // ② 登记一次:之后输入由框架推给你 } void OnDisable() => RevInput.OffAllOf(this); // ③ 一行退订(事件 + 屏蔽一起清) public override void OnInputPressed(string action) { switch (action) // ④ 在回调里干活,不用每帧问"按了没" { case "Jump": Jump(); break; case "Fire": Shoot(); break; } } public override void OnInputRepeat(string action) => Shoot(); // 按住连发(框架按节拍推) public override void OnInputAxis(string axis, float value) // 轴:只在数值变化时回调 { if (axis == "MoveX") _moveX = value; // 只记下来;什么时候用由你的移动逻辑决定 if (axis == "MoveY") _moveY = value; } public override void OnInputGesture(RevGestureEvent g) // 手势:点击 / 滑动 / 长按 / 双指… { if (g.Kind == RevGestureKind.Swipe) Dodge(g.Direction); } }
不想继承基类也行,按事件单独订阅(可混用):
RevInput.OnPressed("Jump", Jump, owner: this); // 按下 RevInput.OnRepeat("Fire", Shoot, owner: this); // 连发 RevInput.OnAxis("MoveX", v => _moveX = v, owner: this); // 轴变化 RevInput.OnGesture(OnAnyGesture, owner: this); // 全部手势
写法 B:在 Update 里轮询(问"此刻状态"时用)
上面两种和轮询可以混用 —— 需要"当前是不是按着 / 摇杆此刻多少 / 按住多久了"这类快照式问题,直接读:
void Start() { // ① 绑定(一次;写在任意启动代码里都行) RevInput.Bind("Jump", RevKey.Space, RevKey.JoystickButton0); // 一个动作可绑多个键 RevInput.Bind("Fire", RevMouseButton.Left); // 鼠标键也是动作 RevInput.BindAxis("MoveX", RevKey.A, RevKey.D); // 键位轴:A 负 / D 正 RevInput.BindAxis("MoveY", RevKey.S, RevKey.W); } void Update() { // ② 查相位:按下 / 按住 / 抬起 if (RevInput.Pressed("Jump")) Jump(); // 只成立一帧 if (RevInput.Held("Fire")) Shoot(); // 一直按着一直播 if (RevInput.Released("Fire")) StopShoot(); // ③ 查轴(-1..1,已过死区) float x = RevInput.Axis("MoveX"); float y = RevInput.Axis("MoveY"); var move = RevInput.Vector("Move"); // 约定:读 MoveX / MoveY,返回 (X, Y) 两个 float transform.Translate(new Vector3(move.X, 0f, move.Y) * Time.deltaTime * speed); }
RevInput 时会自动创建一个隐藏宿主 [RevInput](DontDestroyOnLoad,不出现在 Hierarchy),
它每帧采一次输入。业务不摆物体、不挂脚本、不写 Update。
二"我要做 X" 对照表(全部 API)
左边找需求,右边抄一行。入口统一是 RevInput。
| 我想… | 这么写 |
|---|---|
| 绑键位(可多个) | RevInput.Bind("Jump", RevKey.Space, RevKey.JoystickButton0) |
| 绑鼠标键 | RevInput.Bind("Fire", RevMouseButton.Left) |
| 绑键位轴 | RevInput.BindAxis("MoveX", RevKey.A, RevKey.D) |
| 绑命名轴(手柄摇杆 / 滚轮) | RevInput.BindNamedAxis("LookX", "Mouse X")(要在 InputManager 里配好轴名) |
| 查"本帧刚按下" | RevInput.Pressed("Jump") |
| 查"按住" | RevInput.Held("Fire") |
| 查"本帧刚抬起" | RevInput.Released("Fire") |
| 查"最近 0.15 秒内按过"(手感补偿) | RevInput.PressedBuffered("Jump") · 自定义窗口 PressedBuffered("Jump", 0.25f) |
| 查连发(按住自动重复) | RevInput.SetRepeat("Auto", 0.35f, 0.08f) → if (RevInput.Repeat("Auto"));不要连发就 SetRepeat("Auto", 0, 0)(delay ≤ 0 = 关连发) |
| 查轴 / 查两个轴 | RevInput.Axis("MoveX") · RevInput.Vector("Move") |
| 查按住多久 / 最近按下帧 | RevInput.HeldSeconds("Fire") · RevInput.LastPressedFrame("Jump") |
| 鼠标位置 / 位移 / 滚轮 | RevInput.MousePosition · RevInput.MouseDelta · RevInput.MouseScroll |
| 鼠标键三态 | RevInput.MouseButton(RevMouseButton.Right)(默认查"按住") |
| 查有几根手指 / 第几根的信息 | RevInput.PointerCount · RevInput.Pointer(i)(含 X/Y/位移/相位/id) |
| 判断指针是不是点在 UI 上 | RevInput.IsPointerOverUI(RevInput.PointerId(i))(鼠标用默认参数) |
| 查手势(7 种) | RevInput.HasGesture(RevGestureKind.Swipe) · RevInput.TryGetGesture(...) |
| 查滑动(方向 + 速度) | RevInput.Swipe(out var sw) → sw.Direction / sw.Value(像素/秒) |
| 查双指缩放 / 旋转 | RevInput.PinchScale(1 = 不变)· RevInput.RotateDegrees |
| 调手势阈值 | RevInput.TapMaxSeconds / LongPressSeconds / DragThreshold / SetSwipeThreshold(距离, 速度) |
| 挡掉世界输入(弹窗/过场/教程) | var b = RevInput.Block() → b.Close();或 using (var s = RevInput.OpenScope()) { s.Block(); } |
| 只挡某根手指 | RevInput.Block(RevInputBlockKind.Pointer, pointerId)(比如"这根手指在拖镜头") |
| 查屏蔽状态 | RevInput.WorldBlocked · RevInput.IsBlocked(RevInputBlockKind.All) · RevInput.BlockCount |
| 登记监听者(推荐:事件驱动) | RevInput.AddListener(this, owner: this) —— this 继承 RevInputListenerBase,只重写关心的方法 |
| 退掉监听者 / 查数量 | RevInput.RemoveListener(this) · RevInput.ListenerCount |
| 订阅轴变化(只在数值变化时回调) | RevInput.OnAxis("MoveX", OnMoveX, owner: this) · 退订 RevInput.OffAxis("MoveX", OnMoveX) |
| 订阅连发(按住按节拍推给你) | RevInput.SetRepeat("Fire", 0.35f, 0.08f) + RevInput.OnRepeat("Fire", Shoot, owner: this) |
| 订阅按下 / 抬起 / 手势 | RevInput.OnPressed("Jump", OnJump, owner: this) · OnReleased(...) · OnGesture(OnAnyGesture, owner: this) |
| 退订(一行) | RevInput.OffAllOf(this)(事件 + 屏蔽一起清) |
| 手动复位(换场景/结算) | RevInput.ResetAll("换场景") |
| 改键后存档 / 读档 | string text = RevInput.SaveBindings(); · RevInput.LoadBindings(text) |
| 改键后查冲突 | RevInput.FindConflicts()(同一个键绑了两个动作会列出来;BindAxis 用的轴键也算输入源) |
| 解绑 / 删动作 / 清空 | RevInput.Unbind("Jump") · RemoveAction("Jump") · ClearBindings() |
| 强制设备类型(测试用) | RevInput.ForceDeviceKind(RevInputDeviceKind.Touch) |
| 诊断 | RevInput.VerboseLog = true; · Debug.Log(RevInput.Dump()) · RevInput.FailedCount |
三键位、轴与"动作名"约定
只有三条约定要记。
| 约定 | 说明 | 例子 |
|---|---|---|
| 动作名 = 业务语义 | 用动词或能力名,不用键名(否则改键时全项目搜字符串) | "Jump" / "Fire" / "Interact" |
| 轴用后缀 X / Y | Vector("Move") 会去读 MoveX 与 MoveY 两个动作 | BindAxis("MoveX", A, D) + BindAxis("MoveY", S, W) |
| 键位名与 Unity KeyCode 一致 | 写 RevKey.Space / RevKey.JoystickButton0 / RevKey.LeftShift,不用记数值 | 手柄键就是 JoystickButton0..19 |
RevInputVector2。
需要 Unity 类型就自己转:new Vector3(v.X, 0f, v.Y)。
手柄要额外做一步:摇杆这类"模拟量"要走命名轴 —— 在 Project Settings → Input Manager 里确认有 Horizontal / Vertical(Unity 默认自带),然后
RevInput.BindNamedAxis("LookX", "Mouse X"); RevInput.BindAxis("MoveX", RevKey.A, RevKey.D); // 或者:手柄也用命名轴
没配轴名时会按 0 处理并只报警一次(不会每帧刷屏)。
四触屏与手势(7 种)
编辑器里用鼠标拖拽就能试:Unity 默认会模拟触摸(Input.simulateMouseWithTouches)。
| 手势 | 怎么用 | 参数 |
|---|---|---|
| Tap 轻点 | RevInput.Tap(out var t) → t.X / t.Y | 最长按住 TapMaxSeconds、最大位移 TapMaxDistance |
| DoubleTap 双击 | RevInput.HasGesture(RevGestureKind.DoubleTap) | 间隔 DoubleTapSeconds(默认 0.3s) |
| LongPress 长按 | RevInput.HasGesture(RevGestureKind.LongPress) | 门槛 LongPressSeconds(默认 0.5s,只报一次) |
| Drag 拖动 | RevInput.Gesture(i) → DeltaX / DeltaY(每帧一条) | 门槛 DragThreshold(默认 8 像素) |
| Swipe 滑动 | RevInput.Swipe(out var sw) → sw.Direction(八向)+ sw.Value(速度) | SetSwipeThreshold(距离, 速度) |
| Pinch 捏合 | RevInput.PinchScale(本帧缩放比例,1 = 不变) | 双指 |
| Rotate 旋转 | RevInput.RotateDegrees(本帧角度增量,度) | 双指 |
// 相机缩放(双指) if (RevInput.HasGesture(RevGestureKind.Pinch)) camera.orthographicSize = Mathf.Clamp(camera.orthographicSize / RevInput.PinchScale, 2f, 12f); // 闪避(四向滑动) if (RevInput.Swipe(out var sw)) { switch (sw.Direction) { case RevSwipeDirection.Left: Dodge(Vector3.left); break; case RevSwipeDirection.Right: Dodge(Vector3.right); break; // 斜向也能收到:UpLeft / UpRight / DownLeft / DownRight } }
Block(World) 一起挡掉
而且不会秋后算账:被屏蔽期间开始的手势,抬手时不会补发 Tap/Swipe ——
否则"关掉弹窗"就会莫名其妙多出一次点击(这是触屏最常见的手感 bug)。
五弹窗 / 暂停:屏蔽输入的正确姿势
弹窗打开时:挡世界输入,但不要把整个模块关掉(ESC、返回键会一起失灵)。
// ① 最省心:作用域(出块自动解除屏蔽 + 自动退订事件) using (var scope = RevInput.OpenScope()) { scope.Block(); // 只挡世界输入(动作 + 手势) scope.OnPressed("Confirm", Save); // 事件也挂在这个作用域上 ShowDialog(); } // 出块:屏蔽解除,事件全部退订 // ② 手动管句柄 var block = RevInput.Block(); … block.Close(); // ③ 只挡某根手指(比如"左手在拖镜头,右手点技能") RevInputBlock pinch = RevInput.Block(RevInputBlockKind.Pointer, RevInput.PointerId(0)); // ④ 全挡(过场/加载:指针位置也读不到) RevInput.Block(RevInputBlockKind.All);
| 场景 | 用什么 |
|---|---|
| 弹窗、设置面板、暂停菜单 | Block(World)(默认) |
| 教程遮罩(只让点指定区域) | Block(World) + 自己判断 IsPointerOverUI() |
| 过场动画、加载 | Block(All) |
| "这根手指归摇杆,别当点击" | Block(Pointer, pointerId) |
| 对象销毁时的兜底 | RevInput.OffAllOf(this)(事件 + 屏蔽一起清) |
屏蔽期间按下的时间仍会记录 —— 解除后
PressedBuffered 的缓冲窗口依然有效(手感不丢)。
RevInput.IsPointerOverUI(id) 会问 UI 系统的 EventSystem(框架不去改 EventSystem)。常见写法:
if (RevInput.Tap(out var t) && !RevInput.IsPointerOverUI(t.PointerId)) { 处理世界点击 }。UI 按钮请用 UI 系统自己的回调(
RevUIPanel.OnClick)—— 本模块只管世界输入。
六改键与存档(文本格式)
绑定表可以整体导出/载入,玩家改键就存这段文本。
# RevInput 绑定表(动作 = 输入源, ...)
Jump = Space, JoystickButton0
Fire = Mouse0, repeat:0.20/0.05
MoveX = -A, +D
MoveY = -S, +W
LookX = axis:Mouse X
string text = RevInput.SaveBindings(); // 导出(存 PlayerPrefs / 文件都行) RevInput.LoadBindings(text); // 载入(整体替换,不是合并) if (RevInput.LoadBindings(badText) == false) RevInput.Failed += (reason, detail) => …; // 坏存档被拒绝且不落地 string conflicts = RevInput.FindConflicts(); // "Space 同时绑给了 Jump 和 Dash"(改键后必须查一次)
| 规则 | 说明 |
|---|---|
# 开头 | 注释行,忽略 |
动作 = 输入源, 输入源 | 键位直接写名字;鼠标写 Mouse0..Mouse4 |
-A / +D | 键位轴(负向 / 正向) |
axis:名字 | 命名轴(axis:Mouse X;末尾加 - 表示取反) |
deadzone=0.2 | 可选:覆盖这个动作的死区 |
repeat:延迟/间隔 | 可选:连发节拍(秒);延迟 ≤ 0 = 关连发。SaveBindings 会带上非默认的连发配置 —— 存档 ⇄ 读档无损往返 |
| 解析失败 | 整体拒绝、旧绑定保留(不会出现"载入一半"的坏状态),原因走 RevInput.Failed |
七新手最容易踩的 6 个坑
| 坑 | 正确做法 |
|---|---|
① 业务里直接写 Input.GetKeyDown(KeyCode.Space) | 一律走 RevInput.Pressed("动作名") —— 否则改键、屏蔽、缓冲全部失效 |
② 弹窗时把整个模块关掉(SetEnabled(false) 那种写法) | 用 Block(World);ESC / 返回键要继续可用 |
| ③ 忘记在对象销毁时退订 | 订阅时给 owner: this,销毁时 RevInput.OffAllOf(this)(或用 OpenScope()) |
④ 拿 Pressed 当"按住了"用 | Pressed 只成立一帧;持续用 Held,松开用 Released |
| ⑤ 手势与 UI 抢:点按钮时还触发了世界点击 | 先 if (RevInput.IsPointerOverUI(id)) return; |
| ⑥ 切场景/结算后输入状态没清 | RevInput.ResetAll("换场景")(失焦已自动处理,这一步是保险) |
八出问题怎么查(3 分钟定位)
先看三样东西,再对症状。
// ① 打开诊断:绑定、屏蔽、设备切换都会走 RevLog 的 Input 标签 RevInput.VerboseLog = true; // ② 看自检:设备名 / 动作数 / 屏蔽数 / 监听数 / 指针数 / 累计帧数 Debug.Log(RevInput.Dump()); // ③ 看失败:不为 0 就订阅 Failed 看原因码(键位名不认识 / 动作名非法 / 存档格式错…) Debug.Log(RevInput.FailedCount); RevInput.Failed += (reason, detail) => Debug.LogWarning($"[Input] {reason}: {detail}");
| 症状 | 怎么查 |
|---|---|
| 按键没反应 | ① 这个动作绑了吗(ActionCount)② 键位名与 KeyCode 一致吗(不一致启动时会有一条 Warn)③ 是不是被 Block 挡住了(RevInput.ActionBlocked("Jump") 能区分"没按"与"被挡") |
| 切回来角色一直跑 | 说明有别的代码自己读了 Input(本模块的宿主会自动复位) |
| 手势不触发 | 编辑器里确认 Input.simulateMouseWithTouches 为 true(用鼠标模拟触摸),或调低 SetSwipeThreshold |
九相关文档
- 《输入系统 · 架构解析》 —— 设计论证:为什么这么分层、每帧到底做了什么、与"为帧同步设计的输入方案"相比取了什么舍了什么
- 《UI 系统 · 使用说明》 —— UI 按钮点击走它,输入模块只管世界输入
- 《公共 Mono 模块 · 使用说明》 —— 纯 C# 类要每帧跑就用它
- 文档总入口 · 在线文档站