65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2011 年 10 月 26 日

CPOL

6分钟阅读

viewsIcon

22551

本教程的目标是探索使用 SVG 和 Canvas (HTML5 的两大主要技术) 进行图形开发。

摘要

  1. 引言
  2. 必备组件
  3. 设置背景
  4. 设置游戏
  5. 结论

引言

为此,我们将一起编写一个打砖块游戏 (类似于《打砖块》或《Blockout》)。它将包含一个动画背景 (使用 Canvas) 并使用 SVG 来绘制砖块、挡板和球。

您可以在此处尝试最终版本: http://www.catuhe.com/ms/en/index.htm

必备组件

  • Internet Explorer 9/10 或其他 硬件加速的 HTML5 现代浏览器
  • Visual Studio 2010 SP1
  • Web 标准更新: http://visualstudiogallery.msdn.microsoft.com/a15c3ce9-f58f-42b7-8668-53f6cdc2cd83
  • 设置背景

    背景只是为了使用 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.}

    结论

    您现在是一名 **游戏开发者**!利用加速图形的强大功能,我们开发了一个小游戏,但具有非常有趣的特殊效果!

    现在轮到您更新游戏,使其成为下一个 **大片**!

    深入了解

    © . All rights reserved.