GameMaker-HTML5-游戏开发-全-

GameMaker HTML5 游戏开发(全)

原文:zh.annas-archive.org/md5/B91F6649162E9805B55AF1CE820DC361

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

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

HTML5 Game Development with GameMaker 将向您展示如何使用实际示例制作和发布基于浏览器的游戏。本书利用 GameMaker 强大的脚本语言,让您能够在短时间内创建您的第一个游戏。通过本指南,您将开发出全面的技能和对开发逐渐复杂的游戏的工具的一致理解,逐渐增强您的编码能力,并将其提升到一个全新的水平。

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

本书内容

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

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

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

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

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

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

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

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

第九章,将您的游戏发布出去,将帮助我们使用 FTP 客户端将游戏上传到 Web 服务器。我们将把 Facebook 整合到游戏中,允许玩家登录他们的账户,并将级别得分发布到他们的墙上。它还涵盖了使用 Flurry 进行分析,以跟踪玩家如何玩游戏。最后,我们将简要了解通过赞助赚钱的方法。

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

您可以从www.packtpub.com/sites/default/files/downloads/4100OT_Appendix_Drag_and_drop_Icons_to_GameMaker_Language_Reference.pdf下载本附录。

您需要为本书准备什么

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

这本书适合谁

这本书适合任何热衷于使用 GameMaker: Studio 创建有趣和充满动作的网页游戏的人。这本直观的实用指南既适合初学者,也适合想要使用强大的 GameMaker 工具创建和发布在线游戏与世界分享的高级用户。

约定

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

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

代码块设置如下:

mySpeed = 4;
myDirection = 0;
isAttacking = false;
isWalking = false;
health = 100;
image_speed = 0.5;

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

isWalking = false;
if (keyboard_check(vk_right) && place_free(x + mySpeed, y))
{
 x += mySpeed;
    myDirection = 0;
    sprite_index = spr_Player_WalkRight;
    isWalking = true;

新术语重要单词以粗体显示。例如,屏幕上看到的单词,菜单或对话框中的单词会出现在文本中,如:“点击下一个按钮会将您移至下一个屏幕”。

注意

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

提示

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

第一章:通过您的第一个游戏了解 Studio

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

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

使 HTML 游戏开发变得简单

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

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

注意

并非所有浏览器都是相同的!虽然 HTML5 标准由 W3C 制定,但每个供应商的实现方式都不同。此外,目前还没有制定所有标准,这意味着某些功能可能在某些浏览器中无法正常工作。例如,有多个音频 API 竞相成为标准。随着标准的确定和浏览器的更加兼容,这些问题应该会消失。要查看您喜欢的浏览器对 HTML5 的支持程度,可以访问html5test.com

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

设置软件

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

注意

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

  1. www.yoyogames.com/buy/studio/professional购买并下载 GameMaker: Studio 专业版和 HTML5 导出模块。

  2. 下载完成后,运行程序GMStudio-Installer.exe

  3. 按照屏幕上的说明操作,然后启动程序。

  4. 输入您的许可密钥。这将解锁已购买的软件和模块。设置软件

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

  1. 新项目窗口中,选择选项卡。它应该看起来像前面的屏幕截图。

  2. GameMaker: Studio 通过为每个资源创建文件夹以及项目文件来管理项目。为此,您需要指定游戏文件存储的目录。将项目名称字段设置为Chapter_01,然后单击创建

我们第一次看到 Studio

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

我们第一次看到 Studio

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

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

菜单

菜单是您将找到每个编辑器和工具所需的地方。有一些非常有用的工具,比如在脚本中搜索定义常量,这些只能在这里找到。为什么不花点时间看看每个菜单选项,以便了解您可以使用的所有内容呢。我们会等一下。

工具栏

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

注意

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

资源树

资源树显示和组织了为游戏创建的所有资产。保持项目有条不紊不会影响软件的性能,但会节省我们的时间,并在长期内减少挫折感。

工作区

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

注意

如果您想了解更多关于源控制的信息,请查看以下 GameMaker: Studio 维基页面:wiki.yoyogames.com/index.php/Source_Control_and_GameMaker:Studio

探索资源编辑器

为了在 GameMaker: Studio 中创建游戏,您需要至少三种类型的资源资产:精灵(您所看到的)、对象(它的功能)和房间(发生的地方)。除此之外,您还可以拥有脚本、背景、声音、字体、路径时间轴

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

使用精灵属性编辑器加载您的艺术资产

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

使用精灵属性编辑器加载您的艺术资产

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

墙精灵

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

  1. 通过导航到资源 | 创建精灵来创建一个新精灵。这将在资源树中创建一个精灵,并打开精灵属性编辑器。

  2. 将精灵命名为spr_Wall

  3. 单击加载精灵以打开精灵图像。在窗口的一侧有一个图像信息部分,我们可以在那里预览所选图像并选择激活几个选项。使不透明将从所选精灵中删除所有透明度。删除背景将删除图像左下角像素中找到的颜色的所有像素。平滑边缘将平滑图像的透明边缘,在导入动画 GIF 文件时非常有用,可以去除硬边缘。

  4. 在没有选中任何选项的情况下,打开Chapter 1/Sprites/Wall.png,然后单击确定

  5. 如下截图所示,它的宽度和高度为 32 像素,有一个子图像。没有其他需要改变的地方,所以点击确定墙壁精灵

玩家精灵

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

  1. 创建一个新的精灵。

  2. 将精灵命名为spr_Player

  3. 点击加载精灵,选择Chapter 1/Sprites/Player.gif。勾选去除背景和平滑边缘。点击确定

  4. 再次,它的宽度和高度为 32 像素,但是有两个子图像,如下一截图所示。这意味着它有动画!让我们点击显示旁边的箭头来看看每一帧的样子。加载动画图像时这样做是有用的,以确保所有帧都按适当的顺序排列并且对齐正确。玩家精灵

  5. 原点中将X设置为16Y设置为16,或者你可以直接点击中心按钮。

  6. 点击确定按钮。

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

使用对象属性编辑器创建游戏对象

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

使用对象属性编辑器创建游戏对象

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

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

使用对象属性编辑器创建游戏对象

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

墙对象

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

  1. 通过导航到资源 | 创建对象来创建一个新对象。这将在资源树中创建一个新对象,并打开对象属性编辑器。

  2. 将此对象命名为obj_Wall

  3. 点击精灵中的输入框,选择spr_Wall

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

  1. 点击实体复选框,然后点击确定

注意

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

玩家对象

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

  1. 创建一个新对象并命名为obj_Player

  2. 选择spr_Player作为精灵。

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

玩家对象

听起来相当简单,不是吗?但是当涉及到事件顺序时可能会有点混乱。GameMaker 将游戏分解为步骤(有限的时间段),每秒运行多次事件。一些事件按照预设顺序发生,比如开始步骤,它总是从步骤的最开始开始。其他事件在被调用时发生,比如创建,它会立即在对象的实例创建时运行,以检查该代码是在步骤的开始还是结束时发生。

注意

访问wiki.yoyogames.com/index.php/Order_of_events了解更多关于 GameMaker: Studio 事件顺序的信息。

  1. 事件:区域,单击添加事件,然后导航到键盘 | 。这个事件将在按住左箭头键的每一步中运行代码。

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

  1. 移动选项卡中,选择并将移动固定图标拖放到左键事件的动作区域。玩家对象

移动固定图标

  1. 移动固定选项框中,有一个选项,用于指定要应用此动作的对象。我们希望将其设置为自身,以便将其应用于玩家的实例。玩家对象

  2. 单击左箭头以指示我们希望移动的方向。

  3. 速度字段设置为8。这将每步应用 8 像素的速度。

  4. 确保相对未被选中。相对会将该值添加到当前值。

  5. 单击确定

  6. 对于其他键盘箭头(右,上,下),重复步骤 4 到 9,使用相同的速度和适当的方向。

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

  1. 事件:区域,单击添加事件,然后导航到键盘 | 无键。这是一个特殊的键盘事件,只有在没有按键时才会发生。

  2. 选择并将移动固定图标拖放到动作区域。

  3. 将方向设置为中心,将速度字段设置为0

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

  1. 单击添加事件,然后导航到碰撞 | obj_Wall

  2. 将移动固定图标拖放到动作:区域。玩家对象

  3. 将方向设置为中心,速度字段设置为0。单击确定

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

使用房间属性编辑器创建世界

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

  • 前端菜单屏幕

  • 非交互式场景

  • 您需要的任何自包含环境!使用房间属性编辑器创建世界

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

  1. 通过导航到资源 | 创建房间来创建一个新房间。这将在资源树中创建一个新房间,并打开房间属性编辑器。

  2. 为了使放置变得更容易,将Snap XSnap Y字段设置为32。这将创建一个每 32 像素一个捕捉点的放置网格。

  3. 选择设置选项卡。在这里,我们可以更改基本的房间属性,大小,每秒步数和房间的名称。

  4. 将房间命名为rm_GameArea

  5. 我们将保留房间宽度高度速度字段的默认值,如下面的截图所示:使用房间属性编辑器创建世界

  6. 选择对象选项卡,在用鼠标左键添加的对象下,选择obj_Wall

  7. 在房间的左上角,用鼠标左键单击放置一个墙的实例。

现在你可能会认为这将需要非常长的时间来逐个点击构建房间。别担心,有一个更简单的方法。如果你按住Shift + Ctrl,你就可以用实例来绘制世界。如果你犯了一个错误并想要删除一个实例,只需右键单击以删除一个实例,或者按住Shift键来取消绘制实例。如果你只想移动实例一点点,而不是整个网格单元,按住Alt键。

  1. 按住Shift + Ctrl键和鼠标左键,绘制周边墙壁。还要放下两个凸出的部分,如下面的示例截图所示:使用房间属性编辑器创建世界

不要忘记添加玩家!

  1. 对象选项卡中,选择obj_Player

  2. 在房间的右下角放置一个obj_Player的单个实例。

  3. 通过单击房间属性编辑器左上角的复选标记来关闭房间。

  4. 在这一点上,我们已经拥有了在 GameMaker: Studio 中运行游戏所需的所有必要元素。在我们测试游戏之前,我们应该通过导航到文件 | 保存来保存我们的工作。

运行游戏

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

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

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

使用脚本属性编辑器引入代码

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

使用脚本属性编辑器介绍代码

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

  1. 通过导航到资源 | 创建脚本来创建一个新脚本。这将在资源树中创建一个新脚本,并打开脚本属性编辑器。

  2. 将其命名为scr_Player_Create。在本书中,我们将大部分脚本命名为事件名称的结尾。在这种情况下,我们将把这段代码放入一个创建事件中。

  3. 要停止精灵动画,我们只需要将精灵的播放速度设置为零。在第1行,输入以下内容:

image_speed = 0;
  1. 通过点击编辑器左上角的复选标记来关闭脚本。

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

  1. 重新打开obj_Player对象属性编辑器。

  2. 添加一个创建事件。

  3. 导航到操作 | 控制,并选择并拖动执行脚本图标到操作:区域。使用脚本属性编辑器介绍代码

执行脚本图标

  1. 选择scr_Player_Create作为要执行的脚本,然后点击确定

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

使用背景属性编辑器填充场景

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

注意

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

  • 背景中的一个大图像

  • 背景移动

注意

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

  • 只需少量的艺术资源就可以创建大型世界

  • 为背景添加独特的细节

使用背景属性编辑器填充场景

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

  1. 通过导航到资源 | 创建背景来创建一个新背景。这将在资源树中创建一个新背景,并打开背景属性编辑器。

  2. 将其命名为bg_Ground

  3. 点击加载背景,打开Chapter 1/Backgrounds/Ground.png

  4. 然后点击确定

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

  1. 重新打开rm_GameArea

  2. 点击背景选项卡。使用背景属性编辑器填充场景

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

  1. 选择背景 0,然后勾选游戏开始时可见的复选框。这必须激活才能在游戏过程中看到背景。

  2. 选择bg_Ground作为要显示的背景。

  3. 其他所有内容都可以保持默认。水平平铺垂直平铺应该被选中,所有其他值应该设置为0

  4. 通过点击编辑器左上角的复选标记来关闭房间。

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

用声音属性编辑器带来噪音

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

  • 正常声音

  • 背景音乐

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

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

用声音属性编辑器带来噪音

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

一点背景音乐

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

  1. 通过导航到资源 | 创建声音来创建一个新声音。这将在资源树中创建一个新声音,并打开声音属性编辑器。

  2. 将其命名为snd_bgMusic

  3. 加载Chapter 1/Sounds/bgMusic.mp3文件。如果你想听音乐,只需点击播放按钮。听完后,点击停止按钮。

  4. 种类下选择背景音乐作为类型,然后点击确定

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

用霸主控制游戏

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

  1. 创建一个新对象,命名为obj_Overlord

  2. 添加一个事件,然后导航到其他 | 游戏开始。这是一个特殊的函数,只有在游戏开始时才会运行。

  3. 导航到操作 | 主 1,并选择并拖动播放声音图标到操作:区域。用霸主控制游戏

播放声音图标

  1. 声音:字段设置为snd_bgMusic,将循环:设置为true,然后点击确定用霸主控制游戏

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

用霸主控制游戏

  1. 重新打开rm_GameArea

  2. 对象选项卡中选择obj_Overlord,并将一个实例放在房间里。

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

可收集的物品

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

  1. 创建一个新声音,命名为snd_Collect

  2. 加载Chapter 1/Sounds/Collect.wav文件,并将其设置为正常声音,然后点击确定

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

  1. 创建一个新精灵,命名为spr_Collect

  2. 选择删除背景平滑边缘,加载文件Chapter 1/Sprites/Collect.png并将其中心设置为原点。

  3. 创建一个新对象,命名为obj_Collect

  4. spr_Collect分配为其精灵

  5. 添加一个与obj_Player碰撞事件

  6. 导航到操作 | Main1,并将播放声音图标拖放到操作:区域。

  7. 声音:字段设置为snd_Collect,并将循环:设置为false

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

  1. 导航到操作 | 分数,并将设置分数图标拖放到操作:区域。可收集物

设置分数图标

  1. 如下截图所示,将新的分数:字段设置为50,勾选相对框,然后点击确定。这将在每次收集对象时为我们的分数增加 50 分。相对使得分数增加到先前的分数。可收集物

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

  1. 导航到操作 | Main1,并将销毁实例图标拖放到操作:区域。此操作将从世界中移除实例。保持值不变,然后点击确定可收集物

销毁实例图标

  1. 我们已经完成了这个对象,如果构建正确,它应该看起来像下面的截图。点击确定可收集物

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

编写文本和字体属性编辑器

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

编写文本和字体属性编辑器

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

  1. 通过导航到资源 | 创建字体,创建一个新的字体。这将在资源树中创建一个新的字体,并打开字体属性编辑器。

  2. 将其命名为fnt_Impact

  3. 字体下拉菜单中选择Impact。这是一个默认的 Windows 字体。

  4. 大小设置为16。然后点击确定

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

  1. 重新打开obj_Overlord

  2. 通过导航到绘制 | 绘制 GUI,添加一个绘制 GUI 事件。

注意

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

  1. 导航到操作 | 绘制,并将设置颜色图标拖放到操作:区域。这将打开一个对话框,您可以在其中设置颜色。编写文本和字体属性编辑器

设置颜色图标

  1. 我们想要将颜色设置为青色。在弹出的颜色调色板中,选择左起第五列底部的青色。点击确定编写文本和字体属性编辑器

  2. 导航到动作 | 绘制,并将设置字体图标拖放到动作:区域。这将打开一个带有两个参数的对话框:要使用的字体以及它应该如何对齐。编写文本和字体属性编辑器

设置字体图标

  1. 字体:字段设置为fnt_Impact并将其对齐到中心。点击确定编写文本和字体属性编辑器

  2. 最后,导航到动作 | 得分,并将绘制得分图标拖放到动作:区域。这将打开一个带有三个参数的对话框:x 和 y 坐标,以及一个可选的标题,可以放在实际得分前面。

  3. x:字段设置为320y:字段可以保持为0,并从标题:字段中删除得分:,使其为空,如下面的屏幕截图所示。点击确定编写文本和字体属性编辑器

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

使用路径属性编辑器创建复杂移动

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

使用路径属性编辑器创建复杂移动

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

  1. 通过导航到资源 | 创建路径来创建一个新路径。这将在资源树中创建一个新路径,并打开路径属性编辑器。

  2. 将其命名为pth_Enemy

  3. 在编辑器工具栏的末尾,我们可以设置显示哪个房间。这对于按房间基础创建准确路径非常有用。将其设置为rm_GameArea使用路径属性编辑器创建复杂移动

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

  1. 将第一个点放在地图的6464处。如果出现错误,您可以随时将点拖动到正确的位置,或者您可以手动设置 X 和 Y 值。

  2. 在这条路径上,我们将添加另外五个点,如参考图像所示。

  3. 我们将保留所有其他设置为默认值,所以点击确定

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

  1. 创建一个新精灵并命名为spr_Enemy

  2. 选择删除背景平滑边缘,加载Chapter 1/Sprites/Enemy.png并将原点居中。

  3. 创建一个新对象并命名为obj_Enemy

  4. 添加一个创建事件,导航到动作 | 移动,并将设置路径图标拖放到动作:区域。这将打开设置路径选项对话框。使用路径属性编辑器创建复杂移动

设置路径图标

使用路径属性编辑器创建复杂移动

  1. 路径:设置为pth_Enemy

  2. 速度:字段设置为4

  3. 下一个选项确定实例到达路径末端时应该发生什么。有选项可以停止从头继续(对于开放路径),从这里继续(对于闭合路径),和反向方向。将在末端:设置为从这里继续

  4. 这里的相对:选项确定路径是从实例开始(相对)还是实例从路径的第一个点开始(绝对)。由于我们建立它来适应房间,所以将相对:设置为绝对。然后点击确定

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

  1. 添加一个与obj_Player碰撞事件,导航到动作 | 主要 2,并将重新开始游戏图标拖放到动作:区域。使用路径属性编辑器创建复杂的移动

重新开始游戏图标

  1. 敌人现在已经完成,所以点击确定关闭它。

  2. 在房间中的任何位置放置一个敌人的单个实例。确切的位置并不重要,因为游戏运行时它会重新定位到正确的位置。

  3. 保存游戏并运行。我们应该看到敌人沿着房间周围的路径移动。如果玩家对象与其发生碰撞,游戏将重新开始。

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

使用时间轴属性编辑器生成可收集物品

时间轴是一个高级的时间跟踪系统,允许对游戏过程中发生的事情进行有限控制。时间轴由一系列时刻组成。每个时刻代表时间轴开始后的步数。

使用时间轴属性编辑器生成可收集物品

时间轴几乎可以用于任何事情,其中最常见的用途之一是生成实例。在这个游戏中,我们将使用它来生成我们的可收集物品,以便玩家有东西可以追逐。

  1. 通过导航到资源 | 创建时间轴来创建一个新的时间轴。这将在资源树中创建一个新的时间轴,并打开时间轴属性编辑器。

  2. 将其命名为tm_Spawn_Collectibles

  3. 点击添加按钮,并将步数设置为60

  4. 我们将通过给它们施加速度来使这些可收集物品移动。导航到动作 | 主要 1,并将创建移动图标拖放到动作:区域。使用时间轴属性编辑器生成可收集物品

创建移动图标

  1. 将对象设置为obj_Collect

  2. 我们希望生成发生在屏幕外,这样玩家就看不到它突然出现。我们将使这个可收集物品水平移动,所以我们将从游戏区域的左侧开始。将x:字段设置为-64

  3. 我们不希望可收集物品总是在完全相同的位置生成,所以我们要为其添加一个随机元素。我们将在屏幕顶部和底部之间的随机垂直位置创建实例。将y:字段设置为random(394) + 48

  4. 给它一个速度4,并将方向:字段设置为0。它应该看起来像下面的截图。点击确定使用时间轴属性编辑器生成可收集物品

  5. 120处添加另一个时刻,并重复上述步骤,不过这次是垂直的。为此,x:字段应设置为random(546) + 48y:字段应为-64速度:字段应为4方向:字段应为270

我们现在有一个时间轴,每两秒钟就会生成一个新的可移动的可收集物品。但是,我们需要将其附加到一个对象上,所以让我们将其应用到obj_Overlord上。

  1. 重新打开obj_Overlord

  2. 在已经存在的游戏开始事件中,通过导航到动作 | 主要 2,将设置时间轴图标拖放到动作:区域。使用时间轴属性编辑器生成可收集物品

设置时间轴图标

  1. 时间轴:字段设置为tm_Spawn_Collectibles

  2. 位置:保留为0;这将从开始位置开始。

  3. 开始:设置为立即开始

  4. 我们希望它无限重复,所以将循环:设置为Loop使用时间轴属性编辑器生成可收集物品

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

使用时间轴属性编辑器生成可收集物品

调试游戏的工具

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

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

使用 HTML5 调试控制台

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

  1. 我们将从在玩家创建时添加传统的Hello World调试消息开始。重新打开scr_Player_Create并在脚本的末尾添加以下代码:
myText = "Hello World";
show_debug_message(myText);

提示

下载示例代码

您可以从您在www.packtpub.com购买的所有 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册以直接通过电子邮件接收文件。

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

  1. 由于此脚本已经附加到我们的玩家创建事件,我们可以直接运行游戏。单击运行调试模式图标。

  2. 当游戏在浏览器中启动时,将会弹出一个带有调试控制台的第二个窗口,如下一张截图所示。如果您没有看到此窗口,请检查浏览器是否允许弹出窗口。滚动到调试输出列的底部。在这里,您应该看到调试消息Hello World。这表明我们的代码已成功执行。如果我们没有如预期看到它,那么我们就会知道游戏出了问题的地方。使用 HTML5 调试控制台

  3. 我们还可以看到游戏中每个实例的所有属性,包括它们的实例编号,当前在房间中的位置,它正在显示的精灵等。单击实例列中的任何一个数字,然后查看实例数据列中的属性。

  4. 点击暂停/恢复按钮。这样我们就可以暂停游戏,如果你有很多调试消息涌入控制台,并且想要花时间看看发生了什么,这是很有用的。

  5. 最后,我们可以点击清除控制台按钮,从调试输出列中删除所有内容。

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

使用 Windows 版本调试器

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

  1. 目标更改为Windows,并以调试模式运行游戏。游戏打开时,GameMaker 调试器将显示在一个单独的窗口中,如下图所示:使用 Windows 版本调试器

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

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

  1. 导航到工具 | 显示实例。这将打开一个窗口,显示游戏中的所有实例。

  2. 滚动列表,直到找到obj_Player。双击它,这样我们就可以看到它的所有属性。就像 HTML5 调试控制台一样,我们可以看到它在世界上的位置以及它有哪个精灵(通过精灵索引号)。然而,如果你滚动列表,还有许多其他属性。事实上,如果我们看列表底部,我们可以看到myText变量。太棒了!

查看 JavaScript 代码

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

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

  1. 目标平台设置为HTML5,以调试模式运行游戏。

  2. 在游戏下方的浏览器窗口中,右键单击并选择检查元素。这将为 Chrome 打开开发者工具

  3. 选择源代码选项卡,在左上角点击名为显示导航器的小图标。

  4. 在导航器中有一个目录树。打开文件夹,直到找到html5文件夹。在这个文件夹里是游戏。点击游戏,我们应该看到所有的代码,就像下一个屏幕截图中所看到的那样。如果我们浏览代码,我们可以清楚地看到我们创建的脚本,对象的属性等。查看 JavaScript 代码

  5. 现在让我们看看混淆版本。关闭浏览器标签,然后以正常模式运行游戏。重复相同的过程并查看代码。它应该看起来像下一个屏幕截图。我们仍然可以读取一些片段,但其中没有任何意义。您可以相当确信,很少有人会想要干预这个。查看 JavaScript 代码

摘要

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

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

第二章:AAA 游戏:艺术和音频

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

制造艺术资源

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

有许多受欢迎的软件选项供您考虑。最全面的选择和最昂贵的选择是 Adobe Photoshop,这是大多数专业艺术家的首选,可以在www.photoshop.com/购买。一个具有许多类似功能的免费替代品是 GIMP,可在www.gimp.org/下载。这两个软件包都提供了一套高级工具,用于创建图像。还有许多其他更简单的工具可供选择,例如 Pickle www.pickleeditor.com/,Spriter www.brashmonkey.com/和 PyxelEdit pyxeledit.com/,所有这些工具都是免费的,值得一试。

如果您只想跳过艺术创作,而更喜欢一些预制的作品,有很多地方可以下载精灵。最受欢迎的精灵网站之一是 Spriters Resource spriters-resource.com/。他们拥有您能想象到的各种类型游戏的资源。您还可以查看 GameMaker 论坛gmc.yoyogames.com/。在这里,您会找到许多愿意制作或分享他们的艺术资源的活跃人士。

了解图像文件格式

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 中剪切出每一帧动画,以构建我们需要的单个精灵。让我们试一试!

  1. 让我们从打开一个名为Chapter_02新项目开始。

  2. 创建一个新的精灵,并命名为spr_PlayerSpriteSheet

  3. 点击编辑精灵按钮打开精灵编辑器

  4. 文件下,选择从条带创建,然后在图像信息部分中不选择任何内容,打开Chapter 2/Sprites/PlayerSpriteSheet.png。这将打开加载条带图像编辑器。

  5. 我们刚刚加载的精灵表包含了一个六帧的奔跑循环。由于我们需要所有的帧,所以我们需要将图像数量设置为6

  6. 精灵表的布局有两行三个图像。将每行图像数设置为3

  7. 由于每个图像的大小为 64 x 64 像素,我们需要将图像宽度图像高度设置为64。对于如此小的精灵表来说,偏移和分离的其他选项并不是必要的,但如果我们有这个角色的完整动画集,它们将会很有用。设置应该如下图所示:导入精灵表

  8. 点击确定。我们现在有一个带有平滑边缘的动画精灵!

  9. 我们已经完成了这个精灵。现在点击精灵编辑器精灵属性编辑器的复选标记,然后点击确定按钮关闭它。

介绍图像编辑器

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

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

介绍图像编辑器

使用图块集创建背景

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

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

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

使用图块集创建背景

  1. 创建一个新的背景资源,并命名为bg_StoneFloor

  2. 由于我们希望这是一个图块集,请确保勾选用作图块集的复选框。这将显示图块属性,允许您设置图块的宽度和高度、偏移和间隔。

  3. 图块宽度图块高度设置为32,如前面的图像所示。我们现在准备开始构建图块。

  4. 点击编辑背景按钮。这将打开图像编辑器

  5. 我们将从创建所有其他图块将基于的主图块开始。在图像编辑器中,选择文件 | 新建,并将宽度高度设置为32使用图块集创建背景

  6. 选择填充区域工具,并将浅灰色应用到整个精灵上。这是基础,我们稍后会更改颜色。

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

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

使用图块集创建背景

  1. 选择在图像上绘制工具和深灰色。

  2. 为了让生活变得更容易,我们可以放大图像。这可以通过放大镜或中间鼠标滚动按钮来完成。使用图块集创建背景

  3. 绘制小石头的轮廓,但记得保持一定的大小和形状的变化。另外,不要忘记将对角线线条保持在一个像素的宽度上!一旦你做到了这一点,它应该看起来类似于前面的示例截图。

  4. 图像编辑器菜单中,选择转换 | 移动。这将打开移动图像对话框,允许您水平或垂直移动像素。使用图块集创建背景

  5. 水平垂直值设置为16,并勾选水平包裹垂直包裹框。这将使图像向下和向右移动 16 像素(瓷砖大小的一半),并将剩余的像素包裹起来,如前面的屏幕截图所示。

  6. 点击确定

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

使用瓷砖集创建背景

  1. 在必要的地方画线并覆盖旧线,以修复任何看起来不正确的石头。

  2. 使用相同的设置重新应用变换 | 移动工具。如果看到错误,修复它们并重复,直到你满意。使用瓷砖集创建背景

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

  1. 选择浅褐色,并使用填充区域工具填充一些石头。

  2. 使用其他褐色变种重复这个过程,直到没有灰色的石头剩下。使用瓷砖集创建背景

  3. 填满所有的石头后,我们需要确保它仍然可以铺砌。使用变换 | 移动 来查看颜色是否正确对齐。如果有任何问题(如前面的截图所示),只需调整颜色,直到你再次满意。

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

  1. 用鼠标左键选择深褐色。这种颜色应该出现在颜色 | 下方。使用瓷砖集创建背景

  2. 选择更改所有相同颜色的像素工具,如前所示,然后在一个深灰色像素上单击左键。现在石头的轮廓应该都是深褐色,就像我们将在下面的截图中看到的那样:使用瓷砖集创建背景

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

  1. 选择变换 | 调整画布

  2. 新尺寸 | 宽度新尺寸 | 高度增加300%或96像素。然后在位置下点击中心方块,使画布在我们创建的瓷砖周围扩展。设置如下屏幕截图所示。使用瓷砖集创建背景

  3. 你需要确保一切都正确对齐,所以打开网格。选择视图 | 切换网格 或点击切换网格图标。

  4. 此时您可能看不到任何网格。这是因为默认网格设置为 1 x 1 像素。选择视图 | 网格选项打开网格设置。将水平大小垂直大小更改为32,并选中对齐到网格。如果需要,可以随意更改颜色,就像我们在之前的屏幕截图中所做的那样。然后点击确定使用瓷砖集创建背景

  5. 使用选择区域工具,拖动以选择整个基础瓷砖。

  6. 复制Ctrl + C)和粘贴Ctrl + V)瓷砖,然后将其拖放到一个可用的空间中。重复此步骤,直到所有九个位置都有一个基础瓷砖,就像以下的屏幕截图中所示:使用瓷砖集创建背景

  7. 返回到视图 | 网格选项,关闭对齐到网格。否则,您将在尝试绘制边框时感到非常沮丧!使用瓷砖集创建背景

  8. 我们希望边框厚度为八个像素。使用与石头相同的颜色,使用绘制线条工具在瓷砖集的外围创建一个边框,就像之前看到的那样。

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

  1. 如果您还没有一个,创建一个新房间。

  2. 房间属性编辑器中,选择瓷砖选项卡。使用瓷砖集创建背景

  3. 如果尚未选择,请将背景图像设置为bg_StoneFloor

  4. 要选择一个瓷砖,只需在预览区域左键单击要使用的瓷砖,如前面的屏幕截图所示。

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

  1. 我们将保持简单,所以让我们将当前瓷砖图层保留在1000000

  2. 在房间中,使用左键单击放置单个瓷砖,或按住Shift在房间中绘制瓷砖。尝试布置瓷砖,就好像有多个带走廊的房间,就像以下屏幕截图一样。使用瓷砖集创建背景

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

  1. 如果尚未打开,请重新打开bg_StoneFloor并选择变换 | 调整画布大小

  2. 将大小增加133%,或者到128像素。在位置下点击左上角箭头,然后点击确定。现在它应该看起来像以下的屏幕截图。使用瓷砖集创建背景

  3. 选择视图 | 切换网格,这样我们就可以看到网格。我们需要复制原始的基础瓷砖,可以在第二行和第二列找到。

  4. 使用选择区域工具,选择原始基础瓷砖的像素。

  5. 将此瓷砖复制并粘贴到图像外边缘的每个空单元格中。

  6. 我们需要创建四个角瓷砖来修复我们的房间布局。为此,我们将使用刚刚放置的右侧边缘的四个瓷砖。使用在图像上绘制工具绘制角落的修饰,并对所有四个角瓷砖重复此操作。

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

  1. 更改内部每个剩余瓷砖的一些石头的形状并交替颜色。如下截图所示,平铺集现在完成了!使用平铺集创建背景

  2. 最后,回到房间,根据需要放置角落瓷砖,并铺设备选瓷砖的随机变化。使用平铺集创建背景

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

动画和创建精灵

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

行动的错觉

创建动画需要时间和敏锐的眼光,但基本的动画,甚至是角色的动画,每个人都可以做到。有一些重要的规则可以让动画变得更容易。首先,它关乎动作的外观,而不是动作的准确性。如下截图所示,第一个挥剑动画在技术上是准确的;剑会在每个位置。然而,第二个集会看起来更自然,因为它包括了人们在挥剑时所期望看到的模糊效果。

行动的错觉

最大化精灵空间

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

最大化精灵空间

循环动画

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

循环动画

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

  1. 创建一个新的精灵,并命名为spr_WalkCycle

  2. 点击编辑精灵;这将打开精灵编辑器。这个编辑器用于处理组成动画精灵的所有单个图像。

  3. 精灵编辑器中,选择文件 | 新建,这将打开一个新图像尺寸的对话框。保持为32 x 32,然后点击确定循环动画

  4. 现在你应该看到,就像之前的截图一样,在精灵编辑器中有一个名为图像 0的空图像。双击图像打开图像编辑器

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

注意

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

  1. 我们将创建的第一帧动画应该是角色在行走循环的最大伸展,腿之间距离很远,触及精灵的底部。角色在这一帧上将处于迈步的最低点,所以确保头部距离精灵顶部至少一个像素,最好是两个像素。循环动画

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

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

循环动画

  1. 精灵编辑器中,选择动画 | 设置长度,将帧数设置为3,如前面的截图所示。这将复制第一帧两次,给我们增加两帧动画。

  2. 打开图像 1并使用选择区域工具将身体的上半部分提高到精灵的顶部,如下截图所示。这一帧将代表迈步的最高点,角色站在一只脚上,另一只脚越过。我们还可以选择并移动手和脚,快速将它们放到正确的位置。循环动画

  3. 使用铅笔和橡皮擦工具,将手臂和腿画到适当的位置,前腿着地,后腿抬起,只有一只手臂显示。一旦你对外观满意,关闭图像。

  4. 打开图像 2。这是第一帧的相反运动,这样改变起来相当容易。手和脚已经在正确的位置,所以我们只需要相应地重新绘制手臂和腿,如左侧截图所示。完成后关闭图像。循环动画

  5. 现在我们需要复制图像 1并将其放在末尾,以便行走循环。选择图像 1并复制并粘贴帧。这将复制帧,并标记为图像 2

  6. 选择图像 2并点击精灵编辑器工具栏中的右箭头。这将把帧移到动画的末尾。选择并打开图像 3,这样我们就可以重新绘制腿,使后腿着地,前腿在空中越过。完成后关闭编辑器。

  7. 要查看动画的播放情况,请在Sprite Editor中选中Show Preview复选框,并将Speed设置为5。请参阅以下截图。循环动画

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

循环动画

制作音频

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

为了创建音频,我们需要使用外部软件,因为 GameMaker: Studio 没有内置的音频创建工具。有各种软件选择可供选择。用于创建音效和音乐的流行程序包括非常全面的Reasonwww.propellerheads.se/,它模拟了一台合成器、混音台和其他组件的机架。在免费方面,还有BFXRwww.bfxr.net/,可以让您在线创建游戏音效,还有Sonantsonantlive.bitsnbites.eu/,用于制作音乐。所有这些软件包都很有趣且易于使用。需要记住的一点是,音频的创建非常具有挑战性。有时最好只是下载一些免费音乐或音效,有很多网站提供免费和可购买的音频。Freesoundhttp://www.freesound.org,有成千上万的音频剪辑可供下载和使用。对于更经典的 8 位风格音乐和音效,还有8-bit Collectivehttp://8bc.org/,这是一个专门用于游戏音频的网站。

了解音频文件格式

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

使用 GM:S 音频引擎

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

GM:S 音频引擎是新的、更强大的声音系统,旨在通过发射器和听者实现完整的 3D 声音景观。发射器允许在游戏空间中定位声音发生的位置。有添加声音衰减、模拟移动的速度等功能。听者通过根据玩家在游戏中的位置、包括他们的方向和速度来播放声音,提供更多的控制。如果您不声明一个听者,那么声音将变得普遍。这将最终成为 GameMaker: Studio 中的主要音频引擎,但由于 HTML5 音频问题,它在所有浏览器中都无法正常工作。

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

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

使用 GM:S 音频引擎

  1. 要选择使用哪个系统,请单击资源 | 更改全局游戏设置。在常规选项卡中,有一个使用新音频引擎复选框;确保您选中它。如果选中,它将使用 GM:S 音频引擎;如果没有,则使用 Legacy Sound。

  2. 创建一个新声音并命名为snd_Effect

  3. 加载Chapter 2/Sounds/Effect.wav。确保类型设置为普通声音

  4. 创建一个新对象并命名为obj_Sound

  5. 创建一个新脚本并命名为scr_Sound_Create。首先,我们需要创建一个发射器并将其捕获在一个变量中:

sem = audio_emitter_create();
  1. 接下来,我们将发射器定位到我们对象的位置。此函数的参数是:要应用此函数的发射器和 X/Y/Z 坐标。我们将使用对象的 X 和 Y,但由于这是一个 2D 示例,我们将 Z 设置为 0:
audio_emitter_position(sem, x, y, 0);
  1. 我们还希望在发射器上有一个衰减,使得随着听者的接近声音变得更大。我们拥有的参数是:发射器、声音在一半音量时的距离、总的衰减距离和衰减因子:
audio_emitter_falloff(sem, 96, 320, 1);
  1. 发射器已经设置好了;现在让我们在发射器上播放声音。此函数的参数是:发射器、要播放的声音、是否应该循环以及其优先级。我们希望这个循环,这样我们就可以听到声音:
audio_play_sound_on(sem, snd_Effect, true, 1);
  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);
  1. 添加一个创建事件,并将一个控制 | 执行脚本图标拖放到附有此脚本的操作中。

  2. 现在声音将播放,但在我们有听者之前,它将没有方向。我们将根据鼠标的位置在每一步上移动听者的位置。创建一个新脚本并命名为scr_Sound_Step

  3. 我们只需要一行代码来定位听者的 X/Y/Z 坐标。X 和 Y 将设置为鼠标的 X 和 Y,再次 Z 设置为0

audio_listener_position(mouse_x, mouse_y, 0);
  1. obj_Sound对象上,添加一个Step | Step事件,并将一个Execute Script图标拖放到附有步骤脚本的操作中。

  2. 打开房间,并将obj_Sound对象的实例放在房间的中心。

  3. 运行游戏。

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

提高质量标准

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

一致性

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

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

一致性

可读性

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

可读性

抛光

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

波兰语

总结

为游戏创建艺术和音频是一项巨大的任务,无论是在所需的时间还是要制作的资源方面。作为游戏开发者,您有责任确保一切都是连贯的和美观的,无论是创建资源还是与艺术家和音效设计师合作。在本章中,您已经开始了解在 GameMaker: Studio 中艺术和音频是如何工作的,以及好和足够好之间的区别。您了解了可接受的图像格式以及如何导入精灵表。您创建了一个将更好地利用计算机内存并允许创建大型独特世界的平铺集。您对精灵进行了动画处理,并使其正确循环。您还学会了如何控制声音以及它们的听觉方向。现在,您已经准备好开始制作真正的游戏了!

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

第三章:射击游戏:创建横向卷轴射击游戏

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

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

  • FloatBot:它没有武器,但很难击中,因为它在移动时上下浮动。

  • SpaceMine:它是最慢的敌人,如果玩家靠得太近,它会发射一圈子弹。

  • Strafer:它是飞行速度最快的敌人,直线飞行并直接朝玩家位置发射子弹。

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

射击游戏:创建横向卷轴射击游戏

编码约定

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

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

  • 除了房间外,所有资产都将以简单的类型标识符和下划线开头。例如:

  • 精灵spr_

  • 对象obj_

  • 脚本scr_

  • 即使可以使用执行代码 DnD 直接在事件上编写代码,所有代码都将放置在脚本中,并且命名约定将指示其附加到的对象和应用的事件。这将使以后更容易找到以进行调试。例如,放置在玩家对象的创建事件上的代码将具有名为scr_Player_Create的脚本。

  • 如果脚本打算由多个对象使用,则名称应使用清晰描述其功能的名称。例如:要在物体离开屏幕后移除物体,脚本将被命名为scr_OffScreenRemoval

  • 变量将使用驼峰命名法编写,其中使用多个单词;第一个单词以小写字母开头,每个后续单词以大写字母开头,例如:variableWithManyWords

  • 布尔变量应该以问题的形式提出,例如:canShootisPlaying

  • 常量使用全大写字母和下划线来分隔单词,例如:LEFTMAX_GRAVITY

  • if语句中的表达式始终用括号括起来。GameMaker 不要求这样做,但这样做可以更容易阅读代码;例如:if (x > 320)

建造玩家

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

  • 箭头键将使玩家在游戏区域内移动

  • 必须保持在游戏区域内

  • 空格键将发射武器

  • 每次按下按钮都会发射一颗子弹

  • 与子弹或敌人碰撞会造成伤害

  • 应该根据类型有不同的值

设置玩家精灵

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

  1. 创建一个新项目并将其命名为Chapter_03

  2. 创建一个新的精灵并命名为spr_Player

  3. 点击加载精灵,加载Chapter 3/Sprites/Player.gif,勾选移除背景。这个.art文件有一个带有透明度和几帧动画的太空飞船。

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

  1. 点击修改遮罩。这将打开遮罩属性编辑器,如下截图所示:设置玩家精灵

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

  1. 我们希望完全控制碰撞区域,所以将边界框设置为手动,并将形状保留为矩形

  2. 调整边界框参数有两种方法。我们可以输入框的角落的确切位置,或者直接在精灵图像上绘制框。用鼠标左键拖动一个小框,大致位于太空飞船的中心,如前一个截图所示。

  3. 点击确定

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

设置玩家精灵

  1. 原点设置为X:28Y:24,然后点击确定

控制玩家对象

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

  1. 创建一个新对象,命名为obj_Player

  2. spr_Player指定为其精灵。

  3. 我们需要初始化一个变量,用于控制玩家移动的速度。这样以后更改数值会更容易,并且obj_Player中的所有脚本都可以引用它。创建一个新的脚本,命名为scr_Player_Create

mySpeed = 8; 
  1. obj_Player中,添加一个创建事件。

  2. 控制中拖动一个执行脚本图标到操作:区域,并将scr_Player_Create应用到脚本选项。点击确定

  3. 创建一个新的脚本,命名为scr_Player_Key_Left。这个脚本将包含左箭头键的代码。

  4. 虽然我们希望玩家能向左移动,但我们也希望防止玩家离开屏幕。将以下代码写入脚本:

if ( x >= sprite_width )
{
    x -= mySpeed;
}

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

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

  1. obj_Player中,在键盘下添加一个事件。

  2. 控制中拖动一个执行脚本图标到操作:区域,并将 scr_Player_Key_Left应用到脚本选项中。点击确定

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

  1. 创建一个新的房间。

  2. 设置选项卡中,将名称更改为TheGame宽度更改为800。使房间变宽将为玩家提供更多的操纵空间,并更容易识别敌人。

  3. 对象选项卡中,选择obj_Player并在房间中心附近放置一个单个实例,如下截屏所示:控制玩家对象

  4. 运行游戏。

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

  1. 创建一个新的脚本,并将其命名为scr_Player_Key_Right。这将用于右箭头键。

  2. 脚本将类似于左侧,只是我们还需要考虑房间的宽度。编写以下代码:

if (x <= room_width - sprite_width)
{
    x += mySpeed;
}

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

  1. obj_Player中,在键盘下添加一个事件。

  2. 控制中拖动一个执行脚本图标到操作:区域,并应用scr_Player_Key_Right。点击确定

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

  1. 对于上箭头键,创建一个新的脚本,并将其命名为scr_Player_Key_Up,并编写以下代码:
if (y >= sprite_height)
{
    y -= mySpeed;
}

这与水平代码类似,只是现在我们要考虑y位置和精灵的高度。

  1. 对于下箭头键,创建一个新的脚本,并将其命名为scr_Player_Key_Down,并编写以下代码:
if (y <= room_height - sprite_height)
{
    y += mySpeed;
}

同样,在这里,我们要考虑的是房间的高度减去精灵的高度作为我们可以向下移动的最远点。移动控制现在已经完成,对象属性应该如下截屏所示:

控制玩家对象

  1. 运行游戏。

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

创建子弹

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

  1. 创建一个新的精灵,并将其命名为spr_Bullet_Player

  2. 点击加载精灵,加载Chapter 3/Sprites /Bullet_Player.gif

  3. 由于我们当前将玩家对象的原点设置为枪口,我们希望子弹的原点在前面。这将有助于使子弹看起来是从枪口射出,而无需直接编码。将值设置为X17Y4

  4. 其他所有内容保持不变,然后点击确定

  5. 子弹发射时也应该发出声音,所以让我们加入一个声音。我们需要切换回传统声音引擎,以确保在所有浏览器中都能听到音频。导航到资源 | 更改全局游戏设置,在常规选项卡下,取消使用新音频引擎的复选框。

  6. 创建一个新的声音,并将其命名为snd_Bullet_Player

  7. 点击加载声音,加载Chapter 3/Sounds/Bullet_Player.wav

  8. 确保类型设置为普通声音。然后点击确定

  9. 现在是时候让子弹自行移动了。创建一个新的脚本,并将其命名为scr_Bullet_Player_Create

  10. 我们希望子弹向右水平移动。使用以下代码很容易实现:

hspeed = 16;
sound_play(snd_Bullet_01); 

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

  1. 创建一个新对象,命名为obj_Bullet_Player,并将精灵设置为spr_Bullet_Player

  2. 添加一个Create事件。Create事件只在创建时执行一次。

  3. 应用scr_Bullet_Player_Create并点击OK构建子弹

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

发射子弹

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

  1. 创建一个新的脚本,命名为scr_Player_KeyPress_Space

  2. 写下以下代码:

instance_create(x, y, obj_Bullet_Player);

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

  1. obj_Player中,添加一个Space事件从Key Press并应用scr_Player_KeyPress_SpaceKey Press事件检查指定的键是否被按下。这将运行一次,并需要释放键才能再次运行。

  2. 运行游戏。

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

发射子弹

注意

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

从世界中移除子弹

每次创建一个对象实例,都需要将其放入内存,并且计算机需要跟踪它。我们有所有这些子弹离开屏幕再也看不到了,但计算机看到了。这意味着随着时间的推移,计算机可能会试图观察数百万个浪费的子弹,这反过来意味着游戏会开始变慢。由于我们不希望发生这种情况,我们需要摆脱所有这些离开屏幕的子弹。

  1. 创建一个新的脚本,命名为scr_OffScreenRemoval。这个脚本可以应用于游戏中任何离开屏幕并且我们想要摆脱的对象。

  2. 要从世界中移除一个实例,写下以下代码:

instance_destroy();
  1. obj_Bullet_Player中,添加一个Outside Room事件从Other并应用脚本。Outside Room事件是一个特殊事件,检查实例化对象的整个精灵是否完全在房间外。

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

构建三个小敌人

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

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

制作敌人父对象

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

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

  1. 创建一个新对象,命名为obj_Enemy_Parent。我们不需要为这个对象添加精灵,因为它在游戏中永远不会被看到。

  2. 创建一个新脚本,命名为scr_Enemy_Collision_Player

  3. 编写以下代码:

with (other)
{
       instance_destroy();
}
instance_destroy();

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

  1. obj_Enemy_Parent中,从碰撞中添加一个obj_Player事件,并应用此碰撞脚本。

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

  1. 与其创建一个新脚本,不如直接复制我们刚刚创建的碰撞脚本。在资源树中,右键单击scr_Enemy_Collision_Player,然后选择复制

  2. 将此脚本命名为scr_Enemy_Collision_Bullet,并在脚本顶部添加以下代码行:

score += 20;

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

score += 20;
with (other)
{
       instance_destroy();
}
instance_destroy();
  1. obj_Enemy_Parent中,从碰撞中添加一个obj_Bullet事件,并应用scr_Enemy_Collision_Bullet。当敌人与子弹碰撞时,敌人现在将被摧毁并奖励分数!

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

  1. 创建一个新脚本,命名为scr_Enemy_Removal

  2. 编写以下代码:

if (x < 0)
{
    instance_destroy();
}

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

  1. obj_Enemy_Parent中,从其他中添加一个外部房间事件,并应用此脚本。我们已经完成了父对象,它应该看起来像下面的截图:制作敌人父对象

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

构建 FloatBot

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

  1. 创建一个新精灵,命名为spr_FloatBot

  2. 加载精灵Chapter 3/Sprites/FloatBot.gif,勾选删除背景

  3. 这是一个动画精灵,每一帧形状都会改变。因此,我们希望确保碰撞相应地改变。在碰撞检查中,勾选精确碰撞检查

  4. 我们希望将原点设置在此精灵的中心,这样当我们添加摆动运动时,它就会正确移动。将原点设置为X:16Y:16。然后单击确定

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

  1. 创建一个新的脚本,并将其命名为scr_FloatBot_Create

  2. 编写以下代码:

hspeed = -4;
angle = 0;

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

  1. 创建一个新脚本,并将其命名为scr_FloatBot_Step

  2. 为了获得我们想要的垂直运动,我们将使用一些简单的三角学。编写以下代码:

vspeed = sin(angle) * 8;
angle += 0.1

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

  1. 创建一个新对象,命名为obj_FloatBot,并将spr_FloatBot设置为精灵。

  2. 我们希望将此对象设置为子对象,因此在父对象下拉框中,选择obj_Enemy Parent

  3. 添加一个创建事件并应用scr_FloatBot_Create脚本。

  4. 添加一个步骤事件并应用scr_FloatBot_Step脚本。FloatBot 现在已经准备好测试,应该看起来像下面的截图:构建 FloatBot

  5. 重新打开房间TheGame,并在屏幕右侧的某个地方放置一个obj_FloatBot的实例。

  6. 运行游戏。

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

构建 FloatBot

创建 SpaceMine

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

  1. 创建一个新精灵,命名为spr_Bullet_SpaceMine。加载Chapter 3/Sprites/Bullet_SpaceMine.gif,勾选删除背景

  2. 将原点居中。我们不需要改变碰撞检查,因为正方形对于这个对象来说效果很好。

  3. 创建一个新对象,命名为obj_Bullet_SpaceMine,并将精灵设置为spr_Bullet_SpaceMine

  4. 创建一个新脚本,并将其命名为scr_Bullet_SpaceMine_Create

  5. 这次我们希望使用speeddirection的实例属性,因为我们稍后需要设置方向。编写以下代码:

speed = 16;
direction = 180;
  1. obj_Bullet_SpaceMine中,添加一个创建事件并应用此脚本。

  2. 我们需要为子弹添加碰撞,为了快速完成这个过程,我们可以重用scr_Enemy_Collision_Player脚本。从碰撞中添加一个obj_Player事件并应用脚本。目前我们已经完成了子弹,如下截图所示:创建 SpaceMine

  3. 是时候建立 SpaceMine 本身了。创建一个新精灵,命名为spr_SpaceMine,并加载Chapter 3/Sprites/SpaceMine.gif,勾选删除背景。正如你所看到的,SpaceMine 有动画闪烁的灯光。

  4. 将原点居中并检查精确碰撞检查

  5. 当 SpaceMine 发射时,我们希望有射击声音,因此创建一个新声音,snd_Bullet_SpaceMine,并加载Chapter 3/Sounds/Bullet_SpaceMine.wav。我们不会将其附加到子弹本身,因为我们将创建八颗子弹,但我们只需要播放一次声音。

  6. 如果尚未设置,将类型设置为普通声音,然后单击确定。创建一个新对象,命名为obj_SpaceMine

  7. 精灵设置为spr_SpaceMine父对象设置为obj_Enemy_Parent

  8. 创建一个新脚本,并将其命名为scr_SpaceMine_Create

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

  1. 写下以下代码:
hspeed = -2;
canFire = false;
image_speed = 0.2;

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

  1. obj_SpaceMine中,添加一个Create事件并应用这个脚本。

  2. 创建另一个新的脚本,命名为scr_SpaceMine_Step

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

  1. 写下以下代码:
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,表示我们已经发射了子弹。

  1. obj_SpaceMine中,添加一个Step事件并应用这个脚本。

  2. 我们几乎完成了 SpaceMine,我们只需要在一个可以触发的警报中添加一些代码,这样它就可以多次射击。创建一个新的脚本,命名为scr_SpaceMine_Alarm0

  3. canFire变量设置回false

canFire = false;
  1. obj_SpaceMine中,添加一个Alarm 0事件并应用这个脚本。现在我们已经完成了 SpaceMine,它应该看起来像下面的截图:Creating the SpaceMine

  2. 打开TheGame,在屏幕的右侧添加一个obj_SpaceMine的实例,然后运行游戏。

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

Creating the SpaceMine

制作 Strafer

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

  1. 创建一个新的精灵,命名为spr_Bullet_Strafer。加载Chapter 3/Sprites/Bullet_Strafer.gif,并勾选Remove Background

  2. 将原点居中。

  3. 创建一个新的对象,命名为obj_Bullet_Strafer,并将精灵设置为spr_Bullet_Strafer

  4. 我们想要一个独特的射击声音,所以创建一个新的声音,snd_Bullet_Strafer,并加载Chapter 3/Sounds/Bullet_Strafer.wav

  5. 如果尚未将种类设置为普通声音,请点击确定

  6. 创建一个新的脚本,并将其命名为scr_Bullet_Strafer_Create

  7. 这个脚本与scr_Bullet_SpaceMine_Create类似,只是这颗子弹速度更快,并播放子弹声音。编写以下代码:

speed = 20;
direction = 180;
sound_play(snd_Bullet_Strafer);
  1. obj_Bullet_Strafer中,添加一个创建事件,并应用此脚本。

  2. 与其他敌人子弹一样,让我们通过重用scr_Enemy_Collision_Player脚本为子弹添加碰撞。从碰撞中添加一个obj_Player事件,并应用该脚本。子弹部分完成后,让我们构建敌人。

  3. 创建一个新的精灵,并将其命名为spr_Strafer,并加载Chapter 3/Sprites/Strafer.gif,勾选删除背景

  4. 我们希望子弹从飞船的前方发射,因此我们需要手动将原点移动到正确的位置。将原点设置为X0Y19

  5. 创建一个新的对象,并将其命名为obj_Strafer

  6. 精灵设置为spr_Strafer父对象设置为obj_Enemy_Parent

  7. 创建一个新的脚本,并将其命名为scr_Strafer_Create

  8. Strafer 将快速在屏幕上移动并不断向玩家发射子弹。编写以下代码:

hspeed = -10;
alarm[0] = 10;

与 SpaceMine 类似,我们将hspeed设置为向左移动,并设置一个警报,以便 Strafer 立即开始射击。

  1. obj_Strafer中,添加一个创建事件,并应用此脚本。

  2. 我们只需要再创建一个脚本,那就是用于警报的脚本。创建一个新的脚本,并将其命名为scr_Strafer_Alarm0

  3. 当警报响起时,我们需要创建一个子弹,将其发射到玩家,并重置警报,以便它可以再次发射。编写以下代码:

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函数将返回一个介于零和传递给它的数字之间的整数。我们这里的代码将给我们一个介于030之间的随机值,然后我们将其加上15。这意味着每隔半秒到一秒半之间将创建一个新的子弹。

  1. obj_Strafer中,添加一个Alarm 0事件,并应用此脚本。制作 Strafer

  2. Strafer 现在已经完成,让我们测试一下,并将其放置在TheGame的左侧。制作 Strafer

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

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

通过 Overlord 控制游戏

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

生成敌人的波浪

我们需要首先创建敌人的波动,以便游戏可玩。为此,我们将利用循环时间线来生成各种敌人。我们将有三个不同的波动,每两秒生成一个不同的敌人。

  1. 创建三个新脚本,并命名为:scr_Wave_Straferscr_Wave_SpaceMinescr_Wave_FloatBot

  2. 我们将从 Strafer 的波动开始,因为它将是最简单的波动。在scr_Wave_Strafer中编写以下代码:

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像素。

  1. 对于 SpaceMine,我们希望它们在随机位置垂直放置,以保持事情的趣味性。在scr_Wave_SpaceMine中编写以下代码:
placeY = irandom_range(64, room_height - 64);
instance_create(room_width - 64, placeY, obj_SpaceMine);

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

  1. FloatBot 将使用类似的垂直轴放置设置,但我们希望有三个实例以“V”形式飞行。在scr_Wave_FloatBot中编写以下代码:
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 像素。

  1. 所有波动都已编写脚本,因此我们现在可以在时间线中实现它们。首次实现时间线时,保持数字简单是有用的,例如相隔两秒。适当平衡时间是在游戏开发的打磨阶段进行的,花费太多时间试图在所有内容都在位之前就把这个问题解决好,很可能是浪费时间。创建一个新的时间线,命名为tm_Wave_Spawning

  2. 点击Add,将Indicate the Moment设置为60,并应用scr_Wave_FloatBot脚本。这将在游戏中生成第一个敌人,持续两秒。

  3. 两秒后我们将添加 SpaceMines。点击Add,将Indicate the Moment设置为120,并应用scr_Wave_SpaceMine脚本。

  4. 最后,六秒后我们将带入 Strafer。点击Add,将Indicate the Moment设置为180,并应用scr_Wave_Strafer脚本。时间线现在已准备好使用,并且应该如下截图所示:生成敌人的波动

构建 Overlord

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

  1. 创建一个新对象,命名为obj_Overlord

  2. 不需要精灵,所以将Sprite设置为no sprite

  3. 我们将在创建 Overlord 时立即开始时间线。创建一个新脚本,命名为scr_Overlord_Create,并编写以下代码:

timeline_index = tm_Wave_Spawning;
timeline_running = true;
timeline_loop = true;

代码的第一行定义了我们要运行的时间线,我们只有一个:tm_Wave_Spawning。接下来,我们启动时间线,然后告诉它循环。这最后两个是布尔变量,这意味着它们只能打开和关闭。

  1. 在 Overlord 中,添加一个Create事件并应用此脚本。

  2. 打开TheGame,并在房间中放置一个 Overlord 的实例。位置无关紧要,但左上角是一个常见的放置位置。

  3. 删除房间中剩余的敌人实例。如下截图所示,房间中应该只有一个 Player 的实例和一个 Overlord 的实例:构建 Overlord

  4. 运行游戏。构建 Overlord

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

  • 我们可以在屏幕上移动玩家,但不能移出屏幕

  • 我们可以射击和摧毁敌人

  • 敌人可以射击和摧毁玩家

  • 敌人将不断生成

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

处理玩家的生死

由于这是一个关于生存的游戏,我们希望胜利/失败条件相对简单。对于胜利条件,我们将使玩家生存一段时间。失败条件是玩家死亡,但我们不希望游戏太难玩,所以我们给玩家三条生命。这意味着我们需要重新生成玩家。最后,为了使这个功能正常工作,我们需要给 Overlord 一些额外的职责。

设置胜利条件

这个游戏的胜利条件是生存一段时间。我们可以通过使用警报和一个变量来实现这一点,向 Overlord 发出信号,玩家已经生存下来。

  1. 我们需要为生命、胜利和失败条件设置一些变量。重新打开scr_Overlord_Create,在底部添加以下代码:
lives = 3;
isVictory = false;
isDefeat = false;

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

  1. 我们还可以通过在这个脚本中设置一个 90 秒的警报来设置我们的胜利条件。为此,在步骤 1 的代码之后添加以下代码行:
alarm[0] = 2700;

scr_Overlord_Create脚本现在应该总共如下所示:

timeline_index = tm_Wave_Spawning;
timeline_running = true;
timeline_loop = true;

lives = 3;
isVictory = false;
isDefeat = false;

alarm[0] = 2700;

  1. 接下来,我们需要为胜利条件的警报事件设置一个脚本。创建一个新脚本,命名为scr_Overlord_Victory,并编写以下代码:
timeline_running = false;
with ( obj_Enemy_Parent )
{
    instance_destroy();
}
alarm[1] = 90; 
isVictory = true;

我们要做的第一件事是停止时间线,因为我们不希望再生成更多的敌人。下一步是移除游戏中仍然存活的所有敌人。我们通过使用with语句来执行obj_Enemy_Parent的所有实例的代码来实现这一点。因为所有的敌人都是这个对象的子对象,它们也会被销毁。最后,我们为三秒钟设置另一个警报。最后,我们将isVictory变量设置为 true。

  1. obj_Overlord中,添加一个Alarm 0事件并应用胜利脚本。

  2. 让我们通过创建重新启动脚本来结束这一切。创建一个新脚本,命名为scr_Overlord_GameRestart,并编写以下代码:

game_restart();
  1. 添加一个Alarm 1事件并应用重新启动脚本。现在胜利条件已经生效,随时可以尝试。

使用 Ghost 对象重新生成

现在我们可以转向失败条件和重新生成。当玩家死亡时,我们不希望玩家立即重新生成,而是有一个较短的无敌时间。为此,我们需要创建一个 Ghost 对象,暂时代替玩家。

  1. 创建一个新精灵,命名为spr_Ghost,并加载Chapter 3/Sprites/Ghost.gif,勾选删除背景。它看起来就像飞机,但是略微透明,在动画时会闪烁。

  2. 我们需要将原点设置为与spr_Player的原点完全相同。将原点设置为X:43Y:22,然后点击确定

  3. 创建一个新对象,命名为obj_Ghost,并将spr_Ghost应用为精灵。

  4. 当玩家死亡时,我们将让 Ghost 出现在屏幕左侧并移入游戏区域。创建一个新的脚本,命名为scr_Ghost_Create,并编写以下代码:

x = -64;
y = room_height * 0.5;
hspeed = 4;

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

  1. obj_Ghost添加一个Create事件并应用此脚本。

  2. Ghost 将在屏幕上移动,我们需要在某个时候将其转换为玩家。在我们的情况下,一旦 Ghost 通过了游戏区域的四分之一,我们将进行切换。创建一个新的脚本,命名为scr_Ghost_Step,并编写以下代码:

if ( x >= 200 )
{
    hspeed = 0;
    instance_change(obj_Player, true);
}

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

  1. obj_Ghost添加一个Step事件并应用此脚本。

  2. 我们将在这种设置中遇到一个问题,那就是玩家无法控制 Ghost,并且在变换时可能会出现在靠近敌人的危险位置。我们不希望出现这种情况,所以让我们给玩家一些有限的控制权。我们可以重用现有的scr_Player_Key_Upscr_Player_Key_Down脚本,以便玩家具有垂直移动。添加适当的键盘事件并附加这些脚本。

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

使用 Ghost 对象重新生成

  1. 重新打开scr_Enemy_Collision_Player

  2. 目前,我们正在销毁子弹和玩家。我们需要更改with语句以允许重新生成。删除第3行:

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

此时我们可以玩游戏。请注意,当玩家死亡时:

  • 玩家消失

  • 创建一个 Ghost 并移入游戏区域

  • Ghost 可以上下移动

  • Ghost 变回 Player

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

  1. 创建一个新的脚本,scr_Overlord_Step,并编写以下代码:
if ( lives < 0 && isDefeat == false ) {
    alarm[1] = 90;    
    isDefeat = true;
}

这段代码中的每一步都会检查玩家是否还有生命。如果玩家没有生命了,而变量isDefeat仍然为false,它将为重新开始游戏警报设置三秒。最后,我们将isDefeat变量设置为true,这样我们就不会再运行这段代码了。

  1. obj_Overlord中,添加一个Step事件并应用此脚本。玩家死亡三次后游戏将重新开始。

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

绘制用户界面

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

  1. 首先,我们需要一个用于显示文本的字体。我们提供了一个名为Retroheavyfuture的字体供本游戏使用,需要在您的计算机上安装。要在 Windows 7 计算机上安装此字体,请右键单击Chapter 3/Fonts/RETRRG__.ttf,然后单击安装。然后按照提示进行操作。

  2. 回到 GameMaker: Studio,创建一个新的字体,命名为fnt_Scoreboard

  3. 选择Retroheavyfuture作为字体

  4. 样式下将大小设置为16

  5. 我们需要一个适当大小的字体来显示游戏中的得分和生命。它应该看起来像下面的截图,所以点击确定绘制用户界面

  6. 当我们显示胜利/失败条件时,我们将需要字体的第二个版本。创建一个新的字体,命名为fnt_WinLose

  7. 再次选择Retroheavyfuture作为字体,但这次将大小设置为32。现在我们已经拥有了所有游戏中需要的字体,所以点击确定

  8. 让我们继续进行新的脚本scr_Overlord_Draw。我们将从以下代码开始设置记分牌文本的颜色和字体:

draw_set_color(c_white);
draw_set_font(fnt_Scoreboard);

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

注意

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

  1. 设置字体后,我们可以开始应用 HUD。我们将从玩家生命开始。将以下代码添加到脚本中:
draw_set_halign(fa_left);
if ( lives >= 0 )
{
    draw_text(8, 0, "Lives: " + string(lives));
} else {
    draw_text(8, 0, "Lives: " );
}

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

  1. 我们想要的另一个 HUD 元素是得分,我们将其放在屏幕的对面,即右上角。添加以下代码:
draw_set_halign(fa_right);
draw_text(room_width-8, 0, "SCORE: " + string(score));

与之前的文本一样,我们设置了水平对齐,这次是右对齐。然后使用相同的连接方法将文本放在正确的位置。

  1. 现在让我们通过向obj_Overlord添加绘制 GUI事件并应用此脚本来测试一下。

  2. 运行游戏。如下截图所示,游戏现在应该在左上角显示生命,并在玩家死亡时更新。它还应该在右上角显示得分,并随着每个敌人被杀而增加。绘制用户界面

  3. 现在我们需要添加玩家赢或输时的显示。在scr_Overlord_Draw的末尾添加以下代码:

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,并将水平对齐设置为居中。我们不希望文本一直显示,而是应该在适当时只显示VICTORYDEFEAT。我们已经在 Overlord 中实现了游戏条件的代码,所以我们只需要在每一步检查isVictory是否为trueisDefeat是否为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");
}

为游戏添加完成细节

游戏现在在功能上已经完成,但它没有任何光泽或我们期望的完整游戏的完成细节。没有音乐,没有背景艺术,也没有爆炸!让我们立即解决这个问题。

添加游戏音乐

我们希望音乐从头开始播放,并在游戏持续时间内播放。当发生胜利/失败条件时,我们希望音乐渐渐消失,以让玩家知道游戏已经结束。

  1. 创建一个新的声音并命名为snd_Music

  2. 加载Chapter 3/Sounds/Music.mp3种类应设置为背景音乐

  3. 重新打开scr_Overlord_Create。由于霸主控制整个游戏,我们将使用它来控制音乐。在最后一行代码之后,添加以下内容:

sound_play(snd_Music);
sound_loop(snd_Music);
volume = 1;
sound_global_volume(volume);

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

  1. 重新打开scr_Overlord_Step。为了淡出音乐,我们需要在几个步骤内降低全局音量,但只有在游戏结束时才这样做。在最后一行代码之后,添加以下内容:
if ( isDefeat == true || isVictory == true )
{
    volume -= 0.02;
    sound_global_volume(volume);
}

在这里,我们检查是否已将胜利或失败条件设置为true。如果是,我们将通过0.02减少音量变量并将其应用于主增益级别。声音级别从最大音量降至静音需要 50 步,大约是游戏重新开始之前的一半时间。

  1. 运行游戏。现在你应该听到背景音乐正在播放。如果玩家快速死亡三次并触发了失败条件,你应该听到声音渐渐消失。

使背景移动

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

  1. 创建一个新的背景并命名为bg_Starscape

  2. 加载Chapter 3/Backgrounds/Starscape.gif,不勾选删除背景。这就是我们需要做的一切,所以点击确定

  3. 打开TheGame并选择背景选项卡。

  4. bg_Starscape设置为背景图像。这应该会自动发生,但确保背景 0被突出显示,并且在房间开始时可见已被选中。

  5. 星空只会水平移动,因此我们只需要勾选水平平铺,以便图像环绕。

  6. 要移动背景,将水平速度设置为-2。这将使其向左移动,从而使玩家看起来向右移动。设置应如下截图所示:使背景移动

  7. 运行游戏。现在你应该看到一个移动的星空!查看以下截图:使背景移动

创建爆炸

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

  1. 创建一个新的精灵,spr_Explosion,并加载Chapter 3/Sprites/Explosion.gif,勾选删除背景

  2. 将原点设置为中心,然后点击确定

  3. 创建一个新的声音,snd_Explosion,并加载Chapter 3/Sounds/Explosion.wav

  4. 如果尚未设置种类普通声音,请设置为普通声音,然后点击确定

  5. 创建一个新的对象,obj_Explosion,并将精灵设置为spr_Explosion

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

  1. 创建一个新的脚本,scr_Explosion_Create,并编写以下代码以播放爆炸声音一次:
sound_play(snd_Explosion);
  1. 添加一个创建事件并应用此脚本。

  2. 要使爆炸自行消失,最好在动画完成时执行。幸运的是,GameMaker: Studio 有一个事件可以做到这一点。从其他中添加一个动画结束事件,然后创建一个名为scr_Explosion_AnimEnd的新脚本,并添加以下代码以删除实例:

instance_destroy();
  1. 爆炸现在已经准备好了,我们所要做的就是在摧毁敌人时生成它。打开scr_Enemy_Collision_Bullet,并在脚本的第一行添加以下代码:
instance_create(x,y, obj_Explosion);

这将在敌人所在的位置创建一个爆炸。这需要在我们将敌人从游戏中移除之前发生。

  1. 使用scr_Enemy_Collision_Player重复这段代码添加。

  2. 运行游戏。现在,每当有东西被摧毁时,你应该看到爆炸,就像下面的截图所示:Creating the explosions

总结

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

我们发现了美妙的with语句,它使我们能够影响单个实例、对象的所有实例,甚至是碰撞中涉及的other实例。我们研究了全局变量,比如livesscore,并使用Draw事件来显示它。敌人的波浪是使用时间轴生成的。通过滚动背景图像创建了移动的错觉。声音被应用,并调整音量以创建淡出效果。我们甚至使用了一点三角学!

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

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

第四章:冒险开始

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

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

冒险开始

创建动画角色

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

简化角色移动

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

  1. 让我们从创建一个名为Chapter_04的新项目开始。

  2. 创建一个精灵,命名为spr_Player_WalkRight

  3. 加载第四章/精灵/Player_WalkRight.gif,并勾选删除背景

  4. 原点设置为中心

  5. 单击修改掩码以打开掩码属性编辑器,并在边界框下选择完整图像的单选按钮。这将设置碰撞框为整个精灵,如下截图所示:简化角色移动

  6. 点击确定。重复此过程以加载spr_Player_WalkLeftspr_Player_WalkUpspr_Player_WalkDown

  7. 创建一个对象,obj_Player,并将spr_Player_WalkRight分配为精灵。实际上,在这里设置玩家精灵的哪一个并不重要,因为我们将使用代码来改变显示的精灵。

  8. 我们需要设置一些初始变量,因此创建一个新脚本,scr_Player_Create,并编写以下代码:

mySpeed = 4;
myDirection = 0;
isAttacking = false;
isWalking = false;
health = 100;
image_speed = 0.5;

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

注意

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

  1. 现在我们可以开始玩家的移动了。我们不再为每个键创建多个脚本,而是将所有控件放入一个单独的脚本中,简化代码。创建一个新脚本,scr_Player_Step,并从以下代码开始:
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的第一行代码。

  1. 重复这段代码,针对剩下的三个方向进行调整。每个方向都应该查看哪个键被按下,并查看从该位置是否有任何碰撞。

  2. 在移动控件完成之前,我们还有一件事要做。如果玩家没有移动,我们希望动画停止,并在开始移动时重新开始播放。在脚本的末尾,添加以下代码:

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;
} 
  1. 将这些脚本应用到适当的事件中,scr_Player_Create创建事件,以及scr_Player_Step步进事件。

玩家已经准备好移动和正确播放动画了,但如果没有添加一些实体障碍物,我们将无法完全测试代码。让我们建一堵墙。

  1. 创建一个精灵,spr_Wall,加载第四章/精灵/墙.png,并取消选中删除背景。我们使用 PNG 文件,因为这堵墙略微透明,这在以后装饰房间时会很有用。

  2. 创建一个新对象,obj_Wall,并将精灵设置为spr_Wall

  3. 勾选实体框。现在这堵墙被标识为可碰撞的对象。

  4. 创建一个新的房间,命名为沙盒。我们将使用这个房间来测试功能。

  5. 在房间的中心某处放置一个obj_Player的实例。

  6. 在房间的周边放置obj_Wall的实例,并添加一些额外的部分,如下屏幕截图所示:简化角色移动

  7. 运行游戏。此时玩家应该能够在开放区域自由移动,并在与墙碰撞时停止。

实施近战攻击

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

  1. 创建一个精灵,spr_Player_Attack,加载第四章/精灵/Player_Attack.gif,并选中删除背景。这是一个动画精灵,代表挥动的近战攻击。

  2. 我们希望碰撞区域影响精灵的整个高度,但不影响整个宽度。点击修改掩码,在掩码属性编辑器中,选择边界框下的手动单选按钮。

  3. 调整边界框的值为024顶部0底部4。最终结果应该看起来像以下的屏幕截图。点击确定实施近战攻击

  4. 我们希望这个对象始终出现在玩家的前面。确保这一点的最简单方法之一是让这个对象随着玩家一起旋转。为了实现这一点,将原点设置为X: -16 Y: 24。将 X 坐标设置为左侧意味着这个对象在生成时将有 16 像素的偏移。然后我们可以旋转攻击以匹配玩家的方向。

  5. 创建一个对象,obj_Player_Attack,并将spr_Player Attack分配为其精灵。

  6. 深度设置为-100深度决定了一个对象实例在屏幕上是在另一个对象的后面还是上面。将其设置为负值意味着它将在具有更高深度值的任何对象上绘制。将值设置为-100允许我们在默认的0-99之间拥有其他深度的对象,而无需担心以后需要重新调整事物。

  7. 创建一个新的脚本,scr_Player_Attack_Create,其中包含以下代码:

image_angle = obj_Player.myDirection;
image_speed = 0.3;
alarm[0] = 6;
obj_Player.isAttacking = true;

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

  1. obj_Player_Attack中添加一个创建事件,并附上这个脚本。

  2. 让我们继续进行警报脚本,scr_Player_Attack_Alarm。它不仅需要移除攻击,还需要让玩家知道它已经消失,他们可以再次进行攻击。我们只需要两行代码就可以做到这一切:

obj_Player.isAttacking = false;
instance_destroy();

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

  1. 现在我们只需要让玩家生成一个攻击的实例。重新打开scr_Player_Step,在底部添加以下代码:
if (keyboard_check_pressed(ord('Z')) && isAttacking == false)
{
    instance_create(x, y, obj_Player_Attack);
}

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

注意

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

  1. 运行游戏。你应该能够按下Z键,无论角色面向哪个方向,都能看到玩家面前独特的挥动动作,如下截图所示。玩家现在已经准备好战斗了!实施近战攻击

在房间之间导航

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

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

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

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

设置房间

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

  1. 创建一个新房间,在设置中命名为TitleScreen

  2. 创建一个新的背景,bg_Title,并加载Chapter 4/Backgrounds/Title.png,不勾选Remove Background

  3. TitleScreenBackgrounds选项卡中,将bg_Title应用为Background 0,并勾选Visible at Start

  4. 创建另一个房间,命名为C04_R01。这里的名称代表章节和房间,如第四章,第 1 房间。

  5. WidthHeight设置为1024。这将允许我们有很多探索空间。

  6. 我们不希望一次看到房间中的所有东西,因此我们需要限制视图。点击Views选项卡,勾选Enable the Use of Views。选择View 0,并勾选Visible when room starts。这将激活房间的摄像机系统。

  7. 我们还希望视图关注玩家并随之移动。在Views选项卡中,选择Object following下的obj_Player,并将Vbor:Hbor:设置为200。这将使摄像机跟随玩家,并在视图边缘留下 200 像素的缓冲区。查看以下截图,确保一切设置正确:设置房间

  8. 使用刚才与C04_R01相同的设置,创建另外两个房间C04_R02C04_R03

  9. 在资源树中,通过将Sandbox拖动到底部和TitleScreen拖动到最顶部来重新排序房间。它应该看起来像以下截图:设置房间

  10. 最后,使用墙对象创建一个迷宫,包括所有三个房间。设计目前并不重要;只需确保玩家能够从一边到达另一边。可以在以下截图中看到它可能的样子:设置房间

创建房间传送门

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

  1. 为了使房间传送系统工作,我们需要使用一些全局变量,这些变量需要在游戏开始时初始化。创建一个新脚本,scr_Globals_StartGame,并使用以下代码:
global.portalA = 0;
global.portalB = 0;
global.portalC = 0;
global.portalD = 0;
global.lastRoom = C04_R01;

我们为四个传送门创建全局变量,并给它们一个零值。我们还跟踪我们上次所在的房间,这样我们就知道我们需要去新房间的哪个传送门。

  1. 创建一个新对象,obj_Globals,添加一个Game Start事件,并附加此脚本。这个对象不需要精灵,因为它只是一个数据对象。

  2. 将一个obj_Globals的实例放入TitleScreen

  3. 我们需要能够从标题屏幕进入游戏,因此让我们通过添加Draw事件并创建一个新脚本scr_Globals_Draw来快速修复,并使用以下代码添加以下内容:

draw_set_color(c_white);
draw_set_halign(fa_center);
draw_text(room_width/2, 360, "Press ANY key");
if (keyboard_check_pressed(vk_anykey))
{
    room_goto_next();
}

在这里,我们只是编写一些白色的居中文本,让玩家知道他们如何开始游戏。我们使用特殊变量vk_anykey来查看键盘是否被按下,如果按下了,我们就按照资源树中的顺序进入下一个房间。

注意

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

  1. 让我们制作一些传送门!创建一个新精灵,spr_Portal_A_Start,加载Chapter 4/Sprites/Portal_A_Start.png,并取消勾选Remove Background。居中原点,然后点击OK

  2. 创建一个新的对象,obj_Portal_A_Start,将精灵设置为spr_Portal_A_Start。这是我们将玩家移动到的着陆点,当他们进入房间时。它不需要任何代码,所以点击确定

  3. 创建一个新的精灵,spr_Portal_A_Exit,并加载Chapter 4/Sprites/Portal_A_Exit.png,取消删除背景,并将原点居中。

  4. 创建一个新的对象,obj_Portal_A_Exit,并相应地设置精灵。这是实际的传送门,当玩家与之碰撞时,我们将改变房间。

  5. 对于obj_Player事件,创建一个新的脚本,scr_Portal_A_Exit_Collision,并编写以下代码:

global.lastRoom = room;
room_goto(global.portalA);

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

  1. 重复步骤 5 到 9,为传送门 B、C 和 D 做同样的操作,确保更改所有适当的值以反映正确的传送门名称。

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

创建房间传送门

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

  1. C04_R01设置选项卡中,单击创建代码以打开代码编辑器,并编写以下代码:
global.portalA = C04_R02;
global.portalB = 0;
global.portalC = 0;
global.portalD = 0;

我们将PORTAL A设置为第二个房间。所有其他传送门都没有被使用,所以我们将变量设置为零。每个房间都需要将所有这些变量设置为某个值,要么是特定的房间,要么是零,否则可能会导致错误。

  1. C04_R02设置选项卡中,单击创建代码以打开代码编辑器,并编写以下代码:
global.portalA = C04_R01;
global.portalB = 0;
global.portalC = 0;
global.portalD = 0;

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

传送持久玩家

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

  1. 打开obj_Player并勾选持久

  2. 接下来,我们需要将玩家重新定位到正确的传送门。我们将创建一个新的脚本,scr_Player_RoomStart,并在obj_Player房间开始事件中使用以下代码。

if (global.lastRoom == global.portalA)
{
    obj_Player.x = obj_Portal_A_Start.x;
    obj_Player.y = obj_Portal_A_Start.y;
} else if (global.lastRoom == global.portalB) {
    obj_Player.x = obj_Portal_B_Start.x;
    obj_Player.y = obj_Portal_B_Start.y;
} else if (global.lastRoom == global.portalC) {
    obj_Player.x = obj_Portal_C_Start.x;
    obj_Player.y = obj_Portal_C_Start.y;
} else if (global.lastRoom == global.portalD) {
    obj_Player.x = obj_Portal_D_Start.x;
    obj_Player.y = obj_Portal_D_Start.y;
} 

当玩家进入一个房间时,我们检查玩家刚刚离开的房间与哪个传送门相关联。然后将玩家移动到适当的着陆点。为了确保玩家被正确构建,其属性应如下截图所示:

传送持久玩家

  1. 将玩家实例放入C04_R01。不要将玩家放入其他任何房间,否则游戏中将会出现多个玩家实例。

  2. 运行游戏。我们应该能够在第一个房间四处移动,并通过 A 门,这将把我们带到第二个房间的 A 门着陆点。有了这个系统,一个游戏可以有数百个房间,只需要四个传送门来管理。

给敌人生命

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

召唤幽灵图书管理员

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

  1. 让我们从 BookPile 开始。创建一个新的精灵,spr_BookPile,并加载Chapter 4/Sprites/BookPile.gif,勾选删除背景

  2. 将原点居中,然后点击确定

  3. 我们还需要一个可怕的声音来警告玩家危险。创建一个新的声音,snd_GhostMoan,并加载Chapter 4/Sounds/GhostMoan.wav。点击确定

  4. 创建一个新的对象,obj_BookPile,并分配spr_BookPile作为精灵。

  5. 我们不希望玩家能够穿过 BookPile,所以勾选固体

  6. 我们需要初始化一些变量,所以创建一个新的脚本,scr_BookPile_Create,并编写以下代码:

myRange = 100;
hasSpawned = false;

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

  1. 添加一个创建事件并应用此脚本。

  2. 接下来我们需要一个新的脚本,scr_BookPile_Step,它将应用于步骤事件,并包含以下代码:

if (instance_exists(obj_Player))
{  
    if (distance_to_object(obj_Player) < myRange && hasSpawned == false)
    {
        ghost = instance_create(x, y, obj_Ghost);
        ghost.myBooks = self.id;
        sound_play(snd_GhostMoan);
        hasSpawned = true;
    }     
}

代码的第一行非常重要。在这里,我们首先检查玩家是否存在,然后再进行其他操作。如果玩家存在,我们检查玩家对象的距离是否在范围内,以及这个 BookPile 是否已经生成了幽灵。如果玩家在范围内并且还没有生成任何东西,我们就生成一个幽灵。我们还会将这个 BookPile 的唯一 ID 使用self变量发送到幽灵中,这样它就知道自己来自哪里。接下来播放幽灵的呻吟声音,确保不要循环播放。最后,我们通过将hasSpawned变量更改为true来指示我们已经生成了一个幽灵。

  1. 唯一剩下的元素是添加一个obj_Player_Attack事件,使用一个新的脚本,scr_BookPile_Collision,并编写以下代码:
if (instance_exists(ghost))
{
    with (ghost)
    {
        instance_destroy();
    }
}
instance_destroy();

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

召唤幽灵图书管理员

  1. 现在我们需要构建幽灵。为此,我们需要引入两个精灵,一个用于生成,一个用于追逐。创建精灵时勾选删除背景,分别为spr_Ghostspr_Ghost_Spawn,并加载Chapter 4/Sprites/Ghost.gifChapter 4/Sprites/Ghost_spawn.gif

  2. 在两个精灵中,将原点居中。

  3. 深度:字段设置为-50,这样幽灵将出现在大多数物体上方,但在玩家攻击物体下方。没有其他需要做的事情,所以点击确定

  4. 创建一个新的对象,obj_Ghost,并应用spr_Ghost_Spawn作为精灵。这将使生成动画成为初始精灵,然后我们将通过代码将其更改为常规幽灵。

  5. 我们有几个变量需要在一个新的脚本scr_Ghost_Create中初始化,如下所示的代码:

mySpeed = 2;
myRange = 150;
myBooks = 0;
isDissolving = false;
image_speed = 0.3; 
   alarm[0] = 6;
  1. 我们设置了移动速度的变量,幽灵将在其中追踪的范围,生成幽灵的人(我们将通过书堆改变),以及幽灵是否已经返回到书堆的变量。请注意,幽灵的范围比书堆的范围大。这将确保幽灵立即开始追逐玩家。然后我们设置了动画速度,并设置了一个六步的警报,我们将用它来改变精灵。

  2. 添加一个Alarm0事件,然后应用一个新的脚本,scr_Ghost_Alarm0,其中包含以下代码来改变精灵:

sprite_index = spr_Ghost;

现在我们准备开始实现一些人工智能。幽灵将是最基本的敌人,会追逐玩家穿过房间,包括穿过墙壁和其他敌人,直到玩家超出范围。在那时,幽灵将漂浮回到它来自的书堆。

  1. 我们将从追逐玩家开始。创建一个新的脚本,scr_Ghost_Step,并编写以下代码:
if (instance_exists(obj_Player))
{
    targetDist = distance_to_object(obj_Player)
    if (targetDist < myRange)
    {       
        move_towards_point(obj_Player.x, obj_Player.y, mySpeed);
    }   
}

在确保玩家还活着之后,我们创建一个变量来保存幽灵到玩家的距离。我们创建targetDist变量的原因是我们将需要这个信息几次,这样可以避免每次有if语句时都重新检查距离。然后我们比较距离和追逐范围,如果玩家在范围内,我们就朝着玩家移动。move_towards_point函数会计算方向并将速度应用到该方向的对象上。

  1. 添加一个Step事件并应用这个脚本。我们将继续向这个脚本添加代码,但它已经可以正常运行了。

  2. 让我们花一点时间来测试我们到目前为止所做的一切。首先,在资源树中,将Sandbox移到接近顶部,这样它就是标题屏幕后的房间。打开Sandbox房间,并像以下截图所示,在边缘放置几个obj_BookPile的实例:召唤幽灵图书管理员

  3. 运行游戏。如果你离书堆太近,一个幽灵会从中产生,并慢慢追逐玩家。如果玩家离幽灵太远,幽灵将继续朝着它最后的方向移动,并最终消失在屏幕外。

  4. 让幽灵返回到它的书堆。在scr_Ghost_Step中,添加以下代码到玩家存在检查的大括号内:

else if (targetDist > myRange && distance_to_point(myBooks.x, myBooks.y) > 4)
{      
move_towards_point(myBooks.x, myBooks.y, mySpeed);
}

首先我们检查玩家是否超出范围,而幽灵又不靠近自己的书堆。在这里,我们使用distance_to_point,这样我们就是检查书堆的原点而不是distance_to_object会寻找的碰撞区域的边缘。如果这一切都是真的,幽灵将开始向它的书堆移动。

  1. 让我们再次运行游戏。和以前一样,幽灵会追逐玩家,如果玩家离得太远,幽灵将返回到它的书堆。

  2. 幽灵最终会在书堆的顶部来回移动,这是一个问题。这是因为幽灵具有基于速度的速度,并且没有任何代码告诉它停下来。我们可以通过在最后的else if语句后添加以下代码来解决这个问题:

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 (targetDist < myRange)
    {       
        move_towards_point(obj_Player.x, obj_Player.y, mySpeed);
    } else if (targetDist > myRange && 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;
        }
    }
}
  1. 需要一个最后的脚本,scr_Ghost_Alarm1,它附加在Alarm 1事件上,并有一行代码来移除实例:
instance_destroy();

幽灵几乎完成了。它生成,追逐玩家,然后返回到它的 BookPile,但是如果它抓住了玩家会发生什么?对于这个幽灵,我们希望它撞到玩家,造成一些伤害,然后在一团烟雾中消失。为此,我们需要为死去的幽灵创建一个新的资源。

  1. 创建一个新精灵spr_Ghost_Dead,并加载Chapter 4/Sprites/Ghost_Dead.gif,勾选删除背景

  2. 居中原点,然后点击确定

  3. 创建一个新的对象obj_Ghost_Dead,并应用该精灵。

  4. 在一个新的脚本scr_Ghost_Dead_AnimEnd中,编写以下代码并将其附加到动画结束事件上:

instance_destroy();

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

  1. 现在我们只需要重新打开obj_Ghost,并添加一个带有新脚本scr_Ghost_Collisionobj_Player事件,其中包含以下代码:
health -= 5;
myBooks.hasSpawned = false;
instance_create(x, y, obj_Ghost_Dead);
instance_destroy();

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

召唤幽灵图书管理员

  1. 运行游戏。现在,幽灵应该能够完全按照设计的方式运行。它会生成并追逐玩家。如果它抓住了玩家,它会造成伤害并消失。如果玩家逃脱,幽灵将返回到它的 BookPile 并消失。干得好!

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

  1. 打开Sandbox房间,并删除所有的 BookPiles 实例。

创建一个漫游的 Brawl

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

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

  1. 创建一个新精灵spr_Brawl_Small,并加载Chapter 4/Sprites/Brawl_Small.gif,勾选删除背景。这是正常状态的精灵。居中原点,然后点击确定

  2. 创建另一个新的精灵spr_Brawl_Large,并加载Chapter 4/Sprites/Brawl_Large.gif,勾选删除背景。我们需要将原点居中,以便 Brawl 能够正确缩放这个图像。愤怒状态是正常状态的两倍大小。

  3. 我们还需要在这两种状态之间进行转换,因此让我们创建一个新的精灵spr_Brawl_Change,并加载Chapter 4/Sprites/Brawl_Change.gif,仍然勾选删除背景。不要忘记居中原点。

  4. 接下来,我们需要一个 Brawl 要遵循的路径。创建一个新路径,并命名为pth_Brawl_01

  5. 我们希望 Brawl 移动起来更加平滑,因此在连接类型下勾选平滑曲线,并将精度更改为8

  6. 要看看我们可以用路径做些什么,让我们制作一个八字形状的路径,如下截图所示:创建一个漫游的 Brawl

  7. 让我们还创建一个新声音snd_Brawl,并加载Chapter 4/Sounds/Brawl.wav

  8. 创建一个新对象obj_Brawl,并将spr_Brawl_S应用为默认精灵。

  9. 我们将从一个创建事件脚本scr_Brawl_Create中初始化一些变量。

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 的路径上;当它到达路径的尽头时,它将循环,最重要的是,路径设置为绝对,这意味着它将按照路径编辑器中设计的方式运行。

  1. 现在我们可以开始处理 Brawl 的人工智能。为Step事件创建一个名为scr_Brawl_Step的新脚本,我们将从使移动工作开始。
image_angle = direction;
if (isBig == true)
{
    path_speed = mySpeed * 2;
} else {
    path_speed = mySpeed;
}

我们首先通过旋转 Sprite 本身来使其面向正确的方向。这将起作用,因为我们的 Sprite 图像面向右侧,这与零度相同。接下来,我们检查 Brawl 是否变大。如果 Brawl 是愤怒版本,我们将路径速度设置为基本速度的两倍。否则,我们将速度设置为默认的基本速度。

  1. 在房间的任何位置放置一个 Brawl 实例并运行游戏。Brawl 应该围绕数字八移动,并正确面向正确的方向。

  2. 接下来,我们将添加第一个变身,变得愤怒。在上一行代码之后,添加:

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 已经变大。

  1. 让我们为Alarm 0事件创建一个名为scr_Brawl_Alarm0的脚本,其中包含将切换到愤怒的 sprite 并指示 Brawl 现在已经完全大小的代码。
sprite_index = spr_Brawl_Large;
isBig = true;
  1. 运行游戏以确保代码正常工作。Brawl 应该保持小尺寸,直到能清楚看到玩家,此时它将变换为大型、愤怒的 Brawl。

  2. Brawl 正在变大,现在我们需要让它平静下来并缩小。在scr_Brawl_Step中,添加一个距离检查的else语句,该语句将位于最终大括号之前,并添加以下代码:

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;
        }
    }
}
  1. 复制scr_Brawl_Alarm0脚本,将其命名为scr_Brawl_Alarm1,并根据以下代码调整值。记得将其添加为Alarm 1事件。
sprite_index = spr_Brawl_Small;
isBig = false;
  1. 运行游戏并确认,当玩家接近并在视线范围内时,Brawl 会变得更大更快,并在超出范围时恢复正常。

  2. 我们唯一剩下的就是攻击。为obj_Player事件创建一个名为scr_Brawl_Collision的新脚本,其中包含以下代码:

if (isAttacking == false)
{
    health -= 10;
    alarm[2] = 60;
    isAttacking = true;
}

如果玩家第一次与 Brawl 碰撞,我们会减少 10 点生命值并设置一个两秒的警报,让 Brawl 可以再次攻击。

  1. 为了完成 Brawl,我们只需要最终的Alarm 2事件和一个新的脚本scr_Brawl_Alarm2,其中包含以下代码行:
isAttacking = false;

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

构建一个漫游的 Brawl

  1. Sandbox房间中删除任何obj_Brawl实例,以便我们可以为最终敌人重新开始。

创建教练

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

  1. 由于这个敌人正在守卫某物,我们将从创建奖杯开始。创建一个新的精灵,spr_Trophy,并加载Chapter 4/Sprites/Trophy.gif,勾选移除背景

  2. 创建一个新的对象,obj_Trophy,并将scr_Trophy应用为其精灵。

  3. 由于这是一个动画精灵,我们将添加一个创建事件,并通过在新脚本scr_Trophy_Create中编写以下代码来使其不进行动画:

image_speed = 0;
image_index = 0;
  1. 现在对于奖杯来说,这就是我们需要的全部,所以点击确定

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

  1. 创建一个新的精灵,spr_Coach_WalkRight,并加载Chapter 4/Sprites/Coach_WalkRight.gif,勾选移除背景

  2. 将原点居中,点击修改掩码,并在边界框下勾选完整图像

  3. 对于spr_Coach_LWalkLeftspr_Coach_WalkDownspr_Coach_WalkUp精灵,重复此过程。

  4. 创建一个新的对象,obj_Coach,并将spr_Coach_WalkRight应用为其精灵。

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

  1. 创建一个新的脚本,scr_Coach_Create,将其应用于创建事件,然后我们将从一些基本变量开始:
mySpeed = 4;
isChasing = false;
isWaiting = false;
isAvoiding = false;
isAttacking = false;
image_speed = 0.3;

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

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

  1. scr_Coach_Create中,将以下代码添加到脚本的末尾:
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 意味着这将适用于任何尺寸的房间,而无需调整代码。最后,我们将在房间中找到的所有墙的实例添加到网格中,作为不允许路径的区域。

  1. 现在,我们需要为教练找到一个目的地。继续在脚本的末尾添加以下代码:
nextLocation = irandom(instance_number(obj_Trophy)-1);
target = instance_find(obj_Trophy, nextLocation);
currentLocation = nextLocation;

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

  1. 现在我们所要做的就是创建路径并使用它。在脚本的末尾添加以下代码:
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); 
  1. 打开 Sandbox,在角落放置两个obj_Coach实例,以及三个obj_Trophy实例,如下截图所示:Creating the Coach

  2. 运行游戏。您应该看到教练们随机选择一个奖杯并朝它移动。尝试重新启动几次,看看每个教练所采取的不同路径。

  3. 基本设置完成后,我们可以继续进行 AI。我们将从根据移动方向切换精灵开始。创建一个新的脚本scr_Coach_Step,将其应用于Step事件,并编写以下代码:

if (direction > 45 && direction <= 135) { sprite_index = spr_Coach_WalkUp; }
else if (direction > 135 && direction <= 225) { sprite_index = spr_Coach_WalkLeft; }
else if (direction > 225 && direction <= 315) { sprite_index = spr_Coach_WalkDown; }
else { sprite_index = spr_Coach_WalkRight; }

在这里,我们根据实例移动的方向更改精灵。我们可以在这里做到这一点,因为我们不允许在路径上进行对角线移动。

  1. 接下来,我们将让教练观察玩家,如果被发现,他们将离开原来的路径进行追逐。在精灵更改代码之后添加以下代码:
targetDist = distance_to_object(obj_Player);
if (targetDist < 150  && targetDist > 16)
{
    canSee = collision_line(x, y, obj_Player.x, obj_Player.y, obj_Wall, false, false)
    if (canSee == noone)
    {
        path_end();
        mp_potential_step(obj_Player.x, obj_Player.y, 4, all);
        isChasing = true;
    }
 }

我们再次使用一个变量来保存玩家距离的值,以节省编码时间并最小化函数调用。如果玩家在范围内且不在攻击距离内,我们进行视线检查。collision_line函数返回线穿过的任何墙实例的 ID。如果它不与任何墙实例相交,它将返回一个名为noone的特殊变量。如果玩家在视线中,我们结束教练正在遵循的路径,并开始朝玩家移动。mp_potential_step函数将使对象朝着期望的方向移动,同时避开障碍物,在这种情况下,我们避开所有实例。最后,我们指示教练正在追逐玩家。

  1. 这对于开始追逐很有效,但是如果玩家逃脱了怎么办?让教练等待一会儿,然后回到巡逻。在进行视线检查的else语句中添加以下代码:
else if (canSee != noone && isChasing == true)
{
    alarm[0] = 60;
    isWaiting = true;
    isChasing = false;
}

这个else语句表示,如果玩家看不见并且教练正在追逐,它将设置一个警报以寻找新目的地,告诉它等待,追逐结束。

  1. 我们设置了一个警报,因此让我们创建一个新的脚本scr_Coach_Alarm0,并将其应用于Alarm 0事件。在脚本中写入以下代码:
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循环来检查下一个位置是否与旧位置相同。这将确保教练总是移动到另一个奖杯。就像我们在初始设置中所做的那样,我们选择一个新的目标并设置当前位置变量。我们还创建一个路径并开始在其上移动,这意味着教练不再等待。

  1. 我们还需要添加一个元素到追逐序列中,即攻击。如果教练靠近玩家,它应该对玩家进行近战攻击。为此,我们首先需要创建一个新的精灵spr_Coach_Attack,加载Chapter 4/Sprites/Coach_Attack.gif并勾选Remove Background

  2. 就像玩家的攻击一样,将Origin设置为X:-16Y:24,并调整Bounding Box的值为Left:0Right:24Top:0,和Bottom:4

  3. 创建一个新的对象obj_Coach_Attack,应用精灵,并将Depth设置为-100

  4. 添加一个Create事件,并应用一个新的脚本scr_Coach_Attack_Create,其中包含控制动画速度的代码,设置一个用于移除实例的警报,并一个我们可以打开的变量。

image_speed = 0.3;
alarm[0] = 6;
isHit = false;
  1. 使用新的脚本scr_Coach_Attack_Alarm0添加一个Alarm 0事件,该脚本会移除实例。
instance_destroy();
  1. 最后,添加一个obj_Player事件,并应用一个新的脚本scr_Coach_Attack_Collision,其中包含以下代码:
if (isHit == false)
{
    health -= 15;
    isHit = true;
}

如果这是第一次碰撞,我们减少一点生命值,然后停用此检查。

  1. 攻击已经完成。现在要在教练中激活它,重新打开scr_Coach_Step,并在最后的大括号后添加攻击代码作为else if语句:
else if (targetDist <= 16)
{
    if (isAttacking == false)
    {
        swing = instance_create(x, y, obj_Coach_Attack);
        swing.image_angle = direction;
        alarm[1] = 90;
        isAttacking = true;
    }
}

如果教练靠近玩家但尚未发动攻击,我们创建一个教练攻击的实例。然后我们旋转攻击精灵,使其面向与教练相同的方向。设置一个三秒的闹钟,以便在再次运行此代码之前有时间喘口气。

  1. 我们需要一个Alarm 1事件来重置攻击,因此创建一个新脚本,scr_Coach_Alarm1,并关闭攻击。
isAttacking = false;
  1. 运行游戏。现在教练会追逐玩家,如果它靠近玩家足够近,它就会发动攻击。

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

  1. 重新打开scr_Coach_Step,并在脚本的最后添加一个else语句,包含以下代码:
else 
{
    if (isWaiting == false)
    {
        if (distance_to_object(target) <= 8) 
        {
            alarm[0] = 60;
            path_end();
            isWaiting = true;
        }
    }
}

这个else语句表示玩家超出范围。然后我们检查教练是否在等待。如果它不在等待,但距离目标奖杯不到八个像素,我们设置两秒钟的选择新目的地的闹钟,结束路径以停止移动,并声明我们现在在等待。

  1. 运行游戏,你会看到教练在不追逐玩家时,停在奖杯附近,停顿片刻,然后移动到另一个奖杯。

  2. 然而,如果两个教练都去同一个奖杯,就会出现问题。让我们通过在检查奖杯的距离后添加以下代码来解决这个问题:

if (isAvoiding == true)
{
     mp_potential_step (target.x, target.y, 4, all);
}

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

  1. 现在,我们需要设置避让发生的条件。在最后的代码之后立即插入以下内容:
 if (distance_to_object(obj_Coach) <= 32 && isAvoiding == false)
 {
     path_end();
     isAvoiding = true;
 }
 else if (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;
 }

首先,我们检查教练实例是否附近,且尚未尝试避让。如果是,则我们让教练脱离路径并开始避让。接着是一个else if语句,检查我们是否与另一个教练足够远,以便我们可以避让。如果是,我们为目的地设置一个新路径,开始移动,并结束避让。

  1. 还有一个小问题尚未解决,如果你运行游戏一段时间就会发现。有时两辆教练会靠得太近,它们就会停下来。这是因为它们试图避开彼此,但实际上它们是在接触并且无法分开。在scr_Coach_Step脚本的最后,写入以下内容:
if (place_meeting(x, y, obj_Coach))
{
    x = xprevious;
    y = yprevious;
    mp_potential_step(target.x, target.y, 4, all);
}

这将检查两个教练实例是否相互碰撞。如果是,我们将xy坐标设置为特殊变量xpreviousyprevious,它们代表实例在上一步中的位置。一旦它们退后一步,我们就可以再次尝试绕过它们。

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

if (direction > 45 && direction <= 135) { sprite_index = spr_Coach_WalkUp; }
else if (direction > 135 && direction <= 225) { sprite_index = spr_Coach_WalkLeft; }
else if (direction > 225 && direction <= 315) { sprite_index = spr_Coach_WalkDown; }
else { sprite_index = spr_Coach_WalkRight; }

targetDist = distance_to_object(obj_Player);
if (targetDist < 150  && targetDist > 16)
{
    canSee = collision_line(x, y, obj_Player.x, obj_Player.y, obj_Wall, false, false)
    if (canSee == noone)
    {
        path_end();
        mp_potential_step(obj_Player.x, obj_Player.y, 4, all);
        isChasing = true;
    }
    else if (canSee != noone && isChasing == true)
    {
        alarm[0] = 60;
        isWaiting = true;
        isChasing = false;
    }
}
else if (targetDist <= 16)
{
    if (isAttacking == false)
    {
        swing = instance_create(x, y, obj_Coach_Attack);
        swing.image_angle = direction;
        alarm[1] = 90;
        isAttacking = true;
    }
}
else 
{
    if (isWaiting == false)
    {
        if (distance_to_object(target) <= 8)
        {
            alarm[0] = 60;
            path_end();
            isWaiting = true;
        }
        if (isAvoiding == true)
        {
            mp_potential_step(target.x, target.y, 4, all);
        }
        if (distance_to_object(obj_Coach) <= 32 && isAvoiding == false)
        {
            path_end();
            isAvoiding = true;
        }
        else if (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;
        }
    }
}
if (place_meeting(x, y, obj_Coach))
{
    x = xprevious;
    y = yprevious;
    mp_potential_step(target.x, target.y, 4, all);
}

为游戏添加最后的细节

游戏现在在功能上已经完成,但还有一些元素需要完善。首先,玩家会受到伤害,但从不会死亡,也没有头顶显示HUD)来显示这一点。让我们快速创建一个 Overlord。

  1. 创建一个新对象,obj_Overlord,不应用精灵并检查持久性。

  2. 添加一个Draw GUI事件和一个新的脚本,scr_Overlord_DrawGUI,其中包含以下代码:

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 本身。移除世界中的任何持久实例是很重要的,否则它们就不会消失!

  1. 将一个obj_Overlord的实例放入C04_R01中。

  2. 用各种敌人填充房间。如果我们使用 Brawl,我们要么需要创建一个适用于我们创建的路径的房间,要么更好的是重新绘制路径以适应我们的房间布局。

  3. 确保Sandbox房间被移回到资源树的底部并运行游戏。我们应该在屏幕顶部看到健康条,如果受到伤害,健康条应该下降。如果玩家受到了太多伤害,游戏将结束并返回到前端。

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

为游戏添加最后的细节

摘要

恭喜你完成了你的第二个游戏!我们学会了通过将键盘检查和碰撞预测放入一个脚本来简化玩家控制。我们涵盖了处理精灵动画的几种方法,从旋转图像到设置应该显示哪些精灵。我们处理了全局变量,并用它们来实现了一个房间过渡系统。我们深入讨论了一些新的对象属性和持久性。然后我们花了一些时间处理人工智能,通过接近检测和路径查找。我们甚至发现了如何使一个对象在避开障碍物的同时自己导航到一个房间。

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

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

第五章:平台乐趣

现在我们对构建游戏的基础有了很好的基础,我们准备创建更复杂和更高效的项目。在本章中,我们将开发一个经典的平台游戏,其中包括一场史诗般的 Boss 战。我们将专注于构建系统,并利用可重复使用的脚本简化我们的代码并节省时间。这也将减少游戏的总体大小,使其下载速度更快。

游戏本身将包括一个玩家角色,可以在世界中奔跑,跳上平台,并朝多个方向射击。玩家需要击败一个巨型机器 Boss,它将有三个不同的阶段。在第一阶段,玩家需要摧毁三门暴露一小段时间的强大火炮。第二阶段需要摧毁一个大型激光炮,它会上下移动,不时地发射巨大的激光束。最后一个阶段将有护盾保护 Boss 核心,偶尔打开以允许玩家消灭 Boss 的核心。所有这些都将发生在玩家试图避免被一门不可摧毁的炮塔击中的情况下。

构建基于系统的代码结构

制作游戏时,通常会单独构建每个组件,而不考虑它将如何影响整个游戏。开发人员将构建一个基本框架,然后在需要时添加功能,通常会使用特殊的条件语句使代码能够正常工作而不破坏游戏。这种方法最终会在软件中产生错误,需要更多的时间和精力来修复每一个错误。游戏越大,出现问题的可能性就越大。这可能是一种令人沮丧的经历。

将代码分解为单独的系统可以真正节省时间和精力。我们可以将代码的各个元素写入脚本中,以便共享,而不是为每个对象一遍又一遍地重写代码。对于这个游戏,我们将把一些更基本的组件,比如重力和动画,分离成它们自己的系统。

创建重力

我们要构建的第一个系统是处理重力的系统。虽然 GameMaker: Studio 确实有一个重力属性,但在平台游戏中并不需要这种复杂性。重力是一个作用于物体速度的力,这意味着物体下落的时间越长,速度就越快。我们的问题是将重力设置为零只意味着它不会移动得更快。我们需要物体完全停下来。因此,我们将创建自己的重力系统,不仅使物体下落,还将处理着陆的情况。我们将创建自己的重力系统,不仅使物体下落,还将处理着陆的情况。

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

  1. 让我们开始创建一个名为Chapter_03新项目

  2. 打开资源 | 定义常量编辑器。在名称列中写入MAXGRAVITY16。以这个速度,我们可以确保下落的物体不会移动得太快,以至于错过游戏中另一个物体的边界框。从现在开始,每当我们看到MAXGRAVITY,计算机将看到16

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

  1. 接下来,我们可以创建一个新的脚本,scr_Gravity,并编写以下代码来创建重力:
if (place_free( x, y + vspeed + 1))
{
    vspeed  += 1;
} else {    
    move_contact_solid(direction, MAXGRAVITY);
    vspeed = 0;
}

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

  1. 现在我们已经让重力起作用了,但如果我们不限制实例下落的速度,它将会加速得太快。将以下代码添加到脚本的底部:
vspeed = min(vspeed, MAXGRAVITY);

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

构建动画系统

我们将创建的下一个系统是动画系统,它将作为状态机实现。状态机将所有对象的条件分解为不同的状态。一个对象在任何时候只能处于一个阶段,因此与之相关的代码可以更有效地被包含和管理。

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

构建动画系统

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

  • 空闲

  • 空闲向上瞄准

  • 空闲向下瞄准

  • 奔跑

  • 奔跑向上瞄准

  • 向下瞄准

  • 在空中

  • 伤害

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

  1. 打开资源 | 定义常量编辑器,在名称列中写入IDLE0

  2. 点击添加或直接按Enter添加新行,并写入IDLEUP,值为1。重复这个过程,为所有状态添加递增的数字,如下截图所示。然后点击确定构建动画系统

  3. 创建一个新的脚本,命名为scr_Animation_Control。我们将首先使用switch语句来控制各种状态。我们还希望这个脚本是可重用的,所以我们将使用一些通用变量来使代码更通用。让我们首先添加空闲状态的以下代码:

switch (action)
{
    case IDLE :
        sprite_index = myIdle;
        image_speed = 0.1;
    break;
}

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

  1. 我们需要将所有的情况插入到这个脚本中,并设置改变精灵和设置图像播放速度的类似设置。以下是其他状态的代码:
    case IDLEUP :
        sprite_index = myIdleUp;
        image_speed = 0.1;
    break;
    case IDLEDOWN :
        sprite_index = myIdleDown;
        image_speed = 0.1;
    break;
    case RUN :
        sprite_index = myRun;
        image_speed = 0.5;
    break; 
    case RUNUP :
        sprite_index = myRunUp;
        image_speed = 0.5;
    break; 
    case RUNDOWN :
        sprite_index = myRunDown;
        image_speed = 0.5;
    break; 
    case INAIR :
        sprite_index = myInAir;
        image_speed = 0.5;
    break; 
    case DAMAGE :
        sprite_index = myDamage;
        image_speed = 0.5;
    break; 
  1. 我们已经拥有了所有需要的状态,但是如何处理玩家面对的方向呢?这是一个平台游戏,所以他们需要向左和向右移动。为此,我们将通过以下代码在 switch 语句结束括号后翻转图像:
image_xscale = facing;

我们再次利用一个变量facing,使脚本更通用。我们现在已经完成了这个脚本,动画系统已经准备好实施了。

创建碰撞预测系统

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

  1. 我们将从预测实例左右两侧的墙壁碰撞开始。创建一个新的脚本,scr_Collision_Forecasting,并写入以下代码:
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,否则该区域被阻塞,我们将停止实例的水平速度。然后我们重复此检查以检查右侧的碰撞。

  1. 接下来我们需要检查地面碰撞。在上一段代码之后,我们需要添加:
if (!place_free(x, y+1))
{
    isOnGround = true;
    vspeed = 0;
    action = IDLE;
} else {
    isOnGround = false;
}

在这里,我们正在检查实例正下方是否有可碰撞的对象。如果发生碰撞,我们将变量isOnGround设置为true,以停止垂直速度,然后将实例的状态更改为IDLE。像这样更改状态将确保实例从INAIR状态中逃脱。

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

  1. 与其为所有角度构建条件检查,我们将允许碰撞发生,然后将其弹回到正确的位置。在脚本的末尾添加下面的代码:
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;
}

if place_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,并写入以下代码:

keyLeft  = keyboard_check(vk_left);
keyRight  = keyboard_check(vk_right);
keyDown  = keyboard_check(vk_down);
keyUp  = keyboard_check(vk_up);
keyJump = keyboard_check(ord('X'));
keyShoot = keyboard_check(ord('Z'));

我们的代码将更容易阅读,例如使用keyJumpkeyShoot等变量来表示控件,而不是实际的键名。为了在键盘上使用字母键,我们需要相关的 ASCII 编号。我们可以使用ord函数,而不必查找每个键的编号,它将把字母转换为正确的数字。

注意

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

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

构建玩家

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

  1. 创建一个新精灵,spr_Player_Idle,并加载Chapter 5/Sprites/Player_Idle.gif,勾选删除背景

  2. 原点设置为X32 Y63,使其在水平中心和垂直底部休息。

  3. 单击修改蒙版以打开蒙版属性编辑器,并选择边界框|手动。将值设置为1648863

  4. 重复此过程,包括以下精灵的相同原点蒙版属性

  • spr_Player_IdleUp

  • spr_Player_IdleDown

  • spr_Player_Run

  • spr_Player_RunUp

  • spr_Player_RunDown

  • spr_Player_InAir

  • spr_Player_Damage

  1. 创建一个对象,obj_Player,并将spr_Player_Idle分配为精灵

  2. 首先,我们需要初始化玩家角色所需的所有变量,从必要的动画变量开始。创建一个新脚本,scr_Player_Create,并使用以下代码:

myIdle = spr_Player_Idle;
myIdleUp = spr_Player_IdleUp;
myIdleDown = spr_Player_IdleDown;
myRun = spr_Player_Run;
myRunUp = spr_Player_RunUp;
myRunDown = spr_Player_RunDown;
myInAir = spr_Player_InAir;
myDamage = spr_Player_Damage;

在这里,我们正在确定要用于各种动画状态的精灵。我们在这里使用的变量必须与我们在scr_Animation_Control中声明的变量相同,以便使用我们创建的动画系统。

  1. 接下来,我们将为碰撞系统添加变量,但在这之前,我们应该添加两个用于面向方向的常量。打开资源|定义常量,并添加RIGHT,值为1,和LEFT,值为-1。这些数字将代表绘制图像的比例,负值将反转精灵。

  2. scr_Player_Create的末尾添加我们需要的其余变量:

mySpeed = 8;
myAim = 0;
facing = RIGHT;
action = IDLE;
isDamaged = false;
canFire = true;

这里有玩家速度、玩家瞄准方向、玩家面向方向和玩家状态的变量。我们还添加了玩家是否能受到伤害或无敌,以及是否能射击的变量。现在我们已经初始化了所有变量。

  1. obj_Player中,添加一个创建事件并应用scr_Player_Create脚本。

  2. 我们已经准备好了一个碰撞预测系统,我们只需要适当地使用它。创建一个新脚本,scr_Player_BeginStep,并使用它来调用预测脚本和键盘检查:

scr_Collision_Forecasting();
scr_Keyboard_Input();

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

  1. obj_Player中添加一个步骤|开始步骤事件,并应用scr_Player_BeginStep开始步骤事件是每个步骤中要执行的第一个事件。步骤事件紧随其后,结束步骤是在实例被绘制在屏幕上之前的最后一个事件。这使我们能够更好地控制代码的运行时间。

  2. 接下来,我们需要创建控件。正如我们之前提到的,实际上有两个独立的控制系统,一个用于在地面上,一个用于在空中。我们将从后者开始,因为它最简单。创建一个新脚本,命名为scr_Player_AirControls,并使用以下代码:

scr_Gravity();

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

您应该注意到的第一件事是,我们不再在代码中使用==等运算符。这些变量都是布尔变量,因此它们只能是真或假。编写keyLeft与编写keyLeft == true是相同的,但更有效率。

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

  1. 现在我们可以转向更加复杂的地面控制。创建一个新的脚本,命名为scr_Player_GroundControls。我们将从编写空闲状态开始:
if (!keyLeft && !keyRight) 
{
    if (hspeed >= 1) { hspeed -= 1; }
    if (hspeed <= -1) { hspeed += 1; }
}

我们首先检查左右键是否都没有被按下。如果键没有被按下而玩家正在移动,我们就检查他们的移动方向,然后相应地减少速度。这实际上意味着玩家会滑行停下来。

  1. 玩家已经停下来,但还没有进入空闲状态。为了做到这一点,我们需要确定玩家是否正在使用上下键,因为这将影响玩家瞄准的方向。在最后一行代码之后,但在最后一个大括号内立即插入下一个代码:
if (keyUp) 
{ 
    action = IDLEUP; 
    myAim = 45;
} else if (keyDown) {   
    action = IDLEDOWN; 
    myAim = 315;
} else { 
    action = IDLE;
    if (facing == LEFT) { myAim = 180; }
    if (facing == RIGHT) { myAim = 0; }
}

我们首先检查上键是否被按下,如果是,我们将动作更改为IDLEUP,并将瞄准设置为 45 度,这样玩家就会向上射击。如果不是,我们检查下键,如果合适的话,更改动作和瞄准。最后,如果这两个键都没有被按下,我们就进入标准的IDLE状态。不过,对于瞄准,我们需要先看一下玩家面对的方向。从现在开始,玩家将正确地进入空闲状态。

  1. 接下来我们可以添加左右控制。在最后一个大括号之后,写下以下代码:
if (keyLeft && canGoLeft)
{
    hspeed = -mySpeed;
    facing = LEFT;
    if (keyUp) 
    { 
        action = RUNUP; 
        myAim = 150; 
    } else if (keyDown) {
        action = RUNDOWN;
        myAim = 205; 
    } else { 
        action = RUN;
        myAim = 180;
    }
}

我们检查左键是否被按下,以及玩家是否能够向左移动。如果是,我们就设置水平速度,并将面向方向设置为向左。再次检查当前是否按下了上下键,然后将动作和瞄准设置为适当的值。

  1. 使用相应的值重复上一步,为右键添加相同的检查。玩家现在可以向左和向右移动了。

  2. 现在我们只需要添加跳跃。在上一个代码之后立即添加:

if (keyJump && isOnGround)
{
    vspeed = -MAXGRAVITY;
    action = INAIR;
}

我们检查跳跃键是否被按下,以及玩家是否在地面上。如果是,我们就将垂直速度向上设置为最大重力,并将动作设置为INAIR

  1. 地面控制现在已经完成;这就是scr_Player_GroundControls应该看起来的样子:
if (!keyLeft && !keyRight)
{
    if (hspeed >= 1) { hspeed -= 1; }
    if (hspeed <= -1) { hspeed += 1; }

    if (keyUp) 
    { 
        action = IDLEUP; 
        myAim = 45;
    } else if (keyDown) {   
        action = IDLEDOWN; 
        myAim = 315;
    } else { 
        action = IDLE;
        if (facing == LEFT) { myAim = 180; }
        if (facing == RIGHT) { myAim = 0; }
    }
}
if (keyLeft && canGoLeft)
{
    hspeed = -mySpeed;
    facing = LEFT;
    if (keyUp) 
    { 
        action = RUNUP; 
        myAim = 150; 
    } else if (keyDown) { 
        action = RUNDOWN; 
        myAim = 205; 
    } else { 
        action = RUN; 
        myAim = 180; 
    }
}
if (keyRight && canGoRight)
{
    hspeed = mySpeed;
    facing = RIGHT;
    if (keyUp) 
    { 
        action = RUNUP; 
        myAim = 30;
    } else if (keyDown) { 
        action = RUNDOWN; 
        myAim = 335;
    } else { 
        action = RUN; 
        myAim = 0;
    }
}
if (keyJump && isOnGround)
{
    vspeed = -MAXGRAVITY;
    action = INAIR;
}
  1. 让我们继续进行玩家攻击。首先我们需要构建子弹,所以创建一个新的精灵,spr_Bullet,并加载Chapter 5/Sprites/Bullet.gif,勾选去除背景。居中原点,然后点击确定

  2. 创建一个新的对象,obj_Bullet,并将spr_Bullet应用为精灵

  3. 我们希望子弹始终在所有物体的前面,所以将深度设置为-2000

  4. 我们现在已经完成了子弹,可以编写攻击代码了。创建一个新的脚本,scr_Player_Attack,并写下以下内容:

if (keyShoot && canFire)  
{
    bullet = instance_create(x + (8 * facing), y-32, obj_Bullet) 
    bullet.speed = 16;
    bullet.direction = myAim;
 bullet.image_angle = myAim;
    alarm[0] = 10;
    canFire = false;
}

我们首先检查攻击键是否被按下,以及玩家是否被允许射击。如果是,我们就从枪口创建一个子弹实例,并将唯一的 ID 捕获到一个变量中。这个子弹的水平位置使用面向变量来偏移它向左或向右。我们设置子弹的速度,然后设置方向和图像旋转到玩家瞄准的位置。然后我们设置一个警报,用于重置canFire变量,我们将其更改为false

  1. 此时,我们已经有了几个用于移动、攻击和动画的脚本,但还没有应用它们。为了做到这一点,我们需要另一个脚本,scr_Player_Step,调用其他脚本如下:
if (isOnGround)
{
    scr_Player_GroundControls();
} else {
    scr_Player_AirControls();
}
scr_Player_Attack();
scr_Animation_Control();

首先,我们通过检查玩家是否在地面上来确定需要使用哪些控制。然后我们运行适当的控制脚本,然后是攻击脚本,最后是动画控制。

  1. obj_Player中,添加一个Step | Step事件,并应用scr_Player_Step

  2. 在测试之前,我们仍然需要重置那个警报。创建一个新脚本,scr_Player_Alarm0,并将canFire设置为true

canFire = true;
  1. 添加一个Alarm | Alarm 0事件,并应用此脚本。

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

构建玩家

设置房间

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

  1. 创建一个新的精灵,spr_Ground,并加载Chapter 5/Sprites/Ground.gif,不勾选Remove Background。点击OK

  2. 创建一个新对象,obj_Ground,并将spr_Ground分配为Sprite

  3. 勾选Solid框。这是必要的,因为我们的碰撞代码正在寻找实心物体。

  4. 让我们来测试一下。创建一个新房间,在Settings选项卡下,将名称更改为BossArena,将Width更改为800。我们希望有一个足够大的房间来进行战斗。

  5. 在房间的边界周围添加obj_Ground的实例。还在房间的地板附近添加一个obj_Player的单个实例。

  6. 运行游戏。此时,玩家应该能够在房间内奔跑和跳跃,但不能穿过墙壁或地板。您还应该能够以各种方向射击武器。还要注意,动画系统正在按预期工作,精灵根据玩家的动作而改变。

  7. 现在来构建平台。创建一个新的精灵,spr_Platform,并加载Chapter 5/Sprites/Platform.gif,不勾选Remove Background。点击OK

  8. 创建一个新对象,obj_Platform,并将spr_Platform分配为Sprite

  9. 我们希望平台只在玩家在其上方时才是实心的。为此,我们需要创建一个新脚本,scr_Platform_EndStep,其中包含以下代码:

if (obj_Player.y < y) 
{
    solid = true;
} else {
    solid = false;
}

在这里,我们将玩家的 Y 坐标与实例的 Y 坐标进行比较。如果玩家在上面,那么平台应该是实心的。否则它不是实心的,玩家可以跳过它。

  1. obj_Platform中,添加一个Step | End Step事件,并应用此脚本。我们在步骤结束时运行此代码,因为我们只想在玩家实际移动之后,但在它进行另一个预测之前进行更改。

  2. 返回到BossArena并添加一些玩家可以跳上的平台。玩家只能跳大约 128 像素,因此确保平台放置得当,如下所示。设置房间

  3. 运行游戏。玩家应该能够跳过平台并站在上面。

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

构建 Boss 战

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

我们的 boss 战将不是与另一个角色对抗,而是与一座堡垒对抗。第一阶段将包括三门可伸缩的大炮,它们将在房间各处发射炮弹。必须摧毁所有三门大炮才能进入第二阶段。第二阶段将有一门强大的激光炮,它将上下移动并发射全屋范围的激光束,玩家需要避开。最后一阶段将是摧毁由两个护盾保护的 boss 核心。护盾只会在短时间内打开。在整个 boss 战中,将有一把不可摧毁的枪,它将在房间中的任何位置向玩家射击子弹。随着每个阶段的进行,这把枪将射击得更加频繁,使游戏更具挑战性。让我们开始建立 boss!

创建不可摧毁的枪

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

  1. 让我们从构建枪子弹开始。创建一个新精灵,spr_Gun_Bullet,并加载Chapter 5/Sprites/Gun_Bullet.gif,勾选删除背景。将原点居中,然后点击确定

  2. 创建一个新对象,obj_Gun_Bullet,并将spr_Gun_Bullet分配为精灵

  3. 我们希望子弹始终出现在地面和平台的上方。将深度设置为-2000

  4. 枪子弹将在接触时对玩家造成伤害,所有其他抛射物也是如此。让我们再次建立一个所有武器都可以使用的单一系统。创建一个新脚本,scr_Damage,其中包含以下代码:

if (obj_Player.action != DAMAGE)
{
    health -= myDamage;
    with (obj_Player) 
    { 
        y -= 1;
        vspeed = -MAXGRAVITY;
        hspeed = 8 * -facing;
        action = DAMAGE;
        isDamaged = true; 
    }
}

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

  1. 创建另一个新脚本,scr_Gun_Bullet_Create,并初始化myDamage变量。然后将其应用于obj_Gun_Bullet创建事件。
myDamage = 5;
  1. 接下来让我们创建一个碰撞脚本,scr_Gun_Bullet_Collision,它调用伤害脚本并移除子弹。我们没有将实例的销毁放入scr_Damage中,这样我们就可以选择无法被摧毁的武器使用这个脚本:
scr_Damage();
instance_destroy();
  1. 现在我们可以在附有此脚本的obj_Gun_Bullet上添加一个碰撞|obj_Player事件。枪子弹现在已经完成。

  2. 现在我们可以移动到枪本身。首先创建两个新的精灵,spr_Gun_Idlespr_Gun_Run。加载Chapter 5/Sprites/Gun_Idle.gifChapter 5/Sprites/Gun_Run.gif到它们关联的精灵中,勾选删除背景

  3. 枪精灵的枪管朝右,所以我们需要在左侧设置原点,以便正确地进行旋转。在两个精灵上将原点设置为X:0Y:16,然后点击确定

  4. 创建一个新对象,obj_Gun,并将spr_Gun_Idle分配为精灵

  5. 我们希望确保枪始终在 boss 的视觉上方,所以将深度设置为-1000

  6. 我们需要在一个新脚本scr_Gun_Create中初始化一些变量,然后将其添加到obj_Gun作为创建事件:

action = IDLE;
facing = RIGHT;
tipOfGun = sprite_width;
canFire = false;
delay = 90;
alarm[0] = delay;

myIdle = spr_Gun_Idle;
myRun = spr_Gun_Run;

我们将在这里使用动画系统,因此需要设置所需的动作和面向变量的值。以下四个变量与枪的射击有关。首先是tipOfGun,用于确定枪口的位置,canFire是触发器,delay是射击间隔时间,警报将发射枪子弹。最后,我们有两种动画状态需要应用。除非对象使用该状态,否则我们不需要添加所有其他变量,如myDamage

  1. 接下来,我们将让枪跟踪玩家并确定何时射击。创建一个新的脚本,scr_Gun_Step,将其放置在步骤 | 步骤事件中。以下是我们需要的代码:
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。最后,我们通过根据枪和玩家之间的角度旋转精灵来跟踪玩家。

  1. 我们在这个对象上使用了两个警报。第一个警报开始射击动画,第二个创建枪子弹。让我们从第一个警报开始,创建一个新的脚本,scr_Gun_Alarm0,用于警报 | 警报 0事件:
canFire = true;
  1. 第二个警报包含了开枪的代码。创建一个新的脚本,scr_Gun_Alarm1,将其添加为警报 | 警报 1事件:
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_xlengthdir_y来为我们进行数学计算。它所需要的只是径向距离和角度,然后我们可以将其添加到枪的本地坐标中。一旦我们有了这些变量,我们就可以在正确的位置创建子弹,设置其速度和方向。最后,我们重置第一个警报,以便枪再次开火。

  1. 我们准备测试枪。打开 BossArena 并在房间的最右侧放置一把枪的实例。一旦测试完成,我们将从房间中移除枪,因此此时确切的放置位置并不重要。创建不可摧毁的枪

  2. 运行游戏。枪应该会跟随玩家在房间中的任何位置,并且每三秒发射一次枪子弹。如果玩家被枪子弹击中,他们将被击飞并受到伤害动画的影响,就像在之前的截图中看到的那样。

  3. 然而,玩家的伤害状态存在一个问题;玩家仍然可以移动和射击。这对于被射击并不是多大的威慑力,因此让我们解决这个问题。创建一个新的脚本,scr_Player_Damage,其中包含以下代码:

if (isOnGround)
{
    isDamaged = false;
} else {
    scr_Gravity();
}

我们检查玩家是否在地面上,因为这将停用伤害状态。如果玩家在空中,我们施加重力,就这样。

  1. 现在我们需要调用这个脚本。重新打开scr_Player_Step,并添加一个条件语句,用于判断玩家是否受伤。以下是包含新代码的整个脚本,新代码用粗体标出:
if (isDamaged)
{
 scr_Player_Damage();
} else {
    if (isOnGround)
    {
        scr_Player_GroundControls();
    } else {
        scr_Player_AirControls();
    }
    scr_Player_Attack(); 
}
scr_Animation_Control();

我们检查玩家是否处于伤害模式,如果是,我们运行伤害脚本。否则,我们像平常一样使用所有控制系统在else语句中。无论是否受伤,动画脚本都会被调用。

  1. 运行游戏。现在当玩家被击中时,冲击效果非常明显。

构建第一阶段:大炮

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

  1. 从 Cannonball 开始,创建一个新的精灵spr_Cannonball,并加载Chapter 5/Sprites/Cannonball.gif,勾选Remove Background

  2. Origin设置为X:12Y:32,然后点击OK

  3. 创建一个新的对象obj_Cannonball,并将spr_Cannonball分配为Sprite

  4. Depth设置为-900,这样它将出现在大多数对象的前面。

  5. 为了使用伤害系统,我们需要在Create事件中设置正确的变量,使用一个新的脚本scr_Cannonball_Create

myDamage = 10;
hspeed = -24;

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

  1. 如果 Cannonball 接触到玩家,我们不会摧毁它,所以我们只需要在Collision | obj_Player事件中应用scr_Damage。Cannonball 现在已经准备好被射击。

  2. 大炮将需要五个精灵,spr_Cannon_IdleDownspr_Cannon_IdleUpspr_Cannon_RunDownspr_Cannon_RunUpspr_Cannon_Damage。从Chapter 5/Sprites/文件夹加载相关文件,不勾选Remove Background

  3. 创建一个新的对象obj_Cannon,并将spr_Cannon_IdleDown分配为Sprite

  4. Depth设置为-1000,这样大炮将位于其他 Boss 部件的前面。

  5. 像往常一样,让我们创建一个新的脚本scr_Cannon_Create,在Create事件中初始化所有变量。

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变量来跟踪伤害。然后通过面向右侧来设置动作状态,因为我们不会翻转精灵,并建立一个射击变量。然后我们有了大炮工作所需的所有动画状态。

  1. 接下来我们可以创建一个新的脚本scr_Cannon_Step,在Step | Step事件中实现切换状态和发射 Cannonballs 的功能:
scr_Animation_Control();

if (image_index > image_number-1)
{
    if (action == RUNUP) { action = IDLEUP;}
    else if (action == RUNDOWN) { action = IDLEDOWN;} 
}

if (canFire) 
{
    action = RUNUP;
    alarm[0] = 60;
    canFire = false;
}

if (myHealth <= 0)
{
    instance_destroy();
}

与枪类似,我们首先调用动画系统脚本。然后检查大炮是否在动画的最后一帧。这里有两种不同的空闲状态,取决于大炮是否暴露出来。我们检查我们处于哪种状态,并设置适当的空闲状态。接下来,我们检查大炮是否应该射击,如果应该,我们就会暴露大炮,并设置一个警报,在两秒后创建 Cannonball。最后,我们进行健康检查,如果大炮没有生命力了,它就会从游戏中移除。

  1. 创建一个新的脚本scr_Cannon_Alarm0,并将其添加到Alarm | Alarm 0事件中,使用以下代码:
instance_create(x, y, obj_Cannonball);
action = RUNDOWN;

在这里我们只是创建一个 Cannonball,然后设置动画以收回大炮。

  1. 大炮的最后一件事是承受伤害。创建一个新的脚本scr_Cannon_Collision,并将其应用到Collision | obj_Bullet事件中,使用以下代码:
if (action == IDLEUP)
{
    myHealth -= 10;
    action = DAMAGE;
    with (other) {instance_destroy();}
}

我们首先确保只有在大炮暴露时才会应用伤害。如果是的话,我们就会减少它的 10 点生命值,切换到伤害动画,并移除子弹。大炮现在已经完成。

  1. 在我们尝试测试大炮之前,我们将开始构建 Boss。大炮不能自行运行,而是由 Boss 控制。创建一个名为obj_Boss的新对象。没有精灵可分配,因为 Boss 由其他对象组成。

  2. 创建一个新的脚本scr_Boss_Create,在Create事件中初始化变量:

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 可以控制它们。

  1. 我们希望大炮按顺序射击,而不是一起射击。为此,我们将使用时间轴。创建一个新的时间轴并命名为tm_Boss_Phase01

  2. 添加一个时刻,并将指示时刻设置为180。这将在战斗开始后的六秒钟内开始。

  3. 创建一个新的脚本,scr_Phase01_180,并发射中间的大炮。将此脚本应用于时间轴:

if (instance_exists(cannonB)) { cannonB.canFire = true;}

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

  1. 360处添加另一个时刻

  2. 创建一个脚本,scr_Phase01_360,并激活另外两门大炮:

if (instance_exists(cannonA)) { cannonA.canFire = true; }
if (instance_exists(cannonC)) { cannonC.canFire = true; }

我们需要分别检查两门大炮,以便如果其中一门被摧毁,另一门仍然会射击。

  1. 重新打开scr_Boss_Create,并在代码的最后开始一个循环时间轴:
timeline_index = tm_Boss_Phase01;
timeline_running = true;
timeline_loop = true;
  1. 重新打开BossArena,确保如果房间内仍有枪的实例,则将其移除。

  2. 在地图的右侧放置一个obj_Boss的实例,实际位置并不重要。

  3. Boss 的任何部分都没有固体属性,这意味着玩家可以穿过它们。为了解决这个问题,在 Boss 的前面创建一个障碍墙,使用obj_Ground的实例,如下截图所示:构建第一阶段:大炮

  4. 运行游戏。在开始时,我们应该看到三门大炮堆叠在一起,还有一个不可摧毁的枪。枪应该瞄准玩家,并每隔几秒钟射出一颗子弹。游戏进行到第六秒时,我们应该看到中间的大炮开始充能,并很快射出一颗炮弹。再过六秒,上下两门大炮也应该做同样的动作。如果玩家被敌人的抛射物击中,他们会被击退。玩家的子弹会从大炮旁边飞过,除非它们被暴露,此时大炮将进入受损状态,子弹会消失。如果任何一门大炮被击中两次,它将消失。第一阶段现在已经完成,应该看起来像下面的截图:构建第一阶段:大炮

构建第二阶段:巨大的激光炮

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

  1. 首先我们将创建激光束。创建一个新的精灵,spr_LaserBeam,并加载Chapter 5/Sprites/LaserBeam.gif,不勾选移除背景。精灵可能看起来很小,只有八个像素宽,但我们将把这个精灵拉伸到整个屏幕,这样它可以在任何房间中使用。

  2. 我们需要将原点放在右侧,以便与激光炮的枪管正确对齐。将原点设置为X8Y32

  3. 创建一个新的对象,obj_LaserBeam,应用spr_LaserBeam作为精灵,并将深度设置为-600

  4. 创建一个新的脚本,scr_LaserBeam_Create,在创建事件中初始化变量:

myDamage = 20;
myLaserCannon = 0; 
image_xscale = room_width / 8;

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

  1. 接下来,我们将使用一个步骤 | 结束步骤事件,使用一个新的脚本scr_LaserBeam_EndStep,使激光炮的激光束移动。
x = myLaserCannon.x;
y = myLaserCannon.y;

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

  1. 现在只剩下将scr_Damage添加到Collision | obj_Player事件中。激光束现在已经完成。

  2. 接下来是激光炮,我们需要创建三个精灵:spr_LaserCannon_Idlespr_LaserCannon_Runspr_LaserCannon_Damage。从Chapter 5/Sprites/文件夹中加载相关文件,所有文件都需要勾选Remove Background

  3. 将所有三个精灵的Origin设置为X16Y56。这将有助于将激光束放置在我们想要的位置。

  4. 创建一个新对象,obj_LaserCannon,并将spr_LaserCannon _Idle分配为Sprite

  5. Depth设置为-700,以便激光炮位于炮台和枪的后面,但在激光束的前面。

  6. Create事件中初始化变量,创建一个新脚本,scr_Laser_Create,代码如下:

myHealth = 50;
mySpeed = 2;
myBuffer = 64;
action = IDLE;
facing = RIGHT;
canFire = false;

myIdle = spr_LaserCannon _Idle;
myRun = spr_LaserCannon _Run;
myDamage = spr_LaserCannon _Damage;

我们首先设置激光炮的健康、当前状态、面向方向和非射击的所有标准变量。然后设置激光炮的三种状态的所有动画系统变量。

  1. 接下来是构建激光的功能。创建一个新脚本,scr_LaserCannon_Step,并将其添加到Step | Step事件中,代码如下:
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();
}

这应该开始看起来相当熟悉了。我们首先运行动画系统脚本。然后检查动画的最后一帧是否已播放,如果是,则将激光炮设置为待机状态。接下来,如果激光炮要射击,我们改变状态并设置一个短暂的警报,以便在射击动画播放后创建激光束。最后,我们进行健康检查,并在健康状况不佳时移除激光炮。

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

  1. 为了让激光炮上下移动,我们只需要在它通过终点时发送相反方向的指令。在scr_LaserCannon_Step的最后一行代码之后立即添加以下代码:
if (y < myBuffer)
{
    vspeed = mySpeed;
}
if (y > room_height - myBuffer)
{
    vspeed = -mySpeed;
} 
  1. 我们将让激光炮在整个房间的高度上移动。如果 Y 坐标距离顶部小于 64 像素,我们将其向下移动。如果距离房间底部大于 64 像素,我们将其向上移动。我们将在 Boss 脚本中开始移动。

  2. 让激光炮射出激光束!激光束将在Alarm | Alarm 0事件中创建,附加一个新脚本scr_LaserCannon_Alarm0,其中包含激光束创建的代码:

beam = instance_create(x, y, obj_LaserBeam);
beam.myLaserCannon = self.id;

我们在激光炮的尖端创建一个激光束的实例,然后将激光束的myLaserCannon变量设置为创建它的激光炮的唯一 ID。这样做的好处是,如果需要,我们可以在房间中放置多个激光炮。

  1. 我们需要构建的最后一个元素是伤害状态。创建一个新脚本,scr_LaserCannon_Collision,并将其放入Collision | obj_Bullet事件中:
if (obj_Boss.isPhase_02)
{
    myHealth -= 5;
    action = DAMAGE;
    with (other) { instance_destroy(); }
}

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

  1. 我们需要做的第一件事是添加一个激光炮的实例。重新打开scr_Boss_Create,并在运行时间轴之前插入以下代码:
laser = instance_create(boss_X, 352, obj_LaserCannon);
  1. 接下来,我们将通过创建一个新的时间轴并命名为tm_Boss_Phase02来构建 LaserCannon 的功能。

  2. 要发射激光束,添加一个时刻并将指示时刻设置为210

  3. 创建一个新的脚本,scr_Phase02_210,并将其与激活 LaserCannon 的代码分配:

laser.canFire = true;
  1. 我们希望完全控制 LaserCannon 的持续时间,因此我们将使用时间轴来移除激光束。在270处添加一个时刻。这将给我们一个持续两秒的激光束。

  2. 创建一个新的脚本,scr_Phase02_270,并移除激光束。

with (laser.beam) { instance_destroy(); }

当 LaserCannon 射击时,它会创建beam变量,现在我们可以使用它来移除它。

  1. 唯一剩下的就是让 Boss 从第一阶段变为第二阶段。为此,我们需要在obj_Boss上添加一个步骤|步骤事件,分配一个新的脚本scr_Boss_Step,其中包含以下代码:
if (!instance_exists(obj_Cannon) && !isPhase_02)
{
    laser.vspeed = laser.mySpeed;
    timeline_index = tm_Boss_Phase02;
    timeline_position = 0;
    gun.delay = 45;
    isPhase_02 = true;
}

我们首先检查世界中是否还有 Cannon 的实例,如果它们都被摧毁了,我们检查第二阶段是否已经开始。第二阶段开始时,我们将 LaserCannon 向下移动,并切换时间轴到新阶段,并将时间轴重置到开始。我们还将通过减少 Gun 射击之间的延迟来增加挑战的难度。最后,我们将isPhase_02更改为 true,以便这个代码只执行一次。

  1. 运行游戏。游戏玩法开始与以前相同,但在三个 Cannon 被摧毁后,我们应该看到 LaserCannon 开始上下移动,并且每七秒发射一次激光束。LaserCannon 可以在任何时候被击中,并且需要多次击中才能被摧毁。无法摧毁的 Gun 应该仍然像以前一样运行,但是射击频率增加了一倍。第二阶段现在已经完成,并且应该看起来像以下截图:构建第二阶段:巨型 LaserCannon

设置最终阶段:有护盾的 Boss Core

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

  1. 我们将从 Boss Core 开始。我们需要创建两个新的精灵,spr_BossCore_Idlespr_BossCore_Damage。勾选移除背景,加载Chapter 5/Sprites/BossCore_Idle.gifChapter 5/Sprites/BossCore_Damage.gif到相应的精灵上。

  2. 将两个精灵的原点设置为X-32Y64,这样它们将正确地位于护盾后面。

  3. 创建一个新的对象,obj_BossCore,并将spr_BossCore_Idle分配为精灵

  4. Boss Core 是一个简单的对象,只需要一些动画状态和生命值。创建一个新的脚本,scr_BossCore_Create,并初始化所需的变量如下。记得将其分配给创建事件:

myHealth = 100;
action = IDLE;
facing = RIGHT;

myIdle = spr_BossCore_Idle;
myDamage = spr_BossCore_Damage;
  1. 我们需要一个步骤|步骤事件来控制动画状态和处理生命值,因此创建另一个新脚本,scr_BossCore_Step,其中包含以下代码:
scr_Animation_Control();

if (action == DAMAGE) 
{
    if (image_index > image_number-1)
    {
        action = IDLE;
    }
}

if (myHealth <= 0)
{
    instance_destroy();
}
  1. Boss Core 现在所需要的就是一个碰撞|obj_Bullet事件来处理伤害。创建一个新的脚本,scr_BossCore_Collision,并编写以下代码:
if (obj_Boss.isPhase_03 && action == IDLE)
{
    myHealth -= 2;
    action = DAMAGE;
    with (other) { instance_destroy(); }
}

我们首先检查 Boss 是否处于最终阶段,并且 Boss Core 处于空闲状态。如果是,我们减少生命值并切换到受损动画。我们还确保子弹被移除。Boss Core 现在已经完成,我们可以转移到护盾。

  1. 我们将有两个护盾,一个是上升的,另一个是下降的。让我们引入我们需要的两个精灵。创建两个新的精灵,spr_Shield_Upperspr_Shield_Lower。加载Chapter 5/Sprites/Shield_Upper.gifChapter 5/Sprites/Shield_Lower.gif到相应的精灵上。记得勾选移除背景

  2. spr_Shield_UpperOrigin设置为X0Y269,以便原点位于图像底部。我们不需要更改spr_Shield_LowerOrigin

  3. 创建两个新对象,obj_Shield_Upperobj_Shield_Lower,并分配适当的精灵。

  4. 在两个护盾上,将深度设置为-500,这样它们就在 Boss 核心的前面,但在 Boss 的所有其他部分的后面。

  5. 我们将首先建造上层护盾,并且我们需要在一个新的脚本scr_ShieldUpper_Create中初始化一些变量,应用于obj_Shield_UpperCreate事件:

isShielding = true;
openPosition = y-64;
mySpeed = 2;

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

  1. 下层护盾几乎完全相同,只是移动方向相反。再次创建一个新脚本scr_ShieldLower_Create,并将其应用于obj_Shield_LowerCreate事件:
isShielding = true;
openPosition = y+64;
mySpeed = 2;
  1. 接下来,我们将在obj_Shield_Upper上添加一个Step | Step事件,附加一个新脚本scr_ShieldUpper_Step,其中包含以下代码来控制护盾的移动:
if (isShielding && y < ystart) { y += mySpeed; }
if (!isShielding && y > openPosition) { y -= mySpeed; } 

我们首先检查护盾是否应该关闭,以及它是否完全关闭。如果没有完全关闭,我们将护盾稍微关闭一点。第二个if语句则相反,检查护盾是否应该打开,以及它是否完全打开。如果没有,我们将抬起护盾一点。

  1. 下层护盾几乎完全相同。在obj_Shield_LowerStep | Step事件中再次创建一个新脚本scr_ShieldLower_Step,附加以下代码:
if (isShielding && y > ystart) { y -= 2; }
if (!isShielding && y < openPosition) { y += 2; }
  1. 我们需要处理的最后一个元素是Collision | obj_Bullet事件,两个护盾都可以使用。创建一个新脚本scr_Shield_Collision,其中包含以下代码:
if (obj_Boss.isPhase_03)
{
    with (other) { instance_destroy(); }
}

护盾永远不会受到伤害,但它们只应在最后阶段检测碰撞。

  1. 现在所有对象都已准备就绪,是时候将它们实现到 Boss 中了。重新打开scr_Boss_Create,并在最后一个武器后插入以下代码:
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 核心和护盾。

  1. 接下来,我们将创建一个时间轴tm_Boss_Phase03来处理护盾和枪的功能。

  2. 120处添加一个Moment,然后创建一个新脚本scr_Phase03_120,其中包含以下代码:

shieldUpper.isShielding = false;
shieldLower.isShielding = false; 
gun.delay = 10;

在这里,我们正在设置护盾打开,并增加枪的射击速率。

  1. 180处添加一个Moment,并创建一个新脚本scr_Phase03_180。我们要做的就是关闭枪的警报,以便射击有一个短暂的休息。这是通过将延迟设置为-1 来实现的。
gun.delay = -1;
  1. 300处添加另一个Moment,并创建一个新脚本scr_Phase03_300。现在我们重新激活枪的警报。
gun.delay = 10;
  1. 最后,我们在360处添加一个Moment,使用另一个新脚本scr_Phase03_360,在那里我们降低护盾并将枪的射击速率恢复正常:
shieldUpper.isShielding = true;
shieldLower.isShielding = true; 
gun.delay = 45;
  1. 现在我们需要添加从第二阶段到最后阶段的切换。重新打开scr_Boss_Step,并在末尾添加以下代码:
if (!instance_exists(obj_LaserCannon) && !isPhase_03)
{
    timeline_index = tm_Boss_Phase03;
    timeline_position = 0;
    isPhase_03 = true;
}

我们检查激光炮是否被摧毁,以及我们是否应该处于最后阶段。如果是,我们需要做的就是切换timeline,将其设置为开始,并设置为最后阶段。

  1. 现在我们只需要一个胜利条件,我们将把它添加到同一个脚本中。在scr_Boss_Step的末尾写上最后的条件语句:
if (!instance_exists(obj_BossCore) && !isBossDefeated)
{
    timeline_running = false;
    with (gun) { instance_destroy(); }
    isBossDefeated = true;
}

我们检查 Boss 核心是否被摧毁,以及是否已调用胜利条件。如果 Boss 被打败,我们停止时间轴并宣布失败。

  1. 运行游戏。这会花费一些时间,但前两个阶段应该与以前一样,一旦激光炮被摧毁,最后一个阶段就会激活。护盾应该会打开,枪会射出一连串的子弹。然后应该会有一个安静的时刻,玩家可以攻击 Boss 核心。几秒钟后,枪应该开始射击,护盾会关闭。这将重复,直到玩家击败 Boss。这个阶段应该看起来像下面的截图:设置最终阶段:有护盾的 Boss 核心

结束

我们正在结束这一章,还有一些未完成的元素,但你已经有能力自己完成。仍然有所有的声音、背景艺术和前端要构建。不仅如此,你可能已经注意到玩家无法被杀死。让玩家无敌使我们更容易测试 Boss 战斗,所以在添加后再试一次战斗。Boss 战斗非常困难,但也很容易改变。为什么不尝试改变每个阶段的时间或尝试调整伤害的值。为了更进一步,你可以构建导致战斗的关卡和敌人。玩得开心,它可能看起来像下面的截图!

结束

总结

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

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

第六章:倾倒的塔

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

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

理解物理引擎

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

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

理解物理引擎

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

  • 物理世界

  • Fixture

  • 连接

激活世界

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

  1. 让我们开始一个名为Chapter_06的新项目。

  2. 创建一个新的房间,命名为Sandbox。我们将只用这个房间进行测试。

  3. 点击物理选项卡,勾选房间是物理世界

  4. 物理世界属性中,将重力设置为X: 0.0Y: 20.0。这将设置世界中重力的方向和强度。如果你想让重力像地球上一样,我们会将值设置为Y: 9.8。我们将其设置为20.0,这样物体看起来会下落得更快。

  5. 最后,还有一个选项可以设置像素到米。整个物理系统都是基于真实世界的测量,所以我们需要确定一个像素代表多少米,以便计算准确。我们将保持默认值为每像素 0.1 米,或约 10 厘米。

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

激活世界

使用 Fixture 定义属性

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

  1. 我们将从创建地面对象开始。创建一个新的精灵,spr_Ground,并加载Chapter 6/Sprites/Ground.png,取消删除背景的勾选。将原点保留在X0Y0,然后点击确定

  2. 创建一个新的对象,obj_Ground,并将spr_Ground分配为精灵。

  3. 为了使物体在物理引擎中响应,我们需要勾选使用物理。这将显示物理属性,如下面的屏幕截图所示:使用夹具定义属性

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

使用夹具定义属性

  1. 地面是一个矩形物体,所以在物理属性下的碰撞形状中选择矩形

  2. 默认形状将根据精灵的原点创建,这种情况下是在左上角。这意味着我们需要调整原点物理形状以使其正确适配。对于这个对象,我们将选择后者。点击修改碰撞形状以打开物理形状编辑器。将点放置在精灵上正确的位置,如下面的屏幕截图所示,然后点击确定使用夹具定义属性

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

  • 密度:这代表单位体积内物体的质量。你可以把它想象成物体相对于其整体大小有多重。

  • 弹性:这代表物体有多有弹性。数字越高,物体在碰撞时弹跳得越多。这并不意味着物体的形状会变形。因为这是一个刚体物理模拟。

  • 碰撞组:这些组有助于简化物体之间的碰撞。这里的正数意味着该组编号内的所有物体将始终发生碰撞。负数意味着该组编号内的物体永远不会相互碰撞。如果设置为零,碰撞事件将需要放置到每个物体中才能发生碰撞。使用组应该尽量减少,因为它会大大增加处理时间。

  • 线性阻尼:这代表了物体运动速度的减小。你可以把它想象成空气摩擦,因为物体不需要与其他物体接触就会减速。

  • 角阻尼:与线性阻尼类似,这是物体旋转运动的减小。

  • 摩擦力:摩擦力是在碰撞过程中与运动物体相反的力。这与线性阻尼类似,因为它会减慢物体的速度。不同之处在于它需要发生碰撞。

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

材料 密度 恢复 摩擦
中等
玻璃 中等
木材 中等 中等 中等
橡胶 中等 中等
石头
  1. 由于这个地面永远不会移动或感受到重力的影响,我们需要将密度设置为0。当一个物体没有密度时,它被认为是一个静态物体。

  2. 我们不希望地面有弹性,所以将恢复设置为0

  3. 我们将碰撞组保留为默认值0

  4. 由于物体不会移动,我们可能会将线性阻尼角阻尼设置为0

  5. 最后,我们确实希望物体在地面上迅速停止,所以让我们将摩擦设置为1。我们已经完成了obj_Ground,所以点击确定

  6. 接下来,我们将制作钢柱。创建一个新的精灵,spr_Pillar_Steel,并加载Chapter 6/Sprites/Pillar_Steel.png,勾选去除背景。居中原点,然后点击确定

  7. 创建一个新的对象,obj_Pillar_Steel,并将spr_Pillar_Steel设置为其精灵

  8. 勾选使用物理的复选框。

  9. 物理属性下的碰撞形状中,选择方块。由于我们将原点放置在精灵的中心,因此形状应该正确地符合精灵,这样我们就不必修改它。然而,我们应该始终打开物理形状编辑器,以确保它正确地定位,以防止任何重大问题。

  10. 我们希望这个对象相当重,所以将密度设置为20

  11. 钢柱也不应该很滑,所以将摩擦设置为2

  12. 将所有其他属性设置为0,因为我们不希望减慢这个物体的速度或使其弹跳。我们现在已经完成了设置这个对象的属性。

  13. 我们唯一剩下要做的就是添加一个obj_Ground事件。如下截图所示,我们不需要任何代码,只需要一个注释。从控件选项卡下的动作中拖动一个注释,并写上与地面碰撞。通过这个小技巧,钢柱现在将与地面发生主动碰撞。使用夹具定义属性

  14. 重新打开Sandbox房间,并在水平中心附近放置一个obj_Pillar_Steel的实例。此外,在底部放置obj_Ground的实例,其中一个额外的实例位于地板上方,略低于钢柱将要掉落的位置,如下截图所示。在房间属性编辑器中自由移动实例,按住Alt键同时按住鼠标左键。使用夹具定义属性

  15. 运行游戏。钢柱应该会倒下并与地面上的小树桩碰撞。然后它应该倒在一边并停下来。

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

用关节连接对象

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

  • 距离关节:这些将保持两个实例相互连接,并保持一定距离。例如,手推车会有一个距离关节,以保持前轮与把手一定距离,无论如何推动它。

  • 旋转关节:这些将使一个实例围绕另一个旋转。例如,门铰链是一个旋转关节,它会使门围绕门框旋转。

  • 滑动关节:这些将允许一个实例在相对于另一个的单个方向上移动。例如,弹球弹簧会有一个滑动关节,因为它只能向后拉或向前推入机器中。

  • 滑轮关节:这些将允许一个实例影响另一个与其运动相关的关系。例如,一套天平使用滑轮关节来称重。如果一边更重,它会下降,而另一边会上升。

  • 齿轮关节:这些将根据另一个实例的旋转影响一个实例的运动。例如,钓鱼竿的旋转卷轴是一个齿轮关节;当它旋转时,它会拉进鱼。

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

  1. 我们将从构建锚点开始,这将是世界中的一个静止静态对象。创建一个新的精灵,spr_Anchor,并加载Chapter 6/Sprites/Anchor.png,勾选删除背景。居中原点,然后点击确定

  2. 创建一个新对象,obj_Anchor,并将spr_Anchor设置为精灵。

  3. 勾选使用物理并将碰撞形状更改为方块

  4. 密度弹性设置为0。我们可以将其他属性保留为默认值,应该看起来像下面的截图:用关节连接对象

  5. 接下来,我们需要创建链条链接。创建一个新的精灵,spr_ChainLink,并加载Chapter 6/Sprites/ChainLink.png,勾选删除背景。居中原点,然后点击确定

  6. 创建一个新对象,obj_ChainLink,并将spr_ChainLink设置为精灵

  7. 勾选使用物理并将碰撞形状更改为方块

  8. 我们希望链条非常坚固和沉重,所以将密度设置为50

  9. 链条不应该伸展并且应该自由摆动,因此我们需要将弹性线性阻尼角阻尼摩擦设置为0。最终设置应该看起来像下面的截图:用关节连接对象

  10. 组件部分现在已经完成;我们只需要构建整个链条并将其连接到锚点。创建一个新脚本,scr_Anchor_Create,编写以下代码,并将其添加到obj_Anchor创建事件中:

for (i = 1; i < 10; i++)
{   
    chain[i] = instance_create(x+ (i * 16), y, obj_ChainLink);
}

为了构建链条,我们运行一个循环,开始创建九个链条链接。我们从1开始循环,以便正确偏移链条。我们使用一个基本的一维数组来存储每个链条链接的 ID,因为当我们添加关节时会需要这个。我们在创建中的x偏移将使每个链接在水平方向上等距离分开。

  1. 接下来,我们需要在链条的第一个链接上应用旋转关节。在上一个代码之后,添加:
physics_joint_revolute_create(self, chain[1], self.x, self.y, 0, 0, false, 0, 0, false, false);

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

  1. 现在我们已经连接了第一个链接,让我们把剩下的链条连接起来。仍然在同一个脚本中,在最后添加:
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 度的两个方向。

  1. 现在我们有一个小链条连接到一个锚点。让我们把它添加到世界中。重新打开Sandbox,在房间的顶部附近添加一个obj_Anchor的实例。

  2. 运行游戏。锚点应该保持在房间的顶部,链条链接向右延伸。由于房间中的重力,链条会下落,但每个链接都会保持连接在上面的链接上,顶部链接仍然连接在锚点上。它应该看起来像下面的截图:使用关节连接对象

对物体施加力

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

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

  1. 让我们先从碎片开始。创建一个新的精灵,spr_TNT_Fragment,并加载Chapter 6/Sprites/TNT_Fragment.png,取消删除背景的勾选。将原点居中,然后点击确定

  2. 创建一个新的对象,obj_TNT_Fragment,并将spr_TNT_Fragment分配为精灵

  3. 勾选使用物理,并将碰撞形状更改为矩形

  4. 密度设置为10。我们将这个值设置得很高,这样当它与物体碰撞时,比如钢柱,它就能够移动它。

  5. 将所有剩余的属性设置为0

  6. 由于我们需要从 TNT 中射出多个碎片,我们需要能够控制它移动的方向。因此,我们需要建立一些变量。创建一个新的脚本,scr_TNT_Fragment_Create,包含以下变量:

mySpeedX = 0;
mySpeedY = 0;

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

  1. 由于这些碎片是用来表示爆炸的,我们希望不断地对它们施加力,这样它们就不会受到过多的重力影响。创建一个新的脚本,scr_TNT_Fragment_Step,并施加一些力。将这个脚本添加到Step事件中。
physics_apply_force(x, y, mySpeedX, mySpeedY);

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

  1. 目前,这些碎片永远不会停止移动,这是一个问题。我们需要限制它们可以移动的距离。在脚本的末尾添加以下代码:
if (point_distance(x, y, xstart, ystart) > 128)
{
    instance_destroy();    
}

我们在这里所做的就是检查碎片是否从创建时移动了超过 128 像素。如果是,我们就将其从世界中移除。

  1. 我们希望这些碎片与游戏中的其他元素发生碰撞。与此同时,我们不希望它们穿过任何东西,所以我们会销毁它们。创建一个新的脚本,scr_TNT_Fragment_Collision并删除实例。
instance_destroy();
  1. 添加一个obj_Ground事件并添加这个脚本。这将在碰到地面时移除碎片。

  2. 我们希望它影响钢柱,但由于我们计划创建更多类型的柱子,让我们为柱子构建一个父对象进行碰撞检测。创建一个新对象,obj_Pillar_Parent。现在它只需要这些,所以点击确定

  3. 重新打开obj_Steel_Pillar并将父级设置为obj_Pillar_Parent

  4. 当我们在obj_Steel_Pillar中时,我们也可以让它对其他柱子做出反应。从控件中添加一个注释操作区域,并输入与柱子碰撞作为注释。

  5. 回到 obj_TNT_Fragment 并添加一个obj_Pillar_Parent并应用scr_TNT_Fragment_Collision。现在我们将与所有柱子发生碰撞!

  6. 现在我们所需要做的就是创建 TNT 并让它爆炸。创建一个新的精灵,spr_TNT,并加载Chapter 6/Sprites/TNT.png,勾选去除背景。居中原点,然后点击确定

  7. 创建一个新对象,obj_TNT,并应用spr_TNT作为精灵。我们将手动放置 TNT 在游戏中,我们不需要它对世界物理做出反应,所以我们需要打开使用物理

  8. 让我们创建一个新的脚本,scr_TNT_Activate,并为测试目的,在按键按下下的空格事件中添加它。我们将只创建一个碎片,并让它向右发射,这样我们就可以看到世界中的力量是如何工作的。

frag_01 = instance_create(x, y, obj_TNT_Fragment);
frag_01.mySpeedX = 100;

我们首先创建一个碎片并将其 ID 存储在一个变量中。然后我们将水平力设置为 100 单位。这个值似乎足够推动这个物体向右移动。

  1. 让我们来测试一下。重新打开Sandbox并将一个单独的实例放在钢柱将要倒下的位置的左侧,离地面三个网格空间。另外,让我们移除多余的地面实例和链条。房间应该看起来像下面的截图:对物体施加力量

  2. 运行游戏,按空格键生成一个碎片。你应该看到碎片向右移动,但也向下掉落。当碎片与钢柱碰撞时,碎片消失,钢柱没有任何变化。所有这些都是由于碎片没有足够的力量。

  3. 增加力量。重新打开scr_TNT_Activate并将第二行更改为:

frag_01.mySpeedX = 1000;
  1. 运行游戏并按空格键查看这些变化。碎片现在似乎只向右移动,并在与钢柱接触时使其摇晃了一下。然而,无论我们多少次击中钢柱,它都不会倒下。这是因为钢柱的密度是碎片的两倍,它需要更大的力量才能将其击倒。

  2. 再次调整数字,将力量改为:

frag_01.mySpeedX = 10000;
  1. 再次运行游戏,尝试把钢柱撞倒。应该需要三次快速点击,它就会倒下。正如我们所看到的,像碎片这样的小物体需要非常大的力量才能移动像钢柱这样的大物体。

  2. 现在我们有一个碎片在工作,让我们把其他的也加进来。我们需要再加入七个碎片,每个以 45 度的增量移动。我们还希望移除 TNT,这样它只能被触发一次。

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

建立推倒塔游戏

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

构建柱子和碎片

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

  1. 创建一个新的精灵,spr_Pillar_Steel_Small,并加载Chapter 6/Sprites/Pillar_Steel_Small.png,勾选Remove Background。将原点居中,然后点击OK

  2. 而不是创建一个新对象,右键单击obj_Pillar_Steel,然后Duplicate该对象。这样可以保持属性不变,因此我们不必重复大部分工作。将其重命名为obj_Pillar_Steel_Small

  3. 将精灵更改为spr_Pillar_Steel_Small

  4. 由于这是一个较大对象的副本,我们需要调整夹具。点击Modify Collision Shape以打开Physics Shape编辑器,并移动点以适当地适应较小的精灵。我们已经完成了这个柱子,Object Properties应该看起来像以下的截图:构建柱子和碎片

  5. 创建一个新的精灵,spr_Debris_Steel_01,并加载Chapter 6/Sprites/Debris_Steel_01.png,勾选Remove Background

  6. 当柱子变成碎片时,我们希望确保每个部分都被正确放置和旋转。为了做到这一点,我们需要将原点放置在与柱子原点对应的位置。这些碎片来自左上角,因此将原点设置为X16Y64,然后点击OK

  7. 让我们再次复制obj_Pillar_Steel,将其命名为obj_Debris_Steel_01,并将精灵更改为spr_Debris_Steel_01

  8. 所有碎片都呈奇怪的形状,我们希望碰撞反映出这一点。在Physics Properties编辑器中,将Collision Shape更改为Shape

  9. 点击Modify Collision Shape以打开Physics Shape编辑器,并移动点以适当地适应碎片。您会注意到它可能已经给了您一个三角形的起点。要添加额外的点,只需点击远离现有点的位置。另一个重要的注意事项是,为了使物理正常工作,形状夹具必须始终按顺时针的方式构建。碰撞形状应该看起来像以下的截图:构建柱子和碎片

  10. 创建一个新的精灵,spr_Debris_Steel_02,并加载Chapter 6/Sprites/Debris_Steel_02.png,勾选Remove Background

  11. 将原点设置为X0Y64,然后点击OK

  12. 复制obj_Debris_Steel_01,将其重命名为obj_Debris_Steel_02,并将Sprite设置为spr_Debris_Steel_02

  13. 再次点击“修改碰撞形状”,并根据以下屏幕截图调整点的位置:构建支柱和碎片

  14. 我们还需要制作最后一个碎片,创建一个新精灵,spr_Debris_Steel_03,并加载“第六章/精灵/Debris_Steel_03.png”,勾选“去除背景”。

  15. 将原点设置为“X”:16,“Y”:0,然后点击“确定”。

  16. 复制obj_Debris_Steel_01,将其重命名为obj_Debris_Steel_03,并将“精灵”更改为spr_Debris_Steel_03

  17. 这个对象需要五个点,因此点击“修改碰撞形状”,并根据以下屏幕截图调整点的位置。我们已完成钢碎片:构建支柱和碎片

  18. 接下来,我们将构建木支柱及其相关部分。我们不会逐步介绍每个步骤,因为这只是重复我们刚刚用钢支柱进行的过程。但是,我们将构建其他材料类型的第一个支柱。创建一个新精灵,spr_Pillar_Wood,并加载“第六章/精灵/Pillar_Wood.png”,勾选“去除背景”。将原点居中,然后点击“确定”。

  19. 创建一个新对象,obj_Pillar_Wood,并将spr_Pillar_Wood分配为“精灵”。

  20. 将“父对象”设置为obj_Pillar_Parent

  21. 勾选“使用物理”。

  22. 将“碰撞形状”更改为“矩形”。由于这是一个新对象,碰撞形状应自动适应精灵,因此我们不需要修改形状。

  23. 木材比钢轻得多,因此我们希望它只需很小的力就能移动。将“密度”设置为8

  24. 木材弹性更大,因此应将“恢复”设置为0.2

  25. 我们将说这种木材比钢更不粗糙,并将“摩擦”设置为0.5

  26. 将“碰撞组”、“线性阻尼”和“角阻尼”的值设置为0,因为我们不希望支柱受到它们的影响。

  27. 我们需要为obj_Groundobj_Pillar_Parent添加事件,并附上注释以使碰撞检测起作用。如果你想知道为什么我们不直接将其放在obj_Pillar_Parent中,那是因为我们稍后将为这些事件添加碎片脚本。

  28. 木支柱已完成,这意味着我们现在可以创建小木支柱和木碎片。继续使用“第六章/精灵/”中提供的文件构建所有这些部分。确保对象属性与以下屏幕截图中所示的相同:构建支柱和碎片

  29. 我们的最后一个支柱,也是最脆弱的支柱,是由玻璃制成的。创建一个新精灵,spr_Pillar_Glass,并加载“第六章/精灵/Pillar_Glass.png”,勾选“去除背景”。将原点居中,然后点击“确定”。

  30. 创建一个新对象,obj_Pillar_Glass,其“精灵”设置为spr_Pillar_Glass

  31. 将父对象设置为obj_Pillar_Parent

  32. 勾选“使用物理”并将“碰撞形状”更改为“矩形”。

  33. 玻璃是最轻的材料,我们希望它只需很小的力就能移动。将“密度”设置为2

  34. 我们希望玻璃会发出很多声音,因此将“恢复”设置为0.3

  35. 玻璃应该非常光滑,具有摩擦值0.07

  36. 与其他支柱一样,将“碰撞组”、“线性阻尼”和“角阻尼”的值设置为0,因为我们不希望支柱受到它们的影响。

  37. 最后,我们需要为obj_Groundobj_Pillar_Parent添加事件,并附上注释以使碰撞检测起作用。最终设置应如以下屏幕截图所示:构建支柱和碎片

  38. 与其他支柱一样,使用“第六章/精灵/”中提供的资源创建剩余的玻璃碎片。

  39. 现在所有的柱子都已经创建好了,重新打开沙盒并放置一些柱子和一些 TNT。运行游戏,注意各种材料的反应。玻璃会轻易移动,而钢是相当坚固的。木头似乎在这两者之间有所反应。

将柱子打碎成碎片

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

  1. 我们将从最脆弱的物体,玻璃柱开始,初始化一些变量。创建一个新的脚本,scr_Pillar_Glass_Create,并将其应用于obj_Pillar_Glass创建事件。
myDamage = 5;
debris01 = obj_Debris_Glass_01;
debris02 = obj_Debris_Glass_02;
debris03 = obj_Debris_Glass_03;

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

  1. 创建一个新的脚本,scr_Pillar_BreakApart,其中包含以下代码:
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 的函数,它将确保我们得到的速度始终是一个正数。这将使比较变得更容易,因为我们不需要考虑运动的方向。如果碰撞物体的速度比柱子的伤害量快,那么我们就检查第二个条件语句,比较碰撞中涉及的两个实例的质量。我们只希望柱子在被比自身更强的东西击中时分解。让玻璃柱摧毁钢柱是毫无意义的。如果柱子被更重的物体击中,我们就生成碎片。对于每个碎片,我们需要根据柱子的物理速度和旋转将其放在适当的位置。创建了碎片后,我们销毁了柱子。

  1. 将此脚本添加到obj_Pillar_Glass中的obj_Pillar_Parent事件。我们可以删除注释,因为它不再需要用于碰撞。

  2. 重新打开Sandbox,在两侧分别放置一个玻璃柱和钢柱的单个 TNT 实例。它应该看起来像下面的截图:将柱子打碎成碎片

  3. 运行游戏并引爆 TNT。我们应该看到玻璃柱向外推,与钢柱碰撞,然后分解成一堆碎片,就像下面的截图:将柱子打碎成碎片

  4. 让我们继续进行木柱。创建一个新的脚本,scr_Pillar_Wood_Create,并初始化必要的变量。将它们添加到obj_Pillar_Wood创建事件中。

myDamage = 16;
debris01 = obj_Debris_Wood_01;
debris02 = obj_Debris_Wood_02;
debris03 = obj_Debris_Wood_03;

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

  1. obj_Pillar_Parent中删除注释,并添加scr_Pillar_BreakApart

  2. 重新打开Sandbox,用木柱替换玻璃柱。

  3. 运行游戏并引爆 TNT。木头会向外移动,但不会破碎。这个结果是有意的,因为我们说需要更大的力量才能打破它。

  4. Sandbox中添加另一个 TNT 实例,放在现有的 TNT 下方。这样在引爆时会施加更大的力。

  5. 运行游戏。如下一张截图所示,这次木柱向外移动并在接触时破碎。钢柱也会因为这种力量而倒下!将柱子打碎成碎片

  6. 现在只剩下钢柱了。我们将设置它以正常运行,尽管在这一点上我们将无法测试它,因为没有比它更密度大的物体。创建一个新的脚本,scr_Pillar_Steel_Create,并将其添加到obj_Pillar_SteelCreate事件中。

myDamage = 25;
debris01 = obj_Debris_Steel_01;
debris02 = obj_Debris_Steel_02;
debris03 = obj_Debris_Steel_03;

与以前一样,我们增加了造成伤害所需的速度,并设置了正确的碎片生成。

  1. 我们还需要从obj_Pillar_Parent中删除注释,并替换为scr_Pillar_BreakApart

  2. 现在我们已经让柱子在受到足够的力量时分解成小块。接下来,我们需要在足够的力量碰撞到它们时摧毁小柱子和碎片。创建一个新的脚本,scr_Pillar_Destroy,并添加以下代码:

if (abs(other.phy_linear_velocity_x) > myDamage || abs(other.phy_linear_velocity_y) > myDamage)
{ 
    if (phy_mass < other.phy_mass)
    { 
        instance_destroy();
    }
}

scr_Pillar_BreakApart类似,我们检查碰撞物体的速度,然后比较质量,看看是否应该被摧毁。这里是密度和质量之间的差异变得明显的地方。所有的碎片都有与生成它的柱子相同的密度,这意味着它们的坚固度是相同的。然而,物体越大,质量就越大。这意味着较小的碎片可以被较大的碎片摧毁。

  1. 将此脚本应用于它们各自的obj_Pillar_Parent事件中的所有小柱子和碎片。

  2. 这个脚本使用与其类型相同的变量,这意味着我们需要初始化它们。我们可以重用现有的脚本来节省时间。对于每个小柱子和碎片,添加一个Create事件,并应用适当的柱子创建脚本,就像所有玻璃都应该分配scr_Pillar_Glass_Create一样。

  3. 是时候测试一下了。重新打开Sandbox,并在木柱顶部放置两个玻璃柱,使其看起来像下面的截图:Breaking the Pillars into Debris

  4. 运行游戏并引爆 TNT。玻璃柱应该很容易破碎,大部分碎片会很快消失。木柱也会有些裂痕,大部分碎片会消失。钢柱会稍微摇晃,但不会受损。

添加碰撞声音

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

  1. 首先我们需要引入一些声音。创建一个新的声音,snd_Shatter_Glass,并加载Chapter 6/Sounds/Shatter_Glass.wav。默认值将起作用,只需确保Kind设置为Normal Sound。这个效果是当玻璃破碎时使用的。

  2. 我们还希望在玻璃柱不破裂时有声音。创建另一个新声音,snd_Impact_Glass,并加载Chapter 6/Sounds/Impact_Glass.wav

  3. 为木头和钢铁的声音效果重复这个过程。

  4. 我们需要初始化一些变量,所以重新打开scr_Pillar_Glass_Create,并在脚本的末尾添加以下内容:

impact = snd_Impact_Glass;
shatter = snd_Shatter_Glass;

isTapped = false;
isActive = false;
alarm[0] = room_speed;

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

  1. 重新打开scr_Pillar_Wood_Createscr_Pillar_Steel_Create,并添加相同的代码和适当的声音。

  2. 现在我们可以开始实现声音了。打开scr_Pillar_BreakApart,并在实例被销毁之前插入以下代码行:

sound_play(shatter);

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

  1. 在脚本中,如果发生碰撞但没有破坏支柱,我们需要播放碰撞声音。在实例被销毁后立即添加一个else语句。
} else {
    if (!isTapped)
    {
        sound_play(impact);
        isTapped = true;
    }
}

如果只发生了轻微的碰撞,我们会检查是否之前已经播放了声音。如果没有,那么我们会播放撞击声音,优先级较低,并阻止该代码再次执行。

  1. 在这个脚本中,我们只剩下一件事要做,那就是将所有的代码放入条件语句中,这样它只有在实例处于活动状态时才会执行。在脚本顶部添加检查,并在所有现有代码周围加上大括号。完成后,整个脚本将如下所示:
if (isActive)
{
    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;

            sound_play(shatter);

            instance_destroy();
        } else {
            if (!isTapped)
            {
                sound_play(impact);
                isTapped = true;
            }
        }
    } 
}
  1. 我们需要重复这个过程,对scr_Pillar_Destroy进行修改,以便在销毁时播放粉碎声音,在轻微碰撞时播放碰撞声音,并且在实例处于活动状态时执行所有这些操作。以下是完整的代码:
if (isActive)
{
    if (abs(other.phy_linear_velocity_x) > myDamage || abs(other.phy_linear_velocity_y) > myDamage)
    { 
        if (phy_mass < other.phy_mass)
        { 
            sound_play(shatter);
            instance_destroy();
        }        else {
            if (!isTapped)
            {
                sound_play(impact);
                isTapped = true;
            }
        }
    }
}
  1. 为了使声音正常工作,我们需要使它们处于活动状态。创建一个新的脚本,scr_Pillar_Alarm0,并将isActive设置为true

  2. 我们不需要为每个支柱和碎片都添加一个警报,只需在obj_Pillar_Parent中添加一个警报 0事件。这不会引起任何冲突,因为警报每个实例只运行一次,并且只改变一个变量。

  3. 运行游戏,引爆 TNT 并听着。我们可以听到不同的声音,因为支柱破碎并相互碰撞。还要注意现在剩下更多的碎片。这是因为现在它们在销毁自己之前有一秒的延迟,这样就有时间让碎片逃离在创建时发生的任何碰撞。

建造拆迁设备

我们已经拥有了建造塔所需的一切,但这个游戏的重点是拆除。如果玩家只能使用 TNT 来摧毁塔,他们会感到无聊。我们将利用一些更多的物理函数,并创建一些新的设备:破坏球和磁吊机。

创建一个破坏球

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

  1. 创建一个新的精灵,spr_WreckingBall,并加载Chapter 6/Sprites/WreckingBall.png,勾选去除背景。居中原点,然后点击确定

  2. 创建一个新的对象,obj_WreckingBall,并将spr_WreckingBall应用为其Sprite

  3. 我们希望破坏球始终显示在支撑它的链条前面。将深度设置为-100

  4. 勾选使用物理。我们不需要更改碰撞形状,因为破坏球是一个圆形。

  5. 我们希望这个破坏球非常强大,所以将密度设置为50

  6. 由于它是一个如此沉重的物体,悬挂在链条上,它不应该能够旋转太多。为了减慢旋转速度,将AngularDamping设置为5

  7. 该对象的所有其他物理值都应设置为0

  8. 我们已经建造了破坏球,现在需要将其添加到锚点和链条上。重新打开scr_Anchor_Create,并在脚本末尾添加以下代码:

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 度。

  1. 接下来,我们需要添加碰撞。我们不会在破坏球上放置碰撞,因为现有的脚本将查找破坏球不会有的变量。相反,我们将从重新打开obj_Pillar_Parent开始,添加一个obj_WreckingBall事件,并附加scr_Pillar_Destroy。因为所有支柱和碎片都是该对象的子对象,它们都会响应这个事件。

  2. 虽然最后一步将正常工作,但这也意味着大柱子也会在接触时被摧毁。我们希望大柱子始终首先破裂。我们仍然可以通过重新打开三个柱子obj_Pillar_Glassobj_Pillar_Woodobj_Pillar_Steel,并添加一个带有scr_Pillar_BreakApartobj_WreckingBall事件来实现这一点。如果父对象和其子对象都具有相同类型的事件,无论是碰撞、步进还是其他事件,子对象的事件将被执行,父对象的事件将被忽略。

注意

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

  1. 让我们测试一下。重新打开Sandbox,并在房间中放置一个obj_Anchor的实例,就在现有柱子的右侧。我们还可以移除 TNT,因为我们不需要它进行测试。设置应该看起来像以下屏幕截图:创建破坏球

  2. 运行游戏。我们应该看到破坏球挂在链和锚上摆动。当破坏球与钢柱碰撞时,柱子会破裂,其他许多柱子也会破裂。一切都运行正常,但存在一些问题。破坏球立即下落,而它应该等待被触发。让我们修复所有这些。

  3. 为了使我们能够立即停止破坏球的移动,我们需要从世界物理中停用。这只需将phy_active变量设置为 false,以停止我们想要停止的每个实例。重新打开scr_Anchor_Create,并对破坏球和每个链应用此更改。整个脚本可以在以下代码中看到:

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);
  1. 破坏球和链将不再在开始时移动,但我们仍然需要在某个时候触发它。创建一个新脚本,scr_Anchor_Activate,并将其附加到按键按下下的空格事件以进行测试。
for (i = 1; i < 10; i++)
{
    chain[i].phy_active = true; 
}
ball.phy_active = true; 

当运行此脚本时,一个简单的for循环会激活每个链,然后激活破坏球。

  1. 运行游戏。破坏球应该向右延伸并保持静止。当我们按下空格键时,破坏球和链应该变得活跃并摆动下来,撞击到塔上。碰撞本身在塔上更高,因为链现在相当刚硬,只有一点弹性。看起来我们已经完成了!

制作磁吊车

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

  1. 我们将首先建立磁铁本身。创建一个新的精灵,spr_Magnet,并加载Chapter 6/Sprites/Magnet.png,并勾选删除背景。居中原点,然后点击确定

  2. 创建一个新对象,obj_Magnet,并将spr_Magnet分配为精灵

  3. 勾选使用物理并将碰撞形状设置为矩形

  4. 我们希望将碰撞区域变小,这样当它吸起物体时,效果看起来更真实。点击修改碰撞形状,将侧面拉进,使其看起来像以下屏幕截图:制作磁吊车

  5. 磁铁需要相当重,这样其他物体就无法推动它。将密度设置为50

  6. 将所有其他属性设置为0,因为我们不希望它们影响磁铁的运动。

  7. 由于我们的意图是让磁铁只吸起由钢制成的小物体,我们应该改变钢屑的父级关系。目前,它是为了碰撞目的而与obj_Pillar_Parent相关联的。我们仍然需要具有这种能力,但我们希望磁性吸引对一些物体是独特的。为了做到这一点,我们可以将碎片与任何具有obj_Pillar_Parent作为其父级的对象相关联。让我们将所有钢屑的父级设置为obj_Pillar_Steel_Small

  8. 我们还需要为所有钢制品添加一个变量,以便我们知道它是否已被收集。重新打开scr_Pillar_Steel_Create,并在脚本的末尾添加以下代码行:

isCollected = false;
  1. 现在我们可以为磁性吸引力编写脚本。创建一个新的脚本,scr_Magnet_Step,并将其附加到obj_MagnetStep事件上。
if (phy_active)
{
    if (instance_exists(obj_Pillar_Steel_Small))
    {
        with (obj_Pillar_Steel_Small)
        {
            if (!isCollected)
            {
                myMagnet = instance_nearest(x,y,obj_Magnet)
                myDist = point_distance(phy_position_x, phy_position_y, myMagnet.x, myMagnet.y);
                myDir = point_direction(phy_position_x, phy_position_y, myMagnet.x, myMagnet.y);
                if (myDist < 200 && myDir > 60 && myDir < 120)
                {
                    physics_apply_impulse(x, y, 0, -2000)
                }
            }
        }
    }
}

我们首先要看磁铁是否活动并开始收集废金属。接下来,我们检查世界中是否有任何小型钢柱,或者任何与之相关的实例。如果存在实例,我们通过with语句直接对它们应用代码。如果实例尚未被收集,我们找到最近的磁铁,看看它离磁铁有多远,以及在什么方向。在物理游戏中检查对象的 X 和 Y 坐标时,我们需要使用phy_position_xphy_position_y值来准确地知道它们在世界空间中的位置。接下来,我们看实例是否在磁力范围内,以及它是否在磁铁下方。如果是的话,我们会向上施加一个强大的冲量,使其向磁铁移动。

  1. 一旦一个小型钢柱或碎片接触到磁铁,我们希望将其视为已收集,并始终与其一起移动。为此,我们将动态地创建一个关节,使其与磁铁碰撞的任何实例连接。创建一个新的脚本,scr_Magnet_Collsion,并将其附加到obj_Magnet中的obj_Pillar_Steel_Small事件。
physics_joint_prismatic_create(id, other, x, y, 0, 1, 0, 0, true, 0, 0, false, false);
other.isCollected = true;

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

  1. 接下来,我们需要为起重机创建一个基座,它将类似于锚。创建一个新的 Sprite,spr_CraneBase,并加载Chapter 6/Sprites/CraneBase.png,勾选Remove Background。将原点居中,然后点击OK

  2. 创建一个新的对象,obj_CraneBase,并将spr_CraneBase应用为Sprite

  3. 勾选Uses Physics框,并将Collision Shape设置为Box

  4. 这个对象在物理世界中是静态的,所以我们需要将Density设置为0。所有其他属性都可以保留其默认值。

  5. 我们希望起重机基座生成磁铁并设置关节。创建一个新的脚本,scr_CraneBase_Create,并将其附加到Create事件上。

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的速度下降磁铁。正如你所看到的,我们使用的值非常高,这是为了确保重磁铁可以吊起大量的钢渣。

  1. 与拆迁球一样,我们需要激活起重机基座。创建一个新的脚本,scr_CraneBase_Activate,并将其附加到Key Press下的Space事件以进行测试。
magnet.phy_active = true;
alarm[0] = 5 * room_speed;

我们希望磁铁首先下降,因此我们在物理世界中使其活动。我们使用了一个设置为五秒的闹钟,这将使磁铁重新上升。

  1. 创建一个新的脚本,scr_CraneBase_Alarm0,并将其附加到Alarm 0事件上。
physics_joint_set_value(crane, phy_joint_motor_speed, -20000);

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

  1. 起重机的最后一件事是在起重机底座和磁铁之间添加一根电缆。为此,我们将简单地在两者之间画一条线。创建一个新的脚本,scr_CraneBase_Draw,并将其应用于Draw事件。
draw_self();
draw_set_color(c_dkgray);
draw_line_width(x, y, magnet.x, magnet.y-16, 8);

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

  1. 现在我们只需要在Sandbox中添加一个起重机底座的实例。将实例放在现有柱子的左侧。还要添加一些碎片和小钢柱的实例,如下图所示:制作磁吸起重机

  2. 运行游戏。磁铁应该悬浮在空中,我们应该注意到碎片有些摇晃,好像发生了一些磁吸。当我们按下空格键时,磁铁应该下降并收集一些碎片。几秒钟后,磁铁将再次上升,带着收集到的碎片。还要注意,其他碎片或柱子都不受磁铁的影响。

完成游戏

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

设置赢的条件

这个游戏的赢的条件是清除特定区域内的所有柱子和碎片。玩家只能激活设备一次,并且有一小段时间来清理区域。如果他们清理了,他们就赢了并继续前进。如果没有清理,他们就输了,然后再试一次。

  1. 我们将首先创建一个父区域,其中包含所有代码,但实际上从未放置到世界中。创建一个新对象,obj_Zone_Parent。没有精灵可以附加。

  2. 创建一个新的脚本,scr_Zone_Create,并将其添加到Create事件。

image_speed = 0;
isTouching = true;

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

  1. 区域将需要不断更新是否清除碰撞。创建一个新的脚本,scr_Zone_Step,并将其附加到Step事件,并使用以下代码:
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来确定柱子父对象当前是否与区域接触。我们不能使用碰撞事件来检查接触,因为我们需要观察缺乏碰撞的发生。我们使用精灵的边界框参数来确定碰撞区域的大小。这将允许我们拥有多个区域精灵,而无需任何额外的代码。如果发生碰撞,我们切换到动画的第一帧,并指示当前正在发生碰撞。否则,我们切换到动画的第二帧,并指示区域当前没有碰撞。

  1. 现在我们已经建立了父区域,我们可以建立子区域,这些区域将放置在世界中。创建一个新的精灵,spr_Zone_01,并加载Chapter 6/Sprites/Zone_01.gif,勾选删除背景。将原点保留在X0 Y0,以便碰撞可以正常工作。点击确定

  2. 创建一个新对象,obj_Zone_01,并将spr_Zone_01应用为其精灵

  3. 我们希望区域始终绘制在塔的后面,所以将深度设置为100

  4. 将父对象设置为obj_Zone_Parent,然后点击确定

  5. 我们在Chapter 6中提供了一些额外的精灵以增加变化。使用适当的命名约定重复步骤 4 到 6 来创建额外区域。

  6. 打开Sandbox并放置一个obj_Zone_01的实例,以便它只覆盖一些玻璃柱,如下截图所示:设置胜利条件

  7. 运行游戏并激活设备。只要柱子或碎片在区域内,你应该看到区域保持红色。一旦清除,它将变成浅蓝色,表示它没有碰撞。

  8. 接下来,我们需要创建一个最高指挥官来检查胜利条件。创建一个新对象,命名为obj_Overlord

  9. 创建一个新脚本,scr_Overlord_Create,并将其附加到创建事件中,以便我们可以初始化一些变量。

isTriggered = false;
isVictory = false;

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

  1. 我们将取消对各个设备单独激活的操作,并将其放入最高指挥官中。重新打开obj_TNTobj_Anchorobj_CraneBase,并删除按键按下事件下的空格事件。

  2. 创建一个新脚本,scr_Overlord_Step,并将其添加到obj_OverlordStep事件中。

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;
}

这段代码只有在变量isTriggeredtrue时才会执行。如果是,我们检查是否存在 TNT 的实例。如果有实例,我们使用with语句来运行每个实例的激活脚本。对于 Anchor 和 Crane Base,我们也做同样的操作。我们还设置了一个 8 秒的警报,这时我们将检查胜利条件。最后,我们将isTriggered设置回false,这样它就会第二次运行。

  1. 让我们激活设备。创建一个新脚本,scr_Overlord_KeyPress,并将其添加到按键按下事件的空格事件下。
isTriggered = true;
  1. 在某些时候,我们可能希望在需要清除的关卡中有多个区域。这会带来一个小问题,我们需要确保所有区域都清除,但又不知道我们将以什么顺序检查每个区域。我们需要做的是让任何有碰撞的区域停止检查过程,并将胜利条件设置为false。创建一个新脚本,scr_WinCondition,并添加以下代码:
with (obj_Zone_Parent)
{
    if (isTouching)
    {
        return false;
    }
}
return true;

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

  1. 现在我们可以在警报事件中使用scr_WinCondition。创建一个新脚本,scr_Overlord_Alarm0,并将其添加到警报 0事件中。
isVictory = scr_WinCondition();
if (isVictory)
{
    if (room_exists(room_next(room)))
    {
        room_goto_next();
    }
} else {
    room_restart();
}

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

  1. 重新打开Sandbox并在房间中的任何位置放置一个obj_Overlord的单个实例。

  2. 我们不能只用一个房间来测试获胜条件,所以让我们复制“沙盒”并将其命名为Sandbox_02

  3. 重新排列房间中的柱子和设备,以便您可以确定它不是与沙盒相同的房间。还将区域移到离地面更近的位置,以确保获胜条件不会发生,如下面的截图所示:设置获胜条件

  4. 运行游戏并按空格键。在第一个房间中,我们应该看到一些破坏清除区域,几秒钟后,房间将切换到Sandbox_02。这次激活设备时,会有一些破坏,但区域中仍然会有柱子和碎片。几秒钟后,这个房间将重新开始。获胜条件达成!

创建装备菜单

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

  1. 为了构建装备菜单,我们需要几个精灵。创建新精灵,并从“第六章/精灵/”中加载适当的文件,对以下精灵进行取消背景的检查。将原点保留在X:0 和Y:0
  • spr_Menu_BG

  • spr_Menu_TNT

  • spr_Menu_WreckingBall

  • spr_Menu_MagneticCrane

  1. 创建一个新对象并将其命名为obj_Menu。我们不会为这个对象应用精灵。

  2. 我们只需要初始化一个变量来指示菜单何时处于活动状态。创建一个新脚本scr_Menu_Create,并将其应用到一个创建事件。

isActive = false;
  1. 在这个游戏中,我们需要不同大小的房间,这样我们就可以有高或宽的塔。这意味着菜单需要适应合适的大小。除非我们始终将屏幕大小设置为 640 x 480,否则这可能会非常令人沮丧。如果我们使用 GameMaker 的绘制 GUI事件,它会忽略世界定位,并使用基于窗口大小的坐标。创建一个新脚本scr_Menu_DrawGUI,并将其应用到一个绘制 GUI事件。
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);

由于我们知道每个房间都将以 640 x 480 的分辨率显示,所以我们首先在屏幕底部绘制背景精灵。我们将使用一个变量menuItem_Zone来帮助确定鼠标在精灵上的坐标。在未来的编码中,我们需要确切地知道图标放置的位置,因此我们为每个菜单项的坐标创建变量,然后在屏幕上绘制精灵。

  1. 重新打开“沙盒”并将房间的设置更改为宽度:800高度:600

  2. 视图选项卡下,勾选启用视图使用房间启动时可见的复选框。

  3. 在房间中查看更改为W:800 H:600。不要更改屏幕上的端口的值。通过这样做,我们将能够看到整个房间,并且它将以标准的 640 x 480 分辨率显示。

  4. 现在在房间中的任何位置放置一个obj_Menu的单个实例。

  5. 运行游戏。您应该看到屏幕底部有三个图标的菜单,如下面的截图所示:创建装备菜单

  6. 为了使菜单功能正常,我们首先需要创建所有的幽灵对象。我们不需要引入任何新的精灵,因为我们将使用每个设备部件的现有精灵。让我们首先创建一个新对象obj_Ghost_TNT,并将spr_TNT应用为精灵

  7. 创建一个新的脚本scr_Ghost_TNT_Create,并将其应用到一个Create事件,其中包含以下代码:

image_alpha = 0.5;
myTool = obj_TNT;

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

  1. 接下来,我们需要能够使用鼠标将此对象在房间中移动以进行放置。为此,我们将编写一个可以供所有幽灵使用的脚本。创建一个新脚本,scr_Ghost_Step,并将其应用于Step事件。
x = mouse_x;
y = mouse_y;
  1. 创建另一个新脚本,scr_Ghost_Released,并将其添加到鼠标下的左释放事件。
winHeight = window_get_height();
winMouse = window_mouse_get_y();
if (!place_meeting(x, y, obj_Pillar_Parent) &&  winMouse < winHeight - 64) 
{
    instance_create(x, y, myTool);
    obj_Menu.isActive = false;
    instance_destroy();
}

我们不希望能够将物品放在菜单顶部或其他我们试图摧毁的实例顶部。为了实现这一点,我们首先需要获取显示区域的高度和鼠标在显示区域内的位置。重要的是要注意,我们不能使用标准的mouseY变量,因为它与世界内的位置有关,而我们需要知道它在屏幕上的位置。我们检查当前位置在房间内是否与任何柱子发生碰撞,并且屏幕上的鼠标距离底部 64 像素,这确保它在菜单上方。如果这一切都是真的,我们创建一个要放置的物品实例,告诉菜单它不再活动,并从世界中移除幽灵。我们现在完成了幽灵 TNT。

  1. 接下来是幽灵挖掘球。创建一个新对象,obj_Ghost_WreckingBall,并将spr_Anchor指定为其精灵

  2. 我们有一些通用脚本,所以让我们快速应用它们。添加一个Step事件,应用scr_Ghost_Step,并在鼠标下添加一个左释放事件,附加scr_Ghost_Released

  3. 创建一个新脚本,scr_Ghost_WreckingBall_Create,并将其添加到Create事件。我们在这里只需要初始化放置时将创建的物品。

myTool = obj_Anchor;
  1. 我们无法像 TNT 那样完全构建这个,因为挖掘球由几个部分组成。对于这个幽灵,我们将需要一个Draw事件和一个新脚本,scr_Ghost_WreckingBall_Draw,其中包含以下代码:
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循环来绘制链条,然后挖掘球在链条末端绘制。最后,我们需要在代码末尾将透明度重置为完整。这一点非常重要,因为绘制事件会影响屏幕上绘制的所有内容。如果我们不重置它,世界中的每个对象都会有半透明度。

  1. 现在是幽灵磁吊机的时候了。创建一个新对象,obj_Ghost_MagneticCrane,并将spr_CraneBase应用为精灵

  2. 与其他幽灵一样,添加一个Step事件和一个左释放事件在鼠标下,并应用适当的脚本。

  3. 创建一个新脚本,scr_Ghost_MagneticCrane_Create,并初始化必要的变量。

myTool = obj_CraneBase;
  1. 现在绘制部件。创建另一个脚本,scr_Ghost_MagneticCrane_Draw,并将其添加为Draw事件。
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%。然后绘制起重机底座,绘制一条粗灰色线和磁铁,位置与放置时相同。然后将透明度恢复到完整。

  1. 幽灵现在全部完成了;我们只需要生成它们。重新打开scr_Menu_DrawGUI,并在末尾添加以下代码:
if (!isActive)
{
    win_X = window_mouse_get_x();
    win_Y = window_mouse_get_y();
    if ((win_Y > menuItems_Y - menuItem_Zone && win_Y < menuItems_Y + menuItem_Zone))
    {
        if ((win_X > menuItem1_X - menuItem_Zone && win_X < menuItem1_X + menuItem_Zone))
        {
            draw_sprite(spr_Menu_TNT, 1, menuItem1_X, menuItems_Y);
            if (mouse_check_button_pressed(mb_left))
            {        
                instance_create(menuItem1_X, menuItems_Y, obj_Ghost_TNT);
                isActive = true;
            }
        }
        if ((win_X > menuItem2_X - menuItem_Zone && win_X < menuItem2_X + menuItem_Zone))
        {
            draw_sprite(spr_Menu_WreckingBall, 1, menuItem2_X, menuItems_Y);
            if (mouse_check_button_pressed(mb_left))
            {        
                instance_create(menuItem1_X, menuItems_Y, obj_Ghost_WreckingBall);
                isActive = true;
            }
        }
        if ((win_X > menuItem3_X - menuItem_Zone && win_X < menuItem3_X + menuItem_Zone))
        {
            draw_sprite(spr_Menu_MagneticCrane, 1, menuItem3_X, menuItems_Y);
            if (mouse_check_button_pressed(mb_left))
            {        
                instance_create(menuItem1_X, menuItems_Y, obj_Ghost_MagneticCrane);
                isActive = true;
            }
        }
    }
}

我们首先检查菜单是否处于活动状态。如果选择了菜单项并且尚未放置,菜单将被视为活动状态。如果我们能够选择菜单项,我们就会在屏幕上抓取鼠标位置。我们首先检查屏幕上的鼠标位置,首先是 Y 坐标和区域偏移量,看看鼠标是否在菜单上方,然后是 X 坐标和每个项目的区域。如果鼠标在图标的顶部,我们会在第二帧动画上重绘精灵以指示悬停状态。然后我们检查左鼠标按钮是否被按下,如果是,我们就会生成相应的幽灵物品,菜单现在是活动的。现在我们可以生成 TNT、毁坏球和磁吊机。

  1. 运行游戏。我们已经在屏幕上有了菜单,但现在当你悬停在图标上时,它们应该会被突出显示。当你点击一个图标时,它会创建相应的幽灵物品,它会随鼠标移动。当你在可玩区域点击时,会创建一个物品的实例,并且可以使用。

构建塔楼

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

  1. 创建一个新的房间,在设置选项卡中,命名为Level_01。确保这个房间移动到资源树中房间部分的顶部。

  2. 打开物理选项卡,勾选房间是物理世界的框,并将重力设置为X:0 Y:20

  3. 对象选项卡中,选择obj_Ground并将实例放置在距离底部 64 像素的位置,横跨整个房间的宽度。菜单将占据底部 64 像素,所以我们不需要在那里放任何地面。

  4. 在地面下方的区域中添加obj_Overlordobj_Menu的单个实例。尽管从技术上讲它们可以放在房间的任何地方,但这样做会使事情更有条理。

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

  1. 只使用obj_Pillar_Glassobj_Pillar_Glass_Small,构建一个简单的两层塔,如下截图所示:构建塔楼

  2. 最后,在塔后面大致垂直中心放置一个obj_Zone_01的实例。这个房间现在完成了。

  3. 让我们建造游戏中更晚的最终房间,但这次要大得多,并且有多个区域。创建一个新的房间,在设置中命名为Level_12,将宽度更改为1280高度更改为960

  4. 在资源树中,将这个房间移动到Level_01之后。

  5. 这个房间现在是Level_01的两倍大,但我们希望以相同的大小在屏幕上显示它。在视图选项卡中,勾选启用视图房间开始时可见的框。

  6. 在房间中查看更改为W:1280 H:960。不要更改屏幕上的端口的值。通过这样做,我们将能够以标准的 640 x 480 分辨率看到整个房间。

  7. 物理选项卡中,勾选房间是物理世界的框,并将重力设置为X:0 Y:20

  8. 我们将从使用obj_Ground铺设地面开始。由于房间的大小加倍,我们的数字也需要加倍。在这个房间中,菜单将以 64 像素的屏幕分辨率高度显示,这意味着地面应该距离底部 128 像素。

  9. 在地面实例下方放置单个obj_Overlordobj_Menu

  10. 由于这个关卡意味着是游戏中的后期关卡,我们可以使用所有类型的小型和常规尺寸的柱子。建造几座高度和建筑材料不同的塔。

  11. 在每个塔后面添加一个obj_Zone_01的实例。关卡可能看起来像下面的截图所示:建造塔

  12. 运行游戏。第一关只需要几个放置得当的 TNT 就能成功摧毁它。下一关应该更难完成,并需要所有三种类型的装备。现在的挑战是看看需要多少装备才能摧毁一切。尽情地摧毁东西,就像下面的截图所示:建造塔

摘要

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

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

第七章:动态前端

在上一章中,我们构建了一个塔倒塌的物理游戏,玩家可以使用 TNT、挖掘球和磁吊机来摧毁由玻璃、木材和钢柱构建的塔。在本章中,我们将通过实现商店、得分屏幕和级别介绍对这个游戏进行构建。我们还将重新设计 HUD,以便只有可用的设备可以用于实现倒计时器,并添加重新开始级别和前往商店的按钮。为了完成所有这些,我们将花一些时间研究用于存储信息和使用全局变量的数组和数据结构。

设置房间

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

  1. 为从Level_02Level_11的每个级别创建一个新房间。将房间的大小设置如下:
  • Level_02Level_04的设置为宽度640高度480

  • Level_05Level_08的设置为宽度960高度720

  • Level_09Level_11的设置为宽度1280高度960

  1. 每个房间都需要在物理选项卡中勾选房间是物理世界

  2. 确保视图 | 屏幕上的端口设置为X0Y0W640,和H480,以便每个房间在屏幕上正确显示。

  3. 我们为每个级别提供了背景,可以在第七章/背景/中找到。确保没有勾选删除背景

  4. 每个级别应该有一个独特的塔,由各种柱子构建,理想情况下比上一个级别更难。首先在需要不同 Y 坐标的房间中放置地面,具体取决于房间的大小。Y 的放置如下所示:

  • Level_02Level_04: 384

  • Level_05Level_08: 576

  • Level_09Level_11: 784

  1. 在每个房间中放置一个obj_Overlord和一个obj_Menu的实例。每个房间应该看起来像下面的截图:设置房间

  2. 建立了级别之后,我们可以继续进行前端的工作。创建一个新房间,在设置中,命名为MainMenu,宽度为640,高度为480。将其移动到资源树中Rooms文件夹的顶部。

  3. 创建一个新的背景,bg_MainMenu,并加载第七章/背景/BG_MainMenu.png。确保没有勾选删除背景

  4. 房间属性 | 背景选项卡中,将背景 0设置为bg_MainMenu。应该勾选房间开始时可见的框。现在我们暂时完成了这个房间,点击确定

  5. 我们需要为前端再添加两个房间:LevelSelectShop,并应用适当的背景。资源树中的位置并不重要。现在我们已经拥有了游戏所需的所有房间。

初始化主菜单

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

  1. 让我们从一个用于初始化游戏的对象开始。创建一个新对象,命名为obj_Global

  2. 创建一个名为scr_Global_GameStart的新脚本。随着我们的进行,我们将向其中添加代码,但现在我们只需要初始化分数:

score = 0;
  1. 添加一个其他 | 游戏开始事件,并应用scr_Global_GameStart。点击确定

  2. 重新打开MainMenu,在房间中放置一个obj_Global的实例。

  3. 我们将创建一些按钮,因此让我们建立一个父对象来运行悬停状态的公共功能。创建一个名为obj_Button_Parent的新对象。

  4. 所有按钮都将有多个动画帧用于悬停状态,因此我们需要停止它们的播放。创建一个新的脚本,scr_Button_Parent_Create,并将其附加到创建事件,并使用以下代码:

image_speed = 0;
image_index = 0;
  1. 创建一个新的脚本,scr_Button_Parent_MouseEnter,并将其附加到鼠标 | 鼠标进入事件,代码用于将其更改为动画的第二帧:
image_index = 1;
  1. 我们还需要通过创建另一个新的脚本scr_Button_Parent_MouseLeave并将其附加到鼠标 | 鼠标离开事件来重置它。
image_index = 0;

父对象现在已经完成,设置应该如下截图所示:

初始化主菜单

  1. 接下来,我们可以构建第一个真正的按钮。创建一个新的精灵,spr_Button_Start,关闭删除背景,加载Chapter 7/Sprites/Button_Start.gif居中 原点,然后点击确定

  2. 创建一个新的对象,obj_Button_Start,并将spr_Button_Start应用为精灵

  3. 父对象设置为obj_Button_Parent,以便悬停状态能够正常工作。

  4. 由于每个按钮都会执行不同的操作,我们需要为每个按钮分配自己的点击事件。创建一个新的脚本,scr_Button_Start_MousePressed,并将其附加到鼠标 | 左键按下事件,使用以下代码前往房间LevelSelect

room_goto(LevelSelect);
  1. 这个按钮现在已经完成。将obj_Button_Start的单个实例放入MainMenu靠近屏幕底部的位置,X320Y416。房间应该看起来像下面的截图:初始化主菜单

  2. 运行游戏,确保它从MainMenu开始,并且Start按钮按预期工作。

使用 2D 数组选择级别

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

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

  1. 创建一个新的脚本,scr_Global_Levels,并开始初始化一些全局变量:
globalvar level, totalLevels;

由于我们一直在试图简化我们的代码,我们可以使用globalvar来声明全局变量的替代方法。这种声明方法与global完全相同,但它允许我们编写level而不是global.level。虽然这将为我们节省大量按键,但我们必须记住它是一个全局变量,因为它并不那么明显。

  1. 接下来,我们需要创建一个 2D 数组,其中一列保存级别,另一列保存它是否被锁定。让我们先添加第一个级别:
level[0, 0] = Level_01;
level[0, 1] = false;

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

  1. 在 GameMaker: Studio 中使用简单数组的一个缺点是没有函数可以找出数组的大小。我们需要知道这个数组的大小,以便我们可以动态创建所有的按钮。我们已经创建了一个全局变量来保存级别的总数;我们只需要手动设置它的值。让我们将所有级别添加到数组中,锁定它们,并设置totalLevels变量。以下是所有 12 个级别的完整脚本:
globalvar level, 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;
  1. 我们需要在游戏开始时初始化这个数组。重新打开scr_Global_GameStart,并在分数变量之后执行此脚本。
scr_Global_Levels();
  1. 让我们继续构建前往商店的按钮。创建一个新的精灵spr_Button_Shop,并关闭Remove Background,加载Chapter 7/Sprites/Button_Shop.gifCenter Origin并单击OK

  2. 创建一个新对象obj_Button_Shop,并将spr_Button_Shop应用为Sprite

  3. 这是一个标准按钮,所以将Parent设置为obj_Button_Parent

  4. 对于这个对象,我们需要做的最后一件事是添加一个Mouse | Left Pressed事件,并应用一个新的脚本scr_Button_Shop_MousePressed,其中包含切换房间的代码。

room_goto(Shop);
  1. 我们将在这些按钮上绘制一些文本,这意味着我们需要引入一些字体。我们在这个游戏中提供了一个名为 Boston Traffic 的字体,需要安装在您的计算机上。要在 Windows 计算机上安装此字体,请右键单击Chapter 7/Fonts/boston.ttf,然后选择安装。然后按照提示进行操作。

  2. 在 GameMaker: Studio 中,我们需要创建三种新字体:fnt_Largefnt_Mediumfnt_Small。所有三种字体都将使用Boston Traffic字体。将fnt_Large的大小设置为20fnt_Medium设置为16fnt_Small设置为10

  3. 接下来,我们可以继续创建用于选择关卡的按钮。我们将动态创建这些按钮,并在每个按钮上绘制一个数字,这样我们只需要一个单一的艺术资源。创建一个新的精灵spr_Button_LevelSelect,并关闭Remove Background,加载Chapter 7/Sprites/Button_LevelSelect.gifCenter Origin并单击OK

  4. 创建一个新对象obj_Button_LevelSelect,并将spr_Button_LevelSelect应用为Sprite。这些按钮不能作为obj_Button_Parent的子对象,因为它们需要具有锁定状态的能力,这将影响悬停状态。

  5. 由于这种按钮类型是独特的,我们需要初始化一些变量。创建一个新脚本scr_Button_LevelSelect_Create,并将其附加到Create事件中。

isLocked = true;
myLevel = MainMenu;
myNum = 0;
image_speed = 0;
alarm[0] = 1;

我们首先将所有按钮默认设置为锁定状态。我们为点击时应该转到的默认房间设置一个默认房间,并在顶部绘制一个数字。最后,我们停止精灵动画,并设置一个步骤的警报。

  1. 我们使用一个警报,以确保关卡是否被锁定都能正确显示。创建一个新脚本scr_Button_LevelSelect_Alarm0,并将其附加到Alarm | Alarm 0事件中。
if (isLocked)
{
    image_index = 2;
} else {
    image_index = 0;
}

如果按钮被锁定,我们将设置精灵显示锁定帧。否则,它是解锁的,我们显示第一帧。

  1. 创建一个新脚本scr_Button_LevelSelect_MouseEnter,并将其应用到Mouse | Mouse Enter事件中。
if (isLocked)
{   
    exit;
} else {
    image_index = 1;
}

对于按钮的悬停状态,我们首先检查它是否被锁定。如果是,我们立即退出脚本。如果未锁定,我们切换到悬停帧。

  1. 同样的逻辑需要应用到鼠标离开按钮时。创建另一个新脚本scr_Button_LevelSelect_MouseLeave,并将其应用到Mouse | Mouse Leave事件中。
if (isLocked)
{   
    exit;
} else {
    image_index = 0;
}
  1. 接下来,我们将添加一个Mouse | Left Pressed事件,并附加一个新的脚本scr_Button_LevelSelect_MousePressed,其中包含仅在解锁时更改房间的代码。
if (isLocked)
{
    exit;
} else {
    room_goto(myLevel);
}
  1. 最后,我们只需要一个新的脚本scr_Button_LevelSelect_Draw,我们可以用它来在按钮上绘制适当的数字。将其添加到Draw | Draw事件中。
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值来强制使用默认字体。这将有助于防止此字体影响游戏中的任何其他绘制字体:

  1. 现在我们已经完成了级别选择按钮,属性应该看起来像以下截图:使用 2D 数组选择级别

  2. 我们现在拥有了级别选择屏幕所需的所有组件,我们只需要生成所有内容。为此,我们将创建一个新对象,obj_LevelSelect_Overlord,以在进入房间时构建菜单。

  3. 添加一个其他 | 房间开始事件,并附加一个新的脚本,scr_LevelSelect_Overlord_RoomStart,其中包含以下代码:

column = 0;
row = 1;
for ( i = 0; i < totalLevels ; i++ )
{
    lvl = instance_create((72 * column) + 128, 80 * row + 128, obj_Button_LevelSelect);
    lvl.myLevel = level[i, 0];
    lvl.isLocked = level[i, 1];
    lvl.myNum = (i + 1);
    column++;
    if (column > 5)
    { 
        row++; 
        column = 0;
    }
}
instance_create(320, 440, obj_Button_Shop);

我们首先为按钮布局所需的行和列建立变量。然后我们从零开始运行一个循环,运行总级数我们在全局变量totalLevels中声明的次数。在这个循环中,我们首先创建一个obj_Button_LevelSelect的实例,并在水平和垂直方向上偏移它,额外增加 128 像素的填充,以便在屏幕边缘和按钮之间留出边距。然后我们通过设置level全局数组中的值来改变按钮的myLevelisLocked变量。接下来,我们改变myNum变量以指示按钮上将绘制的数字。代码的最后几行是我们如何限制列数并添加额外的按钮行。每次循环我们增加列数,一旦超过五,就将其重置为零。这将给我们一行六个按钮。如果我们有超过六个按钮,将创建一个新行,可以有另外六个按钮。这意味着我们可以稍后向数组中添加级别,并且它们将自动添加到此菜单中,为每六个级别创建新行。最后但并非最不重要的是,在屏幕底部生成一个SHOP按钮的实例。

  1. 打开LevelSelect,并在房间中放置一个obj_LevelSelect_Overlord的实例。这就是我们需要的全部,要这样做,请点击复选标记。

  2. 运行游戏。点击开始游戏后,您应该进入LevelSelect,并且它应该看起来像以下截图。目前只有 1 级可访问,按钮是黄色的。所有其他按钮都是灰色的,表示它们被锁定。点击Level 1按钮将带您到该级别,SHOP按钮应该带您到商店。使用 2D 数组选择级别

使用数据结构准备商店

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

  1. 在构建任何东西之前,我们需要做的第一件事是建立一些常量,以使我们的代码更易于阅读。打开资源 | 定义常量编辑器,并为装备设置值:TNT0WRECKINGBALL1MAGNET2

  2. 我们还需要一些常量来描述组成装备的所有元素。添加SPRITE0OBJECT1AMOUNT2COST3。完成后,编辑器中的设置应该如下截图所示:使用数据结构准备商店

  3. 为了保持游戏的颜色方案,我们需要创建一个全局访问的独特黄色。创建一个新脚本,scr_Global_Colors,其中包含以下代码:

globalvar yellow;
yellow = make_color_rgb(249, 170, 0);

我们为我们的颜色创建一个全局变量,然后使用一个带有红色、绿色和蓝色数量参数的函数来制作我们特殊的黄色。

  1. 打开scr_Global_GameStart并执行scr_Global_Colors()

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

  • :这种结构是后进先出的,意味着每个新的数据都被放置在前一个数据的顶部,当读取时,最新的数据首先被读取。可以把它想象成一叠盘子,你将使用最后放在架子上的那个。

  • 队列:这种结构是先进先出的,意味着每个新的数据都被放置在前一个数据的后面,当读取时,最旧的数据首先被读取。可以把它想象成商店里的排队,排在最前面的人将首先被服务。

  • 列表:这种结构更加灵活。在这种结构中,数据可以放置在列表中的任何位置,并且可以进行排序、修改和搜索。可以把它想象成一副扑克牌,可以以任何顺序放置,并且随时可以改变。

  • 映射:这种结构允许使用键和值的链接对存储信息,尽管它不能被排序,所有键必须是唯一的。可以把它想象成一组钥匙,每个钥匙只能打开相应的门。

  • 优先队列:这种结构类似于队列,但每个值都被分配了一个优先级。可以把它想象成夜店里的排队,VIP 有更高的优先级,先被放行。

  • 网格:这种结构是最健壮的,类似于 2D 数组。它有行和列,但有许多用于排序、搜索和操纵数据的函数。可以把它想象成一个可搜索的机场出发时间表,你可以看到所有的飞机、公司、飞行时间等,并根据自己的喜好进行排序。

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

globalvar equip;
equip = ds_grid_create(3,4);
ds_grid_set(equip, TNT, SPRITE, spr_Menu_TNT);
ds_grid_set(equip, TNT, OBJECT, obj_Ghost_TNT);
ds_grid_set(equip, TNT, AMOUNT, 1);
ds_grid_set(equip, TNT, COST, 100);
ds_grid_set(equip, WRECKINGBALL, SPRITE, spr_Menu_WreckingBall);
ds_grid_set(equip, WRECKINGBALL, OBJECT, obj_Ghost_WreckingBall);
ds_grid_set(equip, WRECKINGBALL, AMOUNT, 0);
ds_grid_set(equip, WRECKINGBALL, COST, 1000);
ds_grid_set(equip, MAGNET, SPRITE, spr_Menu_MagneticCrane);
ds_grid_set(equip, MAGNET, OBJECT, obj_Ghost_MagneticCrane);
ds_grid_set(equip, MAGNET, AMOUNT, 0);
ds_grid_set(equip, MAGNET, COST, 3000);

我们首先声明一个全局变量,然后使用它来保存网格的 ID。创建网格时,我们需要声明它需要多少行和列。对于这个游戏,我们有三行装备,每个装备有四列数据。我们分别为每个网格单元设置了值,所以槽 0 是要使用的精灵,槽 1 是要生成的对象,槽 2 是玩家起始的数量,最后,槽 3 是购买的成本。我们已经为每个装备做了这个设置,我们(玩家)将在游戏开始时只拥有单个 TNT。

  1. 重新打开scr_Global_GameStart并调用这个脚本。现在我们已经对所有的装备进行了分类,并为商店做好了准备。

  2. 接下来,我们需要为玩家创建一个库存,以跟踪他们购买了什么装备。由于玩家需要将装备添加到库存中,并且还将使用这些装备,我们需要一个易于改变的数据结构。我们将使用列表来实现这个目的。创建一个新的脚本,scr_Global_Inventory,并开始一个列表:

globalvar inventory;
inventory = ds_list_create();
ds_list_add(inventory, TNT);

我们声明一个全局变量,然后使用它来保存我们创建的列表的 ID。在游戏开始时,我们已经确定玩家将拥有一些 TNT,所以这就是我们在库存中需要的全部。

  1. 再次在scr_Global_GameStart中调用这个脚本。以下是完整的代码:
score  = 0;
scr_Global_Levels();
scr_Global_Colors();
scr_Global_Equipment();
scr_Global_Inventory();
  1. 现在我们已经存储了所有的数据,我们可以继续构建物品菜单。我们需要创建的第一个元素是一个购买按钮。创建一个新的精灵,spr_Button_Buy,并关闭删除背景,加载Chapter 7/Sprites/Button_Buy.gif居中 原点,然后点击确定

  2. 创建一个新对象,obj_Button_Buy,并将spr_Button_Buy分配为Sprite

  3. 这是一个标准按钮,所以将Parent设置为obj_Button_Parent

  4. 添加一个Mouse | Left Pressed事件,并应用一个新脚本,scr_Button_Buy_MousePressed,其中包含以下代码:

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,它在按钮本身中尚未初始化。稍后,当我们生成按钮时,我们将动态创建该变量。如果玩家可以购买该物品,我们增加玩家拥有的物品数量,并将金钱减去物品的价格。最后,我们检查玩家当前库存中是否已经有该物品。如果这是其类型的第一个物品,我们将其添加到库存列表中。

  1. 现在我们已经准备好用一个名为obj_Shop_Overlord的新对象在房间中生成所有东西。

  2. 添加一个Other | Room Start事件,并附加一个新脚本,scr_Shop_Overlord_RoomStart,其中包含在商店中生成所需按钮的代码:

for ( i = 0; i < ds_grid_width(equip); i++ )
{
    buyButton = instance_create(512, (96 * i) + 152, obj_Button_Buy);
    buyButton.myItem = i;   
}

instance_create(502, 440, obj_Button_Start);

我们首先通过每一行的装备网格运行一个循环,以便我们知道需要创建多少按钮。然后我们生成一个购买按钮,它将垂直堆叠在屏幕上。接下来,我们传递在鼠标按下事件中使用的myItem变量。我们做的最后一件事是在屏幕的右下角创建一个开始按钮,以便玩家可以返回到LevelSelect选项。

  1. 现在我们已经放置了所有的按钮,但我们仍然需要绘制所有其他必要的信息。创建一个新脚本,scr_Shop_Overlord_Draw,并将其添加到Draw | Draw事件中:
draw_set_color(c_black);
draw_set_halign(fa_center);

for ( i = 0; i < ds_grid_width(equip); i++ )
{
    draw_sprite(ds_grid_get(equip, i, SPRITE), 0, 96, (96 * i) + 152);
    draw_set_font(fnt_Small);
    draw_text(116, (96 * i) + 166, ds_grid_get(equip, i, AMOUNT));
    draw_set_font(fnt_Large);
    draw_text(300, (96 * i) + 140, ds_grid_get(equip, i, COST));    
}

首先,我们需要将字体颜色设置为黑色,并将文本居中对齐。然后我们通过装备网格运行一个循环来绘制每个组件。我们首先在正确的位置绘制正确的精灵,以与按钮对齐。在这里,我们使用小字体在精灵右下角的小空间中绘制玩家拥有的物品数量。然后我们改为大字体,并显示物品的价格。

  1. 菜单现在已经建好了,但仍然缺少一个重要的信息;玩家有多少现金。在脚本的末尾添加以下内容:
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);

我们将颜色设置为我们特殊的黄色,用于本文的其余部分。我们设置一个中等字体来显示单词现金,然后改为大字体显示实际金额。最后,我们将字体重置为默认值。

  1. 打开商店,并在房间的某个地方放置一个obj_Shop_Overlord对象的单个实例。我们已经完成了这个房间,所以点击确定

  2. 运行游戏并前往商店。此时您将无法购买任何物品,但您应该能够看到图标、按钮和信息正确显示。它应该看起来像下面的截图:使用数据结构准备商店

重建 HUD

游戏开发是一个迭代的过程,元素在需要时被添加进去,通常会被重新制作多次,因为功能被实现并且用户的反馈改变了项目的方向。以这种方式构建游戏可以节省时间,因为它允许我们快速完成任务,看到结果,并在进行中进行调整。在上一章中,我们专注于基本游戏玩法的功能。我们建立了一个简单的 HUD,允许我们通过点击生成每个装备。然而,我们没有限制玩家可以访问的装备,也没有能力重新开始级别或显示倒计时计时器,显示剩余多少时间来清除区域。我们需要修复所有这些,另外我们应该允许玩家前往商店,以防他们的供应不足。所有这些可以按以下方式完成:

  1. 我们将首先添加一些全局变量。创建一个新的脚本scr_Global_Gameplay,并声明必要的全局变量:
globalvar isGameActive, isTimerStarted;
isGameActive = true;
isTimerStarted = false;

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

  1. 打开scr_Global_GameStart并调用此脚本。

  2. 菜单还需要一些新的变量。打开scr_Menu_Create并添加以下代码:

timer = 10;
isTimerStarted = false;
menuItem_Zone = 32;
menuItems_Y = 440;
restartX = 468;
shopX = 564;
tempCost = 0;
tempScore = 0;
startEquip = ds_grid_create(3, 4);
ds_grid_copy(startEquip, equip);

第一个变量是玩家进行倒计时的时间。在这里,我们将给予区域被清除的十秒钟。然后我们将菜单的垂直位置设置在屏幕底部附近。接下来的两个变量是重启和商店按钮的水平位置。我们需要一些临时变量来保存使用的装备的值以及玩家在关卡中赚取的金额,因为我们不希望在玩家赢得关卡之前改变全局得分。最后,我们创建另一个网格并复制equip网格中的数据,以便如果关卡重新开始,我们仍然拥有原始设置。

  1. 我们还希望确保玩家如果库存中没有装备,则无法玩关卡。如果发生这种情况,我们将自动去商店。在脚本的顶部,添加以下代码:
if (ds_list_size(inventory) == 0)
{
    room_goto(Shop);
}

我们检查库存的大小,如果里面什么都没有,我们就去商店。

  1. 我们之前用于菜单的绘制脚本在当时的需求下运行得很好。然而,现在我们有了数据结构,我们可以简化系统并添加新功能。我们将首先创建一个新的脚本scr_Menu_Equipment,我们需要接受一些参数:
slot = argument0;
item = argument1;

if (slot == 0) { myX = 40; }
if (slot == 1) { myX = 104; }
if (slot == 2) { myX = 168; }

我们首先声明两个变量,这两个参数在调用此脚本时必须提供。参数只是在调用脚本或函数时从脚本或函数传递信息的变量。在这里,我们将在菜单上有一个插槽位置和一个要在插槽中显示的物品的声明。由于我们在菜单上有一个预定数量的插槽,为了是三个,我们可以检查传递的是哪个插槽并应用适当的水平偏移。

  1. 接下来,我们将添加菜单装备按钮的功能。添加以下代码:
draw_sprite(ds_grid_get(startEquip, item, SPRITE), 0, myX, menuItems_Y);
if (!isActive)
{   
    if (win_Y > menuItems_Y - menuItem_Zone && win_Y < menuItems_Y + menuItem_Zone)
    {
        if (win_X > myX - menuItem_Zone && win_X < myX + menuItem_Zone) 
        {
            draw_sprite(ds_grid_get(startEquip, item, SPRITE), 1, myX, menuItems_Y);
            if (mouse_check_button_pressed(mb_left) && ds_grid_get(startEquip, item, AMOUNT) > 0)
            {        
                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网格中提取的精灵。然后我们检查菜单是否处于活动状态,因为玩家试图放置物品。我们检查鼠标在屏幕上的位置,看它是否悬停在按钮上,并更改为适当的动画帧。如果点击按钮,我们创建所选的物品,从网格中减去一个物品单位,将物品的价值添加到玩家的支出中,并使菜单处于活动状态。

  1. 我们已经绘制了所有的装备按钮,但我们没有显示玩家在他们的库存中有多少物品。为了解决这个问题,在脚本的末尾添加以下代码:
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));

我们在这里所做的只是设置文本的颜色、水平对齐和字体。然后我们像在商店里一样在右下角绘制每个物品的单位数量。

  1. 现在我们有了改进和简化的装备按钮代码,我们可以回到scr_Menu_DrawGUI并删除所有旧的笨重代码。删除除绘制菜单背景的第一行代码之外的所有代码。一旦删除了,添加以下代码来绘制菜单:
if (isGameActive)
{
    Win_X = window_mouse_get_x();
    Win_Y = window_mouse_get_y();
    for (i = 0; i < ds_list_size(inventory); i++)
    {
        scr_Menu_Equipment(i, ds_list_find_value(inventory, i));
    }
}
draw_set_font(-1);

我们首先检查全局变量isGameActive是否为 true。如果为 true,我们获取鼠标的屏幕位置,以便正确放置菜单的位置。然后,我们为玩家在库存中拥有的物品运行一个循环,然后执行菜单装备脚本以绘制所有按钮。在脚本的最后,我们再次将字体设置回默认值。

  1. HUD 不仅需要装备按钮,还需要其他内容。对于这样的游戏,我们肯定需要一个允许玩家重新开始关卡的按钮。让我们快速创建一个新的精灵,spr_Button_Restart,并关闭Remove Background,加载Chapter 7/Sprites/Button_Restart.gifCenter Origin,然后单击OK

  2. 我们不需要为此按钮创建对象,因为它将绘制在菜单上。创建一个新的脚本,scr_Menu_Button_Restart,并编写以下代码:

draw_sprite(spr_Button_Restart, 0, restartX, menuItems_Y);
if (win_Y > menuItems_Y - menuItem_Zone && win_Y < menuItems_Y + menuItem_Zone)
{
    if (win_X > restartX - menuItem_Zone && win_X < restartX + menuItem_Zone)
    {
        draw_sprite(spr_Button_Restart, 1, restartX, menuItems_Y);
        if (mouse_check_button_pressed(mb_left))
        {        
            room_restart();
        }
    }
}

与装备按钮一样,我们首先以非悬停状态绘制按钮。然后检查鼠标是否悬停在按钮上,如果是,则将动画更改为悬停状态。如果点击按钮,我们重新启动房间。

  1. 重新打开scr_Menu_DrawGUI,并在创建装备按钮的循环之后调用此脚本。

  2. 我们还需要一个按钮,允许玩家访问商店。我们不能使用先前创建的按钮,因为我们需要它绘制在菜单上,而不是在世界中生成。幸运的是,我们可以使用它的精灵,所以我们只需要创建一个新的脚本,scr_Menu_Button_Shop,代码类似于所有其他菜单按钮:

draw_sprite(spr_Button_Shop, 0, shopX, menuItems_Y);
if (win_Y > menuItems_Y - menuItem_Zone && win_Y < menuItems_Y + menuItem_Zone)
{
    if (win_X > shopX - menuItem_Zone*2 && win_X < shopX + menuItem_Zone*2)
    {
        draw_sprite(spr_Button_Shop, 1, shopX, menuItems_Y);    
        if (mouse_check_button_pressed(mb_left)) 
        {        
            room_goto(Shop);
        }
    }
}

与以前一样,我们绘制精灵,然后检查鼠标是否悬停,确保我们将宽度更改为此精灵的较大尺寸。如果点击按钮,我们就会进入商店。

  1. 再次打开scr_Menu_DrawGUI,并在重新启动按钮之后立即调用此脚本。

  2. 我们几乎已经完成了 HUD,我们只需要向玩家显示一个非常重要的信息:剩余多少时间。这将完全通过文本完成,所以我们只需要创建一个新的脚本,scr_Menu_Clock

draw_set_color(yellow);
if (isTimerStarted)
{
    draw_set_font(fnt_Small);
    draw_text(320, 416,"COUNTDOWN");
    draw_set_font(fnt_Large);
    draw_text(320, 436, timer);
} else {
    draw_set_font(fnt_Small);
    draw_text(320,416,"PRESS SPACE TO");    
    draw_set_font(fnt_Large);
    draw_text(320,436,"DESTROY")
} 

背景是黑色的,所以我们将使用我们为所有文本创建的黄色。如果全局变量isTimerStarted为 true,我们以小写字母绘制单词"COUNTDOWN",并在其下方以大字体显示剩余时间。如果isTimerStarted为 false,我们将以类似的方式绘制文本,以指示玩家他们应该做什么。

  1. 重新打开scr_Menu_DrawGUI,并在商店按钮调用之后调用此脚本。完成的脚本应如下所示:
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 < ds_list_size(inventory); i++)
    {
        scr_Menu_Equipment(i, ds_list_find_value(inventory, i));
    }
    scr_Menu_Button_Restart();
    scr_Menu_Button_Shop();
    scr_Menu_Clock();
}
draw_set_font(-1);
  1. 要启动倒计时,我们需要激活它,在scr_Overlord_KeyPress中可以这样做。添加以下代码:
if (!isTimerStarted)
{
    obj_Menu.alarm[0] = room_speed;
    isTimerStarted = true;
}

我们检查isTimerStarted变量,看它是否已经被激活,因为我们只希望它发生一次。如果计时器尚未启动,它将在一秒钟内在菜单中打开一个警报。

  1. 我们需要做的最后一件事是打开obj_Menu,并添加一个Alarm | Alarm 0事件,附加一个新的脚本scr_Menu_Alarm0
if (timer > 0)
{
    timer -= 1;
    alarm[0] = room_speed;
} else {
    obj_Overlord.alarm[0] = 1;
}

菜单已经初始化了一个为十秒的计时器,在这个警报中,我们检查是否还有剩余时间。如果有,我们将时间减少一秒,并重置另一个秒的警报。这将重复直到时间到期,此时我们告诉 Overlord 立即运行胜利条件警报。

  1. HUD 现在正在控制时间,所以我们需要从 Overlord 中删除该功能。重新打开scr_Overlord_Step,并删除设置警报的代码行。

  2. 运行游戏并玩第一关。菜单有一个 TNT 的单个装备按钮,一个重新开始按钮和一个商店按钮。一旦按下空格键,倒计时器就会开始倒计时,直到为零。当时间到期时,房间将根据区域是否清除而重新启动或进入下一个房间。游戏应该看起来像下面的截图:重建 HUD

添加破坏的风险和回报

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

  1. 我们将从游戏结束屏幕开始。创建一个名为GameOver的新房间,并将bg_MainMenu应用为其背景。

  2. 创建一个新对象,obj_GameOver,不附加Sprite

  3. 创建一个变量,其中包含游戏结束消息,并设置一个五秒的闹钟,用于重新开始游戏。创建一个新脚本scr_GameOver_Create,并将其附加到Create事件:

gameOverText = "You ran out of money, better luck next time!";
alarm[0] = 5 * room_speed;
  1. 添加一个Alarm | Alarm 0事件,然后附加一个新脚本scr_GameOver_Alarm0,并重新启动游戏:
game_restart();
  1. 我们所要做的最后一件事就是绘制胜利/失败声明。创建一个新脚本scr_GameOver_Draw,并将其附加到Draw | Draw事件:
draw_set_color(c_black);
draw_set_halign(fa_center);
draw_set_font(fnt_Large);
draw_text(320, 280, "Game Over");
draw_set_font(fnt_Small);
draw_text(320, 320, gameOverText);
draw_set_font(-1);
  1. 如果它还没有打开,重新打开GameOver,并在房间中的某个地方放置一个obj_GameOver的单个实例。我们现在已经完成了这个,可以关闭房间了。

  2. 接下来我们要创建一个新对象obj_ScoreFloat,来显示每个柱子或碎片被摧毁时奖励的分数。

  3. 添加一个Create事件,使用一个新脚本scr_ScoreFloat_Create,并初始化两个变量:

fadeOut = 0;
alpha = 1;

我们将让分数随着时间淡出,所以我们有一个变量来触发淡出,还有一个用于透明度值的变量,目前设置为完全不透明。

  1. 接下来,我们需要添加一个Draw | Draw事件,使用一个新脚本scr_ScoreFloat_Draw来在屏幕上显示值:
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变量将在创建时从生成它的对象传递。最后,我们将透明度设置回完全不透明;否则整个房间中的其他所有东西也会淡出。

  1. 现在我们可以显示分数,我们需要生成它并传递一个值给它。由于我们已经知道每个柱子和碎片的质量不同,我们可以使用这个数字来在其被摧毁时奖励分数。重新打开scr_Pillar_BreakApart,在播放破碎声音后但实例被销毁之前插入以下代码:
scoreFloat = instance_create(x, y, obj_ScoreFloat);
scoreFloat.myValue = floor(phy_mass);
obj_Menu.tempScore += scoreFloat.myValue;

当柱子破碎时,它将生成一个obj_ScoreFloat的实例。然后我们将显示的值设置为对象总质量的向下取整。最后,我们将菜单的tempScore增加相同的数量。

  1. 我们需要让小柱子和碎片做同样的事情,所以打开scr_Pillar_Destroy,并在同样的位置插入相同的代码。

  2. 运行游戏并摧毁第一关的柱子。每块破碎的部分都会浮出一个数字,表示它的价值。浮动的数字应该在几秒钟后淡出,并且应该看起来像以下截图:为破坏添加风险和回报

  3. 现在我们只需要制作一个总结损坏并显示关卡总利润的得分屏幕。我们将首先引入一些额外的精灵,spr_Screen_BGspr_Button_NextLevel,都在第七章/精灵/中提供。确保不要删除背景,并为两者都居中设置原点

  4. 让我们创建一个新脚本scr_Menu_Button_NextLevel,实现此按钮的功能:

if (isVictory)
{
    draw_sprite(spr_Button_NextLevel, 0, nextLevelX, menuItems_Y);
    if (win_Y > menuItems_Y - menuItem_Zone && win_Y < menuItems_Y + menuItem_Zone)
    {
        if (win_X > nextLevelX - menuItem_Zone && win_X < nextLevelX + menuItem_Zone)
        {
            draw_sprite(spr_Button_NextLevel, 1, nextLevelX, menuItems_Y);
            if (mouse_check_button_pressed(mb_left))
            {
                for(i = 0; i < totalLevels; i++)
                { 
                    if (level[i, 0] == room)
                    { 
                        level[i+1, 1] = false;
                        room_goto( level[i+1, 0] );                        
                    }
                }    
            }
        }
    }
}

我们只希望下一关按钮在玩家成功清除区域时出现,因此我们首先检查这一点。如果玩家赢得了关卡,我们绘制精灵,然后检查鼠标是否悬停在其上。如果鼠标悬停在按钮上并按下,我们快速遍历关卡数组,查看我们当前所在的房间,并解锁下一关。最后,我们进入刚刚解锁的房间。

  1. 现在我们准备创建一个新对象obj_ScoreScreen,用于显示得分屏幕。将深度设置为-100,以便它始终显示在所有其他 GUI 元素的顶部。

  2. 为新脚本scr_ScoreScreen_Create添加一个Create事件,并初始化以下变量:

isGameActive = false;
obj_Menu.isActive = true;
isVictory = scr_WinCondition();
screenX = 320;
screenY = 200;
menuItem_Zone = 32;
menuItems_Y = 440;
restartX = 200;
shopX = 320;
nextLevelX = 440;

我们不希望玩家在这段时间内进行游戏,因此我们关闭isGameActive变量,并激活菜单,使得装备按钮不再起作用。接下来,我们需要检查玩家是否成功,以便知道要绘制什么。最后的七个变量都是用于放置我们将显示的各种文本和按钮。

  1. 现在添加一个Draw | Draw GUI事件,使用新脚本scr_ScoreScreen_DrawGUI,我们将首先绘制所需的所有文本:
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, "Damage Estimate");
draw_text(screenX, 180, "Equipment Cost");
draw_text(screenX, 260, "Total Profit");
draw_set_font(-1);

首先绘制背景精灵。然后设置颜色、对齐和字体。我们使用最大的字体来绘制房间的名称和损坏量、使用的装备量和总利润的值。然后切换到中等字体,写出每个值的描述,放在相应数字的上方。我们完成了绘制文本,所以将字体设置回默认值。

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

就像我们在菜单中所做的那样,我们获取屏幕上鼠标的坐标,然后执行三个按钮的脚本。

  1. 为了激活得分屏幕,我们需要重新打开scr_Overlord_Alarm0,并让它生成obj_ScoreScreen的一个实例,而不是当前运行的代码。删除所有代码,并用以下代码替换它:
instance_create(0, 0, obj_ScoreScreen);
  1. 运行游戏并完成第一关。计时器结束后,得分屏幕将显示损坏、成本和利润。游戏菜单已消失,并被三个按钮替换,用于重新玩关卡、前往商店或下一关。它应该看起来像以下截图:为破坏添加风险和回报

  2. 我们需要解决一个问题。虽然屏幕显示我们已经赚了钱,但如果我们去商店,将没有现金可用。这是因为我们还没有将临时值转移到全局值,我们将在一个名为scr_ScoreCleanUp的新脚本中完成这个操作:

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

        if (e == 0)
        {
            inv = ds_list_find_index(inventory, i);
            ds_list_delete(inventory, inv);
        }
    }
}

当执行此脚本时,它将进入菜单,并将剩余的装备复制到全局装备值中,然后从内存中删除临时网格。接下来,根据游戏过程中发生的情况增加全局得分。然后,我们通过库存运行循环,查找玩家是否已用完任何物品。如果是,我们将其从库存中删除。

  1. 如果玩家进入下一关,我们应该立即支付他们。我们还应该检查分数,以确保玩家有钱。如果他们钱花光了,那就是游戏结束,否则他们可以进入下一关。重新打开scr_Menu_Button_NextLevel并用以下代码替换切换房间的那行代码:
scr_ScoreCleanUp(); 
if (score < 0)
{
    room_goto(GameOver);
} else {
    room_goto( level[i+1, 0] );
}
  1. 如果玩家决定去商店,情况会变得有点棘手。我们正在调用的脚本也在菜单上使用,所以我们不希望它在游戏进行时改变数据。重新打开scr_Menu_Button_Shop并用以下代码替换切换房间的那行代码:
if (!isGameActive) { scr_ScoreCleanUp();}
if (score < 0)
{
    room_goto(GameOver);
} else {
    room_goto(Shop);
}

现在只有在游戏停止时才会传输分数。我们还在这里检查游戏结束状态的分数,以决定点击时要去哪个房间。

  1. 现在一切应该正常工作了,所以运行游戏并检查一下,确保在游戏进行时去商店时分数不会改变。

为每个级别添加介绍性文本

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

  1. 我们需要一个按钮来开始关卡,同样需要在屏幕上绘制。创建一个新脚本scr_Menu_Button_Start,其中包含一些非常熟悉的代码:
draw_sprite(spr_Button_Start, 0, startX, startY);
if (win_Y > startY - start_ZoneHeight && win_Y < startY + start_ZoneHeight)
{
    if (win_X > startX - start_ZoneWidth && win_X < startX + start_ZoneWidth)
    {
        draw_sprite(spr_Button_Start, 1, startX, startY);
        if (mouse_check_button_pressed(mb_left)) 
        {        
            isGameActive = true;
            instance_destroy();
        }
    }
}

所有标准按钮代码都在这里,但当按钮被点击时,我们激活游戏并销毁 Story screen 对象。这里使用的start_ZoneWidthstart_ZoneHeight变量尚未初始化,但我们很快就会做到。

  1. 接下来,我们需要所有我们想要为每个关卡显示的文本。为此,我们将使用一个地图数据结构,以便我们可以将文本链接到关卡。创建一个新脚本scr_Global_Dialogue,并编写我们需要的对话:
globalvar dialogue;
dialogue = ds_map_create();
ds_map_add(dialogue, Level_01, "Welcome to Destruct! A tower toppling game. 
# Let's start with some basic training. Here we have a glass tower that needs to come down. You have one stick of TNT to use to completely clear the Zone. 
# Let's see what you can do.");
ds_map_add(dialogue, Level_02, "Temporary Dialogue for Level 02");
ds_map_add(dialogue, Level_03, "Temporary Dialogue for Level 03");
ds_map_add(dialogue, Level_04, "Temporary Dialogue for Level 04");
ds_map_add(dialogue, Level_05, "Temporary Dialogue for Level 05");
ds_map_add(dialogue, Level_06, "Temporary Dialogue for Level 06");
ds_map_add(dialogue, Level_07, "Temporary Dialogue for Level 07");
ds_map_add(dialogue, Level_08, "Temporary Dialogue for Level 08");
ds_map_add(dialogue, Level_09, "Temporary Dialogue for Level 09");
ds_map_add(dialogue, Level_10, "Temporary Dialogue for Level 10");
ds_map_add(dialogue, Level_11, "Temporary Dialogue for Level 11");
ds_map_add(dialogue, Level_12, "Temporary Dialogue for Level 12");

我们创建了一个新的全局变量,并将其附加到我们创建的地图数据结构上。对于每个条目,我们需要一个Key和一个Value。在这里,我们使用每个房间的名称作为键,并将对话写为值。我们需要为游戏中的每个房间都有文本,以免出错,所以我们为房间 2-12 提供了临时对话,您可以用自己的文本替换。在 Level 01 的对话中,我们使用#,这是一个特殊字符,用于开始新的段落。这将使大量文本更易读。

  1. 打开scr_Global_GameStart并调用这个脚本。

  2. 我们已经拥有了所有需要的艺术资源,但我们需要一个新的对象obj_StoryScreen,深度为-100

  3. 添加一个Create事件并应用一个新脚本scr_StoryScreen_Create来初始化变量:

isGameActive = false;
screenX = 320;
screenY = 200;
startY = 440;
startX = 320;
start_ZoneWidth = 128;
start_ZoneHeight = 32;
myText = ds_map_find_value(dialogue, room);
textLength = 0;

我们停止游戏并设置六个变量来确定我们将要绘制的文本的位置。然后根据玩家当前所在的房间从地图中加载文本。我们最后一个变量textLength将用于一个库存效果,文本看起来会随着时间而被打出来。

  1. 接下来,我们需要添加一个Draw | Draw GUI事件,并使用一个新脚本scr_StoryScreen_DrawGUI来绘制一切:
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, string(room_get_name(room)));

draw_set_halign(fa_left);
draw_set_font(fnt_Small);
textLength++;
writeText = string_copy(myText, 1, textLength);
draw_text_ext(160, 120, writeText, -1, 320);
draw_set_font(-1);

win_X = window_mouse_get_x();
win_Y = window_mouse_get_y();
scr_Menu_Button_Start();

与得分屏幕一样,我们绘制背景并设置标题的颜色、对齐方式和字体。接下来是对话框在屏幕上显示的打字效果。我们改变对话框的对齐方式和字体,然后开始逐步增加textLength变量。这个值决定了需要复制到writeText变量的对话中有多少个字符,这意味着文本会随着时间增长。我们使用draw_text_ext函数,它允许我们限制段落在下移一行之前可以有多宽,本例中为 320 像素。最后,我们再次获取鼠标位置以使开始按钮工作。

  1. 我们要做的最后一件事就是在scr_Overlord_Create中生成一个 Story screen 的实例:
instance_create(0, 0, obj_StoryScreen);
  1. 运行游戏并进入第一关。故事画面出现,对话开始一次出现一个字母,应该看起来像下面的图片。当点击开始按钮时,游戏玩法如常开始。为每个级别添加介绍性文本

保存玩家的进度

游戏中的润色并不总是关于视觉装饰。有时也涉及添加一些不会立即被注意到但可以极大改善整体体验的小功能。目前游戏看起来不错,玩起来也很顺畅,但如果我们关闭浏览器然后在以后的时间返回来玩,我们将需要重新开始。如今的玩家期望他们可以回到游戏并从他们离开的地方继续。为了做到这一点,我们需要保存玩家的进度。

理解本地存储

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

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

注意

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

写入本地存储

对于这个游戏,我们将保存所有与解锁的级别、累积现金金额和购买的装备相关的数据。为了保存游戏数据,我们需要写入文件。GameMaker: Studio 有两种文件格式可以处理 HTML5 游戏:文本文件Ini 文件。文本文件适用于读取或写入大量数据,并且可以按任何您选择的方式进行结构化。Ini 文件用于较小量的数据,并使用部分/键/值结构。这种结构将数据分成单独的部分,在每个部分中将有键/值对,看起来像这样:

[section]
key = value
[playerData]
playerFirstName = Jason
playerLastName = Elliott

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

  1. 任何保存系统需要做的第一件事是创建一个具有所需结构和设置的保存文件。创建一个新的脚本,scr_GameSave,并编写以下代码:
theFile = argument0;
ini_open(theFile);
ini_write_real("Score","Cash", score);
for (i = 0; i < totalLevels; i++)
{
    ini_write_string("Levels", string("Level_" + i), level[i, 1]);
}
for ( j = 0; j < ds_grid_width(equip); j++ )
{
    ini_write_real("Equipment",string("Equip_" + j), ds_grid_get(equip, j, AMOUNT));
}
ini_close(); 

当我们执行这个脚本时,我们需要传递文件的名称作为参数。然后我们可以打开请求的文件,或者如果找不到文件,将创建一个打开。一旦文件打开,我们就可以写入所有必要的数据。我们首先写入一个Score部分,使用一个名为Cash的键来设置分数的值。我们使用级别数组运行一个循环,在Levels部分中存储每个级别以及它是否已解锁。接下来我们运行另一个循环,这次是遍历装备网格,并写入玩家当前在游戏中拥有的每种物品的数量。最后,在所有数据都被写入后,我们关闭文件。

  1. 保存游戏只有在实际加载数据时才有用。创建一个新脚本,scr_GameLoad,以便我们可以从文件中读取。
theFile = argument0;
if (!file_exists(theFile)) 
{
    scr_GameSave(theFile); 
} else {
    ini_open(theFile);
    score = ini_read_real("Score","Cash", "");
    for (i = 0; i < totalLevels; i++)
    {
        level[i, 1] = ini_read_string("Levels", string("Level_" + i), "");
    }
    for ( j = 0; j < ds_grid_width(equip); j++ )
    {
        ds_grid_set(equip, j, AMOUNT, ini_read_real("Equipment",string("Equip_" + j), ""));
        if (ds_list_find_index(inventory, j) == -1 && ds_grid_get(equip, j, AMOUNT) > 0)
        {
            ds_list_add(inventory, j);
        }
    }   
    ini_close();
}

我们首先检查通过参数传递的文件是否存在。如果在本地存储中找不到文件,比如游戏首次运行时,我们会运行保存脚本来初始化数值。如果找到文件,我们会打开保存文件并将数据读入游戏,就像我们保存的那样。我们设置分数,解锁适当的关卡,并加载装备。我们还会遍历库存,以确保所有装备对玩家可用。

  1. 我们希望在游戏开始时加载任何游戏数据。打开scr_Global_GameStart,并在脚本末尾添加以下代码:
globalvar saveFile;
saveFile = "Default.ini";
scr_GameLoad(saveFile);

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

  1. 保存游戏的最合逻辑的地方是在玩家完成每个关卡后。打开scr_ScoreCleanUp,并在最后的大括号之前插入对scr_GameSave的调用。整个脚本如下所示:
with (obj_Menu)
{
    ds_grid_copy(equip, startEquip);
    ds_grid_destroy(startEquip);
    score += tempScore - tempCost;
    for ( i = 0; i < ds_grid_width(equip); i++)
    {
        e = ds_grid_get(equip, i, AMOUNT);

        if (e == 0) 
        {
            inv = ds_list_find_index(inventory, i);
            ds_list_delete(inventory, inv);
        }
    }
    scr_GameSave(saveFile);
}
  1. 当玩家在商店购买装备时,我们还需要保存游戏。打开scr_Button_Buy_MousePressed,并在最后的大括号之前插入对scr_GameSave的调用。

  2. 保存游戏并玩几个关卡。完成几个关卡后,刷新浏览器。您应该看到您的现金、装备和已解锁的关卡仍然保持不变。

保存多个游戏配置文件

我们现在有一个可以保存玩家进度的游戏,但没有清除数据的方法,如果他们想重新玩游戏。正如我们已经提到的,删除数据的唯一选择是让用户清除其浏览器缓存或覆盖数据,这两种选择都有缺点。大多数用户不会想清除其缓存,因为这将删除本地存储中的所有数据,而不仅仅是游戏数据。如果多个人想在同一浏览器中玩游戏,覆盖数据会有问题。只有一个保存文件变得毫无意义。我们还有第三个可用的选项,即我们根本不清除数据,而是创建可以随时加载的额外保存文件。我们当前的保存/加载系统已经准备好让我们拥有多个用户配置文件,我们只需要添加一个输入系统来捕获用户的名称。我们将保持系统相当简单,将其放在前端,并将用户名称限制为最多八个字符。当玩家点击开始按钮时,它将在切换房间之前加载适当的配置文件:

  1. 我们将首先添加一个用于玩家名称的全局变量。打开scr_Global_GameStart,初始化playerName变量,并在脚本末尾将其设置为空字符串。
globalvar playerName;
playerName = "";
  1. 我们需要创建一个新的对象,obj_NameInput,用于跟踪玩家的输入。由于我们将在屏幕上绘制文本,因此它不需要精灵。

  2. 添加一个Create事件,附加一个名为scr_NameInput_Create的新脚本,用于初始化字符串的长度和已输入字符的数量的变量。

nameSpace = 0;
nameMax = 8;
  1. 接下来,我们将添加一个Draw | Draw事件,附加一个名为scr_NameInput_Draw的新脚本,用于绘制玩家输入的名称以及一个简单的指示,告诉玩家输入他们的名字:
draw_set_color(c_black);
draw_set_halign(fa_center);
draw_set_font(fnt_Small);
draw_text(320, 280, "Type In Your Name");
draw_set_font(fnt_Large);
draw_text(320, 300, playerName);
draw_set_font(-1);
  1. 现在我们已经在屏幕上显示了所有内容,我们需要收集键盘输入。添加一个Key Press | Any Key事件,并附加一个名为scr_NameInput_KeyPressed的新脚本。
if (nameSpace < nameMax) 
{    
    if (keyboard_key >= 65 && keyboard_key <= 90) 
    {
        playerName = playerName + chr(keyboard_key);
        nameSpace++;
    }
}

我们只希望名称最多为八个字母,因此我们首先检查当前名称是否仍有空间。如果我们可以输入另一个字母,然后我们检查正在按下的键是否是字母。如果按下了字母,我们将该字母添加到字符串的末尾,然后指示另一个空间已被使用。

  1. 如果我们现在运行游戏,我们将能够输入字母,但我们无法撤消任何字母。我们可以使用以下代码来解决这个问题:
if (keyboard_key == vk_backspace) 
{  
    lastLetter = string_length(playerName);
    playerName = string_delete(playerName, lastLetter, 1)
    if (nameSpace > 0)
    {
        namespace--;
    }
}

如果用户按下退格键,我们获取字符串的长度,以找出字符串的最后一个空格在哪里。一旦我们知道了这一点,我们就可以删除字符串末尾的字母。最后,我们检查是否仍然有剩余的字母,如果有,就减少空格计数。这是必要的,这样我们就不会进入负空间。

  1. 打开MainMenu,在房间的某个地方放置一个obj_NameInput的单个实例,位置无关紧要。

  2. 保存并玩游戏。在前端,你应该能够输入最多八个字母的名字,并通过点击退格键删除所有这些字母。它应该看起来像下面的截图:保存多个游戏配置文件

  3. 保存系统现在已经完成;剩下要做的就是在玩家点击开始按钮时加载数据。由于我们在商店和主菜单中都使用开始按钮,我们需要运行一个检查,以确保我们只在游戏开始时加载游戏数据。打开scr_Button_Start_MousePressed,在房间改变之前,添加以下代码:

if (room == MainMenu)
{
    saveFile = string(playerName + ".ini");
    scr_GameLoad(saveFile);
}
  1. 保存并玩游戏。使用你的名字玩游戏,完成几个级别。然后刷新页面并输入一个不同的名字。当你到达级别选择时,只有第一个房间应该是可用的。

  2. 刷新浏览器第二次,再次使用你的名字。这次当你到达级别选择时,你应该能看到所有解锁的级别。保存系统有效!

总结

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

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

第八章:玩转粒子

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

介绍粒子效果

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

理解粒子系统

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

利用粒子发射器

粒子发射器是系统内定义的区域,粒子将从这些区域产生。有两种类型的发射器可供选择:爆发发射器一次性产生粒子,发射器随时间不断喷射粒子。我们可以定义每个发射器在空间中的大小和形状区域,以及粒子在区域内的分布方式。

利用粒子发射器

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

利用粒子发射器

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

应用粒子

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

应用粒子

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

注意

设计粒子事件的外观通常是一个漫长的试错过程。为了加快速度,可以尝试使用互联网上提供的许多粒子效果生成器之一,比如 Alert Games 的 Particle Designer 2.5,网址为:alertgames.net/index.php?page=s/pd2

HTML5 的限制

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

将粒子效果添加到游戏中

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

创建一个尘埃云

我们要创建的第一个效果是一个简单的尘埃云。它将在每个柱子被摧毁时向外迸发,并随着时间的推移消失。由于这个效果将在游戏的每个级别中使用,我们将使其所有元素全局化,因此只需要声明一次。

  1. 打开我们之前正在工作的倒塔项目,如果还没有打开的话。

  2. 在构建游戏时,我们需要确保启用了 WebGL。导航到资源 | 更改全局游戏设置,然后点击HTML5选项卡。

  3. 在左侧,点击图形选项卡。如下截图所示,在选项下有三个WebGL选项。如果禁用了 WebGL,游戏将无法使用 GPU,所有浏览器都会受到潜在的延迟影响。如果需要WebGL,任何不支持此功能的浏览器都将无法运行游戏。最后一个选项是自动检测,如果浏览器支持它,将使用 WebGL,但无论如何都允许所有浏览器玩游戏。选择自动检测,然后点击确定创建一个尘埃云

  4. 现在我们已经激活了 WebGL,我们可以构建我们的效果。我们将首先通过创建一个名为scr_Global_Particles的新脚本,将我们的粒子系统定义为全局变量。

globalvar system;
system = part_system_create();
  1. 我们要制作的第一个效果是尘埃云,它将附着在柱子上。为此,我们只需要一个发射器,当需要时我们将其移动到适当的位置。创建一个发射器的全局变量,并在脚本的末尾添加以下代码将其添加到粒子系统中:
globalvar dustEmitter;
dustEmitter = part_emitter_create(system);
  1. 对于这个粒子,我们将使用内置形状之一pt_shape_explosion,看起来像一团浓密的尘埃云。在脚本的末尾添加以下代码:
globalvar particle_Dust;
particle_Dust = part_type_create();
part_type_shape(particle_Dust, pt_shape_explosion);

我们再次将其设置为全局变量,这样我们只需要创建一次这个尘埃云粒子。目前我们只声明了这个粒子的形状属性。一旦我们在游戏中看到效果是什么样子,我们将再添加更多内容。

  1. 我们需要用其他全局变量初始化粒子系统。重新打开scr_Global_GameStart并调用粒子脚本。
scr_Global_Particles();
  1. 一切初始化完成后,我们现在可以创建一个新的脚本scr_Particles_DustCloud,用于设置发射器的区域并激活一次粒子爆发。
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 个尘埃粒子的单次爆发。

  1. 现在我们只需要从柱子的破坏中执行这个脚本。重新打开scr_Pillar_Destroy并在实例被销毁之前的一行插入以下代码:
scr_Particles_DustCloud();
  1. 我们还需要将这个效果添加到柱子的破碎中。重新打开scr_Pillar_BreakApart并在同一位置插入相同的代码。

  2. 保存游戏,然后进行游戏。当玻璃柱被摧毁时,我们应该看到厚厚的白色云朵出现,如下图所示:创建尘埃云

  3. 这时粒子很无聊且静止,因为我们除了让粒子看起来像云的形状之外,还没有告诉粒子做任何事情。让我们通过向粒子添加一些属性来解决这个问题。重新打开scr_Global_Particles并在脚本的末尾添加以下代码:

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

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

  1. 保存游戏并再次运行。这次效果看起来更自然,云朵向外爆炸,稍微变大,然后消失。它应该看起来像下一个截图。尘埃云现在已经完成。创建尘埃云

添加弹片

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

  1. 创建一个新的 Sprite,spr_Particle_Glass,并勾选删除背景,加载Chapter 8/Sprites/Particle_Glass.gif。这个 Sprite 不是用来做动画的,尽管它内部有几个帧。每一帧代表一个将在生成粒子时随机选择的不同形状的粒子。

  2. 我们希望粒子在向外移动时旋转,因此我们需要将原点居中。点击确定

  3. 重新打开scr_Global_Particles,并在脚本的末尾初始化玻璃粒子。

globalvar particle_Glass;
particle_Glass = part_type_create();
part_type_sprite(particle_Glass, spr_Particle_Glass, false, false, true);

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

  1. 我们还需要为这种粒子添加一些生命和运动属性。在脚本的末尾添加以下代码:
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,因为我们只希望粒子自由旋转。

  1. 为了测试这种效果,打开scr_Particles_DustCloud,并在发射尘埃云之前插入一个爆发发射器,这样玻璃粒子就会出现在其他效果的后面。
part_emitter_burst(system, dustEmitter, particle_Glass, 8);
  1. 保存游戏,然后玩游戏。当柱子破碎时,应该会有玻璃碎片和尘埃云一起爆炸出来。效果应该看起来类似于以下截图:添加弹片

  2. 接下来,我们需要为木材和钢铁粒子创建弹片。以与我们为玻璃做的方式相同,在Chapter 8/Sprites/中创建新的 Spritesspr_Particle_Woodspr_Particle_Steel

  3. 由于这些粒子是全局的,我们不能动态地交换 Sprite。我们需要为每种类型创建新的粒子。在scr_Global_Particles中,添加木材和钢铁的粒子,属性与玻璃相同。

  4. 目前,效果被设置为始终创建玻璃粒子,这是我们不想做的。为了解决这个问题,我们将在每个不同的柱子中添加一个变量myParticle,以允许我们生成适当的粒子。打开scr_Pillar_Glass_Create,并在脚本的末尾添加以下代码:

myParticle = particle_Glass;
  1. 使用适当的粒子重复最后一步,为木材和钢铁分配适当的粒子。

  2. 为了正确生成粒子,我们只需要重新打开scr_Particles_DustCloud,并将变量particle_Glass更改为myParticle,如下所示:

part_emitter_burst(system, dustEmitter, myParticle, 8);
  1. 保存游戏并玩游戏,直到你可以摧毁所有三种类型的柱子以查看效果。它应该看起来类似于以下截图,其中每个柱子都会生成自己的弹片:添加弹片

制作 TNT 爆炸

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

  1. 我们将首先创建一些自定义颜色,以便实现我们想要的火焰和烟雾效果。打开scr_Global_Colors并添加以下颜色:
orange = make_color_rgb(255, 72, 12);
fireWhite = make_color_rgb(255, 252, 206);
smokeBlack = make_color_rgb(24, 6, 0);

我们已经有了一个漂亮的黄色,所以我们添加了一个橙色、一个略带黄色色调的白色,以及一个部分橙色的黑色。

  1. 为了实现虚假混合效果,我们需要产生一种粒子类型,并在其死亡时产生下一种粒子类型。为了使其正常工作,我们需要按照它们将被看到的相反顺序构建粒子的创建。在这种情况下,我们需要从烟雾粒子开始构建。在scr_Global_Particles中添加一个新的烟雾粒子,具有以下属性:
globalvar particle_Smoke;
particle_Smoke = part_type_create();
part_type_shape(particle_Smoke, pt_shape_smoke);
part_type_life(particle_Smoke, 30, 50);
part_type_direction(particle_Smoke, 80, 100, 0, 0);
part_type_speed(particle_Smoke, 2, 4, 0, 0);
part_type_size(particle_Smoke, 0.6, 0.8, 0.05, 0);
part_type_alpha2(particle_Smoke, 0.5, 0);
part_type_color1(particle_Smoke, smokeBlack);
part_type_gravity(particle_Smoke, 0.4, 90);

我们首先添加粒子并使用内置的烟雾形状。我们希望烟雾停留一段时间,所以我们将其寿命设置为一秒到接近两秒。然后,我们设置方向和速度大致向上,这样烟雾就会上升。接下来,我们设置大小并随着时间增长。对于 alpha 值,我们不希望烟雾完全不透明,所以我们将其设置为半透明并随着时间消失。接下来,我们使用part_type_color1,这样可以给粒子着色而不会对性能产生太大影响。最后,我们对粒子施加一些重力,这样任何倾斜的粒子都会慢慢向上飘浮。

  1. 烟雾是我们效果的最后一步,它将从先前的橙色火焰中产生。
globalvar particle_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);

我们再次使用内置的烟雾形状来设置粒子,这次寿命要短得多。总体方向仍然主要向上,尽管比烟雾更散。这些粒子稍小,呈橙色,整个寿命都将是部分透明的。我们增加了一点向上的重力,因为这个粒子介于火和烟雾之间。最后,我们使用一个函数,当每个橙色粒子死亡时会产生一个烟雾粒子。

  1. 这种效果链中的下一个粒子是黄色粒子。这次我们将使用 FLARE 形状,这将给火焰一个更好的外观。它也会稍小一些,比橙色粒子活得稍长一些,速度更快,向各个方向扩散。我们不会给这个粒子添加任何透明度,这样它看起来会燃烧得更明亮。
globalvar particle_FireYellow;
particle_FireYellow = part_type_create();
part_type_shape(particle_FireYellow, pt_shape_flare);
part_type_life(particle_FireYellow, 6, 12);
part_type_direction(particle_FireYellow, 0, 360, 0, 0);
part_type_speed(particle_FireYellow, 4, 6, 0, 0);
part_type_size(particle_FireYellow, 0.4, 0.6, 0.01, 0);
part_type_color1(particle_FireYellow, yellow);
part_type_death(particle_FireYellow, 1, particle_FireOrange);
  1. 我们只需要为最后一个粒子创建这种效果,这是最热和最明亮的白色粒子。它的构造与黄色粒子相同,只是它更小更快。
globalvar particle_FireWhite;
particle_FireWhite = part_type_create();
part_type_shape(particle_FireWhite, pt_shape_flare);
part_type_life(particle_FireWhite, 2, 10);
part_type_direction(particle_FireWhite, 0, 360, 0, 0);
part_type_speed(particle_FireWhite, 6, 8, 0, 0);
part_type_size(particle_FireWhite, 0.3, 0.5, 0.01, 0);
part_type_color1(particle_FireWhite, fireWhite);
part_type_death(particle_FireWhite, 1, particle_FireYellow);
  1. 我们现在已经拥有了这种粒子效果所需的所有粒子,我们只需要添加一个发射器来产生它们。这次我们将使用一个流发射器,这样火焰就会不断地从每个碎片中流出。由于碎片在移动,我们需要为每个创建的碎片都有一个独特的发射器。这意味着它不能是全局发射器,而是一个局部发射器。打开scr_TNT_Fragment_Create并在脚本的末尾添加以下代码:
myEmitter = part_emitter_create(system);
part_emitter_region(system, myEmitter, x-5, x+5, y-5, y+5, ps_shape_ellipse, ps_distr_linear);
part_emitter_stream(system, myEmitter, particle_FireWhite, 5);

我们创建了一个具有相对较小区域的发射器,用于生成平衡分布的火焰粒子。在每一步中,只要发射器存在,它就会创建五个新的火焰粒子。

  1. 发射器现在与碎片同时创建,但我们需要发射器随之移动。打开scr_TNT_Fragment_Step,并添加以下代码:
part_emitter_region(system, myEmitter, x-5, x+5, y-5, y+5, ps_shape_ellipse, ps_distr_linear);
  1. 如前所述,我们需要摧毁发射器,否则它将永远不会停止流出粒子。为此,我们需要打开obj_TNT_Fragment,并添加一个destroy事件,附加一个新的脚本scr_TNT_Fragment_Destroy,用于移除附加的发射器。
part_emitter_destroy(system, myEmitter);

这个函数将从系统中移除发射器,而不会移除已生成的任何粒子。

  1. 我们需要做的最后一件事是取消可见复选框的选中,因为我们不想看到碎片精灵,只想看到粒子。

  2. 保存游戏并引爆 TNT。现在不仅可以看到一些碎片,还有火焰喷射出爆炸,变成漂浮上升的黑色烟雾。它应该看起来像以下截图:制作 TNT 爆炸

清理粒子

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

清理粒子

  1. 我们需要做的第一件事是在离开房间时摧毁发射器。幸运的是,我们已经编写了一个可以做到这一点的脚本。打开obj_TNT_Fragment,添加一个房间结束事件,并将scr_TNT_Fragment_Destroy附加到其中。

  2. 即使我们在改变房间之前摧毁了发射器,游戏中任何剩余的粒子仍然会在下一个房间中出现,即使只是短暂地。我们需要做的是清除系统中的所有粒子。虽然这听起来可能是很多工作,但实际上非常简单。由于 Overlord 存在于每个级别中,但不在任何其他房间中,我们可以使用它来清理场景。打开obj_Overlord,添加一个房间结束事件,并附加一个新的脚本scr_Overlord_RoomEnd,其中包含以下代码:

part_particles_clear(system);

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

  1. 保存游戏,引爆一些 TNT,并立即重新开始房间。您现在不应该在场景中看到任何粒子。

总结

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

在下一章中,我们将考虑将这个游戏发布到互联网上。我们将介绍将其上传到您自己的网站,将其托管在 Facebook 上,并将其提交到游戏门户。我们还将研究使用各种内置的开发者服务,如分析和广告。让我们把游戏发布出去!

第九章:让你的游戏走出去

经过所有的努力,我们的游戏已经准备好发布了。在本章中,我们将把游戏上传到 Web 服务器,以便任何人都可以在互联网上玩。我们将看看如何允许用户登录他们的 Facebook 账户,并将某个级别的得分发布到他们的 Facebook 动态。我们还将集成 Flurry 分析,以跟踪有用的数据,这将使我们能够了解人们是如何以及在哪里玩游戏的。最后,我们将简要讨论从游戏中赚钱的问题。

在自己的网站上发布游戏

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

创建应用程序

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

  1. 如果尚未打开,打开我们一直在开发的 Tower Toppling 游戏。

  2. 在创建最终版本之前,我们应该查看一些可用的选项。转到资源 | 更改全局游戏设置,然后转到HTML5选项卡。

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

创建应用程序

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

当我们编译游戏时,GameMaker: Studio 会在根目录创建一个名为favicon.ico的文件,并在全局游戏设置中设置的图标。用户将在浏览器标签中看到这个图标,以及标签显示的页面名称;在保存页面为书签时也可见。图标是大多数网站的常见特征,是 ICO 格式的小图像,用于显示网站的代表性符号。大多数图像编辑器不能原生保存为 ICO 格式,通常需要插件来完成。幸运的是,有很多免费的网站可以将任何图像转换为图标。我个人更喜欢使用iconverticons.com/online/,因为它们接受大多数常见的图像格式,并转换为包括 Windows、Mac 和 Web 图标在内的所有主要图标格式。一旦我们有了合适的 ICO 文件,就可以在我们的游戏中使用它。

  1. 让我们通过点击“更新”来更新图标,并加载第九章/资源/额外/GameIcon.ico。点击“确定”。

  2. 游戏现在已经准备好导出了。转到文件 | 创建应用程序,并将游戏保存到游戏项目目录中一个名为Destruct的新文件夹中。

  3. 点击保存,您将看到游戏编译并创建运行游戏所需的所有文件。在Destruct文件夹中,您应该看到两个文件和一个文件夹。有一个favicon.ico文件,和一个index.html文件,这是将显示游戏的网页。文件夹html5game包含所有资产,比如所有声音的 OGG 和 MP3 格式,一些以index_texture开头的 PNG 文件,其中包含编译成单独精灵表的所有图形,以及一个包含所有游戏功能的index.js文件。还有一个particles文件夹,其中包含用于粒子形状的所有图像。

托管游戏

游戏已经构建好了;我们只需要一个放置它的地方。在互联网上有许多选项可供托管游戏的网站。这些可以从免费的网站托管到拥有个人服务器以及其中的一切。由于所有不同的套餐、不同的价格点以及我们的整体意图,选择一个托管商可能会非常耗时。每个开发者都需要考虑一些事情,比如预计有多少人会玩游戏,将来是否会添加更多游戏,网站上是否会有广告等等。如果游戏只会展示给家人和朋友,免费的网站托管服务可能就够了,但如果目标是从游戏中赚钱,最好使用某种付费服务。在选择提供商时,我们想要寻找的主要功能是:有多少服务器空间、带宽量、FTP 访问和最大文件大小。

另外,您应该确保网站允许上传 MP3 文件,因为许多免费和一些付费网站不允许这样做。一些知名的网站,如www.godaddy.comwww.globat.com提供了大量的服务器空间和带宽,价格非常实惠,适合大多数开发者,至少在他们开始发布游戏时。

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

使用 FTP 上传游戏

为了将我们的游戏放到服务器上,我们需要使用 FTP 客户端来传输文件。有许多免费可下载的 FTP 客户端可用,如 WinSCP,CuteFTP 和 FileZilla。一些浏览器可以用于 FTP,如果安装了适当的插件,比如 Firefox 的 FireFTP。一些网络托管服务甚至提供拖放式 FTP 功能。对于这个项目,我们将使用可以从winscp.net下载的 WinSCP。

  1. 下载 WinSCP 客户端,并根据说明进行安装。当初始用户设置页面出现时,选择指挥官界面,如下截图所示:使用 FTP 上传游戏

  2. 运行 WinSCP。

  3. 由于这是我们第一次访问网站的 FTP,我们需要点击新建来创建一个新的 FTP 会话。

  4. 我们需要通过导航到会话 | 文件协议来选择文件传输的协议方法。默认是SFTP安全 FTP),但许多托管站点只允许标准 FTP 访问,所以我们将选择它。导航到文件协议 | FTP

注意

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

  1. 接下来,我们需要输入服务器 FTP 地址,通常是您的网站名称,加上您的用户名和密码。它应该看起来像下面的截图:使用 FTP 上传游戏

  2. 为了将来更容易访问网站,我们可以保存这些设置,包括密码。点击保存

  3. 这将带我们回到登录界面,现在我们可以在存储的会话列表中看到 FTP 连接,如下一个截图所示。要打开连接,我们可以双击站点名称,或者选择站点名称,然后单击登录使用 FTP 上传游戏

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

使用 FTP 上传游戏

  1. 在左侧面板中,找到我们的游戏已经导出的Destruct文件夹。将整个文件夹拖到右侧面板上,以将所有文件传输到服务器上。

  2. 弹出对话框会询问我们是否要复制所有文件。点击复制。可能需要一些时间来转移所有内容。

  3. 游戏现在已经上传并可以在互联网上访问。要访问它,只需打开浏览器,转到网站和Destruct子目录,例如http://www.yoursitename.com/Destruct/

与 Facebook 集成

现在游戏已经上传到服务器,任何人都可以在世界上玩它。他们可以玩这个游戏,只要他们知道它。任何开发者面临的最困难的挑战之一是让人们了解他们的产品。通过社交媒体网站,比如 Facebook,传播消息是最简单的方法之一。GameMaker: Studio 已经集成了与 Facebook 连接的功能,这使得这一点变得容易。我们将在游戏的前端添加一个 Facebook 登录按钮,并允许玩家将他们的分数发布到他们的 Facebook 动态中。

  1. 为了使用 Facebook 功能,我们需要拥有 Facebook 账户和 Facebook 开发者账户。前往developers.facebook.com/并登录。如果你没有 Facebook 账户,它会提示你创建一个。

  2. 一旦我们登录到开发者页面,我们需要点击顶部菜单栏上的应用。这将带我们到应用页面。

  3. 接下来,我们需要点击注册为开发者按钮。这将打开一个注册对话框,我们需要通过它。首先,我们需要接受条款和条件,然后我们需要提供一个电话号码来验证账户。这必须是一个有效的号码,因为它将发送一条需要验证的短信。按照指示完成流程。

注意

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

  1. 完成注册后,我们应该会发现自己回到了应用仪表板。在注册按钮附近有一个创建新应用的按钮。点击它。

  2. 创建新应用对话框中,如下一张截图所示,我们需要输入一个应用名称。只要我们没有另一个同名应用,这个名称就不需要是唯一的。关于命名约定有一些规则,你可以点击Facebook 平台政策链接阅读。可选的应用命名空间是为了更深入地与 Facebook 集成,使用应用页面和使用 Open Graph,一个通知工具。我们不需要应用命名空间,所以可以留空。我们也不需要Web Hosting,可以点击继续与 Facebook 集成

注意

要了解更多关于 Facebook Open Graph、应用命名空间等内容,请查看 Facebook 开发者 API 文档developers.facebook.com/docs/reference/apis/

  1. 下一步是 CAPTCHA 安全检查。按照指示操作,然后点击继续

  2. 应用程序现在已经创建,我们在基本信息页面上。在这里,我们可以完成设置游戏如何集成到 Facebook。在基本信息 | 应用域中输入游戏网站的基本域名。这将允许应用在该域和所有子域上运行。它不应包括http://或根站点名称之外的任何其他元素。

  3. 选择应用程序如何与 Facebook 集成下,我们需要选择带 Facebook 登录的网站,然后输入游戏所在的确切 URL。

  4. 点击保存更改,因为我们已经完成了基本信息。设置应该如下截图所示,输入适用于您网站的适当域信息:与 Facebook 集成

  5. 在返回 GameMaker: Studio 之前,我们需要从基本页面顶部复制应用 ID:

  6. 重新打开游戏项目,导航到资源 | 更改全局游戏设置

  7. 转到Facebook选项卡,如下截图所示,勾选使用 Facebook的框,然后粘贴我们复制的 ID 到Facebook 应用 ID中。点击确定与 Facebook 集成

  8. 我们现在可以访问 Facebook 应用程序;现在我们只需要初始化它。创建一个新的脚本,scr_Global_Facebook,其中包含以下代码:

facebook_init();
globalvar permissions;
permissions = ds_list_create();
ds_list_add(permissions, "publish_stream");

我们首先初始化 Facebook,然后创建一个全局变量,用于包含我们想要从 Facebook 请求的所有权限的ds_list。在我们的情况下,我们只是要求能够发布到已登录用户的 Facebook 墙上。所有可用的选项都可以在 Facebook 开发人员网站上找到。

  1. 打开scr_Global_GameStart,并在最后执行以下行:
scr_Global_Facebook();

添加 Facebook 登录按钮

现在我们已经激活了 Facebook,我们可以将其实现到游戏中。我们将首先添加一个登录按钮。

  1. 创建一个新的精灵,spr_Button_FacebookLogin,取消删除背景的选项,加载Chapter 9/Resources/Sprites/FacebookLogin.gif,并将原点居中。

  2. 创建一个新对象,obj_Button_FacebookLogin,附加我们刚刚创建的精灵,然后将父级设置为obj_Button_Parent

  3. 添加一个鼠标 | 左键按下事件,并附加一个新的脚本,scr_Button_FbLogin_MousePressed,让用户登录 Facebook。

facebook_login(permissions);
  1. 打开MainMenu,并在开始按钮下方添加一个按钮的单个实例。

  2. 接下来,我们需要让玩家发布到他们的墙上。为此,我们将在得分屏幕上添加另一个按钮。创建一个新的精灵,spr_Button_FacebookPost,取消删除背景的选项,加载Chapter 9/Resources/Sprites/FacebookPost.gif,并将原点居中。

  3. 得分屏幕都是代码,所以我们不需要一个新对象,但我们需要向现有脚本添加代码。打开scr_ScoreScreen_Create,并添加一个用于按钮的 Y 位置、宽度偏移和高度偏移的变量。

postfb_Y = 340;
postfb_OffsetW = 64;
postfb_OffsetH = 16;
  1. 接下来,我们将创建一个新的脚本,scr_Menu_Button_FbPost,用于控制功能。
if (isVictory)
{
    status = facebook_status()
    if (status == "AUTHORISED")
    {
        draw_sprite(spr_Button_FacebookPost, 0, screenX, postfb_Y);
        if ((win_Y > postfb_Y - postfb_OffsetH && win_Y < postfb_Y + postfb_OffsetH))
        {
            if ((win_X > screenX - postfb_OffsetW && win_X < screenX + postfb_OffsetW)) 
            {            {
                draw_sprite(spr_Button_FacebookPost, 1, screenX, postfb_Y);
                if (mouse_check_button_pressed(mb_left)) 
                {
                    myTitle = "Destruct";
                    myCaption = "Play this game at yournamesite.com"
                    myText = "I just destroyed the " + room_get_name(room) + " Towers playing Destruct!";    
                    myImage = "http://yoursitename.com/Destruct/Thumbnail.gif"; 
                    mySite = "http://yoursitename.com/Destruct/"      
                    facebook_post_message(myTitle, myCaption, myText, myImage , mySite, "", "");                    
                }
            }
        }
    }
}
}

我们只想在玩家完成一个级别时发布到 Facebook,所以我们首先检查胜利条件。我们检查 Facebook 连接的状态,因为我们只想在玩家登录时显示按钮。如果玩家已登录,我们在屏幕上绘制按钮,并检查鼠标是否悬停在按钮上,就像我们对所有其他按钮做的那样。如果点击按钮,我们创建一些变量用于消息标题、说明和文本、图像以及返回网站的链接。然后我们在 Facebook 上发布一条消息。该函数还有两个额外的参数,用于使用更高级的 Facebook 操作,但我们将这些参数留空。

注意

要查看可用的高级选项,请参阅 Facebook 开发人员 API 帖子页面developers.facebook.com/docs/reference/api/post/

  1. 为了在屏幕上绘制这个,我们需要重新打开scr_ScoreScreen_DrawGUI并执行我们刚刚创建的脚本:
scr_Menu_Button_FbPost();
  1. 保存游戏并点击创建应用程序。可以覆盖现有的项目文件。

  2. 打开 WinSCP 并连接到 FTP 服务器。

  3. 将所有文件传输到服务器。在提示确认覆盖文件时,点击全部是

  4. 我们还需要传输我们想要包含在帖子中的图像。打开Chapter_09/Resources/Extras/,将Thumbnail.gif传输到服务器的Destruct文件夹中。

  5. 打开浏览器并转到游戏网站。当游戏加载完成时,我们应该会看到新按钮,就在开始按钮下方,如下面的屏幕截图所示:添加 Facebook 登录按钮

  6. 点击登录 Facebook按钮。应该会出现一个弹出窗口,就像下一个屏幕截图一样。如果没有发生任何事情,请检查浏览器是否已阻止弹出窗口并解除阻止。当弹出窗口出现时,我们只需要登录我们的 Facebook 账户。添加 Facebook 登录按钮

  7. 成功玩一个级别。当得分屏幕出现时,我们应该会看到发布到 Facebook按钮,如下面的屏幕截图所示:添加 Facebook 登录按钮

  8. 点击按钮,然后转到你的 Facebook 页面。我们将看到一个新的帖子已经与世界分享,看起来像下面的屏幕截图:添加 Facebook 登录按钮

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

使用 Flurry Analytics 跟踪游戏

分析是收集和发现一组数据中的模式的过程。这些数据可以是任何可量化的行为,比如鼠标点击,以及相关的元素,比如点击了什么。这些信息使开发人员能够看到用户如何使用他们的产品。在创建游戏时,这是非常有用的,因为有很多东西可以被跟踪。

我们将实施 Flurry Analytics,这是 GameMaker: Studio 内置的两个系统中最强大的一个。虽然可以跟踪任何事物,但通常最好专注于对用户体验最相关的事物。对于我们的游戏,我们将跟踪每个级别的得分、使用的装备和游玩次数。我们只会在玩家成功完成一个级别时发送这些数据。这将使我们能够看到每个级别被玩的频率,最常使用的装备,得分的变化,每个级别的难度,以及人们平均在哪里退出游戏。

设置 Flurry Analytics

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

  1. 首先,让我们注册 Flurry 分析。转到www.flurry.com/并按照网站的说明注册一个免费账户。

  2. 设置好账户并登录后,我们应该会进入开发者主页。在菜单栏上点击应用程序选项卡,进入应用程序页面,如下面的屏幕截图所示:设置 Flurry Analytics

  3. 此时我们还没有任何应用程序,所以需要添加一个。点击添加新应用程序

  4. 接下来的页面要求选择一个平台。点击 Java 图标。

  5. 接下来,我们需要为应用程序添加一些基本信息。如下一张截图所示,输入游戏的名称Destruct,并选择一个合适的类别,在我们的情况下,游戏-模拟似乎最合适。然后点击创建应用设置 Flurry 分析

  6. 在下一页中,它询问我们如何将 SDK 与几个选项集成。GameMaker: Studio 已经将其集成到软件中,这意味着我们可以跳过这一步。点击取消完成此步骤,并返回到主页

  7. 现在我们应该在应用程序摘要中看到我们的应用程序,如下一张截图所示。我们需要获取我们的应用程序 ID,所以点击Destruct设置 Flurry 分析

  8. 接下来,我们需要导航到左侧菜单中的管理 | 应用信息,以访问包含我们应用程序信息的页面。在列表底部,如下一张截图所示,是API 密钥。这个密钥是连接 GameMaker: Studio 中的游戏到这个分析应用程序所需的。复制这个密钥。我们现在暂时完成了这个网站。设置 Flurry 分析

  9. 重新打开项目文件并打开全局游戏设置

  10. 点击全局游戏设置顶部的右箭头,直到看到Analytics选项卡,如下一张截图所示。点击Analytics选项卡。设置 Flurry 分析

  11. HTML5子选项卡中,将分析提供程序设置为Flurry,选中启用 Flurry复选框,并将 API 密钥粘贴到Flurry Id中。现在我们已经设置好并准备输出一些数据。

跟踪游戏中的事件

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

  1. 我们已经有了用于跟踪装备(TNT: 0, WRECKINGBALL: 1MAGNET: 2)的常量,可以用于跟踪目的。这使我们需要一些额外的常量来跟踪级别,尝试次数和得分。导航到资源 | 定义常量,并添加LEVEL: 3, ATTEMPTS: 4, LVLSCORE: 5

  2. 我们需要将这些数据保存在全局可访问的网格中。创建一个新的脚本scr_Global_Analytics,并初始化整个游戏的值。

globalvar levelData;
levelData = ds_grid_create(totalLevels, 6);
for (i = 0; i < totalLevels; i++)
{
    ds_grid_set(levelData, i, TNT, 0);
    ds_grid_set(levelData, i, WRECKINGBALL, 0);
    ds_grid_set(levelData, i, MAGNET, 0);
    ds_grid_set(levelData, i, LEVEL, level[i, 0]);
    ds_grid_set(levelData, i, ATTEMPTS, 0);
    ds_grid_set(levelData, i, LVLSCORE, 0);
}

我们首先创建一个全局数据结构,为游戏中的每个级别设置六个值。我们运行一个循环,为每个装备的初始值设置,通过从先前创建的级别数组中获取级别名称,尝试次数和级别得分,全部设置为零。

  1. 重新打开scr_Global_GameStart并执行此脚本。

  2. 接下来,我们需要插入一些代码来改变每个级别的这些值。我们将从跟踪每个级别的尝试开始。这将需要更改几个脚本。我们将更改的第一个是scr_Button_LevelSelect_MousePressed,在玩家选择级别时,我们需要添加一次尝试。在else语句中,在更改房间之前,添加以下代码:

currentLevel = ds_grid_value_x(levelData, 0, LEVEL, totalLevels-1, LEVEL, myLevel);
ds_grid_add(levelData, currentLevel, ATTEMPTS, 1);

我们搜索 levelData 网格,以找出已选择的房间,以便找出我们需要更改的行。一旦我们有了行,我们就为该级别的数据添加一次尝试。

  1. 由于我们正在跟踪尝试,我们需要在重新启动房间之前将相同的代码插入scr_Menu_Button_Restart中。

  2. 最后,我们还需要在scr_Menu_Button_NextLevel中添加类似的代码,除了我们不能使用myLevel来找到房间。相反,我们需要向下查找到下一个房间。在更改房间之前,插入以下代码:

currentLevel = ds_grid_value_x(levelData, 0, LEVEL, totalLevels-1, LEVEL, level[i+1,0]);
ds_grid_add(levelData, currentLevel, ATTEMPTS, 1); 
  1. 现在尝试已经被跟踪,我们可以继续跟踪其他所需的数据。创建一个新的脚本scr_Level_Stats,并更新所有相关的统计数据。
levelCompleted = ds_grid_value_x(levelData, 0, LEVEL, totalLevels-1, LEVEL, room)
for (i = 0; i < ds_grid_width(equip); i += 1)
{
    equipUsed = abs(ds_grid_get(obj_Menu.startEquip, i, AMOUNT) - ds_grid_get(equip, i, AMOUNT));
    ds_grid_set(levelData, levelCompleted, i, equipUsed);   
}
levelScore = obj_Menu.tempScore - obj_Menu.tempCost
ds_grid_set(levelData, levelCompleted, LVLSCORE, levelScore);

我们首先找到刚刚完成的级别的行。然后,我们通过循环运行设备,看看在级别中使用了多少设备,通过从玩家开始使用的设备数量中减去剩余设备数量来实现。为了确保我们得到一个正数,我们使用绝对值函数,它返回一个绝对值。我们还获取级别的最终得分并更新网格。

  1. 我们希望仅在成功完成级别时运行此脚本,而将其放置的最简单的地方是在scr_WinCondition中,在我们返回一个真值的最后一行代码之前。
scr_Level_Stats();

将数据发送到 Flurry

现在数据在每次玩级别并成功完成后都会得到正确更新。现在我们需要做的就是将数据发送到 Flurry。Flurry 不是实时更新的,而是每天编译数据几次。如果我们在整个游戏会话期间逐个发送数据的各个部分,那么当编译时,这些数据可能会被分开,导致异常。为了帮助防止这种情况发生,我们将在每次更新时发送每个级别的所有相关数据。Flurry 将识别更改并保持数据在一起。

  1. 创建一个新的脚本,scr_Analytics_Send,并通过所有级别数据运行一个循环并发送出去。
for (a = 0; a < totalLevels; a++)
{
    levelName = room_get_name(ds_grid_get(levelData, a, LEVEL));
    levelScore = ds_grid_get(levelData, a, LVLSCORE);
    levelAttempt = ds_grid_get(levelData, a, ATTEMPTS);
    usedTNT = ds_grid_get(levelData, a, TNT);
    usedWB = ds_grid_get(levelData, a, WRECKINGBALL);
    usedMagnet = ds_grid_get(levelData, a, MAGNET);
    analytics_event_ext(levelName, "Level Score", levelScore, "Attempts", levelAttempt, "TNT Used", usedTNT, "WBalls Used", usedWB, "Magnets Used", usedMagnet);
}

在这个循环中,我们首先获取网格中存储的房间名称以及每个数据的所有值。使用函数analytics_event_ext,我们可以向 Flurry 发送多达 10 个不同的数据。第一个参数是数据的类别,以字符串形式发送,这里我们使用级别的名称作为类别。所有接下来的参数都是键/值对,其中包括我们正在跟踪的数据的名称及其关联值。

  1. 我们需要在游戏开始时发送一组初始分析数据,这样我们就可以从干净的状态开始。重新打开scr_Global_Analytics并在脚本的最后发送数据。
scr_Analytics_Send();
  1. 我们还需要在完成级别时发送数据。重新打开scr_Level_Stats并在脚本的最后发送数据。

  2. 我们现在已经完成了分析的实施。现在剩下的就是将其放到网络上。保存游戏,单击创建应用程序,并将游戏的新版本上传到服务器。

  3. 多次玩游戏,确保使用不同数量的设备并在每次重试级别。我们希望跟踪一些基本数据,以便我们可以看到这一切意味着什么。

理解分析

我们正在跟踪几个数据,并且 Flurry 将把这些信息编译成事件日志。我们可以看到会话何时发生以及在播放会话期间发生了什么。虽然这有些有用,但 Flurry 甚至在全球范围内进一步细分,以显示每个级别的平均游玩情况。让我们花点时间看看 Flurry 为我们提供了什么。在开始之前,重要的是要知道 Flurry Analytics 不是实时更新的,可能需要几个小时才能看到任何数据出现。

  1. 登录到您的 Flurry 帐户并转到您的Destruct应用程序页面。

  2. 仪表板上,您将看到的第一个统计数据是会话图表,如下截图所示。在这里,我们可以看到游戏每天被玩的次数。还有一些信息,比如每个游戏会话的平均持续时间,世界各地的人们从哪里玩游戏等。理解分析

  3. 在左侧菜单上单击事件。第一个选项是事件摘要,如下截图所示,显示了级别被玩的频率以及每个会话中完成该级别的用户百分比。理解分析

  4. 如果我们点击一个级别的小饼图标,我们将得到单个事件参数的详细信息。每个参数将显示所有会话的总使用量。如下面的截图所示,一个玩家使用了三个 TNT,另一个只需要两个,还有六个玩家根本没有使用任何 TNT。理解分析

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

用你的游戏赚钱

发布游戏是一个了不起的成就,但是每个开发者都会在某个时候希望从自己的努力中赚点钱。赚钱的最常见方式是在网站上放置广告,但是当涉及到 HTML5 游戏时,这种方式有一些缺点。第一个问题是网站需要非常高的流量才能够积累足够的点击量来赚钱。这影响了第二个问题,即广告只有在人们在特定网站的网页上玩游戏时才会起作用。不幸的是,其他网站可以通过 iframe 嵌入 HTML5 游戏,并在周围放置自己的广告。这可能会令人沮丧,因为这意味着尽管游戏在我们的网站上运行,但我们却没有赚到钱。幸运的是,还有其他赚钱的方式,比如赞助。

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

寻找赞助的最佳地方并不是游戏门户,而是各种基于浏览器的游戏的市场。FGL,www.fgl.com,最初是作为一个连接 Flash 游戏开发者和赞助商的地方创建的,但最近它已经扩展到 HTML5 和 Unity 游戏,并接受 iOS 和 Android 设备的游戏。这个市场允许开发者私下向赞助商和其他游戏开发者展示他们的游戏,获得反馈,并在准备好时进行竞标。与传统的拍卖行不同,最高出价者获胜,开发者可以选择他们更喜欢的报价,并可以与出价者就交易的具体条款进行协商。不能保证游戏会被提供任何资金,但如果有机会提前获得资金,那么很可能会在这里发生。

总结

这就是我们的全部内容!在本章中,我们涵盖了各种事情。我们首先使用 FTP 客户端将游戏上传到 Web 服务器。然后我们将 Facebook 集成到游戏中,允许玩家登录他们的账户并将级别分数发布到他们的动态。然后我们使用 Flurry 实施了分析,跟踪玩家如何玩游戏。最后,我们简要谈到了通过赞助赚钱的问题。

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

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

posted @ 2025-09-26 22:10  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报