使用说明 · 从零到能写业务

输入系统 · 使用说明

这份文档只回答一个问题:我该怎么用它?(先绑动作名,再登记一个监听者 —— 输入由框架推给你,业务里不用写 Update)

读完你能做到:3 分钟绑好跳跃与移动 · 分清"按下 / 按住 / 抬起" · 会用点击/滑动/长按/双指 · 知道弹窗怎么挡输入 · 出问题 3 分钟定位

〇它是干什么的

先花 30 秒建立直觉,再动手写。

只学 3 件事就能干活(真的)
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"),两种写法可混用。

人话 输入系统 = 你只说"什么动作、什么状态",剩下的(这个动作是空格还是手柄 A、这根手指算不算点击、弹窗开着要不要挡)它全包。

输入一发生,它主动通知你,你在回调里干活就行;键位写在绑定表里,业务代码里一个键位字面量都没有。

它替你解决的问题怎么做的
键位写死在业务里,改键要改代码动作名 + 绑定表:业务只写 "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);
}
就这样,没有第 4 步 第一次用到 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 / YVector("Move") 会去读 MoveX 与 MoveY 两个动作BindAxis("MoveX", A, D) + BindAxis("MoveY", S, W)
键位名与 Unity KeyCode 一致写 RevKey.Space / RevKey.JoystickButton0 / RevKey.LeftShift,不用记数值手柄键就是 JoystickButton0..19
轴返回的是"两个 float",不是 Vector2 内核刻意不引用 UnityEngine(这样能脱机跑断言),所以返回 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)(事件 + 屏蔽一起清)
屏蔽 World 期间,轴读数一并归零 动作相位(按下 / 按住 / 抬起)与轴都会被挡:弹窗里角色不会继续移动(轴属于世界输入)。
屏蔽期间按下的时间仍会记录 —— 解除后 PressedBuffered 的缓冲窗口依然有效(手感不丢)。
"点在 UI 上"永远由你自己判 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

九相关文档