Up to date

This page is up to date for Godot 4.3. If you still find outdated information, please open an issue.

控制器、手柄和摇杆

Godot 支持数百种控制器模型,这要归功于社区提供的 SDL 游戏控制器数据库

控制器支持 Windows、macOS、Linux、Android、iOS 和 HTML5。

请注意,诸如方向盘、方向盘踏板和 HOTAS 等更专业的设备测试较少,可能并不总是按照预期工作。目前尚未实现在这些设备上的力反馈覆盖。如果你有机会使用这些设备,请不要犹豫,在 GitHub 上报告错误。

在本指南中,你将学会:

  • 如何编写你的输入逻辑,从而支持键盘和控制器输入。

  • 控制器的行为如何与键盘/鼠标输入不同。

  • 解决 Godot 中控制器的问题。

支持通用输入

得益于 Godot 的输入动作系统,Godot 可以同时支持键盘和控制器输入,而不需要编写单独的代码路径。你不应该在脚本中对控制器的按键进行硬编码,应该在项目设置中创建输入动作,这些动作引用按键和控制器输入。

输入动作在 使用 InputEvent 页面上有详细解释。

备注

与键盘输入不同,支持鼠标和控制器输入的动作(例如在第一人称游戏中环顾四周)将需要不同的代码路径,因为它们必须分开处理。

我应该使用哪个输入单例方法?

有 3 种方式可以以模拟感知的方式获得输入:

  • 当你有两个轴(例如摇杆或 WASD 运动)并且希望两个轴都作为单个输入时,请使用 Input.get_vector()

# `velocity` will be a Vector2 between `Vector2(-1.0, -1.0)` and `Vector2(1.0, 1.0)`.
# This handles deadzone in a correct way for most use cases.
# The resulting deadzone will have a circular shape as it generally should.
var velocity = Input.get_vector("move_left", "move_right", "move_forward", "move_back")

# The line below is similar to `get_vector()`, except that it handles
# the deadzone in a less optimal way. The resulting deadzone will have
# a square-ish shape when it should ideally have a circular shape.
var velocity = Vector2(
        Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
        Input.get_action_strength("move_back") - Input.get_action_strength("move_forward")
).limit_length(1.0)
  • 当你有一个轴可以双向移动时(比如飞行摇杆上的油门),或者你想单独处理不同的轴时,使用 Input.get_axis() :

# `walk` will be a floating-point number between `-1.0` and `1.0`.
var walk = Input.get_axis("move_left", "move_right")

# The line above is a shorter form of:
var walk = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
  • 对于其他类型的模拟输入,例如处理一个触发器或一次处理一个方向,使用 Input.get_action_strength()

# `strength` will be a floating-point number between `0.0` and `1.0`.
var strength = Input.get_action_strength("accelerate")

对于非模拟数字/布尔输入(只有 "按下 " 或 "未按下 " 的值),如控制器按钮、鼠标按钮或键盘按键,使用 Input.is_action_pressed()

# `jumping` will be a boolean with a value of `true` or `false`.
var jumping = Input.is_action_pressed("jump")

备注

如果你想要知道上一帧是否刚刚按下了某个输入,请使用 Input.is_action_just_pressed(),不要使用 Input.is_action_pressed()Input.is_action_pressed() 是只要输入处于按下的状态就会返回 true,而 Input.is_action_just_pressed() 只会在按下按钮后的一帧内返回 true

振动

振动(也叫触觉反馈)可以用来提升游戏手感。比如在赛车游戏中,可以通过振动来体现车辆当前所处的路面,也可以在撞车时进行突然的振动。

请使用 Input 单例的 start_joy_vibration 方法开启游戏手柄的振动。要提前结束振动,请使用 stop_joy_vibration(尤其适用于启动时未指定时长的情况)。

在移动设备上,你还可以使用 vibrate_handheld 来振动设备本身(与游戏手柄的振动是分开的)。在 Android 上,这个功能需要在导出项目前启用 Android 导出预设中的 VIBRATE 权限。

备注

振动可能造成某些玩家的不适。请确保在游戏中提供滑块,用来禁用振动或降低振动强度。

键盘/鼠标和控制器输入之间的差异

如果你习惯于处理键盘和鼠标输入,可能会对控制器处理特定情况的方式感到惊讶。

死区

与键盘和鼠标不同,控制器提供带有模拟输入的轴。模拟输入的好处是它们为动作提供了额外的灵活性。不像数字输入只能提供 0.01.0 的强度,模拟输入可以提供 0.01.0 之间的任何强度。缺点是没有死区系统,由于控制器的物理结构,模拟轴的强度永远不会等于 0.0。相反,它将徘徊在一个低值,如 0.062。这种现象被称为漂移,在旧的或有问题的控制器上会更加明显。

让我们把赛车游戏作为一个现实世界的例子。由于有了模拟输入,我们可以将汽车慢慢地转向一个或另一个方向。然而,如果没有死区系统,即使玩家不接触操纵杆,汽车也会自己慢慢转向。这是因为方向轴的强度在我们期望的时候不会等于 0.0。因为我们不希望我们的车在这种情况下自动转向,我们定义了一个“死区”值 0.2,它将忽略所有强度低于 0.2 的输入。一个理想的死区值是足够高的,可以忽略操纵杆漂移引起的输入,但又足够低,不会忽略玩家的实际输入。

Godot 提供了内置的死区系统来解决这个问题。默认值是 0.5,但你可以在“项目设置”的“输入映射”选项卡中针对具体的动作进行调整。Input.get_vector()可以在第五个参数中指定死区。如果没有指定,则会计算向量中的所有动作死区的平均值。

“回显”事件

与键盘输入不同,按住一个控制器按钮,如十字方向键,不会产生固定间隔的重复输入事件(也被称为“回显”事件)。这是因为操作系统首先不会为控制器输入发送“回显”事件。

如果你想让控制器按钮发送回显事件,你将不得不通过代码生成 InputEvent 对象,并使用 Input.parse_input_event() 定期解析它们。这可以在 Timer 节点的帮助下完成。

窗口焦点

与键盘输入不同,控制器的输入可以被操作系统中的所有窗口看到,包括未持有焦点的窗口。

虽然这对于第三方分屏功能很有用,但也可能产生不利影响。玩家在与另一个窗口互动时可能会意外地将控制器输入传送到正在执行的项目。

如果你希望在项目窗口未聚焦时忽略事件,则需要使用以下脚本创建一个名为 Focus自动加载,并使用它来检查所有输入:

# Focus.gd
extends Node

var focused := true

func _notification(what: int) -> void:
    match what:
        NOTIFICATION_APPLICATION_FOCUS_OUT:
            focused = false
        NOTIFICATION_APPLICATION_FOCUS_IN:
            focused = true


func input_is_action_pressed(action: StringName) -> bool:
    if focused:
        return Input.is_action_pressed(action)

    return false


func event_is_action_pressed(event: InputEvent, action: StringName) -> bool:
    if focused:
        return event.is_action_pressed(action)

    return false

然后,不要使用 Input.is_action_pressed(action),而是使用 Focus.input_is_action_pressed(action),其中 action 是输入动作的名称。另外,不要使用 event.is_action_pressed(action),而是使用 Focus.event_is_action_pressed(event, action),其中 event 是 InputEvent 引用,action 是输入动作的名称。

防止省电模式

与键盘和鼠标输入不同,控制器输入不会抑制睡眠和省电措施(例如在经过一定时间后关闭屏幕)。

为了解决这个问题,Godot 在项目运行时默认启用预防省电。如果你注意到在使用游戏手柄玩游戏时系统正在关闭其显示屏,请检查项目设置中的显示 > 窗口 > 节能 > 保持屏幕开启的值。

在 Linux 上,要防止省电,引擎必须能够使用 D-Bus。如果在 Flatpak 中运行项目,请检查 D-Bus 是否已安装且可以访问,因为沙盒限制默认可能会导致无法实现这一点。

故障排除

参见

你可以在 GitHub 上查看控制器支持的已知问题列表

Godot 无法识别我的控制器。

首先,检查你的控制器是否被其他应用程序识别。你可以使用 Gamepad Tester 网站来确认你的控制器是否被识别。

在 Windows 上,Godot 一次最多支持 4 个控制器。这是因为 Godot 使用了 XInput API,该 API 一次最多支持 4 个控制器。超过该限制的其他控制器将被 Godot 忽略。

我的控制器的按钮或轴映射不正确。

首先,如果你的控制器提供某种固件更新实用程序,请确保运作它以从制造商处获取最新修复程序。例如,Xbox One 和 Xbox 系列控制器可以使用 Xbox 附件应用 更新固件。(此应用程序仅在 Windows 上执行,因此你必须使用 Windows 电脑或支持 USB 的 Windows 虚拟机来更新控制器的固件。)更新控制器的固件后,如果你在无线模式下使用控制器,请取消配对控制器并将其重新与你的 PC 配对。

如果按钮存在映射错误,可能是由于来自 SDL 游戏控制器数据库的错误的映射。你可以在链接的仓库中提交拉取请求,为下一个 Godot 版本提供映射更新。

有很多方法可以创建映射。一种选择是使用官方 Joypads 演示中的映射向导。一旦你为控制器建立了可用的映射,你就可以在运行 Godot 之前通过定义 SDL_GAMECONTROLLERCONFIG 环境变量来测试它:

export SDL_GAMECONTROLLERCONFIG="your:mapping:here"
./path/to/godot.x86_64

要在非桌面平台上测试映射,或者用额外的控制器映射来分发你的项目,你可以通过调用 Input.add_joy_mapping() 尽早在脚本的 _ready() 函数中添加它们。

我的控制器在特定的平台上工作,但在另一个平台上却不能。

Linux

如果你使用自编译引擎二进制文件,请确保它是使用 udev 支持进行编译的。默认启用该功能,但可以通过在 SCons 命令列上指定 udev=no 来禁用 udev 支持。如果你使用的是 Linux 发行版提供的引擎二进制文件,请仔细检查它是否是使用 udev 支持进行编译的。

控制器在没有 udev 支持的情况下仍然可以工作,但可靠性较差,因为必须使用定期轮询来检查游戏过程中控制器是否连接或断开连接(热插拔)。

HTML5

与“原生”平台相比,HTML5 控制器的支持通常不太可靠。不同浏览器对控制器的支持质量往往差别很大。因此,如果玩家无法使用控制器,你可能需要指示他们使用其他浏览器。