移动输入优化

2026-07-08 · unity

这篇记录 2D 移动输入的一次重构:把左右移动的输入从轴输入(GetAxisRaw)改成”最后按下的键优先”(last-pressed-wins),解决转向时闪一帧 Idle 的问题。

演示

如下分别为解决方案在游戏内的具体体现和关键变量的变化过程,观察可知我们将转向时0这一状态彻底的抹去了。

具体分析与实现请看下文。

问题:一直往右跑,突然按左,角色闪一帧 Idle

现象很具体:角色匀速向右跑,这时按下左方向,画面里角色会先闪一下站立的 Idle 动画,再转身往左跑。这一闪极短,但肉眼能察觉,手感上就是”一顿”。

原理:轴输入在转向时必然经过 0

先看移动和动画是怎么连起来的:

问题就在这:角色往右跑时轴值是 1,突然按左,轴值要从 1 变成 -1,中间必然经过 0。经过 0 的那一帧,Speed = 0,动画机判定”停下来了”,切入 Idle;下一帧值变成 -1,又切回 Run。于是闪一帧 Idle。

这个 0 是”两个键在极短时间内被先后按下”这种操作在数学上不可避免的中间态,和玩家无关——他按左就是想转向,游戏却先播放了一帧”停住”。

顺带说 GetAxis(带平滑的版本):它内部有加速度和摩擦力,手感偏”黏”,转向更迟钝,同样躲不过 0 值瞬间,只是把问题藏在了平滑曲线里。

解决思路:不从动画侧补丁,从输入源头消掉 0

动画本身一直弄不流畅,因为输入本质还是会在转向的时候有0的输入,只能用动画让转向不那么明显,且本人没有转向的素材( 因此最终选择是让水平输入在转向的时候跳过0。值就会从1跳到-1。

实现:OptimizedInput()

核心是一个持续存在的状态 horizontalMoveLastFrame,它不再是读取轴值,而是由按键事件自己维护,规则只有两条:

  1. 按下:谁后按,谁生效(最后按下的键优先)。
  2. 松开:先问另一边是否还按着——还按着就切到那边,否则归 0。
public float horizontalMoveLastFrame;

void OptimizedInput()
{
    // 按下:谁后按谁生效("最后按下的键优先")
    if (Input.GetKeyDown(KeyCode.A))
        horizontalMoveLastFrame = -1;
    if (Input.GetKeyDown(KeyCode.D))
        horizontalMoveLastFrame = 1;

    // 松开:问"另一边是否还按着"——还按着就切到那边,否则停下
    // 注意用 GetKey(查"按住"),不能用 GetKeyDown(只在一帧内为 true)
    if (Input.GetKeyUp(KeyCode.A))
        horizontalMoveLastFrame = Input.GetKey(KeyCode.D) ? 1 : 0;
    if (Input.GetKeyUp(KeyCode.D))
        horizontalMoveLastFrame = Input.GetKey(KeyCode.A) ? -1 : 0;
}

它只在 Update 里跑,结果被两处消费:

void Update()
{
    OptimizedInput();
    // ... 跳跃缓冲、冲刺冷却 ...
    UpdateCharacterFacing(horizontalMoveLastFrame);   // 朝向 + Speed 动画参数
}

void UpdateCharacterFacing(float moveX)
{
    if (moveX != 0) sr.flipX = moveX < 0;
    anim.SetFloat("Speed", Mathf.Abs(moveX));
}

void FixedUpdate()
{
    float moveX = horizontalMoveLastFrame;
    // ... 重力、土狼/缓冲、冲刺 ...
    rb.velocity = new Vector2(moveX * moveSpeed, rb.velocity.y);   // 只改 X,保留 Y
}

这样当角色按住 D(值 1)时按 A,值直接从 1 变成 -1,|值| 全程是 1,Speed 永远是 1,动画不会经过 Idle。 且后续内容中若有需要判断输入方向时,就可以直接用horizontalMoveLastFrame了。

Lessons

松开键时要用 GetKey,不能再用 GetKeyDown。 GetKeyDown 只在”按下那一帧”返回 true,之后立刻变回 false;而松开检测发生在 GetKeyUp 那一帧,此时再用 GetKeyDown 查另一边的按键,永远查不到(它只在按下帧为真)。GetKey 查的是”此刻是否按住”,才能正确回答”松开 A 时 D 还按着吗”。

这个函数只负责输入状态,不直接改物理。 它把结果存在 horizontalMoveLastFrame 里,由 FixedUpdate 每物理帧消费。输入采样在 Update(跟随帧率),物理消费在 FixedUpdate(固定 50Hz),两者解耦——按键永远不丢,物理速度永远一致。

小结

转向闪 Idle 的根源是轴输入在转向时必然经过 0,而动画恰好以 0 为 Run/Idle 的判据。与其在动画侧掩盖,不如让输入状态根本不产生 0 ,直接把 1 和 -1 之间的中间态消灭在输入源头。这也让输入状态变成”谁后按谁生效”的明确语义,后续加移动功能时更可控。