让我们在 10 分钟内使用 Tensorflow.js 构建一个游戏🎮
我最近一直在玩Tensorflow.js,觉得它的整体概念非常吸引人。之前我写了一篇关于如何入门的文章,我打算写更多关于如何创建新模型、如何利用 Tensorflow 进行迁移学习,或者如何使用预训练模型进行推理的文章。
让我们回顾一下我们的计划🗺️
今天我们要:
- 查找使用 JavaScript 编写的游戏
- 找到一个我们可以控制游戏的模型
- 将两者混合在一起并享受乐趣🤪
- 不要抱怨我们刚刚写的代码破坏了乐趣😁
免责声明
我这里没有展示任何 JavaScript 代码的最佳实践。它是从其他地方复制粘贴过来的,我们的目的是让它正常工作。
寻找游戏
当我为我的关于AI和JavaScript的演讲制作一个演示程序时,我做的第一件事就是寻找一个用JavaScript编写的游戏。我不想要一个复杂的游戏,只要一个轻量级的、可以轻松定制的游戏就行。
您现在可以观看我的演讲👇🏼:
在 Google 上搜索后JavaScript game
,我在 W3Schools 上找到了这个游戏:
游戏很简单。你只需要引导红色方块穿过来袭的绿色条。如果它碰到任何一条绿色条,游戏就结束。
找到合适的模型
对于这个游戏,我很快就想到了SpeechCommand。这个模型可以识别由简单单词组成的语音命令。目前它只有 18 个单词,但我们甚至不需要这些单词,因为我们只需要指示(up
、down
、left
和right
)。
该模型使用WebAudio API通过麦克风收听您的声音。
让我们开始编码吧💻
首先,我们需要获取游戏背后的代码。幸运的是,它就在我们找到游戏的同一个页面上。
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
canvas {
border:1px solid #d3d3d3;
background-color: #f1f1f1;
}
</style>
</head>
<body onload="startGame()">
<script>
var myGamePiece;
var myObstacles = [];
var myScore;
function startGame() {
myGamePiece = new component(30, 30, "red", 10, 120);
myScore = new component("30px", "Consolas", "black", 280, 40, "text");
myGameArea.start();
}
var myGameArea = {
canvas : document.createElement("canvas"),
start : function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
this.frameNo = 0;
this.interval = setInterval(updateGameArea, 20);
},
clear : function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
},
stop : function() {
clearInterval(this.interval);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.update = function() {
ctx = myGameArea.context;
if (this.type == "text") {
ctx.font = this.width + " " + this.height;
ctx.fillStyle = color;
ctx.fillText(this.text, this.x, this.y);
} else {
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
this.newPos = function() {
this.x += this.speedX;
this.y += this.speedY;
}
this.crashWith = function(otherobj) {
var myleft = this.x;
var myright = this.x + (this.width);
var mytop = this.y;
var mybottom = this.y + (this.height);
var otherleft = otherobj.x;
var otherright = otherobj.x + (otherobj.width);
var othertop = otherobj.y;
var otherbottom = otherobj.y + (otherobj.height);
var crash = true;
if ((mybottom < othertop) || (mytop > otherbottom) || (myright < otherleft) || (myleft > otherright)) {
crash = false;
}
return crash;
}
}
function updateGameArea() {
var x, height, gap, minHeight, maxHeight, minGap, maxGap;
for (i = 0; i < myObstacles.length; i += 1) {
if (myGamePiece.crashWith(myObstacles[i])) {
myGameArea.stop();
return;
}
}
myGameArea.clear();
myGameArea.frameNo += 1;
if (myGameArea.frameNo == 1 || everyinterval(150)) {
x = myGameArea.canvas.width;
minHeight = 20;
maxHeight = 200;
height = Math.floor(Math.random()*(maxHeight-minHeight+1)+minHeight);
minGap = 50;
maxGap = 200;
gap = Math.floor(Math.random()*(maxGap-minGap+1)+minGap);
myObstacles.push(new component(10, height, "green", x, 0));
myObstacles.push(new component(10, x - height - gap, "green", x, height + gap));
}
for (i = 0; i < myObstacles.length; i += 1) {
myObstacles[i].speedX = -1;
myObstacles[i].newPos();
myObstacles[i].update();
}
myScore.text="SCORE: " + myGameArea.frameNo;
myScore.update();
myGamePiece.newPos();
myGamePiece.update();
}
function everyinterval(n) {
if ((myGameArea.frameNo / n) % 1 == 0) {return true;}
return false;
}
function moveup() {
myGamePiece.speedY = -1;
}
function movedown() {
myGamePiece.speedY = 1;
}
function moveleft() {
myGamePiece.speedX = -1;
}
function moveright() {
myGamePiece.speedX = 1;
}
function clearmove() {
myGamePiece.speedX = 0;
myGamePiece.speedY = 0;
}
</script>
<div style="text-align:center;width:480px;">
<button onmousedown="moveup()" onmouseup="clearmove()" ontouchstart="moveup()">UP</button><br><br>
<button onmousedown="moveleft()" onmouseup="clearmove()" ontouchstart="moveleft()">LEFT</button>
<button onmousedown="moveright()" onmouseup="clearmove()" ontouchstart="moveright()">RIGHT</button><br><br>
<button onmousedown="movedown()" onmouseup="clearmove()" ontouchstart="movedown()">DOWN</button>
</div>
<p>The score will count one point for each frame you manage to "stay alive".</p>
</body>
</html>
创建项目结构
现在让我们设置一下项目结构。我们需要以下文件:
package.json
index.html
game.js
index.js
在我们的系统中package.json
,我们需要一些依赖项和脚本命令来运行该应用程序。
{
...
"dependencies": {
"@tensorflow-models/speech-commands": "^0.3.9",
"@tensorflow/tfjs": "^1.2.8"
},
"scripts": {
"watch": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=development parcel index.html --no-hmr --open"
},
"devDependencies": {
"@babel/core": "^7.0.0-0",
"@babel/plugin-transform-runtime": "^7.1.0",
"babel-core": "^6.26.3",
"babel-polyfill": "~6.26.0",
"babel-preset-env": "~1.6.1",
"babel-preset-es2017": "^6.24.1",
"clang-format": "~1.2.2",
"cross-env": "^5.2.0",
"eslint": "^4.19.1",
"eslint-config-google": "^0.9.1",
"parcel-bundler": "~1.10.3"
},
...
}
我在这里使用 Parcel,因为它设置简单,而且运行良好。此外, Tensorflow.js 模型 GitHub 仓库中的大多数演示都使用了 Parcel。
您的项目结构应如下所示:
将游戏代码转换为模块
现在先别太关注代码,因为我们需要做一些重构。首先,让我们把 JavaScript 代码分离到一个单独的文件中。这样做之后,你的 HTML 代码应该如下所示:
<!DOCTYPE html>
<html class="no-js">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>TensorFlow.js Speech Commands Model Demo</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
canvas {
border: 1px solid #d3d3d3;
background-color: #f1f1f1;
}
</style>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
由于我们将使用SpeechCommand模块,因此我们需要修改 JavaScript 代码以支持该模块。创建一个名为 的文件main.js
,并将以下代码粘贴到其中。
var myGamePiece;
var myObstacles = [];
var myScore;
export const startGame = function() {
myGamePiece = new component(30, 30, "red", 10, 120);
myScore = new component("30px", "Consolas", "black", 280, 40, "text");
myGameArea.start();
}
var myGameArea = {
canvas : document.createElement("canvas"),
start : function() {
this.canvas.width = 480;
this.canvas.height = 270;
this.context = this.canvas.getContext("2d");
document.body.insertBefore(this.canvas, document.body.childNodes[0]);
this.frameNo = 0;
this.interval = setInterval(updateGameArea, 40);
},
clear : function() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
},
stop : function() {
clearInterval(this.interval);
}
}
function component(width, height, color, x, y, type) {
this.type = type;
this.width = width;
this.height = height;
this.speedX = 0;
this.speedY = 0;
this.x = x;
this.y = y;
this.update = function() {
let ctx = myGameArea.context;
if (this.type == "text") {
ctx.font = this.width + " " + this.height;
ctx.fillStyle = color;
ctx.fillText(this.text, this.x, this.y);
} else {
ctx.fillStyle = color;
ctx.fillRect(this.x, this.y, this.width, this.height);
}
}
this.newPos = function() {
this.x += this.speedX;
this.y += this.speedY;
}
this.crashWith = function(otherobj) {
let myleft = this.x;
let myright = this.x + (this.width);
let mytop = this.y;
let mybottom = this.y + (this.height);
let otherleft = otherobj.x;
let otherright = otherobj.x + (otherobj.width);
let othertop = otherobj.y;
let otherbottom = otherobj.y + (otherobj.height);
let crash = true;
if ((mybottom < othertop) || (mytop > otherbottom) || (myright < otherleft) || (myleft > otherright)) {
crash = false;
}
return crash;
}
}
function updateGameArea() {
let x, height, gap, minHeight, maxHeight, minGap, maxGap;
for (let i = 0; i < myObstacles.length; i += 1) {
if (myGamePiece.crashWith(myObstacles[i])) {
myGameArea.stop();
return;
}
}
myGameArea.clear();
myGameArea.frameNo += 1;
if (myGameArea.frameNo == 1 || everyinterval(180)) {
x = myGameArea.canvas.width;
minHeight = 20;
maxHeight = 200;
height = Math.floor(Math.random()*(maxHeight-minHeight+1)+minHeight);
minGap = 50;
maxGap = 200;
gap = Math.floor(Math.random()*(maxGap-minGap+1)+minGap);
myObstacles.push(new component(10, height, "green", x, 0));
myObstacles.push(new component(10, x - height - gap, "green", x, height + gap));
}
for (let j = 0; j < myObstacles.length; j += 1) {
myObstacles[j].speedX = -1;
myObstacles[j].newPos();
myObstacles[j].update();
}
myScore.text="SCORE: " + myGameArea.frameNo;
myScore.update();
myGamePiece.newPos();
myGamePiece.update();
}
export const everyinterval = function(n) {
if ((myGameArea.frameNo / n) % 1 == 0) {return true;}
return false;
}
export const moveup = function() {
myGamePiece.speedY = -1;
}
export const movedown = function() {
myGamePiece.speedY = 1;
}
export const moveleft = function() {
myGamePiece.speedX = -1;
}
export const moveright = function() {
myGamePiece.speedX = 1;
}
export const clearmove = function() {
myGamePiece.speedX = 0;
myGamePiece.speedY = 0;
}
我在这里所做的只是将其中一些函数改为常量,并导出稍后需要的内容。现在让我们编写index.js
并导入语音命令模型。
import * as speechCommands from "@tensorflow-models/speech-commands";
接下来,导入我们玩游戏所需的功能:
import { startGame, moveup, movedown, clearmove } from './main';
我不会使用 left 和 write,但你完全可以使用它们。接下来,我们需要创建模型的实例并初始化它。由于加载模型需要异步调用,我们将使用IIFE(立即调用函数表达式):
const recognizer = speechCommands.create("BROWSER_FFT");
(async function() {
await recognizer.ensureModelLoaded();
console.log(recognizer.wordLabels());
startGame();
})();
模型加载完成后,我们调用 start game。最后,我们需要调用listen
实例上的方法来监听命令:
const suppressionTimeMillis = 1000;
const allowedCommands = ['up', 'down'];
recognizer
.listen(
result => {
checkPredictions(recognizer.wordLabels(), result.scores,
2, suppressionTimeMillis);
},
{
includeSpectrogram: true,
suppressionTimeMillis,
probabilityThreshold: 0.8
}
)
.then(() => {
console.log("Streaming recognition started.");
})
.catch(err => {
console.log("ERROR: Failed to start streaming display: " + err.message);
});
const checkPredictions = (
candidateWords,
probabilities,
topK,
timeToLiveMillis
) => {
if (topK != null) {
let wordsAndProbs = [];
for (let i = 0; i < candidateWords.length; ++i) {
wordsAndProbs.push([candidateWords[i], probabilities[i]]);
}
wordsAndProbs.sort((a, b) => b[1] - a[1]);
wordsAndProbs = wordsAndProbs.slice(0, topK);
candidateWords = wordsAndProbs.map(item => item[0]);
probabilities = wordsAndProbs.map(item => item[1]);
console.log(wordsAndProbs);
// Highlight the top word.
const topWord = wordsAndProbs[0][0];
if(allowedCommands.includes(topWord)) {
if(topWord === 'up') {
moveup();
setTimeout(() => clearmove(), 850);
console.log('up');
} else {
movedown();
setTimeout(() => clearmove(), 850);
console.log('down');
}
}
}
}
这将启动游戏并监听你的麦克风。如果你还没有授权浏览器使用麦克风,则需要先授权。
Listen 方法返回一个包含结果对象的 Promise。获取结果后,我们将调用辅助函数来检查哪些单词被识别,并根据结果触发相应的操作。
改进
如果你看过SpeechCommand 仓库的文档,你会发现在调用 时speechCommands.create()
,你可以指定加载的模型能够识别的词汇。这是 的第二个可选参数speechCommands.create()
。我们可以用它来将单词限制为仅限方向性的:
const recognizer = speechCommands.create('BROWSER_FFT', 'directional4w');
现在如果你尝试该模型,它会更准确,因为它只关注方向词(up
,,,和)。down
left
right
概括
就是这样。这就是你需要做的全部,以便使用Tensorflow.js
SpeechCommand模型用语音控制这个小游戏。
现在使用这些预先训练的模型创建一些有趣的东西,并确保检查它们的示例存储库。
要查看更多鼓舞人心的演示,请查看由Asim Hussain、Eleanor Haproff和Osama Jandali构建的aijs.rocks。
文章来源:https://dev.to/yashints/let-s-build-a-game-with-tensorflow-js-in-10-minutes-1co5