用Arduino和操纵杆通过Johnny Five控制一个浏览器游戏

366 阅读8分钟

在这个项目中,我们将使用一个操纵杆电子设备,并将其与我们在 "如何用Phaser.js创建一个游戏"一文中创建的平台游戏相连接。

我们的目标是让玩家在游戏中移动

![](Screenshot 2020-03-27 at 13.29.25.png)

使用操纵杆。

听起来很有趣,对吗?

我们将使用Johnny Five来完成。在我们的Node.js应用程序中,我们将连接到设备,并创建一个Websockets服务器。在开始之前,我强烈建议你阅读Johnny Five教程

浏览器客户端将连接到这个Websockets服务器,我们将流式处理左/右/静止/上升事件来处理玩家的运动。

开始吧!

操纵杆

这是我们将在这个项目中使用的操纵杆组件。

它就像你在现实世界的设备上可以找到的那样,比如Playstation控制器。

它有5个引脚。GND、+5V(VCC)、X、Y和SW。

X和Y是操纵杆的坐标。

X是一个模拟输出,并发出操纵杆在X轴上移动的信号。

Y也是如此,用于Y轴。

![](Immagine PNG-CF8A79FB3C80-1.png)

以上图中的操纵杆为例,插针在左边,这些将是措施。

![](Immagine PNG-9DF1E09693E2-1.png)

左满时X为-1,右满时为1。

Y在到达顶部时为-1,在底部为1。

SW引脚是一个数字输出,当操纵杆被按下时被激活(可以被按下),但我们将不使用它。

将4根线连接到操纵杆上。

连接到Arduino上

我使用Arduino Uno板的克隆版,我将1号和2号针脚连接到GND和+5V。

3号针脚(x)接A0,4号针脚(y)接A1。

现在通过USB端口将Arduino连接到电脑上,我们将在下一课中进行Node.js Johnny Five程序的工作。

确保你已经使用Arduino IDE将StandardFirmataPlus 程序加载到Arduino板上,就像我在Johnny Five教程中解释的那样

强尼五号Node.js应用程序

让我们来初始化我们的Johnny Five应用程序。

创建一个joystick.js 文件。

添加

const { Board, Joystick } = require("johnny-five")

在上面。

初始化一个板子,并添加板子就绪事件。

const { Board, Joystick } = require("johnny-five")
const board = new Board()

board.on("ready", () => {
  //ready!
})

现在初始化一个新的操纵杆对象,告诉它我们要用哪个输入针脚来输入X和Y。

const { Board, Joystick } = require("johnny-five")
const board = new Board()

board.on("ready", () => {
  const joystick = new Joystick({
    pins: ["A0", "A1"],
  })
})

现在我们可以开始监听joystick 对象上的change 事件。当一个变化发生时,我们可以通过引用this.xthis.y 来获得X和Y坐标,就像这样。

joystick.on("change", function () {
  console.log("x : ", this.x)
  console.log("y : ", this.y)
})

下面是完整的代码操作。

const { Board, Joystick } = require("johnny-five")
const board = new Board()

board.on("ready", () => {
  const joystick = new Joystick({
    pins: ["A0", "A1"],
  })

  joystick.on("change", function () {
    console.log("x : ", this.x)
    console.log("y : ", this.y)
  })
})

如果你使用node joystick.js 启动这个程序,当你移动操纵杆时,你会看到很多数值被打印到控制台。

![](Screenshot 2020-03-27 at 14.08.21.png)

很好!

现在让我们试着从这些数据中获得更多的意义。

位置0是操纵杆静止的地方,未被触动。

我想在X高于0.5时检测 "右 "的移动,而在X低于-0.5时检测 "左 "的移动。

对Y轴也是如此。换句话说,我想只在操纵杆超出某个灰色区域时触发运动。

![](Immagine PNG-FF3FDDB0ACE7-1.jpg)

这就是我们可以做到的。

const { Board, Joystick } = require("johnny-five")
const board = new Board()

board.on("ready", () => {
  const joystick = new Joystick({
    pins: ["A0", "A1"],
  })

  joystick.on("change", function () {
    if (this.x > 0.5) {
      console.log("right")
    }
    if (this.x < -0.5) {
      console.log("left")
    }
    if (this.x > -0.5 && this.x < 0.5) {
      console.log("still")
    }
    if (this.y > 0.5) {
      console.log("down")
    }
    if (this.y < -0.5) {
      console.log("up")
    }
  })
})

试着运行该程序,你会看到left/right/up/down/still 字样被打印出来,而不是实际的坐标数字。

![](Screenshot 2020-03-27 at 14.40.13.png)

创建一个Websockets服务器

现在的问题是:我们如何在浏览器中运行的游戏和我们的硬件项目之间进行通信?

由于应用程序是在本地工作的,我的想法是在浏览器和Node.js Johnny Five进程之间创建一个Websockets连接。

Node.js应用程序将是Websockets服务器,而浏览器将连接到它。

然后,服务器将在操纵杆被移动时发送消息。

现在我们来研究一下Websockets服务器。

首先安装ws npm包。

npm install ws

当板子准备好后,我们初始化joystick 对象和一个新的Websockets服务器进程。我将使用8085端口,作为一个.NET端口。

const { Board, Joystick } = require('johnny-five')
const board = new Board()

board.on('ready', () => {
  const joystick = new Joystick({
    pins: ['A0', 'A1']
  })
  const WebSocket = require('ws')
  const wss = new WebSocket.Server({ port: 8085 })
})

接下来,我们添加一个事件监听器,提供一个回调,当客户端连接到Websockets服务器时被触发。

wss.on('connection', ws => {

})

在这里面,我们将开始监听joystick 对象上的change 事件,就像我们在上一课所做的那样,除了打印到控制台之外,我们还将使用ws.send() 方法向客户端发送一个消息。

const { Board, Joystick } = require('johnny-five')
const board = new Board()

board.on('ready', () => {
  const joystick = new Joystick({
    pins: ['A0', 'A1']
  })

  const WebSocket = require('ws')
  const wss = new WebSocket.Server({ port: 8085 })

  wss.on('connection', ws => {
    ws.on('message', message => {
      console.log(`Received message => ${message}`)
    })
    
    joystick.on('change', function() {
      if (this.x > 0.5) {
        console.log('->')
        ws.send('right')
      }
      if (this.x < -0.5) {
        console.log('<-')
        ws.send('left')
      }
      if (this.x > -0.5 && this.x < 0.5 ) {
        console.log('still')
        ws.send('still')
      }
      if (this.y > 0.5) {
        console.log('down')
      }
      if (this.y < -0.5) {
        console.log('up')
        ws.send('jump')
      }
    })
  })
})

从游戏中连接到Websockets服务器

如果你对我在游戏关卡中建立的平台游戏不熟悉,请先看看它。

这个游戏全部建立在一个单一的app.js 文件中,它使用Phaser.js框架。

我们有2个主要函数:create()update()

create() 函数的最后,我要连接到Websockets服务器。

const url = 'ws://localhost:8085'
connection = new WebSocket(url)

connection.onerror = error => {
  console.error(error)
}

我们还需要在文件的顶部,在函数之外初始化let connection ,因为我们也要在update() 函数中引用这个变量。

我对Websockets服务器的URL进行了硬编码,因为它是一个本地服务器,而这在一个单一的本地场景之外是行不通的。

毕竟,操纵杆是一个。但这对测试东西是很有好处的。

如果在连接过程中出现任何错误,我们会在这里看到一个错误。

在update()函数中,现在我们有这样的代码。

function update() {
  if (cursors.left.isDown) {
    player.setVelocityX(-160)
    player.anims.play('left')
  } else if (cursors.right.isDown) {
    player.setVelocityX(160)
    player.anims.play('right')
  } else {
    player.setVelocityX(0)
    player.anims.play('still')
  }

  if (cursors.up.isDown && player.body.touching.down) {
    player.setVelocityY(-330)
  }
}

我要改变这一切,因为我们不是用键盘来控制玩家的移动,而是要用操纵杆。

我们将监听connection 对象上的message 事件。

function update() {
  connection.onmessage = e => {

  }
}

传给回调的e 对象代表 "事件",我们可以在其data 属性上获得服务器发送的数据。

function update() {
  connection.onmessage = e => {
    console.log(e.data)
  }
}

现在我们可以检测到发送的信息,我们可以相应地移动播放器。

connection.onmessage = e => {
  if (e.data === 'left') {
    player.setVelocityX(-160)
    player.anims.play('left')
  }
  if (e.data === 'right') {
    player.setVelocityX(160)
    player.anims.play('right')
  }
  if (e.data === 'still') {
    player.setVelocityX(0)
    player.anims.play('still')
  }
  if (e.data === 'jump' && player.body.touching.down) {
    player.setVelocityY(-330)
  }
}

这就是了!现在,我们的操纵杆将在屏幕上移动播放器

使用WebUSB的替代方案

带有Websockets的Node.js服务器是解决连接问题的一个很好的跨浏览器方法。

另一种方法是使用WebUSB,这项技术只适用于基于Chromium的浏览器,如Chrome、Edge和其他。

利用这一点,我们可以让页面检测到一个设备,它们可以直接与它对话。

要做到这一点,我们必须要求用户执行一个动作,比如按下一个 "连接 "按钮,就像我添加到游戏index.html 文件中的那个。

<!DOCTYPE html>
<html>
  <head>
    <script src="./dist/app.js"></script>
  </head>
  <body>
    <button id="connect">Connect</button>
  </body>
</html>

(游戏的其余部分被自动附加到正文中的canvas 标签中)

这次我使用了Arduino MKR WiFi 1010设备,因为WebUSB不支持Arduino Uno板,这是技术原因。

我把操纵杆连接到那个板子上,使用了我们在以前的课程中使用的相同的接线。

为了配置Arduino以便与WebUSB很好的配合,我建议你阅读webusb.github.io/arduino/。

这里是Arduino的草图,这次是用Arduino语言(C++)写的,而不是用Johnny Five。

#include <WebUSB.h>
WebUSB WebUSBSerial(1 /* http:// */, "localhost:3000"); //provide a hint at what page to load

#define Serial WebUSBSerial

const int xpin = 0;
const int ypin = 1;

void loop() {
  if (Serial) {

    int x = analogRead(xpin);
    int y = analogRead(ypin);

    bool still = false;

    if (x > 768) {
      still = false;
      Serial.println('R');
    }
    if (x < 256) {
      still = false;
      Serial.println('L');
    }
    if (x > 256 && x < 768 ) {
      if (!still) {
        still = true;
        Serial.println('S');
      }
    }
    if (y < 256) {
      Serial.println('J');
    }
  }
}

它与我们之前用Node.js编写的程序非常相似。

不过这次我们使用WebUSB定义的WebUSBSerial接口向网页发送一封信。

我们发送一个字母。R代表右边,L代表左边,S代表静止,J代表跳跃。

我添加了一个still 布尔值来防止S 信件被不必要地发送太多次。这样,当我们返回到静止状态时,我们只发送一次。

在网页方面,我添加了一个serial.js 文件,用来抽象出很多我们不需要担心的低级代码。我在WebUSB Arduino demowebusb.github.io/arduino/dem…中发现了它,我把它改编成了ES模块文件,去掉了IIFE(立即调用的函数),并在最后添加了一个export default serial

const serial = {}

serial.getPorts = function () {
  return navigator.usb.getDevices().then((devices) => {
    return devices.map((device) => new serial.Port(device))
  })
}

serial.requestPort = function () {
  const filters = [
    { vendorId: 0x2341, productId: 0x8036 }, // Arduino Leonardo
    { vendorId: 0x2341, productId: 0x8037 }, // Arduino Micro
    { vendorId: 0x2341, productId: 0x804d }, // Arduino/Genuino Zero
    { vendorId: 0x2341, productId: 0x804e }, // Arduino/Genuino MKR1000
    { vendorId: 0x2341, productId: 0x804f }, // Arduino MKRZERO
    { vendorId: 0x2341, productId: 0x8050 }, // Arduino MKR FOX 1200
    { vendorId: 0x2341, productId: 0x8052 }, // Arduino MKR GSM 1400
    { vendorId: 0x2341, productId: 0x8053 }, // Arduino MKR WAN 1300
    { vendorId: 0x2341, productId: 0x8054 }, // Arduino MKR WiFi 1010
    { vendorId: 0x2341, productId: 0x8055 }, // Arduino MKR NB 1500
    { vendorId: 0x2341, productId: 0x8056 }, // Arduino MKR Vidor 4000
    { vendorId: 0x2341, productId: 0x8057 }, // Arduino NANO 33 IoT
    { vendorId: 0x239a }, // Adafruit Boards!
  ]
  return navigator.usb
    .requestDevice({ filters: filters })
    .then((device) => new serial.Port(device))
}

serial.Port = function (device) {
  this.device_ = device
  this.interfaceNumber_ = 2 // original interface number of WebUSB Arduino demo
  this.endpointIn_ = 5 // original in endpoint ID of WebUSB Arduino demo
  this.endpointOut_ = 4 // original out endpoint ID of WebUSB Arduino demo
}

serial.Port.prototype.connect = function () {
  let readLoop = () => {
    this.device_.transferIn(this.endpointIn_, 64).then(
      (result) => {
        this.onReceive(result.data)
        readLoop()
      },
      (error) => {
        this.onReceiveError(error)
      }
    )
  }

  return this.device_
    .open()
    .then(() => {
      if (this.device_.configuration === null) {
        return this.device_.selectConfiguration(1)
      }
    })
    .then(() => {
      var configurationInterfaces = this.device_.configuration.interfaces
      configurationInterfaces.forEach((element) => {
        element.alternates.forEach((elementalt) => {
          if (elementalt.interfaceClass == 0xff) {
            this.interfaceNumber_ = element.interfaceNumber
            elementalt.endpoints.forEach((elementendpoint) => {
              if (elementendpoint.direction == "out") {
                this.endpointOut_ = elementendpoint.endpointNumber
              }
              if (elementendpoint.direction == "in") {
                this.endpointIn_ = elementendpoint.endpointNumber
              }
            })
          }
        })
      })
    })
    .then(() => this.device_.claimInterface(this.interfaceNumber_))
    .then(() => this.device_.selectAlternateInterface(this.interfaceNumber_, 0))
    .then(() =>
      this.device_.controlTransferOut({
        requestType: "class",
        recipient: "interface",
        request: 0x22,
        value: 0x01,
        index: this.interfaceNumber_,
      })
    )
    .then(() => {
      readLoop()
    })
}

serial.Port.prototype.disconnect = function () {
  return this.device_
    .controlTransferOut({
      requestType: "class",
      recipient: "interface",
      request: 0x22,
      value: 0x00,
      index: this.interfaceNumber_,
    })
    .then(() => this.device_.close())
}

serial.Port.prototype.send = function (data) {
  return this.device_.transferOut(this.endpointOut_, data)
}

export default serial

app.js ,我在顶部包括这个文件。

import serial from "./serial.js"

因为现在我们有一个按钮,所以我添加了一个DOMContentLoaded 事件监听器。

document.addEventListener('DOMContentLoaded', () => {

}

我把我在文件中的所有东西,除了import 语句,都包在里面。

接下来我添加了一个对Connect 按钮的引用。

let connectButton = document.querySelector("#connect")

和一个我们稍后要初始化的port 变量。

let port

create() 函数的最后,我们立即向serial 对象询问设备,如果有一个设备已经配对和连接,我们就调用connect()

我在Connect 按钮上添加一个点击事件监听器。当我们点击该按钮时,我们将请求连接到一个设备。

connectButton.addEventListener("click", () => {
  if (port) {
    port.disconnect()
    connectButton.textContent = "Connect"
    port = null
  } else {
    serial.requestPort().then((selectedPort) => {
      port = selectedPort
      port.connect().then(() => {
        connectButton.remove()
      })
    })
  }
})

我们的请求必须是在一个用户发起的事件中,比如点击,否则浏览器将不做任何事情,并拒绝我们的行动。

当用户按下按钮时,浏览器会要求获得连接的许可。

![](Screenshot 2020-03-27 at 16.40.24.png)

而一旦获得许可,我们就可以用操纵杆来控制游戏了!

这里是新的update() 函数,它将处理从设备接收数据。我们有一个函数连接到port 对象的onReceive 属性。当有新的信息进来时,这将启动,我们将处理传达给我们的信件。

function update() {
  if (port) {
    port.onReceive = (data) => {
      let textDecoder = new TextDecoder()
      let key = textDecoder.decode(data)

      if (key === "L") {
        player.setVelocityX(-160)
        player.anims.play("left")
      }
      if (key === "R") {
        player.setVelocityX(160)
        player.anims.play("right")
      }
      if (key === "S") {
        player.setVelocityX(0)
        player.anims.play("still")
      }
      if (key === "J" && player.body.touching.down) {
        player.setVelocityY(-330)
      }
    }
    port.onReceiveError = (error) => {
      console.error(error)
    }
  }
}

就这样了!现在我们可以像以前使用Websockets时那样玩游戏了,只是现在我们不需要外部的Node.js服务器了--连接直接发生在浏览器和设备之间。