在這個項目中,我們將使用一個搖桿電子裝置,並將其連接到我們在“如何使用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.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,作為:
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服務器-連接直接在瀏覽器和設備之間進行。