· 4 分钟阅读 · 1662 字

摇杆斜向输入为何迟迟不动:径向死区必须重映射长度

把同一个固定摇杆输入从中心推到长度 0.3:向右已经产生输出,沿 45° 推动却仍然为零;后者需要更大的输入半径才会启动,方向还会先贴向某条坐标轴。对照只改变方向,差异来自运行时把两个轴分别做了死区处理。

本文固定内死区为 0.25,对照逐轴与径向两条路径。输入是设备层完成归一化后的二维向量,每个分量位于 [-1,1];输出交给移动、瞄准或 UI 导航,不在这里叠加加速度曲线和方向吸附。

摇杆与纸面圆环表达径向死区的概念摄影,非设备测试现场

方形静止区改变了启动半径

逐轴算法通常对 X、Y 各自执行同一函数:绝对值不超过 d 就归零,超过后把 [d,1] 线性映射到 [0,1]。沿正 X 方向,输入半径超过 d 即可启动;沿 45° 方向,两个分量都等于 r/√2,因此必须满足 r>d√2。当 d=0.25 时,正轴阈值是 0.25,45° 阈值约为 0.353553。

这不是手感参数的细微差别。两个分量各自归零,整体静止区变成方形。半径 0.3 的正轴输入 (0.3,0) 得到长度 0.0666667,等长的 45° 输入 (0.212132,0.212132) 却被归零。阈值外也会继续扭曲方向:两个分量被独立减去 0.25,向量方向不再只由原始摇杆角度决定。

技术图左侧的浅色十字表示“至少一个分量仍被截断”,只有中心重叠的深色方块表示 X、Y 同时归零;十字臂上仍可能保留另一轴输出。右侧圆内则是整个二维向量归零。

逐轴分量截断区域与径向整体死区在固定输入下的输出差异

死区要消除的是中心附近的二维噪声,因此判定量应是向量长度 m=|v|。m≤d 返回零;其余输入保持单位方向 v/m,只把长度改为 (min(m,1)-d)/(1-d)。减去内死区后再除以剩余跨度,才能让刚离开死区时连续从零开始,并在物理满量程时到达一。只做 m-d 会让最大输出停在 1-d;若上层仍使用原来的速度标量,角色达不到预定最大速度。

核心实现位于 .tmp/daily-labs/2026-10-26-1930/StickDeadzone.cs,调用链为 Program → StickDeadzone.Radial → Validate:

using System.Numerics;

public static class StickDeadzone
{
    public static Vector2 Radial(Vector2 input, float deadzone)
    {
        Validate(input, deadzone);
        float magnitude = input.Length();
        if (magnitude <= deadzone)
            return Vector2.Zero;

        float clampedMagnitude = MathF.Min(magnitude, 1f);
        float outputMagnitude = (clampedMagnitude - deadzone) / (1f - deadzone);
        return input / magnitude * outputMagnitude;
    }

    private static void Validate(Vector2 input, float deadzone)
    {
        if (!float.IsFinite(input.X) || !float.IsFinite(input.Y))
            throw new ArgumentOutOfRangeException(nameof(input));
        if (!float.IsFinite(deadzone) || deadzone < 0f || deadzone >= 1f)
            throw new ArgumentOutOfRangeException(nameof(deadzone));
        if (MathF.Abs(input.X) > 1f || MathF.Abs(input.Y) > 1f)
            throw new ArgumentOutOfRangeException(nameof(input));
    }
}

分量允许落在单位方形内,所以 (1,1) 合法;它来自某些设备原始轴时,长度会超过一。实现先保留原方向,再把参与长度映射的值钳到一,输出不会越过单位圆。这里不能先把每个分量夹紧后就直接交给玩法层,否则对角线仍会达到 √2 倍速度。

deadzone=0 退化为只做外圆钳制,deadzone=1 则没有可映射的有效区间,因而被拒绝。非有限分量和超过设备归一化合同的单轴值也直接失败,避免把配置或采集错误伪装成静止输入。等号归入静止区,使阈值点输出严格为零;这个分支与映射公式在阈值外连续。

固定半径应给所有方向同一响应

探针先验证中心噪声、阈值等号、正轴、45°、满量程、方形角点、负方向和非法输入,再枚举 101 个半径与 360 个整数角,共 36,360 组。半径 0.3 上,径向输出长度最小值为 0.06666664,最大值为 0.06666672;逐轴输出从 0 到 0.066666685。径向路径的最大方向误差为 0.03426402°,来自单精度归一化与反余弦回读,不是有意的角度修正。

执行记录如下,$LAB_ROOT 表示本文探针目录:

$ cd $LAB_ROOT
$ dotnet build DeadzoneLab.csproj -c Release --nologo
Build succeeded. 0 Warning(s), 0 Error(s)
exit code: 0

$ dotnet run --project DeadzoneLab.csproj -c Release --no-build
passed=23 failed=0 skipped=0 samples=36360 elapsed_ms=63.5175
fixed_input=(0.21213204,0.21213204) magnitude=0.3
radial_output=(0.047140464,0.047140464) magnitude=0.066666685
axial_output=(0,0) magnitude=0
exit code: 0

这次耗时只证明当天检查实际执行,不表示输入系统的性能。角度扫描证明当前数值范围内的旋转对称性,但没有测量具体手柄的中心漂移、轴最大值或回报率;设备层仍需按型号采集噪声包络,并把归一化快照交给这条纯函数。

曲线、吸附与设备校准留在边界两侧

径向死区只解决“哪个二维输入视为静止”和“剩余长度怎样填满输出区间”。若产品需要低速精细控制,可以在径向重映射后对标量长度应用幂曲线,再乘回同一单位方向。若需要八方向 UI 导航,应在死区之后做角度量化;把吸附提前,会让噪声方向影响是否越过阈值。

椭圆形硬件行程是另一类问题。若某根摇杆的不同方向无法到达同一原始长度,应先依据校准数据把设备包络变换到单位圆,再执行径向死区。本文的 min(m,1) 只提供安全饱和,不会恢复椭圆包络中缺失的动态范围。内死区也不应被自动放大来掩盖磨损:阈值越大,可用行程越短,随后重映射的增益越高,中心附近的小变化反而更敏感。

开篇的延迟启动由逐轴阈值直接造成:它让有效区的内边界随方向变化。输入层以长度拥有静止判定、以单位方向保留角度,并把有效半径连续映射到 [0,1] 后,移动与瞄准系统才能在不同方向上消费同一份强度合同。本文没有处理外死区、迟滞、时间滤波和设备热插拔;这些策略可以继续组合,但不应重新把二维向量拆回两条互不知情的轴。