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

使用操纵杆。
听起来很有趣,对吗?
我们将使用Johnny Five来完成。在我们的Node.js应用程序中,我们将连接到设备,并创建一个Websockets服务器。在开始之前,我强烈建议你阅读Johnny Five教程。
浏览器客户端将连接到这个Websockets服务器,我们将流式处理左/右/静止/上升事件来处理玩家的运动。
开始吧!
操纵杆
这是我们将在这个项目中使用的操纵杆组件。
它就像你在现实世界的设备上可以找到的那样,比如Playstation控制器。

它有5个引脚。GND、+5V(VCC)、X、Y和SW。
X和Y是操纵杆的坐标。

X是一个模拟输出,并发出操纵杆在X轴上移动的信号。
Y也是如此,用于Y轴。

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

左满时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.x 和this.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 启动这个程序,当你移动操纵杆时,你会看到很多数值被打印到控制台。

很好!
现在让我们试着从这些数据中获得更多的意义。
位置0是操纵杆静止的地方,未被触动。
我想在X高于0.5时检测 "右 "的移动,而在X低于-0.5时检测 "左 "的移动。
对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 () {
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 字样被打印出来,而不是实际的坐标数字。

创建一个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()
})
})
}
})
我们的请求必须是在一个用户发起的事件中,比如点击,否则浏览器将不做任何事情,并拒绝我们的行动。
当用户按下按钮时,浏览器会要求获得连接的许可。

而一旦获得许可,我们就可以用操纵杆来控制游戏了!
这里是新的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服务器了--连接直接发生在浏览器和设备之间。