让我们在 10 分钟内使用 Tensorflow.js 构建一个游戏🎮

2025-05-28

让我们在 10 分钟内使用 Tensorflow.js 构建一个游戏🎮

我最近一直在玩Tensorflow.js,觉得它的整体概念非常吸引人。之前我写了一篇关于如何入门的文章,我打算写更多关于如何创建新模型、如何利用 Tensorflow 进行迁移学习,或者如何使用预训练模型进行推理的文章。

让我们回顾一下我们的计划🗺️

今天我们要:

  • 查找使用 JavaScript 编写的游戏
  • 找到一个我们可以控制游戏的模型
  • 将两者混合在一起并享受乐趣🤪
  • 不要抱怨我们刚刚写的代码破坏了乐趣😁

免责声明

我这里没有展示任何 JavaScript 代码的最佳实践。它是从其他地方复制粘贴过来的,我们的目的是让它正常工作。

寻找游戏

当我为我的关于AI和JavaScript的演讲制作一个演示程序时,我做的第一件事就是寻找一个用JavaScript编写的游戏。我不想要一个复杂的游戏,只要一个轻量级的、可以轻松定制的游戏就行。

您现在可以观看我的演讲👇🏼:

在 Google 上搜索后JavaScript game,我在 W3Schools 上找到了这个游戏

用 JavaScript 和 HTML 编写的简单游戏

游戏很简单。你只需要引导红色方块穿过来袭的绿色条。如果它碰到任何一条绿色条,游戏就结束。

找到合适的模型

对于这个游戏,我很快就想到了SpeechCommand。这个模型可以识别由简单单词组成的语音命令。目前它只有 18 个单词,但我们甚至不需要这些单词,因为我们只需要指示(updownleftright)。

该模型使用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>
Enter fullscreen mode Exit fullscreen mode

创建项目结构

现在让我们设置一下项目结构。我们需要以下文件:

  • 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"
  },
...
}
Enter fullscreen mode Exit fullscreen mode

我在这里使用 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>
Enter fullscreen mode Exit fullscreen mode

由于我们将使用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; 
}
Enter fullscreen mode Exit fullscreen mode

我在这里所做的只是将其中一些函数改为常量,并导出稍后需要的内容。现在让我们编写index.js并导入语音命令模型。

import * as speechCommands from "@tensorflow-models/speech-commands";
Enter fullscreen mode Exit fullscreen mode

接下来,导入我们玩游戏所需的功能:

import { startGame, moveup, movedown, clearmove } from './main';
Enter fullscreen mode Exit fullscreen mode

我不会使用 left 和 write,但你完全可以使用它们。接下来,我们需要创建模型的实例并初始化它。由于加载模型需要异步调用,我们将使用IIFE(立即调用函数表达式)

const recognizer = speechCommands.create("BROWSER_FFT");

(async function() {
  await recognizer.ensureModelLoaded();

  console.log(recognizer.wordLabels());

  startGame();
})();
Enter fullscreen mode Exit fullscreen mode

模型加载完成后,我们调用 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');
      }
    }

  }
}
Enter fullscreen mode Exit fullscreen mode

这将启动游戏并监听你的麦克风。如果你还没有授权浏览器使用麦克风,则需要先授权。

Listen 方法返回一个包含结果对象的 Promise。获取结果后,我们将调用辅助函数来检查哪些单词被识别,并根据结果触发相应的操作。

在此处查看现场演示

改进

如果你看过SpeechCommand 仓库的文档,你会发现在调用 时speechCommands.create(),你可以指定加载的模型能够识别的词汇。这是 的第二个可选参数speechCommands.create()。我们可以用它来将单词限制为仅限方向性的:

const recognizer = speechCommands.create('BROWSER_FFT', 'directional4w');
Enter fullscreen mode Exit fullscreen mode

现在如果你尝试该模型,它会更准确,因为它只关注方向词(up,,,)。downleftright

概括

就是这样。这就是你需要做的全部,以便使用Tensorflow.js SpeechCommand模型用语音控制这个小游戏。

现在使用这些预先训练的模型创建一些有趣的东西,并确保检查它们的示例存储库。

要查看更多鼓舞人心的演示,请查看由Asim HussainEleanor HaproffOsama Jandali构建的aijs.rocks

文章来源:https://dev.to/yashints/let-s-build-a-game-with-tensorflow-js-in-10-minutes-1co5
PREV
让我们在 AWS 上免费设置一个 VPN 服务器,只需 5 分钟😱🤯🔥
NEXT
JavaScript 可以做到这一点吗?