使用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.x
和this.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包:
當板準備好時,我們初始化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 , "localhost:3000" ) ; #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 }, { vendorId : 0x2341 , productId : 0x8037 }, { vendorId : 0x2341 , productId : 0x804d }, { vendorId : 0x2341 , productId : 0x804e }, { vendorId : 0x2341 , productId : 0x804f }, { vendorId : 0x2341 , productId : 0x8050 }, { vendorId : 0x2341 , productId : 0x8052 }, { vendorId : 0x2341 , productId : 0x8053 }, { vendorId : 0x2341 , productId : 0x8054 }, { vendorId : 0x2341 , productId : 0x8055 }, { vendorId : 0x2341 , productId : 0x8056 }, { vendorId : 0x2341 , productId : 0x8057 }, { vendorId : 0x239a }, ] return navigator.usb .requestDevice ({ filters : filters }) .then ((device ) => new serial.Port (device)) } serial.Port = function (device ) { this .device_ = device this .interfaceNumber_ = 2 this .endpointIn_ = 5 this .endpointOut_ = 4 } 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
變量:
在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服務器-連接直接在瀏覽器和設備之間進行。