Building JavaScript Games for Phones Tablets and Desktop(8)- 游戏对象类型

游戏对象类型

之前的章节里,已经知道如何创造有少量游戏对象的游戏世界,比如大炮和炮弹。同时也知道了它们之间如何交互。比如,炮弹通过大炮的颜色来更换颜色。这章里,在游戏中增加降落的油漆罐。然而,在这之前,不得不重新学习如何在JavaScript中创建和管理对象。我引进类的概念作为一种手段用来一种确定类型的不同对象。然后,应用类的概念到Painter游戏中的其它部分。此外,学会如何在游戏中引进随机性。

创建同一类型的多个游戏对象

截止目前,在painter游戏中你只需要为每个游戏对象创建一个实例。这里只有一个炮弹和一个大炮。这对其它JavaScript代码中的游戏对象同样适用。只有一个Game对象,一个Keyboard对象,一个Mouse对象等等。你通过声明一个空变量然后向里面添加一个或多个变量和方法来创建这些对象。比如ball对象:

var ball = {
};
ball.initialize = function() {
ball.position = { x : 0, y : 0 };
// etc.
};
ball.handleInput = function (delta) {
if (Mouse.leftPressed && !ball.shooting) {
// do something
}
};
// etc.

假设你想能够在painter游戏中同时射出三个炮弹。如果你像截止目前那样创建对象的方法来实现的话,你需要创建两个变量,ball2和ball3,然后把ball中的代码复制粘贴两次。这不是一个好的解决办法,有以下原因。一是复制代码意味着你不得不处理版本管理的问题。比如,如果你发现在update代码中有个Bug?你必须确保改善后的代码粘贴复制到其他ball对象中。如果你忘记复制粘贴其中一个,bug仍然会继续存在。另外一个问题是这种方法不只是简单的比例增加上述的BUG。假设这个游戏中玩家需要同时射出20个炮弹?复制代码20次?也要注意当你JavaScript文件变得越来越大时,加载其文件的时间也会越来越长。最好避免复制代码。最终,重复的代码看起来丑陋,而且让你很难在其中找到你想要的代码。导致过多的鼠标滚动和降低编码效率。

幸运的是,这里有一个好的解决办法。她就是JavaScript编程中叫做原型的东西。原型允许你创造一个对象的一种蓝图,其中包含变量和方法。一旦原型被定义,你可以只有一行代码就创建一个使用此原型的对象!之前其实你已经使用过了,比如:

var image = new Image();

这里,创建了一个image对象,使用了Image原型。

定义一个原型很简单,比如:

function Dog() {
}
Dog.prototype.bark = function () {
console.log("woof!");
};

这里有一个叫做Dog的函数。当这个函数结合new关键字使用后,一个对象就被创建了。JavaScript中的每个函数都有一个包含了与对象相关的信息的prototype,此对象通过new关键字和调用此函数进行创建。这个例子定义了一个bark方法,是Dog原型的一部分。当你创建一个dog对象时,对象中的东西就是原型中的东西。如下:

var lucy = new Dog();

因为lucy对象是通过Dog原型创建的,那么lucy对象也包含有bark方法:

lucy.bark(); // outputs "woof!" to the console

这么做的好处就是现在你可以创建多条可以bark的狗,但是只需要定义一次bark方法:

var max = new Dog();
var zoe = new Dog();
var buster = new Dog();
max.bark();
zoe.bark();
buster.bark();

当然,这本书的目的不是让你成为狗饲养员,而是如何创造游戏。对游戏而言,原型概念非常有用。它允许你把其构造对象的过程和实际对象创建分离开来。

来个练习,应用原型概念创建ball对象。为了完成这个,需要定义一个函数。这个函数叫做Ball,然后为原型添加initialize方法:

function Ball() {
}
Ball.prototype.initialize = function() {
// ball object initialization here
};

在initialize方法中,需要定义与ball对象相关的变量。问题是,还没有创建对象——你只有一个包含了initialize方法的函数和一个原型。因此在initialize方法中,如何引用相关的对象呢?在JavaScript中,this关键字可以做到这些。在方法中,this总是引用的当前方法的对象。使用这个关键字,就可以在initialize方法中添加代码了:

Ball.prototype.initialize = function() {
this.position = { x : 0, y : 0 };
this.velocity = { x : 0, y : 0 };
this.origin = { x : 0, y : 0 };
this.currentColor = sprites.ball_red;
this.shooting = false;
};

现在你可以创建足够多的炮弹并初始化它们:

var ball = new Ball();
var anotherBall = new Ball();
ball.initialize();
anotherBall.initialize();

每当你创建一个新的炮弹,原型中的任何方法就被加载进了这个对象。当用ball对象initialize方法时,this就代表了ball.当是anotherBall时,this引用的就是anotherBall。

你可以让上述的代码更简短些。为什么当Ball自己就是一个函数时还要额外的添加anotherBall方法呢?可以直接在Ball函数中进行初始化:

function Ball() {
this.position = { x : 0, y : 0 };
this.velocity = { x : 0, y : 0 };
this.origin = { x : 0, y : 0 };
this.currentColor = sprites.ball_red;
this.shooting = false;
}

现在当你创建炮弹时,炮弹就被初始化了:

var ball = new Ball();
var anotherBall = new Ball();

并且因为Ball就是一个函数,你甚至可以传递你想要的参数:

function Ball(pos) {
this.position = pos;
this.velocity = { x : 0, y : 0 };
this.origin = { x : 0, y : 0 };
this.currentColor = sprites.ball_red;
this.shooting = false;
}
var ball = new Ball({ x : 0, y : 0});
var anotherBall = new Ball({ x : 100, y : 100});

因为Ball函数的责任是初始化对象,所以此函数也被叫做构造函数。构造函数与原型中的其他方法一起被称作类。当一个对象通过类进行创建,可以说对象来自于那种类。之前的例子中,ball对象是Ball类型,因为使用了Ball的构造函数和原型。类就是一个对象的蓝图,描述了下面两个事情:

  • 类的数据包含了对象的数据。在balls这个例子中,数据包含了坐标,速度,原点,当前颜色和一个变量表明炮弹是否射击。通过,数据在构造函数中进行初始化。
  • 用方法操作数据。在Ball类中,方法就是游戏循环中的一些方法(handleInput, update, draw, and reset)

你可以非常简单的将游戏循环中的方法转换成Ball原型中的方法,简单的替换ball为this。比如,handleInput方法:

Ball.prototype.handleInput = function (delta) {
if (Mouse.leftPressed && !this.shooting) {
this.shooting = true;
this.velocity.x = (Mouse.position.x - this.position.x) * 1.2;
this.velocity.y = (Mouse.position.y - this.position.y) * 1.2;
}
};

看一看Painter5例子中的Ball.js文件,你可以看见Ball类和它的方法。注意我没有添加任何功能进去;我只是通过原型理论定义了炮弹的蓝图。

对象和类的概念非常有用。它们构成了基本的面向对象编程。JavaScript是一种非常灵活的语言,它并没有强制你使用类。你可以只通过函数来写脚本,正如截止目前所做的那样。但是因为类似如此强大并被广泛用于游戏行业中,这本书会尽可能使用它。通过学习如何适当的使用类,你可以设计出更好的软件,即使使用其它任何编程语言。

把构造游戏对象当做游戏世界的一部分

现在知道如何创建类了,那么需要重新思考在哪里构造游戏对象。截止目前,游戏对象被声明为全局变量,因此,它们可以在任何地方获得。比如,这是如何创建cannon对象:

var cannon = {
};
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;
};

在Painter5例子中,下面是Cannon类的构造函数:

var cannon = {
};
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;
};

在ball的update方法中,需要获得当前的炮身的颜色来更新炮弹的颜色,下面是之前实现的方法:

if (cannon.currentColor === sprites.cannon_red)
ball.currentColor = sprites.ball_red;
else if (cannon.currentColor === sprites.cannon_green)
ball.currentColor = sprites.ball_green;
else
ball.currentColor = sprites.ball_blue;

当使用JavaScript的原型定义了一个类,需要替换ball为this(因为没有对象的实例名称)。因此之前的代码修改后如下:

if (cannon.currentColor === sprites.cannon_red)
this.currentColor = sprites.ball_red;
else if (cannon.currentColor === sprites.cannon_green)
this.currentColor = sprites.ball_green;
else
this.currentColor = sprites.ball_blue;

但是当cannon也是用类的构造函数实现的话如何引用cannon对象呢?这里引出两个问题:

  • 游戏对象在哪里被构造?
  • 如何引用那些不是全局变量的游戏对象?

逻辑上,当游戏世界被构造时游戏对象就应该被构造。这就是为什么Painter5的例子中在PainterGameWorld类创建游戏对象(之前是PainterGameWorld对象)。下面是这个类构造函数的一部分:

function PainterGameWorld() {
this.cannon = new Cannon();
this.ball = new Ball();
// create more game objects if needed
}

因此,这回答了第一个问题。但是引出了另一个问题。当游戏世界创建时游戏对象被创建,在哪里调用PainterGameWorld的构造函数创建游戏世界?如果你打开Game.js文件,会发现又一个用到了原型理论的类:Game_Singleton。下面是构造函数:

function Game_Singleton() {
this.size = undefined;
this.spritesStillLoading = 0;
this.gameWorld = undefined;
}

如上所示,这个类就是之前章节里用于组合游戏对象的类。 Game_Singleton类有一个initialize方法,用来创建游戏世界:

Game_Singleton.prototype.initialize = function () {
this.gameWorld = new PainterGameWorld();
};

好了,现在知道游戏世界在哪里被构造了。但是Game_Singleton对象的实例在哪里被构造呢?你需要这个实例来进入游戏世界。反过来也可以让你获取游戏对象。如果看到Game.js文件的最后一行:

var Game = new Game_Singleton();

最后一个宣布一个变量!通过Game这个变量,可以进入游戏世界和游戏对象里,可以获得游戏世界中的游戏对象。比如,下面是如何得到cannon对象:

Game.gameWorld.cannon

你也许会问为什么这么复杂?为什么不像之前那样把每个游戏对象声明成全局变量?这里有几个原因。首先,在不同的地方声明许多的全局变量,你的代码会变得难以复用。假设你想在其他的游戏中使用Painter例子中的大炮和炮弹。你不得不筛选这些全局变量看是否对你的游戏有用。最好在一个地方声明这些变量(比如PainterGameWorld类),这样声明很容易被找到。

第二个原因是使用大量的全局变量会让一切结构和变量之间的关系分开。在Painter游戏中,很明显大炮和炮弹是游戏的一部分。如果你明确的通过游戏对象时游戏的一部分来表达它们之间的关系的话,你的代码将非常易于理解。

一般的,尽可能的避免使用全局变量。在Painter游戏里,主要的全局变量是Game。这个变量是一个树状结构,包含了游戏世界,同时也包含了游戏对象,当然也包含了其他变量。

使用新的Game对象结构,你就可以获取cannon对象来获取想要的颜色,如下:

if (Game.gameWorld.cannon.currentColor === sprites.cannon_red)
this.currentColor = sprites.ball_red;
else if (Game.gameWorld.cannon.currentColor === sprites.cannon_green)
this.currentColor = sprites.ball_green;
else
this.currentColor = sprites.ball_blue;

有时候在草稿上画出游戏对象的树形结构非常有用。或者创建一个图表。但你的游戏变得更复杂时,那样的树形结构会提供一个非常有用的概览,哪些对象属于哪些对象。并且它可以帮助你节省时间来进行再创造。

写一个类的多个实例

注意到你可以用同一类型构造多个对象,让我们在Painter游戏中添加几个油漆罐。这些油漆罐需要一个随机的颜色,然后它们从屏幕顶端降落下来。一旦它们降落屏幕的底部,为它们分配一个新的颜色并重新从顶部降落。对玩家来说,看起来好像每次有不同的油漆罐降落。实际上,只需要三个油漆罐对象来进行复用。在PaintCan类中,定义一个油漆罐和它的表现形式。然后就可以创建这个类的多个实例。在PainterGameWorld类中,通过三个不同的成员变量来储存着三个实例,声明和实现都在PainterGameWorld构造函数中:

function PainterGameWorld() {
this.cannon = new Cannon();
this.ball = new Ball();
this.can1 = new PaintCan(450);
this.can2 = new PaintCan(575);
this.can3 = new PaintCan(700);
}   

PaintCan类和其他Ball类和Cannon类不同的地方在于油漆罐具有不同的位置。这就是为什么当油漆罐被构造是需要传递一个坐标值作为参数。这个参数值是油漆罐想要的X坐标值,Y坐标值没有被考虑,因为它会根据y方向的速度进行计算。为了让事情看起来更有趣些,让油漆罐以不同的随机的速度降落(下节会讲如何实现)。为了计算这个速度,你需要知道油漆罐降落的最小速度,因为不能降落太慢。所以添加一个minVelocity变量来储存最小速度。下面是PaintCan类的构造函数:

function PaintCan(xPosition) {
this.currentColor = sprites.can_red;
this.velocity = new Vector2();
this.position = new Vector2(xPosition, -200);
this.origin = new Vector2();
this.reset();
}

跟大炮和炮弹一样,油漆罐有一个确定的颜色。默认使用红色。最初,将油漆罐的Y坐标设置在屏幕之外,所以在游戏开始后能看见它降落。在PainterGameWorld构造函数中,调用三次构造函数来创建三个PaintCan对象,每个都有不同的X坐标。

因为油漆罐不处理任何输入,不需要为这个类添加handleInput方法。然而,油漆罐需要更新。其中之一就是你想油漆罐随机速度降落。但是这一切怎么实现呢?

处理游戏中的随机性

油漆罐最重要的表现之一就是它的某些方面是不可预知的。你不想它们以一个预定的时间或速度降落。你想添加一个随机数因子,这样一旦玩家开始游戏时,游戏都是不一样的。当然,随机性需要在控制范围之内,你不会想让一个油漆罐3个小时才降落下来而另一个1毫秒就就降落下来。速度是随机的,但是需要在一个可游戏的范围内。

随机性是什么意思呢?一般来说,游戏中或者其他应用中的随机事件或者随机值都由一个随机数产生器管理。在JavaScript中,Math对象有一个random方法。你也许会好奇:计算机如何产生一个完整的随机数?随机性真实存在吗?随机性是不是就是一种你完全不能预料的表现而因此被叫做“随机”?好了,我们不要这么哲学了。在游戏世界和计算程序中,你可以为即将发生的事做出精确的语言,因为计算机是人来告诉它做什么的。因此,严格意义上说,计算机没有能力产生一个完全的随机数。一个方式是你可以伪装通过预先定义的一个非常大的数来产生一个随机数,因为你没有真正的产生随机数,所以这个叫做虚拟随机数发生器。许多的随机数发生器可以产生在一个范围的数,比如0到1之间,但是也可以产生一个任意的数或者一个其它范围的数。这个范围内的数产生的概率都是一样的。在统计学中,这被叫做均匀分布。

假设当你开始游戏时,通过一个表产生一个“随机”数。因为这个表没有改变,每次你玩游戏的时候,随机数产生的序列都是一样的。为了避免这个问题,你可以在开始时指定表中一个不同的位置来开始随机数的产生。表中开始的位置也被叫做随机数发生器的种子。通常,每次启动程序时随机数种子都是不一样的。

如何使用随机数发生器在游戏里创造随机性呢?比如当玩家每次通过一扇门时有75%的概念出现敌人。这种情况下,产生一个0到1的随机数。如果这个数小于等于0.75,出现敌人,否则不出现。因为均匀分布的关系,这能实现你想要的效果。实现代码如下:

var spawnEnemyProbability = Math.random();
if (spawnEnemyProbability >=0.75)
// spawn an enemy
else
// do something else

如果你想产生一个0.5到1的随机速度。首先产生0到1的随机数,然后除以2,加上0.5:

var newSpeed = Math.random()/2 + 0.5;

计算一个随机的速度和颜色

每次油漆罐落下时,想让它们有一个随机的速度和颜色。可以使用Math.random方法。首先实现随机速度,为了整洁性,在PaintCan类中单独实现一个计算随机速度的方法calculateRandomVelocity.使用成员变量minVelocity来定义油漆罐下落的最小速度。在reset方法中给这个变量一个初始值,然后在构造函数中进行调用。

PaintCan.prototype.reset = function () {
this.moveToTop();
this.minVelocity = 30;
};

calculateRandomVelocity方法的实现用到了minVelocity变量:

PaintCan.prototype.calculateRandomVelocity = function () {
return { x : 0, y : Math.random() * 30 + this.minVelocity };
};

这个方法通过一条语句返回一个速度的对象。速度在X方向上是0,因为油漆罐不能水平移动。y方向上的速度利用随机数发生器。乘以30然后再加上最小速度值。这样范围就在minVelocity和minVelocity+30之间了。

为了计算出随机颜色,也要使用随机数发生器。但是颜色的范围很小,只有少数几个选项(红绿蓝)。问题是Math.random产生的随机数在0和1之间。你希望产生的随机数是整数0,1,2这样。然后用if语句处理不同的情况。幸运的是,Math.floor方法可以实现这种要求。Math.floor方法返回小于传递参数的最大整数值。比如:

var a = Math.floor(12.34); // a will contain the value 12
var b = Math.floor(199.9999); // b will contain the value 199
var c = Math.floor(-3.44); // c will contain the value -4

这下就可以实现产生整数0,1,2的要求了:

var randomval = Math.floor(Math.random() * 3);

使用这个方法,就可以通过if语句来选择油漆罐的颜色了。这一切在calculateRandomColor方法中实现:

PaintCan.prototype.calculateRandomColor = function () {
var randomval = Math.floor(Math.random() * 3);
if (randomval == 0)
return sprites.can_red;
else if (randomval == 1)
return sprites.can_green;
else
return sprites.can_blue;
};

更新油漆罐

PaintCan类中的update方法至少要做以下事情:

  • 在油漆罐没下落之前设置好速度和颜色
  • 通过速度更新油漆罐的位置
  • 检测油漆罐是否完全落下,并且进行复位重置

对上述第一个要实现的事情来说。可以用If语句来检测油漆罐是否移动(速度为0)。此外,当油漆罐出现时,你想引进一些不可预测的事情。为了实现这种效果,但随机数的范围小于0.01的时候分配一个随机速度和颜色。因为均匀分布的原因,只有百分之一的机会数才会小于0.01。结果就是,if语句只在某些情况下才执行,即使油漆罐的速度为0.在if语句中,使用之前的两个方法产生随机速度和颜色:

if (this.velocity.y === 0 && Math.random() < 0.01) {
this.velocity = this.calculateRandomVelocity();
this.currentColor = this.calculateRandomColor();
}

同样需要随着时间过去,通过当前速度更新油漆罐位置,跟炮弹类似:

this.position.x = this.position.x + this.velocity.x * delta;
this.position.y = this.position.y + this.velocity.y * delta;

现在你初始化了油漆罐和更新了位置,你需要处理特殊情况。对油漆罐而言,你需要检测它是否掉落到游戏边界之外。如果是那样的话,就对油漆罐进行重置。好的事情是你已经写了一个方法来检测一个位置是否在游戏边界之外:PainterGameWorld类中的isOutsideWorld方法。那么可以使用这个方法检测油漆罐的位置是否超出了游戏边界。如果是这样的话,需要重置油漆罐到顶部,通过if语句实现:

if (Game.gameWorld.isOutsideWorld(this.position))
this.moveToTop();

最后,为了让游戏更有挑战性。每次油漆罐更新时轻微的增加最小速度:

this.minVelocity = this.minVelocity + 0.01;

因为最小速度缓慢增加,所以随着时间的消逝游戏变得越来越难。

在屏幕上画出油漆罐

在PaintCan类中添加draw方法。在PainterGameWorld类中,对不同的对象调用handInput,update,draw方法。比如,在PainterGameWorld类中,draw方法如下:

PainterGameWorld.prototype.draw = function () {
Canvas2D.drawImage(sprites.background, { x : 0, y : 0 }, 0,
{ x : 0, y : 0 });
this.ball.draw();
this.cannon.draw();
this.can1.draw();
this.can2.draw();
this.can3.draw();
};

把位置和速度当做矢量

你发现类其实就是变量的概念,通过方法改变对象。这在你处理相似对象的时候很有用(比如3个油漆罐)。类的另外一个好处是定义一个基本的数据结构和操作管理这种数据结构的方法。一个常见的结构就是之前看到的代表二维坐标或者速度矢量的对象:

var position = { x : 0, y : 0 };
var anotherPosition = { x : 35, y : 40 };

不幸的是,下面的指令不被允许:

var sum = position + anotherPosition;

原因是加法运算对这种复合的对象没有定义。你当然也可以定义一个方法来进行实现。比如,你可以进行矢量之间的相减,相乘等等。为了实现这个,创建一个Vector2类。先建立构造函数:

function Vector2(x, y) {
this.x = x;
this.y = y;
}

现在可以像下面这样创建对象:

var position = new Vector2(0,0);

某些时候初始化矢量时不需要传递参数。一种实现方法是检测x或y是否未定义。如果是那样的话,初始化成员变量为0,如下:

function Vector2(x, y) {
if (typeof x === 'undefined')
this.x = 0;
else
this.x = x;
if (typeof y === 'undefined')
this.y = 0;
else
this.y = y;
}

typeof关键字返回一个变量的类型。这里用来检测x和Y是否有定义的类型。现在对上述代码有一个更简短的写法:

function Vector2(x, y) {
this.x = typeof x !== 'undefined' ? x : 0;
this.y = typeof y !== 'undefined' ? y : 0;
}

这段代码和If实现的效果是一样的。这让代码更易读。下面是创建vector2对象的几种方式:

var position = new Vector2(); // create a vector (0, 0)
var anotherPosition = new Vector2(35, 40); // create a vector (35, 40)
var yetAnotherPosition = new Vector2(-1); // create a vector (-1, 0)

现在你可以为这个类添加方法来进行矢量间的计算了。比如,下面的方法实现了向量的复制:

Vector2.prototype.copy = function () {
return new Vector2(this.x, this.y);
};

向量的比较也很有用:

Vector2.prototype.equals = function (obj) {
return this.x === obj.x && this.y === obj.y;
};

同理,也可以添加向量的其他方法,比如加减乘除。首先,定义一个加的方法:

Vector2.prototype.addTo = function (v) {
this.x = this.x + v.x;
this.y = this.y + v.y;
return this;
};

上面的方法可以这么用:

var position = new Vector2(10, 10); // create a vector (10, 10)
var anotherPosition = new Vector2(20, 20); // create a vector (20, 20)
position.addTo(anotherPosition); // now represents the vector (30, 30)

addTo方法的最后一条指令返回this。这样做的原因是你可以进行操作符链式操作。比如:

var position = new Vector2(10, 10); // create a vector (10, 10)
var anotherPosition = new Vector2(20, 20); // create a vector (20, 20)
position.addTo(anotherPosition).addTo(anotherPosition);
// position now represents the vector (50, 50)

依据传递参数的不同类型,可以做不同的事情。如果参数是数字的话,那么只需简单的将向量的每个元素与之相加。如果是向量的话,就进行向量的相加。实现这个可以通过typeof操作符,比如:

Vector2.prototype.addTo = function (v) {
if (typeof v === 'Vector2') {
this.x = this.x + v.x;
this.y = this.y + v.y;
}
else if (typeof v === 'Number') {
this.x = this.x + v;
this.y = this.y + v;
}
return this;
};

除了上面的方法,也可以使用constructor来判断变量的类型。下面使用constructor代替typeof的方法:

Vector2.prototype.addTo = function (v) {
if (v.constructor === Vector2) {
this.x = this.x + v.x;
this.y = this.y + v.y;
}
else if (v.constructor === Number) {
this.x = this.x + v;
this.y = this.y + v;
}
return this;
};

addTo是加上一个已知的矢量。也可以定义一个add方法来加上两个矢量。为了实现这个,可以复用copy和addTo方法:

Vector2.prototype.add = function (v) {
var result = this.copy();
return result.addTo(v);
};

现在可以这么做:

var position = new Vector2(10, 10); // create a vector (10, 10)
var anotherPosition = new Vector2(20, 20); // create a vector (20, 20)
var sum = position.add(anotherPosition); // creates a new vector (30, 30)

在这个例子中,position和anotherPostion在第三条指令没有发生改变。一个新的矢量诞生了。

查看Painter6例子中的Vector2.js文件可以看到矢量的完整实现。

现在使用Vector2类型的对象来代表游戏中所有的位置和速度。比如,下面是Ball类新的构造函数:

function Ball() {
this.position = new Vector2();
this.velocity = new Vector2();
this.origin = new Vector2();
this.currentColor = sprites.ball_red;
this.shooting = false;
}

由于Vector2类的方法,你可以一行代码直接更新向量的位置:

this.position.addTo(this.velocity.multiply(delta));

参数的默认值

在完成这章之前,再看一眼Vector2定义的构造函数:

function Vector2(x, y) {
this.x = typeof x !== 'undefined' ? x : 0;
this.y = typeof y !== 'undefined' ? y : 0;
}

因为函数中的代码,你可以在没有传递任何参数的时候也能创建一个有效的矢量对象。你可以应用这种方法到其他情况中,因为默认值可以让代码变得更简单。比如,在屏幕上画出背景图:

Canvas2D.drawImage(sprites.background, { x : 0, y : 0 }, 0, { x : 0, y : 0 });

你可以让这个方法更加紧凑,通过让drawImage自动提供默认参数:

Canvas2D_Singleton.prototype.drawImage = function (sprite, position,
rotation, origin) {
position = typeof position !== 'undefined' ? position : Vector2.zero;
rotation = typeof rotation !== 'undefined' ? rotation : 0;
origin = typeof origin !== 'undefined' ? origin : Vector2.zero;
// remaining drawing code here
...
}

那么,现在绘画背景图就是下面这样子了:

Canvas2D.drawImage(sprites.background);

虽然默认值对创建紧凑的代码非常有用,但是必须确保为你写的方法提供文档说明告诉使用者如果没有传递参数的话默认值是什么。

你学到了什么

在这章,学到了:

  • 如何通过原型机制实现类
  • 如何创建一个类的多个实例
  • 如何为游戏增加随机性来提供游戏可玩性

你可能感兴趣的:(JavaScript)