手感「黏笨」不是延迟,是你的摇杆设计逼玩家先拖一段

移动端摇杆的相对式 vs 混合式——一个 5 分钟就能读代码定位、却容易被误诊成「输入延迟」的手感陷阱。

现象

玩家反馈:「拖动摇杆到角色动起来之间有明显延迟。」

第一反应大多数人(包括当时的我)会往这几个方向猜:

  • 采样频率不够(pointermove 被回调节流了?)
  • 加速曲线起步太缓(lerp(0, maxSpeed, t) 的 t 曲线?)
  • 触摸事件死区太大
  • 首帧输入延迟(touchstart 到下一次 requestAnimationFrame 之间?)

这些都是可能的原因。但在动手改任何一个之前,先读一遍摇杆的 pointerdown handler,往往能一分钟定位到真正的根因。

根因:相对式摇杆的先天延迟

看到的代码大致长这样:

onPointerDown(p) {
  this.stick.cx = p.x;      // 摇杆中心 = 手指落下的位置
  this.stick.cy = p.y;
  this.stick.dx = 0;        // 初始位移 = 0
  this.stick.dy = 0;
}

这是一个典型的相对式(relative)摇杆——摇杆基座跟着手指落点走,落下瞬间基座和手指重合,dx / dy = 0,角色不动

玩家必须从落点开始拖动,拖出一段距离后 dx / dy 才有值,角色才动起来。这段「先拖一段」的距离,就是他感知到的「延迟」。

它根本不是延迟,是设计要求的空拖。 没有任何一行采样节流、加速曲线、死区参数能解决这个问题——因为源头在 pointerdown 那一瞬间,dx / dy 被人为清零了。

混合摇杆:首帧就响应

修复思路很简单:落下瞬间就要有方向输入。三个小改动:

1. 保持上一次摇杆中心,落点直接算 dx/dy

onPointerDown(p) {
  // 不重置 cx/cy,用上一次的中心(或屏幕左半区默认中心)
  this.stick.dx = p.x - this.stick.cx;
  this.stick.dy = p.y - this.stick.cy;
}

首帧手指落下就有非零位移,角色立刻朝那个方向蹿。

2. 动态基座跟随(避免拉不到边)

如果手指离原基座太远,基座跟着手指移动,保持一个最大摇杆半径:

const len = Math.hypot(dx, dy);
if (len > MAX_R) {
  this.stick.cx += (len - MAX_R) * dx / len;
  this.stick.cy += (len - MAX_R) * dy / len;
}

3. 最小死区

len < 8px 才算 0,避免手指微抖时角色反复启停:

if (len < 8) { dx = 0; dy = 0; }

为什么值得写下来

这个坑的价值不在「摇杆代码怎么改」——网上模板一搜一大把。价值在诊断路径

  1. 用户描述「有延迟」→ 别急着改采样 / 曲线 / 死区
  2. 先读 pointerdown handler 那 5 行代码
  3. dx / dy 在落下瞬间是不是 0
  4. 是 → 根因锁定,是「相对式先天延迟」,不是任何数值参数问题

绝大多数移动端手感问题里,「玩家说的延迟」和「代码里真正在延迟的地方」经常不是同一个东西。用户能描述症状,但根因命名是设计者的活。命名错了,就会去调错的旋钮,调半天还回来说「奇怪,改了几个参数怎么还是黏」。

顺带一个通用推论:能直接读代码定位的时候,别用手感猜。手感是最后一步的验收,不是诊断工具。


马启航Marvis