十九、菜单和设置

在 Jewel Jam 游戏中,您看到了一些向游戏中添加 GUI 元素的基本示例,如按钮或框架。在这一章中,您将向 Penguin Pairs 游戏添加一些 GUI 元素,例如开/关按钮和滑块按钮。您还将看到如何读取和存储游戏设置,如音乐音量和是否允许提示。

设置菜单

当想到菜单时,你可能会想到下拉菜单(如文件或编辑)或应用顶部的按钮。但是,菜单可以是灵活的,尤其是在游戏中,菜单通常是按照游戏的风格设计的,并且在许多情况下可以覆盖部分屏幕甚至整个屏幕。作为一个例子,让我们看看如何定义一个包含两个控件的基本选项菜单屏幕:一个用于打开或关闭提示,另一个用于控制音乐的音量。首先,您需要绘制这些控件周围的元素。向菜单添加背景,然后添加文本标签来描述提示控件。你可以使用宝石果酱游戏中的Label类来实现。你定义应该绘制的文本,并把它放在适当的位置(下面的代码摘自PenguinPairsGameWorld ):

var background = new SpriteGameObject(sprites.background_options,
    ID.layer_background);
this.add(background);
var onOffLabel = new Label("Arial", "60px", ID.layer_overlays);
onOffLabel.text = "Hints";
onOffLabel.position = new Vector2(150, 360);
onOffLabel.color = Color.darkBlue;
this.add(onOffLabel);

同样,为音乐音量控制器添加一个文本标签。有关完整代码,请参见本章的PenguinPairs2示例。

添加开/关按钮

下一步是添加一个开/关按钮,在游戏过程中显示(或不显示)提示。在本章的后面,您将看到如何使用这个按钮的值。就像你为宝石果酱游戏中的Button类所做的一样,你为开/关按钮创建了一个特殊的类,名为OnOffButton(这并不奇怪)。该类是SpriteGameObject的子类,它期望一个精灵条包含两个精灵:一个用于关闭状态,一个用于开启状态(参见图 19-1 )。

9781430265382_Fig19-01.jpg

图 19-1 。用于开/关按钮的精灵条

按钮的一个重要方面是你需要能够读取和设置它是开还是关。因为按钮基于长度为 2 的 sprite 条,所以您可以定义如果工作表索引为 0,按钮处于 off 状态,如果工作表索引等于 1,按钮处于 on 状态。然后,您可以添加一个获取和设置该值的布尔属性。该属性的定义如下:

Object.defineProperty(OnOffButton.prototype, "on",
    {
        get: function () {
            return this.sheetIndex === 1;
        },
        set: function (value) {
            if (value)
                this.sheetIndex = 1;
            else
                this.sheetIndex = 0;
        }
    });

最后,您需要处理按钮上的鼠标点击,以切换按钮的开和关状态。与您在Button类中所做的类似,您在handleInput方法中检查鼠标左键是否被按下,以及鼠标位置是否在按钮的边界框内。对于触摸输入,您遵循类似的过程。如果播放器已按下按钮,则需要修改纸张索引。如果表索引是 0,它应该变成 1,反之亦然。下面是完整的handleInput方法:

OnOffButton.prototype.handleInput = function (delta) {
    if (!this.visible)
        return;
    if (Touch.containsTouchPress(this.boundingBox) ||
        Mouse.containsMousePress(this.boundingBox))
        this.sheetIndex = 1 - this.sheetIndex;
};

请注意,只有当按钮可见时才处理输入。在PenguinPairsGameWorld类中,你添加一个OnOffButton实例到游戏世界,在期望的位置:

this.onOffButton = new OnOffButton(sprites.button_offon, ID.layer_overlays);
this.onOffButton.position = new Vector2(650, 340);
this.onOffButton.on = GameSettings.hints;
this.add(this.onOffButton);

在这个例子中,您使用了一个变量GameSettings,其中存储了与游戏相关的任何设置。在这种情况下,您需要维护一个指示是否应该在屏幕上显示提示的设置。稍后,本章将对此进行更详细的讨论。

添加滑块按钮

接下来添加第二种 GUI 控件:滑块。这个滑块将控制游戏中背景音乐的音量。它由两个 sprite 组成:一个代表工具条的 back sprite 和一个代表实际滑块的 front sprite。因此,Slider类继承自GameObjectList。因为 back sprite 有边框,所以在移动或绘制滑块时需要考虑到这一点。因此,您还需要定义左右边距,这些边距定义了 back sprite 左右两侧的边框宽度。您还可以将滑块定位在比背景精灵略低的位置,以适应顶部边框。完整的构造函数如下:

function Slider(sliderback, sliderfront, layer) {
    GameObjectList.call(this, layer);
    this.dragging = false;
    this.draggingId = -1;
    this.leftmargin = 5;
    this.rightmargin = 7;

    this.back = new SpriteGameObject(sliderback);
    this.front = new SpriteGameObject(sliderfront, 1);
    this.front.position = new Vector2(this.leftmargin, 8);
    this.add(this.back);
    this.add(this.front);
}

正如你所看到的,你还设置了一个布尔变量draggingfalse和一个变量draggingId为-1。您需要这些变量来跟踪玩家何时拖动滑块以及触摸 ID 是什么,以便在需要时更新滑块位置,即使鼠标指针/触摸位置不在 back sprite 的边界内。

下一步是添加一个属性value ,允许您检索和设置滑块的值。您希望值为 0 表示滑块完全移动到左侧,值为 1 表示滑块完全移动到右侧。你可以通过查看精灵的位置来计算当前值,并查看它向右移动了多少。因此,以下代码行根据滑块位置计算滑块值:

return (this.front.position.x - this.back.position.x - this.leftmargin) /
       (this.back.width - this.front.width - this.leftmargin - this.rightmargin);

在分数的上半部分,计算前面的精灵向右移动了多远。您可以根据后面的位置加上左边距来计算。然后用它除以滑块可以移动的总长度。这条return指令形成了 tT5【何】T1value房产的一部分。对于属性的set部分,您需要将一个介于零和一之间的值转换为前滑块x-位置。这相当于重写先前的公式,使得前 x 位置是未知的,然后计算如下:

var newxpos = value * (this.back.width - this.front.width - this.leftmargin –
    this.rightmargin) + this.back.position.x + this.leftmargin;

剩下要做的就是用正确的 x 位置创建新的前方位置向量:

this.front.position = new Vector2(newxpos, this.front.position.y);

现在您已经有了设置和获取滑块值的方法,您需要编写代码来处理玩家输入。类似于你在以前的课程中所做的,你分别处理触摸和鼠标输入。对于每种类型的输入,添加一个特定的方法,然后从handleInput方法中调用:

Slider.prototype.handleInput = function (delta) {
    GameObjectList.prototype.handleInput.call(this, delta);
    if (Touch.isTouchDevice) {
        this.handleInputTouch(delta);
    } else {
        this.handleInputMouse(delta);
    }
};

您仍然需要处理触摸输入来将滑块拖动到新的位置。第一步是检查玩家是否正在触摸屏幕。如果不是这样,您只需将拖动状态变量重置为初始值,就大功告成了:

if (!Touch.isTouching) {
    this.dragging = false;
    this.draggingId = -1;
    return;
}

如果执行了写在这个if指令之后的指令,你就知道玩家正在触摸屏幕。

你需要检查玩家是否真的触摸了按钮。如果是这种情况,您可以为拖动状态变量指定新值:

if (Touch.containsTouch(this.back.boundingBox)) {
    this.draggingId = Touch.getIndexInRect(this.back.boundingBox);
    this.dragging = true;
}

如果玩家正在拖动,最后一步是更新滑块位置。第一步是检索玩家触摸屏幕的位置:

var touchPos = Touch.getPosition(this.draggingId);

接下来你计算滑块的 x 位置应该是什么。因为触摸位置是在世界坐标中,所以从它减去 back sprite 的世界位置来获得滑块的局部位置。你还要从这个数字中减去滑块宽度的一半,这样玩家触摸屏幕的地方就在滑块的中心。这是使用以下表达式计算的:

touchPos.x - this.back.worldPosition.x - this.front.width / 2

但是,您需要做更多的工作,因为您必须确保滑块不能移动到其范围之外。因此,您需要将滑块位置固定在一定范围内。在 JavaScript 中,您可以动态地用额外的方法来扩充对象,所以让我们给可以执行这个箝位操作的Math对象添加一个方法:

Math.clamp = function (value, min, max) {
    if (value < min)
        return min;
    else if (value > max)
        return max;
    else
        return value;
};

现在您使用该方法来计算滑块的箝位值,并将其存储为滑块的新x-位置:

this.front.position.x = Math.clamp(touchPos.x - this.back.worldPosition.x -
    this.front.width / 2, this.back.position.x + this.leftmargin,
    this.back.position.x + this.back.width - this.front.width –
    this.rightmargin);

这就完成了处理触摸输入的代码。

你用非常相似的方式处理鼠标输入。看看Slider类,看看完整的handleInputTouchhandleInputMouse方法。

PenguinPairs类中,你给游戏世界添加了一个滑块:

this.musicSlider = new Slider(sprites.slider_bar, sprites.slider_button,
    ID.layer_overlays);
this.musicSlider.position = new Vector2(650, 500);
this.add(this.musicSlider);

然后,您可以使用该类中的value属性来设置滑动条,使其与背景音乐的当前音量相匹配,只需一行代码:

this.musicSlider.value = sounds.music.volume;

最后,在PenguinPairsGameWorld类的update方法中,您检索滑块的当前值,并使用它来更新背景音乐的音量:

sounds.music.volume = this.musicSlider.value;

注意大多数游戏都包含一些菜单屏幕。通过这些屏幕,玩家可以设置选项、选择关卡、观看成就和暂停游戏。创建所有这些额外的屏幕可能是大量的工作,对实际的游戏没有贡献,所以开发者倾向于在它们上面投入较少的精力。但这是一个非常错误的决定。

一位艺术家曾经说过,“你的游戏和它最差的屏幕一样好。”如果其中一个菜单屏幕质量很差,玩家会觉得游戏没有完成,开发者没有付出足够的努力。因此,确保你所有的菜单屏幕看起来漂亮,易于使用和导航。

仔细想想你在这些屏幕里放了什么。你可能会想为所有的东西创建选项:游戏的难度、播放的音乐、背景的颜色等等。但是请记住,你是应该创造游戏的人,而不是玩家。你或你的艺术家应该决定什么能给游戏带来最有趣的玩法和最引人注目的视觉风格,而不是用户。

尽量避免选项。比如玩家真的应该设置难度吗?难道不能通过监控玩家的进度来自动适应难度吗?你真的需要一个关卡选择屏幕吗?你不能简单地记起玩家最后一次在哪里,然后立即在那里继续吗?让你的界面尽可能简单!

读取和存储游戏设置

让滑块控制背景音乐的音量并不复杂。现在,假设您想要一个开/关按钮来控制玩家是否可以按下按钮来查看提示。您应该将此选项信息存储在哪里?在这个例子中,它存储在一个名为GameSettings的特殊变量中。在声明spritessounds变量的同一个地方声明这个变量——即PenguinPairs.js文件:

var GameSettings = {
    hints: true
};

这个变量现在随处可见。但是,请注意,它不会在游戏中持续存在:如果您关闭游戏,稍后在浏览器中打开它,该变量将默认为其原始设置。在第 21 章中,你会看到一种跨不同游戏维护数据的方法。

在选项菜单中,确保该变量的值始终与提示按钮的状态相同。这是在PenguinPairsGameWorldupdate方法中完成的,您使用添加到按钮的on属性:

GameSettings.hints = this.onOffButton.on;

这就完成了选项菜单。图 19-2 显示了它的样子。

9781430265382_Fig19-02.jpg

图 19-2PenguinPairs2示例的屏幕截图

你学到了什么

在本章中,您学习了:

  • 如何创建带有各种按钮和滑块的菜单
  • 如何检索按钮和滑块的值,并将这些信息转换成游戏设置