九、颜色和碰撞

到目前为止,您已经实现了 Painter 游戏的很大一部分。您已经看到了如何使用原型机制定义游戏对象类。通过使用这些类,您可以更好地控制游戏对象的结构以及如何创建特定类型的游戏对象。您将这些类定义分隔在不同的文件中。这样,当你在未来的游戏中需要一个具有相同行为的大炮或球时,你可以简单地复制这些文件并在你的游戏中创建这些游戏对象的实例。

当您更仔细地查看类的定义时,您会发现类定义了对象的内部结构(它由哪些变量组成)以及以某种方式操作该对象的方法。这些方法可以帮助更精确地定义一个对象的可能性和局限性。例如,如果有人想重用Ball类,他们不需要很多关于球是如何构造的详细信息。简单地创建一个实例并调用 game-loop 方法就足以在游戏中添加一个飞行球。一般来说,当你设计一个程序时,无论是游戏还是完全不同的应用,清楚地定义某个类的对象的可能性是很重要的。方法是做到这一点的一种方式。本章向你展示了另一种定义对象可能性的方法:通过定义属性。本章还介绍了一种表示颜色的类型,并展示了如何处理球和颜料罐之间的碰撞(如果发生这种情况,颜料罐需要改变颜色)。

表现颜色的不同方式

在 Painter 的早期版本中,您已经相当实际地处理了颜色。例如,在Cannon类中,您通过使用currentColor变量跟踪当前颜色,该变量最初指向红色的 cannon sprite:

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类使用的精灵。如果可以更统一地定义颜色,并在所有游戏对象类中使用该定义来表示不同的颜色,不是更好吗?当然会!现在开始在游戏中统一颜色使用的另一个原因是,如果你决定增加游戏中可能的颜色数量(到 4、6、10 或更多),当前的方法将需要更长的编程时间。

属于本章的 Painter7 示例添加了一个Color.js JavaScript 文件。要定义不同的颜色,可以使用与定义不同键类似的方法。这个文件定义了一个名为Color的变量。Color变量包含许多子变量,每个子变量代表一种不同的颜色。您可以如下定义颜色:

var Color = {
    red : 1,
    blue : 2,
    yellow : 3,
    green : 4,
    // and so on
}

然而,这种方法并不是一个好的解决方案。用数字来表示颜色并不是一个坏主意,但是你不应该自己编一个没有人知道的编号方案。在 HTML 中已经有了一个定义颜色的标准,它使用以十六进制形式表示的整数,您也可以使用这个标准。好处是这种方法被广泛使用,被广泛理解,被工具广泛支持(比如 Adobe 的 Kuler,比如 at http://kuler.adobe.com)。

在 HTML 标准中,您可以通过使用十六进制表示来定义网页中元素的颜色。例如:

<body style="background: #0000FF">
That's a very nice background.
</body>

在这种情况下,您指定主体的背景颜色应该是颜色蓝色。十六进制表示让您定义红、绿、蓝(RGB)值中的颜色,其中 00 表示没有颜色分量, FF 表示颜色分量最大。

#符号不是数字的一部分,它只是向浏览器表明后面是十六进制数而不是十进制数。所以,#0000FF十六进制数代表蓝色,#00FF00是绿色,#FF0000是红色。当然,颜色分量的任何混合或渐变都可以用类似的方式来定义。#808080为灰色,#800080为紫色,#FF00FF为洋红色。

下面是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文件。您现在可以开始在您的类中使用这些颜色定义。

对象的受控数据访问

三个游戏对象类代表一个特定颜色的对象:CannonBallPaintCan。为了简单起见,让我们从如何修改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;
}

然而,这并不理想。您现在存储了冗余数据,因为颜色信息由两个变量表示。此外,当加农炮的颜色改变时,如果您忘记改变两个变量中的一个,您可能会以这种方式引入错误。

另一种方法是不存储对当前 sprite 的引用。这将是构造函数:

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方法时,您都需要查找正确的 sprite。

一个解决方案是定义两个方法,允许Cannon类的用户检索和设置颜色信息。然后,您可以保持构造函数不变,但添加方法来读取和写入颜色值。例如,您可以将以下两个方法添加到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类的用户不需要知道在内部,您使用一个 sprite 来确定大炮的当前颜色。用户可以简单地传递颜色定义来读取或写入加农炮的颜色:

myCannon.setColor(Color.blue);
var cannonColor = myCannon.getColor();

有时,程序员称这类方法为gettersetter。在许多面向对象的编程语言中,方法是访问对象内部数据的唯一方式,因此对于每个需要在类外部访问的成员变量,程序员都添加了一个 getter 和一个 setter。JavaScript 提供了一个对面向对象编程语言来说相对较新的特性:属性。属性是 getter 和 setter 的替代品。它定义了从对象中检索数据时会发生什么,以及向对象内部的数据赋值时会发生什么。

只读属性

按照基于原型的编程范式,您希望能够向类添加属性。JavaScript 有一个叫做defineProperty的简便方法可以让你做到这一点。这个方法是对象的一部分,俗称ObjectObject还有几个其他有用的方法,您稍后会了解到。defineProperty方法需要三个参数:

  • 应添加属性的原型(例如,Cannon.prototype)
  • 属性的名称(例如,color)
  • 一个最多包含两个变量的对象:getset

getset变量都应该指向一个函数,该函数应该在属性被读取或写入时被执行。然而,可以只定义一个getset零件。如果属性只读取信息而不能更改信息,这将非常有用。如果一个属性只读取信息,它被称为只读属性 。下面是一个简单的例子,它是您添加到Cannon类中的只读属性:

Object.defineProperty(Cannon.prototype, "center",
    {
        get: function () {
            return new Vector2(this.currentColor.width / 2,
                this.currentColor.height / 2);
        }
    });

如您所见,您向defineProperty方法提供了三个参数:原型、名称和对象。这个楼盘的名字叫center。它的目标是提供代表大炮的精灵的中心。因为不可能改变中心的值,所以这个属性只有一个get部分。这反映在作为第三个参数传递的对象中,该对象包含一个指向函数的变量get。以下是使用该属性的方法:

var cannonCenter = cannon.center;

很简单,不是吗?同样,您可以添加一个提供加农炮高度的属性,如下所示:

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关键字来引用属性所属的对象。属于本章的 Painter7 示例将属性添加到不同的类中。例如,Ball类也包含一个center属性。结合您添加到Vector2中的便捷的方法,您现在可以在一行代码中根据大炮的旋转来计算球的新位置:

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;

正如您在本节前面所看到的,新的方法要短得多。这种效果在你在本书中开发的游戏代码中随处可见,我鼓励你接受类、方法和属性所提供的力量!

检索加农炮的颜色

在本章中,您定义了一个名为Color的新类型。因此,让我们结合使用该类型和属性来读取和写入大炮的颜色。根据currentColor变量指向的 sprite,您希望返回不同的颜色值。为了实现这一点,您需要在Cannon类中添加一个名为color的属性。在该属性的get部分,您使用一个if指令来找出返回哪种颜色。就像有返回值的方法一样,使用return关键字来指示属性应该返回什么值:

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;

您还希望能够为加农炮颜色赋值。为此,您必须定义属性的set部分。在那个部分,您需要修改currentColor变量的值。当在另一个方法中使用该属性时,会提供此值。例如,它可能是这样的指令:

cannon.color = Color.Red;

同样,您使用一条if指令来确定currentColor变量的新值应该是什么。右边的赋值作为参数传递给set部件。完整的属性如下所示:

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属性到所有的彩色游戏对象类型:CannonBallPaintCan。在getset部分的代码中唯一的区别是用来表示颜色的精灵。例如,这是Ball类的color属性:

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 示例,了解它如何以及在何处使用属性来使代码更易于阅读。对于一些程序员来说,属性乍一看可能很奇怪,因为它们被用来获取和设置方法。然而,属性确实有更直观的意义。它们是降低代码行复杂性的好方法。

处理球和罐子之间的碰撞

Painter7 示例通过处理球和罐子之间的碰撞来扩展游戏。如果两个对象发生冲突,你必须在两个对象之一的update方法中处理这个冲突。在这种情况下,您可以选择在Ball类或PaintCan类中处理冲突。Painter7 在PaintCan类中处理碰撞,因为如果你要在Ball类中处理碰撞,你需要重复同样的代码三次,每个油漆罐一次。通过在PaintCan类中处理碰撞,您可以自动获得这种行为,因为每个类都可以自己检查是否与球碰撞。

虽然可以用许多不同的方法进行冲突检查,但是这里使用一个非常简单的方法。如果两个对象中心之间的距离小于某个值,则可以定义这两个对象之间存在碰撞。游戏世界中任何时候球的中心位置是通过将球精灵的中心与球的位置相加来计算的。您可以用类似的方法计算油漆罐的中心。因为您添加了一些很好的属性来计算游戏对象的中心,所以让我们使用它们来计算球和油漆罐之间的距离,如下所示:

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();

您可以尝试 Painter7 示例,查看球和颜料罐之间的碰撞是否得到了正确处理。

您可能已经注意到,这里使用的碰撞检测方法不是很精确。在第 26 章中,你会看到一种更好的方法来处理每像素级别的碰撞,尽管如果你不小心的话,这会让你的游戏运行得更慢。

注意最后,像本节中所写的简单代码行在玩家体验中产生了很大的不同。当你构建你的游戏应用时,你会发现有时候对玩家来说最小的东西也要花最长的时间来编程,而最大的变化只用一两行就实现了!

你学到了什么

在本章中,您学习了:

  • 如何向类中添加属性
  • 如何处理游戏对象之间的基本碰撞
  • 如何定义具有不同颜色的游戏对象