十、创建游戏控制器
有了动画,你可以创造任何你能想到的东西:真实的或超现实的,简单的或复杂的。它可以讲述一个故事,也可以是完全抽象的,或者介于两者之间。当 HTML5 发布时,它包含了一个可用于 2D 和 3D 动画的 canvas 元素。
在本章中,您将创建一个游戏,游戏杆连接到 Arduino,Arduino 用作游戏控制器。本章中的 3D 图形是使用名为 Three.js 的 JavaScript 库创建和操作的,来自 Arduino 的数据用于操作图形。
动画
动画的流畅取决于两件事:每秒帧数(FPS),动画运行的帧速率;以及一帧和下一帧有多么不同。
当你为网页浏览器制作动画时,你不能保证它的帧率。每一帧都必须由观众的电脑渲染,所以这将取决于他们的电脑渲染一帧的速度。场景越复杂,计算机处理器要做的工作就越多,因此这会降低帧速率。
HTML5 画布元素
当 HTML5 在 2014 年发布时,它支持新的多媒体和图形格式,而以前的 HTML 版本不支持这些格式。它包括新的元素,如视频、音频和画布。
canvas 元素在网页上为图形元素创建一个区域。它与其他 HTML 元素具有相同的结构,并且像其他元素一样可以具有宽度和高度等属性。创建一个画布元素。
它在 DOM 中,可以由 JavaScript 选择,并用于显示和动画脚本形状和场景。开发了许多 JavaScript 库,使得在画布上创建动画变得更加容易;其中包括 processing.js、PixiJS、Paper.js、BabylonJS 以及本章中使用的 Three.js。
CSS 动画
用 JavaScript 制作动画还有一个替代方案,那就是用 CSS 制作动画。它的优点在于它使用较少的处理能力。在网页上制作 2D 动画已经成为一种流行的方式,并且已经发布了许多 CSS 动画框架,使得制作 CSS 动画变得更加容易。其中包括 Animate.css、Animatic 和 Loader CSS。
网络上的 3D
现代网络浏览器上的三维图形使用 WebGL。它允许您创建可以在 HTML canvas 元素中显示的动画。
web GL(web GL)
WebGL (Web Graphics Library)是一个 JavaScript API,允许您在 Web 浏览器中创建和动画 3D 对象。它基于 OpenGL,可以在大多数现代网络浏览器上运行。它使用计算机的图形处理单元(GPU)来处理 3D 场景,而不是浏览器。WebGL 使用两种编程语言来处理和渲染场景,JavaScript 和 GLSL (OpenGL 着色语言)。GLSL 翻译 JavaScript,这样它就可以通过 GPU 运行代码。有两个着色器程序通过 GPU 运行,一个顶点着色器和一个片段着色器。您需要实现这两者来渲染场景。
WebGL 中的编码会变得相当复杂;它没有渲染器,所以你必须编写所有的着色功能。在 WebGL 之上已经构建了许多 JavaScript 库;为了更容易使用,其中一个是 Three.js。
三维空间
三维图形使用三维来创建一个世界。对象(网格)存在于 3D 世界中,并与世界中的其他对象进行动画制作和交互。3D 场景可以由网格、灯光、相机以及颜色和纹理组成。
有三个轴,每个维度一个:x 轴、y 轴和 z 轴。在 Three.js 中,x 轴是水平轴,y 轴是垂直轴,z 轴是深度。图 10-1 显示了 WebGL 和 Three.js 中的 3D 轴。
图 10-1
The x-, y-, and z-axes
Three.js 场景中的坐标系不同于浏览器的坐标系。浏览器的坐标系统通常由一个称为像素的单位组成。它从浏览器窗口左上角的 0,0 开始。位置 1,1 将向右 1 个像素,向下 1 个像素。WebGL 坐标系从 WebGL 场景中心的 0,0 开始。WebGL 中的 1 单位不是 1 像素。所以移动一个 WebGL 对象 1,1 会使它移动超过 1 个像素。
该浏览器也是二维的,因此 3D 场景需要转换以适应 2D 画布。
3D 网格
三维(3D)对象由 3D 空间中的顶点(点)组成,由边连接,形成面。对象存在于具有 x、y 和 z 坐标的 3D 空间中。图 10-2 显示了三维物体的点、线和面。
图 10-2
A 3D object
着色器
着色器计算如何在 3D 空间中渲染对象。着色器在对象设置动画时计算出对象的位置和颜色。他们还计算出物体的每个部分相对于其位置和场景中其他元素(如照明)的明暗程度。WebGL 使用 GPU 的顶点着色器和片段着色器来渲染场景。
顶点明暗器
3D 中的对象由顶点组成。顶点着色器处理这些顶点中的每一个,并在屏幕上给它一个位置。它将顶点的三维位置转换成屏幕上的 2D 点。
片段着色器
片段着色器在渲染对象时计算出对象的颜色。可以有许多元素决定对象的颜色,包括其材质颜色和场景中的照明。WebGL 通过在三个顶点之间创建三角形来构成对象的面。片段着色器计算出每个顶点的值,并在顶点处插值以计算出整体颜色。渲染所涉及的基本步骤如图 10-3 所示。
图 10-3
The rendering process
顶点数组被发送到 GPU 顶点着色器函数,经过处理后,它们被组装成三角形并被光栅化。光栅化将表示为矢量的三角形转换为像素表示。这些然后通过 GPU 上的片段着色器函数发送,并且一旦被处理,就被发送到准备在网页上渲染的帧缓冲区。
摄像机和灯
相机和灯光可以添加到空间中。Three.js 提供了许多不同的相机,包括正交相机和透视相机。
在 Three.js 中有不同类型的灯光。它们是环境光,平行光,点光和聚光灯。环境光将均匀地照亮整个场景。它的位置、旋转和比例没有影响,但它的颜色和强度有影响。平行光类似于太阳;他们有一个方向,但却无限遥远。这意味着与物体的距离无关紧要,但位置和旋转有关系。点光源类似于灯泡;它们从各个方向照亮空间,它们的位置很重要。聚光灯与点光源相似,因为它们的位置很重要,但它们只在一个方向上发光。
三. js
Three.js 是构建在 WebGL 之上的众多 JavaScript 库之一。它的功能可以让您在几个步骤中创建一个场景,并添加着色器,灯光,相机和网格。
三个向量
Three.js Vector3 是一个表示具有三个元素的向量的类,用于表示 x 轴、y 轴和 z 轴。Vector3 代表的主要东西是 3D 空间中的一个点,3D 空间中的方向和长度,或者是一个仲裁有序的三个数字。在本章中,Vector3 用于摄像机设置。
Create a Three.js Scene
在 Three.js 中查看任何内容所需的基本组件是场景、摄像机和渲染器。3D 对象和灯光等元素附加到场景对象。场景对象和相机对象被附加到渲染器对象以便被渲染。
创建一个名为 basic_scene.html 的文件,并将清单 10-1 中的代码复制到其中。
<html>
<head>
<title>three.js </title>
<style>
body { margin: 0; }
canvas { width: 100%; height: 100% }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.js"></script>
<script>
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );
cube.rotation.y = 40;
camera.position.z = 5;
renderer.render(scene, camera);
</script>
</body>
</html>
Listing 10-1Basic_scene.html
在 web 浏览器中打开该文件,您应该会在浏览器中看到一个绿色立方体。清单 10-1 中的 Three.js 代码首先创建一个三场景,然后是一个摄像机,然后是一个渲染器。渲染器附加到 HTML 的主体。创建一个立方体并添加到场景中。立方体有位置和旋转。渲染器在代码结尾被调用,场景和摄像机被附加。
代码 ExplainedTable 10-1 在 basic_scene.html 中有更详细的代码。
表 10-1
basic_scene.html explained
| var scene = new THREE.Scene();
| Three.js 函数三。Scene()用于创建一个新的场景对象,该对象存储在变量 scene 中。 |
| var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
| 一个新的照相机对象被存储在可变照相机中。是一台 Three.js 透视相机。0.1 是视野,1000 是长宽比。 |
| var renderer = new THREE.WebGLRenderer();
| Three.js 渲染器对象存储在变量渲染器中。 |
| renderer.setSize( window.innerWidth, window.innerHeight );
| setSize 函数将调整画布的大小;在这个例子中,画布的大小是浏览器窗口的大小,所以它使用浏览器窗口的当前宽度和高度。 |
| document.body.appendChild( renderer.domElement );
| 渲染器被附加到一个 DOM 元素上,因此可以放在网页上。 |
| var geometry = new THREE.BoxGeometry( 1, 1, 1 );
| Three.js 有很多几何对象;这些对象包含对象的点(顶点)和填充(面)。 |
| var``material
T2】 | MeshBasicMaterial 是 Three.js 中可用的材质之一。它们都接受一个 properties 对象,这个对象使用十六进制数字被赋予绿色。十六进制数前面的 0x 告诉 JavaScript 后面的数字是十六进制数。这是 JavaScript 和其他编程语言中使用的语法。 |
| var cube = new THREE.Mesh( geometry, material );
| Three.js 网格对象将材质添加到几何体中。 |
| scene.add( cube );
| 立方体被添加到场景中。 |
| cube.rotation.y = 40;
camera.position.z = 5;
| 立方体在 y 轴上旋转,并在 z 轴上给定一个位置。 |
| renderer.render(scene, camera);
| 渲染器在网页上渲染,场景和相机是渲染函数的参数。 |
Animating the Cube
此刻立方体是静止的。需要有一个动画循环来创建动画。使用 JavaScript requestAnimationFrame(回调)函数。当您希望浏览器重新绘制网页时,会调用该函数。回调函数被传递给函数,它通常以每秒 60 帧的速率运行。打开清单 10-1 中的 basic_scene.html,用清单 10-2 中的粗体代码更新它。
<html>
<head>
<title>three.js basics</title>
<style>
body { margin: 0; }
canvas { width: 100%; height: 100% }
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.js"></script>
<script>
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
var geometry = new THREE.BoxGeometry( 1, 1, 1 );
var material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
var cube = new THREE.Mesh( geometry, material );
scene.add( cube );
cube.rotation.y = 40;
camera.position.z = 5;
var animate = function () {
cube.rotation.x += 0.05;
cube.rotation.y += 0.01;
cube.rotation.z += 0.007;
renderer.render(scene, camera);
requestAnimationFrame( animate );
};
animate();
</script>
</body>
</html>
Listing 10-2basic_scene.html updated
请注意,渲染器现在位于 animate 函数内部。在 web 浏览器中重新加载页面;你现在应该看到立方体动画。它在 x 轴、y 轴和 z 轴上旋转,每个轴的速率略有不同。
代码解释
表 10-2 更详细的解释了 basic_scene.html 中的代码。
表 10-2
basic_scene.html updated
explained
| var animate = function () {}
| 创建一个函数,其中包含动画场景对象的代码。 |
| cube.rotation.x += 0.05;``cube.rotation.y += 0.01;
T2】 | 立方体的旋转在 x 轴、y 轴和 z 轴上略有变化。 |
| renderer.render(scene, camera);
| 场景和相机被渲染到浏览器。 |
| requestAnimationFrame( animate );
| 一旦对多维数据集进行了更改,就会调用 JavaScript requestAnimationFrame()函数。animate 函数作为回调函数传递,因此它将继续运行 animate 函数。 |
Table
游戏
您在本章中创建的游戏将使用 Three.js 作为浏览器,并使用 Node.js 服务器通过串行端口从 Arduino 获取数据。这是一个简单的游戏,玩家试图抓住一个球拍上的球。附在 Arduino 上的操纵杆控制着船桨。操纵杆离中心位置越远,叶片在 x 轴上的移动速度就越快。球可以是蓝色、绿色或红色的。玩家需要用操纵杆上的按钮把球拍的颜色换成球的颜色;如果他们做到了,他们会得到一分,如果没有,他们会失去一分。如果他们错过了球,他们也会失去一分。游戏是计时的,当时间用完时,会给出分数,并有一个重新开始的选项。游戏截图如图 10-4 所示。
图 10-4
A screenshot of the game
操纵杆允许你控制两个方向的运动;它还有一个可以按下的按钮。桨可以在 x 轴和 y 轴上移动。y 轴上的移动很小,但对于玩家来说,如果他们需要更多的时间,就足够向上移动一点来尽早接球,或者向下移动一点。控制面板在 x 轴上的移动被限制在浏览器窗口的宽度范围内。
球使用动画来移动。它在 y 轴上的初始位置正好在浏览器窗口高度的上方。它在 x 轴上的初始位置是浏览器窗口宽度内的一个随机位置。它以恒定的速度下降。
球和桨需要相互作用,所以碰撞对象被添加到两者中,以便它们可以检测何时彼此接触。
游戏的最后三个元素是记分牌、计时器和开始游戏的方式。用户点击消息“开始游戏”开始游戏。然后计时器开始倒数到 0。当玩家试图抓住球时,得分和失分都有。当计时器计时到 0 时,浏览器上会出现一条新消息,显示玩家的最终得分和重新游戏的选项。
Set Up the Joystick
Arduino 游戏手柄的品牌有很多。我用的是 Elegoo 做的一个;它有 5 个引脚,GND(接地)、+5V、VRx(控制 x 轴上的移动)、VRy(控制 y 轴上的移动)和 SW(用于开关、按钮)。根据品牌或操纵杆的不同,针脚可能会反过来。要设置 Arduino,您需要:
- 5 根公母跨接线
- 在 Arduino 操纵杆上
- 在 Arduino
组成如图 10-5 所示,操纵杆的设置如图 10-6 所示。
图 10-6
The setup
图 10-5
1. An Arduino Uno; 2. A Joystick The Arduino Code
捕捉操纵杆上 x 和 y 运动的针脚是模拟的,开关(按钮)是数字的。这些值被捕获到单个变量中,然后在 Serial.println()函数中连接在一起。每个值前面还会添加一个字母,以便 web 应用程序可以根据需要区分这些值。每个值都由一个“,”字符分隔,这样就可以在 Node.js 服务器中将传入的字符串拆分为一个字符串数组。这些连接如下:
- 操纵杆上的 GND 连接到 Arduino 上的 GND
- 操纵杆上的+5V 连接到 Arduino 上的 5V
- VRx 连接到 A01 引脚
- VRy 连接到引脚 A02
- SW 连接到引脚 2(一个数字引脚)
操纵杆没有连接到试验板,所以需要公母线。在 Arduino IDE 中创建新的草图,命名为 chapter _ 10 复制清单 10-3 中的代码。
int xAxisPin = A1;
int yAxisPin = A0;
int buttonPin = 2;
int xPosition = 0;
int yPosition = 0;
int buttonState = 0;
void setup() {
Serial.begin(9600);
pinMode(xAxisPin, INPUT);
pinMode(yAxisPin, INPUT);
pinMode(buttonPin, INPUT_PULLUP);
}
void loop() {
xPosition = analogRead(xAxisPin);
yPosition = analogRead(yAxisPin);
buttonState = digitalRead(buttonPin);
xPosition=map(xPosition,0,1023,1,10);
yPosition=map(yPosition,0,1023,1,10);
Serial.println("x" + (String)xPosition + ",y" + (String)yPosition + ",b" + (String)buttonState);
delay(100);
}
Listing 10-3chapter_10.ino
验证代码,然后将其上传到 Arduino,如果您在 Arduino IDE 中打开串行监视器,您应该会看到来自操纵杆的数据。
代码解释
表 10-3 更详细地解释了 chapter_10.ino 中的代码。
表 10-3
chapter_10.ino
explained
| pinMode(buttonPin, INPUT_PULLUP);
| 因为按钮是一个开关,所以它必须设置为 INPUT_PULLUP,这样 Arduino 就知道当开关打开时给它什么值。 |
| xPosition = analogRead(xAxisPin);``yPosition = analogRead(yAxisPin);
T2】 | 在每次循环中,记录 x 轴、y 轴和按钮的值。 |
| xPosition=map(xPosition,0,1023,1,10);
yPosition=map(yPosition,0,1023,1,10);
| 模拟数据可以在 0 和 1023 之间;这些值被映射到 1 到 10 之间的值。这使得它们更容易在 web 应用程序中使用。 |
| Serial.println("x" + (String)xPosition + ",y" + (String)yPosition + ",b" + (String)buttonState);
| 数据被连接在一起,来自 x 轴和 y 轴的数字以及按钮被转换成字符串,这样它们就可以被发送到 Node.js 服务器。 |
Web 应用程序
游戏将使用 Three.js JavaScript 库编写。Node.js 服务器将类似于本书中的其他应用程序。客户端代码将被分解成多个 JavaScript 文件:一个用于游戏,一个用于计时器,一个用于比分,一个 main.js 文件从 Node.js 服务器获取数据。应用程序的结构将是:
/chapter_10
/node_modules
/public
/css
main.css
/javascript
Countdown.js
Game.js
main.js
Points.js
/views
index.ejs
index.js
Set Up the Node.js Server
本章将再次使用 Express、ejs 和 socket.io。要设置框架应用程序:
- 创建一个新文件夹来存放应用程序。我把我的叫做第 10 章。
- 打开命令提示符(Windows 操作系统)或终端窗口(Mac)并导航到新创建的文件夹。
- 当你在正确的目录键入 npm init 创建一个新的应用程序;您可以按下 return 键浏览每个问题或对其进行更改。
- 现在可以开始添加必要的库了,所以要在命令行下载 Express.js,请键入 npm install express@4.15.3 - save。
- 然后安装 ejs,键入 npm install ejs@2.5.6 - save。
- 下载完成后,安装串口。在 Mac 上键入 npm 安装 serial port @ 4 . 0 . 7–save,在 Windows PC 上键入 npm 安装 serial port @ 4 . 0 . 7–build-from-source。
- 然后最后安装 socket.io,输入 npm install socket.io@1.7.3 - save。
在应用程序的根目录下打开或创建一个 index.js 文件,并复制清单 10-4 中的代码,确保使用 Arduino 连接的串行端口更新串行端口。
var http = require('http');
var express = require('express');
var app = express();
var server = http.createServer(app);
var io = require('socket.io')(server);
var SerialPort = require('serialport');
var serialport = new SerialPort('<add in the serial port for your Arduino>', {
parser: SerialPort.parsers.readline('\n')
});
app.engine('ejs', require('ejs').__express);
app.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public'));
app.get('/', function (req, res){
res.render('index');
});
serialport.on('open', function(){
console.log('serial port opened');
});
io.on('connection', function(socket){
console.log('socket.io connection');
serialport.on('data', function(data){
data = data.replace(/(\r\n|\n|\r)/gm,"");
var dataArray = data.split(',');
console.log(dataArray);
socket.emit("data", dataArray);
});
socket.on('disconnect', function(){
console.log('disconnected');
});
});
server.listen(3000, function(){
console.log('listening on port 3000...');
});
Listing 10-4index.js
当通过串行端口从 Arduino 接收数据时,任何额外的字符(如换行符)都会被删除,然后数据被拆分成一个数组,并使用 id 为“data”的 socket io 发出。
由于串行端口功能仅在有套接字 io 连接时检查数据,因此如果启动服务器,您将看不到数据,因为套接字 io 连接将在尚未创建的 main.js 文件中进行。
构建游戏
这个游戏有许多独特的元素,这些都是构建这个游戏的步骤。它们是:
- 创建包含 HTML 的主 index.ejs 页面。
- 创建 main.js 文件,该文件将有一个从服务器获取数据的套接字。
- 使用在 x 轴和 y 轴上移动并带有约束的桨创建场景。
- 通过添加可以与球拍碰撞的动画球来更新场景。
- 创建一个保存游戏评分代码的 JavaScript 文件。
- 创建一个 JavaScript 文件作为计时器。
- 增加启动和重启游戏的功能。
Create the Web Page
index.ejs 页面将包含许多 div 元素,这些元素将保存分数、计时器以及开始和重新开始文本。Three.js 场景将被附加到 HTML 的主体。在 views 文件夹中打开或创建 index.ejs 文件,并复制清单 10-5 中的代码。
<html>
<head>
<title>three.js basics</title>
<style>
body { margin: 0; }
canvas { width: 100%; height: 100% }
</style>
</head>
<body>
<script src="/socket.io/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.js"></script>
<script src="javascript/Game.js"></script>
<script src="javascript/main.js"></script>
</body>
</html>
Listing 10-5index.ejs
如果您已经创建了 Game.js 和 main.js 文件,即使它们是空的,您也应该能够运行代码。在控制台中,转到应用程序的根目录,键入 nodemon index.js 或 node index.js。这将启动服务器;您可以通过键入 http://localhost:3000 在 web 浏览器上打开该页面。
Create Main.js
在 public/javascript 文件夹中打开或创建 main.js 文件,并复制清单 10-6 中的代码。
(function(){
var socket = io();
socket.on('data', function(data){
Game.newData(data);
});
})();
Listing 10-6main.js
创建一个变量来保存套接字。当服务器上运行 id 为“data”的 socket.emit()函数时,main.js 中 id 为“data”的 socket.on()函数接收数据。然后使用 newData()函数将这些数据传递给 Game.js 文件。
Create Game.js
Game.js 文件包含创建 Three.js 场景和网格以及动画的所有代码。它需要来自服务器的数据来知道将桨移动到哪里。它包含一个名为 newData()的函数,该函数接收来自操纵杆的数据并相应地更新拨片。
桨需要留在浏览器窗口内,所以需要检查桨的新位置不会将它带离屏幕。Three.js 中挡板的位置使用不同的坐标来表示挡板在屏幕上的位置。例如,如果屏幕宽度为 625,您希望桨板移动的最大位置将是 625–桨板宽度。Three.js 不懂 625;它有自己的坐标系。对于宽度为 625 的浏览器,面板在 x 轴上的最大位置约为 4,最小位置为-4。在代码中有一个函数进行这种转换,这样你就可以计算出面板相对于浏览器坐标系的位置。
根据操纵杆离其中心位置的距离,操纵杆也会在 x 轴上加速。要做到这一点,来自 Arduino 的轴数据必须被映射到一个新值,该值将用于告诉立方体移动多少以及向哪个方向移动。这必须在正负方向上实现,因此调用 moveObjectAmount()。它做的第一件事是调用一个名为 scaleInput()的函数,该函数从 Arduino 数据中获取号码并映射到一个新号码。这个数字被返回给 moveObjectAmount()函数,在这里它被除以 10,使它足够小,这样桨位置的增加或减少就不会移动得太远。
在 public/javascript 文件夹中打开或创建 Game.js 文件,并复制清单 10-7 中的代码。
var Game = (function(){
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
console.log(windowWidth);
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, windowWidth/windowHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( windowWidth, windowHeight );
document.body.appendChild( renderer.domElement );
var geometry = new THREE.BoxGeometry( 2, 0.2, 0.8 );
var material = new THREE.MeshLambertMaterial( { color: 0x00ff00 } );
var paddle = new THREE.Mesh( geometry, material );
scene.add( paddle );
paddle.position.y = -2;
camera.position.z = 5;
var light = new THREE.DirectionalLight(0xe0e0e0);
light.position.set(5,2,5).normalize();
scene.add(light);
renderer.render(scene, camera);
var newData = function(data){
updateScene(data);
}
var updateScene = function(data){
var screenCoordinates = getCoordinates();
var moveObjectBy;
var x = data[0];
var y = data[1];
var button = data[2];
x = x.substr(1);
y = y.substr(1);
button = button.substr(1);
if(x > 5){
if(screenCoordinates[0] < windowWidth - 80){
moveObjectBy = moveObjectAmount(x);
paddle.position.x = paddle.position.x + moveObjectBy;
}
} else if (x < 5){
if(screenCoordinates[0] > 0 + 80){
moveObjectBy = moveObjectAmount(x);
paddle.position.x = paddle.position.x + moveObjectBy;
}
}
if(y > 5){
if(screenCoordinates[1] < windowHeight - 100){
paddle.position.y = paddle.position.y - 0.2;
}
} else if (y < 5){
if(screenCoordinates[1] > 0 + 300){
paddle.position.y = paddle.position.y + 0.2;
}
}
renderer.render(scene, camera);
}
var moveObjectAmount = function(x){
var output = scaleInput(x);
output = utput/10;
output = Math.round( utput * 10) / 10;
return output;
}
var scaleInput=function(input){
var xPositionMin = -4;
var xPositionMax = 4;
var inputMin = 1;
var inputMax = 10;
var percent = (input - inputMin) / (inputMax - inputMin);
var output = percent * (xPositionMax - xPositionMin) + xPositionMin;
return output;
}
var getCoordinates = function() {
var screenVector = new THREE.Vector3();
paddle.localToWorld( screenVector );
screenVector.project( camera );
var posx = Math.round(( screenVector.x + 1 ) * renderer.domElement.offsetWidth / 2 );
var posy = Math.round(( 1 - screenVector.y ) * renderer.domElement.offsetHeight / 2 );
return [posx, posy];
}
return{
newData: newData
}
})();
Listing 10-7Game.js
代码解释
表 10-4 更详细的解释了 Game.js 中的代码。
表 10-4
Game.
js
explained
| var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
| 浏览器的窗口宽度和高度在代码中被多次使用,因此将它们放在各自的变量中是有意义的。这意味着您只需要在代码中对窗口函数进行两次调用。 |
| var material = new THREE.MeshLambertMaterial( { color: 0x00ff00 } );
| 在清单 10-1 中,桨上的材质使用了一个 THREE.MeshBasicMaterial。这个场景使用了一个 MeshLamabertMaterial 作为基本材质,它对灯光没有反应,而 lambert 材质有反应。 |
| var light = new THREE.DirectionalLight(0xe0e0e0);``light.position.set(5,2,5).normalize();
T2】 | 新的平行光被创建,其位置被设置并添加到场景中。Normalize 确保新位置处于正确的方向。 |
| var newData = function(data){``updateScene(data);
T2】 | 当新数据来自 Arduino 时,函数 newData 由 main.js 调用。当它被调用时,它将数据传递给 Game.js 函数 updateScene(),然后该函数将处理数据并更新场景。 |
| var updateScene = function(data){``...
T2】 | updateScene 数据有一个参数,即来自 Arduino 的数据。 |
| var screenCoordinates = getCoordinates();
| 调用 getCoordintes()函数;这是计算相对于浏览器窗口坐标系而不是 Three.js 坐标系的屏幕坐标的函数。坐标由函数返回并存储在一个变量中。 |
| var moveObjectBy;
| 这个变量将保持桨应该在 x 轴上移动的量。 |
| var x = data[0];``var y = data[1];
T2】 | 来自 Arduino 的数据包含 x 轴和 y 轴的信息。和按钮;变量保存数组中相关位置的数据。 |
| x = x.substr(1);``y = y.substr(1);
T2】 | Arduino 中的数据在开头包含一个识别字符,JavaScript substr()函数会删除这个字符。 |
| if(x > 5){``...
T2】 | 有一系列 if 语句检查 x 和 y 数据是大于还是小于 5。如果大于 5 °,桨的位置将正向更新;少于 5 的将被负向更新。 |
| if(screenCoordinates[0] < windowWidth – 80){``...
T2】 | 每个 if 语句内部都有另一个 if 语句。它检查桨的屏幕坐标是否在浏览器窗口的界限内;如果是,可以移动桨。 |
| moveObjectBy = moveObjectAmount(x);
| 如果移动是针对 x 轴的,则来自 Arduino 的数据被映射,以获得桨移动量的值。moveObjectAmount()函数被传递给 x 的 Arduino 值。moveObjectBy 变量保存该函数的返回值。 |
| paddle.position.x = paddle.position.x + moveObjectBy;
| 用新值更新挡板位置,将其添加到挡板的当前位置。 |
| if(screenCoordinates[1] < windowHeight - 100){``paddle.position.y = paddle.position.y – 0.2;
T2】 | 操纵杆的 y 位置的变化是恒定的,因此它将是+0.2 或-0.2,这取决于操纵杆的位置。新值将添加到 y 轴上的桨的当前位置。 |
| renderer.render(scene, camera);
| 当所有的 if 语句都被解析后,渲染器被调用来更新场景。 |
| var moveObjectAmount = function(x){``...
T2】 | moveObjectAmount()函数接受一个参数,即来自 Arduino 的 x 值。 |
| var output = scaleInput(x);
| 调用 scaledInput()函数,传递 x 值,并将该调用的返回值放入一个变量中。 |
| output = output/10;
| 10 除以返回值。这使得它足够小,以增加桨的 x 值。 |
| output = Math.round(output * 10) / 10;
| 然后将该值四舍五入到小数点后 1 位。 |
| return output;
| 该值被返回给调用函数。 |
| var scaleInput=function(input){``...
T2】 | scaleInput()函数有一个参数,一个需要映射的数字。 |
| var xPositionMin = -4;
var xPositionMax = 4;
| 两个变量保存输入数字可以映射到的最小和最大数字。 |
| var inputMin = 1;
var inputMax = 10;
| 两个变量包含输入的最小和最大值。 |
| var percent = (input – inputMin) / (inputMax – inputMin);
var output = percent * (xPositionMax – xPositionMin) + xPositionMin;
| 计算映射值。 |
| var getCoordinates = function() {``....
T2】 | getCoordinates()函数将在 Three.js 坐标空间中查找桨的当前位置,并将该位置转换为浏览器坐标。 |
| var screenVector = new THREE.Vector3();
| 创建一个保存 Three.js 向量的变量。 |
确保 Arduino 已连接到您的电脑,但串行监视器已关闭。在控制台中转到应用程序的根目录,键入 nodemon index.js 或 node index.js 来启动服务器。转到 http://localhost:3000 并打开页面。
Add an Interactive Ball
球需要从浏览器窗口的顶部落下。它要么被桨抓住,要么错过桨。在这些事件中的任何一个之后,它需要被移动到它的初始 y 位置和浏览器窗口中的一个随机的 x 位置,然后再次放下。在球击中球拍之前,球拍需要改变颜色以匹配球。
从清单 10-7 中打开你的 Game.js 文件,并对清单 10-8 中粗体部分进行修改。
var Game = (function(){
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var colorArray = [0xff0000, 0x00ff00, 0x0000ff];
var ballColor = Math.floor(Math.random() * 3);
var colorChoice = 0;
var collisionTimer = 15;
var minMaxX = xMinMax(windowWidth);
minMaxX = (parseFloat((minMaxX/10))-3.0.toFixed(1));
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, windowWidth/windowHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( windowWidth, windowHeight );
document.body.appendChild( renderer.domElement );
var geometry = new THREE.BoxGeometry( 2, 0.2, 0.8 );
var material = new THREE.MeshLambertMaterial( { color: 0x00ff00 } );
var geometrySphere = new THREE.SphereGeometry( 0.3, 32, 32 );
var materialSphere = new THREE.MeshLambertMaterial( {color: colorArray[ballColor]} );
var paddle = new THREE.Mesh( geometry, material );
var ball = new THREE.Mesh( geometrySphere, materialSphere );
updateBallPosition();
paddle.position.y = -2;
camera.position.z = 5;
var light = new THREE.DirectionalLight(0xe0e0e0);
light.position.set(5,2,5).normalize();
scene.add(light);
scene.add(new THREE.AmbientLight(0x656565));
scene.add( paddle );
scene.add( ball );
// renderer.render(scene, camera);
var newData = function(data){
updateScene(data);
}
var updateScene = function(data){
var screenCoordinates = getCoordinates();
var moveObjectBy;
var x = data[0];
var y = data[1];
var button = data[2];
x = x.substr(1);
y = y.substr(1);
button = button.substr(1);
if(button ==="0"){
updatePaddleColor();
}
if(x > 5){
if(screenCoordinates[0] < windowWidth - 150){
moveObjectBy = moveObjectAmount(x);
paddle.position.x = paddle.position.x + moveObjectBy;
}
} else if (x < 5){
if(screenCoordinates[0] > 0 + 150){
moveObjectBy = moveObjectAmount(x);
paddle.position.x = paddle.position.x + moveObjectBy;
}
}
if(y > 5){
if(screenCoordinates[1] < windowHeight - 100){
paddle.position.y = paddle.position.y - 0.2;
}
} else if (y < 5){
if(screenCoordinates[1] > 0 + 300){
paddle.position.y = paddle.position.y + 0.2;
}
}
renderer.render(scene, camera);
}
var moveObjectAmount = function(x){
var scaledX = scaleInput(x);
scaledX = scaledX/10;
scaledX = Math.round(scaledX * 10) / 10;
return scaledX;
}
var scaleInput=function(input){
var xPositionMin = -4;
var xPositionMax = 4;
var inputMin = 1;
var inputMax = 10;
var percent = (input - inputMin) / (inputMax - inputMin);
var outputX = percent * (xPositionMax - xPositionMin) + xPositionMin;
return outputX;
}
var getCoordinates = function() {
var screenVector = new THREE.Vector3();
paddle.localToWorld( screenVector );
screenVector.project( camera );
var posx = Math.round(( screenVector.x + 1 ) * renderer.domElement.offsetWidth / 2 );
var posy = Math.round(( 1 - screenVector.y ) * renderer.domElement.offsetHeight / 2 );
return [posx, posy];
}
var updatePaddleColor = function(){
colorChoice++;
if(colorChoice === 3){
colorChoice = 0;
}
paddle.material.color.setHex( colorArray[colorChoice]);
}
function randomPosition(num){
var newPostion = (Math.random() * (0 - num) + num).toFixed(1);
newPostion *= Math.floor(Math.random()*2) == 1 ? 1 : -1;
return newPostion;
}
function xMinMax(input){
xPositionMin = 4;
xPositionMax = 184;
xWindowMin = 200;
xWindowMax = 2000;
var percent = (input - xWindowMin) / (xWindowMax - xWindowMin);
var outputX = percent * (xPositionMax - xPositionMin) + xPositionMin;
return outputX;
}
function updateBallPosition(){
xPos = randomPosition(minMaxX);
ball.position.y = 5;
ball.position.x = xPos;
ballColor = Math.floor(Math.random() * 3);
ball.material.color.setHex( colorArray[ballColor]);
}
var animate = function () {
var firstBB = new THREE.Box3().setFromObject(ball);
var secondBB = new THREE.Box3().setFromObject(paddle);
var collision = firstBB.isIntersectionBox(secondBB);
if(!collision){
collisionTimer = 15;
if(ball.position.y > (paddle.position.y - 0.5)){
ball.position.y -= 0.08;
} else {
updateBallPosition();
}
}
if(collision){
if(collisionTimer > 0){
collisionTimer = collisionTimer -1;
} else {
updateBallPosition();
}
}
renderer.render(scene, camera);
requestAnimationFrame( animate );
};
animate();
return{
newData: newData
}
})();
Listing 10-8Game.js first update
代码解释
表 10-5 更详细的解释了 Game.js 首次更新中的代码。
表 10-5
Game.js first update explained
| var colorArray = [0xff0000, 0x00ff00, 0x0000ff];``var ballColor = Math.floor(Math.random() * 3);
T2】 | 包含三个颜色值的数组保存在一个变量中。每当操纵杆按钮被按下时,这个阵列循环改变桨的颜色。colorChoice 变量保存了面板颜色的数组位置。球的颜色是从数组中随机选择的。JavaScript Math.random()函数用于在 0 和 3 之间选择一个数字。 |
| var collisionTimer = 15;
| 当球和桨发生碰撞时,球会被移动到初始的 y 位置,然后再次下落。所以它不会很快消失,因为当发生碰撞时,它们会触动一个设置好并倒计时的计时器。 |
| var minMaxX = xMinMax(windowWidth);
| 当计算球的新 x 位置时,它需要在当前浏览器窗口的宽度内。调用函数 xMinMax 并将其传递给当前浏览器窗口宽度。这个窗口宽度将被映射到一个将球保持在浏览器窗口内的数字。返回值存储在一个变量中。 |
| minMaxX = (parseFloat((minMaxX/10))-3.0.toFixed(1));
| 返回值除以 10,因此它符合 Three.js 坐标系。toFixed()函数将其设置为一个小数位。toFixed()函数返回一个字符串。 |
| updateBallPosition();
| 球的起始位置需要在整个游戏过程中重新设置,updateBallPosition()函数实现了这一点。 |
| // renderer.render(scene, camera);
| 这一行已经被注释掉了,因为它可以被删除,现在调用渲染场景发生在球动画或球拍移动的时候。 |
| if(button ==="0"){``updatePaddleColor();
T2】 | 检查来自操纵杆按钮的数据是否为“0”;如果是,则通过调用 updatePaddleColor()函数来改变面板颜色。 |
| var updatePaddleColor = function(){``colorChoice++;``if(colorChoice === 3){``colorChoice = 0;``}``paddle.material.color.setHex( colorArray[colorChoice]);
T6】 | colorChoice 变量用作颜色数组的索引。当桨在阵列周围循环时,变量增加 1。如果 colorChoice 变成 3(在数组索引之外),它就变成 0。然后桨上的材料颜色会改变。 |
| function randomPosition(num){``var newPostion = (Math.random() * (0 - num) + num).toFixed(1);``newPostion *= Math.floor(Math.random()*2) == 1 ? 1 : -1;``return newPostion;
T4】 | 该函数接受一个数字,并在 0 和传递给它的数字之间计算出一个新的随机数。然后,它随机地使其为正或为负。该函数用于查找球的新 y 位置。 |
| function updateBallPosition(){``xPos = randomPosition(minMaxX);``ball.position.y = 5;``ball.position.x = xPos;``ballColor = Math.floor(Math.random() * 3);``ball.material.color.setHex( colorArray[ballColor]);
T6】 | 该函数在球开始运动之前设置球的初始位置。通过调用 randomPosition()函数找到一个随机的 x 位置。y 轴上的初始位置保持不变;它位于浏览器窗口顶部之外。还选择了球的随机颜色。 |
| var firstBB = new THREE.Box3().setFromObject(ball);
var secondBB = new THREE.Box3().setFromObject(paddle);
| 在球和球拍周围创建一个方框。它们是围绕对象的边界框,用于检查碰撞。 |
| var collision = firstBB.isIntersectionBox(secondBB);
| 函数的作用是:检查第一个边界框(球)是否接触到第二个边界框(球)。变量 collision 保存返回值:如果是接触,则为 true,否则为 false。 |
| if(!collision){
collisionTimer = 15;
if(ball.position.y > (paddle.position.y - 0.5)){
ball.position.y -= 0.08;
} else {
updateBallPosition();
}
}
| 如果碰撞变量为假,则 collisionTimer 保持在 15。然后,检查球的位置是否低于桨的位置;如果是,球将重置到它的初始 y 位置,并再次开始向下运动。如果不是这样,球就会一直往下掉。 |
| if(collision){``if(collisionTimer > 0){``collisionTimer = collisionTimer -1;``} else {``updateBallPosition();``}
T6】 | 如果存在冲突,则检查 collisionTimer 是否大于 0,1 表示 collisionTimer 无效。当它达到 0 时,球被重置到它的初始 y 位置,并再次开始向下运动。 |
如果你重启服务器,你现在应该可以用球拍接住下落的球了。当你抓住它时,它应该等一会儿,然后又开始下落。你也应该能够改变球拍的颜色。
Update Index.ejs
最后的步骤是给游戏打分,创建一个倒计时钟,并有一个开始和重新开始游戏的方法。第一件事是用乐谱和时钟的元素和脚本更新 index.ejs,并创建 main.css 文件。打开清单 10-5 中的 index.ejs 文件,用清单 10-9 中的粗体代码更新它。
<html>
<head>
<title>three.js game</title>
<link href="/css/main.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id = "again" class="hidden">
<h1>you scored <span id="current-score"></span></h1>
<h2 id="replay">PLAY AGAIN</h2>
</div>
<div id="start">
<h1>start game</h1>
</div>
<div id="score">
<h1>points: <span id="points"></span></h1>
<div id="timer">
<h1 id="countdown"><time></time></h1>
</timer>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.js"></script>
<script src="javascript/Points.js"></script>
<script src="javascript/Countdown.js"></script>
<script src="javascript/Game.js"></script>
<script src="javascript/main.js"></script>
</body>
</html>
Listing 10-9Upated index.ejs
你会注意到 CSS 已经消失了,现在有一个链接指向一个 CSS 文件。有许多新的 div 元素;这些显示点和分数以及开始和再次播放按钮。
脚本的顺序很重要。如果在 Game.js 之后添加脚本,Countdown.js 中的某个功能将不会被 Game.js 识别。
HTML 解释道
表 10-6 更详细地解释了更新的 index.ejs 文件的代码。
表 10-6
updated index.ejs explained
| <div id = "again" class="hidden">
| 这个 div 会在游戏结束时出现;它有一个“隐藏”类,当游戏结束时,这个类就会被删除。这是在 CSS 中实现的。 |
| :-- | :-- |
| <h1>you scored <span id="current-score"></span></h1>
| 使用 JavaScript,最终分数将显示在这个元素中。 |
| <h2 id="replay">PLAY AGAIN</h2>
| 按下“再玩一次”,新的游戏开始;这是用 JavaScript 实现的。 |
| <h1>points: <span id="points"></span></h1>
| 当点更新时,此元素也将更新。 |
| <h1 id="countdown"><time></time></h1>
| 这个元素保存着倒计时钟。它是使用 JavaScript 更新的。 |
Create Main.css
在 public/css 中打开或创建一个名为 main.css 的文件,并从清单 10-10 中复制 css。
*{
margin: 0;
padding: 0;
}
body {
margin: 0;
}
body {
font-family: Verdana, Arial, sans-serif;
}
canvas {
width: 100%; height: 100%
z-index: -1;
}
#start{
position: absolute;
left: 30;
top: 40;
color: white;
cursor: pointer;
background: red;
z-index: 10;
}
#start h1{
padding: 6px;
}
#score{
position: absolute;
left: 30;
top: 10;
color: white;
width: 200px;
height: 120px;
}
#score h1{
font-size: 16px;
}
#again{
position: absolute;
left: 30;
top: 40;
color: white;
cursor: pointer;
z-index: 10;
}
#again h2{
margin-top: 4px;
}
#replay{
cursor: pointer;
z-index: 11;
background: red;
}
.hidden{
visibility: hidden;
}
Listing 10-10main.css
CSS 解释道
你希望分数和倒计时钟在游戏中。因此,它们必须被绝对地放置在页面上。这些元素还必须具有比画布更高的 z 索引。z 索引指定元素相互堆叠的顺序。具有较低 z 索引的元素被放置在具有较高 z 索引的元素之下。表 10-7 更详细地解释了 main.css 中的 CSS。
表 10-7
main.css explained
| canvas {``width: 100%; height: 100%``z-index: -1;
T3】 | 画布被设置为 web 浏览器的宽度和高度。画布的 z 索引为-1,它需要位于需要被点击的元素之后。 |
| position: absolute;``left: 30;
T2】 | id 为“start”、“score”、“again”和“replay”的元素在浏览器页面上都是绝对定位的,CSS 命令 position: absolute。然后,需要使用 left 或 right 命令以及 top 或 bottom 命令给它们在页面上的位置。 |
| cursor: pointer;
| 使用此命令的元素会在光标经过时将光标变为指针。 |
| z-index: 11;
| 指定元素的 z 索引。 |
Create Points.js
您需要编写 JavaScript 来计算和显示这些点。在 public/javascript 中打开或创建一个名为 Points.js 的文件,并将清单 10-11 中的代码复制到其中。
var Points = (function(){
var points = 0;
var pointDisplay = document.getElementById("points");
var resetPoints = function(){
points = 0;
}
var updatePoints = function(num){
points = points + num;
pointDisplay.innerHTML = points;
}
var getPoints = function(){
return points;
}
return{
resetPoints: resetPoints,
updatePoints: updatePoints,
getPoints: getPoints
}
})();
Listing 10-11Points.js
代码解释
表 10-8 更详细地解释了 Points.js 中的代码。
表 10-8
Points.js explained
| var updatePoints = function(num){``points = points + num;``pointDisplay.innerHTML = points;
T3】 | 当调用这个函数时,它被传递一个正数或负数,points 变量被更新,id 为“points”的元素的 innerHTML 也被更新。 |
| var getPoints = function(){``return points;
T2】 | 这个函数返回当前点。 |
Create a Countdown Clock
你需要一个倒计时钟来为比赛计时。一个布尔变量控制倒计时钟开始和停止的时间。当变量“stopGame”为假时,时钟将运行。运行时钟的函数检查每个循环,看时钟是否为零;如果是,变量“停止游戏”被设置为真。它有一个函数返回“stopGame”的值,其他脚本可以使用该值来检查游戏是否应该结束。
If 有一个名为 add 的函数,它允许其他脚本启动时钟。该函数中有对当前时间的检查,以及一个三元运算符,用于检查时钟显示的零点位置。
三元运算符是 if/else 语句的不同形式。if else 语句
if(condition){
do something
} else{
do a different thing
}
可以写成
var statementResult = (condition) ? do something : do a different thing;
将 if 和 else 结果分开
您可以使用 init()函数中的倒计时时钟来设置游戏运行的小时、分钟和秒。
在 public/javascript 文件夹中打开或创建 Countdown.js 文件,并复制清单 10-12 中的代码。
var Countdown = (function(){
var countdown = document.getElementById('countdown');
var seconds;
var minutes;
var hours;
var stopGame;
var init = function(){
hours = 0;
seconds = 25;
minutes = 1;
}
var add = function(stop){
stopGame = stop;
seconds--;
if(seconds === 0 && minutes === 0){
stopGame = true;
}
if(seconds < 0){
seconds = 59;
minutes--;
}
countdown.textContent = (hours ? (hours > 9 ? hours : "0" + hours) : "00") + ":" + (minutes ? (minutes > 9 ? minutes : "0" + minutes) : "00") + ":" + (seconds > 9 ? seconds : "0" + seconds);
if(!stopGame){
setTimeout(add, 1000);
}
}
var getStopGame = function(){
return stopGame;
}
return{
init:init,
add: add,
getStopGame: getStopGame
}
})();
Listing 10-12Countdown.js
代码解释
表 10-9 更详细的解释了 Countdown.js 中的代码。
表 10-9
Countdown.js explained
| var add = function(stop){``...
T2】 | 该函数被传递一个增量,一个布尔值。另一个脚本使用该函数来启动时钟。 |
| stopGame = stop;
| 变量 stopGame 被赋予传递给它的值。 |
| seconds--;
| 秒递减。 |
| if(seconds === 0 && minutes === 0){``stopGame = true;
T2】 | 有一个检查,看看秒和分钟是否在 0,如果是,这是游戏的结束,变量 stopGame 成为真。 |
| if(seconds < 0){``seconds = 59;``minutes--;
T3】 | 然后检查秒数是否为 0;如果是,这意味着分钟需要减少,秒钟变成 59。 |
| countdown.textContent = (hours ? (hours > 9 ? hours : "0" + hours) : "00") + ":" + (minutes ? (minutes > 9 ? minutes : "0" + minutes) : "00") + ":" + (seconds > 9 ? seconds : "0" + seconds);
| 三元运算符用于检查输出字符串中应该有哪些前导数字:例如,如果分钟数小于 9,则分钟变量前应该有一个 0。结果字符串更新浏览器页面上的时钟元素。 |
| if(!stopGame){``setTimeout(add, 1000);
T2】 | 如果游戏仍在进行,倒计时应该继续运行,因此 setTimeout 在 1000 毫秒(1 秒)后再次调用 add 函数。 |
| var getStopGame = function(){``return stopGame;
T2】 | 这个函数返回 stopGame 的当前值。 |
Update Game.js
Game.js 文件必须更新以包含时钟和分数。打开清单 10-8 中的 Game.js 文件,复制清单 10-13 中的粗体代码。
var Game = (function(){
var currentScore = document.getElementById('current-score');
var playAgainElement = document.getElementById('again');
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var colorArray = [0xff0000, 0x00ff00, 0x0000ff];
var ballColor = Math.floor(Math.random() * 3);
var colorChoice = 0;
var collisionTimer = 15;
var stopGame;
var minMaxX = xMinMax(windowWidth);
minMaxX = (parseFloat((minMaxX/10))-3.0.toFixed(1));
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, windowWidth/windowHeight, 0.1, 1000 );
var renderer = new THREE.WebGLRenderer();
renderer.setSize( windowWidth, windowHeight );
document.body.appendChild( renderer.domElement );
var geometry = new THREE.BoxGeometry( 2, 0.2, 0.8 );
var material = new THREE.MeshLambertMaterial( { color: 0x00ff00 } );
var geometrySphere = new THREE.SphereGeometry( 0.3, 32, 32 );
var materialSphere = new THREE.MeshLambertMaterial( {color: colorArray[ballColor]} );
var paddle = new THREE.Mesh( geometry, material );
var ball = new THREE.Mesh( geometrySphere, materialSphere );
updateBallPosition();
paddle.position.y = -2;
camera.position.z = 5;
var light = new THREE.DirectionalLight(0xe0e0e0);
light.position.set(5,2,5).normalize();
scene.add(light);
scene.add(new THREE.AmbientLight(0x656565));
scene.add( paddle );
scene.add( ball );
var newData = function(data){
updateScene(data);
}
var updateScene = function(data){
var screenCoordinates = getCoordinates();
var moveObjectBy;
var x = data[0];
var y = data[1];
var button = data[2];
x = x.substr(1);
y = y.substr(1);
button = button.substr(1);
if(button ==="0"){
updatePaddleColor();
}
if(x > 5){
if(screenCoordinates[0] < windowWidth - 150){
moveObjectBy = moveObjectAmount(x);
paddle.position.x = paddle.position.x + moveObjectBy;
}
} else if (x < 5){
if(screenCoordinates[0] > 0 + 150){
moveObjectBy = moveObjectAmount(x);
paddle.position.x = paddle.position.x + moveObjectBy;
}
}
if(y > 5){
if(screenCoordinates[1] < windowHeight - 100){
paddle.position.y = paddle.position.y - 0.2;
}
} else if (y < 5){
if(screenCoordinates[1] > 0 + 300){
paddle.position.y = paddle.position.y + 0.2;
}
}
renderer.render(scene, camera);
}
var moveObjectAmount = function(x){
var scaledX = scaleInput(x);
scaledX = scaledX/10;
scaledX = Math.round(scaledX * 10) / 10;
return scaledX;
}
var scaleInput=function(input){
var xPositionMin = -4;
var xPositionMax = 4;
var inputMin = 1;
var inputMax = 10;
var percent = (input - inputMin) / (inputMax - inputMin);
var outputX = percent * (xPositionMax - xPositionMin) + xPositionMin;
return outputX;
}
var getCoordinates = function() {
var screenVector = new THREE.Vector3();
paddle.localToWorld( screenVector );
screenVector.project( camera );
var posx = Math.round(( screenVector.x + 1 ) * renderer.domElement.offsetWidth / 2 );
var posy = Math.round(( 1 - screenVector.y ) * renderer.domElement.offsetHeight / 2 );
return [posx, posy];
}
function randomPosition(num){
var newPostion = (Math.random() * (0 - num) + num).toFixed(1);
newPostion *= Math.floor(Math.random()*2) == 1 ? 1 : -1;
return newPostion;
}
function xMinMax(input){
xPositionMin = 4;
xPositionMax = 184;
xWindowMin = 200;
xWindowMax = 2000;
var percent = (input - xWindowMin) / (xWindowMax - xWindowMin);
var outputX = percent * (xPositionMax - xPositionMin) + xPositionMin;
return outputX;
}
function updateBallPosition(){
xPos = randomPosition(minMaxX);
ball.position.y = 5;
ball.position.x = xPos;
ballColor = Math.floor(Math.random() * 3);
ball.material.color.setHex( colorArray[ballColor]);
}
var updatePaddleColor = function(){
colorChoice++;
if(colorChoice === 3){
colorChoice = 0;
}
paddle.material.color.setHex( colorArray[colorChoice]);
}
var animate = function () {
var firstBB = new THREE.Box3().setFromObject(ball);
var secondBB = new THREE.Box3().setFromObject(paddle);
var collision = firstBB.isIntersectionBox(secondBB);
if(!collision){
collisionTimer = 15;
if(ball.position.y > (paddle.position.y - 0.5)){
ball.position.y -= 0.08;
} else {
Points.updatePoints(-1);
updateBallPosition();
}
}
if(collision){
if(collisionTimer > 0){
collisionTimer = collisionTimer -1;
} else {
var tempPaddleColor = paddle.material.color;
var tempBallColor = ball.material.color;
if((tempBallColor.r === tempPaddleColor.r)&& (tempBallColor.g === tempPaddleColor.g) && (tempBallColor.b === tempPaddleColor.b) ){
Points.updatePoints(1);
} else {
. Points.updatePoints(-1);
}
updateBallPosition();
}
}
renderer.render(scene, camera);
// requestAnimationFrame( animate );
stopGame = Countdown.getStopGame();
if(!stopGame){
requestAnimationFrame( animate );
} else {
updateBallPosition();
var gamePoints = Points.getPoints();
currentScore.innerHTML = gamePoints;
playAgainElement.classList.remove("hidden");
}
};
// animate();
return{
newData: newData,
animate: animate
}
})();
Listing 10-13Game.js second update
代码解释
大部分变化都围绕着动画功能。现在,您只希望游戏在玩家按下 start 时运行。animate 函数现在被返回,因此它可以被 main.js 调用,main . js 可以访问开始按钮。现在有一个调用来检查倒计时脚本中 stopGame 的值,只有当它为 false 时才会调用 requestAnimateFrame()函数。如果是真的,游戏就结束了。必须重置球的位置和点数,并且必须看到“重新播放”元素。表 10-10 更详细的解释了 Game.js 第二次更新中的代码。
表 10-10
Game.js second update explained
| stopGame = Countdown.getStopGame();
| 在 requestAnimateFrame 的每个循环中,需要倒计时时钟的当前状态;这是通过调用它的 getStopGame()函数来完成的,调用的结果是一个放置在 StopGame 变量中的布尔值。 |
| if(!stopGame){``requestAnimationFrame( animate );
T2】 | 如果变量 stopGame 为 false,则游戏仍在进行,因此调用 requestAnimateFrame(animate)再次运行动画循环。 |
| else {
updateBallPosition();
var gamePoints = Points.getPoints();
currentScore.innerHTML = gamePoints;
playAgainElement.classList.remove("hidden");
| 如果变量 stopGame 为真,游戏结束,调用 else 语句。这将调用函数 updateBallPosition()来重置球。它还获取当前的游戏点,并在 id 为“current-score”的 HTML 元素上设置 innerHTML。id 为“again”的 HTML 元素的隐藏类已被移除,因此可以在 web 浏览器上看到它。 |
| // animate();
| 在这个位置不再调用 animate 函数;动画在 main.js 中启动。 |
Update Main.js
最后,您需要更新清单 10-6 中的 main.js 文件,打开它,并复制清单 10-14 中的粗体代码。
(function(){
var socket = io();
var stopGame = false;
var startElement = document.getElementById('start');
var playAgainElement = document.getElementById('again');
socket.on('data', function(data){
Game.newData(data);
});
startElement.addEventListener("click", function(){
Countdown.init();
Countdown.add(stopGame);
Points.updatePoints(0);
Game.animate();
startElement.classList.add('hidden');
});
playAgainElement.addEventListener("click", function(){
stopGame = false;
Points.resetPoints();
Points.updatePoints(0);
Countdown.init();
Countdown.add(stopGame);
Game.animate();
playAgainElement.classList.add("hidden");
})
})();
Listing 10-14main.js updated
代码解释
当玩家点击开始按钮时,游戏需要开始。在游戏结束时,会出现“再次游戏”按钮,当玩家点击它时,游戏需要重新开始。这两个操作都发生在 main.js 文件中。
变量保存对 id 为“开始”和“再次”的 HTML 元素的引用。它们有 click 事件侦听器,这些侦听器调用函数来启动游戏并更新前端。表 10-11 更详细地解释了更新后的 main.js 文件中的代码。
表 10-11
main.js update explained
| var stopGame = false;
| 变量保存布尔值 false 这将被传递到倒计时钟启动它。 |
| startElement.addEventListener("click", function(){``...
T2】 | 当单击 HTML 开始元素时,会调用许多函数。 |
| Countdown.init();
| 调用倒计时时钟初始化方法,将时钟设置为初始值。 |
| Points.updatePoints(0);
| 调用 updatePoints 方法将点重置为 0。 |
| Game.animate();
| 在 Game.js 文件中调用 animate 函数来启动球动画。 |
| startElement.classList.add('hidden');
| 持有文本开始的元素在浏览器页面上是隐藏的。 |
| playAgainElement.addEventListener("click", function(){
})
| 当单击 HTML play again 元素时,会调用一些函数来重新开始游戏。 |
现在,如果您重新启动服务器,更改应该会生效。如果你在网页浏览器上进入 http://localhost:3000,你应该可以玩这个游戏。
摘要
本章已经给了你用 Three.js 创建一个应用程序所需要的基础知识,以及你如何用 Arduino 来使用它。能够在网页上使用复杂的动画真的打开了与 Arduino 交互的可能性。
在本书中,引入了新的 JavaScript 和 Arduino 概念,让您对技术和代码有一个基本的了解。可以在这些基础结构的基础上构建更加复杂和有趣的项目。
版权属于:月萌API www.moonapi.com,转载请注明出处