Dive Into HTML5:绘图(续五)

一个完整的示例

我们已经简单地把 HTML5 的<canvas>元素介绍过了。<canvas>可以说是 HTML5 带给我们的最激动人心的特性之一。这一章我们将利用<canvas>,完成一个简单但是完整的游戏,使大家了解在实际应用中,我们该如何使用<canvas>

我们准备实现一个跳棋游戏。这是一个有几百年历史的古老游戏,现在有很多变种。在我们的示例中,我们将创建一个9×9的棋盘。游戏开始,9颗棋子位于棋盘左下角3×3的区域内;游戏目标是使用尽可能少的步数,将所有棋子移动到棋盘右上角3×3的区域。

跳棋的棋子有两种走法:

  • 选定一个棋子,将其移动到相邻的空位置。所谓“空位置”,就是没有棋子的位置。“相邻”,意味着是当前位置的上、下、左、右、左上、右上、左下、右下等八个位置。注意,我们的棋盘的边缘是不算“循环”的,也就是说,如果你的棋子在最右侧,那么,它所能移动的位置只有上、下、左、左上、左下这么五个;
  • 选定一个棋子,可以跳过相邻的一个棋子,这个跳的过程可以是连续的。也就是说,如果你跳过一个相邻的棋子,到达一个新的位置,这个新的位置相邻又有棋子,那么你就可以继续跳过这个棋子,从而又到达一个新的位置。这一过程仅算一步。事实上,任意多次连跳都只算是一步。(跳棋的目的就是最少步数,那么我们获胜的策略就是尽早搭建好连跳的线路,然后让后面的棋子能够一直连跳到达终点。)

这里是我们最终实现的游戏。你可以使用浏览器的开发者工具来分析一下游戏的实现。这里我们就不将这个页面嵌入到这里了。

我们是怎么实现这个的?很高兴你会这么问。不过,在这里还不是全部。你可以打开上面给出的链接,查看那个页面的源代码。这里我们将简单掠过有关游戏算法的部分,重点在如何使用 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()也是一个“铅笔”方法。为了把这个圆画出来,我们还是要使用strokeStylestroke()来给出一个“钢笔”:

    gDrawingContext.strokeStyle = "#000";
    gDrawingContext.stroke();

那么,如果棋子被选择呢?这里,我们还是使用前面的代码,复用其形状和边框,然后使用fill()函数来填充内部:

    if (selected) {
        gDrawingContext.fillStyle = "#000";
        gDrawingContext.fill();
    }

好了!大功告成!剩下的代码都是有关游戏逻辑的,比如如何判断合法移动和非法移动,计算移动次数以及判断游戏是否已经结束等等。9个圆,一些直线和一个点击处理函数,我们就使用<canvas>完成了一个小游戏!

Leave a Reply