Building JavaScript Games for Phones Tablets and Desktop(6)- 响应玩家输入

响应用户输入

这章里,学会如何处理按键输入。为了实现这个要求,你需要if语句或者一组相关语句来检测按键条件满足与否。

游戏中的对象

截止目前为止,所有例子都有一个叫做Game的对象。这个对象里有很多变量。来看看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对象里面。从概念上说,这很容易理解,因为Game含有所有跟painter游戏有关的东西。然而,如果你把这些东西拆分一下会更好理解。

你仔细观察Game对象,会发现其中某些变量是相关的。比如,canvas和canvasContext相关联,因为都跟画布有关系。比如也有些变量储存有关大炮的信息,比如位置和角度。你可以把这些相关的变量再次组合成其它对象,这样能让代码看起来更清晰。比如:

var Canvas2D = {
canvas : undefined,
canvasContext : undefined
};
var Game = {
backgroundSprite : undefined,
};
var cannon = {
cannonBarrelSprite : undefined,
position : { x : 72, y : 405 },
origin : { x : 34, y : 34 },
rotation : 0
};

var Mouse = { position : { x : 0, y : 0 } };

现在,有几个不同的对象了,每个都有一些变量,能轻松的看出哪些变量属于哪些对象。更棒的是你也可以对方法做同样的事情。比如,可以为Canvas2D对象添加清除画布和画图的方法,比如:

Canvas2D.clear = function () {
Canvas2D.canvasContext.clearRect(0, 0, this.canvas.width,
this.canvas.height);
};
Canvas2D.drawImage = function (sprite, position, rotation, origin) {
// canvas drawing code
};

使用不同的对象而不是使用一个包含一切有关游戏的对象,这样能让代码更加易读。当然,这是在你能逻辑区分这些对象下说的。即使简单的游戏,你也可以有多种组织代码的方式。每个开发者都有自己的方式。本书就是其中某种方式。你也许不同意这种方式,某些时候你有一个与本书有关问题的不同解决办法。当然这是正确的,因为程序问题不仅仅是只有一个单一的解决办法。

看前面命名的对象,你会发现大多对象的首字母是大写(比如Canvas2D),但是cannon对象小写字母开头。这么做是有原因的,后面会详细说明。现在,我们认为大写字母开头的对象对任何游戏都是有用的,小写字母开头的对象只对特定的游戏有用。这里,你可以认为Canvas2D对象对所有的HTML5游戏都有用,但cannon对象只对painter游戏有用。

加载精灵

现在你游戏中有不同的对象了,那么从哪里加载精灵呢?你可以在start方法里面加载所有精灵,但是另外一个方法是添加一个类似的方法。比如,cannon对象和其加载都放在cannon里面。到底哪种好呢?

关于在cannon对象里面创建个加载cannon精灵的方法有些东西要说。那样的话,你可以清楚的看到哪个精灵对应哪种对象。然而,这也意味着如果你想在不同的游戏对象里使用这个精灵,需要多次加载这个精灵。这意味着运行在浏览器中的游戏需要从服务器端下载图片文件,需要花费时间。更好的方法是在游戏开始时加载所有需要的精灵。为了和余下的程序部分区分,你把这些精灵放在一个叫做sprite的对象里面。这个对象作为一个空对象在程序顶部被声明:

var sprites = {};

在start方法里面填充这个变量。对每一个想加载的精灵,创建一个Image对象,然后告诉其图片地址。因为需要使用不少不同的精灵,你把这些精灵都放在同一个文件夹里。这样在本书中其它例子你就不需要再复制这些图片文件了。下面是painter2例子加载不同精灵的代码:

var spriteFolder = "../../assets/Painter/sprites/";
sprites.background = new Image();
sprites.background.src = spriteFolder + "spr_background.jpg";
sprites.cannon_barrel = new Image();
sprites.cannon_barrel.src = spriteFolder + "spr_cannon_barrel.png";
sprites.cannon_red = new Image();
sprites.cannon_red.src = spriteFolder + "spr_cannon_red.png";
sprites.cannon_green = new Image();
sprites.cannon_green.src = spriteFolder + "spr_cannon_green.png";
sprites.cannon_blue = new Image();
sprites.cannon_blue.src = spriteFolder + "spr_cannon_blue.png";

上面用到了+运算符。举例说明:spriteFolder + “spr_background.jpg”结果就是”../../assets/Painter/sprites/spr_background.jpg”.因为sprites文件夹与painter2文件夹不再同一个目录,所以有这个东西,所以还添加了两层目录。下一步就是检测玩家的按键情况。

处理按键事件

之前的章节里,学习了如何通过使用事件处理来获取当前的鼠标值。获取按键信息的方式与这个类似,定义一个相关的事件处理函数。你需要用个变量来储存用户按下的键值以便以后使用。最早储存键值的方法是使用虚拟键码。虚拟键码就是一个数字代表某个键值。比如,空格键可以是数字13或者数字65。那么为什么使用这些特定的数值呢,而不是其它的?因为字符表已经是一个标准了,多年来不同的标准应运而生。

在20时间70年代,程序员认为2的6次方64个符号就足够代表所有的符号,26个字母,10个数字,28个标点符号。虽然这意味着字母没有大小写之分,但在当时这不是个问题。

在20时间80年代,使用2的7次方128个符号:26个小写字母,26个大写字母,10个数字,33个标点符号和33个特殊字符。这些字符就是ASCII字符:美国信息交换标准代码。这在英语里很有用,但是不适用于其他语言,比如法语,西班牙语等等。

因此,20世纪九十年代,一个256个字符表出现。里面有包含了其他国家的字符。0到127还是ASCII字符,128-255就是指定语言的字符。这样处理不同的语言是合理的,如果你想同时用不同的语言储存文本就是一件复杂的事情了。那些超过128个字符的语言就不能用这种格式表示。

在21世纪初,编码规范再次扩展,现在有65536个字符了。这个字符表能轻易的包含世界上所有的语言了。这个编码表叫做Unicode。

回到前面你想储存的虚拟按键那里,添加一个储存最近按下按键的变量。

var Keyboard = { keyDown : -1 };

变量初始化的值为-1.表示用户现在任何键都没有按下。但用户按下按键时,按键的值就储存在Keyboard.keyDown变量里,写一个事件处理来存放Keyboard.keyDown值:

function handleKeyDown(evt) {
Keyboard.keyDown = evt.keyCode;
}

上面的参数是一个事件,这个事件对象有个叫做keyCode的变量,储存着用户当前按下的键值。

把这个事件处理函数放在Game.start里面:

document.onkeydown = handleKeyDown;

当用户松掉按键时怎么处理呢?Keyboard.keyDown值应该再次变为-1,表示当前用户没下按下任何键。通过按键弹起事件处理。

function handleKeyUp(evt) {
Keyboard.keyDown = -1;
}

同样把它放在Game.start里面:

document.onkeyup = handleKeyUp;

现在你已经可以在游戏中处理按键事件了。注意的是这里处理按键有点限制。比如,这里不能记录同时按下的键,比如用户同时按下A和B。第13章里面,将会通过扩展Keyboard对象考虑到这些。

条件执行

通过上面简单的例子知道了如何使用Keyboard对象做些事情。现在来扩展Painter1程序,在炮身上面画一个彩色的球。通过按下R,G,B键,玩家可以改变小球的颜色为红色,绿色,蓝色。图6-1是这个程序的截图:

(省略图6-1)

需要添加额外的三个精灵,每个代表一个颜色小球:

sprites.cannon_red = Game.loadSprite(spriteFolder + "spr_cannon_red.png");
sprites.cannon_green = Game.loadSprite(spriteFolder + "spr_cannon_green.png");
sprites.cannon_blue = Game.loadSprite(spriteFolder + "spr_cannon_blue.png");

为cannon对象添加一个initialize方法,用来进行相关变量的赋值。这个方法通过initialize调用。那样,大炮在游戏开始被初始化:

Game.start = function () {
Canvas2D.initialize("myCanvas");
document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;
document.onmousemove = handleMouseMove;
...
cannon.initialize();
window.setTimeout(Game.mainLoop, 500);
};

cannon.initialize方法实现如下:

cannon.initialize = function() {
cannon.position = { x : 72, y : 405 };
cannon.colorPosition = { x : 55, y : 388 };
cannon.origin = { x : 34, y : 34 };
cannon.currentColor = sprites.cannon_red;
cannon.rotation = 0;
};

现在有两个位置变量。一个是炮身的,一个是彩球的。此外,添加一个代表现在彩球颜色的变量,它的初始值为红色。

为了清楚的区分这些对象,可以为cannon添加一个draw方法。用这个方法来画炮身和彩球:

cannon.draw = function () {
Canvas2D.drawImage(sprites.cannon_barrel, cannon.position, cannon.rotation,
cannon.origin);
Canvas2D.drawImage(cannon.currentColor, cannon.colorPosition, 0,
{ x : 0, y : 0 });
};

这里的draw方法通过Game.draw调用:

Game.draw = function () {
Canvas2D.clear();
Canvas2D.drawImage(sprites.background, { x : 0, y : 0 }, 0,
{ x : 0, y : 0 });
cannon.draw();
};

那样,你可以更清楚的看到绘画指令对应着哪个对象。现在准备工作已经做好了,可以开始处理玩家的按键事件了。到目前为止,你的所有代码都在运行着。比如,程序一直在画着背景图和炮身。但是现在你需要满足某些条件下才会执行另外一些代码。比如,按下G键时球的颜色才会发生改变,这种指令叫做条件指令,这种指令使用if关键字。

有了条件指令,就可以根据条件满足情况来执行相关代码了。比如用户是否按下某个按键,时间是否已经过去了一定的时间,怪物是否吃掉了你的角色。

条件有真有假,条件是一个表达式,有值,这个值叫做布尔值。当条件值为真时,执行相关语句。比如:

if (Game.mousePosition.x > 200) {
 Canvas2D.drawImage(sprites.background, { x : 0, y : 0 }, 0,
 { x : 0, y : 0 });
}

如果这里if下只有一行代码,可以省略花括号写成:

if (Game.mousePosition.x > 200)
 Canvas2D.drawImage(sprites.background, { x : 0, y : 0 }, 0,
 { x : 0, y : 0 });

在这个例子中,需要检测用户是否按下了R,G,B键,表示R键按下可以像下面这么写:

Keyboard.keyDown === 82

===运算符比较两边的值,然后返回true或false。现在可以在update方法里面使用if指令来确认是否按下了R键:

if (Keyboard.keyDown === 82)
 cannon.currentColor = sprites.cannon_red;

比较烦人的事情是需要记住所有的虚拟键值。但是你可以用一个叫做Keys的变量来记住大部分常用的键值:

var Keys = {
 A: 65, B: 66, C: 67, D: 68, E: 69, F: 70,
 G: 71, H: 72, I: 73, J: 74, K: 75, L: 76,
 M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82,
 S: 83, T: 84, U: 85, V: 86, W: 87, X: 88,
 Y: 89, Z: 90
};

现在判断键值的代码就很清晰好理解了:

if (Keyboard.keyDown === Keys.R)
 cannon.currentColor = sprites.cannon_red;

比较运算符

  • < Less than
  • <= Less than or equal to
  • > Greater than
  • >= Greater than or equal to
  • === Equal to
  • !== Not equal to

常见的运算符就是这些,需要注意的是===、一个=表示赋值,那么有没有两个=的比较运算符了。其实是有的。==也能进行比较,但是如果==两边不是同一类型,那么就会自动把其中一种类型转换成另一类型进行比较。所以,有时候结果看起来会很奇怪。比如:

'' == '0' // false
 0 == '' // true!
 0 == '0' // true!

如果用===的话,上面的结果都是false。所以,最好避免使用==。

逻辑运算符

(省略)

布尔类型

(省略)

通过鼠标指针来进行瞄准

上节里,知道如何通过if来判断用户是否按下R键。现在,假设鼠标左键按下时才更新炮身角度。为了处理鼠标按键,需要额外的两个事件处理:一个处理用户鼠标按下,一个处理用户松开鼠标按键。这与键盘按键的按下松开类似。当鼠标按下或松开,evt对象的which变量可以知道是哪个按键(1是左键,2是中键,3是右键)可以用一个布尔变量来表示一个按键是否按下:

var Mouse = {
 position : { x : 0, y : 0 },
 leftDown : false
};

需要添加两个事件处理函数:

function handleMouseDown(evt) {
 if (evt.which === 1)
 Mouse.leftDown = true;
}
function handleMouseUp(evt) {
 if (evt.which === 1)
 Mouse.leftDown = false;
}

然后:

document.onmousedown = handleMouseDown;
document.onmouseup = handleMouseUp;

现在,在cannon的update方法里,只有当鼠标左键按下时更新鼠标左键。

if (Mouse.leftDown) {
 var opposite = Mouse.position.y - this.position.y;
 var adjacent = Mouse.position.x - this.position.x;
 cannon.rotation = Math.atan2(opposite, adjacent);
}

假设想鼠标左键松开后炮身角度为0,需要添加另外一条if语句:

if (!Mouse.leftDown)
 cannon.rotation = 0;

当条件越来越复杂,处理条件的语句会越来越难理解。这里有一个非常好的处理方式,使用与if相对的条件,当不为真时执行,用到的关键字是else:

if (Mouse.leftDown) {
 var opposite = Mouse.position.y - this.position.y;
 var adjacent = Mouse.position.x - this.position.x;
 cannon.rotation = Math.atan2(opposite, adjacent);
} else
 cannon.rotation = 0;

上面的代码和之前的两条if代码是一样的,但是只需写一个条件。执行painter2程序会看到效果。

多重条件选择

(省略)

切换炮筒行为

这节例子是如何处理鼠标的点击而不是鼠标按键按下(也就是说怎么知道本次按下不是先前的按下),看程序Painter2a。当鼠标点击后,炮身跟随鼠标指针移动。再次点击后,炮身停止移动。

炮筒切换中有个问题就是你只知道当前的鼠标状态。这些信息不能说明是否点击了鼠标。可以通过下面两个方面来判断用户是否点击了鼠标:

  • 当前鼠标状态为按下
  • 在最近的一次update方法调用期间中鼠标键状态不是按下

添加另外一个布尔变量Mouse.leftDown ,表明用户是否按下鼠标。如果鼠标按下,该变量为true并且Mouse.leftDown非true。下面是handleMouseDown事件方法:

function handleMouseDown(evt) {
 if (evt.which === 1) {
 if (!Mouse.leftDown)
 Mouse.leftPressed = true;
 Mouse.leftDown = true;
 }
}

上面的代码有一个if嵌套,现在可以根据鼠标是否按下来进行大炮动作的切换了、

if (Mouse.leftPressed)
 cannon.calculateAngle = !cannon.calculateAngle;

在if语句中,切换calculateAngle变量。这个变量是cannon对象的一个布尔成员变量。为了进行动作的切换,使用了非逻辑运算符。

现在可以上面的calculateAngle值来决定是否更新角度值了:

if (cannon.calculateAngle) {
 var opposite = Mouse.position.y - this.position.y;
 var adjacent = Mouse.position.x - this.position.x;
 cannon.rotation = Math.atan2(opposite, adjacent);
} else
 cannon.rotation = 0;

为了完成这个程序,需要在每次主循环后将Mouse.leftPressed复位。如下在Mouse对象中添加一个reset方法:

Mouse.reset = function() {
 Mouse.leftPressed = false;
};

最终,主循环看起来像这样:

Game.mainLoop = function() {
 Game.update();
 Game.draw();
 Mouse.reset();
 window.setTimeout(Game.mainLoop, 1000 / 60);
};

你学到了什么

这章里,学到了:

如何通过If语句处理按键按下和鼠标点击
如何通过布尔值来规范判断条件
如何选择不同的if条件

你可能感兴趣的:(JavaScript)