五、知道玩家在做什么

在这一章中,你开始创建一个名为 Painter 的游戏。在这个游戏中,你需要显示在屏幕上移动的精灵。您已经看到了一些加载和显示精灵的例子。此外,您已经看到了使用当前时间信息来改变精灵的位置是可能的。您可以在这些示例的基础上开始创建 Painter。此外,您还将学习如何处理游戏中玩家的输入。你会看到如何检索玩家正在做什么,以及游戏世界如何根据这些信息而变化。从 FlyingSprite 程序的一个简单扩展开始,它在鼠标指针的位置绘制一个气球。下一章将探讨其他类型的输入,如键盘和触摸输入。

跟随鼠标指针的精灵

现在你知道了如何在屏幕上显示精灵,让我们看看你是否可以使用玩家输入来控制精灵的位置。为此,您必须找出鼠标的当前位置。本节向您展示了如何检索这个位置,以及如何使用它来绘制一个跟随鼠标指针的 sprite。

检索鼠标位置

看看本书示例中的程序 Balloon1。它和 FlyingSprite 程序没有太大区别。在 FlyingSprite 中,您通过使用系统时间来计算气球的位置:

var d = new Date();
Game.balloonPosition.x = d.getTime() * 0.3 % Game.canvas.width;

您计算的位置存储在变量balloonPosition中。现在您想要创建一个程序,其中气球位置与当前鼠标位置相同,而不是基于经过的时间进行计算。使用事件很容易获得当前鼠标位置。

在 JavaScript 中,您可以处理许多不同种类的事件。事件示例如下:

  • 玩家移动鼠标。
  • 玩家左键点击。
  • 玩家点击一个按钮。
  • 已经加载了一个 HTML 页面。
  • 从网络连接接收消息。
  • 精灵已完成加载。

当这样的事件发生时,你可以选择执行指令。例如,当玩家移动鼠标时,您可以执行一些指令来检索新的鼠标位置,并将其存储在一个变量中,这样您就可以使用它在该位置绘制一个精灵。一些 JavaScript 对象可以帮助您做到这一点。例如,当您显示一个 HTML 页面时,document变量让您可以访问页面中的所有元素。但是,更重要的是,这个变量还允许您访问用户通过使用鼠标、键盘或触摸屏与文档交互的方式。

您已经以多种方式使用了这个变量。例如,这里使用document从 HTML 页面中检索 canvas 元素:

Game.canvas = document.getElementById("myCanvas");

除了getElementByIddocument对象还有很多其他的方法和成员变量。例如,有一个名为onmousemove的成员变量,您可以给它赋值。这个成员变量不是指数值或字符串,而是指函数/方法。每当鼠标移动时,浏览器都会调用该函数。然后,您可以在函数中编写指令,以您希望的任何方式处理该事件。因此,这类函数被称为事件处理程序。使用事件处理函数是一种非常有效的处理输入的方式。

另一种方法是将指令放入游戏循环中,在每次迭代中检索当前鼠标位置或当前按下的键。虽然这样做可行,但比使用事件处理程序要慢得多,因为你必须在每次迭代中检查输入,而不是只在玩家实际做某件事的时候才检查。

事件处理函数有一个特定的头。它包含一个参数,当调用该函数时,该参数包含一个提供事件信息的对象。例如,下面是一个空的事件处理函数:

function handleMouseMove(evt) {
    // do something here
}

如您所见,该函数只有一个参数evt,它将包含关于需要处理的事件的信息。现在您可以将该函数分配给onmousemove变量:

document.onmousemove = handleMouseMove;

现在,每次移动鼠标,都会调用handleMouseMove函数。您可以在此函数中输入指令,从evt对象中提取鼠标位置。例如,这个事件处理函数获取鼠标的 x 位置和 y 位置,并将它们存储在变量balloonPosition : 中

function handleMouseMove(evt) {
    Game.balloonPosition = { x : evt.pageX, y : evt.pageY };
}

evt对象的pageXpageY成员变量包含鼠标相对于页面的位置,意味着页面的左上角有坐标(0,0)。你可以在图 5-1 中看到一些鼠标位置的例子:在浏览器中运行程序时,其中三个角标有它们各自的位置。

9781430265382_Fig05-01.jpg

图 5-1 。左上角、右上角和右下角的鼠标位置

因为Draw方法只是在鼠标位置绘制气球,所以气球现在跟随鼠标。图 5-2 显示了它的样子。您可以看到鼠标指针下方绘制的气球;当你移动指针时,它会跟踪它。

9781430265382_Fig05-02.jpg

图 5-2气球 1 项目截图

你可以在图 5-2 中看到,气球并没有出现在指针的正下方。这是有原因的,下一节将详细讨论这个问题。现在,只要记住精灵被视为一个矩形。左上角与指针尖端对齐。气球看起来没有对齐,因为气球是圆形的,没有延伸到矩形的角上。

除了pageXpageY,你还可以使用clientXclientY,它们也给出了鼠标的位置。然而,clientXclientY并没有把滚动考虑进去。假设你计算鼠标位置如下:

Game.balloonPosition = { x : evt.clientX, y : evt.clientY };

图 5-3 显示了现在可能出现的问题。由于滚动,clientY值小于 480,即使鼠标位于背景图像的底部。因此,不再在鼠标位置绘制气球。所以我建议一直用pageXpageY,不要用clientXclientY。然而,在某些情况下,不考虑滚动可能是有用的——例如,如果您正在开发一个令人讨厌的广告,即使用户试图滚动它,它也会一直出现在浏览器视图的中间。

9781430265382_Fig05-03.jpg

图 5-3 。鼠标指针在背景精灵的底部,但是鼠标的 y 位置是 340(而不是 480 ),因为clientY没有考虑滚动

更改精灵的来源

当您运行 Balloon1 示例时,请注意绘制的气球使得 sprite 的左上角位于当前鼠标位置。当您在某个位置绘制精灵时,默认行为是在该位置绘制精灵的左上角。如果执行下面的指令

Game.drawImage(someSprite, somePosition);

名为someSprite的子画面被绘制在屏幕上,使得其左上角位于位置somePosition。你也可以把精灵的左上角叫做它的原点。那么,如果要改变这个原点呢?例如,假设您想要在位置somePosition绘制精灵someSprite的中心。嗯,你可以通过使用Image类型的widthheight变量来计算。让我们声明一个名为origin的变量,并将精灵的中心存储在其中:

var origin = { x : someSprite.width / 2, y : someSprite.height / 2 };

现在,如果你想用这个不同的原点绘制精灵someSprite,你可以这样做:

var pos = { x : somePosition.x - origin.x,
            y : somePosition.y - origin.y };
Game.drawImage(someSprite, pos);

通过从位置中减去原点,子画面被绘制在一个偏移量处,使得位置somePosition指示子画面的中心。除了自己计算相对于原点的位置,canvas 上下文中的drawImage方法也有一种指定原点偏移量的方法。这里有一个例子:

Game.canvasContext.save();
Game.canvasContext.translate(position.x, position.y);
Game.canvasContext.drawImage(sprite, 0, 0, sprite.width, sprite.height,
     -origin.x, -origin.y, sprite.width, sprite.height);
Game.canvasContext.restore();

在本例中,第一步是保存当前绘图状态。然后,应用变换。你从翻译到一个给定的位置开始。然后您调用drawImage方法,在该方法中您必须提供许多不同的参数:将绘制哪个精灵以及(使用四个参数)应该绘制精灵的哪个部分。您可以通过指示 sprite 的左上角坐标和应该绘制的矩形部分的大小来做到这一点。在这个简单的例子中,您想要绘制整个 sprite,所以左上角的坐标是点(0,0)。您绘制一个矩形部件,其宽度和高度与整个 sprite 相同。这也表明,可以使用该特性在一个图像文件中存储多个精灵,而只需将该文件加载到内存中一次。在本书的后面,在第 18 章中,你会看到一个很好的方法来做到这一点,并将其整合到你的游戏应用中。

接下来,您可以指定位置偏移。您可以在前面的代码中看到,您将该偏移量设置为负的原点值。换句话说,你从当前位置减去原点。这样,左上角的坐标就移动到原点。假设您有一个宽度和高度为 22 像素的球精灵。假设你想在位置(0,0)画这个球,这个位置是屏幕的左上角。根据你选择的原点,结果是不同的。图 5-4 显示了用两个不同的原点在位置(0,0)绘制一个球精灵的两个例子。左边的例子显示球的原点在左上角,右边的例子显示球的原点在精灵的中心。

9781430265382_Fig05-04.jpg

图 5-4 。在位置(0,0)绘制一个球精灵,原点在精灵的左上角(左)或精灵的中心(右)

您可能已经注意到,在 JavaScript 中,从一个位置减去另一个位置有点麻烦:

var pos = { x : somePosition.x - origin.x,
            y : somePosition.y - origin.y };

如果你能这样写就更好了:

var pos = somePosition - origin;

不幸的是,这在 JavaScript 中是不可能的。一些编程语言(如 Java 和 C#)支持运算符重载。这允许程序员定义当两个对象使用加号运算符“相加”时应该发生什么。然而,并没有失去一切。可以定义在对象文字上执行这些算术运算的方法,比如上面定义的方法。第 8 章对此有更详细的论述。

现在你知道了如何在不同的原点绘制精灵,例如,你可以绘制一个气球,使其底部中心与鼠标指针相连。要了解这一点,请看气球 2 计划。您声明了一个额外的成员变量,用于存储气球精灵的来源:

var Game = {
    canvas : undefined,
    canvasContext : undefined,
    backgroundSprite : undefined,
    balloonSprite : undefined,
    mousePosition : { x : 0, y : 0 },
    balloonOrigin : { x : 0, y : 0 }
};

你只能在精灵载入后计算原点。所以,为了确保万无一失,您可以使用下面的指令在draw方法中计算原点:

Game.balloonOrigin = { x : Game.balloonSprite.width / 2,
                       y : Game.balloonSprite.height };

原点设置为精灵宽度的一半,但为其全高。换句话说,这个原点就是精灵的底部中心,这正是你想要的。用draw方法计算原点不理想;如果您可以只计算一次原点,就在图像加载之后,那就更好了。后来,你发现了一个更好的方法。

现在可以扩展Game对象中的drawImage方法,使其支持在不同的原点绘制精灵。您唯一需要做的就是添加一个额外的位置参数,并将该参数中的xy值传递给画布上下文的drawImage方法。下面是完整的方法:

Game.drawImage = function (sprite, position, origin) {
    Game.canvasContext.save();
    Game.canvasContext.translate(position.x, position.y);
    Game.canvasContext.drawImage(sprite, 0, 0, sprite.width, sprite.height,
        -origin.x, -origin.y, sprite.width, sprite.height);
    Game.canvasContext.restore();
};

draw方法中,您现在可以计算原点并将其传递给drawImage方法,如下所示:

Game.draw = function () {
    Game.drawImage(Game.backgroundSprite, { x : 0, y : 0 }, { x : 0, y : 0 });
    Game.balloonOrigin = { x : Game.balloonSprite.width / 2,
                           y : Game.balloonSprite.height };
    Game.drawImage(Game.balloonSprite, Game.mousePosition, Game.balloonOrigin);
};

使用鼠标位置来旋转炮管

画师游戏的一个特点是包含了一个根据鼠标位置旋转的炮管。这个大炮是由玩家控制的,目的是发射颜料球。您可以使用本章中讨论的工具编写程序的一部分来完成这项工作。您可以在本章的 Painter1 示例中看到这一点。

要做到这一点,您必须声明一些成员变量。首先,你需要变量来存储背景和炮管精灵。你还需要存储当前的鼠标位置,就像你在本章前面的例子中所做的那样。然后,因为你旋转炮管,你需要存储它的位置,它的原点,和它当前的旋转。最后,您需要画布和画布上下文,以便您可以绘制游戏对象。像往常一样,所有这些变量都被声明为Game对象的成员:

var Game = {
    canvas : undefined,
    canvasContext : undefined,
    backgroundSprite : undefined,
    cannonBarrelSprite : undefined,
    mousePosition : { x : 0, y : 0 },
    cannonPosition : { x : 72, y : 405 },
    cannonOrigin : { x : 34, y : 34 },
    cannonRotation : 0
};

定义Game变量时,炮管的位置和原点都被赋予一个值。炮管的位置是这样选择的,它正好适合已经画在背景上的炮座。桶图像包含一个圆形零件,实际的桶附着在该零件上。您希望桶围绕圆形部分的中心旋转。这意味着你必须把这个中心作为原点。因为圆形部分在 sprite 的左侧,并且这个圆的半径是 cannon-barrel sprite 高度的一半(68 像素高),所以将 barrel 原点设置为(34,34),如代码所示。

为了以一个角度绘制炮管,当你在屏幕上绘制炮管精灵时,你需要应用一个旋转。这意味着您必须扩展drawImage方法,以便它可以考虑旋转。应用旋转是通过作为画布上下文一部分的rotate方法完成的。您还可以向drawImage方法添加一个参数,让您指定对象应该旋转的角度。这是新版本的drawImage方法的样子:

Game.drawImage = function (sprite, position, rotation, origin) {
    Game.canvasContext.save();
    Game.canvasContext.translate(position.x, position.y);
    Game.canvasContext.rotate(rotation);
    Game.canvasContext.drawImage(sprite, 0, 0, sprite.width, sprite.height,
        -origin.x, -origin.y, sprite.width, sprite.height);
    Game.canvasContext.restore();
};

start方法中,加载两个精灵:

Game.backgroundSprite = new Image();
Game.backgroundSprite.src = "spr_background.jpg";
Game.cannonBarrelSprite = new Image();
Game.cannonBarrelSprite.src = "spr_cannon_barrel.png";

下一步是在游戏循环中实现这些方法。直到现在,update方法一直是空的。现在你有充分的理由使用它了。在update方法中,您更新了游戏世界,在这种情况下,这意味着您计算了绘制炮管的角度。这个怎么算?看一下图 5-5

9781430265382_Fig05-05.jpg

图 5-5 。基于鼠标指针位置计算桶的角度

如果你记得你的数学课,你可能记得三角形的角度可以用三角函数来计算。在这种情况下,您可以使用正切函数计算角度,如下所示:

image

换句话说,角度由下式给出

image

通过计算当前鼠标位置和炮管位置之间的差值,可以计算对边和邻边的长度,如下所示:

var opposite = Game.mousePosition.y - Game.cannonPosition.y;
var adjacent = Game.mousePosition.x - Game.cannonPosition.x;

现在,您必须使用这些值来计算反正切。你是怎么做到的?幸运的是,JavaScript 知道一个Math对象可以提供帮助。Math对象包含许多有用的数学函数,包括三角函数,如正弦、余弦和正切,以及它们的逆反正弦、反余弦和反正切。Math对象中的两个函数计算反正切。第一个版本采用单个值作为参数。你不能在这种情况下使用这个版本:当鼠标直接在桶上时,会发生被零除的情况,因为相邻是零。

对于需要计算反正切同时考虑可能的奇点的情况,有一种替代的反正切方法。atan2方法将相反和相邻的长度作为单独的参数,并在这种情况下返回 90 度的等效弧度。你可以用这个方法计算角度,如下:

Game.cannonRotation = Math.atan2(opposite, adjacent);

这些指令都放在update里。下面是完整的方法:

Game.update = function () {
    var opposite = Game.mousePosition.y - Game.cannonPosition.y;
    var adjacent = Game.mousePosition.x - Game.cannonPosition.x;
    Game.cannonRotation = Math.atan2(opposite, adjacent);
};

剩下唯一要做的就是用draw方法在屏幕上绘制精灵,在正确的位置和角度:

Game.draw = function () {
    Game.clearCanvas();
    Game.drawImage(Game.backgroundSprite, { x : 0, y : 0 }, 0,
         { x : 0, y : 0 });
    Game.drawImage(Game.cannonBarrelSprite, Game.cannonPosition,
         Game.cannonRotation, Game.cannonOrigin);
};

你学到了什么

在本章中,您学习了:

  • 如何使用事件处理程序读取当前鼠标位置,以及如何在当前鼠标位置绘制精灵
  • 如何画一个有角度的精灵
  • 如何根据鼠标位置改变绘制精灵的角度