GameMakerHTML5游戏开发全绝不原创的飞龙

HTML5的引入彻底改变了Web浏览器作为一个合法的游戏平台,具有无限的潜力。制作浏览器游戏从未如此简单,特别是使用GameMakerStudio。

本书指导您如何轻松有效地使用高级功能,包括数据结构,并演示如何用简单的解释和视觉示例创建刚体物理。通过本书,您将深入了解如何使用GameMaker开发和发布在线社交浏览器游戏。

第一章,通过你的第一个游戏了解Studio,将帮助你制作自己的游戏。您将有机会探索GameMaker:Studio界面。在本章中,我们将创建和实现所有类型的资源,同时利用各种资源编辑器。

第二章,三A游戏:艺术和音频,将帮助您了解艺术和音频在GameMaker:Studio中的工作原理。它将涵盖可接受的图像格式以及如何导入精灵表。在本章中,我们将创建一个瓷砖集,它将更好地利用计算机内存,并允许创建大型独特的世界,并了解如何控制声音以及它们被听到的方向。

第三章,射击游戏:创建横向卷轴射击游戏,将帮助您创建您的第一个横向卷轴射击游戏。在本章中,我们将应用所有三种移动方法:手动调整X和Y坐标,并设置速度和方向。我们将能够动态地向游戏世界添加和删除实例。

第四章,冒险开始,通过将键盘检查和碰撞预测放入单个脚本中,简化了玩家控制。它涵盖了处理精灵动画的几种方法,从旋转图像到设置应显示哪些精灵。我们将通过接近检测和路径查找来处理人工智能。

第五章,平台乐趣,深入探讨系统设计和创建一些非常有用的脚本。我们将构建一个动画系统,游戏中的大多数对象都会使用,并预测碰撞,并将我们自己的自定义重力应用于玩家。最后,我们将利用我们之前的知识和新系统创建一个三阶段的Boss战。

第六章,倾覆的塔,涵盖了使用Box2D物理系统的基础知识。我们将学习如何为对象分配Fixture以及可以修改的不同属性。我们将创建一个利用旋转关节的链条和破坏球,以便每个部分都会随着前面的部分旋转。此外,本章涵盖了绘制GUI事件以及精灵在房间中的位置与屏幕上的位置之间的区别。

第七章,动态前端,包括添加整个前端,包括商店和可解锁级别。我们将处理网格,地图和列表数据结构,以保存各种信息。我们将重建HUD,以便显示更多按钮,仅显示可用设备,并构建基本倒计时器。最后,我们将添加一个保存系统,教会我们如何使用本地存储,并允许我们拥有多个玩家保存。

第八章,玩转粒子,将向您展示如何添加一些细节和修饰,使我们的游戏真正闪耀。我们将深入研究粒子的世界,并创建各种效果,为TNT和柱子的破坏增添影响力。游戏现在已经完成,准备发布。

附录,拖放图标到GameMaker语言参考,将帮助我们了解每个图标的功能,因为每个图标通常不止一个功能。该附录提供了所有拖放图标的代码等效的彻底参考。

这本书需要GameMaker:Studio专业版与HTML5导出模块,以及一个符合HTML5标准的浏览器(GoogleChrome效果最好)。

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码单词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟URL,用户输入和Twitter句柄显示如下:“创建一个新的声音并命名为snd_Collect”。

代码块设置如下:

isWalking=false;if(keyboard_check(vk_right)&&place_free(x+mySpeed,y)){x+=mySpeed;myDirection=0;sprite_index=spr_Player_WalkRight;isWalking=true;新术语和重要单词以粗体显示。例如,屏幕上看到的单词,菜单或对话框中的单词会出现在文本中,如:“点击下一个按钮会将您移至下一个屏幕”。

警告或重要说明会出现在这样的框中。

提示和技巧会出现在这样。

欢迎来到使用GameMaker进行HTML5游戏开发!您即将进入令人兴奋的网络游戏开发世界。如果您以前从未使用过GameMaker:Studio,本书将向您展示有关使用该软件、制作游戏以及将其上载到互联网的一切。如果您以前有GameMaker:Studio的经验,但这是您首次尝试HTML5,本书将帮助您更好地了解开发独立游戏和基于浏览器的游戏之间的区别。随意浏览本章并转到项目。

现在,如果您仍在阅读本文,我们可以假设您想了解更多关于这个软件的信息。您可能会问自己,“为什么我应该使用GameMaker:Studio?HTML5模块给我什么功能?说到底,HTML5是什么,我为什么要关心?”所有这些都是很好的问题,让我们试着回答它们。

GameMaker:Studio是一个非常强大且易于使用的开发工具,用于制作游戏。该软件最初是设计用于课堂环境,作为学生学习基本编程概念、了解游戏架构和创建功能齐全的游戏的方式。因此,由于拖放式编码系统,开发环境对于初次使用者来说非常直观。与许多其他具有类似功能的竞争开发工具不同,GameMaker:Studio具有非常强大的脚本语言,允许用户创建几乎可以想象的任何东西。再加上您可以轻松导入和管理图形和音频资源,集成了出色的Box2D物理库以及内置的源代码控制,为什么不使用它呢?直到现在,制作游戏通常意味着您正在创建一个独立的产品。

互联网并不是真正的考虑,因为它相当静态,并且需要一堆专有插件来显示动态内容,例如游戏、电影和音频。然后,HTML5出现并改变了一切。HTML5是一组开放标准的代码语言,允许任何人开发交互式体验,并能够在具有现代浏览器和互联网连接的任何设备上本地运行。开发人员现在能够使用尖端功能,例如WebGL(一种允许进行3D渲染的图形库)、音频API和资产管理,来推动在浏览器中所能做的事情的边界。

通常,为HTML5开发游戏需要对三种不同的编码语言有所了解:HTML5(超文本标记语言),用于创建网页结构的代码语言,CSS3(层叠样式表3),用于确定网站的呈现方式,以及实际实现魔术的JavaScript。GameMaker:StudioHTML5导出模块通过允许开发人员在集成环境中工作并通过按下按钮导出到这些语言,使所有这些变得简单。除了作为游戏引擎之外,HTML导出模块还包括用于处理URL和浏览器信息的特定功能。它还配备了自己的本地服务器软件,可以让您测试游戏,就好像它实时上网一样。最后,您可以进一步扩展GameMaker:Studio,因为它允许您导入外部JavaScript库,以获取您可能需要或想要的任何功能。听起来很棒,不是吗?现在让我们启动Studio。

为了使用本书,我们需要一些软件。首先,我们需要一个HTML5兼容的浏览器,如MozillaFirefox,MicrosoftInternetExplorer9.0,或者为了获得最佳效果,GoogleChrome。其次,我们需要购买并安装GameMaker:Studio专业版和HTML5导出模块。一旦我们拥有了所有这些,我们就可以开始制作游戏了!

请注意,GameMaker:Studio专业版和HTML5导出模块是两个单独的项目,您需要拥有两者才能为网络创建游戏。

GameMaker:Studio已经准备就绪,让我们开始一个项目吧!

现在我们已经安装并运行了软件,让我们来看看界面。GameMaker:Studio的基本布局可以分为四个组件:菜单、工具栏、资源树和工作区。我们将在本书中探索这些组件,所以不要期望对每个项目进行详细分解。这不仅会让阅读变得枯燥无味,还会延迟我们制作游戏。相反,让我们专注于我们现在需要知道的东西。

首先,与大多数复杂软件一样,每个组件都有自己的方式让用户执行最常见的任务。例如,如果要创建一个精灵,可以导航到菜单|资源|创建精灵,或者单击工具栏中的创建精灵按钮,或者在资源树中右键单击精灵组,或者使用Shift+Ctrl+S在工作区中打开精灵编辑器窗口。实际上,还有更多的方法可以做到这一点,但您明白了。

虽然有很多重叠的功能,但也有许多事情只能在每个特定的组件中完成。以下是我们需要知道的内容。

工具栏使用简单的图形图标来表示我们将要使用的最常见的编辑器和工具。这些按钮是创建新资产和运行游戏的最简单、最快速的方式,所以预计会经常使用这些按钮。工具栏上有一个非常重要的独特元素:目标下拉菜单。目标确定我们将编译和导出到哪种格式。将其设置为HTML5。

目标菜单的默认设置是Windows,所以确保将其更改为HTML5。

工作区是各种编辑器将打开的地方。运行游戏时,编译器信息框将出现在底部,并在运行游戏时显示正在编译的所有内容。还有一个源控制选项卡,如果您有一个SVN客户端和用于团队合作的存储库,可以使用它。

您可以将每个可以引入GameMaker:Studio的资源都有自己的属性编辑器。为了熟悉它们中的每一个,我们将构建一个非常简单的猫鼠游戏。我们将创建一个玩家角色(一只老鼠),可以在房间中移动,收集物品(奶酪),并避开敌人(一只猫)。让我们立即开始创建一些精灵。

精灵是用于对象的图形表示的位图图像。这些可以是单个图像或一系列动画图像。GameMaker有自己的图像编辑器来创建这些,但也允许导入JPG、GIF、PNG和BMP文件。

在我们的示例中,我们将首先创建两个精灵;一个用于墙,一个用于玩家角色。如果您已经下载了支持文件,我们在Chapter_01文件夹中提供了这些图像文件。

我们将从一个简单的精灵开始,它将代表我们游戏的墙。

这个游戏中的玩家将是一个鼠标,精灵由两帧动画组成。

恭喜!你已经创建了你的第一个精灵。在下一章中,我们将更深入地探讨艺术资源的创建,所以让我们继续到对象。

这就是GameMaker:Studio真正展示其实力的地方。对象可以被看作是容器,其中包含了我们希望游戏中的每个项目执行的属性、事件和功能。当我们将一个对象放入游戏世界时,它被称为实例,它将独立于该对象的所有其他实例运行。

在我们继续之前,理解对象和对象的实例之间的区别是很重要的。对象是描述某物的一组规则,而实例是该某物的独特表示。一个现实世界的例子是你是人对象的一个实例。人是有手臂、腿、说话、睡觉等特征的东西。你是这些元素的独特解释。这个概念的一个例子可以在前面的图表中看到。

这很重要的原因是,根据所使用的功能,效果将被应用于该类型的所有项目或个别项目。一般来说,你不会希望射击一个敌人然后世界上所有的敌人都死掉,对吧?

继续我们的例子,我们将创建一个墙对象和一个玩家对象。墙将是一个固定的障碍物,而玩家将有控制,使其能够在世界中移动并与墙碰撞。

我们将从实体墙对象开始,我们可以用它来创建迷宫供玩家使用。

GameMaker处理与实体对象的碰撞与非实体对象的碰撞方式不同。如果实体对象和非实体对象发生碰撞,GameMaker会尝试通过将非实体对象移回其先前的位置来防止它们重叠。当然,为了正确地做到这一点,实体对象必须是静止的。因此,我们应该将实体属性添加到墙上。

实体属性应该只用于不移动的对象。

玩家对象将向我们介绍使用事件和动作来进行移动和碰撞等操作。

GameMaker的强大之处在于其事件驱动系统。事件是游戏运行过程中发生的时刻和动作。当您向对象添加事件时,您要求该项在发生该动作时做出响应,然后应用指定的指令。

事件需要动作来应用它们才能发挥作用。GameMaker:Studio使用拖放(DnD)系统,其中代表常见行为的图标可以很容易地实现。这些行为根据功能分为七个不同的选项卡。在本书的绝大部分内容中,我们将只使用在常见选项卡中找到的执行脚本图标,因为我们将编写放置在脚本中的代码。然而,在本章中,我们将使用DnD动作,以便您了解它们的作用。

移动固定图标

现在我们有一个对象,当按下箭头键时会在世界中移动。但是,如果我们运行这个程序,一旦开始移动,我们将无法停止。这是因为我们正在给对象应用速度。为了停止对象,我们需要给它一个速度为零。

我们需要做的最后一件事是添加碰撞检测。在GameMaker:Studio中,碰撞是由两个实例组成的单个事件。每个实例都能在这个单一碰撞上执行一个事件调用,尽管通常将代码放在其中一个上更有效。在我们的情况下,将碰撞事件放在玩家身上,当它与墙碰撞时,这是有意义的,因为玩家将是执行动作的实例。墙将保持原样,什么也不做。

演员已经准备好了;我们有一些可以看到并且可以做一些事情的对象。现在我们需要做的就是把它们放到一个房间里。

房间代表我们对象实例所在的世界。您创建的大多数房间可能会被用作各种级别,但房间也可以用于:

我们想要布置一个包含玩家并呈现一些障碍物的世界。为此,我们将在房间的外缘放置墙对象,并在中心放置几条线。

不要忘记添加玩家!

在创建游戏时,有三种不同类型的编译可以进行。如果游戏已经完成了100%,您可以选择创建应用程序以用于目标平台。如果游戏仍在开发中,有正常运行,它将编译并运行游戏,就像它是一个应用程序一样,还有调试模式运行,它运行调试工具。

让我们不再等待。通过导航到运行|运行游戏,或者按下F5来运行游戏。

如果一切正常,玩家对象应该能够使用箭头键在世界中移动,但不能通过任何墙对象。然而,有一些地方不太对。玩家对象似乎在闪烁,因为它是动画的。让我们在查看脚本属性编辑器时修复这个问题。

GameMaker:Studio利用自己的专有脚本语言称为GameMakerLanguage,又称为GML。这种语言被开发成非常适合初学者使用,并利用了一些在其他脚本语言中可能找不到的功能。例如,GML将接受标准表达式&&来组合两个比较,或者替代地使用单词and。GameMaker:Studio通过提供一组出色的函数、变量和常量,在创建游戏时做了大量的工作。

如前所述,我们希望停止玩家对象的动画。使用脚本非常容易实现这一点。

image_speed=0;为了使脚本运行,我们需要将其附加到一个对象上。

执行脚本图标

现在我们可以运行游戏,我们会发现玩家对象不再动画。

背景是一种特殊的艺术资源,有两种不同的类型:背景图片和瓷砖集。与精灵不同,背景从不作为艺术资源的一部分进行任何动画。背景图片主要用作房间的大背景,并且在需要背景移动时非常有用。瓷砖集是可以用来绘制背景的小艺术片段,非常适合创建大型、独特的世界,并且可以保持图形成本的计算低。

如果需要,可以使用背景图片:

如果需要,可以使用瓷砖集:

对于这个简单的例子,我们将只创建一个静态背景。我们将在下一章更深入地了解瓷砖集:

现在我们已经准备好艺术资源,只需要将其放置到房间中。

每个房间最多可以同时显示八个背景。这些背景也可以用作前景元素。如果没有激活背景,它将显示为纯色。

让我们再次运行游戏,现在我们可以看到我们有了一个背景。事情看起来确实更好了,但是缺少了一些东西。让我们给游戏加点声音。

声音属性编辑器是您可以引入用于游戏的声音的地方。GameMaker只允许引入MP3和WAV文件。您可以使用两种类型的声音:

正常声音都是你听到的小声音效,比如枪声和脚步声。这些通常应该是WAV文件。背景音乐是指较长的声音,比如游戏音乐,还有一些像口语对话之类的东西。这些应该是MP3格式。

当GameMaker:Studio为HTML5导出游戏音频时,所有声音都将转换为MP3和OGG格式。这是因为不同的浏览器在实现HTML5音频标签时使用不同的音频文件格式。幸运的是,GameMaker:Studio会自动将浏览器识别代码添加到游戏中,所以游戏知道正在使用哪些文件。

我们将为游戏创建两种声音,一些背景音乐和一个可收集物品的音效。

让我们为我们的游戏引入一些音乐,以帮助营造一些氛围。

我们希望音乐在游戏开始时立即开始。为此,我们将创建一个名为霸主的数据对象。数据对象通常不会在游戏中显示,所以我们不需要为它分配一个精灵。

我们将使用一个霸主对象来监视游戏并控制一些东西,比如音乐和胜利/失败条件。

播放声音图标

在我们测试之前,我们需要确保霸主在世界中。当你把它放在一个房间里时,它将被一个小蓝色圆圈图标代表,如下面的截图所示:

让我们运行游戏并听一下。音乐应该立即开始播放并无限循环。让我们继续创建一个可收集的物品。

我们将创建一个玩家在游戏中可以收集的物品。当玩家与其碰撞时,声音将被播放一次。

我们还没有为此创建一个对象,也没有引入一个精灵。现在是你测试记忆的机会。我们只会快速地复习一下我们需要的东西。

现在,当玩家与对象发生碰撞时,它将播放一次声音。这是一个良好的开始,但为什么我们不给玩家更多的奖励呢?

设置分数图标

现在我们有值得收集的东西。只剩下一个问题,那就是我们只是碰到对象就得到了分数和声音。我们不能让这种情况永远持续下去!

销毁实例图标

让我们在房间里放置一些这些可收集物,并运行游戏。我们应该能够在世界中移动玩家并与可收集物发生碰撞。我们应该听到声音播放并且对象消失。但是,我们的分数在哪里呢?嗯,在显示它之前,我们需要引入一些文本。

您可以导入字体以在游戏中使用它们作为文本。这些字体需要安装在您的机器上,以便在开发过程中使用。每个字体资源都设置为特定的字体类型、大小,以及是否为粗体/斜体。如果您想要稍微变化,比如一个字体大两个点,那么必须创建一个单独的字体资源。这是因为在导出时,GameMaker将把字体转换为图像,这样就可以在用户的机器上使用而不需要预先安装字体。

我们将创建一个用于显示游戏分数的字体。

现在我们有一个可以在游戏中使用的字体。为此,我们将让Overlord对象在屏幕顶部绘制游戏分数。我们还将使文本为白色,并将其居中对齐。

绘制事件发生在每个步骤的最后,在所有计算完成并需要在屏幕上显示之后。绘制GUI事件用于显示游戏中的悬浮显示,并始终呈现在所有其他游戏图形的顶部。

设置颜色图标

设置字体图标

现在我们可以运行游戏,得分现在将显示在屏幕顶部的中心位置。现在,当您与可收集物品碰撞时,您应该看到得分增加。

路径是为对象创建复杂移动模式的最佳方式。路径由一系列点组成,对象可以沿着这些点移动。点之间的过渡可以是直线的,这意味着对象将精确地到达每个点,也可以是曲线的,是三个点之间的插值。路径可以是开放线或闭合循环。以下屏幕截图将在本节中用作参考图像。

我们将创建一个简单的敌人,它将沿着房间周围的路径移动。如果玩家与敌人碰撞,玩家将被销毁。让我们从创建路径开始。

要为路径添加一个点,您可以在地图的任何位置单击左键。第一个点将由绿色方块表示,其后的所有点将是圆圈。

路径已准备就绪,现在我们只需要创建一个敌人并将路径附加到它。这个敌人将简单地沿着路径移动,如果它与玩家碰撞,它将重新开始游戏。

设置路径图标

我们现在有一个准备好跟随路径的敌人,但它对玩家并不构成威胁。让我们在敌人上添加一个碰撞事件,并使其在接触时重新开始游戏。

重新开始游戏图标

现在游戏中有一些风险,但奖励还不够。让我们来解决这个问题,好吗?

创建移动图标

就是这样!运行游戏,您应该看到可收集物品在两秒后开始生成,并将继续无限地生成。正如您从下一张截图中看到的,我们的游戏已经完成,但还有一个组件我们需要看一看。

无论您在脚本编写和制作游戏方面有多么经验,错误总是会发生。有时可能是拼写错误或缺少变量,在这种情况下,GameMaker:Studio会捕捉到并显示代码错误对话框。其他时候,游戏可能不会按照您的期望进行,比如在不应该通过墙壁时却通过了。在这种情况下,代码在技术上没有问题,只是构造不当。追踪这些错误可能非常乏味,如果没有调试工具,可能会是不可能的。为了使用这些工具,游戏必须在调试模式下运行,您可以通过单击工具栏中的运行调试模式按钮或转到菜单并导航到运行|运行调试模式来访问。

在调试模式下,我们可以利用调试消息来帮助我们理解游戏中发生的情况。这些消息只能通过脚本编写时使用show_debug_message()函数实现(没有拖放选项),并且每当执行该函数时,它们将出现在控制台窗口中。您可以使用这个来传递一个字符串或显示一个变量,以便您可以将结果与您期望的结果进行比较。这是您在尝试解决问题时的第一道防线。

我们应该使用的第一个控制台是GameMaker:Studio的HTML5调试控制台。当游戏以HTML5为目标并在调试模式下运行时,将会创建一个弹出窗口,其中包含调试输出,所有调试消息都将显示在其中,以及实例列表和它们的基本数据信息。让我们测试一下这个控制台!

myText="HelloWorld";show_debug_message(myText);提示下载示例代码

我们首先创建一个变量来保存字符串。虽然我们可以直接通过show_debug_message函数传递字符串而不使用变量,但我们将在以后的调试测试中使用这个变量。

恭喜!现在你可以开始调试你的脚本了。虽然在游戏开发过程中你会经常使用show_debug_message,但是保持活跃消息的数量最少是很重要的。有太多调试消息发生,以至于你看不到发生了什么是没有意义的!

虽然你可以通过调试消息解决大部分问题,但有时你需要更详细的了解游戏中发生了什么。GameMaker:Studio有一个更高级的调试器,只有在游戏被定位为Windows版本时才会运行。如果我们不至少粗略地看一下这个精彩的工具,那就不够意思了。

一些基本信息会立即显示出来,比如它的表现如何,通过查看房间速度:(每秒步数)和每秒帧数(FPS:)。如果你把鼠标光标移到游戏中的实例上,你会注意到鼠标id:会改变。这个ID是该特定实例的唯一标识符,非常方便。

GameMaker调试器窗口有更多选项可用于调试游戏。运行菜单不仅允许我们暂停游戏,还可以一步一步地向前走。监视菜单允许您跟踪特定表达式,比如函数调用或属性。工具菜单不仅可以访问调试消息,还可以显示所有全局变量、每个实例的变量,以及当前存在的所有实例的列表。让我们看看这个控制台中实例有什么信息。

我们要看的最后一件事是编译后的JavaScript代码。所有现代浏览器,比如MozillaFirefox、MicrosoftInternetExplorer9.0和GoogleChrome都带有内置的调试控制台,允许任何人查看任何网站的源代码,甚至影响本地屏幕上显示的内容。没错。每个人都可以看到游戏的代码。虽然这可能吓到你,但不用担心!当GameMaker:Studio导出游戏或正常运行时,它会对代码进行混淆,使其非常难以解读。另一方面,在调试模式下运行时,除了引擎本身,它不会进行任何混淆。

让我们快速看一下这段代码是什么样子的。我们将从调试版本开始,这样我们就可以看到没有混淆时它是什么样子的。在这个例子中,我们将使用Chrome,因为它有最强大的调试控制台。

嗯,就是这样。在本书的第一章中,您已经制作了自己的第一个HTML5游戏。这样做,您有机会探索GameMaker:Studio界面并熟悉它。您还创建并实现了所有类型的资源,同时利用了各种资源编辑器。希望您已经意识到,这款软件让您轻松地为网络制作游戏。凭借您已经获得的知识,您可以开始制作更高级的游戏。例如,为什么不添加射击,因为您知道如何使用按键事件,使对象移动,并在碰撞时执行操作?

在下一章中,我们将深入研究资产创建。游戏的好坏取决于它的外观和声音。我们将学习如何创建动画角色,构建一个瓷砖集来装饰一个房间,并使用音频来增加氛围。让我们继续前进,因为事情即将变得更加令人兴奋!

现在我们已经熟悉了界面导航,并建立了一个简单的游戏,我们可以开始创建更复杂的项目。在本章中,我们将专注于创建艺术作品,添加动画,并实现音频音景。这三个元素对于游戏的创建非常重要,因为它们每个都有助于玩家理解发生了什么,并使体验更加沉浸。我们构建游戏的方式可能会受到我们使用的资产类型以及它们的实施方式的极大影响。我们将首先看看如何导入外部图像,然后进行一些实际示例,如如何创建一个瓷砖集并制作一个动画角色。然后我们将转向音频文件,以及如何为游戏添加环境氛围。最后,我们将简要讨论如何使游戏看起来更专业。让我们开始吧!

在创建游戏时,大多数艺术资源都将在外部程序中创建,并且需要导入。GameMaker:Studio确实有一个内置的图像编辑器,我们稍后会进行调查,但其功能相当有限。它非常适合创建简单的艺术作品,但还有许多其他工具可以为我们提供更高级的复杂艺术创作技术。

GameMaker:Studio能够导入四种图像类型:BMP、GIF、JPG和PNG。每种格式都有其独特的功能和缺点,这将决定它们应该如何使用。BMP格式是如今最不常用的格式,因为数据未经压缩。未经压缩的图像通常被认为效率低下,因为它们的文件大小很大。GIF是唯一可以制作动画的格式,但限于256种颜色和单一透明级别。这非常适合经典的8位风格艺术,其中所有内容都有硬边缘。JPG图像由于没有任何透明度和其有损压缩格式,具有最小的文件大小。这是背景和不透明精灵的不错选择。PNG图像格式最有用,因为它们比BMP更有效,具有1600万种颜色和完全透明度,并且这是GameMaker:Studio在编译游戏时输出为纹理页的格式。

在本书中,我们将只使用两种图像格式,GIF和PNG。我们将使用GIF图像来制作所有动画,因为这是导入动画的最简单方式。与上一章一样,如果我们加载一个动画GIF图像,每一帧动画都将在精灵属性编辑器中分开。不幸的是,这意味着我们在角色的艺术风格上受到了限制,由于单一的透明度水平,我们的角色边缘会有硬边。如果我们想要更平滑、更清晰的外观,我们需要使用PNG图像来进行反锯齿处理。试图在GIF图像中获得平滑的边缘是艺术家可能犯的最常见的错误之一。正如我们将在下面的截图中看到的,左侧是一个具有清晰硬边的8位艺术风格的GIF图像,右侧是一个具有平滑、反锯齿边缘的PNG图像。

在中间,我们有相同的平滑精灵,使用PNG保存,但保存为GIF。注意曾经略微透明的边缘像素现在是一个实心的白色轮廓。

尽管本书中的所有动画都将使用GIF图像出于便利性的考虑,但如果我们不介绍如何导入精灵表,那就有失职了。精灵表通常是一个PNG文件,其中包含一个对象(如角色)的所有动画帧,均匀地放置在一个网格中。然后我们可以快速地在GameMaker中剪切出每一帧动画,以构建我们需要的单个精灵。让我们试一试!

使用GameMaker:Studio开发的一个重要好处是它内置了一个用于创建精灵和背景的图像编辑器。这个编辑器可能看起来非常基础,但有很多优秀的可用工具。有各种不同的绘图工具,包括标准工具,如铅笔、橡皮擦和填充。编辑器中一个非常有用且独特的功能是能够用鼠标的两个按钮进行绘画。颜色|左和颜色|右颜色选项,如下图所示,表示根据使用左键或右键,将使用的颜色。我们还可以通过变换和图像菜单调整各种东西。变换菜单包含影响图像中像素大小和位置的能力。图像菜单包含图像修改工具,如改变颜色、模糊图像和添加发光效果。

与其谈论图像编辑器,不如在其中构建一些艺术资源。我们将首先创建一个图块集,然后转移到一个动画角色,这两者都可以在第四章中稍后使用,冒险开始。如果您更愿意在外部编辑器中工作,也可以跟着做,因为创建这些资源的一般理论是普遍适用的。

图块集是一种特殊类型的背景资源,允许游戏在不使用大量计算机内存的情况下在环境中拥有巨大的变化。保持文件大小和内存使用量小是非常重要的,特别是对于HTML5游戏。浏览器需要下载所有这些资源,因为我们不知道用户有多强大的计算机。

创建自然外观的图块集主要是为了欺骗眼睛。我们的眼睛非常擅长发现模式;当有重复时,它们会识别形状、对比和颜色的差异。知道我们的大脑是这样硬编码的,让我们能够利用这一点。我们可以通过使用奇怪的形状、最小化对比和在艺术作品中使用类似的颜色来打破模式。

我们将为游戏中最常见的表面之一创建一个图块集:石头地板。现在这可能看起来很容易,但惊人的是这经常被错误地完成。

在开始绘制一堆石头之前,我们需要首先考虑潜在的问题和解决方案。人们在创建平铺图块时最常见的问题是他们试图直接创建最终产品,而不是逐步构建。这包括在确保可以正确平铺之前选择颜色和添加细节。

在查看平铺纹理时,我们需要确保尽量打破网格。整个世界将基于小的32x32像素图块,但我们不希望观察者注意到这一点。因此,我们的目标是使用不规则的形状,并尽量避免水平和垂直对齐。

通过移动像素,我们现在可以看到边缘是如何铺砌的。你可能会注意到它并不完美。在下面的示例截图中,你可以看到有几条线只是结束了,没有形成完整的石头。你可能也不喜欢某些石头的大小,或者看到一些线条太粗。目标是修复这些问题,并重复这个过程,直到一切都符合你的要求。

一旦我们对瓷砖图案和沿边缘正确重复感到满意,我们就可以开始添加颜色了。一般来说,最好不要使用完全脱饱和的灰色调来代表石头,因为大多数石头都有一些颜色。在选择颜色时,目标是避免只使用单一颜色和明暗变化,而是选择一系列相似的颜色。为此,首先选择一个中性的基础颜色,比如米色。然后,每种额外的颜色都应该在色调、饱和度和亮度上略有变化。例如,第二种颜色可以比第一个米色略微偏红,略微不那么鲜艳,比第一个米色暗一些。

基础瓷砖的最后一步是将深灰色线条改为深褐色。现在你可能会认为这将是非常乏味的,但幸运的是,图像编辑器有一个工具可以让这变得容易。

干得好!现在我们有了一个基础瓷砖,可以用来制作其他所有瓷砖。下一步是添加边框瓷砖,以便有一个用于分隔不同材料的边缘。如果我们要有一个正方形房间,我们将需要总共九块瓷砖:基础瓷砖和代表边缘和角落的八块瓷砖。让我们给我们的画布增加一些空间,并用我们的瓷砖填满它。

太棒了!我们现在有了一个基本的瓷砖集,让我们来测试一下。

可以有多个层的瓷砖,这在您想要放置奇形怪状的瓷砖(树木、路标)时非常有用,而无需为每种表面类型(石地板、草地)创建新的瓷砖。它还可用于编译多个瓷砖以创建更自然的表面,例如在石地板上放置一个泥土瓷砖组。

看起来相当不错,但有一些明显的问题,特别是内角没有边框。您可能还会觉得在这么大的区域里,瓷砖重复得有点太多了。由于我们将为第一个问题创建更多的瓷砖,我们也可以为第二个问题添加一些!

我们还有三个沿底部的瓷砖,我们将用作基础瓷砖的替代品。只要不影响外边缘周围的一个像素边框,我们可以随意更改内部,它仍然可以正确平铺。

正如您所看到的,使用一个小小的128x128纹理,我们可以轻松填满一个大区域,同时提供随机性的错觉。为了增加更多变化,我们可以轻松地创建调色板交换版本,从而可以调整色调和饱和度。因此,我们可以有一个蓝灰色的平铺集。通过更多的练习,我们可以开始添加诸如阴影之类的细节,以增加世界的透视。对于您未来的平铺集,只需记住使用非均匀形状,最小化对比度,并仅轻微变化颜色。更重要的是,始终确保基本平铺正确重复,然后再构建边缘和备选!

动画精灵是一系列静态图像,播放时看起来有动作。它让玩家知道他们正在奔跑,当他们用剑攻击时,以及按钮是可点击的。好的游戏在所有互动元素上都有动画,通常还有许多背景元素上也有动画,以至于您可能甚至都没有注意到。正是诸如动画之类的微小细节真正为游戏注入了生命。

第二条规则是最大化精灵空间。大多数游戏使用基于框的碰撞而不是像素完美的碰撞。因此,您希望尽可能多地利用精灵可用于所需动画的空间。通常开发人员会浪费很多空间,因为他们在考虑现实世界而不是游戏世界。例如,一个常见的问题可以在跳跃动画中看到。在下面的截图中,第一个跳跃动画中的角色从地面起跳,跳到空中,落下并着陆。第二个跳跃动画是一样的,但所有空白空间都被移除了。这不仅更有效,而且还可以帮助防止碰撞错误,因为我们始终知道碰撞框的位置。

最后一个重要规则,可能也是最重要的规则是可重复性。大多数游戏动画在某个时候都会循环,而有一个明显重复的序列对玩家来说会非常刺眼。这种可重复性问题的一个常见原因是动画太多。动画帧数越多,出现问题的可能性就越大。关键在于简单化并删除不需要的帧。在下面的截图中,您可以看到两个奔跑动画,第一个有五帧,第二个只有三帧。顶部的看起来会更流畅一些,但由于步幅的轻微差异,重复性会稍微差一些。第二个最终看起来会更好,因为它的帧数更少,步幅的差异也更小。

牢记这三条规则,让我们来制作一个简单的角色奔跑循环:

现在我们需要一个角色设计。在设计角色时,你需要考虑角色要做什么,他们存在的世界以及碰撞区域。在我们的情况下,角色只会行走,世界将是一个户外冒险游戏,并且会有一个大的方形碰撞框。

如果你不想自己设计角色,我们提供了一个精灵,Chapter_02/Sprites/WalkCycle.gif,其中包含了动画的第一帧。

在前面的截图中设计的角色是一种穿着夹克的猿类生物。穿夹克的原因是在摆动时使手臂更易读。我们可以看到这个角色相当厚,这使得大碰撞区域更加真实。最后,后腿稍微更暗,好像有一个阴影。再次强调,这是为了帮助可读性。

一旦我们对第一帧满意,我们需要继续下一个关键帧。关键帧是动画中发生最大变化的点。在这种情况下,当角色处于最高点时,手臂和腿交叉时就是关键帧。

就是这样!一个不错的循环行走动画,虽然有点生硬。如果我们想要稍微平滑这个动画,只需在关键帧之间添加一帧动画,然后按照刚才进行的相同步骤进行。最终应该看起来类似于以下截图:

音频对于创建专业质量的游戏非常重要。不幸的是,它通常是最被忽视的元素,也是最后实施的。其中一个原因是我们可以在没有音频的情况下玩游戏,仍然享受体验。然而,游戏中良好的声音景观将使其更具沉浸感,并有助于改善用户反馈。

如果添加音频还不够具有挑战性,HTML5会使它变得更加困难。我们将遇到的第一个困难是HTML5音频标签尚未标准化。有两种文件格式竞相成为官方HTML5标准:MP3和OGG。MP3文件格式是最常用的格式之一,但缺点是需要许可和专利,这可能导致支付大额费用。OGG文件格式既是开源又不受专利保护,因此是一个可行的替代方案。除此之外,各种浏览器对文件类型有自己的偏好。例如,InternetExplorer接受MP3但不接受OGG,而Opera接受OGG但不接受MP3。GoogleChrome和MozillaFirefox则支持两种格式。GameMaker:Studio通过在游戏导出时将所有音频转换为MP3和OGG文件格式来解决这个问题。

GameMaker:Studio配备了两种不同的声音引擎来控制游戏中的各种音频:GM:S音频和传统声音。这些系统彼此完全独立,您可以在游戏中使用其中一个系统。

LegacySound引擎是GameMaker使用的原始声音系统,正如其名称所示,这个引擎已不再得到积极开发,并且许多功能已经过时。这是一个更简单的系统,没有3D功能,尽管对于大多数游戏来说这将是足够的。这个引擎的一个很大的好处是音频应该在所有浏览器中都能工作。

在本书中,我们将一直使用LegacySound引擎以确保最大的功能,但我们需要知道如何使用GM:S音频引擎以备将来使用。让我们通过创建一个非常简单的定位声音演示来测试这些功能。我们将在房间中创建一个对象,并使其播放一个只有当鼠标接近位置时才能听到的声音。

sem=audio_emitter_create();audio_emitter_position(sem,x,y,0);audio_emitter_falloff(sem,96,320,1);audio_play_sound_on(sem,snd_Effect,true,1);sem=audio_emitter_create();audio_emitter_position(sem,x,y,0);audio_emitter_falloff(sem,96,320,1);audio_play_sound_on(sem,snd_Effect,true,1);audio_listener_position(mouse_x,mouse_y,0);你应该能够听到声音很轻,并且当你把鼠标移到屏幕中心附近时,声音应该变得更大声。如果你有环绕声或耳机,你还会注意到声音从左到右的声道移动。这只是GM:S音频引擎可以做的一些示例,一旦它在所有浏览器中都能正常工作,就会变得令人兴奋。

当我们看着成千上万的游戏时,很容易辨认出顶尖游戏和底层游戏。然而,当我们在整个光谱上看所有最好的游戏时,它们之间存在着明显的差异。有些游戏非常简约,有些是逼真的,而有些是奇幻的。这些游戏可能是由少数人制作的,也可能是由一大队专家团队制作的。是什么让根本上如此不同的游戏仍然能够达到相同的质量定义呢?答案非常简单,可以用三个一般原则来概括:一致性、可读性和抛光。虽然创作高水准的艺术和音频确实需要通过多年的学习和实践获得的技能,但遵循这些几条规则将有助于使任何游戏看起来更加专业。

一致性听起来很明显,但实际上比人们预期的要具有挑战性得多。每个精灵、背景或其他艺术资源都需要按照相同的规则集构建。在下面的截图中,你可以看到飞机在城市背景下飞行的三种变化。第一张图片完全不一致,因为它有一个平面阴影和像素块风格的飞机,以及一个逼真的背景。下一张图片比第一张图片更一致,因为城市是平面阴影的,但缺乏像素块风格的清晰度。这是大多数人可能会停下来的地方,因为它已经足够接近了,但仍然有改进的空间。最后一张图片是最一致的,因为所有东西都有平面阴影和像素块风格。

这个过程同样可以轻松地朝相反方向进行,让飞机变得更加逼真。所需的只是选择一组选项,并将其均匀应用到所有内容上。

可读性就是确保向用户传达正确的信息。这可能意味着很多事情,比如确保背景与前景分离,或者确保可收集的物品不像危险物品。在下面的图片中,有两组药水;一种是毒药,另一种是治疗药水。仅仅改变颜色对玩家来说并不那么可读,而用骷髅头表示毒药,用心脏表示治疗药水则更容易让玩家理解。重要的是让玩家能够轻松理解发生了什么,以便他们能够做出反应而不是思考。

最后,尽管通常不太显眼,但最重要的因素是抛光。抛光关乎细节。它涵盖了很多方面,从收集物品时产生粒子效果到确保记分牌正确居中。在下面的图片中,我们有两个带有统计条的头像图标。左边的那个在功能上是正确的,看起来还不错。然而,右边的那个似乎更加抛光。统计条被移到左边,这样它们和头像图标之间就没有间隙了,头像图标也被正确地居中了。希望你能看到一些微小的调整如何能够大大提高抛光的质量。

在下一章中,我们将构建我们的第二个游戏,一个横向卷轴射击游戏。我们将创建一个在屏幕上移动的玩家,建立几个射击武器的敌人,创建移动背景,并实现胜利/失败的条件。最令人兴奋的是,我们将在学习GameMaker语言(GML)的同时完成所有这些工作。

在本章中,我们将创建一个非常简单的横向卷轴射击游戏,这将使我们了解使用GML代码制作完整游戏的基础知识。我们将有一个玩家角色,可以在游戏区域内移动并发射武器。如果他们与敌人或敌人的子弹相撞,他们将被摧毁,并且如果他们还有剩余生命,可以重新生成。

我们将创建三种不同类型的飞越屏幕的敌人:

我们将通过显示得分和玩家生命,滚动背景以营造移动的错觉,播放音乐并添加爆炸效果来完善游戏。最后,我们将通过实现胜利/失败条件来重新开始游戏。游戏将如下截图所示:

为了编写有效的代码,无论编程语言如何,遵循推荐的编码约定是很重要的。这将有助于确保其他人可以阅读和理解代码尝试做什么并对其进行调试。虽然许多语言遵循类似的指南,但编程实践没有通用标准。GameMaker语言(GML)没有官方推荐的约定集,部分原因是它被开发为学习工具,因此非常宽容。

对于本书,我们将根据常见做法和学习的便利性来定义自己的约定。

我们将从构建我们的玩家对象开始。我们已经简要描述了设计,但我们还没有将设计分解为可以开始创建的内容。首先,我们应该列出每个功能及其包含的内容,以确保我们拥有所有我们需要的变量和事件。

让我们创建玩家精灵并为游戏做好准备:

接下来,我们要调整太空飞船的碰撞区域。默认的碰撞是一个覆盖具有像素数据的精灵整个区域的矩形。这意味着即使外观上没有接触任何东西,飞船也会受到伤害。我们希望的是有一个非常小的碰撞区域。

在遮罩属性编辑器中,我们可以控制碰撞遮罩的大小、形状和位置,即精灵中进行碰撞检测的区域。一些游戏需要像素级的碰撞检测,即根据单个像素确定碰撞。这是最精确的碰撞检测,但也需要大量计算。然而,大多数游戏可以使用简单得多的形状,比如矩形。这种方法更有效,但限制了碰撞的视觉准确性。选择哪种方法取决于游戏的设计以及为实现期望的结果需要多少控制。

现在我们回到精灵属性编辑器,可以看到碰撞检测现在显示为已修改。我们要做的最后一件事是将原点移动到太空飞船枪的尖端。这样做,我们就不必担心通过代码在创建时偏移子弹。

让我们创建玩家对象,并让它在世界中移动。

mySpeed=8;if(x>=sprite_width){x-=mySpeed;}我们首先使用条件if语句查询玩家当前的x位置是否大于或等于精灵的宽度。在这种情况下,这意味着玩家的原点大于48像素的图像宽度。如果大于,我们将对象放在当前位置的左侧八个像素处。

我们在这里使用的移动方法不是传统意义上的移动。对象没有施加速度,而是我们将对象从一个位置瞬间传送到另一个位置。使用这种方法的好处是,如果没有按键,对象就不会移动。这在这个游戏中是必要的,因为我们不能使用无按键事件来射击武器。

在继续处理所有其他键及其脚本之前,最好检查对象是否按预期工作。

如果一切设置正确,玩家应该只在按下左箭头时向左移动,并且应该保持在游戏区域内。现在我们可以继续处理其他控制。

if(x<=room_width-sprite_width){x+=mySpeed;}在这里,我们正在测试玩家当前的x位置是否小于房间宽度减去精灵的宽度。如果小于这个值,我们将mySpeed添加到当前位置。这将确保玩家在向右移动时保持在屏幕上。

我们现在有了水平控制,并且需要添加垂直移动。我们将介绍上键和下键脚本的代码,但现在您应该能够将它们实现到对象中。

if(y>=sprite_height){y-=mySpeed;}这与水平代码类似,只是现在我们要考虑y位置和精灵的高度。

if(y<=room_height-sprite_height){y+=mySpeed;}同样,在这里,我们要考虑的是房间的高度减去精灵的高度作为我们可以向下移动的最远点。移动控制现在已经完成,对象属性应该如下截屏所示:

玩家应该能够在整个屏幕上移动,但永远不会离开屏幕。我们剩下的唯一控制是开枪的按钮。然而,在实现这一点之前,我们需要一颗子弹!

制作子弹很容易,因为它们通常一旦被发射就沿着直线移动。

hspeed=16;sound_play(snd_Bullet_01);Hspeed是GameMaker:Studio中表示对象水平速度的属性。我们需要在子弹实例化到世界中的那一刻应用这段代码。我们还会播放子弹的声音一次。

如前面的截图所示,子弹现在已经完成,准备好发射。让我们回到太空船!

子弹只有在被发射后才对敌人构成威胁。玩家飞船将处理这段代码。

instance_create(x,y,obj_Bullet_Player);通过这段代码,我们只是在玩家飞船当前位置,或者更具体地说,玩家飞船精灵的原点处创建一个子弹实例。这将使子弹看起来是从飞船的枪中射出的。

如果一切正常,我们应该能够在屏幕上四处移动并尽可能快地射击子弹,如下图所示。我们几乎可以开始添加游戏玩法了,但在这之前,我们还有一点清理工作要做。

如果一切看起来正确,但仍然无法看到预期的结果,请尝试刷新您的浏览器。偶尔,浏览器会将游戏保存在内存中,并不会立即加载更新的版本。

instance_destroy();好了!现在我们有一个在屏幕上移动、射击子弹并且内存使用率低的太空船。让我们制作一些敌人!

在这个游戏中,我们将有三种独特类型的敌人供玩家对抗:FloatBot,SpaceMine和Strafer。这些敌人每个都会以不同的方式移动并具有独特的攻击。然而,它们也有一些共同的元素,比如它们都会与子弹和玩家发生碰撞,但彼此之间不会发生碰撞。

考虑各种对象的共同点总是有用的,因为可能有简化和减少所需工作量的方法。在这种情况下,由于我们正在处理碰撞,我们可以使用一个父对象。

父对象是GameMaker:Studio中非常有用的功能。它允许一个对象,父对象,将其属性传递给其他对象,称为子对象,通常被称为继承。最好的理解这种关系的方式是,父对象是一个群体,子对象是个体。这意味着我们可以告诉一个群体做某事,每个个体都会去做。

我们将创建一个父对象,并将其用于所有常见的碰撞事件。这样我们就不必为每个不同的敌人应用新的碰撞事件。

with(other){instance_destroy();}instance_destroy();在这里,我们使用了一个with语句,它允许我们对另一个对象应用代码。在这种情况下,我们还可以使用一个特殊的变量叫做other,它只在碰撞事件中可用。这是因为总是涉及两个实例,两者之间只有一个碰撞。谁拥有代码被标识为self,然后是另一个。当obj_Enemy_Parent或其任何子对象与obj_Player发生碰撞时,我们将移除玩家,然后移除它碰撞的实例。

玩家碰撞现在可以工作了,但是当子弹碰撞时目前什么也不会发生。如果所有实例都将被移除,我们可以使用相同的脚本。在这种情况下,如果敌人被玩家子弹击中,我们希望做一些不同的事情。我们想要奖励分数。

score+=20;这将为游戏的总分数增加20分。为了确保一切设置正确,此脚本的整个代码应该如下所示:

score+=20;with(other){instance_destroy();}instance_destroy();我们需要父对象监视的最后一个事件是,如果敌人离开屏幕,将其移除。我们不能使用与我们的子弹清理脚本相同的脚本,因为我们将在屏幕右侧生成敌人。因此,我们需要确保它们只在离开左侧时被移除。

if(x<0){instance_destroy();}首先,我们检查实例的x位置是否小于0,或者在屏幕左侧。如果是,我们将其从游戏中移除。

现在我们有了一个父对象,它将处理子弹碰撞并在敌人离开屏幕时移除它们。让我们通过创建一些子对象来测试它。

FloatBot是游戏中最基本的敌人。它不会发射武器,这使它更像是要避开的障碍物。FloatBot将横穿屏幕向左移动,同时上下浮动。

我们需要两个脚本来使FloatBot以我们想要的方式飞行。在创建时,我们将应用水平移动,然后在每一步之后我们将调整垂直摆动运动。

hspeed=-4;angle=0;水平速度的负值意味着它将向左移动。angle是我们将在下一个脚本中使用的变量,用于摆动运动。

vspeed=sin(angle)*8;angle+=0.1在这里,我们根据变量角的正弦值(以弧度为单位)乘以基本速度8来改变垂直速度。我们还每一步增加angle的值,这是必要的,以便它遵循正弦波。

如果一切正常,FloatBot应该沿着屏幕向左移动,并在大约240像素的高度上上下摆动,模式与下一个截图中显示的类似。如果我们用子弹击中FloatBot,子弹和FloatBot都将消失。我们还成功创建了父子关系。让我们再创建一个!

SpaceMine将是一个缓慢移动的对象,如果玩家靠近,它将发射一圈子弹。由于这将需要两个对象,我们应该始终从最简单的对象开始,即子弹。

speed=16;direction=180;我们需要SpaceMine做一些事情。它将发射子弹,所以我们需要一个变量来控制何时射击。它需要在屏幕上移动,所以我们需要应用速度。最后,我们希望减慢动画的速度,以免闪烁太快。

hspeed=-2;canFire=false;image_speed=0.2;首先,我们将水平速度设置为向左缓慢移动。canFire是一个布尔变量,将决定是否射击。最后,image_speed设置了动画的速度。以0.2的速度,它以正常速度的20%进行动画,换句话说,每一帧动画将保持五个步骤。

每一步,我们都希望查看玩家是否在SpaceMine的附近。如果玩家离得太近,SpaceMine将开始发射子弹环。我们不希望有一串子弹,所以我们需要在每次射击之间添加延迟。

if(distance_to_object(obj_Player)<=200&&canFire==false){alarm[0]=60;sound_play(snd_Bullet_SpaceMine)for(i=0;i<8;i+=1){bullet=instance_create(x,y,obj_Bullet_SpaceMine);bullet.direction=45*i;bullet.hspeed-=2;}canFire=true;}我们首先检查两个语句;SpaceMine和obj_Player之间的距离,以及我们是否能够射击。我们选择的距离是200像素,这应该足够让玩家偶尔避免触发它。如果玩家在范围内并且我们能够射击,我们将alarm设置为60步(2秒),并播放一次子弹声音。

警报是一个事件,当触发时,将执行一次代码。

为了创建子弹环,我们将使用一个for循环。当我们创建一个对象的实例时,它会返回该实例的唯一ID。我们需要将这个ID捕获在一个变量中,这样我们才能与对象交互并影响它。在这里,我们使用一个名为bullet的变量,它是obj_Bullet_SpaceMine的一个实例。然后我们可以改变子弹的属性,比如方向。在这种情况下,每颗子弹的偏移角度为45度。我们还给子弹添加了一些额外的hspeed,这样它们就可以跟随SpaceMine移动。最后,我们将canFire变量设置为true,表示我们已经发射了子弹。

canFire=false;如果一切设置正确,SpaceMine将缓慢地向左移动并闪烁。当玩家靠近SpaceMine时,应该会有八颗子弹从中射出,就像下一个截图中所示。每两秒,这个实例将发射另一个子弹环,只要玩家仍然在范围内。如果SpaceMine被玩家的子弹击中,它将被摧毁。最后,如果玩家与敌人的子弹相撞,玩家就会消失。让我们继续我们的最终敌人!

Strafer是游戏中最危险的敌人。它以直线非常快速移动,并且会瞄准玩家无论他们在哪里。再次,我们需要两个对象,所以让我们从子弹开始。

speed=20;direction=180;sound_play(snd_Bullet_Strafer);hspeed=-10;alarm[0]=10;与SpaceMine类似,我们将hspeed设置为向左移动,并设置一个警报,以便Strafer立即开始射击。

bullet=instance_create(x,y,obj_Bullet_Strafer);if(instance_exists(obj_Player)){bullet.direction=point_direction(x,y,obj_Player.x,obj_Player.x,obj_Player.y);}alarm[0]=irandom(30)+15;我们首先创建obj_Bullet_Strafer的一个实例。当创建一个实例时,该函数会返回该实例的唯一ID;然后我们将其捕获在一个变量中,比如bullet。接下来,我们查询玩家是否存在。这是一个非常重要的步骤,因为如果没有这个检查,如果玩家死亡并且Strafer试图瞄准它,游戏将出错并崩溃。

如果玩家存在,我们将设置子弹的方向指向玩家。这是通过point_direction函数完成的,该函数接受空间中的任意两点(x1,y1)和(x2,y2),并返回角度(以度为单位)。

最后,我们重置警报。在这种情况下,为了增加趣味性,我们添加了一些随机性。irandom函数将返回一个介于零和传递给它的数字之间的整数。我们这里的代码将给我们一个介于0和30之间的随机值,然后我们将其加上15。这意味着每隔半秒到一秒半之间将创建一个新的子弹。

如果一切正常,Strafer将快速横穿屏幕,并直接朝向玩家位置发射子弹。确保您将玩家移动到房间的各个方向,以确保它可以朝各个方向射击!玩家应该能够射击并摧毁Strafer。如果被Strafer的子弹击中,玩家应该消失。

游戏的敌人都已经完成;现在我们只需要一种方法来填充游戏世界。让我们引入一个Overlord!

在这个游戏中,我们将使用Overlord,游戏的主控制器,来控制敌人的生成,监视玩家的生命,并处理胜利/失败条件。胜利条件很简单,就是在两分钟内生存下来,抵御敌人的波浪。失败条件是玩家耗尽生命。

instance_create(room_width-64,room_height/2-64,obj_Strafer);instance_create(room_width-64,room_height/2+64,obj_Strafer);在这里,我们生成两个Strafer的实例,位于屏幕右侧64像素处。这将确保我们看不到它们突然出现。我们还将它们偏移了64像素,使其与房间的垂直中心相差64像素。

placeY=irandom_range(64,room_height-64);instance_create(room_width-64,placeY,obj_SpaceMine);我们创建一个名为placeY的变量来保存垂直位置的值。GameMaker:Studio有一个特殊的函数irandom_range,它将返回传递给它的两个数字之间的整数。我们使用的数字将确保SpaceMine距离屏幕顶部和底部至少64像素。然后我们在创建实例时使用placeY变量。

placeY=irandom_range(80,room_height-80);instance_create(room_width-32,placeY,obj_FloatBot);instance_create(room_width-64,placeY-32,obj_FloatBot);instance_create(room_width-64,placeY+32,obj_FloatBot);在这里,我们再次使用placeY变量,但数字范围更窄。我们需要一些额外的填充,以便所有三个飞机都保持在屏幕上。创建的第一个实例是编队的前部单位。接下来的两个实例在第一个实例的后面生成,偏移了32像素,并分别在第一个实例的上方和下方偏移了32像素。

我们现在准备开始构建Overlord并应用我们的生成系统。

游戏现在有敌人!第一个敌人FloatBots出现需要几秒钟,但之后敌人将会不断生成。到目前为止,我们已经实现了大部分核心游戏玩法,如下所示:

在这个阶段玩游戏时,唯一剩下的元素非常明显;玩家可以死亡,但游戏不会停止。我们需要实现胜利/失败条件。

lives=3;isVictory=false;isDefeat=false;GameMaker:Studio有一些内置的全局变量,包括lives。这个变量可以被游戏中的每个实例访问,永远不会消失。在这里,我们将其设置为3,并将其用作我们的起点。我们还创建了另外两个变量,isVictory和isDefeat,我们将其设置为false。我们之所以使用两个变量来表示游戏的胜利和失败,而不是一个,是因为我们希望在游戏过程中检查它们,当他们既没有赢也没有输时。

alarm[0]=2700;scr_Overlord_Create脚本现在应该总共如下所示:

x=-64;y=room_height*0.5;hspeed=4;我们首先将x坐标设置为屏幕外64像素。然后通过将y坐标设置为房间高度的一半来垂直居中Ghost。最后,我们对Ghost施加正向速度,使其开始自行移动。

if(x>=200){hspeed=0;instance_change(obj_Player,true);}在这里,我们检查Ghost的x坐标是否已经越过了200像素。如果是,我们停止向前的速度,然后转换为玩家。instance_change函数需要两个参数:要转换为的对象以及是否要运行此新对象的Create事件。

Ghost对象的属性应该如下截图所示,现在已经准备好成为游戏的一部分。我们只需要改变玩家被击中时发生的事情。

instance_destroy();并替换为:

if(lives>0){instance_change(obj_Ghost,true);}else{instance_destroy();}lives-=1;我们只想在有生命可用时变成Ghost,因此我们首先要检查这一点。如果我们至少有一条生命,我们就将玩家变成Ghost。否则,我们只是销毁玩家,玩家将永远死亡。最后,无论我们是否有生命,每次都要减少一条生命。最终的代码应该如下所示:

with(other){if(lives>0){instance_change(obj_Ghost,true);}else{instance_destroy();}lives-=1;}instance_destroy();此时我们可以玩游戏。请注意,当玩家死亡时:

当然,这会发生三次,然后玩家永远消失。然而,游戏的其余部分正在继续,就好像什么都没有发生。我们需要添加失败条件。

if(lives<0&&isDefeat==false){alarm[1]=90;isDefeat=true;}这段代码中的每一步都会检查玩家是否还有生命。如果玩家没有生命了,而变量isDefeat仍然为false,它将为重新开始游戏警报设置三秒。最后,我们将isDefeat变量设置为true,这样我们就不会再运行这段代码了。

游戏的核心机制现在已经完成,但对于玩家来说,发生了什么并不是很清楚。玩家可以死亡并重新生成几次,但没有显示剩余生命的指示。也没有显示玩家是赢了还是输了。让我们来解决这个问题!

创建一个伟大游戏的最重要元素之一是确保玩家拥有玩游戏所需的所有信息。其中很多通常显示在HUD中,也就是heads-updisplay。每个游戏都有不同的组件可以成为HUD的一部分,包括我们需要的记分牌和生命计数器等。

draw_set_color(c_white);draw_set_font(fnt_Scoreboard);第一行代码设置了一个GameMaker:Studio预设颜色c_white。接下来的一行将记分牌设置为字体。

设置颜色是全局应用于draw事件的。这意味着如果您不设置颜色,它将使用上次设置的颜色,而不管对象如何。

draw_set_halign(fa_left);if(lives>=0){draw_text(8,0,"Lives:"+string(lives));}else{draw_text(8,0,"Lives:");}为了确保文本格式正确,我们将文本的水平对齐设置为左对齐。文本本身需要是一个字符串,可以通过两种方式完成。首先,任何用引号括起来的内容都被视为字符串,比如"生命:"。如果我们想传递一个数字,比如我们拥有的生命数量,我们需要通过字符串函数进行转换。如下所示,如果我们还有剩余的生命,我们可以将这两个东西连接起来创建一个句子“生命:3”,并将其绘制在屏幕的左上角。如果我们没有生命了,我们就绘制不带连接值的文本。

draw_set_halign(fa_right);draw_text(room_width-8,0,"SCORE:"+string(score));与之前的文本一样,我们设置了水平对齐,这次是右对齐。然后使用相同的连接方法将文本放在正确的位置。

draw_set_font(fnt_WinLose);draw_set_halign(fa_center);if(isVictory==true){draw_text(room_width/2,room_height/2,"VICTORY");}if(isDefeat==true){draw_text(room_width/2,room_height/2,"DEFEAT");}我们将字体更改为fnt_WinLose,并将水平对齐设置为居中。我们不希望文本一直显示,而是应该在适当时只显示VICTORY或DEFEAT。我们已经在Overlord中实现了游戏条件的代码,所以我们只需要在每一步检查isVictory是否为true或isDefeat是否为true。一旦游戏赢了或输了,我们就在房间的中心绘制适当的文本。

完整的scr_Overlord_Draw脚本应该如下所示:

draw_set_color(c_white);draw_set_font(fnt_Scoreboard);draw_set_halign(fa_left);draw_text(8,0,"LIVES:"+string(lives));draw_set_halign(fa_right);draw_text(room_width-8,0,"SCORE:"+string(score));draw_set_font(fnt_WinLose);draw_set_halign(fa_center);if(isVictory==true){draw_text(room_width/2,room_height/2,"VICTORY");}if(isDefeat==true){draw_text(room_width/2,room_height/2,"DEFEAT");}为游戏添加完成细节游戏现在在功能上已经完成,但它没有任何光泽或我们期望的完整游戏的完成细节。没有音乐,没有背景艺术,也没有爆炸!让我们立即解决这个问题。

sound_play(snd_Music);sound_loop(snd_Music);volume=1;sound_global_volume(volume);我们首先播放音乐并设置为循环。然后创建一个名为volume的变量,我们将用它来控制音量和淡出。我们已将音量设置为1,即最大音量。最后,我们将全局音量,或主增益级别,设置为变量volume。

这个游戏发生在外太空,所以我们需要添加一个太空背景。为了让游戏宇宙感觉玩家在移动,我们需要使背景不断向左移动。

让敌人突然消失不仅看起来很糟糕,而且对玩家来说也不是很有意义。让我们通过添加一些爆炸效果来使游戏更加令人兴奋!

我们希望爆炸发出声音,播放其动画,然后从游戏中移除自身。

sound_play(snd_Explosion);instance_destroy();instance_create(x,y,obj_Explosion);这将在敌人所在的位置创建一个爆炸。这需要在我们将敌人从游戏中移除之前发生。

恭喜!你刚刚完成了创建你的第一个横向卷轴射击游戏。在本章中,我们涵盖了相当多的内容。我们应用了移动的三种方法:手动调整X和Y坐标,使用hspeed和vspeed,以及设置speed和direction变量。我们现在能够动态地向游戏世界添加和移除实例。通过子弹,我们学会了将信息从一个实例传输到另一个实例,比如移动的方向,通过捕获实例的ID并通过点运算符访问它。

有了本章中所学的技能和知识,现在轮到你来接管这个游戏,并进一步扩展它。尝试添加你自己的敌人、可收集物品和武器升级。玩得开心吧!

在下一章中,我们将通过制作一个恐怖冒险游戏,更多地了解碰撞和玩家控制。我们还将研究人工智能,并使用路径使敌人看起来像在自己思考和行动。

在本章中,我们将创建一个有趣的小动作冒险游戏,这将建立在我们的基础知识之上。我们将从一个可以在世界中导航并具有短程近战攻击的动画玩家角色开始。游戏世界将由多个房间组成,玩家将能够从一个房间移动到另一个房间,同时保留所有他们的统计数据。我们将把所有玩家控制的代码和处理墙壁碰撞的代码放在一个脚本中,以创建一个更高效的项目。

如下截图所示,这个游戏的主题是高中的恐怖,世界里会有三个基本人工智能的敌人:一个幽灵图书管理员,一个乱斗,和一个教练。幽灵图书管理员会在玩家接近它的休息地点时出现,并追逐玩家直到距离太远,然后返回原来的位置。乱斗会在房间里漫游,如果它发现玩家,它会增加体积和速度。教练是奖杯的守护者,会独自在世界中导航。如果它看到玩家,它会追击并避开墙壁和其他教练,如果足够接近,它会对玩家进行近战攻击。

到目前为止,我们创建的玩家对象非常基本。在第一章中,与您的第一个游戏一起了解Studio,玩家没有动画。在第三章中,射击游戏:创建一个横向卷轴射击游戏,飞船有动画,但始终面向右侧。在本章中,我们将拥有一个可以朝四个方向移动并具有每个方向的动画精灵的角色。我们还将实现一个近战攻击,可以在角色面对的方向上使用。

玩家角色的行走循环需要四个单独的精灵。我们将先介绍第一个,然后您可以创建其他三个。

mySpeed=4;myDirection=0;isAttacking=false;isWalking=false;health=100;image_speed=0.5;前两个变量是玩家速度和方向的占位符。这将很有用,因为我们可以影响这些值,而不影响对象的本地mySpeed和myDirection变量,比如在对象面对一个方向移动时产生的击退效果。变量isAttacking将用于指示我们何时发起战斗,isWalking将指示玩家何时移动。接下来,我们有全局变量health,设置为100%。最后,我们将动画速度设置为50%,以便行走循环播放正确。

要了解更多关于GameMaker:Studio内置变量和函数的信息,请点击帮助|目录查看GameMaker用户手册。

isWalking=false;if(keyboard_check(vk_right)&&place_free(x+mySpeed,y)){x+=mySpeed;myDirection=0;sprite_index=spr_Player_WalkRight;isWalking=true;}我们首先将isWalking设置为false,使其成为玩家正在进行的默认状态。之后,我们检查键盘是否按下右箭头键(vk_right),并检查当前位置右侧是否有实体物体。place_free函数将返回指定点是否无碰撞。如果玩家能够移动并且按下了键,我们就向右移动,并将方向设置为零以表示向右。我们将精灵更改为面向右侧的行走循环,然后将isWalking更改为true,这将覆盖我们将其设置为false的第一行代码。

if(isWalking==true){image_speed=0.5;}else{image_speed=0;}我们创建了变量isWalking来在行走和停止状态之间切换。如果玩家在移动,精灵将播放动画。如果玩家没有移动,我们也停止动画。

当代码完成时,应该如下所示:

isWalking=false;if(keyboard_check(vk_right)&&place_free(x+mySpeed,y)){x+=mySpeed;myDirection=0;sprite_index=spr_Player_WalkRight;isWalking=true;}if(keyboard_check(vk_up)&&place_free(x,y-mySpeed)){y-=mySpeed;myDirection=90;sprite_index=spr_Player_WalkUp;isWalking=true;}if(keyboard_check(vk_left)&&place_free(x-mySpeed,y)){x-=mySpeed;myDirection=180;sprite_index=spr_Player_WalkLeft;isWalking=true;}if(keyboard_check(vk_down)&&place_free(x,y+mySpeed)){y+=mySpeed;myDirection=270;sprite_index=spr_Player_WalkDown;isWalking=true;}if(isWalking==true){image_speed=0.5;}else{image_speed=0;}玩家已经准备好移动和正确播放动画了,但如果没有添加一些实体障碍物,我们将无法完全测试代码。让我们建一堵墙。

现在我们已经让玩家移动正常了,我们可以开始进行攻击了。我们正在创建的攻击只需要影响玩家角色前面的物体。为了实现这一点,我们将创建一个近战攻击对象,它将在命令下生成并在游戏中自行移除。

image_angle=obj_Player.myDirection;image_speed=0.3;alarm[0]=6;obj_Player.isAttacking=true;这就是我们将图像旋转到与玩家面向相同方向的地方,结合我们设置的偏移原点,这意味着它将出现在玩家的前面。我们还会减慢动画速度,并设置一个六帧的警报。这个警报将在触发时移除攻击对象。最后,我们告诉玩家正在进行攻击。

obj_Player.isAttacking=false;instance_destroy();我们可以直接与玩家的isAttacking变量交谈,并将其设置回false。然后我们销毁近战攻击的实例。将这个脚本附加到Alarm0事件。

if(keyboard_check_pressed(ord('Z'))&&isAttacking==false){instance_create(x,y,obj_Player_Attack);}keyboard_check_pressed函数只在按下键时激活,而不是在按下位置,而在这种情况下,我们正在检查Z键。键盘上的各种字母没有特殊命令,因此我们需要使用ord函数,它返回传递给它的字符的相应ASCII代码。我们还检查玩家当前是否没有进行攻击。如果一切顺利,我们生成攻击,攻击将改变isAttacking变量为true,这样就只会发生一次。

在使用ord函数时,始终使用大写字母,否则可能会得到错误的数字!

如果一切都发生在一个非常大的房间里,冒险游戏会变得相当无聊。这不仅效率低下,而且世界也会缺乏探索的感觉。从一个房间切换到另一个房间很容易,但确实会带来问题。

第一个问题是保留玩家的统计数据,比如健康,从一个房间到另一个房间。解决这个问题的一个方法是在玩家身上激活持久性。持久性意味着我们只需要在一个房间放置一个对象的单个实例,从那时起它将一直存在于游戏世界中。

第二个问题是在一个有多个入口点的房间中放置玩家。如果玩家不是持久的,我们可以将玩家放在房间里,但它总是从同一个地方开始。如果玩家是持久的,那么当他们切换房间时,他们将保持在上一个房间中的完全相同的坐标。这意味着我们需要将玩家重新定位到每个房间中我们选择的位置。

如果您的游戏将有很多房间,这可能会成为很多工作。通过创建自我感知的传送门和使用房间创建代码,有一种简单的解决方法。

让我们从构建一些房间开始,首先是标题屏幕。

为了改变房间,我们将创建可重复使用的传送门。每个传送门实际上由两个单独的对象组成,一个是Start对象,一个是Exit对象。Start对象将代表玩家进入房间时应该放置的着陆点。Exit对象是改变玩家所在房间的传送器。我们将利用四个独特的传送门,这将允许我们在地图的每一侧都有一个门。

global.portalA=0;global.portalB=0;global.portalC=0;global.portalD=0;global.lastRoom=C04_R01;我们为四个传送门创建全局变量,并给它们一个零值。我们还跟踪我们上次所在的房间,这样我们就知道我们需要去新房间的哪个传送门。

draw_set_color(c_white);draw_set_halign(fa_center);draw_text(room_width/2,360,"PressANYkey");if(keyboard_check_pressed(vk_anykey)){room_goto_next();}在这里,我们只是编写一些白色的居中文本,让玩家知道他们如何开始游戏。我们使用特殊变量vk_anykey来查看键盘是否被按下,如果按下了,我们就按照资源树中的顺序进入下一个房间。

您不必总是关闭脚本,因为即使打开多个脚本窗口,游戏也会运行。

global.lastRoom=room;room_goto(global.portalA);在我们可以传送之前,我们需要将上一个房间设置为玩家当前所在的房间。为此,我们使用内置变量room,它存储游戏当前显示的房间的索引号。之后,我们去到这个传送门的全局变量指示我们应该去的房间。

传送门已经完成,我们可以将它们添加到房间中。在每个房间中不必使用所有四个传送门;您只需要至少一个起点和一个终点。在放置这些对象时,重要的是同一类型的传送门只能有一个。起点传送门应始终放置在可玩区域,并确保只能从一个方向访问终点。您还应确保,如果一个房间的PORTALA在底部,那么它要进入的房间应该在顶部有PORTALA,如下面的截图所示。这将帮助玩家理解他们在世界中的位置。

现在是有趣的部分。我们需要在每个房间中更改全局传送门数值,我们不想有一个检查所有房间发生情况的大型脚本。相反,我们可以在房间本身使用创建代码来在玩家进入时更改这些值。让我们尝试一下,通过使C04_R01中的传送门A去到C04_R02,反之亦然。

global.portalA=C04_R02;global.portalB=0;global.portalC=0;global.portalD=0;我们将PORTALA设置为第二个房间。所有其他传送门都没有被使用,所以我们将变量设置为零。每个房间都需要将所有这些变量设置为某个值,要么是特定的房间,要么是零,否则可能会导致错误。

global.portalA=C04_R01;global.portalB=0;global.portalC=0;global.portalD=0;现在我们已经将PORTALA设置为第一个房间,这是有道理的。如果我们通过那个传送门,我们应该能够再次通过它回去。随意更改这些设置,以适用于您想要的所有传送门。

房间都已经建好,准备就绪。我们唯一需要做的就是让玩家从一个房间移动到另一个房间。让我们首先使玩家持久,这样我们在游戏中只需要一个玩家。

敌人不仅仅是要避免的障碍物。好的敌人让玩家感到有一些潜在的人工智能(AI)。敌人似乎知道你何时靠近,可以在墙上追逐你,并且可以自行徘徊。在本章中,我们将创建三种生物,它们将在世界中生存,每种都有自己独特的AI。

第一个生物将由两部分组成:过期的BookPile和保护它的幽灵图书管理员。如果玩家靠近一个BookPile,幽灵将生成并追逐玩家。如果玩家离幽灵太远,幽灵将返回生成它的BookPile。如果玩家攻击幽灵,它将消失并从BookPile重新生成。如果玩家摧毁BookPile,生成的幽灵也将被摧毁。

myRange=100;hasSpawned=false;第一个变量设置玩家需要多接近才能变得活跃,第二个变量是布尔值,将确定这个BookPile是否生成了幽灵。

if(instance_exists(obj_Player)){if(distance_to_object(obj_Player)

if(instance_exists(ghost)){with(ghost){instance_destroy();}}instance_destroy();再次,我们首先检查是否有幽灵从这个BookPile生成并且仍然存在。如果是,我们销毁那个幽灵,然后移除BookPile本身。BookPile现在已经完成,应该看起来像以下截图:

mySpeed=2;myRange=150;myBooks=0;isDissolving=false;image_speed=0.3;alarm[0]=6;sprite_index=spr_Ghost;现在我们准备开始实现一些人工智能。幽灵将是最基本的敌人,会追逐玩家穿过房间,包括穿过墙壁和其他敌人,直到玩家超出范围。在那时,幽灵将漂浮回到它来自的书堆。

if(instance_exists(obj_Player)){targetDist=distance_to_object(obj_Player)if(targetDist

elseif(targetDist>myRange&&distance_to_point(myBooks.x,myBooks.y)>4){move_towards_point(myBooks.x,myBooks.y,mySpeed);}首先我们检查玩家是否超出范围,而幽灵又不靠近自己的书堆。在这里,我们使用distance_to_point,这样我们就是检查书堆的原点而不是distance_to_object会寻找的碰撞区域的边缘。如果这一切都是真的,幽灵将开始向它的书堆移动。

else{speed=0;if(isDissolving==false){myBooks.hasSpawned=false;sprite_index=spr_Ghost_Spawn;image_speed=-1;alarm[1]=6;isDissolving=true;}}这里有一个最终的else语句,如果玩家超出范围,幽灵靠近它的书堆,将执行。我们首先停止幽灵的速度。然后我们检查它是否可以溶解。如果可以,我们告诉书堆可以再次生成幽灵,我们将精灵改回生成动画,并通过将image_speed设置为-1来以相反的方式播放该动画。我们还设置了另一个警报,这样我们就可以将幽灵从世界中移除并停用溶解检查。

整个scr_Ghost_Step应该如下所示的代码:

if(instance_exists(obj_Player)){targetDist=distance_to_object(obj_Player)if(targetDistmyRange&&distance_to_point(myBooks.x,myBooks.y)>4){move_towards_point(myBooks.x,myBooks.y,mySpeed);}else{speed=0;if(isDissolving==false){myBooks.hasSpawned=false;sprite_index=spr_Ghost_Spawn;image_speed=-1;alarm[1]=6;isDissolving=true;}}}instance_destroy();幽灵几乎完成了。它生成,追逐玩家,然后返回到它的BookPile,但是如果它抓住了玩家会发生什么?对于这个幽灵,我们希望它撞到玩家,造成一些伤害,然后在一团烟雾中消失。为此,我们需要为死去的幽灵创建一个新的资源。

instance_destroy();动画结束事件将在播放精灵的最后一帧图像时执行代码。在这种情况下,我们有一个烟雾的动画,在结束时将从游戏中移除对象。

health-=5;myBooks.hasSpawned=false;instance_create(x,y,obj_Ghost_Dead);instance_destroy();我们首先减少五点生命值,然后告诉幽灵的BookPile它可以重新生成。接下来,我们创建幽灵死亡对象,当我们将其从游戏中移除时,它将隐藏真正的幽灵。如果一切构建正确,它应该看起来像以下截图:

最后一件事,由于房间是用来进行实验而不是实际游戏的一部分,我们应该清理房间,为下一个敌人做准备。

我们将创建的下一个敌人是一个Brawl,它将在房间里漫游。如果玩家离这个敌人太近,Brawl会变得愤怒,变得更大并移动得更快,尽管它不会离开它的路径。一旦玩家离开范围,它会恢复冷静,并缩小到原来的大小和速度。玩家无法杀死这个敌人,但是如果接触到Brawl,它会对玩家造成伤害。

对于Brawl,我们将利用一个路径,并且我们需要三个精灵:一个用于正常状态,一个用于状态转换,另一个用于愤怒状态。

mySpeed=2;canGrow=false;isBig=false;isAttacking=false;image_speed=0.5;sound_play(snd_Brawl);sound_loop(snd_Brawl);path_start(pth_Brawl_01,mySpeed,1,true);第一个变量设置了Brawl的基本速度。接下来的三个变量是变身和愤怒状态以及是否已攻击的检查。接下来,我们设置了动画速度,然后播放了Brawl声音,在这种情况下,我们希望声音循环。最后,我们将Brawl设置到速度为2的路径上;当它到达路径的尽头时,它将循环,最重要的是,路径设置为绝对,这意味着它将按照路径编辑器中设计的方式运行。

image_angle=direction;if(isBig==true){path_speed=mySpeed*2;}else{path_speed=mySpeed;}我们首先通过旋转Sprite本身来使其面向正确的方向。这将起作用,因为我们的Sprite图像面向右侧,这与零度相同。接下来,我们检查Brawl是否变大。如果Brawl是愤怒版本,我们将路径速度设置为基本速度的两倍。否则,我们将速度设置为默认的基本速度。

if(instance_exists(obj_Player)){if(distance_to_object(obj_Player)<=200){if(canGrow==false){if(!collision_line(x,y,obj_Player.x,obj_Player.y,obj_Wall,false,true)){sprite_index=spr_Brawl_Change;alarm[0]=12;canGrow=true;}}}}我们首先确保玩家存在,然后检查玩家是否在范围内。如果玩家在范围内,我们检查自己是否已经愤怒。如果Brawl还没有变大,我们使用collision_line函数来查看Brawl是否真的能看到玩家。这个函数在两个点之间绘制一条线,即Brawl和玩家位置,然后确定一个对象实例或墙壁是否穿过了该线。如果Brawl能看到玩家,我们将Sprite更改为变身Sprite,设置一个警报以便我们可以完成变身,并指示Brawl已经变大。

sprite_index=spr_Brawl_Large;isBig=true;else{if(canGrow==true){sprite_index=spr_Brawl_Change;alarm[1]=12;canGrow=false;}}如果玩家超出范围,这个else语句将变为活动状态。我们检查Brawl是否仍然处于愤怒状态。如果是,我们将Sprite更改为变身状态,设置第二个警报,并指示Brawl已恢复正常。

以下是完整的scr_Brawl_Step脚本:

image_angle=direction;if(isBig==true){path_speed=mySpeed*2;}else{path_speed=mySpeed;}if(instance_exists(obj_Player)){if(distance_to_object(obj_Player)<=200){if(canGrow==false){if(!collision_line(x,y,obj_Player.x,obj_Player.y,obj_Wall,false,true)){sprite_index=spr_Brawl_Change;alarm[0]=12;canGrow=true;}}}else{if(canGrow==true){sprite_index=spr_Brawl_Change;alarm[1]=12;canGrow=false;}}}sprite_index=spr_Brawl_Small;isBig=false;if(isAttacking==false){health-=10;alarm[2]=60;isAttacking=true;}如果玩家第一次与Brawl碰撞,我们会减少10点生命值并设置一个两秒的警报,让Brawl可以再次攻击。

isAttacking=false;Brawl现在已经完成并按设计进行。如果一切实现正确,对象属性应该如下截图所示:

我们将创建的最后一个敌人,教练,将是迄今为止最具挑战性的对手。这个敌人将在房间中四处移动,随机地从一个奖杯到另一个奖杯,以确保奖杯仍在那里。如果它看到玩家,它会追逐他们,如果足够接近,它会进行近战攻击。如果玩家逃脱,它会等一会儿然后返回岗位。教练有一个身体,所以它需要绕过障碍物,甚至避开其他教练。这也意味着如果玩家能够攻击它,它可能会死亡。

image_speed=0;image_index=0;与玩家一样,我们需要四个精灵,代表敌人将移动的四个方向。

我们将为这个敌人动态创建路径,以便它可以自行导航到奖杯。我们还希望它避开障碍物和其他敌人。这并不难实现,但在初始化时需要进行大量设置。

mySpeed=4;isChasing=false;isWaiting=false;isAvoiding=false;isAttacking=false;image_speed=0.3;再次,我们首先设置对象的速度。然后我们有四个变量,表示我们需要检查的各种状态,全部设置为false。我们还设置了精灵的动画速度。

接下来,我们需要设置路径系统,该系统将利用GameMaker的一些运动规划功能。基本概念是我们创建一个覆盖敌人移动区域的网格。然后我们找到所有我们希望敌人避开的对象,比如墙壁,并将网格的这些区域标记为禁区。然后我们可以在自由区域中分配起点和目标位置,并在避开障碍物的情况下创建路径。

myPath=path_add();myPathGrid=mp_grid_create(0,0,room_width/32,room_height/32,32,32);mp_grid_add_instances(myPathGrid,obj_Wall,false);首先需要一个空路径,我们可以用于所有未来的路径。接下来,我们创建一个网格,该网格将设置路径地图的尺寸。mp_grid_create属性有参数,用于确定其在世界中的位置,宽度和高度有多少个网格,以及每个网格单元的大小。在这种情况下,我们从左上角的网格开始,以32像素的增量覆盖整个房间。将房间尺寸除以32意味着这将适用于任何尺寸的房间,而无需调整代码。最后,我们将在房间中找到的所有墙的实例添加到网格中,作为不允许路径的区域。

nextLocation=irandom(instance_number(obj_Trophy)-1);target=instance_find(obj_Trophy,nextLocation);currentLocation=nextLocation;我们首先得到一个基于房间中奖杯数量的四舍五入随机数。请注意,我们从奖杯数量中减去了一个。我们需要这样做,因为在下一行代码中,我们使用instance_find函数搜索特定实例。这个函数是从数组中提取的,数组中的第一项总是从零开始。最后,我们创建了第二个变量,用于当我们想要改变目的地时。

mp_grid_path(myPathGrid,myPath,x,y,target.x,target.y,false);path_start(myPath,mySpeed,0,true);在这里,我们选择了我们创建的网格和空路径,并创建了一个新的路径,该路径从教练的位置到目标位置,并且不会对角线移动。然后我们让教练动起来,这一次,当它到达路径的尽头时,它将停下来。path_start函数中的最终值将路径设置为绝对值,在这种情况下我们需要这样做,因为路径是动态创建的。

这是整个scr_Coach_Create脚本:

mySpeed=4;isChasing=false;isWaiting=false;isAvoiding=false;isAttacking=false;image_speed=0.3;myPath=path_add();myPathGrid=mp_grid_create(0,0,room_width/32,room_height/32,32,32);mp_grid_add_instances(myPathGrid,obj_Wall,false);nextLocation=irandom(instance_number(obj_Trophy)-1);target=instance_find(obj_Trophy,nextLocation);currentLocation=nextLocation;mp_grid_path(myPathGrid,myPath,x,y,target.x,target.y,false);path_start(myPath,mySpeed,0,true);if(direction>45&&direction<=135){sprite_index=spr_Coach_WalkUp;}elseif(direction>135&&direction<=225){sprite_index=spr_Coach_WalkLeft;}elseif(direction>225&&direction<=315){sprite_index=spr_Coach_WalkDown;}else{sprite_index=spr_Coach_WalkRight;}在这里,我们根据实例移动的方向更改精灵。我们可以在这里做到这一点,因为我们不允许在路径上进行对角线移动。

elseif(canSee!=noone&&isChasing==true){alarm[0]=60;isWaiting=true;isChasing=false;}这个else语句表示,如果玩家看不见并且教练正在追逐,它将设置一个警报以寻找新目的地,告诉它等待,追逐结束。

while(nextLocation==currentLocation){nextLocation=irandom(instance_number(obj_Trophy)-1);}target=instance_find(obj_Trophy,nextLocation);currentLocation=nextLocation;mp_grid_path(myPathGrid,myPath,x,y,target.x,target.y,false);path_start(myPath,mySpeed,1,false);isWaiting=false;我们首先使用一个while循环来检查下一个位置是否与旧位置相同。这将确保教练总是移动到另一个奖杯。就像我们在初始设置中所做的那样,我们选择一个新的目标并设置当前位置变量。我们还创建一个路径并开始在其上移动,这意味着教练不再等待。

image_speed=0.3;alarm[0]=6;isHit=false;instance_destroy();if(isHit==false){health-=15;isHit=true;}如果这是第一次碰撞,我们减少一点生命值,然后停用此检查。

isAttacking=false;教练现在只完成了一半的工作,追逐玩家。我们还需要添加正常的巡逻任务。目前,如果教练看不到玩家并且到达路径的尽头,它就会停下来再次什么都不做。它应该只等几秒,然后继续移动到下一个奖杯。

if(isAvoiding==true){mp_potential_step(target.x,target.y,4,all);}我们需要做的第一件事是检查变量,看教练是否需要避让。如果需要,我们使用mp_potential_step函数,该函数将使实例朝着指定目标移动,同时尝试避开某些对象,或者在这种情况下,避开所有实例。

if(distance_to_object(obj_Coach)<=32&&isAvoiding==false){path_end();isAvoiding=true;}elseif(distance_to_object(obj_Coach)>32&&isAvoiding==true){mp_grid_path(myPathGrid,myPath,x,y,target.x,target.y,false);path_start(myPath,mySpeed,1,true);isAvoiding=false;}首先,我们检查教练实例是否附近,且尚未尝试避让。如果是,则我们让教练脱离路径并开始避让。接着是一个elseif语句,检查我们是否与另一个教练足够远,以便我们可以避让。如果是,我们为目的地设置一个新路径,开始移动,并结束避让。

if(place_meeting(x,y,obj_Coach)){x=xprevious;y=yprevious;mp_potential_step(target.x,target.y,4,all);}这将检查两个教练实例是否相互碰撞。如果是,我们将x和y坐标设置为特殊变量xprevious和yprevious,它们代表实例在上一步中的位置。一旦它们退后一步,我们就可以再次尝试绕过它们。

教练现在已经完成。要检查scr_Coach_Step的所有代码是否都写正确,这里是完整的代码:

draw_healthbar(0,0,200,16,health,c_black,c_red,c_green,0,true,true);if(health<=0){with(obj_Player){instance_destroy();}room_goto(TitleScreen);instance_destroy();}首先,我们使用了函数draw_healthbar,你可以看到它有很多参数。前四个是矩形条的大小和位置。接下来是用于控制条的满度的变量,在我们的例子中是全局健康变量。接下来的三个是背景颜色和最小/最大颜色。接下来是条应该下降的方向,零表示向左。最后两个布尔值是用于绘制我们想要的背景和边框。

之后,我们进行健康检查,如果玩家应该死了,我们移除玩家,返回前端,然后移除Overlord本身。移除世界中的任何持久实例是很重要的,否则它们就不会消失!

所有剩下的就是创建关卡,用瓷砖集来绘制世界,并添加一些背景音乐。在这一点上,你应该知道如何做了,所以我们会把它留给你。我们已经在“第四章”文件夹中提供了一些额外的资源。完成后,你应该会看到类似以下截图的东西:

通过本章中所学的技能,你现在可以构建具有多个房间和敌人的游戏,这些敌人看起来会思考。现在轮到你通过添加更多独特的敌人、打开奖杯并生成战利品来扩展这个游戏了。玩得开心,探索你新发现的能力!

在下一章中,我们将为平台游戏构建一场史诗般的boss战。将会有枪支和激光,还有很多乐趣。我们将开始通过创建可重复使用的脚本,以及学习如何系统地构建我们的代码来提高代码的效率。所有这些都将帮助我们使游戏变得更快更容易,所以让我们开始吧!

我们将首先介绍常量。常量允许我们使用名称来表示永远不会改变的值。这不仅使我们更容易阅读代码,还有助于提高性能,与变量相比:

按照惯例,将所有常量都用大写字母写出,尽管如果不遵循惯例,也不会出错。

if(place_free(x,y+vspeed+1)){vspeed+=1;}else{move_contact_solid(direction,MAXGRAVITY);vspeed=0;}首先,我们检查实例下方的区域是否没有任何可碰撞的对象以当前速度行进。如果清晰,那么我们知道我们在空中,应该施加重力。我们通过每一步增加垂直速度的小量来实现这一点。如果有可碰撞的对象,那么我们即将着地,所以我们将实例移动到对象表面,以实例当前向上行进的方向到我们的MAXGRAVITY,即16像素。在那一点,实例在地面上,所以我们将垂直速度设为零。

vspeed=min(vspeed,MAXGRAVITY);在这里,我们将vspeed值设置为当前vspeed和MAXGRAVITY之间的较小值。如果实例移动得太快,这段代码将使其减速到允许的最大速度。现在我们有了一个简单的重力系统,游戏中的所有对象都可以利用它。

为了更好地理解这个概念,想想一扇门。一扇门有几种独特的状态。可能首先想到的两种状态是门可以打开或者关闭。还有两种其他状态,即打开和关闭,如下图所示。如果门正在打开,它既不是打开的,也不是关闭的,而是处于一种独特的动作状态。这使得状态机非常适合动画。游戏中几乎每个可交互的对象都可能有一些动画或利用几个不同的图像。

由于玩家角色通常是在不同动画方面最强大的对象,我们将首先分解其独特的状态。我们的玩家可以在空中或地面上,所以我们希望确保分开这些控制。我们还希望玩家能够朝多个方向射击并受到伤害。总共我们将有八种不同的状态:

让我们首先将这些状态定义为常量:

switch(action){caseIDLE:sprite_index=myIdle;image_speed=0.1;break;}在这里,我们将使用一个名为action的变量来切换状态。如果动作恰好是IDLE,那么我们就改变精灵;在这种情况下,我们使用另一个变量myIdle,我们将在每个对象中定义它,这将允许我们重用这个脚本。我们还设置了动画速率,这将允许我们对不同的动作有不同的播放速度。

caseIDLEUP:sprite_index=myIdleUp;image_speed=0.1;break;caseIDLEDOWN:sprite_index=myIdleDown;image_speed=0.1;break;caseRUN:sprite_index=myRun;image_speed=0.5;break;caseRUNUP:sprite_index=myRunUp;image_speed=0.5;break;caseRUNDOWN:sprite_index=myRunDown;image_speed=0.5;break;caseINAIR:sprite_index=myInAir;image_speed=0.5;break;caseDAMAGE:sprite_index=myDamage;image_speed=0.5;break;image_xscale=facing;我们再次利用一个变量facing,使脚本更通用。我们现在已经完成了这个脚本,动画系统已经准备好实施了。

接下来我们要构建的系统是处理世界碰撞。我们希望摆脱使用GameMaker:Studio的碰撞系统,因为它需要两个实例相互交叉。这对于子弹与玩家的碰撞效果很好,但如果玩家需要陷入地面以知道何时停止,这种方法就不太有效。相反,我们希望在实例移动之前预测碰撞是否会发生:

if(place_free(x-mySpeed,y)){canGoLeft=true;}else{canGoLeft=false;hspeed=0;}if(place_free(x+mySpeed,y)){canGoRight=true;}else{canGoRight=false;hspeed=0;}我们首先检查实例左侧的区域是否没有可碰撞的对象。我们正在查看的距离由变量mySpeed确定,这将允许此检查根据实例可能的移动速度进行调整。如果区域清晰,我们将canGoLeft变量设置为true,否则该区域被阻塞,我们将停止实例的水平速度。然后我们重复此检查以检查右侧的碰撞。

if(!place_free(x,y+1)){isOnGround=true;vspeed=0;action=IDLE;}else{isOnGround=false;}在这里,我们正在检查实例正下方是否有可碰撞的对象。如果发生碰撞,我们将变量isOnGround设置为true,以停止垂直速度,然后将实例的状态更改为IDLE。像这样更改状态将确保实例从INAIR状态中逃脱。

此时,我们已经构建了大部分碰撞检测,但我们还没有涵盖所有边缘情况。我们目前只检查实例的左侧、右侧和下方,而不是对角线。问题在于所有条件可能都成立,但当实例以角度移动时,可能导致实例被卡在可碰撞的对象内。

if(!place_free(x,y)){x=xprevious;y=yprevious;move_contact_solid(direction,MAXGRAVITY);vspeed=0;}在这里,我们正在检查实例当前是否与可碰撞的对象相交。如果是,我们将X和Y坐标设置为上一步的位置,然后将其捕捉到移动方向的表面并将垂直速度设置为零。这将以一种现实的方式清理边缘情况。整个脚本应该如下所示:

if(place_free(x-mySpeed,y)){canGoLeft=true;}else{canGoLeft=false;hspeed=0;}ifplace_free(x+mySpeed,y){canGoRight=true;}else{canGoRight=false;hspeed=0;}if(!place_free(x,y+1)){isOnGround=true;vspeed=0;action=IDLE;}else{isOnGround=false;}if(!place_free(x,y)){x=xprevious;y=yprevious;move_contact_solid(direction,MAXGRAVITY);vspeed=0;}检查键盘当我们将系统分解为更可用的脚本时,我们也可以将所有键盘控件放入一个单独的脚本中。这将简化我们将来要创建的代码,并且还可以轻松更改控件或提供替代控件。

创建一个新的脚本,scr_Keyboard_Input,并写入以下代码:

我们现在已经拥有了这个游戏所需的所有通用系统。接下来我们将实施它们。

我们正在构建的玩家角色是我们迄今为止创建的最复杂的对象。玩家不仅会奔跑和跳跃,控制本身也会因玩家是在地面上还是在空中而略有不同。玩家需要知道他们面向的方向,要播放什么动画,是否可以射击武器以及射击的角度。让我们从导入所有精灵开始构建这个:

mySpeed=8;myAim=0;facing=RIGHT;action=IDLE;isDamaged=false;canFire=true;这里有玩家速度、玩家瞄准方向、玩家面向方向和玩家状态的变量。我们还添加了玩家是否能受到伤害或无敌,以及是否能射击的变量。现在我们已经初始化了所有变量。

scr_Collision_Forecasting();scr_Keyboard_Input();您创建的每个脚本实际上都是一个可执行函数。如您在这里所见,您只需编写脚本的名称并在末尾放置括号,即可运行该代码。我们将经常使用这种方法。

scr_Gravity();if(keyLeft&&canGoLeft){if(hspeed>-mySpeed){hspeed-=1;}facing=LEFT;myAim=180;}if(keyRight&&canGoRight){if(hspeed

现在,由于玩家在空中,我们首先要做的是施加重力。接下来是水平移动的控制。我们检查适当的键是否被按下,以及玩家是否能够朝着该方向移动。如果这些条件成立,我们就检查水平速度是否达到了最大速度。如果玩家能够增加速度,我们就稍微增加它。这可以防止玩家在空中太快地改变方向。然后我们设置面向和瞄准方向。

if(!keyLeft&&!keyRight){if(hspeed>=1){hspeed-=1;}if(hspeed<=-1){hspeed+=1;}}我们首先检查左右键是否都没有被按下。如果键没有被按下而玩家正在移动,我们就检查他们的移动方向,然后相应地减少速度。这实际上意味着玩家会滑行停下来。

if(keyUp){action=IDLEUP;myAim=45;}elseif(keyDown){action=IDLEDOWN;myAim=315;}else{action=IDLE;if(facing==LEFT){myAim=180;}if(facing==RIGHT){myAim=0;}}我们首先检查上键是否被按下,如果是,我们将动作更改为IDLEUP,并将瞄准设置为45度,这样玩家就会向上射击。如果不是,我们检查下键,如果合适的话,更改动作和瞄准。最后,如果这两个键都没有被按下,我们就进入标准的IDLE状态。不过,对于瞄准,我们需要先看一下玩家面对的方向。从现在开始,玩家将正确地进入空闲状态。

if(keyLeft&&canGoLeft){hspeed=-mySpeed;facing=LEFT;if(keyUp){action=RUNUP;myAim=150;}elseif(keyDown){action=RUNDOWN;myAim=205;}else{action=RUN;myAim=180;}}我们检查左键是否被按下,以及玩家是否能够向左移动。如果是,我们就设置水平速度,并将面向方向设置为向左。再次检查当前是否按下了上下键,然后将动作和瞄准设置为适当的值。

if(keyJump&&isOnGround){vspeed=-MAXGRAVITY;action=INAIR;}我们检查跳跃键是否被按下,以及玩家是否在地面上。如果是,我们就将垂直速度向上设置为最大重力,并将动作设置为INAIR。

if(isOnGround){scr_Player_GroundControls();}else{scr_Player_AirControls();}scr_Player_Attack();scr_Animation_Control();首先,我们通过检查玩家是否在地面上来确定需要使用哪些控制。然后我们运行适当的控制脚本,然后是攻击脚本,最后是动画控制。

canFire=true;玩家已经准备好测试。为了确保您已经正确设置了玩家,它应该看起来像下面的截图:

我们已经有了玩家,现在我们需要一个世界来放置它。由于我们正在制作一个平台游戏,我们将使用两种类型的构建块:地面对象和平台对象。地面将对玩家不可通过,并将用于外围。平台对象将允许玩家跳过并着陆在上面:

if(obj_Player.y

我们已经成功为平台游戏开发了一系列系统。这要求我们将动画系统和控制等常见元素分离为独特的脚本。如果我们停在这里,可能会感觉做了很多额外的工作却毫无意义。然而,当我们开始构建Boss战时,我们将开始收获这一努力的回报。

Boss战是游戏中最令人愉快的体验之一。构建一个好的Boss战总是一个挑战,但其背后的理论却非常简单。遵循的第一条规则是,Boss应该由三个不断增加难度的独特阶段组成。第二条规则是,Boss应该强调用户最新掌握的技能。第三条也是最后一条规则是,玩家应该始终有事可做。

我们将从不可摧毁的枪开始,因为它将是整个战斗中的主要boss攻击。枪需要旋转,以便始终指向玩家。当它射出枪子弹时,枪子弹的实例将从枪的尖端出现,并朝着枪指向的方向移动。

if(obj_Player.action!=DAMAGE){health-=myDamage;with(obj_Player){y-=1;vspeed=-MAXGRAVITY;hspeed=8*-facing;action=DAMAGE;isDamaged=true;}}这个脚本专门用于敌人的武器。我们首先检查玩家是否已经受伤,以免玩家受到重复惩罚。然后我们通过变量myDamage减少全局生命值。通过使用这样的变量,我们可以让不同的武器造成不同数量的伤害。然后我们通过with语句直接影响玩家。我们想要将玩家抛入空中,但首先我们需要将玩家提高一像素以确保地面碰撞代码不会将其弹回。接下来我们施加垂直速度和水平速度,以相反的方向推开。我们将玩家的动作设置为DAMAGE状态,并指示发生了伤害。

scr_Animation_Control();if(image_index>image_number-1){action=IDLE;}if(canFire){action=RUN;alarm[1]=5;canFire=false;}image_angle=point_direction(x,y,obj_Player.x,obj_Player.y);我们首先运行动画脚本。我们希望枪只播放一次射击动画,因此我们将当前显示的图像与精灵的最后一个图像进行比较。使用image_number可以得到帧数,但由于动画帧从零开始,我们需要减去一。如果是最后一帧,那么枪就进入“空闲”状态。接下来,我们检查枪是否要射击。如果是,我们改变状态以播放射击动画,设置第二个警报为5帧,然后关闭canFire。最后,我们通过根据枪和玩家之间的角度旋转精灵来跟踪玩家。

canFire=true;myX=x+lengthdir_x(tipOfGun,image_angle);myY=y+lengthdir_y(tipOfGun,image_angle);bullet=instance_create(myX,myY,obj_Gun_Bullet);bullet.speed=16;bullet.direction=image_angle;alarm[0]=delay;由于我们需要子弹离开枪口,我们需要一些三角函数。我们可以使用正弦和余弦来计算X和Y值,但有一个更简单的方法。在这里,我们使用lengthdir_x和lengthdir_y来为我们进行数学计算。它所需要的只是径向距离和角度,然后我们可以将其添加到枪的本地坐标中。一旦我们有了这些变量,我们就可以在正确的位置创建子弹,设置其速度和方向。最后,我们重置第一个警报,以便枪再次开火。

if(isOnGround){isDamaged=false;}else{scr_Gravity();}我们检查玩家是否在地面上,因为这将停用伤害状态。如果玩家在空中,我们施加重力,就这样。

if(isDamaged){scr_Player_Damage();}else{if(isOnGround){scr_Player_GroundControls();}else{scr_Player_AirControls();}scr_Player_Attack();}scr_Animation_Control();我们检查玩家是否处于伤害模式,如果是,我们运行伤害脚本。否则,我们像平常一样使用所有控制系统在else语句中。无论是否受伤,动画脚本都会被调用。

第一阶段的武器是一个大炮,它会隐藏自己以保护自己,只有在射击时才会暴露出来。我们将有三门大炮堆叠在一起,使玩家必须跳上平台。要摧毁大炮,玩家需要在大炮暴露时射击每门大炮:

myDamage=10;hspeed=-24;这个武器很强大,会造成10点伤害。我们还设置了水平速度,以便它可以快速穿过房间。

myHealth=20;action=IDLEDOWN;facing=RIGHT;canFire=false;myIdleUp=spr_Cannon_IdleUp;myIdleDown=spr_Cannon_IdleDown;myRunUp=spr_Cannon_RunUp;myRunDown=spr_Cannon_RunDown;myDamage=spr_Cannon_Damage;大炮在被摧毁之前需要承受多次打击,所以我们有一个myHealth变量来跟踪伤害。然后通过面向右侧来设置动作状态,因为我们不会翻转精灵,并建立一个射击变量。然后我们有了大炮工作所需的所有动画状态。

scr_Animation_Control();if(image_index>image_number-1){if(action==RUNUP){action=IDLEUP;}elseif(action==RUNDOWN){action=IDLEDOWN;}}if(canFire){action=RUNUP;alarm[0]=60;canFire=false;}if(myHealth<=0){instance_destroy();}与枪类似,我们首先调用动画系统脚本。然后检查大炮是否在动画的最后一帧。这里有两种不同的空闲状态,取决于大炮是否暴露出来。我们检查我们处于哪种状态,并设置适当的空闲状态。接下来,我们检查大炮是否应该射击,如果应该,我们就会暴露大炮,并设置一个警报,在两秒后创建Cannonball。最后,我们进行健康检查,如果大炮没有生命力了,它就会从游戏中移除。

instance_create(x,y,obj_Cannonball);action=RUNDOWN;在这里我们只是创建一个Cannonball,然后设置动画以收回大炮。

if(action==IDLEUP){myHealth-=10;action=DAMAGE;with(other){instance_destroy();}}我们首先确保只有在大炮暴露时才会应用伤害。如果是的话,我们就会减少它的10点生命值,切换到伤害动画,并移除子弹。大炮现在已经完成。

isPhase_01=true;isPhase_02=false;isPhase_03=false;isBossDefeated=false;boss_X=672;gun=instance_create(32,32,obj_Gun);cannonA=instance_create(boss_X,64,obj_Cannon);cannonB=instance_create(boss_X,192,obj_Cannon);cannonC=instance_create(boss_X,320,obj_Cannon);我们首先建立了三个阶段和Boss是否被击败的变量。然后创建了一个Boss的X位置变量,其中包括不可摧毁的位于房间左上角的枪和Boss所在位置的一堆大炮。我们为Boss的每个武器建立变量,以便Boss可以控制它们。

if(instance_exists(cannonB)){cannonB.canFire=true;}由于玩家可以摧毁大炮,我们需要检查大炮是否仍然存在。如果是,我们将大炮的canFire变量设置为true,大炮的代码将处理其余部分。

if(instance_exists(cannonA)){cannonA.canFire=true;}if(instance_exists(cannonC)){cannonC.canFire=true;}我们需要分别检查两门大炮,以便如果其中一门被摧毁,另一门仍然会射击。

timeline_index=tm_Boss_Phase01;timeline_running=true;timeline_loop=true;构建第二阶段:巨大的激光炮一旦玩家摧毁了所有的大炮,第二阶段就会开始。在这里,我们将有一个巨大的激光炮,不断上下移动。每隔几秒钟,它将发射一道横跨整个房间的巨大激光束。玩家可以随时对激光炮造成伤害,尽管它的生命值要多得多:

myDamage=20;myLaserCannon=0;image_xscale=room_width/8;这个武器的伤害量比其他武器高得多,这非常适合第二阶段。我们还有一个myLaserCannon变量,将用于使激光束与移动的激光炮保持对齐。该值已设置为零,尽管这将成为生成它的激光炮的ID,我们稍后会讨论。最后,我们将精灵拉伸到整个房间。变量image_xscale是一个乘数,这就是为什么我们要将房间宽度除以八,即精灵的宽度。

x=myLaserCannon.x;y=myLaserCannon.y;我们使用创建激光束的激光炮的X和Y坐标。我们将其放入EndStep事件中,因为激光炮将在Step事件中移动,这将确保它始终处于正确的位置。

myHealth=50;mySpeed=2;myBuffer=64;action=IDLE;facing=RIGHT;canFire=false;myIdle=spr_LaserCannon_Idle;myRun=spr_LaserCannon_Run;myDamage=spr_LaserCannon_Damage;我们首先设置激光炮的健康、当前状态、面向方向和非射击的所有标准变量。然后设置激光炮的三种状态的所有动画系统变量。

scr_Animation_Control();if(image_index>image_number-1){action=IDLE;}if(canFire){action=RUN;alarm[0]=5;canFire=false;}if(myHealth<=0){instance_destroy();}这应该开始看起来相当熟悉了。我们首先运行动画系统脚本。然后检查动画的最后一帧是否已播放,如果是,则将激光炮设置为待机状态。接下来,如果激光炮要射击,我们改变状态并设置一个短暂的警报,以便在射击动画播放后创建激光束。最后,我们进行健康检查,并在健康状况不佳时移除激光炮。

这个脚本还没有完成。我们仍然需要添加移动。当激光炮首次创建时,它不会移动。我们希望它在第二阶段开始后才开始移动。在那之后,我们希望激光炮负责垂直运动。

if(yroom_height-myBuffer){vspeed=-mySpeed;}beam=instance_create(x,y,obj_LaserBeam);beam.myLaserCannon=self.id;我们在激光炮的尖端创建一个激光束的实例,然后将激光束的myLaserCannon变量设置为创建它的激光炮的唯一ID。这样做的好处是,如果需要,我们可以在房间中放置多个激光炮。

if(obj_Boss.isPhase_02){myHealth-=5;action=DAMAGE;with(other){instance_destroy();}}由于我们不希望玩家在第二阶段之前就能摧毁激光炮,因此我们检查Boss当前所处的阶段,以确定是否应该施加伤害。如果Boss处于第二阶段,我们会减少激光炮的生命值,将其改为受损状态并移除子弹。激光炮现在已经完整,并准备好实现到Boss中。

laser=instance_create(boss_X,352,obj_LaserCannon);laser.canFire=true;with(laser.beam){instance_destroy();}当LaserCannon射击时,它会创建beam变量,现在我们可以使用它来移除它。

对于最后阶段,我们不会添加另一种武器,而是创建一个受到两个护盾保护的可摧毁的BossCore。护盾将每隔几秒打开一次,以暴露BossCore。我们还将改变Gun,使其快速连发:

myHealth=100;action=IDLE;facing=RIGHT;myIdle=spr_BossCore_Idle;myDamage=spr_BossCore_Damage;scr_Animation_Control();if(action==DAMAGE){if(image_index>image_number-1){action=IDLE;}}if(myHealth<=0){instance_destroy();}if(obj_Boss.isPhase_03&&action==IDLE){myHealth-=2;action=DAMAGE;with(other){instance_destroy();}}我们首先检查Boss是否处于最终阶段,并且BossCore处于空闲状态。如果是,我们减少生命值并切换到受损动画。我们还确保子弹被移除。BossCore现在已经完成,我们可以转移到护盾。

isShielding=true;openPosition=y-64;mySpeed=2;第一个变量将激活护盾是上升还是下降。第二个变量设置抬起护盾的高度值;在这种情况下,它将上升64像素。最后,我们设置一个移动速度的变量。

isShielding=true;openPosition=y+64;mySpeed=2;if(isShielding&&yopenPosition){y-=mySpeed;}我们首先检查护盾是否应该关闭,以及它是否完全关闭。如果没有完全关闭,我们将护盾稍微关闭一点。第二个if语句则相反,检查护盾是否应该打开,以及它是否完全打开。如果没有,我们将抬起护盾一点。

if(isShielding&&y>ystart){y-=2;}if(!isShielding&&y

core=instance_create(boss_X,272,obj_BossCore);shieldUpper=instance_create(boss_X,272,obj_Shield_Upper);shieldLower=instance_create(boss_X,272,obj_Shield_Lower);我们在同一位置创建Boss核心和护盾。

shieldUpper.isShielding=false;shieldLower.isShielding=false;gun.delay=10;在这里,我们正在设置护盾打开,并增加枪的射击速率。

gun.delay=-1;gun.delay=10;shieldUpper.isShielding=true;shieldLower.isShielding=true;gun.delay=45;if(!instance_exists(obj_LaserCannon)&&!isPhase_03){timeline_index=tm_Boss_Phase03;timeline_position=0;isPhase_03=true;}我们检查激光炮是否被摧毁,以及我们是否应该处于最后阶段。如果是,我们需要做的就是切换timeline,将其设置为开始,并设置为最后阶段。

恭喜,你刚刚建立了一场史诗般的Boss战!我们从探讨系统设计和创建一些非常有用的脚本开始这一章。我们建立了一个动画系统,游戏中的大多数对象都使用了它。我们学会了预测碰撞并在玩家身上应用我们自己的自定义重力。我们甚至创建了玩家可以跳跃和着陆的平台。我们介绍了常量,这使得代码对我们来说更容易阅读,对计算机更有效。然后,我们继续构建了一个利用我们之前的知识和新系统的三阶段Boss战斗。

在下一章中,我们将开始创建一个基于物理的游戏,利用GameMaker:Studio的Box2D实现。这将使用完全不同的碰撞检测和物理系统的方法。这也将允许我们拥有对世界做出反应的对象,几乎不需要编写代码!

在本书的其余部分,我们将专注于从概念到完成、发布的单个游戏的创建。我们将利用到目前为止学到的一切,并将介绍各种其他功能,如GameMaker:Studio的物理和粒子系统。我们将构建一些系统来允许角色对话和一个库存。最后,我们将探讨发布游戏的不同方式,包括发布到Facebook。

在本章中,我们将构建一个基于物理的塔倾倒游戏,展示GameMaker:Studio对Box2D开源物理引擎的实现。游戏将包括由各种不同材料制成的塔,如玻璃、木头和钢铁。游戏的目标是通过摧毁这些塔来清除受限区域,利用各种工具。我们将创建TNT来爆炸,一个会摆动的破坏球,以及一个会吸引松散部件的磁铁。最重要的是,所有的碰撞和移动都将由引擎自己完成!

在构建基于物理的游戏时,需要以不同的方式思考如何创建事物。到目前为止,我们专注于通过X/Y坐标来对实例应用移动,或者通过改变speed、vspeed和hspeed变量来实现。当我们使用物理引擎时,这些属性将被忽略。相反,系统本身通过对实例施加力来处理移动。实例将根据自身的属性对力做出反应,并相应地行动。

此外,世界坐标的方向在物理世界中并不相同。在GameMaker标准物理世界中,零度表示右方向,而在Box2D物理世界中,零度表示向上,如下图所示:

要完全理解Box2D物理引擎的工作原理,我们需要看一下它由以下四个组件组成:

就像现实世界一样,物理世界从施加重力开始。重力的大小将决定物体下落的速度以及抵消它所需的力量。在游戏中使用任何物理函数之前,我们需要激活世界物理。

世界现在已经准备好使用物理引擎了!房间的物理设置应该如下截图所示:

为了让物体受到重力和其他力的影响,物体需要一个Fixture。Fixture定义了物理对象的形状和属性。我们需要构建两个对象:一个永远不会移动的地面对象,和一个会受到重力影响的钢柱。

我们需要设置的第一个元素是碰撞形状。有三个选项可供选择:圆形,矩形和形状。最常见的形状是矩形,它只有四个点,总是呈矩形形状。圆形形状适用于完全圆形的物体,因为它是由半径确定的,因此不适用于像鸡蛋这样的圆形。形状是最有用的选项,因为你可以有多达八个碰撞点。这种形状的一个缺点是所有形状必须是凸的,否则将无法工作。请参考下面的屏幕截图,以更好地理解什么是可接受的:

现在形状已经完成,我们可以设置其他物理属性。这里有六个可调整的属性可供我们使用:

现实世界中的不同材料将具有这些属性的不同值。有许多图表可以显示许多类型材料的值,例如钢的密度为每立方米7,820千克,与其他钢接触时的摩擦系数为0.78。试图考虑这些值与游戏中的对象对应时,可能会很快变得令人不知所措。幸运的是,游戏不需要使用现实世界的值,而是可以使用材料的一般概念,例如钢的密度很高,而冰的密度很低。下面是一个图表,其中包含了我们需要处理密度、恢复和摩擦值的一些基本概念。对于线性阻尼和角阻尼来说,情况会有些棘手,因为它们更多地与物体的形状有关。例如,一个圆形的钢钉的角阻尼会比一个方形的钢钉小。无论我们将这些材料的值设置为多少,都应该始终调整,直到它们在游戏中感觉正确。在一个游戏中,金属棒的密度为3,在另一个游戏中为300,这是完全有效的,只要它符合开发者的意图。

我们刚刚完成了我们的第一个物理模拟!现在让我们来看看关节。

有时我们希望两个或更多的对象相互约束,比如链条或布娃娃身体。在物理引擎中,这是通过使用关节来实现的。我们可以使用五种不同类型的关节:

让我们通过创建一个简单的链条来看看关节是如何工作的,它连接到一个锚点上。

for(i=1;i<10;i++){chain[i]=instance_create(x+(i*16),y,obj_ChainLink);}为了构建链条,我们运行一个循环,开始创建九个链条链接。我们从1开始循环,以便正确偏移链条。我们使用一个基本的一维数组来存储每个链条链接的ID,因为当我们添加关节时会需要这个。我们在创建中的x偏移将使每个链接在水平方向上等距离分开。

physics_joint_revolute_create(self,chain[1],self.x,self.y,0,0,false,0,0,false,false);我们首先要创建一个旋转关节,从锚点到第一个链条链接。旋转将围绕锚点的X和Y轴发生。接下来的三个参数与旋转的限制有关:旋转的最小和最大角度,以及这些限制是否生效。在这种情况下,我们不关心,所以我们关闭了任何角度限制。接下来的三个参数是关节是否会自行旋转,以及最大速度、设定速度和是否生效的值。同样,我们关闭了它,所以链条将悬挂在空中。最后一个参数是锚点是否可以与链条发生碰撞,我们不希望发生碰撞。

for(i=1;i<9;i++){physics_joint_revolute_create(chain[i],chain[i+1],chain[i].x,chain[i].y,-20,20,true,0,0,false,false);}这里我们再次使用循环,这样我们可以遍历每个链接并连接下一个链接。注意循环停在9,因为我们已经连接了一段链条。对于链条来说,我们不希望每个链接都有完全的旋转自由度。我们激活了旋转限制,并将其设置为20度的两个方向。

为了在物理世界中移动一个物体,除了由于重力而产生的运动,需要对其施加力。这些力可以从世界中的某一点施加,也可以局部施加到实例上。物体对力的反应取决于它的属性。就像现实世界一样,物体越重,移动它所需的力就越大。

为了查看力,我们将创建TNT,它将爆炸,射出八个碎片。这些碎片将非常密集,需要大量的力才能使它们移动。

mySpeedX=0;mySpeedY=0;力的强度和方向由矢量确定,这就是为什么我们需要X和Y变量。我们将其设置为零,这样它默认不会移动。不要忘记将其应用到obj_TNT_Fragment的创建事件中。

physics_apply_force(x,y,mySpeedX,mySpeedY);函数physics_apply_force是一个基于世界的力,前两个参数表示力来自世界的哪个位置,后两个参数是要施加的力的矢量。

if(point_distance(x,y,xstart,ystart)>128){instance_destroy();}我们在这里所做的就是检查碎片是否从创建时移动了超过128像素。如果是,我们就将其从世界中移除。

instance_destroy();frag_01=instance_create(x,y,obj_TNT_Fragment);frag_01.mySpeedX=100;我们首先创建一个碎片并将其ID存储在一个变量中。然后我们将水平力设置为100单位。这个值似乎足够推动这个物体向右移动。

frag_01.mySpeedX=1000;frag_01.mySpeedX=10000;frag_01=instance_create(x,y,obj_TNT_Fragment);frag_01.mySpeedX=10000;frag_02=instance_create(x,y,obj_TNT_Fragment);frag_02.mySpeedX=-10000;frag_03=instance_create(x,y,obj_TNT_Fragment);frag_03.mySpeedY=10000;frag_04=instance_create(x,y,obj_TNT_Fragment);frag_04.mySpeedY=-10000;frag_05=instance_create(x,y,obj_TNT_Fragment);frag_05.mySpeedX=5000;frag_05.mySpeedY=5000;frag_06=instance_create(x,y,obj_TNT_Fragment);frag_06.mySpeedX=5000;frag_06.mySpeedY=-5000;frag_07=instance_create(x,y,obj_TNT_Fragment);frag_07.mySpeedX=-5000;frag_07.mySpeedY=-5000;frag_08=instance_create(x,y,obj_TNT_Fragment);frag_08.mySpeedX=-5000;frag_08.mySpeedY=5000;instance_destroy();正如我们所看到的,对于每个碎片,我们在X和Y方向上应用适当的力值。我们无需拿出计算器和一些花哨的方程来准确计算需要多少力量,尤其是在倾斜的部分。记住,这是一个视频游戏,我们只需要担心玩家所看到的整体效果和体验,以确定结果是否正确。当你运行游戏时,它应该看起来像以下的截图:

目前,我们对Box2D物理引擎的工作原理有了良好的基础知识。我们已经建立了一个启用了物理的房间,并创建了几个具有夹具和物理属性的对象。我们使用关节将一系列实例连接在一起,并对一个对象施加力以使其移动。现在我们准备开始建立推倒塔游戏!

为了建立一个推倒塔游戏,我们需要创建的第一个元素是支撑结构。我们已经有了一个钢柱,它将是最坚固的部分,但我们还需要几个。将有三种材料类型,每种材料都具有独特的物理属性:钢、木和玻璃。还需要两种不同的尺寸,大和小,以便变化。最后,我们希望大型结构能够分解成小块的碎片。

我们将首先建立所有额外的钢柱和碎片。

我们已经创建了从柱子生成碎片所需的所有对象;我们只需要编写两者之间切换的功能。为此,我们将构建一个简单的系统,可以用于所有柱子。在这个游戏中,我们只会打碎较大的柱子。如果施加了足够的力,小柱子和碎片将被销毁。

myDamage=5;debris01=obj_Debris_Glass_01;debris02=obj_Debris_Glass_02;debris03=obj_Debris_Glass_03;我们首先要使用的变量将用于柱子可以承受的伤害量。在这种情况下,玻璃柱需要至少五点伤害才能分解。接下来,我们为需要生成的每个碎片设置变量。

if(abs(other.phy_linear_velocity_x)>myDamage||abs(other.phy_linear_velocity_y)>myDamage){if(phy_mass<=other.phy_mass){p1=instance_create(x,y,debris01);p1.phy_speed_x=phy_speed_x;p1.phy_speed_y=phy_speed_y;p1.phy_rotation=phy_rotation;p2=instance_create(x,y,debris02);p2.phy_speed_x=phy_speed_x;p2.phy_speed_y=phy_speed_y;p2.phy_rotation=phy_rotation;p3=instance_create(x,y,debris03);p3.phy_speed_x=phy_speed_x;p3.phy_speed_y=phy_speed_y;p3.phy_rotation=phy_rotation;instance_destroy();}}我们首先确定碰撞的速度,这样它只适用于移动的物体,而不是静止的物体。我们使用一个叫做abs的函数,它将确保我们得到的速度始终是一个正数。这将使比较变得更容易,因为我们不需要考虑运动的方向。如果碰撞物体的速度比柱子的伤害量快,那么我们就检查第二个条件语句,比较碰撞中涉及的两个实例的质量。我们只希望柱子在被比自身更强的东西击中时分解。让玻璃柱摧毁钢柱是毫无意义的。如果柱子被更重的物体击中,我们就生成碎片。对于每个碎片,我们需要根据柱子的物理速度和旋转将其放在适当的位置。创建了碎片后,我们销毁了柱子。

myDamage=16;debris01=obj_Debris_Wood_01;debris02=obj_Debris_Wood_02;debris03=obj_Debris_Wood_03;我们已经增加了需要施加的伤害速度,以便它能够分解的要求。玻璃容易破碎,而木头不容易。我们还为木头分配了适当的碎片。

myDamage=25;debris01=obj_Debris_Steel_01;debris02=obj_Debris_Steel_02;debris03=obj_Debris_Steel_03;与以前一样,我们增加了造成伤害所需的速度,并设置了正确的碎片生成。

if(abs(other.phy_linear_velocity_x)>myDamage||abs(other.phy_linear_velocity_y)>myDamage){if(phy_mass

一切都运行正常,尽管有点无聊,因为缺少声音。碎片被很快摧毁也不太令人满意。让我们解决这两个问题。

impact=snd_Impact_Glass;shatter=snd_Shatter_Glass;isTapped=false;isActive=false;alarm[0]=room_speed;我们首先为Impact和Shatter声音分配变量。我们只希望允许撞击声音播放一次,所以我们创建了isTapped变量。isActive变量和警报将被使用,以便在游戏开始时不会发出声音。当物理系统开始时,世界中的所有活动实例都将受到重力的影响,这将导致碰撞。这反过来意味着当似乎没有东西在移动时,撞击声音会发生。

sound_play(shatter);当碎片生成时,我们将播放一次Shatter声音。请注意,我们已经给这个声音设置了优先级为10,这意味着如果需要播放太多声音,这个声音将优先于优先级较低的声音。

}else{if(!isTapped){sound_play(impact);isTapped=true;}}如果只发生了轻微的碰撞,我们会检查是否之前已经播放了声音。如果没有,那么我们会播放撞击声音,优先级较低,并阻止该代码再次执行。

if(isActive){if(abs(other.phy_linear_velocity_x)>myDamage||abs(other.phy_linear_velocity_y)>myDamage){if(phy_massmyDamage||abs(other.phy_linear_velocity_y)>myDamage){if(phy_mass

让我们从破坏球开始,因为我们已经建造了它的大部分。我们将利用链条和锚点,并在其上添加一个球。

ball=instance_create(chain[9].x+24,y,obj_WreckingBall);physics_joint_revolute_create(chain[9],ball,chain[9].x,chain[9].y,-30,30,true,0,0,false,false);在链条末端创建一个破坏球,偏移24像素以便正确定位。然后在链条的最后一个链接和破坏球之间添加一个旋转关节,旋转限制为每个方向30度。

可以通过在子事件代码中使用函数event_inherited()同时执行父事件和子事件。

for(i=1;i<10;i++){chain[i]=instance_create(x+(i*16),y,obj_ChainLink);chain[i].phy_active=false;}physics_joint_revolute_create(self,chain[1],self.x,self.y,0,0,false,0,0,false,false);for(i=1;i<9;i++){physics_joint_revolute_create(chain[i],chain[i+1],chain[i].x,chain[i].y,-20,20,true,0,0,false,false);}ball=instance_create(chain[9].x+24,y,obj_WreckingBall);ball.phy_active=false;physics_joint_revolute_create(chain[9],ball,chain[9].x,chain[9].y,-30,30,true,0,0,false,false);for(i=1;i<10;i++){chain[i].phy_active=true;}ball.phy_active=true;当运行此脚本时,一个简单的for循环会激活每个链,然后激活破坏球。

我们的第三个拆迁设备将是一个磁吊车。这个吊车将下降并拾起任何由钢制成的小柱子和碎片。然后它将带着它收集到的任何东西抬起来。

physics_joint_prismatic_create(id,other,x,y,0,1,0,0,true,0,0,false,false);other.isCollected=true;在这里,我们使用磁铁和与之碰撞的实例创建了一个棱柱关节。前两个参数是要连接的两个实例,然后是它们在世界中连接的位置。第五和第六个参数是它可以移动的方向,在这种情况下只能垂直移动。接下来的三个是移动的限制。我们不希望它移动,所以将最小/最大值设置为零。限制需要启用,否则它们将不会随着磁铁一起升起。接下来的三个是用于移动这个关节的电机。最后一个参数是与我们想要避免碰撞的对象的碰撞。关节创建后,我们将收集变量设置为false。

magnet=instance_create(x,y+160,obj_Magnet);magnet.phy_active=false;crane=physics_joint_prismatic_create(id,magnet,x,y,0,1,-128,128,true,100000,20000,true,false);我们将磁铁创建在起重机基座下方,并将其从物理世界中取消激活。然后我们在两个实例之间应用了一个棱柱关节。这次我们允许在垂直方向上移动128像素。我们还运行一个电机,这样磁铁就可以自己上下移动。电机可以施加的最大力是100000,我们让电机以20000的速度下降磁铁。正如你所看到的,我们使用的值非常高,这是为了确保重磁铁可以吊起大量的钢渣。

magnet.phy_active=true;alarm[0]=5*room_speed;我们希望磁铁首先下降,因此我们在物理世界中使其活动。我们使用了一个设置为五秒的闹钟,这将使磁铁重新上升。

physics_joint_set_value(crane,phy_joint_motor_speed,-20000);我们将电机速度的值设置为-20000。同样,我们使用一个非常大的数字来确保在柱子碎片的额外重量下再次上升。

draw_self();draw_set_color(c_dkgray);draw_line_width(x,y,magnet.x,magnet.y-16,8);每当使用Draw事件时,它会覆盖对象的默认精灵绘制。因此,我们使用draw_self来纠正该覆盖。接下来,我们设置要使用的颜色,这里我们使用默认的深灰色,然后在起重机底座和磁铁顶部之间绘制一条8像素宽的线。

到目前为止,我们已经建立了一个有趣的小玩具,但它还不是一个游戏。我们没有赢或输的条件,没有挑战,也没有奖励。我们需要给玩家一些事情去做,并挑战自己。我们将从实现赢的条件开始;清除预设区域内的所有柱子。我们将创建一些具有各种塔和区域的关卡来清理。我们还将创建一个装备菜单,让玩家可以选择他们想要使用的物品,并将它们放置在世界中。

image_speed=0;isTouching=true;我们首先停止分配精灵的动画。所有区域都将包括两帧动画的精灵。第一帧表示碰撞,第二帧是全清信号。我们还有一个变量,用于识别柱子或碎片是否与区域接触。

if(collision_rectangle(bbox_left,bbox_top,bbox_right,bbox_bottom,obj_Pillar_Parent,false,false)){image_index=0;isTouching=true;}else{image_index=1;isTouching=false;}在这里,我们使用一个函数collision_rectangle来确定柱子父对象当前是否与区域接触。我们不能使用碰撞事件来检查接触,因为我们需要观察缺乏碰撞的发生。我们使用精灵的边界框参数来确定碰撞区域的大小。这将允许我们拥有多个区域精灵,而无需任何额外的代码。如果发生碰撞,我们切换到动画的第一帧,并指示当前正在发生碰撞。否则,我们切换到动画的第二帧,并指示区域当前没有碰撞。

isTriggered=false;isVictory=false;我们将使用两个变量。我们将使用isTriggered来检查设备是否已被激活。isVictory变量将确定胜利条件是否发生。

if(isTriggered){if(instance_exists(obj_TNT)){with(obj_TNT){scr_TNT_Activate();}}if(instance_exists(obj_Anchor)){with(obj_Anchor){scr_Anchor_Activate();}}if(instance_exists(obj_CraneBase)){with(obj_CraneBase){scr_CraneBase_Activate();}}alarm[0]=8*room_speed;isTriggered=false;}这段代码只有在变量isTriggered为true时才会执行。如果是,我们检查是否存在TNT的实例。如果有实例,我们使用with语句来运行每个实例的激活脚本。对于Anchor和CraneBase,我们也做同样的操作。我们还设置了一个8秒的警报,这时我们将检查胜利条件。最后,我们将isTriggered设置回false,这样它就会第二次运行。

isTriggered=true;with(obj_Zone_Parent){if(isTouching){returnfalse;}}returntrue;通过使用with语句来检查obj_Zone_Parent,我们能够查找该对象及其所有子对象的所有实例。我们将在这里使用return语句来帮助我们退出脚本。当执行返回时,脚本将立即停止,之后的任何代码都不会运行。如果任何实例有碰撞,我们返回false;否则,如果没有实例有碰撞,我们返回true。

isVictory=scr_WinCondition();if(isVictory){if(room_exists(room_next(room))){room_goto_next();}}else{room_restart();}我们首先捕获从scr_WinCondition返回的boolean,并将其存储在isVictory变量中。如果它是true,我们检查当前房间之后是否有另一个房间。房间的顺序由它们在资源树中的位置决定,下一个房间是资源树中它下面的房间。如果有另一个房间,我们就进入它。如果胜利条件是false,我们重新开始这个房间。

虽然我们现在有了获胜条件,但玩家还没有任何事情可做。我们将通过添加一个装备菜单来解决这个问题。该菜单将放置在游戏屏幕的底部,并具有TNT、挖掘机和磁吊机的可选择图标。当点击图标时,它将创建相应设备的可放置幽灵版本。要放置设备,玩家只需在世界的某个地方点击,幽灵就会变成真正的物品。

isActive=false;draw_sprite(spr_Menu_BG,0,0,400);menuItem_Zone=32;menuItems_Y=440;menuItem1_X=40;draw_sprite(spr_Menu_TNT,0,menuItem1_X,menuItems_Y);menuItem2_X=104;draw_sprite(spr_Menu_WreckingBall,0,menuItem2_X,menuItems_Y);menuItem3_X=168;draw_sprite(spr_Menu_MagneticCrane,0,menuItem3_X,menuItems_Y);由于我们知道每个房间都将以640x480的分辨率显示,所以我们首先在屏幕底部绘制背景精灵。我们将使用一个变量menuItem_Zone来帮助确定鼠标在精灵上的坐标。在未来的编码中,我们需要确切地知道图标放置的位置,因此我们为每个菜单项的坐标创建变量,然后在屏幕上绘制精灵。

image_alpha=0.5;myTool=obj_TNT;为了区分幽灵TNT和真实TNT,我们首先将透明度设置为50%。我们将使用一些通用脚本来处理所有幽灵,因此我们需要一个变量来指示这个幽灵代表什么。

x=mouse_x;y=mouse_y;winHeight=window_get_height();winMouse=window_mouse_get_y();if(!place_meeting(x,y,obj_Pillar_Parent)&&winMouse

myTool=obj_Anchor;draw_set_alpha(0.5);draw_sprite(spr_Anchor,0,x,y)for(i=1;i<10;i++){draw_sprite(spr_ChainLink,0,x+i*16,y)}draw_sprite(spr_WreckingBall,0,x+(9*16+24),y);draw_set_alpha(1);我们首先将实例设置为半透明,使其看起来像幽灵。然后绘制锚,运行一个for循环来绘制链条,然后挖掘球在链条末端绘制。最后,我们需要在代码末尾将透明度重置为完整。这一点非常重要,因为绘制事件会影响屏幕上绘制的所有内容。如果我们不重置它,世界中的每个对象都会有半透明度。

myTool=obj_CraneBase;draw_set_alpha(0.5);draw_sprite(spr_CraneBase,0,x,y)draw_set_color(c_dkgray);draw_line_width(x,y,x,y+144,8);draw_sprite(spr_Magnet,0,x,y+160);draw_set_alpha(1);与幽灵挖掘球类似,我们首先将透明度设置为50%。然后绘制起重机底座,绘制一条粗灰色线和磁铁,位置与放置时相同。然后将透明度恢复到完整。

现在我们有一个可用的游戏,剩下的就是创建一些可以玩的关卡。我们将建立一些具有不同塔和房间大小的关卡,以确保我们的所有代码都能正常工作。

由于这是第一关,让我们为玩家设置简单一点,只使用玻璃柱。在本书的这一部分,我们一直在按照创建的顺序放置对象。在放置柱子时,我们可以轻松地旋转它们并将它们放置在世界中。要在房间属性编辑器中旋转一个实例,首先将实例正常放置在房间中,然后在仍然选中的情况下,在对象选项卡中更改旋转值。有缩放实例的选项,但我们不能在物理模拟中使用这些选项,因为它不会影响夹具大小。

我们在本章涵盖了很多内容。我们从使用Box2D物理系统的基础知识开始。我们学会了如何为对象分配Fixture以及可以更改的不同属性。我们创建了一个利用旋转关节的链条和破坏球,使每个部分都会随着前面的部分旋转。我们建造了使用力来移动世界中的物体的TNT和磁吊机。当它们与更重、更坚固的物体碰撞时,我们还制造了从大柱子上产生碎片的效果。此外,我们了解了DrawGUI事件以及精灵在房间中的位置与屏幕上的位置之间的区别。这使我们能够创建一个菜单,无论房间的大小如何,都能在屏幕上正确显示。

我们将在下一章继续开发这个游戏。我们将创建一个商店和库存系统,以便玩家拥有有限数量的装备,并可以购买额外的物品。我们还将深入探讨显示对话,以便我们可以向游戏添加一些基本的故事元素,激励玩家摧毁更多的东西!

在上一章中,我们为测试HUD和游戏难度构建了两个房间,Level_01和Level_12。现在我们需要为这两个之间的所有级别制作房间,以及为前端、商店和级别选择制作一些额外的房间:

主菜单是玩家将看到的第一个屏幕,它由两个对象组成:一个开始游戏的按钮和一个包含所有全局变量的游戏初始化对象:

score=0;image_speed=0;image_index=0;image_index=1;image_index=0;父对象现在已经完成,设置应该如下截图所示:

room_goto(LevelSelect);使用2D数组选择级别我们要构建的下一个房间是LevelSelect。在这个房间中,将有一个用于前往商店的按钮,以及游戏中每个级别的按钮,但一开始只有第一个级别是解锁的。随着玩家的进展,按钮将会解锁,玩家将可以访问所有以前的级别。为了实现这一点,我们将动态创建游戏中每个级别的按钮,并使用2D数组来存储所有这些信息。

2D数组就像我们在书中已经使用过的数组一样。它是一个静态的数据列表,但它允许每行有多个值,就像电子表格一样。这是我们可以使用的一个非常强大的工具,因为它使得将几个不同的元素组合在一起变得更加简单:

level[0,0]=Level_01;level[0,1]=false;要创建一个2D数组,只需要在括号内放入两个数字。第一个数字是行数,第二个是列数。这里我们只有一行,有两列。第一列将保存房间名称,第二列将保存该房间是否被锁定;在这种情况下,Level_01是解锁的。

globalvarlevel,totalLevels;level[0,0]=Level_01;level[0,1]=false;level[1,0]=Level_02;level[1,1]=true;level[2,0]=Level_03;level[2,1]=true;level[3,0]=Level_04;level[3,1]=true;level[4,0]=Level_05;level[4,1]=true;level[5,0]=Level_06;level[5,1]=true;level[6,0]=Level_07;level[6,1]=true;level[7,0]=Level_08;level[7,1]=true;level[8,0]=Level_09;level[8,1]=true;level[9,0]=Level_10;level[9,1]=true;level[10,0]=Level_11;level[10,1]=true;level[11,0]=Level_12;level[11,1]=true;totalLevels=12;scr_Global_Levels();room_goto(Shop);isLocked=true;myLevel=MainMenu;myNum=0;image_speed=0;alarm[0]=1;我们首先将所有按钮默认设置为锁定状态。我们为点击时应该转到的默认房间设置一个默认房间,并在顶部绘制一个数字。最后,我们停止精灵动画,并设置一个步骤的警报。

if(isLocked){image_index=2;}else{image_index=0;}如果按钮被锁定,我们将设置精灵显示锁定帧。否则,它是解锁的,我们显示第一帧。

if(isLocked){exit;}else{image_index=1;}对于按钮的悬停状态,我们首先检查它是否被锁定。如果是,我们立即退出脚本。如果未锁定,我们切换到悬停帧。

if(isLocked){exit;}else{image_index=0;}if(isLocked){exit;}else{room_goto(myLevel);}draw_self();draw_set_color(c_black);draw_set_font(fnt_Large);draw_set_halign(fa_center);draw_text(x,y-12,myNum);draw_set_font(-1);首先,我们需要绘制应用于对象本身的精灵。接下来,我们将绘图颜色设置为黑色,设置字体,并居中对齐文本。然后,我们绘制myNum变量中保存的文本,将其在Y轴上下降一点,使其在垂直方向上居中。由于我们将在这个游戏中绘制大量文本,我们应该通过将字体设置为-1值来强制使用默认字体。这将有助于防止此字体影响游戏中的任何其他绘制字体:

我们唯一剩下要构建的房间是商店,玩家将能够购买用于每个级别的装备。房间将包括每种装备的图标、价格列表和购买装备的按钮。我们还将有一个显示当前玩家拥有多少现金的显示,并且当他们花钱时,这将更新:

globalvaryellow;yellow=make_color_rgb(249,170,0);我们为我们的颜色创建一个全局变量,然后使用一个带有红色、绿色和蓝色数量参数的函数来制作我们特殊的黄色。

为了构建一个合适的商店和库存系统,我们需要比静态数组更多的数据控制。我们需要更加灵活和可搜索的东西。这就是数据结构的用武之地。数据结构是特殊的动态结构,类似于数组,但具有使用特定函数操纵数据的能力,例如洗牌或重新排序数据。GameMaker:Studio带有六种不同类型的数据结构,每种都有自己的一套函数和好处:

我们将从网格数据结构开始,因为我们需要每个物品的几行和列的信息。创建一个新的脚本,scr_Global_Equipment,并编写以下代码来构建网格:

score=0;scr_Global_Levels();scr_Global_Colors();scr_Global_Equipment();scr_Global_Inventory();if(score>ds_grid_get(equip,myItem,COST)){ds_grid_add(equip,myItem,AMOUNT,1);score-=ds_grid_get(equip,myItem,COST);if(ds_list_find_index(inventory,myItem)==-1){ds_list_add(inventory,myItem);}}为了购买物品,我们首先需要检查玩家是否有足够的钱。为此,我们将score与我们创建的网格中保存的数据进行比较。您会注意到我们有一个变量myItem,它在按钮本身中尚未初始化。稍后,当我们生成按钮时,我们将动态创建该变量。如果玩家可以购买该物品,我们增加玩家拥有的物品数量,并将金钱减去物品的价格。最后,我们检查玩家当前库存中是否已经有该物品。如果这是其类型的第一个物品,我们将其添加到库存列表中。

for(i=0;i

draw_set_color(c_black);draw_set_halign(fa_center);for(i=0;i

draw_set_color(yellow);draw_set_font(fnt_Medium);draw_text(96,416,"Cash");draw_set_font(fnt_Large);draw_text(96,440,score);draw_set_font(-1);我们将颜色设置为我们特殊的黄色,用于本文的其余部分。我们设置一个中等字体来显示单词现金,然后改为大字体显示实际金额。最后,我们将字体重置为默认值。

globalvarisGameActive,isTimerStarted;isGameActive=true;isTimerStarted=false;在这里,我们初始化了两个变量,这些变量将改进游戏的功能。变量isGameActive将在每个级别开始时设置为true,以开始游戏。它还将使我们能够在关卡结束时显示信息,同时防止玩家使用菜单。isTimerStarted变量将用于倒计时清除区域。

if(ds_list_size(inventory)==0){room_goto(Shop);}我们检查库存的大小,如果里面什么都没有,我们就去商店。

draw_sprite(ds_grid_get(startEquip,item,SPRITE),0,myX,menuItems_Y);if(!isActive){if(win_Y>menuItems_Y-menuItem_Zone&&win_YmyX-menuItem_Zone&&win_X0){instance_create(myX,menuItems_Y,ds_grid_get(startEquip,item,OBJECT));ds_grid_add(startEquip,item,AMOUNT,-1);tempCost+=ds_grid_get(startEquip,item,COST);isActive=true;}}}}以前,我们为每个装备都有类似的代码。现在我们有了数据结构,我们可以使用信息动态创建所有的装备。我们首先绘制从本地startEquip网格中提取的精灵。然后我们检查菜单是否处于活动状态,因为玩家试图放置物品。我们检查鼠标在屏幕上的位置,看它是否悬停在按钮上,并更改为适当的动画帧。如果点击按钮,我们创建所选的物品,从网格中减去一个物品单位,将物品的价值添加到玩家的支出中,并使菜单处于活动状态。

draw_set_color(c_black);draw_set_halign(fa_center);draw_set_font(fnt_Small);draw_text(myX+20,menuY+14,ds_grid_get(startEquip,item,AMOUNT));我们在这里所做的只是设置文本的颜色、水平对齐和字体。然后我们像在商店里一样在右下角绘制每个物品的单位数量。

if(isGameActive){Win_X=window_mouse_get_x();Win_Y=window_mouse_get_y();for(i=0;i

draw_sprite(spr_Button_Restart,0,restartX,menuItems_Y);if(win_Y>menuItems_Y-menuItem_Zone&&win_YrestartX-menuItem_Zone&&win_X

draw_sprite(spr_Button_Shop,0,shopX,menuItems_Y);if(win_Y>menuItems_Y-menuItem_Zone&&win_YshopX-menuItem_Zone*2&&win_X

draw_sprite(spr_Menu_BG,0,0,400);if(isGameActive){Win_X=window_mouse_get_x();Win_Y=window_mouse_get_y();for(i=0;i

到目前为止,游戏中几乎没有任何风险或回报。我们已经在游戏中添加了商店,可以购买物品,但我们还不能赚取任何现金。只要物品在我们的库存中,我们可以使用尽可能多的装备,这意味着没有必要进行策略。如果玩家用完所有的钱或完成所有的关卡,我们需要添加一个游戏结束屏幕。由于目前玩家不知道自己的表现如何,我们还需要一个得分屏幕来显示。现在是时候添加这些功能了,首先是奖励玩家分数:

y-=1;fadeOut++;if(fadeOut>60){alpha-=0.05;}if(alpha<=0){instance_destroy();}draw_set_color(c_black);draw_set_font(fnt_Small);draw_set_alpha(alpha);draw_text(x,y,myValue);draw_set_alpha(1);这个对象不是物理世界的一部分,所以我们可以手动在每一帧垂直移动它。我们增加fadeOut变量,一旦它达到60,我们开始逐渐减少alpha变量的值。一旦alpha达到零,我们销毁实例,以便它不占用任何内存。之后我们设置颜色、字体和透明度值,并绘制文本。myValue变量将在创建时从生成它的对象传递。最后,我们将透明度设置回完全不透明;否则整个房间中的其他所有东西也会淡出。

scoreFloat=instance_create(x,y,obj_ScoreFloat);scoreFloat.myValue=floor(phy_mass);obj_Menu.tempScore+=scoreFloat.myValue;当柱子破碎时,它将生成一个obj_ScoreFloat的实例。然后我们将显示的值设置为对象总质量的向下取整。最后,我们将菜单的tempScore增加相同的数量。

if(isVictory){draw_sprite(spr_Button_NextLevel,0,nextLevelX,menuItems_Y);if(win_Y>menuItems_Y-menuItem_Zone&&win_YnextLevelX-menuItem_Zone&&win_X

draw_sprite(spr_Screen_BG,0,screenX,screenY);draw_set_color(c_black);draw_set_halign(fa_center);draw_set_font(fnt_Large);draw_text(screenX,60,room_get_name(room));draw_text(screenX,144,obj_Menu.tempScore);draw_text(screenX,204,obj_Menu.tempCost);draw_text(screenX,284,obj_Menu.tempScore-obj_Menu.tempCost);draw_set_font(fnt_Medium);draw_text(screenX,120,"DamageEstimate");draw_text(screenX,180,"EquipmentCost");draw_text(screenX,260,"TotalProfit");draw_set_font(-1);首先绘制背景精灵。然后设置颜色、对齐和字体。我们使用最大的字体来绘制房间的名称和损坏量、使用的装备量和总利润的值。然后切换到中等字体,写出每个值的描述,放在相应数字的上方。我们完成了绘制文本,所以将字体设置回默认值。

Win_X=window_mouse_get_x();Win_Y=window_mouse_get_y();scr_Menu_Button_Restart();scr_Menu_Button_Shop();scr_Menu_Button_NextLevel();就像我们在菜单中所做的那样,我们获取屏幕上鼠标的坐标,然后执行三个按钮的脚本。

instance_create(0,0,obj_ScoreScreen);with(obj_Menu){ds_grid_copy(equip,startEquip);ds_grid_destroy(startEquip);score+=tempScore-tempCost;for(i=0;i

scr_ScoreCleanUp();if(score<0){room_goto(GameOver);}else{room_goto(level[i+1,0]);}if(!isGameActive){scr_ScoreCleanUp();}if(score<0){room_goto(GameOver);}else{room_goto(Shop);}现在只有在游戏停止时才会传输分数。我们还在这里检查游戏结束状态的分数,以决定点击时要去哪个房间。

我们已经有了关卡的良好结局,但玩家可能不确定该怎么做。我们需要一点故事来推销摧毁塔的想法,并解释玩家在每个关卡需要做什么。为此,我们将在每个关卡开始时添加一个屏幕,就像在每个关卡开始时的得分屏幕一样:

draw_sprite(spr_Button_Start,0,startX,startY);if(win_Y>startY-start_ZoneHeight&&win_YstartX-start_ZoneWidth&&win_X

每当游戏需要保存数据时,唯一可行的选择是将数据写入游戏本身之外的文件。对于基于网络的游戏来说,这可能会带来问题,因为任何需要下载的文件都需要用户明确允许。这意味着玩家会知道文件的名称和位置,这反过来意味着他们可以轻松地黑客自己的保存文件。为了避开这个障碍,HTML5提供了一个名为本地存储的解决方案。

本地存储允许网页,或者在我们的情况下是嵌入到网页中的游戏,将数据保存在浏览器内部。这类似于互联网cookie,但具有更快、更安全的优势,并且可能能够存储更多的信息。由于这些数据保存在浏览器中,用户不会收到文件被创建或访问的通知;他们无法轻松地看到数据,而且只能从创建它的域中访问。这使得它非常适合保存我们的游戏数据。

清除保存的数据只有两种方法。覆盖数据或清除浏览器的缓存。建议您始终在私人浏览器模式下测试游戏,以确保保存系统正常工作。

[section]key=value[playerData]playerFirstName=JasonplayerLastName=Elliott本地存储要求所有数据都是键/值对,因此我们将使用Ini文件系统。虽然可以使用文本文件系统,但对于我们需要保存的少量数据以及额外编码所需的工作量来说,这并不是很有益。

theFile=argument0;ini_open(theFile);ini_write_real("Score","Cash",score);for(i=0;i

theFile=argument0;if(!file_exists(theFile)){scr_GameSave(theFile);}else{ini_open(theFile);score=ini_read_real("Score","Cash","");for(i=0;i0){ds_list_add(inventory,j);}}ini_close();}我们首先检查通过参数传递的文件是否存在。如果在本地存储中找不到文件,比如游戏首次运行时,我们会运行保存脚本来初始化数值。如果找到文件,我们会打开保存文件并将数据读入游戏,就像我们保存的那样。我们设置分数,解锁适当的关卡,并加载装备。我们还会遍历库存,以确保所有装备对玩家可用。

globalvarsaveFile;saveFile="Default.ini";scr_GameLoad(saveFile);我们为文件名创建一个全局变量,以便稍后轻松保存我们的数据。然后将字符串传递给加载脚本。此代码必须放在脚本的末尾,因为我们需要首先初始化网格和数组的默认值。

with(obj_Menu){ds_grid_copy(equip,startEquip);ds_grid_destroy(startEquip);score+=tempScore-tempCost;for(i=0;i

globalvarplayerName;playerName="";nameSpace=0;nameMax=8;draw_set_color(c_black);draw_set_halign(fa_center);draw_set_font(fnt_Small);draw_text(320,280,"TypeInYourName");draw_set_font(fnt_Large);draw_text(320,300,playerName);draw_set_font(-1);if(nameSpace=65&&keyboard_key<=90){playerName=playerName+chr(keyboard_key);nameSpace++;}}我们只希望名称最多为八个字母,因此我们首先检查当前名称是否仍有空间。如果我们可以输入另一个字母,然后我们检查正在按下的键是否是字母。如果按下了字母,我们将该字母添加到字符串的末尾,然后指示另一个空间已被使用。

if(keyboard_key==vk_backspace){lastLetter=string_length(playerName);playerName=string_delete(playerName,lastLetter,1)if(nameSpace>0){namespace--;}}如果用户按下退格键,我们获取字符串的长度,以找出字符串的最后一个空格在哪里。一旦我们知道了这一点,我们就可以删除字符串末尾的字母。最后,我们检查是否仍然有剩余的字母,如果有,就减少空格计数。这是必要的,这样我们就不会进入负空间。

if(room==MainMenu){saveFile=string(playerName+".ini");scr_GameLoad(saveFile);}总结干得好!在本章中,我们通过添加整个前端,包括商店和可解锁的级别,真正完善了游戏体验。我们学会了使用网格、地图和列表数据结构来保存各种信息。我们重建了HUD,以便能够显示更多按钮,只显示可用的装备,并建立了一个基本的倒计时器。我们创建了一个得分屏幕,向玩家展示他们在级别中的表现。我们还在每个级别的前面创建了一个介绍屏幕,利用了一个简单的打字机效果,向我们展示了如何操作字符串。最后,我们添加了一个保存系统,教会了我们如何使用本地存储,并允许我们拥有多个玩家存档!

总的来说,我们将游戏从一个可玩的原型变成了一个完全成熟的游戏,有一个开始和结束,还有很多风险和回报。在下一章中,我们将继续通过查看粒子效果并将其添加到柱子和碎片的销毁中来完善这个游戏。让我们继续吧!

在过去的两章中,我们构建了一个利用关节、夹具和力的强大的基于物理的游戏。然后我们添加了一个完整的前端,其中有一个商店,玩家可以购买装备和解锁级别。我们还更新了HUD并实现了介绍和得分屏幕来完善每个级别。感觉几乎像是一个完整的游戏,但缺少了一些东西。TNT突然消失了,柱子的破裂也突然出现了。在本章中,我们将通过向游戏添加一些粒子效果来解决这个问题,以帮助掩盖这些变化。经过这一点点的润色,我们的游戏就可以发布了!

粒子效果是游戏中用来表示动态和复杂现象的装饰性修饰,比如火、烟和雨。要创建一个粒子效果,需要三个元素:一个系统,发射器和粒子本身。

粒子系统是粒子和发射器存在的宇宙。就像宇宙一样,我们无法定义大小,但可以定义一个原点,所有发射器和粒子都将相对于该原点放置。我们也可以同时存在多个粒子系统,并可以设置不同深度来绘制粒子。虽然我们可以拥有尽可能多的粒子系统,但最好尽可能少,以防止可能的内存泄漏。原因是一旦创建了粒子系统,它将永远存在,除非手动销毁。销毁生成它的实例或更改房间不会移除系统,因此确保在不再需要时将其移除。通过销毁粒子系统,将同时移除系统中的所有发射器和粒子。

在定义空间区域时,有四种形状选项:菱形、椭圆、线和矩形。可以在前图中看到每种形状的示例,它们都使用完全相同的尺寸、粒子数量和分布。虽然使用这些形状之一之间没有功能上的区别,但效果本身可以受益于正确选择的形状。例如,只有线才能使效果看起来呈30度角。

粒子的分布也会影响粒子从发射器中喷射出来的方式。如前图所示,有三种不同的分布。线性将在发射器区域内均匀随机分布粒子。高斯将更多地在区域中心产生粒子。反高斯是高斯的反向,粒子将更靠近发射器的边缘产生。

粒子是从发射器产生的图形资源。可以创建两种类型的粒子:形状和精灵。形状是内置在GameMaker:Studio中用作粒子的64x64像素精灵的集合。如下图所示,这些形状适用于大多数常见的效果,比如烟花和火焰。当想要为游戏创建更专业的效果时,可以使用资源树中的任何精灵。

通过调整许多可用属性,我们可以通过粒子做很多事情。我们可以定义它的寿命范围,它应该是什么颜色,以及它如何移动。我们甚至可以在每个粒子的死亡点产生更多的粒子。然而,也有一些我们无法做到的事情。为了降低图形处理成本,没有能力在效果中操纵单个粒子。此外,粒子无法与任何对象进行交互,因此无法知道粒子是否与世界中的实例发生了碰撞。如果我们需要这种控制,我们需要构建对象。

使用粒子效果可以真正提高游戏的视觉质量,但在开发旨在在浏览器中玩的游戏时,我们需要小心。在实施粒子效果之前,了解可能遇到的问题非常重要。围绕粒子的最大问题是,为了使它们能够平稳地渲染而没有任何延迟,它们需要使用图形处理器而不是主CPU进行渲染。大多数浏览器通过一个名为WebGL的JavaScriptAPI允许这种情况发生。然而,这不是HTML5的标准,微软已经表示他们没有计划在可预见的未来支持InternetExplorer。这意味着游戏潜在受众的一个重要部分可能会因为使用粒子而遭受游戏体验不佳。此外,即使启用了WebGL,粒子具有附加混合和高级颜色混合的功能也无法使用,因为目前没有浏览器支持这个功能。现在我们知道了这一点,我们准备制作一些效果!

我们将构建一些不同的粒子效果,以演示游戏中实现效果的各种方式,并研究可能出现的一些问题。为了保持简单,我们创建的所有效果都将是单个全局粒子系统的一部分。我们将使用两种发射器类型,并利用基于形状和精灵的粒子。我们将从一个尘埃云开始,每当柱子被打破或摧毁时都会看到。然后,我们将添加一个系统,为每种柱子类型创建一个独特的弹片效果。最后,我们将创建一些火焰和烟雾效果,用于TNT爆炸,以演示移动发射器。

scr_Global_Particles();part_emitter_region(system,dustEmitter,x-16,x+16,y-16,y+16,ps_shape_ellipse,ps_distr_gaussian);part_emitter_burst(system,dustEmitter,particle_Dust,10);我们首先根据调用此脚本的实例的位置定义一个发射器的小区域。区域本身将是圆形的,具有高斯分布,使得粒子从中心喷射出来。然后我们激活发射器中的10个尘埃粒子的单次爆发。

scr_Particles_DustCloud();part_type_life(particle_Dust,15,30);part_type_direction(particle_Dust,0,360,0,0);part_type_speed(particle_Dust,1,2,0,0);part_type_size(particle_Dust,0.2,0.5,0.01,0);part_type_alpha2(particle_Dust,1,0);我们添加的第一个属性是粒子的寿命,这是在15和30步之间的范围,或者以我们房间的速度来说,是半秒到一秒。接下来,我们希望粒子向外爆炸,所以我们设置角度并添加一些速度。我们使用的两个函数都有类似的参数。第一个值是要应用于的粒子类型。接下来的两个参数是将从中随机选择一个数字的最小和最大值。第四个参数设置每步的增量值。最后,最后一个参数是一个摆动值,将在粒子的寿命中随机应用。对于尘埃云,我们设置方向为任意角度,速度相当慢,每步只有几个像素。我们还希望改变粒子的大小和透明度,使得尘埃看起来消散。

尘埃云效果有助于使柱子的破坏看起来更真实,但缺少人们期望看到的更大的材料碎片。我们希望各种形状和大小的弹片朝外爆炸,以适应不同类型的柱子。我们将从玻璃粒子开始。

globalvarparticle_Glass;particle_Glass=part_type_create();part_type_sprite(particle_Glass,spr_Particle_Glass,false,false,true);一旦我们创建了全局变量和粒子,我们就将粒子类型设置为Sprite。在分配Sprites时,除了应该使用哪些资源之外,还有一些额外的参数。第三和第四个参数是关于它是否应该是动画的,如果是,动画是否应该延伸到粒子的寿命。在我们的情况下,我们不使用动画,所以它被设置为false。最后一个参数是关于我们是否希望它选择Sprite的随机子图像,这正是我们希望它做的。

part_type_life(particle_Glass,10,20);part_type_direction(particle_Glass,0,360,0,0);part_type_speed(particle_Glass,4,6,0,0);part_type_orientation(particle_Glass,0,360,20,4,false);与尘埃云相比,这种粒子的寿命会更短,但速度会更高。这将使效果更加强烈,同时保持一般区域较小。我们还通过part_type_orientation添加了一些旋转运动。粒子可以设置为任何角度,并且每帧旋转20度,最多可变化四度。这将给我们每个粒子的旋转带来很好的变化。旋转还有一个额外的参数,即角度是否应该相对于其运动。我们将其设置为false,因为我们只希望粒子自由旋转。

part_emitter_burst(system,dustEmitter,particle_Glass,8);myParticle=particle_Glass;part_emitter_burst(system,dustEmitter,myParticle,8);制作TNT爆炸当TNT爆炸时,它会发射一些目前外观普通的TNT碎片。我们希望这些碎片在穿过场景时着火。我们还希望爆炸产生一团烟雾,以表明我们看到的爆炸实际上是着火的。这将引起一些复杂情况。为了使某物看起来着火,它需要改变颜色,比如从白色到黄色再到橙色。由于WebGL并非所有浏览器都支持,我们无法利用任何允许我们混合颜色的函数。这意味着我们需要解决这个问题。解决方案是使用多个粒子而不是一个。

orange=make_color_rgb(255,72,12);fireWhite=make_color_rgb(255,252,206);smokeBlack=make_color_rgb(24,6,0);我们已经有了一个漂亮的黄色,所以我们添加了一个橙色、一个略带黄色色调的白色,以及一个部分橙色的黑色。

globalvarparticle_FireOrange;particle_FireOrange=part_type_create();part_type_shape(particle_FireOrange,pt_shape_smoke);part_type_life(particle_FireOrange,4,6);part_type_direction(particle_FireOrange,70,110,0,0);part_type_speed(particle_FireOrange,3,5,0,0);part_type_size(particle_FireOrange,0.5,0.6,0.01,0);part_type_alpha2(particle_FireOrange,0.75,0.5);part_type_color1(particle_FireOrange,orange);part_type_gravity(particle_FireOrange,0.2,90);part_type_death(particle_FireOrange,1,particle_Smoke);我们再次使用内置的烟雾形状来设置粒子,这次寿命要短得多。总体方向仍然主要向上,尽管比烟雾更散。这些粒子稍小,呈橙色,整个寿命都将是部分透明的。我们增加了一点向上的重力,因为这个粒子介于火和烟雾之间。最后,我们使用一个函数,当每个橙色粒子死亡时会产生一个烟雾粒子。

part_emitter_region(system,myEmitter,x-5,x+5,y-5,y+5,ps_shape_ellipse,ps_distr_linear);part_emitter_destroy(system,myEmitter);这个函数将从系统中移除发射器,而不会移除已生成的任何粒子。

到目前为止,我们已经使用各种粒子和发射器构建了各种效果。这些效果为游戏增添了许多亮点,但粒子存在一个缺陷。如果玩家决定在爆炸发生后立即重新开始房间或前往商店,那么发射器将不会被摧毁。这意味着它们将继续永远产生粒子,我们将失去对这些发射器的所有引用。游戏最终会看起来像以下截图:

part_particles_clear(system);这个函数将移除系统中存在的任何粒子,但不会从内存中移除粒子类型。重要的是我们不要销毁粒子类型,因为如果其类型不再存在,我们将无法再次使用粒子。

我们从一个完整的游戏开始这一章,现在我们已经添加了一些修饰,使其真正闪耀。我们深入了解了粒子的世界,并创建了各种效果,为TNT和柱子的破坏增添了影响力。游戏现在已经完成,准备发布。

为了让人们玩游戏,我们需要将游戏放到一个网站上,最好是你自己的网站。这意味着我们需要找一个托管网站的地方,导出游戏的最终版本,当然还要利用FTP程序上传游戏。

在整本书中,我们一直在使用GameMaker:Studio内置的服务器模拟器来测试和玩我们的游戏。它允许我们查看游戏在实际网站上的表现,但只能访问我们正在开发的计算机。要将游戏上传到网站,我们需要将所有文件构建成适当的HTML5格式。

在常规子选项卡中,有四个选项部分,如下一张截图所示。查看HTML5文件选项,可以使用自定义网页文件和自定义加载栏,如果我们想要特定的布局或页面上的额外内容。创建这些文件需要了解HTML和JavaScript,并且需要支持这些语言的代码编辑器,这些都超出了本书的范围。

启动画面在游戏加载之前可见,并实际上嵌入到index.html代码中。它需要一个PNG文件,应该与游戏区域的大小相同;如果大小不同,它将被缩放以适应正确的尺寸。使用启动画面的一个缺点是,图像将被绘制而不是加载栏。由于通常认为始终让用户知道发生了什么是最佳实践,特别是在加载数据时,我们不会在这个游戏中添加启动画面。

为了进入下一步,请确保您已经获得了安全的网络服务器空间,并且可以访问FTP。

请查阅您的托管提供商的文档,了解如何配置您的FTP连接的说明。

如果所有信息都输入正确,一个目录窗口应该会打开。如下一张截图所示,有两个带有文件目录的窗格。左侧是计算机的本地驱动器,右侧是服务器目录。服务器应该打开到根目录,尽管它可能显示为在一个名为www或public_html的文件夹中。目录中可能已经有至少一个文件,index.html,这将是人们访问域名时看到的默认页面。

在同意条款和条件之前一定要阅读它们,并确保你完全理解你所要合法同意的内容。

scr_Menu_Button_FbPost();恭喜!游戏现在可以供所有人玩,并通过Facebook向世界展示。任何开发者的目标都是创建有趣的游戏,让每个人都喜欢玩并能够完成。但是你怎么知道是否发生了这种情况?人们是否在游戏中卡住了?是不是太容易?太难?在制作游戏时付出了所有的努力,不知道这些答案将是一件遗憾的事。这就是分析派上用场的地方。

为了使用FlurryAnalytics,我们需要在该服务上拥有一个账户,一个要发送数据的应用程序,并在GameMaker:Studio中激活它。一旦完成了这些步骤,就需要上传一个新的构建到网站上,然后人们就可以玩游戏了。

现在我们可以发送数据,我们只需要将其实现到现有游戏中。我们需要在几个脚本中添加一些代码,并创建一些新的脚本,以便获得有用的可跟踪信息。我们想要跟踪正在玩的级别,每个装备的使用情况,级别被玩的次数以及级别的得分。

globalvarlevelData;levelData=ds_grid_create(totalLevels,6);for(i=0;i

currentLevel=ds_grid_value_x(levelData,0,LEVEL,totalLevels-1,LEVEL,myLevel);ds_grid_add(levelData,currentLevel,ATTEMPTS,1);我们搜索levelData网格,以找出已选择的房间,以便找出我们需要更改的行。一旦我们有了行,我们就为该级别的数据添加一次尝试。

currentLevel=ds_grid_value_x(levelData,0,LEVEL,totalLevels-1,LEVEL,level[i+1,0]);ds_grid_add(levelData,currentLevel,ATTEMPTS,1);levelCompleted=ds_grid_value_x(levelData,0,LEVEL,totalLevels-1,LEVEL,room)for(i=0;i

for(a=0;a

拥有这种类型的信息非常有价值。知道玩家在哪里停止玩游戏可以告诉我们哪里可以做出改进。跟踪玩家在游戏中使用的内容让我们知道游戏是否平衡。我们能够收集的数据越多,我们就能更好地将所学到的经验应用到未来的游戏中。

赞助商是愿意支付费用将他们的品牌放在游戏上的游戏门户。品牌通常是赞助商的标志,显示在游戏开始时的闪屏上,但也可以包括诸如一个按钮,链接回他们的网站或者Facebook帖子显示的内容等。赞助的唯一缺点是目前没有许多游戏门户托管HTML5游戏,这意味着较少的潜在报价。展望未来,随着HTML5游戏的成熟和需求的增加,预计会有越来越多的门户加入。

现在你已经完成了这本书,你应该有一个非常扎实的基础来制作自己的游戏。我们从探索GameMaker:Studio界面和构建最简单的游戏开始。我们看了一下如何创建艺术和音频,以提高游戏的质量。然后,我们专注于使用GameMaker语言来编写几款游戏。我们从一个简单的横向射击游戏开始,展示了脚本编写的基础知识。然后,我们通过创建一个有多个房间和敌人路径的冒险游戏来扩展这些知识。我们学会了如何更好地构建我们的游戏,并在我们的平台Boss战中提高我们的脚本效率。然后,我们开始使用Box2D物理引擎创建一个简单的塔倒游戏,然后将其打磨成一个完整的游戏,包括完整的前端、粒子效果、Facebook集成和FlurryAnalytics。

GameMaker:Studio仍然有很多功能可以提供,新功能也在不断添加。现在轮到你利用所有这些所学知识来制作自己设计的游戏了。玩得开心,探索HTML5平台的可能性,并让你的游戏问世。祝你好运!

THE END
1.书号申请需要准备哪些材料?书稿包括版权证明:如果图书涉及版权引进、翻译作品或引用他人作品,需要提供相应的版权证明文件。这包括版权授权书、版权转让协议等,以确保不存在版权纠纷。 版权声明:如果书稿中有引用他人作品、图片等内容,需要提供版权声明,明确引用内容的来源、使用范围和授权情况等,以确保不存在侵权行为。 https://cul.sohu.com/a/839495070_541156
2.857.个人出书有哪些具体步骤?如何申请书号?全世界最大风力发电机,制造难度有多大?中国为何要修建它? 灵活胖子 讲真,是球网先动手的!!! 新媒体 论背景音乐的重要性!! 新媒体 旁边有个抢活的…怎么办?人都过去了? 新媒体 多喝热水,热水治百病! 新媒体 59跟贴 这滑的挺远的哈,棒棒哒 新媒体 1跟贴 战斗民族糗事汇,每个都是爆笑 新媒体 https://m.163.com/v/video/VCIS5H98B.html
3.三年级:美妙数学之“数字编码”(二)(1126三)美美 爷爷,什么是邮政编码呢? 邮政编码是实现邮件机器分拣的邮政通信专用代号,是实现邮政现代化的必需工具,最终目的是使您的信件在传递过程中提高速度和准确性,因此在交寄信件、包裹时务必写明邮政编码。 老爷爷 美美 邮政编码是谁发明的呢? 早在1943年,https://mp.weixin.qq.com/s?__biz=Mzg2ODEwMDIzOA==&mid=2247508101&idx=4&sn=aee67bf1371575a6f64b8836dfcb5537&chksm=ceb39164f9c41872a04eff081101a5184d942c236df4abe0a9bd7cae574387629a8e99142d67&scene=27
4.人教版三年级数学上册《数字编码》教案(通用10篇)2.试试看,给学校的每名学生编一个学号。 三年级数学上册《数字编码》教案 2 教学目标: 1、初步了解身份证号码中蕴含的一些简单信息和编码的含义,探索数字编码的简单方法,尝试用数学的方法解决实际生活中的简单问题,初步培养应用意识和实践能力。 2、通过调查、比较、猜测、交流等活动初步了解身份证编码中蕴含的一些https://www.fwsir.com/jiaoan/html/jiaoan_20200409041018_404468.html
5.急:公司图书室的图书如何编号?序号,编号,书名,ISBN编码,出版社名称,出版时间,金额,入库时间。 是否准备一本比较详细的存档,另外再多准备几分简单的放在图书室供同学们查询?查询手册需要包括以下内容: 序号,编号,书名,入库时间。 盖图书室印章: 印章内容:乡村教育促进会河南大杨学校图书室? 印章盖在图书扉页正中心,书的侧面。 (可以把鼓励的话https://wenda.so.com/q/1464992874721489
6.图书编写规范范文10篇(全文)[关键词]图书辅文内容提要调查 [中图分类号]G232[文献标识码]A[文章编号]1009-5853(2010)06-0024-04 辅文是相对于正文而言,在图书内容中起辅助说明作用或辅助参考作用的内容,多置于正文前后,如内容提要、序跋、出版说明、编者的话等。辅文在图书中扮演着重要角色,它既有保证图书完整性、强化图书内容的功能,也https://www.99xueshu.com/w/filexilsr7up.html
7.图书如何分类编号图书如何分类编号 简介 图书分类管理是一个比较大得问题,但是,只要细心加上不断的改进,管理工作肯定会做的越来越好的,在这里我提供一个比较简单的,但是,很有效的方法。工具/原料 用来记录索引记录的册子(如果身边有电脑建议用EXCEL这样查起来很方便)贴在书上的小标签、笔、书架 方法/步骤 1 需要将手中的https://jingyan.baidu.com/article/c843ea0bb7983f77931e4af5.html
8.暖通空调系统设计手册3.6 暖通空调设备未编号列表表示,图画繁杂不清 94 3.7 平面图、剖面图、系统图不一致 94 3.8 设计图纸与计算书不一致 94 四、问题原因及克服方法 94 五、施工图设计深度要求 94 5.1 设计说明、施工说明、图例和设备表 95 5.2 设备平面图 95 5.3 剖面图 95 http://www.iwuchen.com/a-88/
9.我上大学期间的日记,直到毕业分配来省城在陈家庄期间,教我们写作课的任耀云老师需要稿纸,向我“借用”。因为我带的稿纸,是供同学们使用的。虽然说借,我并没有让他归还,他也没有归还。稿纸足够用,我怎么可能向老师讨要?要是那样,就不是我的做派了。 后来到济南开门办学,也是让我把图书带着,装在我的手提柳条箱子里。到曲阜的纸坊学大寨,也用了我的https://www.meipian.cn/2bwi5f4p
10.三年级数字编码教案(通用12篇)通过观察、比较、猜测来探索用字母和数字一起进行编码的简单方法 教学具准备: 课前到图书馆进行实地调查,在图书馆借阅图书,怎样方便快捷地查找图书? 教学过程: 一、激趣引入: 同学们,课前到图书馆去调查了吗?图书馆那么多图书,怎样方便快捷地查找图书?(用字母和数字给图书编码),对了!图书编号、车子牌号都是用字https://www.yuwenmi.com/jiaoan/sannianji/552359.html
11.查询超时未还的读者编号,图书编号以及超出天数。图书管理系统的简单查询语句 单表查询: 假设读者的借阅日期为1个月,查询超时为还的读者编号,图书编号以及超出天数。 【查询语句】:selectRid,Bid,DATEDIFF(day,BorrowDate,ReturnDate)-30超出天数fromRB whereDATEDIFF(day,BorrowDate,ReturnDate)>30 6)假设读者的借阅日期为1个月,超出每天罚款0.1元,查询超时为还的https://blog.csdn.net/qq_41684261/article/details/83659981
12.Python实战案例:简单的图书管理系统腾讯云开发者社区输入对应选项的编号来执行相应的操作。 根据提示,输入图书的详细信息或选择待删除的图书编号,然后按回车键确认。 根据菜单提示进行下一步操作,直到你完成所有任务或选择退出程序。 现在,让我们一起来编写这个简单的图书管理系统。 首先,我们需要定义一个空的图书列表,我们可以使用 Python 的列表数据结构来实现: https://cloud.tencent.com/developer/article/2429145
13.软件测试工程师面试英文自我介绍(共13篇)核心提示:第一个问题:自我介绍(心理学首因效应告诉我们第一印象非常重要),自我介绍最重要的是能够在面试官心目中留下一个好的第一感觉。说得更直白一点是让面试官舒服。但是我发现很多人就是直接简单的介绍了一下过去的经历,但是实际上一方面过去的经历没有很好的让人发现优点。其实面试好比相亲,你想说什么不重要https://www.hrrsj.com/wenshu/gerenjieshao/710389.html
14.农村书屋管理规章制度范本(通用5篇)书屋的管理工作主要包括五个方面:一要做好图书接收工作,清点、核对图书、报刊及光盘的总数,建立图书接收登记册;二要做好图书分类工作,把图书分成几个大的类型,了解每类图书的数量;三要做好图书编号工作,给每本图书编号,建立图书分类登记本;四要做好图书上架工作,根据类型及编号排列好图书;五要做好图书保管工作,掌https://mip.yjbys.com/zhidu/3025945.html
15.西安决策参考西安图书馆编 ----------------- 2018年第3期总第21期 地址:西安市未央路145号邮编:710018电话:86521355-8202传真:86521358邮箱:xt_ckzx2012@163.com -------------------- http://www.xalib.org.cn/info/72409.jspx
16.一本书最重要的一页——图书版权页里的“秘密”京城散客一本书最重要的一页——图书版权页里的“秘密” 书号 印数 isbn 印张 书芯 中国 传统纸质图书会消亡吗? 前一段时间,媒体一直在炒作:传统纸质图书将走向消亡,除了新媒体力量的炒作因素外,我确实能感觉到传统图书的面临的巨大冲击。但传统纸质图书不会消亡,会消亡的是那些传统纸质图书没特色,又没有实力开发新媒体https://blog.sina.com.cn/s/blog_691038090100jpr6.html
17.关于调查方案(通用13篇)为确保事情或工作高质量高水平开展,我们需要提前开始方案制定工作,方案的内容和形式都要围绕着主题来展开,最终达到预期的效果和意义。那么方案应该怎么制定才合适呢?下面是小编为大家收集的调查方案,供大家参考借鉴,希望可以帮助到有需要的朋友。 调查方案 篇1 https://mip.ruiwen.com/gongwen/fangan/461653.html
18.济宁市人民政府教学教研济宁第一职业中等专业学校专业建设人才面向幼儿园及其他幼教机构学前教育专业领域,培养拥护党的基本路线,适应学前教育专业领域管理、教学、服务第一线需要的,德、智、体、美等方面全面发展,具有良好的教师职业道德和先进的幼儿教育理念,掌握学前教育专业必备基础理论知识和保教专业技能,具有较强的保育能力、活动设计与组织能力、反思与自我发展能力,善于沟通与合https://www.jining.gov.cn/art/2023/11/30/art_81890_2794562.html