四、游戏素材

前面的章节已经向你展示了如何通过编写你自己的游戏循环方法作为一个名为Game的对象的一部分来制作一个非常基本的游戏应用。您已经看到了 JavaScript 中的哪些指令检索画布以及用于在画布上执行操作的画布上下文。你已经看到了一些简单的例子,其中你改变了背景颜色。您还通过使用当前系统时间和游戏循环方法在屏幕上移动了一个矩形。本章展示了如何在屏幕上绘制图像,这是制作好看游戏的第一步。在计算机图形学中,这些图像也被称为精灵。精灵通常是从文件中加载的。这意味着任何绘制精灵的程序不再仅仅是一组孤立的指令,而是依赖于存储在某处的游戏素材。这立即引入了许多您需要考虑的事情:

  • 你可以从哪个位置载入精灵?
  • 如何从图像文件中检索信息?
  • 你如何在屏幕上画一个精灵?

本章回答了这些问题。

声音是另一种类型的游戏素材。它的处理方式与精灵非常相似。所以,在这一章的最后,你也看到了你如何在你的游戏中回放音乐和音效。

注意精灵的名字来自精灵,这是一种创建用于视频游戏的二维、部分透明光栅图形的过程。在早期,创建这些二维图像需要大量的手工工作;但它产生了一种特殊的图像风格,启发人们创造自己的类似图像,从而产生了一种被称为像素艺术或精灵艺术的艺术技巧。

定位精灵

在程序可以使用任何种类的素材之前,它需要知道在哪里寻找这些素材。默认情况下,充当解释器的浏览器在 JavaScript 文件所在的文件夹中查找精灵。看看属于这一章的 SpriteDrawing 示例。您会在 HTML 文件和 JavaScript 文件所在的文件夹中看到一个名为spr_balloon.png的文件。你可以加载这个精灵并把它画在屏幕上。

装载精灵

现在让我们看看如何从文件中加载精灵。一旦你这样做了,你通过使用一个变量把它存储在内存的某个地方。在几个不同的游戏循环方法中都需要这个变量。在start方法中,加载精灵并将其存储在变量中。在draw方法中,您可以访问该变量以便在屏幕上绘制精灵。因此,你给Game对象添加了一个名为balloonSprite的变量。在这里你可以看到Game变量的声明及其初始化:

var Game = {
    canvas : undefined,
    canvasContext : undefined,
    balloonSprite : undefined
};

Gamestart方法中,你给这些变量赋值。您已经看到了如何检索画布和画布上下文。就像Game一样,画布和画布上下文是由其他变量(或对象)组成的对象。如果你加载了一个 sprite,你就有了一个代表 sprite 的对象。您可以定义一个包含图像中所有信息的对象变量:

Game.balloonSprite = {
    src : "spr_balloon.png",
    width : 35,
    height : 63,
    ...
}

当你想为你的游戏加载数百个精灵时,这就成了问题。每次,你都必须通过使用一个对象文字来定义这样一个对象。此外,您必须确保不会在对象中意外地使用其他变量名,因为这样会导致图像的不一致表示。幸运的是,您可以通过使用类型来避免这个麻烦。

类型基本上是对该类型的对象应该是什么样子的定义;这是一个物体的蓝图。比如 JavaScript 知道一个叫Image的类型。该类型指定图像对象应该具有宽度、高度、源文件等等。有一个非常简单的方法来创建一个类型为Image的对象,使用new关键字:

Game.balloonSprite = new Image();

这比键入变量应该包含的所有内容要容易得多。这个表达基本上对你有用。通过使用类型,您现在有了一个创建对象的简单方法,并且您可以确保这些对象总是具有相同的结构。当一个对象被构造成具有由Image类型指定的结构时,你说这个对象属于 Image类型。

你还没有指出什么数据应该包含在这个变量中。您可以通过将文件名分配给src变量来设置该图像的源文件,该变量始终是Image对象的一部分:

Game.balloonSprite.src = "spr_balloon.png";

一旦设置了src变量,浏览器就开始加载文件。浏览器会自动填充widthheight变量的数据,因为它可以从源文件中提取这些信息。

有时,加载源文件需要一段时间。例如,文件可以存储在世界另一端的网站上。这意味着,如果您试图在设置源文件后立即绘制图像,您可能会遇到麻烦。因此,在开始游戏之前,您需要确保每个图像都已加载。有一种非常简洁的方法可以做到这一点,那就是使用一个事件处理器函数。 在第七章中,你看这是怎么回事。现在,假设加载图像的时间不会超过半秒。通过使用setTimeOut方法,您在 500 毫秒的延迟后调用mainLoop方法:

window.setTimeout(Game.mainLoop, 500);

这就完成了start方法,现在看起来像这样:

Game.start = function () {
    Game.canvas = document.getElementById("myCanvas");
    Game.canvasContext = Game.canvas.getContext("2d");
    Game.balloonSprite = new Image();
    Game.balloonSprite.src = "spr_balloon.png";
    window.setTimeout(Game.mainLoop, 500);
};

精灵可以从任何位置加载。如果你在用 JavaScript 开发游戏,那么考虑一下精灵的组织是个好主意。例如,你可以将游戏中的所有精灵放在一个名为精灵的子文件夹中。然后你必须如下设置源文件:

Game.balloonSprite.src = "sprites/spr_balloon.png";

或者,您甚至可能没有使用自己的图像,而是引用了在另一个网站上找到的图像:

Game.balloonSprite.src = "
http://www.somewebsite.cimg/spr_balloon.png";

JavaScript 允许你从任何你想要的地方加载图像文件。只要确保从另一个网站加载图像时,图像文件的位置是固定的。否则,如果该网站的管理员决定在不通知您的情况下移动所有内容,您的游戏将无法运行。

绘图精灵

加载一个精灵并把它存储在内存中并不意味着精灵被绘制在屏幕上。为此,您需要在draw方法中做一些事情。要在画布上的某个地方绘制一个精灵,可以使用drawImage方法,它是画布上下文对象的一部分。在 JavaScript 中,当一个图像被绘制在某个位置时,那个位置总是指的是图像左上角的。下面是在屏幕左上角绘制精灵的指令:

Game.canvasContext.drawImage(sprite, 0, 0, sprite.width, sprite.height,
    0, 0, sprite.width, sprite.height);

drawImage方法有许多不同的参数。例如,您可以指定要在哪个位置绘制精灵,或者是否只绘制精灵的一部分。你可以简单地调用这个方法并完成它。然而,如果你正在考虑你想要构建的未来游戏,你可以使用一个绘制状态来绘制精灵。

一个绘图状态基本上是一组参数和转换,它们将应用于在该状态下绘制的所有事物。使用绘制状态而不是单独调用drawImage方法的好处是,你可以用精灵做更复杂的转换。例如,使用绘图状态,您可以旋转或缩放精灵,这在游戏中是非常有用的功能。创建一个新的绘图状态是通过调用save方法来完成的:

Game.canvasContext.save();

然后,您可以在此绘图状态下应用各种变换。例如,您可以将精灵移动(或平移)到某个位置:

Game.canvasContext.translate(100, 100);

如果你现在调用drawImage方法,精灵将被绘制在位置(100,100)。完成绘制后,您可以按如下方式移除绘制状态:

Game.canvasContext.restore();

为了方便起见,让我们定义一个为您完成所有这些工作的方法:

Game.drawImage = function (sprite, position) {
    Game.canvasContext.save();
    Game.canvasContext.translate(position.x, position.y);
    Game.canvasContext.drawImage(sprite, 0, 0, sprite.width, sprite.height,
        0, 0, sprite.width, sprite.height);
    Game.canvasContext.restore();
};

通过查看参数,可以看出,这个方法需要两条信息:应该绘制的精灵和应该绘制的位置。sprite 的类型应该是Image(尽管在 JavaScript 中定义函数时不容易实现这一点)。位置是一个由x部分和y部分组成的对象变量。当你调用这个方法时,你必须提供这个信息。例如,可以在位置(100,100)绘制气球精灵,如下所示:

Game.drawImage(Game.balloonSprite, { x : 100, y : 100 });

您使用大括号来定义一个包含xy组件的对象文字。如你所见,允许在调用方法的指令中定义一个对象。或者,您可以首先定义一个对象,将其存储在一个变量中,然后使用该变量调用drawImage方法:

var balloonPos = {
    x : 100,
    y : 100
};
Game.drawImage(Game.balloonSprite, balloonPos);

这段代码做的事情与前面对drawImage的调用完全一样,除了要写得更长。您可以简单地将drawImage方法调用放入draw方法中,气球将被绘制在所需的位置:

Game.draw = function () {
    Game.drawImage(Game.balloonSprite, { x : 100, y : 100 });
};

图 4-1 显示了程序在浏览器中的输出。

9781430265382_Fig04-01.jpg

图 4-1 。SpriteDrawing 程序的输出

同样,请注意,如果您告诉浏览器在给定的位置绘制一个 sprite,sprite 的左上角部分将被绘制在那里。

移动精灵

现在你可以在屏幕上画一个精灵了,你可以使用游戏循环让它移动,就像你在第三章的 MovingSquare 例子中对正方形所做的那样。让我们对这个程序做一个小小的扩展,根据经过的时间改变气球的位置。为了做到这一点,你必须把气球的位置存储在某个地方。您需要在update方法中计算这个位置,并在draw方法中在那个位置绘制气球。因此,您向表示位置的Game对象添加一个变量,如下所示:

var Game = {
    canvas : undefined,
    canvasContext : undefined,
    balloonSprite : undefined,
    balloonPosition : { x : 0, y : 50 }
};

正如您所看到的,您将位置定义为由Game对象中的两个变量(xy)组成的对象。现在,您可以向update方法添加一条指令,根据经过的时间修改 x 位置,就像您在 MovingSquare 示例中所做的那样。下面是update的方法:

Game.update = function () {
    var d = new Date();
    Game.balloonPosition.x = d.getTime() % Game.canvas.width;
};

现在剩下要做的唯一一件事就是确保在屏幕上用draw方法绘制气球时使用了balloonPosition变量:

Game.drawImage(Game.balloonSprite, Game.balloonPosition);

加载和绘制多个精灵

只用纯白色背景构建游戏有些无聊。通过显示背景精灵,你可以让你的游戏看起来更有吸引力。这意味着你必须在start方法中加载另一个精灵,并扩展draw方法来绘制它。这个程序的最终版本叫做 FlyingSprite,你可以在属于本章的 sample 文件夹中找到完整的源代码。如果您在浏览器中打开 FlyingSprite 程序,您会看到现在绘制了两个精灵:一个背景和一个气球。为此,您添加另一个变量来包含背景精灵。像balloonSprite变量一样,这个变量也是Game对象的一部分:

var Game = {
    canvas : undefined,
    canvasContext : undefined,
    backgroundSprite : undefined,
    balloonSprite : undefined,
    balloonPosition : { x : 0, y : 50 }
};

另外,在draw方法中,现在有两个对drawImage方法的调用,而不是一个:

Game.draw = function () {
    Game.drawImage(Game.backgroundSprite, { x : 0, y : 0 });
    Game.drawImage(Game.balloonSprite, Game.balloonPosition);
};

这些方法的调用顺序非常重要!因为你想让气球出现在背景的上面,你先画背景,然后画气球。如果你反过来做,背景会画在气球上,你就看不到了(自己试试)。图 4-2 显示了程序的输出。

9781430265382_Fig04-02.jpg

图 4-2 。FlyingSprite 程序的输出

每次你想在屏幕上画一个精灵,你就在draw方法中添加一个对drawImage方法的调用。您可以在屏幕上的不同位置多次绘制一个精灵。例如,如果你想在背景的不同位置画几个气球,你只需为每个你想画的气球调用drawImage,并把想要的位置作为参数传递,如下所示:

Game.draw = function () {
    Game.drawImage(Game.backgroundSprite, { x : 0, y : 0 });
    Game.drawImage(Game.balloonSprite, { x : 0, y : 0 });
    Game.drawImage(Game.balloonSprite, { x : 100, y : 0 });
    Game.drawImage(Game.balloonSprite, { x : 200, y : 0 });
    Game.drawImage(Game.balloonSprite, { x : 0, y : 300 });
    Game.drawImage(Game.balloonSprite, { x : 200, y : 300 });
};

再次,注意你绘制精灵的顺序。

你也可以同时绘制多个移动的精灵。对于每个气球,您可以定义它自己的位置变量,该变量在update方法中更新:

Game.update = function () {
    var d = new Date();
    Game.balloonPosition1.x = d.getTime() % Game.canvas.width;
    Game.balloonPosition2.x = (d.getTime() + 100) % Game.canvas.width;
    Game.balloonPosition3.x = (d.getTime() + 200) % Game.canvas.width;
};

draw方法中,您使用这些位置同时绘制移动和静止的气球:

Game.draw = function () {
    Game.drawImage(Game.backgroundSprite, Game.balloonPosition1);
    Game.drawImage(Game.balloonSprite, Game.balloonPosition2);
    Game.drawImage(Game.balloonSprite, Game.balloonPosition3);
    Game.drawImage(Game.balloonSprite, { x : 200, y : 0 });
    Game.drawImage(Game.balloonSprite, { x : 0, y : 300 });
    Game.drawImage(Game.balloonSprite, { x : 200, y : 300 });
};

摆弄一下这个例子。想出在屏幕上画移动气球的不同方法。尝试几个不同的位置值。你能让一些气球比另一些移动得更快或更慢吗?

音乐和声音

另一种常用的游戏素材是声音。大多数游戏都有音效和背景音乐。出于各种原因,这些都很重要。音效提供了重要的线索,向用户表明发生了什么事情。例如,当用户点击按钮时播放卡嗒声向用户提供了按钮确实被按下的反馈。听到脚步声表明敌人可能就在附近,尽管玩家可能还没有看到他们。听到远处有铃声响起,可以表明有事情要发生了。在这方面,老游戏 Myst 是一个经典,因为许多关于如何进步的线索通过声音传递给了玩家。

水滴声、树风声和远处汽车声等大气音效增强了体验,给人一种身临其境的感觉。它们使环境更加生动,即使屏幕上什么也没有发生。

注意音乐在玩家体验环境和行动的过程中起着至关重要的作用。音乐可以用来制造紧张、悲伤、快乐和许多其他情绪。然而,在游戏中处理音乐比在电影中要困难得多。在电影中,很清楚将要发生什么,所以音乐可以完美地匹配。但是在游戏中,部分动作是在玩家的控制之下。现代游戏使用自适应音乐,这种音乐随着游戏剧情的发展而不断变化。

如果你想在游戏中实现更高级的音乐和声音处理,基本的 JavaScript 声音引擎是不行的。改用 Web Audio ( http://www.w3.org/TR/webaudio/),这是一个高级库,用于处理和合成许多现代浏览器支持的音频。

在 JavaScript 中,播放背景音乐或声音效果非常容易。要使用声音,您首先需要一个可以播放的声音文件。在 FlyingSpriteWithSound 程序中,你播放文件snd_music.mp3,它作为背景音乐。与存储和使用精灵类似,您向存储音乐数据的Game对象添加一个变量。因此,Game对象的声明和初始化如下:

var Game = {
    canvas : undefined,
    canvasContext : undefined,
    backgroundSprite : undefined,
    balloonSprite : undefined,
    balloonPosition : { x : 0, y : 50 },
    backgroundMusic : undefined
};

为了加载音效或背景音乐,您需要向start方法添加一些指令。JavaScript 提供了一种类型,您可以将其用作创建表示声音的对象的蓝图。这种类型叫做Audio。您可以创建该类型的对象,并开始加载声音,如下所示:

Game.backgroundMusic = new Audio();
Game.backgroundMusic.src = "snd_music.mp3";

正如你所看到的,这几乎和加载精灵的方式一样。现在,您可以调用定义为该对象一部分的方法,并且可以设置该对象的成员变量。例如,以下指令告诉浏览器开始播放存储在Game.backgroundMusic变量中的音频:

Game.backgroundMusic.play();

您希望降低背景音乐的音量,以便稍后播放(更大声)的音效。按照以下说明设置音量:

Game.backgroundMusic.volume = 0.4;

volume成员变量一般是 0 到 1 之间的值,其中 0 表示没有声音,1 表示以最大音量播放声音。

从技术上来说,背景音乐和音效没有区别。正常情况下,背景音乐以较低的音量播放;许多游戏会循环播放背景音乐,这样当歌曲结束时,音频会从头开始播放。你稍后会看到如何去做。你在本书中开发的所有游戏都使用这两种声音(背景音乐和声音效果)来使游戏更加刺激。

注意在游戏中使用声音和音乐时,你需要注意一些事情。声音对一些播放器来说很烦人,所以如果你使用音效或音乐,确保播放器有办法关掉它们。还有,不要强迫玩家等到一个声音播放完了才可以继续。您可能已经创作了一首很棒的歌曲,想在介绍屏幕显示时播放,但是玩家并不是为了听您的音乐而启动您的游戏,他们只是想玩!同样的原理也适用于游戏中的视频序列。总是为用户提供一个跳过这些的方法(即使你让你最喜欢的家庭成员提供僵尸声音)。最后,加载声音和音乐可能需要时间,尤其是当文件托管在网站上时。尽可能使用小的声音文件。

你学到了什么

在本章中,您学习了:

  • 如何将精灵和声音等游戏资源加载到内存中
  • 如何在屏幕上绘制多个精灵并移动它们
  • 如何在游戏中播放背景音乐和音效*