GameQuery入门教程

翻译:bjc5233 (Email:bjc5233@gmail.com)

目录

第1章

简介 1.1 开篇语

自从这篇教程写了之后,API就发生过变化。但教程还没有更新过,所以请先看看变更向导吧

在这篇教程中,我会尽力指导你如何从头到尾做一个javascript的游戏。我会一一个非常基础的横轴滚动射击游戏作为例子。为了将精力放在游戏开发的基本流程任务上,我会使这个游戏在拥有完整游戏开发流程的基础上,尽量精简。所以导致的结果是,你会发现下面这个游戏其实并不怎么好玩,但是我们的目标是学习怎么使用Query和gameQuery来制作一个javascript游戏,而不是别的!

目前的版本(0.2.6, 翻译时最新为0.6.1)gameQuery非常适合用来做简单的基于精灵的2D游戏,也就是我们现在将要做的游戏的类型。你可以用各种方式制作精灵,这完全取决于你。你可以,手画精灵,扫描,然后用你最喜欢的图片编辑器编辑它们。当然,你也可以直接在电脑上画,就像我为了第一个演示程序而做的一样。

1.1.1 条件

我会尽力使这篇教程通俗易懂的,即使那些人没有编程基础、网页设计经验。当介绍这些内容的时候,我会指出相关知识,如果你觉得有必要的话,你应该找些资料,更深入的去了解。或许这对那些有经验的编程人员来说很无聊,那么你应该去看看程序源代码,它们都是自解释的。

代码片段都提供了到gameQueryAPI文档的链接。当你点击其中的关键字,一个小窗口会弹出并显示相应的文档说明,再次点击这个弹出窗口,它就会关闭。这种方式会使我们明白哪些代码属于gameQuery,而哪些不是。

1.1.2 警告

由于英语并不是我的母语,所以请容忍我的那些拼写错误,也请你能给我发邮件指出那些错误。我会尽力使得教程对于一个初学者来说易于理解的,而在这种情况下,我可能会有些你认为是不正确的话,也希望你能在评论中说明,让其他读者了解。

1.2 工具

下面是一系列你能马上入手的工具。它们可能是开源免费软件,也可能是些功能强大的商业软件。

1.2.1 图片处理

1.2.2 开发环境

1.3 游戏描述

玩家操作一艘飞船往右边飞,玩家必须躲避或者消灭从右边出现朝飞船飞来的敌人。敌人分为三种,每一种都有不同的行动方式和武器。一旦玩家的飞船碰到敌人,或者敌人的导弹,飞船将减少一层护盾。如果飞船在没有任何护盾的情况下再次碰撞,飞船会减少一条生命。如果玩家在飞船爆炸之后还有“命”的话,飞船会再次出现,否则游戏结束。在飞船复活之后,它会有3秒钟的无敌时间。

第一种敌人会很频繁的出现,我们称之为“炮灰”。它们走直线。它们并不会发射导弹。

第二种敌人稍微有些难缠,它们行走路线有些不可预测,我们称之为“有头脑的”。它们以随机的速率发射导弹。

最后一种敌人是某种“最终boss”。它们更大更强,但是比起其他两种敌人出现概率很少。它们并不怎么移动,但发射导弹。但出现一个这样的敌人,就不会出现新的敌人直到你摧毁它为止。

这个游戏没有终点,没有分数,完全是随机的,记住,这仅仅是个向导!为游戏增加功能特色非常容易,这对你来说也是个非常好的经历。

1.4 最终游戏展示

这就是我们在教程结束得到的东西。使用a,d,w,s来移动飞船,k射击。

第2章 步骤1 – 精灵动画

2.1 游戏屏幕组织结构

在教程的步骤1当中,我们将会学习到游戏屏幕展示到底是由什么组成的。如果你了解Gimp或者Photoshop的话,你可能会联想到游戏屏幕是由一系列的层叠加而成,就像图1.

为了在gameQuery中生成“屏幕”,你只需要操作两类对象:精灵和组(sprites、groups),为了渲染精灵gameQuery使用了绝对定位元素。每一个精灵都有它自己的层。你添加精灵的顺序就是默认顺序,类似“堆栈”结构。

在游戏的某些点上,如果想在一个已经存在的精灵之后再增加新的精灵的话,该怎么做呢?最简单的方法是使用组,就像图1展示的那样。如果你在一个已经定义过的组(例如 groupA)中增加新的精灵的话,这个精灵就会位于groupA前元素之后(精灵3 精灵4)。

使用组还有其他好处:如果你移动组,那么其中所有的元素也会移动,如果你删除组,那么其中所有的元素也会被删除。它们也可以作为某种遮罩(mask)来使用:如果你在组选项中指定overflow为hidden的话,超过组边界的精灵就是不可见的。你可以在组中嵌套组。在某些情况下,正确使用组,通过减少手动指定移动、检测碰撞的元素数量,会有更好的性能。

你也许想知道图1中那些名字前面的“#”代表什么意思? 它们存在是因为组合精灵都是基本的html元素。Html元素组成了网页,而为了更好的标识元素,我们使用两种标识符:给你的元素起个唯一的名字,这就是“id”;把它归属为一类而起的名字,称为“class”。元素可以同时具有“class”名和“id”名。这种名称标识常常被用来书写层叠样式表CSS。在CSS中,你描述网页中元素的展现方式。在CSS中,“class”类名前加“.”号,而“id”名前加“#”号。所以在图1中 #groupA的意思是id名是groupA的html元素。这种标识方式称为“CSS Selector”(CSS选择器)。我希望你能学习更多关于HTMLCSS的知识,但是要做好被混淆的心理准备哦:)

现在让你看看接下来我们要面对的东西。图1的情形结果,如果用代码来描述的话,会是下面这个样子。

$(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH})

.addSprite("sprite1",{animation: animation1})
.addGroup("groupA")
  .addSprite("sprite2",{animation: animation2}).end()
.addSprite("sprite3",{animation: animation3})
.addGroup("groupB",{overflow: hidden})
  .addSprite("sprite4",{animation: animation4});

2.1.1 对jQuery的超短介绍

gameQuery是基于jQuery的,所以我会简单的介绍一下jQuery的工作方式。虽然介绍异常简单,但你可以从这里开始理解jQuery。更多关于jQuery的信息,请看jQuery文档。

jQuery可以分为两部分内容:“选择”、“修改”。“选择”是通过CSS选择器(CSS selector)在页面中寻找对应的元素。“修改”是指改变、增加、删除指定元素。你写代码的时候,其实就是这两种命令的联合。例如,如果你想要删除页面中所有类(class)名叫“foo”的元素,你可以这么写:

$(“.foo”).remove();

在这里,$(“.foo”)的意思是选择:选择所有类名叫foo的元素;.remove()的意思是修改:删除它们。当情况需要的时候,我会再深入讲解的。

2.1.2 开始快乐的编程吧!

首先为你的游戏画个草图,描述精灵和组的那种关系。在这篇教程中,草图将会是图2这个样子。就像你所看到的那样,这个游戏是如此的简单以至于我们都不需要真正复杂的结构来描述。

让我们开始写游戏屏幕的代码吧:首先我们需要告诉gameQuery游戏该画在哪里。这可以通过.playground()方法。gameQuery的每个方法都是基于jQuery的。这也是为什么下面代码是以$()开头的原因。

playground()方法需要两个参数,第一个是容纳游戏的html元素的CSS选择器(CSS selector)。第二个是一系列配置游戏屏幕(gamescreen)的值,这里我们制定了playgound的height和width。一个非常好的做法是,用变量来代替你的常量,因为以这种方式,你可以在将来很容易的改变它们的值。这也使得代码更具可读性。

var PLAYGROUND_HEIGHT = 250;
var PLAYGROUND_WIDTH = 700;

$("#playground").playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH});

现在我们需要创建组来包含精灵们(参看图2)。背景(background)离远我们观察者最远,所以首先创建它。我们可以这么写:

$.playground().addGroup(“background”, {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT});

这里我们调用了playground()方法,却没有提供任何参数,这将返回之前定义的html元素。然后我们通过调用addGroup()以及它的两个参数来添加新组。第一参数是我们想给组起的名字(这也将成为展示组的html元素的id)。第二个参数是一系列配置组的值(就像之前playground方法),这里我们定义了组的大小,同playground一样。

如果上面两段代码最后一行是一个接着一个的话,使用链式语句将会更加方便。

$(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH})
   .addGroup("background", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT});

这看起来更像图1的代码了。通过传输return返回值、分行对齐的方式使得代码更易阅读,它们并不改变代码含义。

我们接着添加其他组:

$(“#playground”).playground({height: PLAYGROUND_HEIGHT, width: PLAYGROUND_WIDTH})

.addGroup("background", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end()
.addGroup("actors", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end()
.addGroup("playerMissileLayer",{width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT}).end()
.addGroup("enemiesMissileLayer",{width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT});

注意.end()方法,这种方式返回上一个被选择的元素。一些“修改操作”会改变当前“被选择元素”:就像addGroup()把自己这个组当做“被选择元素”返回,所以如果你在这样的命令之后再添加的话,之前创建的组就会改变。你也可以使用这种方式方便的给组添加精灵。而如果你想在之前创建的组中嵌套组的话,你可以再次调用addGroup()方法,当然,我们现在的代码中并不需要这样。我们需要的是为playground再次添加组,所以我们调用end()方法来重新获取playground,继续链接代码。现在我们需要在那些组中增加精灵了!

2.2 游戏背景

为了使背景看起来具有远近的感觉,我们将会使用到许多以不同速度移动的层。这被称为是平行效果(parallax effect)。 这种效果是基于这样的事实:在观察者看来更远的物体,比更近的物体移动更少距离,前提是它们在同样的时间内,以相同的速度。

背景需要一刻不停的移动着,为了产生parallax效果,我们需要用到在背景层中的两个精灵。当只有一个层显示的时候,我们可以让第二个层在运动到第一个层末尾的时候又回到第一个层的开始部位,就像图3显示的。

由图片构成的精灵总是动画,即使精灵本身并不动!一个动画只需一张图片。这里,我用到6个精灵(每层2个)来使层的滚动的重复性看起来更少。然后,我通过为“#background”层添加精灵,创建了6个精灵。注意,当你创建精灵的时候就应该指定精灵的大小,而不是创建动画的时候!

var background1 = new $.gameQuery.Animation({imageURL: “background1.png”});

var PLAYGROUND_WIDTH           = 700;
var PLAYGROUND_HEIGHT         = 250;

var background2 = new $.gameQuery.Animation({imageURL: "background2.png"});
var background3 = new $.gameQuery.Animation({imageURL: "background3.png"});
var background4 = new $.gameQuery.Animation({imageURL: "background4.png"});
var background5 = new $.gameQuery.Animation({imageURL: "background5.png"});
var background6 = new $.gameQuery.Animation({imageURL: "background6.png"});

$("#background").
   .addSprite("background1", {animation: background1, 
               width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT})
   .addSprite("background2", {animation: background2,
               width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT,
               posx: PLAYGROUND_WIDTH})
   .addSprite("background3", {animation: background3, 
              width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT})
   .addSprite("background4", {animation: background4, 
              width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT,
              posx: PLAYGROUND_WIDTH})
   .addSprite("background5", {animation: background5,
              width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT})
   .addSprite("background6", {animation: background6,
              width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT,
              posx: PLAYGROUND_WIDTH});

现在我们需要让这些精灵动起来。如果你想在动画的每个间隔中执行指定动作,那么可以使用registerCallback()方法。通过这样,你就可以每过n毫秒,周期性的调用指定函数。我们通过这样就能够创造出图3中精灵的移动效果。为了使精灵移动,你不需要使用任何的gameQuery函数。jQuery就能完全满足你的要求。最简单的方法当数操纵精灵的CSS属性,改变top属性就能使精灵垂直移动,改变left属性就能使精灵水平移动。

对每个背景层的精灵,我们想要使它们产生从屏幕右边到屏幕左边的移动效果。由于是水平移动,所以只需要从PLAYGROUND_WIDTH到-PLAYGROUND_WIDTH改变left值,而不用改变top属性。每当你想要从时间间隔中获取指定值,并循环检测,一个十分有用的方法是使用求模表达式:%。这个操作符会得到整数相除之后的剩余值。举个例子,如果15%6,那么整数相除的结果是2(15/6=2),因为2*6=12,而15-12=3中的3就是余数,所以15%6=3。千万不要被混淆了。现在要是你想数字从0递增到20,每次增量为1,那么可以这么写:

while(condition){

   myValue++; //equivalent to myValue = myValue + 1; 
   if(myValue >= 20){
      myValue = 0;
   }
}

这里,你需要一个条件检测,以及为myValue赋值两次。使用求模表达式将会更简单:

while(condition){

   myValue = (myValue + 1) % 20;                                      
}

我们需要或得精灵的即时水平位置,并修改它。为了修改元素的css属性,我们使用到了jQuery中的css方法,第一次我们只是用一个参数来获取当前值,第二次使用两个值来设定新值。现在你应该完全能理解下面的代码。

$.playground().registerCallback(function(){

   //Offset all the pane:
   var newPos = (parseInt($("#background1").css("left")) - smallStarSpeed - PLAYGROUND_WIDTH) 
                          % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH;
  $("#background1").css("left", newPos);

  newPos = (parseInt($("#background2").css("left")) - smallStarSpeed - PLAYGROUND_WIDTH)
                     % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH;
  $("#background2").css("left", newPos);

  newPos = (parseInt($("#background3").css("left")) - mediumStarSpeed - PLAYGROUND_WIDTH)
                     % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH;
  $("#background3").css("left", newPos);

  newPos = (parseInt($("#background4").css("left")) - mediumStarSpeed - PLAYGROUND_WIDTH)
                     % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH;
  $("#background4").css("left", newPos);

  newPos = (parseInt($("#background5").css("left")) - bigStarSpeed - PLAYGROUND_WIDTH)
                     % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH;
  $("#background5").css("left", newPos);

  newPos = (parseInt($("#background6").css("left")) - bigStarSpeed - PLAYGROUND_WIDTH)
                     % (-2 * PLAYGROUND_WIDTH) + PLAYGROUND_WIDTH;
  $("#background6").css("left", newPos);
}, REFRESH_RATE);

这就是我们添加了6个精灵后的结果,是不是很炫?

2.3 游戏玩家

对于背景,我们并没有使用任何动画,仅仅是张静态图片。但对于“玩家的飞船”,我们得用上所谓真正的动画了。做法是给用户提供飞船的可视化反馈,就像他们想的那样:向上、向下、向左、向右。为了实现这样的效果,我们需要用来展现飞船的静态图片和展现喷射器效果的动画。在移动飞船的时候,为了使精灵切换之间看起来具有无关性,我们把它们统统放到同一组里。

在gameQuery,动画的所有帧都包含在同一张图片中,有点类似电影的感觉。这些帧既可以一帧挨着一怔(水平方式)也可以一帧在另一帧之上(垂直方式)。如果精灵的每帧大小都相同,排列方式可以任选,但如果精灵以某种排列方式比动画大,那么就只好选另一中排列方式了。当你在gameQuery中创建多帧动画,你需要提供帧的数量,帧之间的距离(delta),帧之间的时间间隔(rate),以及动画的类型(例如垂直动画还是水平动画)。

// Player spaceship animations:

playerAnimation["idle"] =               new $.gameQuery.Animation({imageURL: "player_spaceship.png"});
playerAnimation["explode"] =         new $.gameQuery.Animation({imageURL: "explode.png"});
playerAnimation["up"] =                 new $.gameQuery.Animation({imageURL: "boosterup.png", 
        numberOfFrame: 6, delta: 14, rate: 60, 
        type: $.gameQuery.ANIMATION_HORIZONTAL});
playerAnimation["down"] =           new $.gameQuery.Animation({imageURL: "boosterdown.png", 
        numberOfFrame: 6, delta: 14, rate: 60, 
        type: $.gameQuery.ANIMATION_HORIZONTAL});
playerAnimation["boost"] =           new $.gameQuery.Animation({imageURL: "booster1.png" , 
        numberOfFrame: 6, delta: 14, rate: 60, 
        type: $.gameQuery.ANIMATION_VERTICAL});
playerAnimation["booster"] =        new $.gameQuery.Animation({imageURL: "booster2.png"});

// Initialize the background
$.playground().addGroup("actors", {width: PLAYGROUND_WIDTH, height: PLAYGROUND_HEIGHT})
        .addGroup("player", {posx: PLAYGROUND_WIDTH/2, posy: PLAYGROUND_HEIGHT/2,
              width: 100, height: 26})
          .addSprite("playerBoostUp", {posx:37, posy: 15, 
              width: 14, height: 18})
          .addSprite("playerBody",{animation: playerAnimation["idle"], 
              posx: 0, posy: 0, width: 100, height: 26})
          .addSprite("playerBooster", {animation:playerAnimation["boost"], 
              posx:-32, posy: 5, width: 36, height: 14})
          .addSprite("playerBoostDown", {posx:37, posy: -7,
              width: 14, height: 18});

表现飞船后推进器的精灵需要有3中状态:常态(中等火力)、关闭(飞船向后退)以及全开(飞船向前进)。还需要另外两个精灵,分别是飞船上推进器、下推进器,它们各自具有开、关两个动画。随着用户键盘输入,这些动画被触发。jQuery通过事件监听器(event listener),能够很方便的实现对用户键盘操作的回应。当特定的事件发生,你可以设定执行对应的函数,例如鼠标点击、按下按键。你可能注意到,在创建某些精灵的时候,我们并没有为其设定动画,那是因为它们现在仅仅是占个位置,当需要动画的时候,我们仍然可以设置。

现在让我们看看“键盘”是如何与“动画”绑定在一起的。这里我们发现了在浏览器中运行javascript游戏的小小不便之处:上/下/左/右键基本都是被用来滚动页面的。所以如果你的游戏超过了浏览器窗口大小,那么按下这些键还会使页面发生滚动。虽然我们可以通过编程手段屏蔽页面滚动,但我仍然认为干涉浏览器动作并不是个好主意,所以我使用了别的键来控制游戏。当然最好的方式应该是让用户自己定义按键,这并非难事,不过我还是想让这篇教程尽量简单。

//this is where the keybinding occurs

$(document).keydown(function(e){
  switch(e.keyCode){
    case 65: //this is left! (a)
      $("#playerBooster").setAnimation();
      break;
    case 87: //this is up! (w)
      $("#playerBoostUp").setAnimation(playerAnimation["up"]);
      break;
    case 68: //this is right (d)
      $("#playerBooster").setAnimation(playerAnimation["booster"]);
      break;
        case 83: //this is down! (s)
      $("#playerBoostDown").setAnimation(playerAnimation["down"]);
      break;
  }
});
//this is where the keybinding occurs
$(document).keyup(function(e){
  switch(e.keyCode){
    case 65: //this is left! (a)
      $("#playerBooster").setAnimation(playerAnimation["boost"]);
      break;
    case 87: //this is up! (w)
      $("#playerBoostUp").setAnimation();
      break;
    case 68: //this is right (d)
      $("#playerBooster").setAnimation(playerAnimation["boost"]);
      break;
    case 83: //this is down! (s)
      $("#playerBoostDown").setAnimation();
      break;
  }
});

当键盘按下或者松开,相应的动画就发生。有时候我们需要移除某个动画,而不是用另一个动画来替代,为了实现这种效果,我们使用不带参数的setAnimation方法。为了找出你想用来作为游戏按键的按键代码(keyCode),你可以使用这个页面。飞船的爆炸效果将会在步骤3中讲解。

下面是结果,不过别忘了这会儿我们只关注游戏精灵动画,所以飞船并不会动。

2.4 敌人和导弹

我们的每个敌人都需要带上两个动画,常态和爆炸态。敌人的行动和等级将在下一步骤中讨论。这里接触到的新玩意儿是ANIMATION_CALLBACK类型。回调(callback)是指将来会被调用的函数。在这里是指当前动画结束的时刻。我们使用这种类型来表现爆炸:当爆炸动画播放完成之后我们移除精灵(在步骤3中会详细讨论具体操作方法)。

当你需要为动画指定两个或者更多的类型时,你可以使用“|”符号来分隔。例如,你的动画可以有这样的类型:ANIMATION_CALLBACK | ANIMATION_ONCE | ANIMATION_VERTICAL,如此这般,动画就会垂直方式播放一次,播放结束时执行回调函数!这对于想在两段循环播放动画之间增加过渡动画来说是很方便的。在这种情况下,你可以通过回调函数改变循环播放的动画(但在这篇教程中我们并不需要)。

var enemies = new Array(3); // There are three kind of enemies in the game

//...

///  List of enemies animations :
// 1st kind of enemy:
enemies[0] = new Array(); // enemies have two animations
enemies[0]["idle"]          = new $.gameQuery.Animation({imageURL: "minion_idle.png", numberOfFrame: 5,
   delta: 52, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL});
enemies[0]["explode"]  = new $.gameQuery.Animation({imageURL: "minion_explode.png", numberOfFrame: 11,
   delta: 52, rate: 30, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK});

// 2nd kind of enemy:
enemies[1] = new Array();
enemies[1]["idle"]            = new $.gameQuery.Animation({imageURL: "brainy_idle.png", numberOfFrame: 8, 
   delta: 42, rate: 60, type:$.gameQuery.ANIMATION_VERTICAL});
enemies[1]["explode"]    = new $.gameQuery.Animation({imageURL: "brainy_explode.png", numberOfFrame: 8,
   delta: 42, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK});

// 3rd kind of enemy:
enemies[2] = new Array();
enemies[2]["idle"]          = new $.gameQuery.Animation({imageURL: "bossy_idle.png", numberOfFrame: 5,
   delta: 100, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL});
enemies[2]["explode"]  = new $.gameQuery.Animation({imageURL: "bossy_explode.png", numberOfFrame: 9,
   delta: 100, rate: 60, type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK});

// Weapon missile:
missile["player"] = new $.gameQuery.Animation({imageURL: "player_missile.png", numberOfFrame: 6,
   delta: 10, rate: 90, type: $.gameQuery.ANIMATION_VERTICAL});
missile["enemies"] = new $.gameQuery.Animation({imageURL: "enemy_missile.png", numberOfFrame: 6,
   delta: 15, rate: 90, type: $.gameQuery.ANIMATION_VERTICAL});
missile["playerexplode"] = new $.gameQuery.Animation({imageURL: "player_missile_explode.png",
   numberOfFrame: 8, delta: 23, rate: 90, 
   type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK});
missile["enemiesexplode"] = new $.gameQuery.Animation({imageURL: "enemy_missile_explode.png",
   numberOfFrame: 6, delta: 15, rate: 90, 
   type: $.gameQuery.ANIMATION_VERTICAL | $.gameQuery.ANIMATION_CALLBACK});

这就是敌人以及导弹的动画!在下一步骤里,我们讲解这个游戏的对象模型(object model)。保持更简单的javascript和更少的gameQuery。

第3章 步骤2 – 对象模型

3.1 Javascript中的对象

Javascript并不是一种典型的面向对象编程语言。如果你使用过其他面向对象语言的话,请记住,在javascript中,面向对象稍微有些特殊。不过,如果你是个面向对象的新手的话,这应该不会困扰你吧。我并不会深入讲解,要是你觉得有必要的话,可以看看javascript面向对象的教程与文档

面向对象编程是一种重新组织代码的方式,它把逻辑上相关的东西合为一体,你可以设定向用户公开的或者关闭的代码部分(封装性)。类(class)是这样的东西:它描述了实体特性(属性attributes),实体怎样对外开放(方法methods)。类的一个唯一实体被称为对象(object)。对象是类的实例化。当你实例化一个类的时候,你其实调用了一个称为构造器(constuctor)的方法。为了创建类的实例,你得用到关键字new,就像你在步骤1中创建动画一样。

var myObject = new myClass(some, important, arguments);

在javascript中,对象其实是函数(function)的实例,对的,你没有看错,就是函数!所以当你定义类的时候,看起来就像是在写一个函数。事实上,当你在javascript中创建类的时候,你写的是它的构造器。一个简单的类看起来应该是这个样子:

var myClass() = function(var some, var important, var arguments){

   this.someVisilbeAttribute = some + arguments;
   someLessVisiblAttribute = important + arguments;
   this.tickleMe = function(var name){
      alert("hello "+name+" you should now that "+this.someVisibleAttribute);
   };
   return true;
}

这段代码定义了一个叫myClass的类,它有两个属性,分别是someVisibleAttribute、someLessVisibleAttribute;一个方法,叫做tickleMe。与此同时,我们也定义了构造器的工作方式,用构造器参数定义对象的属性。/p>

就像你在例子中所看到的那样,存在着很多定义类属性的方式。当使用var关键字的时候,你在对象中创建了一个本地变量,这意味着对象中任何执行性的代码都可以通过属性名引用到具体属性。通过使用this关键字,对象属性也能被外界所以引用到。但这并不意味通过var关键字定义的属性不能被外界所引用,而是这稍嫌麻烦,而且拐弯抹角。下面的代码展示了如何从外界引用对象的可见属性:

myObject.someVisibleProperty = “a new value”;

alert(myObject.someVisibleProperty)

而且你需要知道在构造器中的参数变量可以被对象中的可执行性代码所引用,就好像它们已经通过var声明过一样。

你可以在对象实例化之后改变它,增加新属性或者方法。当这么做的时候,你需要考虑是要改变类的所有对象还是仅仅这一个对象。下面的例子展现的是后者。

myObject.lastMinuteMethode = function(){/*Do something here*/};

myObject.aNewProperty = "aValue";

有时你并不只是想改变这一个对象,还想改变它的兄弟姐妹对象“sibing”。为了实现这样的效果,可以使用对象的原型(prototype)。原型就像在对象中对类进行定义,可以通过.prototype属性。任何相同类的实例们都共享同一个原型。所以当你在一个对象的原型中做了什么,其他实例也受影响。

var firstObject = new myClass(1,2,3);

var secondObject = new myClass(4,5,6);
firstObject.prototype.aNewMethode = function(){alert("I'm new");}
secondObject.aNewMethode();

3.2 Javascript中的继承

当一个类继承另一个类,子类接受了父类的所有属性、方法。然后子类可以增加、重写某些方法来使其不同于其他类。根据你所使用的语言的不同,继承会有不同的形式和约束。对于javascript来说,并没有提供语言级别上的支持,不过很多爱钻牛角尖的家伙还是让它出现了。所以选择哪种方式,取决于你想用到继承的哪些特性。

在这篇教程中我们选择一种非常简单且具约束性的javascript继承形式。我认为再增加更多可能会导致难以理解。这种方式是在类中注入另一个类的实例。然后你课余==可以通过修改原型(prototype)来扩展类,而不会影响父类。

function inheritingClass(){};

inheritingClass.prototype = new myClass();
inheritingClass.prototype.soneOnlyMethode = function(){/*Do something here*/};
inheritingClass.prototype.tickleMe = function(){/*Change the behavior of the parent methode*/};

为了实现继承,这就是所以你需要了解的东西。但是如果你想要更复杂的编码方式,或者用别的面向对象语言实现的话,你应该更加深入的去了解javascript继承。

3.3 玩家对象

玩家就是只有一个实例的简单类(因为这是个单人游戏嘛),所以在这里使用面向对象只会使得代码更易阅读。

在玩家对象中,我们需要有属性来描述玩家飞船状态,例如飞船是否还有护盾、还有命?同时,飞船的基本动作可以通过对象方法实现。因为在这个游戏里,我们不需要物理引擎来管理飞船移动,你仅仅只需增加/减少位置,我选择不使用任何方法来管理飞船的移动。

第一个方法damage(),用来判断“损伤”事件。当导弹击中飞船时,我们调用这个方法,并根据返回值判断飞船是否该“死”了。对外界来说,这个方法屏蔽了对护盾的管理。如果你想知道variabl–含义,它代表variable = variable – 1,这也适用于其他操作符。

function Player(node){

   this.node = node;
  //this.animations = animations;

  this.grace = false;
  this.replay = 3; 
  this.shield = 3; 
  this.respawnTime = -1;

  // This function damage the ship and return true if this cause the ship to die 
  this.damage = function(){
    if(!this.grace){
      this.shield--;
      if (this.shield == 0){
        return true;
      }
      return false;
    } 
    return false;
  };
//...

第二个方法叫做respawn(),它所关注的是当飞船死亡(之间的damage()方法返回true),而且玩家仍然有剩余的“生命”时所发生的事。在这种情形下,飞船护盾必须重新生成,并且飞船会处于敌人无法攻击到的无敌(grace)模式。无敌模式只能持续3秒钟,我们需要测定时间以便得知何时该启动无敌模式,何时该关闭无敌模式,这就是代码this.respawnTime = (new Date()).getTime();所做的事情。

为了让玩家知道飞船正处于无敌(grace)模式,我们使飞船变得稍微透明,jQuery的fadeTo()方法可以实现。这个方法的第一个参数是变成指定透明度所花费的时间(这里因为是即使生效所以0ms),第二个参数是我们想要得到的透明度(1代表完全不透明,0代表不可见)。

//…

  // this try to respawn the ship after a death and return true if the game is over
  this.respawn = function(){
    this.replay--;
    if(this.replay==0){
      return true;
    }

    this.grace             = true;
    this.shield              = 3;

    this.respawnTime = (new Date()).getTime();
    $(this.node).fadeTo(0, 0.5); 
    return false;
  };
//...

玩家类的最后部分是检测无敌模式是否结束。这个方法必须在每个时隙都被调用。、

//…

  this.update = function(){
    if((this.respawnTime > 0) && (((new Date()).getTime()-this.respawnTime) > 3000)){
      this.grace = false;
      $(this.node).fadeTo(0, 1); 
      this.respawnTime = -1;
    }
  }

  return true;
}

3.4 敌人对象

存在3中类型的敌人,它们具有共同的祖先。我们在祖先类中定义敌人的默认共同行为,在继承类中定义特殊方法。因为我想在之后调用方法的时候不必考虑对象到底是哪个类的实例,所以我们在子类中只重写那些已经存在的方法。就像玩家类一样,敌人类也需要管理“损伤“的方法。你应该会注意到,其实这个方法和玩家类的一模一样,除了检测无敌(grace)模式之外。

function Enemy(node){

  this.shield                  = 2;
  this.speedx                = -5;
  this.speedy                = 0;
  this.node = $(node);

  // deals with damage endured by an enemy
  this.damage = function(){
    this.shield--;
    if(this.shield == 0){
      return true;
    }
    return false;
  };
//...

敌人具有某些符合基本逻辑的自动行为。我们需要一些飞来使敌人移动。这里我们有一个叫update()的方法,它会调用在每个方向轴上移动的函数。这使子类可以轻松重写其中任何一个。你可以发现敌人的默认行为是在每个方向轴上以一定的速度移动。如果你仔细看updateX和updateY方法的话,你会发现其实背景的移动也采用了同样的方式。

下图是敌人的家族树(family tree)。我们使用继承的目标是减少代码冗余。所以如果;两个对象拥有同一行为,我们让它们从另一对象继承。

3.4.1 导弹

导弹与默认祖先敌人的差别并不大,导弹是想在屏幕中尽量停留而已。所以代码相当直观:

function Minion(node){

  this.node = $(node);
}
Minion.prototype = new Enemy();

Minion.prototype.updateY = function(playerNode){
  var pos = parseInt(this.node.css("top"));
  if(pos > (PLAYGROUND_HEIGHT - 50)){
    this.node.css("top",""+(pos - 2)+"px");
  }
}

3.4.2 有头脑的敌人

对于有头脑的敌人,我们需要给它们配置更多护盾,我们只需在构造器中重新定义这个值即可。定义在子类中的同名值会重写父类,在构造器中定义的值则重写子类和父类。这种类型的敌人稍具智能,它们尝试躲避玩家。为了实现这个我们重写updateY方法,使得垂直速度增加,或者远离玩家的相对位置。这里也没有任何新的东西。注意“有头脑的敌人”类可不是继承至导弹(Minion)类而是敌人(Enemy)类。

function Brainy(node){

  this.node   = $(node);
  this.shield = 5;
  this.speedy = 1;
  this.alignmentOffset = 5;
}
Brainy.prototype = new Enemy();
Brainy.prototype.updateY = function(playerNode){
  if((this.node[0].gameQuery.posy+this.alignmentOffset) > $(playerNode)[0].gameQuery.posy){
    var newpos = parseInt(this.node.css("top"))-this.speedy;
    this.node.css("top",""+newpos+"px");
  } else if((this.node[0].gameQuery.posy+this.alignmentOffset) < $(playerNode)[0].gameQuery.posy){
    var newpos = parseInt(this.node.css("top"))+this.speedy;
    this.node.css("top",""+newpos+"px");
  }
}

3.4.3 boss型敌人

boss型敌人的行为与同有头脑的敌人一样,它们躲避玩家。不同的是:它们不会朝玩家前进,而是垂直移动,玩家得花些时间才能杀死它们(因为它们的护盾很厚)。

function Bossy(node){

  this.node   = $(node);
  this.shield = 20;
  this.speedx = -1;
  this.alignmentOffset = 35;
}
Bossy.prototype = new Brainy();
Bossy.prototype.updateX = function(){
  var pos = parseInt(this.node.css("left"));
  if(pos > (PLAYGROUND_WIDTH - 200)){
    this.node.css("left",""+(pos+this.speedx)+"px");
  }
}

敌人的生成、破坏和发射导弹并不需要在对象中设定,请看下一步骤。

第4章 步骤3 – 游戏逻辑与控制

4.1 游戏状态

每一个稍微复杂点的游戏都需要指定一系列的规则来决定哪些动作是允许或允许发生的。在这个游戏里,我们的游戏规则十分简单,但如果你想看更复杂的例子的话,看这个演示程序。我需要3种信息:

另外还有一个我们曾经见过的隐藏的游戏规则,飞船对象的无敌(grace)变量。

为了存储上述3种信息,我们使用3个布尔变量(飞船的无敌也是如此)。其实我们可以把变量减少到2个,因为boss模式并不和其他同事发生,但却使代码不易阅读(当然,如果你执意要这么做的话,一个变量也是可以的,但那时我们的游戏规则会难以理解)。

4.2 敌人的产生

为了产生敌人,我们注册一个回调函数。它每秒都会被调用,然后随机的决定是否创建新敌人。为了实现随机效果,我们使用Math.random()方法产生一个伪随机数,伪随机数平均的分布于0到1之间(javascript包含了一些预定义的内置函数)。我们给新创建的精灵定义一个名字,为了生成一个几乎唯一的名字,我们用到两个函数。Math.random()和Math.ceil()函数。Math.ceil返回的是所给浮点数的近似整数值。联合上述两个函数,我们可以生成0到1000之间的随机整数。当然这并不是说,在同一时刻,两个精灵的id永远不会一样,而是说这很少见。

一旦通过addSprite()方法创建了新敌人,我们需要立即使用它,把它和之前定义的敌人类的实例相关联。一个好的做法是保持对象集合的分离因为这使得代码看起来更清洁。当然每次选择对象节点是要花点时间,但这却便于调试。为了通过jQuery选择器往指定节点存储信息,必须使用[0]符号。当只有一个一个对象的时候,这是很有意义的。【var s = $(“div”) jQuery对象默认都有个0索引,s为jQuery对象,s[0]为dom对象,可以使用dom对象的所有属性方法】

//This function manage the creation of the enemies

$.playground().registerCallback(function(){
  if(!bossMode && !gameOver){
    if(Math.random() < 0.4){
      var name = "enemy1_"+Math.ceil(Math.random()*1000);
      $("#actors").addSprite(name, {animation: enemies[0]["idle"], 
          posx: PLAYGROUND_WIDTH, posy: Math.random()*PLAYGROUND_HEIGHT,
          width: 150, height: 52});
      $("#"+name).addClass("enemy");
      $("#"+name)[0].enemy = new Minion($("#"+name));
    } else if (Math.random() > 0.5){
      var name = "enemy1_"+Math.ceil(Math.random()*1000);
      $("#actors").addSprite(name, {animation: enemies[1]["idle"],
          posx: PLAYGROUND_WIDTH, posy: Math.random()*PLAYGROUND_HEIGHT,
          width: 100, height: 42});
      $("#"+name).addClass("enemy");
      $("#"+name)[0].enemy = new Brainy($("#"+name));
    } else if(Math.random() > 0.8){
      bossMode = true;
      bossName = "enemy1_"+Math.ceil(Math.random()*1000);
      $("#actors").addSprite(bossName, {animation: enemies[2]["idle"],
          posx: PLAYGROUND_WIDTH, posy: Math.random()*PLAYGROUND_HEIGHT,
          width: 100, height: 100});
      $("#"+bossName).addClass("enemy");
      $("#"+bossName)[0].enemy = new Bossy($("#"+bossName));
    }
  } else {
    if($("#"+bossName).length == 0){
      bossMode = false;
    }
  }

}, 1000); //once per seconds is enough for this

我们让敌人先出现在屏幕右边的不可见区域。jQuery中的addClass()方法可以把参数值指定为精灵的类名。为敌人精灵添加同一类名,可以方便修改位置,检测碰撞。当我们生成一个boss类型的敌人后,我需要设置合适的游戏状态来描述;为了检测boss型敌人是否已经被消灭,我们也需要存储它的名字。请注意,我是如何巧妙的在非boss模式下生成敌人,又是如何在boss模式下复位游戏到正常模式的。

4.3 敌人的行为

为了让敌人能够行动,我们已经完成了大部分的工作准备了,现在仅仅需要调用精灵节点对象上的call()方法。为了实现这个我们注册了一个每30毫秒就执行一次的回调函数(callback)。在回调函数中,我们通过jQuery选择了所以类名叫enemy的精灵。然后通过each()函数对每个选择到的精灵执行同一方法。当然游戏没结束之前我才需要这么做。

$.playground().registerCallback(function(){

  if(!gameOver){
    //Update the movement of the enemies
    $(".enemy").each(function(){
      this.enemy.update($("#player"));
      var posx = parseInt($(this).css("left"));
      if((posx + 150) < 0){
        $(this).remove();
        return;
      }
      //Test for collisions
      var collided = $(this).collision("#playerBody,.group");
      if(collided.length > 0){
        if(this.enemy instanceof Bossy){
          $(this).setAnimation(enemies[2]["explode"], function(node){$(node).remove();});
          $(this).css("width", 150);
        } else if(this.enemy instanceof Brainy) {
          $(this).setAnimation(enemies[1]["explode"], function(node){$(node).remove();});
          $(this).css("width", 150);
        } else {
          $(this).setAnimation(enemies[0]["explode"], function(node){$(node).remove();});
          $(this).css("width", 100);
        }
        $(this).removeClass("enemy");
        //The player has been hit!
        if($("#player")[0].player.damage()){
          explodePlayer($("#player"));
        }
      }
    });
  }
}, REFRESH_RATE);

我们还得好好想想敌人怎么从左边出去的问题。事实上,它们既回不来也不会再被玩家射中,所以让它们继续留在游戏里还有什么意义呢?jQuery函数remove()会移除那些“没有用处的敌人”,关键字return使得函数在此地停下。函数的第二部分关注的是敌人与玩家飞船的碰撞检测。

gameQuery函数collision返回指定对象之间发生碰撞的个体的列表。参数值是过滤器的作用。当你为其指定参数,collision函数会检测指定元素的碰撞情况。由于碰撞检测是个开销相当大的函数,所以拜托少用点。这里我们检测的是groups与player,gameQuery为那些通过addGroup()方法创建的节点添加了共同的类名group。我们需要对每个组检测,因为player是被嵌套在组当中而返回结果却只是精灵的集合。

在当前版本的gameQuery中,collision函数把精灵认定是标准的盒子形状(这意味着没有一像素一像素的检测过去),所以会发生类似下面那样的情况。我希望更多的选项能在将来的版本中得到实现。

每当碰撞发生的时候,我们需要让敌人爆炸,让玩家掉护盾。为了实现敌人爆炸效果,我们就简单的将敌人动画替换成爆炸动画(记住如果你需要的话,你可以改变精灵的大小)。但爆炸动画只需播放一次,然后就应该被移除。这也是Animation.CALLBACK被被使用的原因!当设置动画的时候,你可以将函数作为第二个参数传入。这个函数将会在动画结束的时候被自动调用。在这个例子当中,我们只是简单的移除节点。在这里有个小陷阱,我们必须把退场精灵从enemy类中移除,否则的话下次函数被调用的时候会把爆炸对象作为一个敌人的。

我们在步骤2中已经写过管理玩家“伤害”的函数了,当函数返回true代表飞船该爆炸了。我们已经将飞船爆炸函数从中分离开来了,因为这段代码还会被使用到几次。

function explodePlayer(playerNode){
          playerNode.children().hide();
          playerNode.addSprite("explosion",{animation: playerAnimation["explode"],
          playerHit = true;
        }

由于玩家是个包含了一系列精灵的组,所以我们不能简单的为其更改动画。我们需要隐藏正常精灵,然后将其替换为新的精灵。jQuery的hide函数会隐藏所有被选中的节点,获取节点是通过jQuery的children方法。然后我们就可以设置正确的游戏状态,以便游戏知道飞船现在正处于爆炸。

4.4 导弹

如果想在按键之后飞船能够发射的话,我们需要用到经典的事件驱动模型。这意味着我们需要为步骤1中的事件监听器增加一段小小的代码来改变飞船动画。

//…

switch(e.keyCode){
  case 75: //this is shoot (k)
    //shoot missile here
    var playerposx = parseInt($("#player").css("left"));
    var playerposy = parseInt($("#player").css("top"));
    var name = "playerMissle_"+Math.ceil(Math.random()*1000);
    $("#playerMissileLayer").addSprite(name,{animation: missile["player"],
        posx: playerposx + 90, posy: playerposy + 14, width: 20, height: 5});
    $("#"+name).addClass("playerMissiles")
    break;
//...

首先是读取飞船的位置信息。然后在导弹(missile)层的正确位置添加精灵。我们使用和敌人精灵同样的方法生成导弹精灵id。还有最后一件事:把这个新创建的精灵添加统一的类名,这对之后检测碰撞来说会很方便。

对于敌人发射导弹的处理,我们需要扩展之前写的update回调函数。只有“有头脑”和boss型敌人才能发射导弹。为了了解当前敌人是否符合,我们使用instanceof操作符。如果操作符左边是右边的实例,就返回true。由于boss型敌人继承了“有头脑”敌人,所以前者也是后者的实例。这里我们再次用到随机数生成器来处理敌人随机发射导弹事件。

      //Make the enemy fire
           if(this.enemy instanceof Brainy){
             if(Math.random() < 0.05){
               var enemyposx = parseInt($(this).css("left"));
               var enemyposy = parseInt($(this).css("top"));
               var name = "enemiesMissile_"+Math.ceil(Math.random()*1000);
               $("#enemiesMissileLayer").addSprite(name,{animation: missile["enemies"],
                   posx: enemyposx, posy: enemyposy + 20, width: 15,height: 15});
               $("#"+name).addClass("enemiesMissiles");
             }
           }

如果你把这段代码与之前的代码片段相比较,你会发现它们几乎是一模一样的。

虽然发射导弹是件简单的事,但别忘了我们还得使导弹碰到玩家后,会对其造成伤害。我们首先看看敌人导弹:这段代码位于一个回调函数当中,并且和之前检测敌人与玩家碰撞代码很相似。对于敌人发射的每个导弹,我们都需要检测它们是否与玩家碰撞。就像之前做的那样,我们调用玩家对象的对应方法来减少护盾,并且检测飞船是否该爆炸了。如果飞船被证实爆炸,我们也想之前所做的为其设置爆炸动画与回调函数。

不要忘记,导弹也和敌人一样,在跑出屏幕的时候需要被移除。

$(“.enemiesMissiles”).each(function(){

  var posx = parseInt($(this).css("left"));
  if(posx < 0){
    $(this).remove();
    return;
  }
  $(this).css("left", ""+(posx-MISSILE_SPEED)+"px");
  //Test for collisions
  var collided = $(this).collision(".group,#playerBody");
  if(collided.length > 0){
    //The player has been hit!
    collided.each(function(){
      if($("#player")[0].player.damage()){
        explodePlayer($("#player"));
      }
    })
    $(this).setAnimation(missile["enemiesexplode"], function(node){$(node).remove();});
    $(this).removeClass("enemiesMissiles");
  }
});

对于玩家导弹,其实也差不多,我们检测敌人类在碰到导弹之后是否设置了正确的动画。不过在最后,你可能会发现一些不可思议的事情:我们不仅改变了精灵的长宽,也改变了它的位置。选取合适的动画能解决精灵在屏幕跳动的异常,就像下图所示。

当然你也可以对所有精灵使用统一的大小。不过比起画图我更偏向使用代码来解决这个问题:)

//Update the movement of the missiles

$(".playerMissiles").each(function(){
  var posx = parseInt($(this).css("left"));
  if(posx > PLAYGROUND_WIDTH){
    $(this).remove();
    return;
  }
  $(this).css("left", ""+(posx+MISSILE_SPEED)+"px");
  //Test for collisions
  var collided = $(this).collision(".group,.enemy");
  if(collided.length > 0){
    //An enemy has been hit!
    collided.each(function(){
        if($(this)[0].enemy.damage()){
          if(this.enemy instanceof Bossy){
            $(this).setAnimation(enemies[2]["explode"], function(node){$(node).remove();});
            $(this).css("width", 150);
          } else if(this.enemy instanceof Brainy) {
            $(this).setAnimation(enemies[1]["explode"], function(node){$(node).remove();});
            $(this).css("width", 150);
          } else {
            $(this).setAnimation(enemies[0]["explode"], function(node){$(node).remove();});
            $(this).css("width", 100);
          }
          $(this).removeClass("enemy");
        }
      })
    $(this).setAnimation(missile["playerexplode"], function(node){$(node).remove();});
    $(this).css("width", 38);
    $(this).css("height", 23);
    $(this).css("top", parseInt($(this).css("top"))-7);
    $(this).removeClass("playerMissiles");
  }
});

4.5 玩家操作

如果游戏没有结束,飞船也没有坠落,我们就允许用户操作飞船的运动。我们需要每隔30毫秒就检测一下用户是否还按着按键,因为飞船只能在按键持续按下的情况下移动。注意这与我们之前在步骤1中看到的keyup、keydown事件有何不同,前者是消极被动,后者是积极主动。Javascript并没有解决这种问题的机制,不过我的gameQuery却提供了!为了激活这个功能,我们需要在指定playground节点的时候就设置参数keyTracker: true。

$(“#playground”).playground({height: PLAYGROUND_HEIGHT,
width: PLAYGROUND_WIDTH, keyTracker: true});

一旦按键记录器激活了,你就可以把你想关注的按键代码作为索引值来读取jQuery.gameQuery.keyTracker数组,这样就能了解指定按键情况。如果得到的值时true,那么指定按键当前正被按下。这能方便你用简单的回调函数管理玩家控制(player controls)。为了控制精灵移动,我们使用同样的技术手段。

// this is the function that control most of the game logic 
          $.playground().registerCallback(function(){
          if(!gameOver){
            //Update the movement of the ship:
            if(!playerHit){
              $("#player")[0].player.update();
              if(jQuery.gameQuery.keyTracker[65]){ //this is left! (a)
                var nextpos = parseInt($("#player").css("left"))-5;
                if(nextpos > 0){
                  $("#player").css("left", ""+nextpos+"px");
                }
              }
              if(jQuery.gameQuery.keyTracker[68]){ //this is right! (d)
                var nextpos = parseInt($("#player").css("left"))+5;
                if(nextpos < PLAYGROUND_WIDTH - 100){
                  $("#player").css("left", ""+nextpos+"px");
                }
              }
              if(jQuery.gameQuery.keyTracker[87]){ //this is up! (w)
                var nextpos = parseInt($("#player").css("top"))-3;
                if(nextpos > 0){
                  $("#player").css("top", ""+nextpos+"px");
                }
              }
              if(jQuery.gameQuery.keyTracker[83]){ //this is down! (s)
                var nextpos = parseInt($("#player").css("top"))+3;
                if(nextpos < PLAYGROUND_HEIGHT - 30){
                  $("#player").css("top", ""+nextpos+"px");
                }
              }
            } else {
            //...

这个时候这些代码对你来说应该很眼熟了吧:),唯一的陷阱就是别忘记飞船不能跑出屏幕。当玩家飞船爆炸,它应该能往下坠落。当飞船坠出屏幕之外并且还有剩余的生命的时候,飞重新生成船;否则游戏结束。为了能重新生成飞船,我们需要移除包含爆炸动画的精灵,重新显示隐藏起来的飞船精灵。这个简单的调用jQuery的show函数即可。不要忘了把飞船摆到正确的位置,重设游戏为正常模式。

    //..

    } else {
      var posy = parseInt($("#player").css("top"))+5;
      var posx = parseInt($("#player").css("left"))-5;
      if(posy > PLAYGROUND_HEIGHT){
        //Does the player did get out of the screen?
        if($("#player")[0].player.respawn()){
          gameOver = true;
          $("#playground").append('<div style="position: absolute; ... >');
          $("#restartbutton").click(restartgame);
          $("#actors,#playerMissileLayer,#enemiesMissileLayer").fadeTo(1000,0);
          $("#background").fadeTo(5000,0);
        } else {
          $("#explosion").remove();
          $("#player").children().show();
          $("#player").css("top", PLAYGROUND_HEIGHT / 2);
          $("#player").css("left", PLAYGROUND_WIDTH / 2);
          playerHit = false;
        }
      } else {
        $("#player").css("top", ""+ posy +"px");
        $("#player").css("left", ""+ posx +"px");
      }
    }
  }
}, REFRESH_RATE);

那么游戏结束的时候我们也该做些什么呢?我们可以显示一条友好的提示,并且慢慢的隐藏游戏内容。为了实现这个我们可以简单的增加hml节点到playground,然后显示我们想显示的。jQuery的append()函数很适合处理这个,它把字符串参数添加到选择元素的末尾。这里我们在playground节点上调用此方法来添包含以下html代码的div层。

<div style="position: absolute; top: 50px; width: 700px;
                                                    color: white; font-family: verdana, sans-serif;">
          <center>
            <h1>Game Over</h1>
            <br>
            <a style="cursor: pointer;" id="restartbutton">Click here to restart the game!</a>
          </center>
        </div>

提供给玩家重启游戏的功能,看起来会更友好。这是个很意义的功能,不过gameQuery并没有提供简单的方法来重设游戏状态。对于我的上个游戏以及这个游戏来说,我用了个小技巧:window.location。这个javascript对象可以操纵浏览器的URL,重新载入页面当然不在话下。这对我们就足够了。

// Function to restart the game:
        function restartgame(){
          window.location.reload();
        };

但“restart”按钮节点被添加,我们需要为其注册页面重载事件。就像我们之前用keydown、keyup那样。这里用到的jQuery函数是click,使用一个函数作为参数,当事件触发的时候就执行这个函数。

厄,这就完了!祝贺你,如果你一直跟着进度看到这里!最后一部分是一系列为了使游戏更完善的小东西。

第5章 步骤4 – 杂七杂八

5.1 游戏信息

让玩家知道他的飞船到底还剩多少护盾、生命应该是很有必要的吧。如果我们有个分数系统的话,最好也能显示给玩家。就像之前的#background,#actors以及其他组,我们创建一个新组(#overlay)来包容这些信息。这里我们不再用任何的gameQuery函数,因为只需要显示指定的文字而已。我们需要在游戏屏幕的右上方创建两个div节点来显示。通过append函数就能实现,因为组实质上(精灵也是)是个简单的div。

$("#overlay").append("<div id='shieldHUD'style='color: white; 
                width: 100px; position: absolute; font-family: verdana, sans-serif;'>
                </div><div id='lifeHUD'style='color: white; width: 100px; position: absolute;
                right: 0px; font-family: verdana, sans-serif;'></div>");

我们需要保持这新信息被更新。为了解决这个,厄……对了,还得用一个回调函数。在这个回调函数当中,我们可以简单的选择这个div,然后用新内容来替代。这里我们没法再用append函数了,因为它是增加内容;我们使用html方法(或者text方法),它用字符串参数来替代原来内容。

$("#shieldHUD").html("shield: "+$("#player")[0].player.shield);
        $("#lifeHUD").html("life: "+$("#player")[0].player.replay);

请记住,gameQuery写的是html节点,不过你仍然可以使用节点上的jQuery方法!

5.2 HTML文件

html文件做了两件事,第一件好是聚合了jQuery、gameQuery以及你自己写的javascript代码。然后它向广大男生女生用户展现了一个不需要任何javascript参与的静态游戏窗口。这里我仅仅展示欢饮窗口以及一个“click here”的文字。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
        <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">
          <head>
            <title>gameQuery - Tutorial 1 - Final result</title>
            <script type="text/javascript" src="../jquery.min.js"></script>
            <script type="text/javascript" src="../jquery.gamequery.js"></script>
            <script type="text/javascript" src="./tutorial.final.js"></script>
            <style type="text/css">
              html,body{
                margin: 0;
                padding: 0;
                background: black;
              }
              #welcomeScreen{
                width: 700px; 
                height: 250px; 
                position: absolute; 
                z-index: 100; 
                background-image: url(logo.png); 
                font-family: verdana, sans-serif;
              }
              #loadingBar{
                position: relative;
                left: 100px; 
                height: 15px; 
                width: 0px; 
                background: red;
              }
            </style>
          </head>
          <body>
          <div id="playground" style="width: 700px; height: 250px; background: black;">
            <div id="welcomeScreen">
              <div style="position: absolute; top: 120px; width: 700px; color: white;">
                <div id="loadingBar" ></div><br />
                <center><a style="cursor: pointer;" id="startbutton">
                  Click here to start!
                </a></center>
              </div>
            </div>
          </div>
          </body>
        </html>

5.3 开始按钮

到这一步,你已经做了所用工作,除了“开始游戏”按钮。为了实现这个,我们只需要使用startGame方法。你可以在你代码的最后部分调用这个方法,这样游戏就能自动开始,但我觉得把何时开始游戏的决定权交给用户更好吧。我们可以通过“cick here”文字调用这个方法,就像之前对“restart”所做的一样。

//initialize the start button
        $("#startbutton").click(function(){
          $.playground().startGame(function(){
            $("#welcomeScreen").fadeTo(1000,0,function(){$(this).remove();});
          });
        })

当游戏初始化完毕,所以图片声音都载入的时候,startGame方法就会被调用。然后我们将欢迎屏幕慢慢移除。

5.4 进度条

“但,大哥,html代码里头的loadingbar是什么东东啊?”你可能会这样问自己。当然这是个好问题,它其实就是个进度条!gameQuery在游戏开始之前会载入所有资源,你可以通过进度条查看资源载入情况。setLoadBar方法允许你在一个div上设置进度条,进度条随着资源载入而逐渐增长。你只需要提供div的名称以及进度条的总长度。

$().setLoadBar("loadingBar", 400);

不过需要提醒你一下,进度指示器不不是非常有效。例如,每个精灵都被认为是同样大小,一个2kb的精灵和300kb的精灵是一样的。对于声音素材,指示就更不如意了,因为没有任何插件可以探测声音载入情况,所以gamQuery只好尽量猜测了,它认为在最后一张图片载入完之后,声音也载入完了(当写此文档的时候,对声音的支持仍然处于实验阶段)。

5.5 结束语

厄,那就这样了。谢谢你能花时间看这个,我希望这能让你确信用javascript写游戏并不是神话!我想强调的是:你在教程中所看到学到的东西或许是开发游戏中最不重要的!当你制作游戏的时候应该关注界面友好以及可玩性!所以我推荐你能花时间在这两个领域上(但我还做到:D)。我非常期待你能用这个js库(或者其他)做出不同寻常的东西。

Fork me on GitHub