在這個項目中,我們將使用一個搖桿電子裝置,並將其連接到我們在“如何使用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文件。

在頂部添加

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

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

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

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

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

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運行此程序,當您移動搖桿時,將在控制台上打印許多值:

太棒了!

現在讓我們試著更好地理解這些數據。

位置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,作為:

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框架。

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

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

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

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

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

我將URL硬編碼為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

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

另一種方法是使用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>

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

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

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

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

以下是Arduino的sketch,這次使用了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中構建的程序非常相似。

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

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

在Web頁面上,我添加了一個名為serial.js的文件,用於抽象我們不需要擔心的很多低級代碼。我在WebUSB Arduino示例https://webusb.github.io/arduino/demos/serial.js找到了它,並將其改為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()函數。我們將onReceive屬性附加到port對象上的函數。當有新消息到來時,將觸發該函數,我們將處理發送給我們的字母:

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服務器-連接直接在瀏覽器和設備之間進行。