首页 > 解决方案 > 用 Puppeteer 模拟游戏手柄?

问题描述

我想编写一些基于 Puppeteer 的测试来测试一些使用Gamepad API的逻辑,但是我在Puppeteer 文档上找不到任何文档来解释如何模拟游戏手柄以及如何将按钮按下发送到浏览器。

正确的方法是什么?

标签: javascriptpuppeteergamepad-api

解决方案


没有真正的“正确”方法来做到这一点。

我所做的是一种你应该能够使用的有点干净的方式。evaluate它本质上涉及创建您自己的游戏控制器管理代码以在 puppeteer 中使用,并使用 puppeteer API 调用将控制器状态注入页面。

本质上,我们劫持了全局作用域navigator.getGamepads函数并注入了我们自己的实现。

我们首先在代码中对游戏控制器进行建模。控制器最基本的部分是一个按钮。所以让我们这样做。

class Button {
  constructor() {
    this.value = 0.0
    this.pressed = false
  }
  press() {
    this.value = 1.0
    this.pressed = true
  }
  unpress() {
    this.value = 0.0
    this.pressed = false
  }
  toObject() {
    return {
      value: this.value,
      pressed: this.pressed,
    }
  }
}

接下来,根据W3C 游戏手柄规范,游戏手柄有两个模拟摇杆,因此我们将对它们进行建模。

class AnalogStick {
  constructor() {
    this.button = new Button()
    this.xAxis = 0.0
    this.yAxis = 0.0
  }

  setXAxis(value) {
    this.xAxis = value
  }

  setYAxis(value) {
    this.yAxis = value
  }
}

最后,让我们创建一个类来表示游戏手柄。此类将具有一个帮助函数,该函数将控制器内部状态转换为匹配W3C 游戏手柄接口签名

class GamePad {
  constructor(index = 0) {
    this.id = 'Standard Gamepad'
    this.displayId = null // this is used for VR
    this.connected = true
    this.index = index
    this.mapping = 'standard'

    this.dPad = {
      up: new Button(),
      right: new Button(),
      down: new Button(),
      left: new Button(),
    }

    this.select = new Button()
    this.home = new Button()
    this.start = new Button()

    this.actions = {
      top: new Button(),
      right: new Button(),
      bottom: new Button(),
      left: new Button(),
    }

    this.leftStick = new AnalogStick()
    this.rightStick = new AnalogStick()

    this.lButton = new Button()
    this.lTrigger = new Button()

    this.rButton = new Button()
    this.rTrigger = new Button()
  }

  getState() {
    return {
      axes: [
        this.leftStick.xAxis,
        this.leftStick.yAxis,
        this.rightStick.xAxis,
        this.rightStick.yAxis,
      ],
      buttons: [
        this.actions.bottom.toObject(),
        this.actions.right.toObject(),
        this.actions.left.toObject(),
        this.actions.top.toObject(),

        this.lButton.toObject(),
        this.rButton.toObject(),

        this.lTrigger.toObject(),
        this.rTrigger.toObject(),

        this.select.toObject(),
        this.start.toObject(),

        this.leftStick.button.toObject(),
        this.rightStick.button.toObject(),

        this.dPad.up.toObject(),
        this.dPad.down.toObject(),
        this.dPad.left.toObject(),
        this.dPad.right.toObject(),

        this.home.toObject(),
      ],
      connected: this.connected,
      displayId: this.displayId,
      id: this.id,
      index: this.index,
      mapping: this.mapping,
    }
  }
}

现在我们有了一种在代码中表示游戏控制器及其状态的便捷方法,我们可以劫持navigator.getGamepads并用我们自己的函数替换它,该函数返回我们虚拟控制器的状态。

现在我们将定义几个辅助函数。navigator.getGamepads设置将返回的游戏手柄状态的一种。

const setGamepadsState = async (page, gamepads) => {
  const result = await page.evaluate((controllers) => {
    navigator.getGamepads = () => controllers
  }, gamepads)
  return result
}

现在我们已经完成了,我们需要一种方法来触发gamepadconnected事件。我们可以使用 puppeteerpage.emit函数调用来做到这一点。

const connectGamepad = async (page, gamepad) => {
  const connectedEvent = {
    gamepad,
  }
  page.emit('gamepadconnected', connectedEvent)
}

现在,我们拥有了使用 puppeteer 模拟控制器的所有构建块!示例用法如下:

;(async () => {
  const controller1 = new GamePad(0)
  const controller2 = new GamePad(1)
  const browser = await puppeteer.launch()
  const page = await browser.newPage()

  await page.goTo('https://www.yourgamepadpage.com')

  // Set the current gamepad state for both controllers in the puppeteer page.
  // We need to call this each time we change a controllers state
  await setGamepadsState(page, [controller1.getState(), controller2.getState()])

  // fires a 'gamepadconnected' event in the page for controller1
  await connectGamepad(page, controller1.getState())
  // fires a 'gamepadconnected' event in the page for controller2
  await connectGamepad(page, controller2.getState())

  // toggles the state of the bottom action button to pressed on controller1, 'X' on a playstation pad or 'A' on an xbox pad
  controller1.actions.bottom.press()
  await setGamepadsState(page, [controller1.getState(), controller2.getState()]) // passes controller1's current state into puppeteer's 'page.evaluate'

  // do a check here in your puppeteer based test!
  console.log('this should be whatever test code you need!')

  controller1.actions.bottom.unpress() // untoggles the state of the bottom action button on controller1

  // now lets simulate an analog stick axis shift, e.g. left analog stick on the horizontal axis all the way to the left.
  controller1.leftStick.setXAxis(-1.0)
  await setGamepadsState(page, [controller1.getState(), controller2.getState()]) // and now we pass it to the page context!

  await browser.close()
})()

希望这应该为您指明正确的方向。如果您有任何问题,请随时在此处跟进 :)


推荐阅读