一个完整的示例
我们已经简单地把 HTML5 的<canvas>
元素介绍过了。<canvas>
可以说是 HTML5 带给我们的最激动人心的特性之一。这一章我们将利用<canvas>
,完成一个简单但是完整的游戏,使大家了解在实际应用中,我们该如何使用<canvas>
。
我们准备实现一个跳棋游戏。这是一个有几百年历史的古老游戏,现在有很多变种。在我们的示例中,我们将创建一个9x9的棋盘。游戏开始,9颗棋子位于棋盘左下角3x3的区域内;游戏目标是使用尽可能少的步数,将所有棋子移动到棋盘右上角3x3的区域。
跳棋的棋子有两种走法:
- 选定一个棋子,将其移动到相邻的空位置。所谓“空位置”,就是没有棋子的位置。“相邻”,意味着是当前位置的上、下、左、右、左上、右上、左下、右下等八个位置。注意,我们的棋盘的边缘是不算“循环”的,也就是说,如果你的棋子在最右侧,那么,它所能移动的位置只有上、下、左、左上、左下这么五个;
- 选定一个棋子,可以跳过相邻的一个棋子,这个跳的过程可以是连续的。也就是说,如果你跳过一个相邻的棋子,到达一个新的位置,这个新的位置相邻又有棋子,那么你就可以继续跳过这个棋子,从而又到达一个新的位置。这一过程仅算一步。事实上,任意多次连跳都只算是一步。(跳棋的目的就是最少步数,那么我们获胜的策略就是尽早搭建好连跳的线路,然后让后面的棋子能够一直连跳到达终点。)
这里是我们最终实现的游戏。你可以使用浏览器的开发者工具来分析一下游戏的实现。这里我们就不将这个页面嵌入到这里了。
我们是怎么实现这个的?很高兴你会这么问。不过,在这里还不是全部。你可以打开上面给出的链接,查看那个页面的源代码。这里我们将简单掠过有关游戏算法的部分,重点在如何使用 canvas 绘制以及 canvas 如何响应鼠标点击事件等。
在页面加载之后,我们使用代码给<canvas>
赋长宽值,并且保存下其绘图上下文的引用:
gCanvasElement.width = kPixelWidth; gCanvasElement.height = kPixelHeight; gDrawingContext = gCanvasElement.getContext("2d");
用户点击画布的时候,会调用halmaOnClick()
函数。这个函数接受一个MouseEvent
类型的参数,包含了用户点击的各种信息:
function halmaOnClick(e) { var cell = getCursorPosition(e); // the rest of this is just gameplay logic for (var i = 0; i < gNumPieces; i++) { if ((gPieces[i].row == cell.row) && (gPieces[i].column == cell.column)) { clickOnPiece(i); return; } } clickOnEmptyCell(cell); }
注意,这里我们调用了getCursorPosition(e)
函数,从MouseEvent
对象计算用户点击的是棋盘上的哪一个方格。由于我们的方格覆盖了整个画布,因此,无论你点击画布的哪个位置,总会落在其中一个方格之中。不过,我们的代码并不是正式应用的版本,因为鼠标坐标信息MouseEvent
对象是与浏览器相关的,而这里我们并没有考虑到这一点。(所以,如果你发现你的浏览器不能运行我们的游戏,请尝试换一个浏览器)
function function getCursorPosition(e) { var x; var y; if (e.pageX != undefined && e.pageY != undefined) { x = e.pageX; y = e.pageY; } else { x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; }
此时,我们有了相对于 HTML 页面的x与y的值。不过这并不是十分有用。下一步,我们将其转换成相对画布的坐标值:
x -= gCanvasElement.offsetLeft; y -= gCanvasElement.offsetTop;
现在,我们有了相对于画布的坐标值。也就是说,如果x是0,y也是0,那么我们就知道此时用户点击的是画布的左上角。
好了,下面我们就可以计算出用户究竟点击的是哪一个方格了:
var cell = new Cell(Math.floor(y/kPieceHeight), Math.floor(x/kPieceWidth)); return cell; }
看吧!MouseEvent 虽然很难用,但是我们已经由此求出我们所需要的东西了!事实上,你可以学习上面的逻辑。这个逻辑是通用的,你可以用在任何使用 canvas 的应用程序之中。记住:鼠标点击 -> 相对于 document 的坐标 -> 相对于 canvas 的坐标 -> 特定应用程序的代码。
下面,我们来看看主要的绘图逻辑。因为我们游戏的绘图很简单,所以我们采取的是每次点击都要重绘整个画布的做法。这种做法很简单,但是效率不是那么高。严格来说,这是不必要的。画布的绘图上下文会保留住你上一次绘制的东西,甚至是用户将画布滚动出界面,或者切换了窗口之后,你画的东西还是存在的。如果你的应用程序有非常复杂的图形逻辑(例如一个街机游戏),你必须考虑优化性能。做法是,标记处哪一块区域是“脏”的,也就是需要更新的,然后仅重绘这一块区域。这种技术很普遍,已经超出我们现在讨论的范围了,这里不再详细阐述。
gDrawingContext.clearRect(0, 0, kPixelWidth, kPixelHeight);
棋盘的绘制同我们上一部分所说的坐标格的绘制非常类似:
gDrawingContext.beginPath(); /* vertical lines */ for (var x = 0; x <= kPixelWidth; x += kPieceWidth) { gDrawingContext.moveTo(0.5 + x, 0); gDrawingContext.lineTo(0.5 + x, kPixelHeight); } /* horizontal lines */ for (var y = 0; y <= kPixelHeight; y += kPieceHeight) { gDrawingContext.moveTo(0, 0.5 + y); gDrawingContext.lineTo(kPixelWidth, 0.5 + y); } /* draw it! */ gDrawingContext.strokeStyle = "#ccc"; gDrawingContext.stroke();
真正有趣的地方在我们绘制棋子的时候。棋子是一个圆,圆的绘制我们以前从来没有接触过。并且,当用户点击棋子的时候,我们还得画一个实心圆。这里,我们的函数有两个参数:p代表棋子,它有两个成员变量row和column,分别表示棋子在棋盘的坐标。我们使用一些辅助函数,将这个 (row, column) 坐标转换成相对于画布的坐标 (x, y),然后在这个位置画一个圆。如果这个棋子是被选择的,则要用颜色填充这个圆。
function drawPiece(p, selected) { var column = p.column; var row = p.row; var x = (column * kPieceWidth) + (kPieceWidth/2); var y = (row * kPieceHeight) + (kPieceHeight/2); var radius = (kPieceWidth/2) - (kPieceWidth/10);
这就是与游戏相关的逻辑。现在,我们得到了相对于画布的x和y的坐标。这个坐标是我们要画的圆的圆心。canvas API 没有类似 circle() 的函数,不过它有一个arc()
函数。所谓圆,不就是一个360度的弧吗?还记得基础几何吗?arc()
函数接受一个中心点 (x, y),一个半径,以弧度为单位的起始角度和终止角度和一个方向标记(顺时针为false
,逆时针为true
)。你可以使用 JavaScript 内置的Math
模块来计算弧度:
gDrawingContext.beginPath(); gDrawingContext.arc(x, y, radius, 0, Math.PI * 2, false); gDrawingContext.closePath();
等一下!到目前为止我们什么都没画呢!类似moveTo()
和lineTo()
,arc()
也是一个“铅笔”方法。为了把这个圆画出来,我们还是要使用strokeStyle
和stroke()
来给出一个“钢笔”:
gDrawingContext.strokeStyle = "#000"; gDrawingContext.stroke();
那么,如果棋子被选择呢?这里,我们还是使用前面的代码,复用其形状和边框,然后使用fill()
函数来填充内部:
if (selected) { gDrawingContext.fillStyle = "#000"; gDrawingContext.fill(); }
好了!大功告成!剩下的代码都是有关游戏逻辑的,比如如何判断合法移动和非法移动,计算移动次数以及判断游戏是否已经结束等等。9个圆,一些直线和一个点击处理函数,我们就使用<canvas>
完成了一个小游戏!