谢夏戈 @ xiexiage.com

手柄

Sep 9

为什么这一章单独列?因为"我测过了"十有八九是用鼠标测的。鼠标点哪个就是哪个,不需要"当前选中"这个概念;手柄没有指针,界面必须自己记住"现在轮到哪个按钮"——这就是焦点。鼠标能用、手柄不能用,是新手最常交出去的成品。Steam Deck 玩家打开你的游戏,第一个按钮按不动,就关了。

焦点三条规矩

规矩为什么
① 每个面板出现时,grab_focus() 给它的第一个按钮不给,手柄按 A 是按在"空气"上。第 7、8 章各有一行就是干这个的
② 先 visible = true,再 grab_focus()隐藏的控件拿不到焦点,顺序反了等于没写
③ 同一个容器里的控件,十字键自动上下跳Godot 自带;跨面板不会自动跳,所以每个面板都要自己给一次焦点

十字键能移亮框,按 A 却按不下按钮

我当时的现象:菜单里十字键上下能在控件之间跳,亮框也在动,但按 A 什么都不发生,键盘 Enter 却可以。

按钮响应手柄,靠的是 Godot 内置动作 ui_accept。去源码里查了一下(core/input/input_map.cpp):Godot 4 默认只给它绑了 Enter、小键盘 Enter、空格,没有手柄键。方向类的 ui_up / ui_down / ui_left / ui_right 有十字键和左摇杆,ui_select 有手柄 Y,但确认(ui_accept)和取消(ui_cancel)都没有。所以十字键能移亮框、A 按不下去,是引擎默认值就这样。

修法:项目设置 → 输入映射 → 勾「显示内置动作」→ ui_accept+ → 按手柄 A;ui_cancel+ → 按 B。没有"一键给所有内置动作加手柄"的开关,但需要补的也只有这两个,而且存在 project.godot 里,以后抽模板项目会跟着走。

补完还不行?在 menu.gd 里加探针:

func _input(event: InputEvent) -> void:
    if event is InputEventJoypadButton and event.pressed:
        print("手柄 ", Input.get_joy_name(0), " | 按钮编号 ", event.button_index, " | 认成确认键=", event.is_action_pressed("ui_accept"))
按 A 时打印意思
有编号,=true已经认成确认键了,问题在焦点——回去看亮框在不在按钮上
有编号,=false这个键还没加进 ui_accept(或加的时候按错了键)
什么都不打印面键事件没进 Godot:关掉 Steam 再试(Steam 会接管手柄)

B / X / Y "没反应"是正常的——B 只对应"取消",X、Y 默认没绑任何 UI 动作。

另一个和手柄无关、但害我半小时的开关

Godot 4.4 起游戏默认嵌在编辑器里跑。「游戏」标签的工具栏有三个模式:输入 / 2D / 3D。只有「输入」模式按键才进游戏;2D / 3D 是运行时用鼠标点选节点的,会把输入截走。代码没错的时候,问题十有八九在"事件有没有送到",先看这个开关。

手柄专项验收

把鼠标推开,只用手柄走完整条链,每一步都看清楚亮框在哪:

  • 菜单:十字键能在选项和 Start 之间跳;A 能切换选项
  • Start → 进游戏,十字键 / 摇杆能转弯
  • Start 键 → 暂停面板出现,"继续"亮框;A → 继续
  • 再暂停 → 下到"重开" → A → 新一局,蛇在动
  • 撞死 → 结算面板,"重开"亮框;下到 Menu → A → 回到菜单,焦点在选项上

五条全过,这个游戏就是 Steam Deck 可玩的。有一条卡住 = 那个面板缺 grab_focus()

这条清单以后每个游戏都要跑。手柄不是一个功能,是一种假设——第一天就绑,最后补要把所有 UI 的焦点重走一遍。

2023-PRESENT © 谢夏戈