截止目前,你已经实现了Painter游戏中的大部分东西。你知道如何通过原型机制定义游戏对象的类。把这些类的实现放到不同的文件中,这样在未来如果要用到它们的话,只需要简单的复制就行了。
在类中,成员变量的管理是通过方法来操作的。这一章,讲述用另一个方法来定义对象,通过定义属性。这章也会引进一个代表颜色的类型和如何处理炮弹和油漆罐之间的碰撞(如果碰撞发生,则油漆罐改变颜色)。
一种代表颜色的不同方式
在之前的章节里,就已经在与颜色打交道了。比如,在Cannon类中,通过 currentColor变量来保存颜色:
this.currentColor = sprites.cannon_red;
Ball类中也做了相似的事情。虽然这看起来没什么不妥,但是通过炮身的颜色来改变炮弹的颜色总是有些不方便:
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;
上面的If语句,需要处理三种不同的颜色。此外,Ball类需要知道Cannon类使用的精灵。如果你能对颜色进行统一不是更好吗?当然可以这么做。另外一个这么做的原因是如果你增加颜色的话那么你的程序代码会变得更长。
Painter7例子添加了Color.js文件。为了定义不同的颜色,使用了跟定义键值类似的方法。如下:
var Color = {
red : 1,
blue : 2,
yellow : 3,
green : 4,
// and so on
}
但是,这种方法并不好。使用数字代表颜色并不是那么坏,但是除了你没人知道这些数字的意义。这里已经有一个HTML的标准来定义颜色,使用十六进制的数。这个方法的好处是它被广泛使用,被广泛理解,被工具广泛支持。
在HTML标准中,可以通过十六进制指定元素的颜色:
<body style="background: #0000FF">
That's a very nice background.
</body>
下面是Color变量的一部分:
var Color = {
aliceBlue: "#F0F8FF",
antiqueWhite: "#FAEBD7",
aqua: "#00FFFF",
aquamarine: "#7FFFD4",
azure: "#F0FFFF",
beige: "#F5F5DC",
bisque: "#FFE4C4",
black: "#000000",
blanchedAlmond: "#FFEBCD",
blue: "#0000FF",
blueViolet: "#8A2BE2",
brown: "#A52A2A",
// and so on
}
想知道更多的细节就看Color.js。
有三个类的对象有颜色,Cannon,Ball,PaintCan。为了简单起见,现在演示如何修改Cannon类通过上面定义的颜色。截止目前,Cannon的构造函数是这样的:
function Cannon() {
this.position = new Vector2(72, 405);
this.colorPosition = new Vector2(55, 388);
this.origin = new Vector2(34, 34);
this.currentColor = sprites.cannon_red;
this.rotation = 0;
}
添加一个变量表示当前炮身的颜色,那么现在Cannon的构造函数时这样的:
function Cannon() {
this.position = new Vector2(72, 405);
this.colorPosition = new Vector2(55, 388);
this.origin = new Vector2(34, 34);
this.currentColor = sprites.cannon_red;
this.color = Color.red;
this.rotation = 0;
}
然而,这并不理想。你储存了多余的数据,有关颜色的信息有两个变量了。此外,如果你忘了修改另一个颜色的变量可能导致Bug。
另外一个方式是储存当前精灵的引用。现在构造函数是这样的:
function Cannon() {
this.position = new Vector2(72, 405);
this.colorPosition = new Vector2(55, 388);
this.origin = new Vector2(34, 34);
this.color = Color.red;
this.rotation = 0;
}
这也不是一个好的方法,因为你不得不每次调用draw方法的时候为其选择正确的精灵。
一个解决办法是定义两个方法允许用户取回Cannon类的数据并且设置颜色。现在可以添加读和写的颜色的方法。如下:
Cannon.prototype.getColor = function () {
if (this.currentColor === sprites.cannon_red)
return Color.red;
else if (this.currentColor === sprites.cannon_green)
return Color.green;
else
return Color.blue;
};
Cannon.prototype.setColor = function (value) {
if (value === Color.red)
this.currentColor = sprites.cannon_red;
else if (value === Color.green)
this.currentColor = sprites.cannon_green;
else if (value === Color.blue)
this.currentColor = sprites.cannon_blue;
};
现在使用Cannon类不需要知道内部情况了。现在可以简单的传递一个定义的颜色来读和写炮身的颜色:
myCannon.setColor(Color.blue);
var cannonColor = myCannon.getColor();
有时候,程序员把这类型的方法叫做获取和设置。在许多面向对象的编程语言中,方法是获取对象内部数据的唯一方式。对比其他面向对象编程语言JavaScript提供了一个比较新的特性:属性。属性是获取和设置的替代。
跟随基于原型的编程范式,现在想为类添加属性。JavaScript有一个叫做defineProperty的方法来实现这个。这个方法时对象的一部分,也被叫做Object。Object也有一组其它有用的方法。defineProperty需要三个参数:
get和set变量应该指向一个函数,当属性被写和读时执行。然而,把get和set独立出来更好。因为如果属性只读并且不能被改变是有好处的。如果属性只能进行读取,它就叫做只读属性。这里有一个添加只读属性的简单例子:
Object.defineProperty(Cannon.prototype, "center",
{
get: function () {
return new Vector2(this.currentColor.width / 2,
this.currentColor.height / 2);
}
});
如上所示,提供了三个参数,原型,名称,对象。属性的名字是center.它的目的是提供炮身的中心位置。因为中心位置不可能改变,所以属性只有get部分。这是通过传递的第三个参数实现的,其中只含有一恶搞变量get。如何使用这个属性:
var cannonCenter = cannon.center;
很简单,不是吗?同理,可以为炮身提供一个height属性:
Object.defineProperty(Cannon.prototype, "height",
{
get: function () {
return this.currentColor.height;
}
});
甚至可以定义一个ballPosition属性来计算炮弹的位置:
Object.defineProperty(Cannon.prototype, "ballPosition",
{
get: function () {
var opposite = Math.sin(this.rotation) *
sprites.cannon_barrel.width * 0.6;
var adjacent = Math.cos(this.rotation) *
sprites.cannon_barrel.width * 0.6;
return new Vector2(this.position.x + adjacent,
this.position.y + opposite);
}
});
通过属性这种方法,现在可以通过一行代码就可以计算炮弹新的位置了:
this.position = Game.gameWorld.cannon.ballPosition.subtractFrom(this.center);
定义类,对象,属性的好处就是你的代码变得更短也更易于理解了。比如,为Vector2添加下面的属性:
Object.defineProperty(Vector2, "zero",
{
get: function () {
return new Vector2();
}
});
现在创建一个二维矢量更简便了:
var position = Vector2.zero;
从现在开始,使用属性和方法来定义对象的表现和实现数据的获取。通过在类中定义有用的方法和属性,游戏代码吧变得易读。比如,在这之前是这么计算球的位置:
this.position = Game.gameWorld.cannon.ballPosition();
this.position.x = this.position.x - this.currentColor.width / 2;
this.position.y = this.position.y - this.currentColor.height / 2;
新的处理方式代码更短,就在之前的那一行代码。
通过在属性添加if语句来实现获取大炮的颜色:
Object.defineProperty(Cannon.prototype, "color",
{
get: function () {
if (this.currentColor === sprites.cannon_red)
return Color.red;
else if (this.currentColor === sprites.cannon_green)
return Color.green;
else
return Color.blue;
}
});
现在可以通过这个属性获取大炮的颜色。比如:
var cannonColor = cannon.Color;
然后,通过if语句和set参数决定currentColor变量新的值:
Object.defineProperty(Cannon.prototype, "color",
{
get: function () {;
if (this.currentColor === sprites.cannon_red)
return Color.red;
else if (this.currentColor === sprites.cannon_green)
return Color.green;
else
return Color.blue;
},
set: function (value) {
if (value === Color.red)
this.currentColor = sprites.cannon_red;
else if (value === Color.green)
this.currentColor = sprites.cannon_green;
else if (value === Color.blue)
this.currentColor = sprites.cannon_blue;
}
});
这是一个关于属性获取和设置的例子。可以把color属性添加其他的游戏对象类型中:Cannon,Ball,PaintCan。不同之处只是get与set方法中精灵的不同。如下:
Object.defineProperty(Ball.prototype, "color",
{
get: function () {
if (this.currentColor === sprites.ball_red)
return Color.red;
else if (this.currentColor === sprites.ball_green)
return Color.green;
else
return Color.blue;
},
set: function (value) {
if (value === Color.red)
this.currentColor = sprites.ball_red;
else if (value === Color.green)
this.currentColor = sprites.ball_green;
else if (value === Color.blue)
this.currentColor = sprites.ball_blue;
}
});
因为你已经定义了这些属性,那么现在根据大炮的颜色来改变炮弹的颜色就变得简单了:
this.color = Game.gameWorld.cannon.color;
更多细节就在Painter7例子。
当两个对象发生碰撞,则必须两者之一中的update方法中处理碰撞。在这里,可以在Ball类或PaintCan类中进行处理。Painter7在PaintCan类里面进行处理。因为如果你在Ball类中进行处理,则必须复制三次代码。
虽然碰撞检测有许多实现方法,这里使用一个非常简单的方法。在这里认为两个对象之间的中心距离小于一个特定的值时就认为两个对象之间发生了碰撞。游戏中任何时候炮弹的中心位置是通过炮弹精灵中心位置加上炮弹的位置。计算油漆罐中心位置的方法类似。因为已经有了很好的属性用于计算游戏对象的中心位置,我们可以它们计算炮弹和油漆罐之间的距离:
var ball = Game.gameWorld.ball;
var distance = ball.position.add(ball.center).subtractFrom(this.position)
.subtractFrom(this.center);
计算了这个矢量后,就要在X坐标和Y坐标上检测它们是否小于一个给定的值。如果X分量的绝对值小于中心的X值,意味着炮弹对象在油漆罐的给定X范围内。这同样适用于Y方向。如果X和Y分量结果都是如此,那么就可以说炮弹碰撞到了油漆罐。可以用if语句实现:
if (Math.abs(distance.x) < this.center.x &&
Math.abs(distance.y) < this.center.y) {
// handle the collision
}
使用Math.abs方法计算距离间的绝对值。如果炮弹与油漆罐之间发生了碰撞,则油漆罐需要改变颜色。
下一步,重置炮弹以备下次发射:
this.color = ball.color;
ball.reset();
如你所见,上面的碰撞检测没有那么精确。以后你会学到一个更好的在像素级上的碰撞检测。
这章里,学到了: