在JewelJam游戏中,您看到了一些向游戏中添加GUI元素的基本示例,如按钮或框架。在这一章中,您将向PenguinPairs游戏添加一些GUI元素,例如开/关按钮和滑块按钮。您还将看到如何读取和存储游戏设置,如音乐音量和是否允许提示。
设置菜单
当想到菜单时,你可能会想到下拉菜单(如文件或编辑)或应用顶部的按钮。但是,菜单可以是灵活的,尤其是在游戏中,菜单通常是按照游戏的风格设计的,并且在许多情况下可以覆盖部分屏幕甚至整个屏幕。作为一个例子,让我们看看如何定义一个包含两个控件的基本选项菜单屏幕:一个用于打开或关闭提示,另一个用于控制音乐的音量。首先,您需要绘制这些控件周围的元素。向菜单添加背景,然后添加文本标签来描述提示控件。你可以使用宝石果酱游戏中的Label类来实现。你定义应该绘制的文本,并把它放在适当的位置(下面的代码摘自PenguinPairsGameWorld):
varbackground=newSpriteGameObject(sprites.background_options,ID.layer_background);this.add(background);varonOffLabel=newLabel("Arial","60px",ID.layer_overlays);onOffLabel.text="Hints";onOffLabel.position=newVector2(150,360);onOffLabel.color=Color.darkBlue;this.add(onOffLabel);同样,为音乐音量控制器添加一个文本标签。有关完整代码,请参见本章的PenguinPairs2示例。
添加开/关按钮
按钮的一个重要方面是你需要能够读取和设置它是开还是关。因为按钮基于长度为2的sprite条,所以您可以定义如果工作表索引为0,按钮处于off状态,如果工作表索引等于1,按钮处于on状态。然后,您可以添加一个获取和设置该值的布尔属性。该属性的定义如下:
Object.defineProperty(OnOffButton.prototype,"on",{get:function(){returnthis.sheetIndex===1;},set:function(value){if(value)this.sheetIndex=1;elsethis.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实例到游戏世界,在期望的位置:
添加滑块按钮
接下来添加第二种GUI控件:滑块。这个滑块将控制游戏中背景音乐的音量。它由两个sprite组成:一个代表工具条的backsprite和一个代表实际滑块的frontsprite。因此,Slider类继承自GameObjectList。因为backsprite有边框,所以在移动或绘制滑块时需要考虑到这一点。因此,您还需要定义左右边距,这些边距定义了backsprite左右两侧的边框宽度。您还可以将滑块定位在比背景精灵略低的位置,以适应顶部边框。完整的构造函数如下:
functionSlider(sliderback,sliderfront,layer){GameObjectList.call(this,layer);this.dragging=false;this.draggingId=-1;this.leftmargin=5;this.rightmargin=7;this.back=newSpriteGameObject(sliderback);this.front=newSpriteGameObject(sliderfront,1);this.front.position=newVector2(this.leftmargin,8);this.add(this.back);this.add(this.front);}正如你所看到的,你还设置了一个布尔变量dragging为false和一个变量draggingId为-1。您需要这些变量来跟踪玩家何时拖动滑块以及触摸ID是什么,以便在需要时更新滑块位置,即使鼠标指针/触摸位置不在backsprite的边界内。
下一步是添加一个属性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位置是未知的,然后计算如下:
varnewxpos=value*(this.back.width-this.front.width-this.leftmargin–this.rightmargin)+this.back.position.x+this.leftmargin;剩下要做的就是用正确的x位置创建新的前方位置向量:
this.front.position=newVector2(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;}如果玩家正在拖动,最后一步是更新滑块位置。第一步是检索玩家触摸屏幕的位置:
vartouchPos=Touch.getPosition(this.draggingId);接下来你计算滑块的x位置应该是什么。因为触摸位置是在世界坐标中,所以从它减去backsprite的世界位置来获得滑块的局部位置。你还要从这个数字中减去滑块宽度的一半,这样玩家触摸屏幕的地方就在滑块的中心。这是使用以下表达式计算的:
touchPos.x-this.back.worldPosition.x-this.front.width/2但是,您需要做更多的工作,因为您必须确保滑块不能移动到其范围之外。因此,您需要将滑块位置固定在一定范围内。在JavaScript中,您可以动态地用额外的方法来扩充对象,所以让我们给可以执行这个箝位操作的Math对象添加一个方法:
Math.clamp=function(value,min,max){if(value
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类,看看完整的handleInputTouch和handleInputMouse方法。
在PenguinPairs类中,你给游戏世界添加了一个滑块:
this.musicSlider=newSlider(sprites.slider_bar,sprites.slider_button,ID.layer_overlays);this.musicSlider.position=newVector2(650,500);this.add(this.musicSlider);然后,您可以使用该类中的value属性来设置滑动条,使其与背景音乐的当前音量相匹配,只需一行代码:
this.musicSlider.value=sounds.music.volume;最后,在PenguinPairsGameWorld类的update方法中,您检索滑块的当前值,并使用它来更新背景音乐的音量:
sounds.music.volume=this.musicSlider.value;注意大多数游戏都包含一些菜单屏幕。通过这些屏幕,玩家可以设置选项、选择关卡、观看成就和暂停游戏。创建所有这些额外的屏幕可能是大量的工作,对实际的游戏没有贡献,所以开发者倾向于在它们上面投入较少的精力。但这是一个非常错误的决定。
一位艺术家曾经说过,“你的游戏和它最差的屏幕一样好。”如果其中一个菜单屏幕质量很差,玩家会觉得游戏没有完成,开发者没有付出足够的努力。因此,确保你所有的菜单屏幕看起来漂亮,易于使用和导航。
仔细想想你在这些屏幕里放了什么。你可能会想为所有的东西创建选项:游戏的难度、播放的音乐、背景的颜色等等。但是请记住,你是应该创造游戏的人,而不是玩家。你或你的艺术家应该决定什么能给游戏带来最有趣的玩法和最引人注目的视觉风格,而不是用户。
尽量避免选项。比如玩家真的应该设置难度吗?难道不能通过监控玩家的进度来自动适应难度吗?你真的需要一个关卡选择屏幕吗?你不能简单地记起玩家最后一次在哪里,然后立即在那里继续吗?让你的界面尽可能简单!
读取和存储游戏设置
varGameSettings={hints:true};这个变量现在随处可见。但是,请注意,它不会在游戏中持续存在:如果您关闭游戏,稍后在浏览器中打开它,该变量将默认为其原始设置。在第21章中,你会看到一种跨不同游戏维护数据的方法。
在选项菜单中,确保该变量的值始终与提示按钮的状态相同。这是在PenguinPairsGameWorld的update方法中完成的,您使用添加到按钮的on属性: