游戏手柄使用#

游戏手柄布局#

罗技 F310 游戏手柄布局示意图。
Xbox 360 游戏手柄布局示意图。
PS4/Etpark 游戏手柄布局示意图。

备注

L1left_bumperL2left_triggerR1/R2 表示右手侧的 bumpertrigger 按键。

按钮别名#

由于 PS4 风格和 Xbox 风格控制器都是合法的 FTC® 控制器,因此在 FTC SDK 中,PS4 风格和 Xbox 风格按钮命名之间存在别名映射关系。

PS4

Xbox

circle

b

cross

a

triangle

y

square

x

share

back

options

start

ps

guide

布尔输入#

TeleOp OpModes通常以迭代风格编写,其中包含通过over调用的代码的循环。在这种范式下,对用户输入的简单处理可能看起来像:

if (gamepad1.a) {
    motor.setPower(1);
}
else {
    motor.setPower(0);
}

在这种情况下,这可能会满足驾驶员的需求:当按钮被按住时,电机的功率设置为1,否则功率设置为0。由于多次向电机写入相同的功率对电机的行为没有影响,因此这完全可以正常工作。然而,当按下按钮时,想要做某事时就会出现问题。例如,写这样的东西来按下a或b来调整舵机是很有诱惑力的。

if (gamepad1.a) {
    servo.setPosition(servo.getPosition()+0.1);
}
else if (gamepad1.b) {
    servo.setPosition(servo.getPosition()-0.1);
}

但是,这将表现出不可预测的行为,因为每次按下按钮时,随着循环频率的变化以及按钮按下长度的变化,将多次调用 setPosition 方法。有一些技术可以避免这种情况,但是它们都需要将游戏手柄状态与上一个循环中的游戏手柄状态进行比较;因此,有必要存储它。

存储游戏手柄状态#

虽然每个游戏手柄输入的先前状态可以单独存储在一个变量中,例如 boolean previousA ,但这很快就会变得烦人。幸运的是,FTC SDK提供了一种复制游戏手柄状态的方法,带有 gamepad.copy(gamepadToCopy)

备注

除了存储循环前一次迭代的gamepad状态之外,还存储循环当前迭代的gamepad状态。这是必要的,因为如果按钮的状态总是从 gamepad1 / gamepad2 读取,它可能会在读取值和存储前一次值之间改变。这是因为 gamepad1 / gamepad2 同时更新 LinearOpMode ,因此可以在循环迭代期间改变。

在基于 LinearOpMode 的TeleOp程序中,存储当前和以前的游戏手柄状态可能如下所示:

public void runOpMode() {
    // By setting these values to new Gamepad(), they will default to all
    // boolean values as false and all float values as 0
    Gamepad currentGamepad1 = new Gamepad();
    Gamepad currentGamepad2 = new Gamepad();

    Gamepad previousGamepad1 = new Gamepad();
    Gamepad previousGamepad2 = new Gamepad();

    // other initialization code goes here

    while (opModeIsActive()) {
        // Store the gamepad values from the previous loop iteration in
        // previousGamepad1/2 to be used in this loop iteration.
        // This is equivalent to doing this at the end of the previous
        // loop iteration, as it will run in the same order except for
        // the first/last iteration of the loop.
        previousGamepad1.copy(currentGamepad1);
        previousGamepad2.copy(currentGamepad2);

        // Store the gamepad values from this loop iteration in
        // currentGamepad1/2 to be used for the entirety of this loop iteration.
        // This prevents the gamepad values from changing between being
        // used and stored in previousGamepad1/2.
        currentGamepad1.copy(gamepad1);
        currentGamepad2.copy(gamepad2);

        // Main teleop loop goes here
    }
 }

上升沿检测器#

为什么叫上升沿检测器?

信号沿是数字信号中的过渡,在这种情况下,数字信号是游戏手柄输入,不持有时为低,持有时为高。上升沿是从低到高的过渡,下降沿是从高到低的过渡。

方波上升沿/下降沿示意图

最常用的技术是上升沿检测器。它允许代码在最初按下按钮时只运行一次,而不是在按下按钮时运行每个循环。它的工作原理是检查按钮当前是否被按下,但在上一个循环中没有被按下。例如,在TeleOp循环内部:

if (currentGamepad1.a && !previousGamepad1.a) {
    servo.setPosition(servo.getPosition() + 0.1);
}

这将使每按 a 一次,舵机位置正好增加0.1。

下降沿检测器#

一种非常相似的技术是下降沿检测器。它允许代码在按钮释放时只运行一次,而不是在按下按钮时运行每个循环。它的工作原理是检查按钮当前是否未被按下,而是在前一个循环中被按下。例如,在TeleOp循环内部:

if (!currentGamepad1.b && previousGamepad1.b) {
    servo.setPosition(servo.getPosition() - 0.1);
}

这将减少舵机位置0.1正好每释放一次 b

备注

一个按钮可以在上升沿和下降沿运行不同的代码。这主要适用于相当复杂的交互,因此此处不演示。

切换#

上升沿检测器的一个常见用例是控制切换。开关可以用来有一个按钮让机器人在状态之间切换;例如,启动和停止吸入拾取装置。这可以针对任意数量的状态进行,但最常见的是在两个状态之间进行。为了在两个状态之间进行切换,上升沿检测器用于将布尔值设置为相反的值,然后该布尔值用于控制动作。

例子#

在初始化代码中:

boolean intakeToggle = false;

在相应的TeleOp循环内部:

// Rising edge detector
if (currentGamepad1.a && !previousGamepad1.a) {
    // This will set intakeToggle to true if it was previously false
    // and intakeToggle to false if it was previously true,
    // providing a toggling behavior.
    intakeToggle = !intakeToggle;
}

// Using the toggle variable to control the robot.
if (intakeToggle) {
    intakeMotor.setPower(1);
}
else {
    intakeMotor.setPower(0);
}

这将在按下 a 时启动吸入拾取装置,并保持运行状态,直到再次按下。

备注

驾驶员对机器人状态的了解越少,他们就越不可能搞砸。由于切换意味着每次按下按钮都会做不同的事情,请考虑替代解决方案。对于具有两个以上状态的切换尤其如此。

游戏手柄反馈#

游戏手柄反馈(即震动和LED控制)是机器人在比赛中向驾驶员传达状态的有用方式。合法游戏手柄支持此功能的程度各不相同:

  • 震动:没有

  • LED控制:无

  • 震动:大(whomp whomp)和小(bzzz)

  • LED控制:无

  • 震动:大(whomp whomp)和小(bzzz)

  • LED控制:控制RGB灯条(纯色或图案)

  • 震动:包含左右震动马达,但两者似乎都只有很小的重量(bzzz)

  • LED控制:RGBLED(纯色或图案)的控制,LED相当小且暗淡,可能不是一个好的选择。

小技巧

游戏手柄反馈可用于提醒驾驶员:游戏结束开始、吸入拾取装置加载、自动结绳完成等。

震动#

SDK提供了一个简单和更复杂的API,用于根据所需的用例控制震动。

备注

  • 震动功率指定为[0.0,1.0]范围内的浮点值。

  • 震动持续时间以毫秒为单位指定为整数。常数 Gamepad.RUMBLE_DURATION_CONTINUOUS 可用于指示震动应继续,直到命令另一个震动动作。

备注

所有震动动作都是异步完成的;即函数调用将立即返回。任何对震动API的调用都会立即替换任何当前正在运行的震动动作。也就是说,如果你命令游戏手柄震动作响750毫秒,然后立即命令震动作响250毫秒,则游戏手柄将从发出第二个命令时起震动作响250毫秒。

简单API#

命令震动的最简单方法:在指定持续时间内以100%功率震动电机1:

gamepad1.rumble(int durationMs);

如果无法控制震动马达和震动强度:

gamepad1.rumble(double rumble1, double rumble2, int durationMs);

让游戏手柄震动作响一定数量的“光点”(“光点”的概念由SDK预定义):

gamepad1.rumbleBlips(int numBlips);

辅助功能:#

这个 public boolean isRumbling() 函数提供了一个关于这个游戏手柄上是否有震动动作正在进行的有根据的猜测。机器人控制器不确定震动动作是否正在进行,因为一旦命令被发送到驱动站,驱动站就会处理运行游戏手柄效果,机器人控制器就会“放手”。

可以使用 public void stopRumble() 函数来停止任何正在进行的游戏手柄的震动动作(也许与持续时间的震动一起最有用)。

高级API#

为了创建更高级的震动行为,可以创建一个 RumbleEffect ,它由指定每个震动电机应运行的功率和持续时间的“步骤”组成。当游戏手柄被命令运行 RumbleEffect 时,它将连续执行每个“步骤”。

要创建 RumbleEffect ,必须使用RumbleEffect.Builder类。构建器提供 addStep(double rumble1, double rumble2, int durationMs) 函数用于向序列添加步骤,以及 build() 函数用于从步骤序列创建RumbleEffect。

使用构建器类的匿名实例是构造 RumbleEffect 的最干净方法,例如:

Gamepad.RumbleEffect effect = new Gamepad.RumbleEffect.Builder()
       .addStep(0.0, 1.0, 500)  //  Rumble right motor 100% for 500 mSec
       .addStep(0.0, 0.0, 300)  //  Pause for 300 mSec
       .addStep(1.0, 0.0, 250)  //  Rumble left motor 100% for 250 mSec
       .addStep(0.0, 0.0, 250)  //  Pause for 250 mSec
       .addStep(1.0, 0.0, 250)  //  Rumble left motor 100% for 250 mSec
       .build();

一旦创建了 RumbleEffect ,它可以通过调用发送到游戏手柄:

gamepad1.runRumbleEffect(effect);

LED控制#

备注

  • RGB LED分量(即红、绿、蓝)的强度被指定为[0.0,1.0]范围内的浮点值。

  • LED持续时间以毫秒为单位指定为整数。常量 Gamepad.LED_DURATION_CONTINUOUS 可用于指示LED应保持指定的颜色,直到发出另一个命令。

备注

所有 LED 操作都是 “异步 “完成的,即函数调用将立即返回。对 LED API 的任何调用都会立即取代当前运行的任何 LED 操作。也就是说,如果你对 LED 发出持续 750 毫秒的绿色命令,然后立即对其发出持续 250 毫秒的紫色命令,那么从发出第二条命令开始,LED 将在 250 毫秒内点亮紫色。

要设置固定持续时间的LED颜色:

gamepad1.setLedColor(double r, double g, double b, int durationMs);

要创建更高级的LED行为,可以创建一个 LedEffect ,它由指定颜色和维护颜色持续时间的“步骤”组成。当游戏手柄被命令运行 LedEffect 时,它将连续执行每个“步骤”。

要创建 LedEffect ,必须使用 LedEffect.Builder 类。构建器提供了 addStep(double r, double g, double b, int durationMs) 函数,用于向序列添加步骤,以及 build() 函数,用于从步骤序列创建 LedEffect

使用构建器类的匿名实例是构建 LedEffect 的最干净方法,例如:

Gamepad.LedEffect rgbEffect = new Gamepad.LedEffect.Builder()
       .addStep(1, 0, 0, 250) // Show red for 250ms
       .addStep(0, 1, 0, 250) // Show green for 250ms
       .addStep(0, 0, 1, 250) // Show blue for 250ms
       .addStep(1, 1, 1, 250) // Show white for 250ms
       .build();

一旦创建了 LedEffect ,它可以通过调用发送到游戏手柄:

gamepad1.runLedEffect(rgbEffect);