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

响应用户输入

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

游戏中的对象

截止目前为止,全部例子都有一个叫作Game的对象。这个对象里有不少变量。来看看painter1例子中的Game对象:canvas

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对象添加清除画布和画图的方法,好比:markdown

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游戏有用。oop

加载精灵

如今你游戏中有不一样的对象了,那么从哪里加载精灵呢?你能够在start方法里面加载全部精灵,可是另一个方法是添加一个相似的方法。好比,cannon对象和其加载都放在cannon里面。到底哪一种好呢?学习

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

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条件
相关文章
相关标签/搜索