HelixWorld 接自己的控制输入——该改哪一层才不会把实时性拖垮?

文章导读
接自定义输入时,先把改动落在输入适配层,不要动 HelixWorld 的主循环或渲染步进。主循环通常承担固定步长推进、状态更新和调度,改动它会作用到所有输入路径,延迟问题也更难隔离;适配层只负责把新设备的数据转成核心已经认识的输入事件。判断标准不是“代码少”,而是“是否改变了核心循环每帧的时间预算”。
📋 目录
  1. Ⅰ 先确认现有输入是从哪里进来的
  2. Ⅱ 在不改核心循环的前提下加一路输入
  3. Ⅲ 测改动前后的端到端延迟
  4. Ⅳ 处理新输入与原有输入的冲突
  5. Ⅴ 把接入方式做成可回滚的一层
A A

接自定义输入时,先把改动落在输入适配层,不要动 HelixWorld 的主循环或渲染步进。主循环通常承担固定步长推进、状态更新和调度,改动它会作用到所有输入路径,延迟问题也更难隔离;适配层只负责把新设备的数据转成核心已经认识的输入事件。判断标准不是“代码少”,而是“是否改变了核心循环每帧的时间预算”。

若目标是接入键盘、手柄或传感器,建议把新输入收敛到一层适配代码:采样、归一化、映射到已有输入结构,再由核心循环按原节奏读取。验证方式用采样时间戳与状态生效时间戳对照,看端到端延迟和抖动是否变化。风险边界是:若必须修改核心循环的读取顺序或加入阻塞等待,实时性不再由适配层保证,需要单独评估后再动。

先确认现有输入是从哪里进来的

不清楚输入从哪进来,就很容易改错位置。先用检索把可疑范围缩小,再根据调用栈判断它是事件驱动还是每帧轮询。

grep -rn `--include`='*.c' `--include`='*.cpp' `--include`='*.h' \
  -E 'input|key|gamepad|axis|poll' . | head -50

把结果按目录归类,常见会出现 input/、input_adapter、input_system 这类路径。接着确认关键函数是否被主循环调用:如果函数只出现在事件回调里,说明是事件驱动;如果每帧都会走到,说明是轮询。把文件路径、函数名和调用它的那几行记录下来,后面选改动点就靠这些证据,而不是靠猜测。

在不改核心循环的前提下加一路输入

适配层只做三件事:读取设备、归一化数值、写入核心已有的输入结构。不要在里面做阻塞等待、平台弹窗或长时间初始化。下面是一个通用骨架,接口名按你项目里的实际命名替换。

// 放在 input/adapter/ 下,避免直接改主循环
struct CustomInputState {
    float axis_x;
    float axis_y;
    bool  button_a;
    long  sample_time_ms;
};

class CustomInputAdapter {
public:
    bool init();
    // 只读取和归一化,不做阻塞等待
    bool poll(CustomInputState& out);
    void shutdown();
};

接入时优先复用已有的输入读取入口。如果核心已经有一个“读取本帧输入”的函数,就在它内部增加对适配器的调用,而不是在主循环里额外插入新分支。最小可运行验证分三步:先只编译适配器,单独调用 poll 并打印原始值;再打开开关,打印映射后的值,确认与设备动作一致;最后关闭开关,确认原有输入行为不变。接口细节不确定时,先只验证“读到值”,不要急着做映射。

HelixWorld 接自己的控制输入——该改哪一层才不会把实时性拖垮?

测改动前后的端到端延迟

延迟要看你关心的那一段,不要只看适配层内部耗时。比较稳妥的做法是记录两个时间戳:设备采样或适配层读到的时刻 t0,核心状态生效的时刻 t1,两者的差值就是这一路的端到端延迟。拿不到设备时间戳时,就用适配层 poll 返回的时刻作为内侧延迟,并在记录里注明口径。

# 日志格式示例,字段按你的项目替换
LAT sample=... adapter_out=... core_apply=... delta_ms=...

改动前后用同一套操作重复采几组,记录 delta 的最大值、最小值和大致分布,不要只记一次平均值。对照表可以先写成这样:

测试项            改动前 delta_ms      改动后 delta_ms     备注
单次短按          记录区间             记录区间            同一次操作
持续按住          记录区间             记录区间            观察是否累计变慢
快速交替          记录区间             记录区间            观察丢帧或抖动
关闭新输入        基线                 基线                确认无回归

如果改动后延迟分布明显变宽,先查适配层是否在 poll 里做了耗时操作,再看新输入是否被放到了核心步进之后。

HelixWorld 接自己的控制输入——该改哪一层才不会把实时性拖垮?

处理新输入与原有输入的冲突

两路输入写同一个状态时,必须有一个明确的裁决规则,否则会出现互相打断。常见做法是给每个来源一个优先级和一个“持有窗口”:高优先级来源活动时,低优先级不写入;高优先级来源静默超过窗口后释放控制权。互斥可以按轴粒度做,同一根轴同一时刻只允许一个来源拥有。

  • 键盘与新输入同时给相反方向:检查最终值只来自高优先级来源,而不是两者相加。
  • 新输入保持时松开键盘:检查方向不会跳变或归中。
  • 新输入断开后:检查在约定帧数内归零或交还给原输入。
  • 高优先级持续按住:检查低优先级输入不会间歇性生效。

这些用例可以在适配层外单独跑,不需要动核心逻辑。测试时打开适配层日志,直接看每帧胜出的来源和最终输出值。

把接入方式做成可回滚的一层

回滚能力要在接入时就设计好,而不是出问题后再补。最省事的方式是给适配层一个独立开关,默认关闭,开启时才实例化。

# 配置或启动参数示例
enable_custom_input=false
custom_input_device=auto

回滚步骤:先把配置改回关闭,再重启或让系统重新加载配置,确认适配器没有被实例化,最后用原有输入做一遍短按、长按和方向测试。验证动作包括:日志中不再出现适配层输出、原有输入延迟回到改动前的记录区间、核心循环的调用路径与改动前一致。开关本身不要依赖新设备是否在线,设备异常时也要能退回原状。