这章里,创造一个叫做Painter的游戏。这个游戏里,需要在屏幕上显示移动的精灵。你已经知道了一些加载和显示精灵的例子。而且,知道了如何用经过的时间值来改变精灵的位置。在这些基础上来创建这个painter游戏。此外,会学到如何处理用户游戏中的输入。你将从之前的FlyingSprite例子开始,把它改变成气球的位置跟随鼠标移动。下一章将检测其它类型的输入,比如键盘或者触摸屏。
现在你知道如何在屏幕上显示精灵,现在来使用用户的输入来控制精灵的位置。为了做到这些,你需要知道当前鼠标的位置。这节教你如何获取鼠标位置和怎样画出一个跟随鼠标移动的精灵。
看例子Ballon1。这个程序与之前的FlyingSprite程序没有多大的不同。在FlyingSprite中,通过系统时间来计算气球的位置。
var d = new Date();
Game.balloonPosition.x = d.getTime() * 0.3 % Game.canvas.width;
位置储存在balloonPosition变量里面。现在的程序不是根据时间来计算位置,而是根据鼠标的位置。使用事件来获取当前鼠标值是非常简单的。
在javascript里有许多不同类型的事件。常见事件例子如下:
发生上述事件,就可以选择执行指令。比如玩家移动鼠标时,你可以通过几条指令来获取鼠标位置值,储存位置信息,之后就可以用它来绘画精灵。比如当HTML网页加载时,document可以让你获取网页中的元素。但是更重要的是,这些变量能让玩家通过鼠标移动,键盘或者点击触摸屏来获取信息。
你已经使用过这些变量,比如下面通过document获取来自HTML网页的canvas元素:
Game.canvas = document.getElementById("myCanvas");
除了getElementById,document还有其它成员变量和方法。比如有个叫做onmousemove的成员变量。这个变量不是数值或字符串,而是一个方法。当鼠标移动,浏览器就会调用这个方法。那么就可以在这个方法里面写你想写的代码。正是如此,这种类型的方法才被叫做事件处理。使用事件处理是处理用户输入非常有效的方式。
另外一个处理用户输入的方式是在每次循环里都检测用户的输入。虽然那样也行,但是那比使用事件处理慢多了,因为需要在每次的循环进行检测,而事件处理是用户做了某事就会自动知道。
一个事件处理函数有个特定的书写方式。它有一个参数,这个参数是个对象,能提供有关事件的信息。比如下面有个空的事件处理函数:
function handleMouseMove(evt) {
// do something here
}
这个函数有个单一的参数evt,包含了需要处理事件的信息。现在可以把这个函数赋值给onmousemove变量。
document.onmousemove = handleMouseMove;
从现在起,每次鼠标移动,handleMouseMove函数就被调用。你可以在这个函数里通过evt对象得到鼠标的位置。比如下面的例子:
function handleMouseMove(evt) {
Game.balloonPosition = { x : evt.pageX, y : evt.pageY };
}
evt对象的两个成员变量pageX和pageY储存了鼠标位置,位置从左上角(0,0)算。在图5-1中可以看到一些有关位置的信息。
(省略图5-1)
因为draw方法只是简单的以鼠标位置画出气球,所以气球跟着鼠标。图5-2展示了这种效果。你可以发现气球在鼠标指针下面;当你移动鼠标,气球会跟着鼠标。
(省略图5-2)
从图5-2可以看出气球并没有出现鼠标指针尖端的中心位置。这就是原因所在,下节会详细说明这一切。现在把这个精灵当作一个矩形。左上角就是鼠标的尖端。气球看起来没有与尖端对齐是因为气球是圆的且没有完全覆盖矩形。
除了pageX和pageY,也可以使用clientX和clientY,也能代表鼠标位置。然而,clientX和clientY不计算鼠标的滚动值。假如下面这样计算鼠标位置:
Game.balloonPosition = { x : evt.clientX, y : evt.clientY };
图5-3告诉了错误所在。因为鼠标滚动,clientY比480小,即使鼠标已经来到了图片的底部。因此,气球不会出现在出表出现的位置。因此,我建议一直使用pageX和pageY。当然在某些情况下,不把滚动值加入位置是有用的——比如,当网页有广告时不会随着滚轮移动而移动。
(省略图5-3)
当运行Balloon1例子,注意到气球的左上角是当前鼠标位置。当你把精灵画在某个确定的位置,那么这个精灵的左上角就是这个位置。比如下面这条指令:
Game.drawImage(someSprite, somePosition);
someSprite的左上角就在somePosition位置上。也可以说左上角就是精灵的原点。如果你想改变原点怎么办?比如你想让精灵的中心为原点,此时可以通过Image类的width和height变量计算这个原点值。现在声明一个叫做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);
通过减去原点值,精灵的原点位置现在就是精灵的中心了。除了自己计算相关原点值,来自canvasContext的方法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();
通过设定上述不同的偏离值,可以得到精灵两种不同的显示方式,一种原点在精灵的左上角,一种在精灵的中心,如图5-4所示。
(省略图5-4)
在JavaScript中与另一个位置之间做减法有点麻烦:
var pos = { x : somePosition.x - origin.x,
y : somePosition.y - origin.y };
如果可以下面这样写那就舒服多了:
var pos = somePosition - origin;
不幸的是,在JavaScript里这是不可能的。其他一些语言(比如Java和C#)支持运算符重载,其允许程序员定义他们自己的运算操作。两个对象可以通过“+”进行相加。但是并不是完全没有可能,我们可以定义方法来让对象之间进行运算就像上面那样。第8章会仔细讲解这个东西。
现在知道怎样绘画精灵的不同原点。同理你也可以让鼠标跟随气球底部的中心进行移动。例子balloon2实现了这种想法。在这里定义了一个额外的储存原点的变量:
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方法里面计算原点并不是最好的,它出现在只需要计算一次原点的地方。之后,你会发现更好的处理方法。
现在只需要扩展下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);
};
painter游戏的特点之一就是通过可以通过鼠标来进行炮身的旋转。玩家可以通过大炮来射击气球改变气球颜色。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
};
炮身的位置和炮身原点的位置都已经定义了。炮身位置的选取是和大炮基座非常契合的。炮身中有一部分圆的。你想让大炮跟随圆心转动。意味着圆心就是原点。因为圆形的这部分在图片的左半边且半径是炮身高度的一半(炮身高度68像素),所以原点就是(34,34),这跟上面的代码一样。
为了让炮身看起来有一个角度,你需要在屏幕上画炮身时让炮身有一个旋转。在canvascontext中有个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中还什么都没有。现在要开始往里面写东西了。需要计算出炮身要偏转的角度。那么怎么计算这个角度呢,看图5-5:
(省略图5-5)
可以用数学中的三角函数来计算角度。在这里,使用正切函数:
tan(angle) =tan(opposite/adjacent)
换个样式,也就是:
angle = arctan(opposite/adjacent)
可以通过计算当前鼠标位置和炮身位置的不同来计算出邻边和对边的长度。如下:
var opposite = Game.mousePosition.y - Game.cannonPosition.y;
var adjacent = Game.mousePosition.x - Game.cannonPosition.x;
通过上面的值可以算出角度。怎么计算呢,javascript提供了一个Math对象,里面包含了一些非常有用的数学函数,包括三角函数。有两个函数可以计算角度值。第一个函数只需要一个单一的函数,但是不能用因为当鼠标在炮身正上方,除数是0.
考虑到这种情况,这里有另外一个方法。atan2使用邻边和对边两个参数来进行角度计算。如下:
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);
};
这章里,学到了: