如何使用 HTML5 SVG 和 Canvas 编写 BrikBloc 游戏





4.00/5 (1投票)
本教程的目标是探索使用 SVG 和 Canvas (HTML5 的两大主要技术) 进行图形开发。
摘要
引言
为此,我们将一起编写一个打砖块游戏 (类似于《打砖块》或《Blockout》)。它将包含一个动画背景 (使用 Canvas) 并使用 SVG 来绘制砖块、挡板和球。
您可以在此处尝试最终版本: http://www.catuhe.com/ms/en/index.htm
必备组件
设置背景
背景只是为了使用 Canvas 的一个借口。它将允许我们在给定区域内绘制像素。因此,我们将用它来绘制一个太空虫洞 (是的,我喜欢 《星际之门》!)。用户可以通过模式按钮选择是否显示它。
您可以注意到,我们将在右上角添加一个性能计数器 (只是为了查看 加速图形的强大功能)。
设置 HTML5 页面
从 index.htm 文件开始,我们将把 Canvas 添加到 div “gameZone” 的子元素中。
1. <canvas id="backgroundCanvas">
2. Your browser doesn't support HTML5. Please install Internet Explorer 9 :
3. <br />
4. <a href="http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing">
5. http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing</a>
6. </canvas>
添加 JavaScript 代码
背景由 background.js 文件处理 (真是出乎意料!)。所以我们必须在 index.htm 中注册它。因此,在 body 标签关闭之前,我们将添加以下代码。
1. <script type="text/javascript" src="background.js"></script>
设置常量
首先,我们需要常量来控制渲染。
1. var circlesCount = 100; // Circles count used by the wormhole
2. var offsetX = 70; // Wormhole center offset (X)
3. var offsetY = 40; // Wormhole center offset (Y)
4. var maxDepth = 1.5; // Maximal distance for a circle
5. var circleDiameter = 10.0; // Circle diameter
6. var depthSpeed = 0.001; // Circle speed
7. var angleSpeed = 0.05; // Circle angular rotation speed
当然,您可以修改这些常量以获得不同的虫洞效果。
获取元素
我们还需要保留对 HTML 页面主要元素的引用。
1. var canvas = document.getElementById("backgroundCanvas");
2. var context = canvas.getContext("2d");
3. var stats = document.getElementById("stats");
如何显示一个圆?
虫洞只是具有不同位置和大小的一系列圆。因此,为了绘制它,我们将使用一个围绕深度、角度和强度 (基本颜色) 构建的圆函数。
1. function Circle(initialDepth, initialAngle, intensity) {
2. }
角度和强度是私有的,但深度是公共的,允许虫洞更改它。
1. function Circle(initialDepth, initialAngle, intensity) {
2.
3. var angle = initialAngle;
4. this.depth = initialDepth;
5. var color = intensity;
6. }
我们还需要一个公共的 draw 函数来绘制圆并更新深度、角度。所以我们需要定义圆的显示位置。为此,定义了两个变量 (x 和 y)。
1. var x = offsetX * Math.cos(angle);
2. var y = offsetY * Math.sin(angle);
由于 x 和 y 是空间坐标,我们需要将它们投影到屏幕上。
1. function perspective(fov, aspectRatio, x, y) {
2. var yScale = Math.pow(Math.tan(fov / 2.0), -1);
3. var xScale = yScale / aspectRatio;
4.
5. var M11 = xScale;
6. var M22 = yScale;
7.
8. var outx = x * M11 + canvas.width / 2.0;
9. var outy = y * M22 + canvas.height / 2.0;
10.
11.return { x: outx, y: outy };
12.}
因此,圆的最终位置由以下代码计算。
1. var x = offsetX * Math.cos(angle);
2. var y = offsetY * Math.sin(angle);
3.
4. var project = perspective(0.9, canvas.width / canvas.height, x, y);
5. var diameter = circleDiameter / this.depth;
6.
7. var ploX = project.x - diameter / 2.0;
8. var ploY = project.y - diameter / 2.0;
使用这个位置,我们可以简单地绘制我们的圆。
1. context.beginPath();
2. context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false);
3. context.closePath();
4.
5. var opacity = 1.0 - this.depth / maxDepth;
6. context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")";
7. context.lineWidth = 4;
8.
9. context.stroke();
您可以注意到,圆在离我们越近时越不透明。
所以最后
1. function Circle(initialDepth, initialAngle, intensity) {
2. var angle = initialAngle;
3. this.depth = initialDepth;
4. var color = intensity;
5.
6. this.draw = function () {
7. var x = offsetX * Math.cos(angle);
8. var y = offsetY * Math.sin(angle);
9.
10.var project = perspective(0.9, canvas.width / canvas.height, x, y);
11.var diameter = circleDiameter / this.depth;
12.
13.var ploX = project.x - diameter / 2.0;
14.var ploY = project.y - diameter / 2.0;
15.
16.context.beginPath();
17.context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false);
18.context.closePath();
19.
20.var opacity = 1.0 - this.depth / maxDepth;
21.context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")";
22.context.lineWidth = 4;
23.
24.context.stroke();
25.
26.this.depth -= depthSpeed;
27.angle += angleSpeed;
28.
29.if (this.depth < 0) {
30.this.depth = maxDepth + this.depth;
31.}
32.};
33.};
初始化
使用我们的圆函数,我们可以有一个圆的数组,我们将它们初始化得越来越靠近我们,每次都稍微偏移一下角度。
1. // Initialization
2. var circles = [];
3.
4. var angle = Math.random() * Math.PI * 2.0;
5.
6. var depth = maxDepth;
7. var depthStep = maxDepth / circlesCount;
8. var angleStep = (Math.PI * 2.0) / circlesCount;
9. for (var index = 0; index < circlesCount; index++) {
10.circles[index] = new Circle(depth, angle, index % 5 == 0 ? 200 : 255);
11.
12.depth -= depthStep;
13.angle -= angleStep;
14.}
计算 FPS
我们可以通过测量对给定函数进行两次调用之间的时间量来计算 FPS。在我们的例子中,该函数将是 computeFPS
。它将保存最后 60 次测量并计算平均值以产生所需的结果。
1. // FPS
2. var previous = [];
3. function computeFPS() {
4. if (previous.length > 60) {
5. previous.splice(0, 1);
6. }
7. var start = (new Date).getTime();
8. previous.push(start);
9. var sum = 0;
10.
11.for (var id = 0; id < previous.length - 1; id++) {
12.sum += previous[id + 1] - previous[id];
13.}
14.
15.var diff = 1000.0 / (sum / previous.length);
16.
17.stats.innerHTML = diff.toFixed() + " fps";
18.}
绘制和动画
Canvas 是一个 **直接模式** 工具。这意味着每次需要更改某些内容时,我们都必须重新生成 Canvas 的所有内容。
首先,我们需要在每一帧之前清除内容。最好的解决方案是使用 clearRect
。
1. // Drawing & Animation
2. function clearCanvas() {
3. context.clearRect(0, 0, canvas.width, canvas.height);
4. }
因此,完整的虫洞绘制代码将如下所示。
1. function wormHole() {
2. computeFPS();
3. canvas.width = window.innerWidth;
4. canvas.height = window.innerHeight - 130 - 40;
5. clearCanvas();
6. for (var index = 0; index < circlesCount; index++) {
7. circles[index].draw();
8. }
9.
10.circles.sort(function (a, b) {
11.if (a.depth > b.depth)
12.return -1;
13.if (a.depth < b.depth)
14.return 1;
15.return 0;
16.});
17.}
排序代码用于防止圆重叠。
设置模式按钮
为了最终确定我们的背景,我们只需要将模式按钮连接起来以显示或隐藏背景。
1.
2. var wormHoleIntervalID = -1;
3.
4. function startWormHole() {
5. if (wormHoleIntervalID > -1)
6. clearInterval(wormHoleIntervalID);
7.
8. wormHoleIntervalID = setInterval(wormHole, 16);
9.
10.document.getElementById("wormHole").onclick = stopWormHole;
11.document.getElementById("wormHole").innerHTML = "Standard Mode";
12.}
13.
14.function stopWormHole() {
15.if (wormHoleIntervalID > -1)
16.clearInterval(wormHoleIntervalID);
17.
18.clearCanvas();
19.document.getElementById("wormHole").onclick = startWormHole;
20.document.getElementById("wormHole").innerHTML = "Wormhole Mode";
21.}
22.
23.stopWormHole();
设置游戏
为了简化教程,鼠标处理代码已经完成。您可以在 mouse.js 文件中找到所有需要的内容。
添加游戏 JavaScript 文件
背景由 game.js 文件处理。所以我们必须在 index.htm 中注册它。因此,在 body 标签关闭之前,我们将添加以下代码。
1. <script type="text/javascript" src="game.js"></script>
更新 HTML5 页面
游戏将使用 **SVG** (可缩放矢量图形) 来显示砖块、挡板和球。SVG 是一个保留模式工具。因此,您不必每次要移动或更改某个项目时都重新绘制所有内容。
要将 SVG 标签添加到我们的页面,只需插入以下代码 (紧随 Canvas 之后)。
1. <svg id="svgRoot"> 2. <circle cx="100" cy="100" r="10" id="ball" /> 3. <rect id="pad" height="15px" width="150px" x="200" y="200" rx="10" ry="20"/> 4. </svg>
正如您所注意到的,SVG 以两个已定义的对象开始:一个用于球的圆和用于挡板的矩形。
定义常量和变量
在 game.js 文件中,我们将开始添加一些变量。
1. // Getting elements
2. var pad = document.getElementById("pad");
3. var ball = document.getElementById("ball");
4. var svg = document.getElementById("svgRoot");
5. var message = document.getElementById("message");
球将由以下方式定义:
- 位置
- 半径
- 速度
- 方向
- 它的前一个位置
1. // Ball
2. var ballRadius = ball.r.baseVal.value;
3. var ballX;
4. var ballY;
5. var previousBallPosition = { x: 0, y: 0 };
6. var ballDirectionX;
7. var ballDirectionY;
8. var ballSpeed = 10;
挡板将由以下方式定义:
- 宽度
- 高度
- 职位
- 速度
- 惯性值 (使事物更平滑)
1. // Pad
2. var padWidth = pad.width.baseVal.value;
3. var padHeight = pad.height.baseVal.value;
4. var padX;
5. var padY;
6. var padSpeed = 0;
7. var inertia = 0.80;
砖块将保存在一个数组中,并由以下方式定义:
- 宽度
- 高度
- 它们之间的边距
- 行数
- 列数
我们还需要一个偏移量和一个用于计算被摧毁砖块的变量。
1. // Bricks
2. var bricks = [];
3. var destroyedBricksCount;
4. var brickWidth = 50;
5. var brickHeight = 20;
6. var bricksRows = 5;
7. var bricksCols = 20;
8. var bricksMargin = 15;
9. var bricksTop = 20;
最后,我们还需要游戏区域的边界和一个开始日期来计算会话持续时间。
1. // Misc.
2. var minX = ballRadius;
3. var minY = ballRadius;
4. var maxX;
5. var maxY;
6. var startDate;
处理砖块
要创建砖块,我们需要一个函数来向 SVG 根目录添加一个新元素。它还将为每个砖块配置所需的信息。
1. var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2. svg.appendChild(rect);
3.
4. rect.setAttribute("width", brickWidth);
5. rect.setAttribute("height", brickHeight);
6.
7. // Random green color
8. var chars = "456789abcdef";
9. var color = "";
10.for (var i = 0; i < 2; i++) {
11.var rnd = Math.floor(chars.length * Math.random());
12.color += chars.charAt(rnd);
13.}
14.rect.setAttribute("fill", "#00" + color + "00");
砖块函数还将提供一个 drawAndCollide
函数来显示砖块并检查是否与球发生碰撞。
1. this.drawAndCollide= function () {
2. if(isDead)
3. return;
4. //Drawing
5. rect.setAttribute("x",position.x);
6. rect.setAttribute("y",position.y);
7.
8. //Collision
9. if (ballX+ ballRadius < position.x || ballX - ballRadius > position.x +brickWidth)
10.return;
11.
12.if (ballY + ballRadius <position.y || ballY - ballRadius > position.y + brickHeight)
13.return;
14.
15.// Dead
16.this.remove();
17.isDead = true;
18.destroyedBricksCount++;
19.
20.// Updating ball
21.ballX = previousBallPosition.x;
22.ballY = previousBallPosition.y;
23.
24.ballDirectionY *= -1.0;
25.};
最后,完整的砖块函数将如下所示。
1. //Brick function
2. functionBrick(x, y) {
3. var isDead= false;
4. varposition = { x: x, y: y };
5.
6. var rect =document.createElementNS("http://www.w3.org/2000/svg", "rect");
7. svg.appendChild(rect);
8.
9. rect.setAttribute("width",brickWidth);
10.rect.setAttribute("height",brickHeight);
11.
12.// Random green color
13.var chars = "456789abcdef";
14.var color = "";
15.for (var i = 0;i < 2; i++) {
16.var rnd = Math.floor(chars.length *Math.random());
17.color += chars.charAt(rnd);
18.}
19.rect.setAttribute("fill", "#00" + color+ "00");
20.
21.this.drawAndCollide= function () {
22.if (isDead)
23.return;
24.// Drawing
25.rect.setAttribute("x",position.x);
26.rect.setAttribute("y",position.y);
27.
28.// Collision
29.if (ballX + ballRadius <position.x || ballX - ballRadius > position.x + brickWidth)
30.return;
31.
32.if (ballY + ballRadius <position.y || ballY - ballRadius > position.y + brickHeight)
33.return;
34.
35.// Dead
36.this.remove();
37.isDead = true;
38.destroyedBricksCount++;
39.
40.// Updating ball
41.ballX = previousBallPosition.x;
42.ballY = previousBallPosition.y;
43.
44.ballDirectionY *= -1.0;
45.};
46.
47.// Killing a brick
48.this.remove = function () {
49.if (isDead)
50.return;
51.svg.removeChild(rect);
52.};
53.}
与挡板和游戏区域的碰撞
球还将具有处理与挡板和游戏区域碰撞的碰撞函数。当检测到碰撞时,这些函数将不得不更新球的方向。
1. //Collisions
2. functioncollideWithWindow() {
3. if (ballX< minX) {
4. ballX =minX;
5. ballDirectionX*= -1.0;
6. }
7. else if (ballX> maxX) {
8. ballX =maxX;
9. ballDirectionX*= -1.0;
10.}
11.
12.if (ballY < minY) {
13.ballY = minY;
14.ballDirectionY *= -1.0;
15.}
16.else if (ballY> maxY) {
17.ballY = maxY;
18.ballDirectionY *= -1.0;
19.lost();
20.}
21.}
22.
23.functioncollideWithPad() {
24.if (ballX + ballRadius < padX ||ballX - ballRadius > padX + padWidth)
25.return;
26.
27.if (ballY + ballRadius < padY)
28.return;
29.
30.ballX = previousBallPosition.x;
31.ballY = previousBallPosition.y;
32.ballDirectionY *= -1.0;
33.
34.var dist = ballX - (padX + padWidth/ 2);
35.
36.ballDirectionX = 2.0 * dist / padWidth;
37.
38.var square =Math.sqrt(ballDirectionX * ballDirectionX + ballDirectionY * ballDirectionY);
39.ballDirectionX /= square;
40.ballDirectionY /= square;
41.}
collideWithWindow
检查游戏区域的边界,collideWithPad
检查挡板的边界 (我们在这里做了一个细微的改变:球的水平速度将使用与挡板中心的距离来计算)。
移动挡板
您可以通过鼠标或左右箭头控制挡板。movePad
函数负责处理挡板移动。它还将处理 **惯性**。
1. //Pad movement
2. functionmovePad() {
3. padX +=padSpeed;
4.
5. padSpeed*= inertia;
6.
7. if (padX< minX)
8. padX =minX;
9.
10.if (padX + padWidth > maxX)
11.padX = maxX - padWidth;
12.}
负责处理输入的代码相当 **简单**。
1. registerMouseMove(document.getElementById("gameZone"), function (posx,posy, previousX, previousY) {
2. padSpeed+= (posx - previousX) * 0.2;
3. });
4.
5. window.addEventListener('keydown', function (evt) {
6. switch(evt.keyCode) {
7. //Left arrow
8. case 37:
9. padSpeed-= 10;
10.break;
11.// Right arrow
12.case 39:
13.padSpeed += 10;
14.break;
15.}
16.}, true);
游戏循环
在设置游戏循环之前,我们需要一个函数来定义游戏区域的大小。当窗口大小调整时,将调用此函数。
1. functioncheckWindow() {
2. maxX =window.innerWidth - minX;
3. maxY =window.innerHeight - 130 - 40 - minY;
4. padY =maxY - 30;
5. }
顺便说一下,游戏循环是这里的 **协调者**。
1. functiongameLoop() {
2. movePad();
3.
4. //Movements
5. previousBallPosition.x= ballX;
6. previousBallPosition.y= ballY;
7. ballX +=ballDirectionX * ballSpeed;
8. ballY +=ballDirectionY * ballSpeed;
9.
10.// Collisions
11.collideWithWindow();
12.collideWithPad();
13.
14.// Bricks
15.for (var index =0; index < bricks.length; index++) {
16.bricks[index].drawAndCollide();
17.}
18.
19.// Ball
20.ball.setAttribute("cx",ballX);
21.ball.setAttribute("cy",ballY);
22.
23.// Pad
24.pad.setAttribute("x", padX);
25.pad.setAttribute("y", padY);
26.
27.// Victory ?
28.if (destroyedBricksCount ==bricks.length) {
29.win();
30.}
31.}
初始化和胜利
初始化的第一步是创建砖块。
1. functiongenerateBricks() {
2. //Removing previous ones
3. for (var index =0; index < bricks.length; index++) {
4. bricks[index].remove();
5. }
6.
7. //Creating new ones
8. var brickID= 0;
9.
10.var offset = (window.innerWidth -bricksCols * (brickWidth + bricksMargin)) / 2.0;
11.
12.for (var x = 0;x < bricksCols; x++) {
13.for (var y = 0;y < bricksRows; y++) {
14.bricks[brickID++] = newBrick(offset + x * (brickWidth + bricksMargin), y * (brickHeight + bricksMargin)+ bricksTop);
15.}
16.}
17.}
下一步是设置游戏使用的变量。
1. functioninitGame() {
2. message.style.visibility= "hidden";
3.
4. checkWindow();
5.
6. padX =(window.innerWidth - padWidth) / 2.0;
7.
8. ballX =window.innerWidth / 2.0;
9. ballY = maxY- 60;
10.
11.previousBallPosition.x = ballX;
12.previousBallPosition.y = ballY;
13.
14.padSpeed = 0;
15.
16.ballDirectionX = Math.random();
17.ballDirectionY = -1.0;
18.
19.generateBricks();
20.gameLoop();
21.}
每次用户更改窗口大小时,我们都必须重置游戏。
1. window.onresize= initGame;
然后,我们必须为新的游戏按钮附加一个事件处理程序。
1. vargameIntervalID = -1;
2. functionstartGame() {
3. initGame();
4.
5. destroyedBricksCount= 0;
6.
7. if(gameIntervalID > -1)
8. clearInterval(gameIntervalID);
9.
10.startDate = (newDate()).getTime(); ;
11.gameIntervalID = setInterval(gameLoop,16);
12.}
13.
14.document.getElementById("newGame").onclick= startGame;
最后,我们将添加两个函数来处理游戏的开始和结束。
1. vargameIntervalID = -1;
2. function lost(){
3. clearInterval(gameIntervalID);
4. gameIntervalID= -1;
5.
6. message.innerHTML= "Game over !";
7. message.style.visibility= "visible";
8. }
9.
10.function win() {
11.clearInterval(gameIntervalID);
12.gameIntervalID = -1;
13.
14.var end = (newDate).getTime();
15.
16.message.innerHTML = "Victory! (" + Math.round((end - startDate) / 1000) + "s)";
17.message.style.visibility = "visible";
18.}
结论
您现在是一名 **游戏开发者**!利用加速图形的强大功能,我们开发了一个小游戏,但具有非常有趣的特殊效果!
现在轮到您更新游戏,使其成为下一个 **大片**!