/

使用Arduino和搖桿通過Johnny Five來控制瀏覽器遊戲

使用Arduino和搖桿通過Johnny Five來控制瀏覽器遊戲

在這個項目中,我們將使用一個搖桿電子裝置,並將其連接到我們在“如何使用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;完全右時,X為1。

到達頂部時,Y為-1;到達底部時,Y為1。

SW引腳是一個數字輸出,當按下搖桿時激活(可按下),但我們將不使用它。

將4根電線連接到搖桿:

將其連接到Arduino

我使用的是Arduino Uno板克隆版,將引腳#1和#2連接到GND和+5V。

引腳#3(x)連接到A0,引腳#4(y)連接到A1:

現在將Arduino通過USB端口連接到計算機,我們將在下一節中使用Node.js的Johnny Five程序。

確保您已經使用Arduino IDE在Arduino板上加載了StandardFirmataPlus程序,就像我在Johnny Five教程中解釋的那樣。

Johnny Five Node.js應用程序

讓我們初始化我們的Johnny Five應用程序。

創建一個joystick.js文件。

在頂部添加

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

初始化一個板,並添加板准備好的事件:

1
2
3
4
5
6
const { Board, Joystick } = require("johnny-five")
const board = new Board()

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

現在初始化一個新的搖桿對象,告訴它我們要使用哪些輸入引腳:

1
2
3
4
5
6
7
8
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坐標,如下所示:

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

以下是完整的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
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軸也是如此。換句話說,我只想在搖桿超出一定灰色區域時觸發移動:

我們可以這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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包:

1
npm install ws

當板準備好時,我們初始化joystick對象和一個新的Websockets服務器進程。我將使用端口8085,作為:

1
2
3
4
5
6
7
8
9
10
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服務器時觸發:

1
2
3
wss.on('connection', ws => {

})

在這裡面,我們將開始監聽joystick對象上的change事件,就像我們在上一節中所做的一樣,除了打印到控制台之外,我們還將使用ws.send()方法向客戶端發送消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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框架。

我們有兩個主要的函數:create()update()

create()函數的結尾,我們將連接到Websockets服務器:

1
2
3
4
5
6
const url = 'ws://localhost:8085'
connection = new WebSocket(url)

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

我們還需要在文件頂部初始化let connection,因為我們將在update()函數中引用該變數。

我將URL硬編碼為Websockets服務器的URL,因為它是一個本地服務器,這在單一的本地場景之外將無法工作。

搖桿只有一個,但這是測試的好方法。

如果在連接期間有任何錯誤,我們將在這裡看到一個錯誤。

update()函數中,現在我們有以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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事件:

1
2
3
4
5
function update() {
connection.onmessage = e => {

}
}

傳遞給回調函數的e對象代表“事件”,我們可以在其data屬性上獲取由服務器發送的數據:

1
2
3
4
5
function update() {
connection.onmessage = e => {
console.log(e.data)
}
}

現在,我們可以檢測發送的消息,並相應地移動玩家:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

Node.js服務器與Websockets是解決連接問題的一種好的跨瀏覽器方法。

另一種方法是使用WebUSB,這是一種僅在基於Chromium的瀏覽器(如Chrome,Edge等)中可用的技術。

使用這種方法,我們可以使頁面檢測到一個設備,並且它們可以直接與其通信。

為此,我們必須要求用戶執行一個操作,例如按一個“連接”按鈕,就像我在遊戲index.html文件中添加的按鈕一樣:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<script src="./dist/app.js"></script>
</head>
<body>
<button id="connect">Connect</button>
</body>
</html>

(遊戲的其餘部分會自動添加到body中的canvas標簽)

這次我使用了Arduino MKR WiFi 1010設備,因為WebUSB由於技術原因不支持Arduino Uno板。

我將搖桿連接到該板上,使用了我們在先前課程中使用的相同連接方式:

將Arduino配置為與WebUSB良好配合使用,我建議您閱讀https://webusb.github.io/arduino/

以下是Arduino的sketch,這次使用了Arduino語言(C++)而不是Johnny Five:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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中構建的程序非常相似。

但是這次,我們使用WebUSBSerial界面通過WebUSB向頁面發送一個信號字母。

我添加了一個still布爾變量,以防止太多次不必要地發送S字母。這樣,當我們返回到靜止狀態時,我們只發送一次。

在Web頁面上,我添加了一個名為serial.js的文件,用於抽象我們不需要擔心的很多低級代碼。我在WebUSB Arduino示例https://webusb.github.io/arduino/demos/serial.js找到了它,並將其改為ES模塊文件,刪除了IIFE(立即調用函數)並在末尾添加了export default serial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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中,我將在頂部引入此文件:

1
import serial from "./serial.js"

並且由於現在有一個按鈕,我添加了DOMContentLoaded事件監聽器:

1
2
3
document.addEventListener('DOMContentLoaded', () => {

}

我將文件中的所有代碼都放在其中,除了import語句之外。

接下來,我添加對Connect按鈕的引用:

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

以及稍後我們將要初始化的port變量:

1
let port

create()函數的末尾,我們立即向serial對象請求設備,如果已經配對並連接了一個設備,則調用connect()

我在Connect按鈕上添加了一個點擊事件監聽器。當我們點擊該按鈕時,我們將請求與設備的連接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()函數。我們將onReceive屬性附加到port對象上的函數。當有新消息到來時,將觸發該函數,我們將處理發送給我們的字母:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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服務器-連接直接在瀏覽器和設備之間進行。