原文:
zh.annas-archive.org/md5/441DA316F62E4350E9115A286AB618B0
译者:飞龙
协议:CC BY-NC-SA 4.0
前言
今天你在阅读本书,是因为你想制作视频游戏。你希望构建自己的视频游戏,可以在人们的浏览器以及他们的智能手机和平板电脑上运行。所有这些今天都是可能的,尽管这并非一直如此。你现在能够相对轻松地构建自己的游戏,原因有两个:HTML5 和 ImpactJS。
HTML5 是我们的全球网络超文本标记语言的最新版本,也是网页的通用语言。HTML 自上世纪 90 年代初就存在了,当时欧洲核子研究组织(CERN)的一名名叫 Tim Berners-Lee 的员工首次引入了它。新版本相继发布:1995 年版本 2,1997 年版本 3,同年稍后版本 4。我们使用的 HTML 版本大致相同,直到 2008 年 HTML5 问世。随着对多媒体实施的需求不断增长,公司一直在构建浏览器插件来播放音乐、显示电影等。Flash 播放器可能是这方面最知名的插件之一。作为游戏开发者,你仍然可以选择使用 Flash 和 ActionScript,但我们不知道 Flash 还能坚持多久(如果有的话),直到 HTML5 完全取代它。Flash 游戏的未来难以预测,但有一件事是相当确定的:基于 HTML5 的游戏的未来看起来很光明。自 HTML5 出现以来,浏览器对其兼容性逐渐增加。HTML5 是一个巨大的进步,因为它引入了新的元素,允许在网页上播放音乐和视频。
然而,对我们来说最重要的新功能是引入了<canvas>
元素。<canvas>
元素基本上是你的图形元素出现的占位符。结合 JavaScript 的使用,可以在 Flash 播放器之外构建浏览器游戏。然而,JavaScript 本身并不适用于构建游戏。以其原始形式,你可以使用它来构建游戏,但这将证明是非常困难的。因此,最后一个必要的成分是一个专门用于游戏开发的 JavaScript 库。这就是 ImpactJS 发挥作用的地方。
ImpactJS 本质上是一种 JavaScript 代码库,能够让游戏开发者的生活变得更加轻松。ImpactJS 是由德国天才 Dominic Szablewski 开发的。ImpactJS 游戏引擎的优势在于,只需基本的 JavaScript 和 HTML 知识,就能快速构建游戏。这使得即使是新手程序员也能专注于他们所热爱的事情:构建实际的游戏。ImpactJS 还配备了非常直观的关卡编辑器和调试系统,我们在本书中也会介绍。ImpactJS 旨在构建基于瓦片的二维游戏。例如,如果你想构建像马里奥或塞尔达传说这样的横向或俯视游戏,你会选择 ImpactJS。现在,让我们毫不拖延地进入行动,继续阅读第一章,“启动你的第一个 Impact 游戏”,在这里我们将通过收集必要的资源为游戏开发做准备。
本书内容
第一章,“启动你的第一个 Impact 游戏”帮助我们设置开发环境,让我们的第一个游戏运行起来,并查看一些对 HTML5 游戏开发者有用的工具。
第二章,“介绍 ImpactJS”深入探讨了 ImpactJS 的基础知识,通过探索一款预制游戏来了解一些关键概念。
第三章,“让我们建立一个角色扮演游戏”是一个从零开始构建俯视游戏的指南。
第四章,让我们建立一个侧卷游戏帮助我们从头开始构建一个侧卷游戏,利用 Box2D 物理引擎。
第五章,为您的游戏添加一些高级功能教会我们为我们在第三章中构建的 RPG 游戏添加一些高级功能,如高级人工智能和数据存储。
第六章,音乐和音效带领我们深入了解如何在 ImpactJS 中使用音乐和音效,从哪里购买它们,以及如何使用 FL Studio 制作基本曲调。
第七章,图形教会我们创建矢量和 Photoshop 图形,并探索从艺术家和专业网站购买它们的选项。制作自己的图形或在其他地方购买它们是一个重要的权衡考虑。
第八章,将您的 HTML5 游戏适应分销渠道帮助我们了解将游戏部署到不同设备的几种选择以及技术上如何实现。这是游戏开发过程的最后一步。
第九章,用您的游戏赚钱介绍了作为游戏开发者赚钱的几种选择,从照顾自己的销售和营销到出售您的分销权。
您需要为本书准备什么
以下是执行书中给出的代码所需的软件要求:
-
服务器(示例:XAMPP)。免费下载。
-
JavaScript 代码编辑器(示例:Komodo edit)。免费下载。
-
ImpactJS 游戏引擎。在www.impactjs.com购买。
-
Google Chrome 浏览器。免费下载。
-
Firefox 浏览器和 Firebug 插件。免费下载。
-
FL Studio。不免费,但仅与第六章,音乐和音效相关。
-
Photoshop。不免费,但仅与第七章,图形相关。
-
Inkscape。免费下载。
本书适用对象
本书适用于至少具有基本 JavaScript、CSS 和 HTML 知识的任何人。如果您想要为您的网站或应用商店构建自己的游戏,但不知道从何开始,这本书适合您。
约定
在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些这些样式的示例,以及它们的含义解释。
文本中的代码词显示如下:“打开浏览器,在地址栏中键入localhost
”。
代码块设置如下:
EntityPlayer = ig.Entity.extend({
size: {x:20,y:40},
offset:{x:6,y:4},
vel: {x:0,y:0},
maxVel:{x:200,y:200},
health: 400,
当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:
.defines(function(){
GameInfo = new function(){
this.score = 0;
},
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“单击下一步按钮将您移动到下一个屏幕”。
注意
警告或重要说明会以这样的方式出现在一个框中。
提示
提示和技巧会以这样的方式出现。
第一章:启动您的第一个 Impact 游戏
现在我们已经在前言中看到为什么 Impact Engine 是游戏开发者的一个不错选择,是时候开始工作了。为了开发游戏,您首先需要设置您的工作环境。就像画家需要他的画笔、画布和颜料一样,您需要设置您的代码编辑器、服务器和浏览器。在本章结束时,您将装备好所有开始冒险甚至在计算机上运行游戏所需的工具。
在本章中,我们将涵盖以下主题:
-
使用 XAMPP 设置您自己的本地服务器
-
在此服务器上运行预制游戏
-
您可以选择以下脚本编辑器的简短列表
-
使用浏览器和 ImpactJS 调试器脚本调试您的游戏
-
一些有趣的工具,您应该考虑帮助您创建游戏
安装 XAMPP 服务器
无论开发任何东西,无论是应用程序、网站还是游戏,创作者总是有一个临时区域。临时区域就像一个实验室;它用于在向世界展示之前构建和测试所有内容。即使发布游戏后,您也会首先在实验室中进行更改,以查看事情是否会出现问题。在您自己的面前出现问题是可以接受的,但您不希望这种情况发生在您的玩家身上。我们的临时区域将是一个本地服务器,在本书的过程中我们将使用XAMPP。XAMPP 中的 X 表示该服务器适用于不同的操作系统(跨环境,因此 X)。
其他字符(AMPP)代表Apache,MySQL,PHP和Perl。您可以根据下载和安装的版本在 Windows、Linux 或 Mac 上安装 XAMPP。还有一些 XAMPP 的替代品,如WAMP(适用于 Windows)和LAMP(适用于 Linux)。当然,这些替代品也很好。
Apache 是开源的 Web 服务器软件,使您能够运行您的代码。MySQL 是一个开源的数据库系统,使您能够使用 SQL 语言存储和查询数据。PHP 是一种能够将 SQL 命令(可以操作数据库)连接到网站或游戏代码(JavaScript)的语言。Perl 通常被称为“编程语言的瑞士军刀”,因为它在用途上非常多样化。安装 XAMPP 服务器相当简单。
您可以转到以下网站,并为您的系统下载适当的安装程序:
www.apachefriends.org/en/xampp.html
安装 XAMPP 服务器后,基本上是通过标准安装向导进行操作,是时候查看XAMPP 控制面板页面了。
在此面板中,您可以看到服务器的不同组件,可以打开和关闭。您需要确保至少 Apache 组件正在运行。其他组件也可以打开,但 Apache 对于运行游戏是绝对必要的。
现在转到您的浏览器。在本书的过程中,我们将使用 Chrome 和 Firefox 浏览器。但是,建议还安装最新的 Internet Explorer 和 Safari 浏览器进行测试。在地址栏中简单地输入localhost
。Localhost 是本地安装服务器的默认位置。您是否看到以下XAMPP 启动屏幕?
恭喜,您已成功设置了自己的本地服务器!
已知问题是IIS(Internet Information Services)占用了您必需的端口。您可能需要禁用或甚至删除它们,以便为 XAMPP 释放端口。
对于MAMP(M代表Mac),可能需要指定端口 8888 才能正常工作。因此,输入localhost: 8888
而不是只输入localhost
。
总结前面的内容,步骤如下:
-
下载并安装 XAMMP。
-
打开控制面板并启动 Apache。
-
在地址栏中输入
localhost
,打开你的浏览器。
安装游戏引擎:ImpactJS
接下来你需要的是实际的 ImpactJS 游戏引擎,你可以从 ImpactJS 网站impactjs.com/
购买,或者在 AppMobi 网站www.appmobi.com
上购买AppMobi的套餐,其中包含 ImpactJS 游戏引擎。
无论你在哪里购买引擎,你都会寻找一个(压缩的)文件夹,里面装满了 JavaScript 文件。这本质上就是 ImpactJS,一个在 HTML 环境中更容易构建 2D 游戏的 JavaScript 库。
现在你已经让服务器运行起来了,并且已经获得了 ImpactJS 引擎,你所需要做的就是把它放在正确的位置并测试它是否起作用。
在 ImpactJS 版本(v1.21)中,在写这本书的时候,你会得到一个名为impact
和一个license.txt
文件的文件夹。
许可证文件会告诉你购买的 Impact 许可证可以做什么,不能做什么,所以建议你至少阅读一下。
impact
文件夹本身不仅包括 Impact 游戏引擎,还包括关卡编辑器。文件夹结构应该能够容纳所有未来的游戏文件。
目前,知道你可以将整个impact
文件夹复制到服务器的根目录就足够了。
对于 XAMPP 来说,应该是:"你的安装位置"\xampp\htdocs
。
对于 WAMP 来说,应该是:"你的安装位置"\wamp\www
。
让我们也复制这个文件夹并将其重命名为myfirstawesomegame
,让它更加个性化。现在你有了原始文件夹,我们将在第三章和第四章中使用,让我们建立一个角色扮演游戏和让我们建立一个横向卷轴游戏。
你还应该在 XAMPP 安装位置\xampp\htdocs\myfirstawesomegame
和 WAMP 安装位置\wamp\www\myfirstawesomegame
中都有以下文件夹结构。
在myfirstawesomegame
文件夹中应该有lib
、media
和tools
子文件夹,以及index.html
和Weltmeister.html
文件。
时间进行一次小测试!只需在浏览器中输入localhost/myfirstawesomegame
。
“它起作用了!”的消息现在应该让你心中充满了喜悦!如果它没有出现在屏幕上,那么肯定出了大问题。如果你没有收到这条消息,请确保所有文件都存在并且位于正确的位置。
ImpactJS 带有一个名为Box2D
的物理引擎。检查一下你的文件夹结构中是否有这个文件夹。如果没有,你可以通过你下载 Impact 引擎时得到的个人下载链接下载一个包含引擎的演示游戏。这个演示游戏叫做Biolab Disaster,你应该能在这里找到box2d
文件夹。如果没有,Dominic(ImpactJS 的创造者)还提供了一个名为physics
的单独文件夹。由于 Box2D 是标准引擎的一个插件,最好在你的lib
文件夹中搜索plugins
文件夹,并将box2d
文件夹放在这里。
总结前面的内容,步骤如下:
-
购买 ImpactJS 许可证并下载其核心脚本
-
将所有必要的文件放在服务器目录中新创建的名为
myfirstawesomegame
的文件夹中。 -
在地址栏中输入
localhost/myfirstawesomegame
,打开你的浏览器。 -
下载 Box2D 插件并将其添加到你自己服务器上的
plugins
文件夹中
选择一个脚本编辑器
我们现在已经让服务器运行起来,并安装了 ImpactJS 游戏引擎,但我们还没有工具来实际编写游戏代码。这就是脚本编辑器的用武之地。
为了选择适合你需求的正确代码编辑器,最好区分纯编辑器和 IDE。IDE或集成开发环境既是脚本编辑器又是编译器。这意味着在一个程序中你可以改变和运行你的游戏。另一方面,脚本编辑器只是用来改变脚本。它不会显示输出,但在大多数情况下,会在你即将发生语法错误时提醒你。虽然编辑器会显示你 JavaScript 代码中的语法错误,但实际执行代码会显示逻辑错误,并给你一些(漂亮的)东西看。
对于 ImpactJS,有一个名为 AppMobi 的 IDE,它是免费的,但收费额外服务。使用 AppMobi 的替代方案是你刚刚安装的 XAMPP 服务器。
脚本编辑器,即使是非常好的脚本编辑器,通常也是免费的。在选择你喜欢的之前,你应该检查一些好的脚本编辑器,比如Eclipse,notepad++,komodo edit和sublime edit 2。特别是对于 Mac,有一个名为Textmate的编辑器,它经常被使用,但不是免费的。当然还有Xcode,官方的苹果开发者编辑器。
所有这些脚本编辑器都会检查你在 JavaScript 代码中所犯的错误,但它们不会检查 ImpactJS 特定的代码。为此,你可以制作自己的脚本颜色编码包,或者从那些花时间构建的人那里下载一个。
下载并安装之前提到的一些脚本编辑器,并选择你最喜欢的。所有这些都可以很好地完成任务,只是个人偏好的问题。
运行预制游戏
是时候在你的电脑上开始运行游戏了。为了做到这一点,你需要书中附带的文件。这些文件可以从以下网站下载:
www.PacktPub.com/support
现在你应该已经准备好了。复制第一章的可下载材料,启动你的第一个 Impact 游戏。用 Packt Publishing 下载页面上的index.html
和main.js
脚本替换 ImpactJS 库附带的index.html
和main.js
脚本。还要用提供的media
、entities
和levels
文件夹覆盖你电脑上的文件夹。
返回浏览器,重新加载localhost/myfirstawesomegame
链接。瞧!一个完全功能的 ImpactJS 游戏!如果你仍然看到下面截图中显示的**it works!**消息,可能需要清除浏览器缓存或刷新页面几次才能显示游戏。如果还有其他问题,我们将在学习调试时找出。
总结前面的内容,步骤如下:
-
从 packtpub 下载服务器下载必要的文件,并将它们放在你自己服务器上的正确位置
-
打开浏览器,在地址栏中输入
localhost/myfirstawesomegame
使用浏览器和 ImpactJS 调试你的游戏
在你调试游戏之前,你至少应该了解ImpactJS 代码的一般结构。你可能已经注意到,ImpactJS 有一个主脚本,用于实际控制整个游戏。main.js
脚本包括所有其他必要的脚本和ImpactJS
库。它包含的每个脚本都代表一个模块。就像这样,你在游戏中为每个级别和实体都有一个模块。它们就像乐高积木,聚集在一个大的(main.js
)城堡中。事实上,主脚本如下面的代码片段所示,本身就是一个模块,需要所有其他模块:
ig.module(
'game.main'
)
.requires(
'impact.game',
'impact.font',
'game.entities.player',
'game.entities.enemy',
'game.levels.main',
...
提示
下载示例代码
您可以从www.packtpub.com
的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support
并注册,以便直接通过电子邮件接收文件。
如果您查看一下级别脚本,您会发现它是用JSON(JavaScript 对象表示法)编写的,这是对象文字表示法的一个子集。 JSON 和普通文字在以下方面有所不同:
-
JSON 键是用双引号括起来的字符串。
-
JSON 用于数据传输。
-
您不能将函数分配为 JSON 对象的值。
有关 JSON 的更深入信息可以在json.org/
上找到。文字在 ImpactJS 中使用,并且看起来像以下代码片段:
EntityPlayer = ig.Entity.extend({
size: {x:20,y:40},
offset:{x:6,y:4},
vel: {x:0,y:0},
maxVel:{x:200,y:200},
health: 400,
属性使用冒号(:
)定义,并用逗号(,
)分隔。在普通 JavaScript 中,这样做的方式不同,如下所示:
if(ig.input.state('up') || ig.input.pressed('tbUp')){
this.vel.y = -100;
this.currentAnim = this.anims.up;
}
等号(=
)用于为属性分配值,分号(;
)用于分隔不同的属性和其他变量。
总结前面的内容,得出以下结论:
-
ImpactJS 使用三种 JavaScript 表示法:JSON,文字和普通表示法
-
ImpactJS 级别脚本使用 JSON 代码
-
ImpactJS 同时使用文字和普通 JavaScript 表示法
使用浏览器调试
在使用新安装的脚本编辑器编写代码时,您会注意到可以立即避免 JavaScript 语法错误,因为编辑器会告诉您出了什么问题。但是,有些错误只有在实际在浏览器中运行代码时才能找到。也许您并不希望公主在英雄救了她后起火,但意外的结果确实会发生。这意味着您应该随时保持浏览器打开,以便可以一遍又一遍地重新加载游戏,直到一切都符合您的喜好。
然而,当您的游戏崩溃或甚至无法完全加载时,找到原因可能会非常痛苦。即使一次更改一小部分代码,错误也可能在意想不到的地方出现。
为此,Firefox 和 Chrome 都配备了很好的工具。
Firebug - Firefox 调试器
对于 Firefox,您可以安装Firebug插件,这是一个不错的 JavaScript 调试器,可以告诉您代码中的错误在哪一行,并且具有一个易于使用的DOM(文档对象模型)资源管理器。DOM 基本上是包含所有实体和函数的 HTML 文档的结构;深入了解 DOM 是必不可少的。
这个 DOM 资源管理器非常有用,可以用来检查某些变量的值,比如您的英雄的生命值或屏幕上敌人的数量。导致游戏崩溃的错误将很容易通过调试器(Firefox 和 Chrome 都有)找到。但是,要发现您在敌人的生命值末尾添加了两个额外的零(所以这些生物就是不会死!),您需要探索 DOM。
Chrome 调试器
对于 Chrome,您甚至不需要下载插件,因为它已经捆绑了 JavaScript 控制台。此控制台可以在选项 | 额外 | JavaScript 控制台下找到,并且非常方便使用。您还可以通过右键单击网页并选择检查元素来打开控制台。
Chrome 调试器(也称为 Chrome 开发者工具)有八个选项卡,其中有四个对调试 Impact 游戏特别有用。
元素选项卡允许您检查页面的 HTML 代码,甚至可以立即对其进行编辑。这样,您可以更改游戏的画布大小。但是,请注意,更改仅适用于加载的网页;它们不会保存到您的 HTML 或 JavaScript 文件中。
在Resources标签中,您可以查找有关您的本地存储的信息。本地存储对于构建游戏并不是必需的,但它是一个用于保存高分和类似内容的很酷的功能。
Sources标签非常有用,因为它允许您检查和更改(再次是临时的)您的 JavaScript 代码。您可以在这个标签中找到您的 DOM,就像在 Firefox 中一样。代码可以手动暂停,也可以通过使用条件断点来暂停。例如,如果您的角色可以获得经验,您可以在升级时暂停游戏,看看所有变量是否都取得了您期望的值。
调试器最重要的部分是Console标签。控制台显示了您的错误所在,甚至指示了发生错误的 JavaScript 文件和行。控制台非常灵活,可以在任何其他标签打开时调用。这样,您可以在Sources标签中检查代码,如果有错误,可以通过单击右下角的X图标来调用控制台。打开Sources和Console标签后,调试变得轻而易举。
其他四个标签是Network,Timeline,Profiles和Audits标签。它们很有用,但您将花费大部分时间与Sources和Console组件一起打开。
在本书的过程中,Firebug 和 Chrome 调试器可以互换使用。
如果启用了缓存,更改游戏并重新加载您的 Web 浏览器通常是不够的。只要您的游戏被缓存,您就无法百分之百确定您是否在评估游戏的最新版本还是浏览器在内存中锁定的先前版本。在开发游戏时,关闭缓存是明智的选择。在 Firefox 中,可以通过下载和使用一个执行此操作的插件来完成。在 Chrome 中,这只是 Chrome 开发者工具本身的一个选项。当调试器打开时,单击右下角的齿轮符号以打开设置。在General标签下,您可以禁用缓存,如下面的屏幕截图所示:
调试可以在单个浏览器中完成,但明智的做法是至少在四个浏览器上测试您的游戏是否运行顺畅,例如 Chrome,Safari,Internet Explorer 和 Firefox。使用这四个浏览器,您至少可以覆盖 95%的浏览器使用率。
如果您想为某些设备进行测试,那么测试也是必要的。这可以通过拥有您希望您的游戏在其上运行的设备之一(例如 iPad,iPhone,HTC,Galaxy 等)并在one.com
等网络托管公司的帮助下将您的游戏在线上发布来完成。或者,您可以使用 AppMobi,该工具具有设备查看器,用于此目的。
测试游戏的另一个好方法是使用模拟器。模拟器是一种模拟实际智能手机的程序。这一切都很好,但让我们看一个实际的例子。
使用 Chrome 和 Firebug 进行调试的练习
在前面的章节中,我们已经让游戏开始运行了。现在让我们看看如果真的出了问题会发生什么(假设到目前为止一切都很顺利)。
首先,我们需要一些有缺陷的代码文件。因此,从debugging tutorial
文件夹中复制main.js
,player.js
,projectile.js
和enemy.js
脚本,并用这些脚本替换旧的脚本。main.js
应该位于您的game
文件夹中,而enemy.js
可以在entities
文件夹中找到。
现在,您的特殊(即有错误的)脚本已经就位,是时候重新启动游戏了。重新加载您的浏览器,并确保缓存是空的,否则不会显示错误。
游戏无法完全加载,如下面的加载栏所示:
在使用 Chrome 和 Firebug 进行调试的练习中
这可能经常发生在你开发新游戏时。例如,如果不同 JavaScript 文件的依赖关系错误,就会经常发生这种情况。要查看现在发生了什么,请打开 Chrome 调试器。
转到控制台选项卡,查看错误消息:i 未定义 main.js:51。在编辑器中打开main.js
脚本,并转到指定的行号。果然,如下代码所示,有一些问题:
i.input.bind(ig.KEY.UP_ARROW, 'up');
ig.input.bind(ig.KEY.DOWN_ARROW,'down');
没有叫做i
的对象,这应该像其他的一样是ig
。
现在我们解决了这个问题,再次加载游戏。加载成功!太棒了!但这并不意味着一切都没有错误。打开调试器,看看是否还有其他问题困扰着你的游戏。目前没有,所以让我们继续探索。
如果一切“顺利”,你的游戏应该在你想向左走的时候卡住。
你会收到消息,无法读取未定义的属性’pos’。问题是很难确定错误实际发生的位置,因为几乎每个脚本中都会出现错误。但是,我们知道pos
是实体的一个参数,并且在错误发生之前我们按下了左
按钮。我们至少应该检查所有定义或使用这个左
按钮的地方。
如果你打开player.js
脚本,你会发现左移的命令相当奇怪,如下代码所示:
else if(ig.input.state('left') || ig.input.pressed('tbLeft')){
this.vel.x = -100;
this.currentAnim = this.anims.left;
this.kill();
}
因此,实体向左移动,动画设置为左侧,然后自毁。随意使用kill()
函数是不好的。在这种情况下,kill()
函数的意外位置导致玩家消失,因此玩家没有了位置,这在游戏的update
循环中进一步产生了错误。移除这个kill()
函数,游戏就不会再崩溃了。
有时控制台会显示错误,但是你的智慧仍然会带领你找到根本原因。控制台只是一个工具,你才是真正的主宰。
我们已经移除了所有主要的错误,因为 Chrome 目前没有指示错误。确保检查所有关卡,因为不同的关卡通常会有其他可能出现错误的实体。但是,现在让我们开始杀死一些敌人!
你可能已经注意到,摧毁这些讨厌的生物相当困难。我们不再有真正的错误,但也许其他事情并没有按计划进行。我们似乎无法杀死它,所以要么我们造成的伤害不够,要么它的生命值非常高。让我们深入了解可能涉及的两个实体:projectile
和enemy
。我们应该检查projectile
实体而不是player
实体,因为尽管玩家射击了抛射物,但是造成伤害的是抛射物。枪不杀人,子弹杀人。打开projectile.js
和enemy.js
脚本,它们都在entities
文件夹中。或者,你可以打开 Chrome 调试器,在脚本选项卡下选择文件。
在projectile.js
脚本中,查找以下代码:
check: function(other){
if(other.name == 'enemy'){other.receiveDamage(100,this);}
this.kill();
this.parent();
我们很快就会深入了解这段代码的具体内容。现在知道子弹在撞击敌人时造成100
点伤害就足够了。
现在查看enemy.js
脚本中敌人的生命值。以下代码显示了生命值:
health:200000,
是的。这是一个问题。敌人比预期的强大一千倍。将生命值改为200
,你就可以用两发子弹杀死敌人。或者,你可以将projectile
实体的伤害设置为100,000
。将damage
属性改为一个大数值可能对喜欢看到大数值而不是适度数值的玩家有用(那些玩过魔兽世界的人知道我在说什么)。
如果你保存代码并重新加载关卡,你应该不会再遇到杀死敌人的问题了。
通过浏览 DOM 来找出可能出错的另一种方法是查看单个实体。让我们使用 Firebug 来做这个。如果您的 Firefox 上还没有安装它,可以搜索并安装它。
我们射击了敌人两次,发现他并不打算死。我们可以通过浏览 DOM 来查看生成的实体本身,而不是检查代码。要找到敌人的生命值,您必须通过按下浏览器中的 bug 符号来打开您的 Firebug,然后选择DOM选项卡。现在按顺序打开ig
,game
和entities
文件夹。您将看到一个编号列表,数字是entities
数组中特定实体的位置。现在您可以打开一些数字,直到找到生命值为19800的敌人,如下面的截图所示:
敌人被埋在其他实体的列表中,但通过他的属性我们可以看出这里发生了什么。我们射击了两次,现在它的health值为19800。这是有道理的,因为抛射物的伤害是100。
掌握 DOM 需要一些努力,但对于找出代码是否按预期工作非常有用。此外,您可以更好地了解 ImpactJS 的核心元素如何相互关联。建议在继续之前花一些时间在这里,以了解整体结构。
所以我们已经看到了三种不同类型的错误,从容易解决到相当难以找到和修复。在第一种情况下,控制台告诉你有一个地方出现了错误,你去设置它就对了。第二种情况显示游戏在多个地方同时产生错误,但只有一个根本原因。由你和你的逻辑大脑来推断是什么导致了游戏崩溃。最后,我们有意想不到的结果,这些并不是真正的错误。控制台不会显示这些错误,因为它无法读取你的思想(也许在下一个版本中,谁知道)。这些是最难找到的,需要你进行一些测试。
总结前面的内容,结论如下:
-
Firefox 和 Chrome 都具有非常强大的调试器功能。
-
Firebug 特别推荐用于探索游戏的 DOM。
-
Chrome 有八个有趣的组件,其中最有用的是控制台,可以检测错误。
-
错误可以有不同类型:语法错误,代码逻辑错误和游戏逻辑错误。
-
大多数语法错误可以通过一个好的脚本编辑器预先检测到。
-
一个简单的语法错误会在调试器控制台中显示为单行错误。这样很容易定位和修复。
-
代码逻辑错误很难检测,因为语法通常在根本上是正确的,但会在其他位置显示错误。
-
游戏逻辑错误是非常主观的错误,因为它们不会导致游戏崩溃,但会导致游戏玩法不佳。
使用 ImpactJS 进行调试
ImpactJS 本身带有一个内置的调试器。但是,默认情况下它是关闭的,可以通过对main.js
脚本进行小修改来打开。main.js
脚本(顾名思义)是您的游戏的主要脚本,并调用所有其他 JavaScript 文件。这个脚本加载到您的浏览器的 HTML 画布中,并一遍又一遍地循环,以使您的游戏运行。main.js
脚本可以在game
文件夹中找到,并且应该随 Impact 许可证一起提供,如下面的代码片段所示:
ig.module(
'game.main'
)
.requires(
'impact.game',
'impact.font',
'impact.debug.debug',
一切都始于ig
(Impact Game)对象。这个对象是您在调试游戏并检查变量和函数时要查找的东西。在main.js
脚本中,有一个对.module
函数的调用,它将game.main
定义为游戏的一个模块。模块名称需要与其位置和文件名相同!因此,JavaScript 文件lib/game/entities/bigmonster.js
最终将被定义为game.entities.bigmonster
。通过以下步骤可以向游戏添加debug
面板:
-
.requires()
函数调用所有需要执行代码的脚本。并非每个模块都需要这样做,但main.js
脚本将始终至少需要impact
库。 -
在这个函数调用中,您将希望添加
impact.debug.debug
脚本,它(正如您猜到的)调用lib/impact/debug
文件夹中的debug.js
脚本。 -
保存
main.js
脚本,并在 Chrome 中重新运行localhost/myfirstawesomegame
。 -
如果一切按计划进行,您现在应该在浏览器底部看到一个名为impact.debug的新工具栏。
-
调试器有三个选项卡:背景地图,实体和性能,以及右上角的四个关键指标。
-
从左到右,这些指标如下:
-
运行游戏一帧所需的毫秒数。
-
游戏的每秒帧数指示器。
-
已经发生的绘制次数。如果您正在进行对话,这将包括角色。
-
在右侧,您会找到当前游戏中的实体数量。
虽然这些指标快速向您展示了需要考虑的最重要的事情,但如下图所示的三个选项卡更深入地展示了:
如果选择背景地图,您将看到游戏拥有的所有图形图层。假设您想让您的史诗角色在树前奔跑;您会期望树的一部分消失在角色的后面,而不是相反。当角色移动到树后面时,您希望它被树隐藏。因此,您至少需要两个图层才能绘制出这样的树。一个图层在玩家前面(很可能是树梢),另一个在玩家后面(树干)。
在调试器的部分中,您可以打开和关闭图层。如果图层设置为预渲染,您将能够看到图层的块。在以下截图中,检查和碰撞被打开,而其他选项被关闭:
使用实体选项卡,您可以打开和关闭几个有趣的指标。如果您打开显示碰撞框,您将能够看到您的角色周围的红色框以及几个(不可见的)实体,它们不断检查碰撞。这些红色框指示触发点碰撞的边界。这很重要,因为如果围绕您的英雄角色的碰撞框比图像大得多,他可能无法再通过门,或者会神秘地被远处的敌人击中。在编写代码时,您可以自己设置这些碰撞框的大小,从而产生一些有趣的效果,比如只能通过射中眼球来杀死 boss。
当您打开显示速度时,您应该让角色四处走动。现在您将看到一条伸出在他前面的线,这是他当前移动速度的指示。
通过显示名称和目标,您可以看到所有命名实体及其目标。这是一个有趣的功能,但对于您的目的,最好使用 ImpactJS 级别编辑器(Weltmeister)。
最后,性能选项卡向您展示了浏览器为运行游戏需要执行的不同任务所付出的努力,如下图所示:
在上一个图表上可以看到两条水平线:33ms和16ms。这些线对应大约 60fps 和 30fps 的帧率。游戏运行在 30fps 以下是不可取的,因为看起来就像游戏在延迟,这样玩起来就没有乐趣。如果你发现游戏延迟,检查哪个部分占用了所有的资源,然后尝试修复它。
在大多数情况下,绘制游戏(图形)占用了大部分资源。这在性能选项卡中用Draw表示。如果发生这种情况,尝试减少图层或增大瓷砖的大小。此外,预渲染在这种情况下可以提高性能。
资源的另一部分由你的实体及其交互占用。如果你的屏幕上有成千上万个不同的实体,因为你决定一群海鸥应该由每只鸟的单独实体表示,你可能会很快遇到麻烦。
系统延迟有一个单独的指示器,这是一个你无法控制的参数,因为它显示了浏览器的性能。相当频繁地,系统延迟会导致帧率下降。然而,在大多数情况下,它实际上并不会被感觉到,因为真正巨大的波动来得快去得也快。
总结前面的内容,得出以下结论:
-
ImpactJS 自带调试器,默认情况下是关闭的
-
调试器有几个组件,可以洞察实体行为、碰撞和游戏性能
-
ImpactJS 调试器在跟踪性能问题方面非常有用,在开发过程中应始终保持开启状态
有哪些有用的工具
如果你有一个不错的脚本编辑器、ImpactJS 库、(本地)服务器和一个具有调试功能的浏览器,你就可以构建一个 ImpactJS 游戏。然而,还有一些有趣的工具可以大大简化你的开发过程。有Ejecta,它与 ImpactJS 一起提供,是为 iPhone 和 iPad 发布游戏的更有效的方式。AppMobi是一个为网络商店准备游戏的好工具。PhoneGap是一个创建应用程序的开源框架。使用lawnchair可以更轻松地使用本地数据存储。还有Scoreoid,一个免费的游戏云服务。最后是Playtomic——一个游戏分析工具。在本章中,我们将简要介绍其中的每一个。
Ejecta
Ejecta 是一款精巧的工具,可以免费下载,链接如下:
impactjs.com/ejecta
它完全取代了iOSImpact,这是一种为苹果商店准备游戏的本地应用程序的方式。Dominic 称 Ejecta 为“没有浏览器的浏览器”。它没有额外开销,只有你的游戏特性和音频元素的画布。
Ejecta 对 ImpactJS 效果很好,但也可以用于其他应用程序。就像以前的 iOSImpact 一样,它利用OpenGL进行动画和OpenAL进行音频,这大大提高了游戏性能。如果你计划将游戏发布到 iPhone 上,Ejecta 绝对值得一看。
AppMobi
AppMobi 提供了一个XDK(跨平台开发工具包),它与 ImpactJS 非常匹配。它们实际上为 ImpactJS(Impact XDK)和普通 XDK 分别提供了一个单独的开发工具包。
开发工具包是免费的,但额外的服务,如他们的云服务、实时更新功能和安全支付,需要额外付费。你可以在www.appmobi.com/
找到所有信息。
只有在注册了 Impact 密钥并包含了他们的 JavaScript 库的情况下,Impact XDK 才允许您在 Impact 游戏上工作。设置正确后,XDK 允许您模拟 iPad、iPhone、Galaxy 等多个设备。XDK 只在 Google Chrome 中运行,尽管这并不是一个真正的弱点。您可以打开脚本编辑器,但它并不像我们之前看过的那些编辑器那样好。您可以选择调用调试器,但它只是 Google Chrome 调试器,而不是他们自己构建的调试器。
Apphub(您的控制中心)允许您在将应用程序发送到商店之前构建和测试应用程序。当然,要发布游戏,您仍然需要为您想要服务的平台拥有开发者帐户。
AppMobi 还拥有他们所谓的直接画布加速,它通过绕过游戏的画布元素来提高游戏的性能。这与 Ejecta 所做的非常相似,但是它是由 AppMobi 提供的。
以下截图是 AppMobi 可以提供的不同地形的概述,这将给一些开发人员带来一些帮助。虽然在编写游戏脚本时 AppMobi 的用途有限,但在测试和部署过程中可以提供出色的支持。
无法直接连接到移动设备。但是,您可以向任何拥有移动设备的人发送链接。这样,您的朋友可以在安装了AppMobi applab的情况下测试您的最新创作。
总的来说,AppMobi 易于入门,并且在整个发布游戏的过程中值得考虑,尽管在开发过程中您几乎完全是靠自己。
PhoneGap
PhoneGap(以前称为Cordova)是另一个用于开发移动本机应用程序的 XDK。
PhoneGap 可以与 AppMobi 相比较,就功能而言,但 AppMobi 非常直观,更适合新手。PhoneGap 使您能够构建本地操作系统的应用程序,集成PayPal,并使用推送通知。
如下截图所示,PhoneGap 提供了一种构建您的应用程序以分发到不同渠道的方法:
开始使用 PhoneGap 比 AppMobi 要复杂一些。您需要安装eclipse(免费)、android 开发工具和 SDK。安装Git可能是针对特定平台的必要条件。如果要发布到 iPhone 或 iPad,您还需要xcode。
总的来说,这绝对值得一看。幸运的是,它们有非常好的文档,因为它往往会变得有点复杂。更多信息可以在phonegap.com/
上找到。
lawnchair
lawnchair 提供了一种免费且简单的使用本地存储的方法。本地存储用于在运行您的游戏的设备上存储您的数据(保存文件和高分)。
在客户端保存所有内容相比在服务器端保存有许多优势。首先,您不需要了解 SQL。网站通过使用 SQL、PHP 和 JavaScript 将所有内容保存在它们的数据库中。如果您使用本地存储,只需要 JavaScript。存储空间的数量不受服务器限制,而是由用户限制。因此,如果您有许多玩家,每个玩家使用少量空间,您在本地存储时永远不会遇到麻烦,而在仅使用服务器存储时可能会遇到麻烦。由于不需要始终传输到服务器,您可以离线玩游戏并保留您的存档。
这些都是相当不错的优势,但是 lawnchair 是如何工作的呢?lawnchair 就像 ImpactJS 一样是一个 JavaScript 库(但这次是免费的)。您只需要将它与其他 JavaScript 文件一起包含,就可以开始使用特定的命令来保存数据。
包括 lawnchair 功能可以通过从brian.io/lawnchair/
下载库并在您的index.html
文件中包含lawnchair.js
脚本来完成,如下面的代码示例所示:
<html>
<head>
<title>my osim app</theitle>
</head>
<body>
<script src="img/lawnchair.js"></script>
<script src="img/app.js"></script>
</body>
</html>
lawnchair 使用 JSON 在游戏的 DOM 中保存数据。如果您想要查看这是什么样子的示例,您可以在代码编辑器中打开任何 ImpactJSlevel
脚本,因为这些脚本也是用 JSON 编码的。
如果您的游戏需要保存游戏、高分、游戏进度或任何其他需要跟踪的选项,以便玩家不需要重新开始,您将需要查看 lawnchair。更多信息可以在brian.io/lawnchair/
上找到。
Scoreoid
Scoreoid 是一个旨在处理一些高级功能的游戏云服务,如排行榜、玩家登录和游戏内通知。
要使用 Scoreoid 及其功能,您需要在他们的网站上注册,并在必要时在您的代码中实现他们的代码。不同的功能有不同的代码。以下代码片段是存储有关加载游戏的人的信息的示例模板:
$.post("API URL",{api_key:"apikey",game_id:"gameid",response:"xml"},
function(data){
alert("Data Loaded: "+ data);
console.log("Data Loaded: "+ data);
});
您需要填写API URL、您自己的API 密钥、游戏 ID和用于传输的数据编码类型(XML 或 JSON),然后就可以开始了。
账户是免费的,但他们也有高级账户的选项,这也是免费的。但这只是因为他们目前仍在努力定义高级账户的额外功能。您可以在他们的网站www.scoreoid.net/
上订阅。
Playtomic
Playtomic 是游戏监控的 Google 分析。
基本账户是免费的,但高级账户目前的价格为每月15 美元或每年120 美元。您可以在他们的网站www.playtomic.com
上订阅。
让分析流程运行起来并不太困难。在您的index.html
文件中,您需要包含对他们 JavaScript 库的引用,如下面的脚本所示:
<script type="text/javascript"src="img/playtomic.v2.1.min.js">
</script>
然后,在您的main.js
脚本中,您可以添加一个命令将数据发送到他们的服务器,如下所示:
Playtomic.Log.View(gameid, "guid", "apikey", document.location);
这两段代码是 Playtomic 建议的。但是,如果您以纯文本形式将数据发送到他们的服务器,可能会出现错误。因此,最好将脚本类型text
替换为application
,如下面的代码片段所示:
<script type="application/javascript"src="img/playtomic.v2.1.min.js">
</script>
one.com webhost
如果您想将自己的游戏放在自己的网站上,您将需要webhost。
您并不总是需要自己的网站,因为像 Scoreoid 这样的云主机也允许您将游戏放在网上。然而,有时在全球网络上拥有自己的小地方也是不错的。
one.com以一种包的形式出售网络空间和域名。这项服务的价格相当合理,特别是与您需要做的事情相比。您需要有安装了 XAMPP 的 PC,并且它应该一直运行。此外,如果您是认真的,您仍然需要购买一个域名,或者从其他地方获得一个免费的域名并将您的 IP 重定向到它。如果您的 IP 始终保持不变,这是可行的。然而,更多时候,这是互联网提供商的高级服务。您可以在www.one.com
上注册一个账户。
如果您想使用 web 主机,还有更多的提供商,但在所有情况下都建议下载和安装FileZilla。FileZilla 是一个高效的文件传输程序,这正是您需要的,可以将您的所有文件从 PC 传输到沙漠中的某个服务器。FileZilla 可以在以下链接下载:
filezilla-project.org/
总结前面的内容,结论如下:
-
有很多工具可以让您作为游戏开发者的生活更加愉快
-
Ejecta 是将游戏发布到 iPad 和 iPhone 的高效解决方案
-
AppMobi 是一个免费的云工具,可以帮助发布和开发几乎每个分发渠道。
-
PhoneGap 与 AppMobi 有很多共同之处,尽管稍微复杂一些
-
lawnchair 提供了一种处理本地数据存储的方式
-
Scoreoid 是一个免费的游戏云服务,它将托管您的游戏并提供诸如排行榜集成之类的功能
-
Playtomic 是一款游戏分析工具,允许您标记游戏的某些元素并将数据存储在他们的服务器上
摘要
在本章中,我们作为游戏开发者做好了准备工作。我们已经建立了一个本地服务器,可以用作开发和初始测试环境。为了编写我们的代码,我们需要一个脚本编辑器,因此我们简要地介绍了一些编辑器。调试是程序员的主要技能之一;为此,我们不仅可以使用 Chrome 和 Firefox 调试器,还可以使用 ImpactJS 调试模块。最后,我们看了一些对 ImpactJS 游戏开发非常有帮助的工具。
现在我们已经准备就绪。在下一章中,我们将通过玩弄一个小的预制示例游戏来深入了解 ImpactJS。
第二章:介绍 ImpactJS
现在我们已经收集了所有必要的工具,并且第一个游戏已经开始运行,现在是时候更多地了解 Impact 的实际工作原理了。
但是,在深入代码之前,我们应该先将代码从chapter 2
文件夹复制到正确的位置。
与我们在第一章中所做的类似,启动您的第一个 Impact 游戏,我们只需要覆盖myfirstawesomegame
项目的main.js
和index.html
文件以及entities
、levels
、plugins
和media
文件夹。
我们现在已经准备好去探索 ImpactJS 引擎的复杂工作原理了!
在本章中,我们将涵盖以下过程:
-
ImpactJS 的 Weltmeister 工具以及更改关卡中某些参数的后果
-
层级图层如何影响关卡设计
-
在 ImpactJS 中如何处理碰撞
-
ImpactJS 实体
-
ImpactJS 实体的属性
-
可玩实体与不可玩实体的区别
-
如何生成或消灭一个角色
-
如何设置玩家控制
-
如何更改游戏的图形
-
如何在触发时播放音效和背景音乐
-
如何使用 Box2D 为游戏添加物理效果
创建自己的关卡
在设计游戏时,您会想要创建发生一切的环境和地点。许多游戏被分成不同的关卡,通常每个关升级都会变得更加困难。对于一些其他游戏,比如RPGs(角色扮演游戏),并没有所谓的关卡,因为这意味着通常没有回头的可能。在整本书中,可以将一个空间保存为 Weltmeister 中的单个文件,称为一个关卡。
Weltmeister 实际上是掌握 ImpactJS 世界的工具。如果您正确安装了 ImpactJS,您应该能够通过在浏览器中输入以下地址来访问下一个截图中显示的关卡编辑器:
http://localhost/myfirstawesomegame/weltmeister.html
在 Weltmeister 中创建、加载和保存关卡
为游戏创建关卡是游戏设计中最令人愉快的事情之一。Weltmeister 的设计非常出色,您将花费数小时来玩弄它,只是因为您可以。
打开 Weltmeister(默认情况下),它会从一个干净的画布开始;有大量的空白等待您填充。很快我们将开始从头构建一个关卡,但现在我们应该加载level1
关卡。按下 Weltmeister 右上角的加载按钮,并在levels
文件夹中选择它。如果您在本章的开头复制了它,它应该就在那里,否则现在将其复制到 Weltmeister 中。
Level1
是一个相当原创的第一关的名字,但让我们通过将其保存为myfirstepiclevel
来个性化一下。按下右上角的另存为按钮,并将其保存在相同的目录中。现在我们有一个副本可以使用和玩弄了。
在我们实际使用名为myfirstepiclevel
的关卡之前,我们需要在main.js
脚本的代码中进行更改:
-
在您首选的脚本编辑器中打开
main.js
脚本。 -
在
main.js
脚本中,您将看到对loadLevel()
函数的调用。
this.loadLevel(LevelLevel1);
注意
这个调用位于游戏的init()
函数中(ig.game.init
)。这意味着main.js
脚本将在初始化(即init
)时做的第一件事情之一是加载Level1
关卡。显然,我们不再需要这样做了,因为现在我们有自己的关卡叫做myfirstepiclevel
。为了让游戏知道它必须包含这个关卡,您需要将它添加到.requires()
函数中,如下面的代码行所示:
'game.levels.level1',
'game.levels.myfirstepiclevel',
- 还要更改对
loadLevel()
函数的调用,使其调用myfirstepiclevel
关卡,而不是Level1
,如下面的代码片段所示:
this.loadLevel(LevelMyfirstepiclevel1);
提示
正如您可能已经注意到的,您总是需要在实际级别名称之前加上Level
一词。此外,您将始终需要用大写字母写Level
和您的级别名称。如果违反其中任何一个,将导致游戏加载时发生严重崩溃。在实际级别名称之前加上Level
一词是一个相当奇怪的约定,特别是因为loadlevel()
等函数被设计为期望一个级别文件。可能在未来的 ImpactJS 版本中会删除这个强制前缀。但是目前,如果未在实际级别名称之前插入Level
一词或者用大写字母写Level
和您的级别名称,将导致显示以下错误:
Weltmeister 中的其他按钮有保存、新建和重新加载图像。保存按钮只是保存您正在处理的文件,新建按钮将打开一个新的空文件。重新加载图像按钮是瓷砖集的刷新按钮。游戏的瓷砖集是图像的集合。单个主题的所有图形可以在单个瓷砖集中,例如outdoor
瓷砖集。因为几个图像存储在一个称为瓷砖集的整体图像中,所以在 Weltmeister 中工作时更容易创建您的级别。您可以将其视为艺术家的调色板,但作为级别创建者,您可以使用与瓷砖集一样多的调色板。
总结我们所遇到的一切,我们可以得出结论:
-
您可以在服务器打开时在浏览器中输入以下地址访问 Weltmeister:
localhost/myfirstawesomegame/weltmeister.html
-
使用加载按钮打开
level1
-
再次保存为
myfirstepiclevel
,使用另存为按钮 -
通过将
myfirstepiclevel
添加到include()
函数中,将新级别包含在main.js
脚本中
图层和 z 轴
打开层级后,您可以看到它包括的不同元素和层。让我们首先看一下编辑器右侧的图层菜单。
选择碰撞图层,您将看到需要填写的图层的标准属性。所有图层(除了实体图层)都有名称、瓷砖集、瓷砖大小、尺寸和距离。
瓷砖集基本上是由方形图像链组成的,当组合得足够好时,形成您理想的风景或可怕的地牢。瓷砖大小是以像素为单位测量的一个瓷砖的宽度和高度。由于所有瓷砖都是正方形,您只需要填写一个数字。图层的尺寸是图层需要出现的整个地图的宽度和高度,以瓷砖数计量。因此,具有瓷砖大小为 8、宽度为 20 和高度为 30 的图层由 4800(8 x 20 x 30)像素组成。在使用移动设备时要考虑这一点。分辨率为 160(8 x 20)x 240(8 x 30)的级别将适合大多数设备。但是,如果瓷砖大小为 32,您将需要一个自动跟随可玩角色的视口来展示您的级别。这个视口相当容易整合,将在本章后面进行解释。按照以下步骤创建一个新的瓷砖集:
-
尝试通过单击图层选择菜单顶部的加号(+)号来创建一个新图层。
-
为图层输入一个名称;比如
astonishinglayer
或tree
,随便你喜欢什么。 -
现在从
media
文件夹中选择tree
瓷砖集,方法是点击瓷砖集字段旁边的空框。如果无法通过 Weltmeister 菜单访问,只需在瓷砖集框中输入media/Tree.png
。将瓷砖大小设置为32
,尺寸设置为30 x 20
(宽度 x 高度)。您可以看到图层边界相应地改变。
提示
一个常见的错误是一个层比另一个层小,然后无法在地图的某个部分添加对象。所以假设你的级别意图是一个尺寸为 30 x 20,瓷砖大小为 32 的地图,然后你添加了一个这样的层,并用草填充它。你想在草地上添加一个长凳,所以你添加了另一个层,并将尺寸设置为 30 x 20。因为你的长凳是一个 32 x 16 的图像,你将瓷砖大小设置为 16。如果你这样做,你将能够相当精确地绘制你的长凳,但只能在你的级别的左上角。你需要将尺寸改为 60 x 40,以便占据与草层相同的空间。
距离是层相对于游戏屏幕位置移动的速度。在“距离”字段中的值为 1 意味着它以相同的速度移动,而值为 2 意味着层移动速度减半。通过将此参数设置为大于 1,可以使事物看起来更远;这对于侧向滚动(或视差)游戏中的漂亮多云背景非常理想,比如马里奥。前往游戏,让你的角色从游戏的最左边向右边走,观察“距离”字段值的改变对效果的影响。
现在返回到 Weltmeister,尝试将“距离”字段的值设置为 2。保存并重新加载游戏,让你的角色从级别的一边跑到另一边,看看会发生什么。游戏的一部分将看起来比其他部分移动得更慢。这在侧向滚动游戏中作为背景很有用,但也用于顶部游戏中创建恐怖深渊的印象。在下面,你有“是否碰撞层”、“游戏中预渲染”、“重复”和“与碰撞链接”的选项。通过点击白色方块(变黑表示选项已关闭)可以打开或关闭它们。
“是否碰撞层”选项将告诉关卡编辑器,你正在绘制的层中的对象是不可穿透的。预渲染一个层会导致游戏在加载时对图块进行聚类。这将增加初始加载时间,但减少游戏所需的绘制次数,从而提高运行性能。
“重复”选项用于背景层。例如,如果你的背景云是一个图案,可以重复出现。
最后,“与碰撞链接”选项将确保对于你绘制的每个对象,碰撞方块都会添加到“碰撞”层。你可以稍后从“碰撞”层中删除它们,但这是一个加快绘制墙壁和其他不可通过地形的有用工具。
在“层”菜单中可以通过将它们拖动到列表中的上方或下方来重新排列层。通过将一个层拖到列表的顶部或底部,你可以定义它在 z 轴上的位置。你应该把 z 轴看作级别的第三维,就像我们生活的世界有一个 x 轴(宽度),一个 y 轴(高度)和一个 z 轴(深度)。你构建的游戏并不是传统意义上的 3D,但由于 2D 图形是叠加在一起的层,这里实际上有一个第三维在起作用。列表顶部的图形层将始终可见,甚至会隐藏实体。底层只有在没有其他东西挡住时才能可见。“碰撞”层永远不可见,但将其拖到顶部将使你更容易对其进行修改。
尝试重新排列层,看看会发生什么。保存游戏并重新加载。根据你对层做了什么疯狂的事情,世界现在确实是一个非常不同的地方。
与其将一个图层拖到堆栈的顶部以便能够查看它,你也可以打开和关闭图层。这是通过点击图层名称前面的方框来实现的。这在实际游戏中不会产生任何影响;它只在 Weltmeister 中可见。这对于碰撞图层非常有用。尝试将碰撞图层拖到堆栈的顶部,然后随意打开和关闭它。你会注意到这是在使用 Weltmeister 时碰撞图层的最佳位置。这是因为碰撞图层本身在玩游戏时实际上没有图形,所以它不会遮挡其他任何东西。
总结我们遇到的细节,我们得出结论:
-
一个关卡由具有诸如图块大小、距离以及是否为collision图层等属性的不同图层组成
-
使用Layers菜单中的(+)号添加一个新图层,并将其命名为
astonishinglayer
-
将图块集
media/tree.png
添加到图层中。将其尺寸设置为30 x 20
,将其图块大小设置为32
-
尝试玩弄图层上的所有属性,包括将图层上下拖动
-
每次调整参数后,保存关卡并在浏览器中重新加载游戏
添加和移除实体和物体
有三种大类型的图层:entities,collision,和其他任何图层。对于实体和死物体,实体和图形图层是感兴趣的。
entities图层包含了entity
文件夹中存在的并由main.js
脚本调用的所有实体。实体可以是任何东西,从玩家使用的角色到一个会杀死靠近的一切的隐形陷阱。所有功能和关卡的人工智能都在这些实体中。它可以包含敌人、触发器、关卡变化、随机飞行物体、可发射的抛射物,以及所有可以互动的东西。
提示
如果这些实体中存在关键错误,或者在你的main.js
脚本中包含了一些不存在的实体,Weltmeister 甚至无法加载。因此,确保这些实体在你想要构建关卡时始终没有错误(或者没有包含)。
一些实体,比如玩家,已经存在于关卡中。首先在Layers菜单中选择entities图层,然后选择玩家实体以查看其属性。**x:和y:**属性是它当前的位置,并且在将新实体放入关卡时始终存在。
通过选择玩家并将其拖动到其他位置来尝试移动玩家实体。**x:和y:**坐标现在会改变。
让我们在关卡中添加一个敌人实体。选择entities图层,并在鼠标悬停在关卡上时按下空格键。一个菜单将出现在鼠标旁边;在这个菜单中选择敌人实体。一个敌人刚刚出现在你鼠标的位置!现在你可以疯狂地在每个方块上画上敌人实体,但这可能有点过火,所以让我们现在只放一个敌人。保存并重新加载你的游戏。现在,当敌人攻击你或者无动于衷地盯着它时,你会感到恐惧,这取决于你。
如果你添加了太多的敌人以至于无法安全地漫游,首先在 Weltmeister 中选择entities图层,然后选择你想要摆脱的敌人,然后简单地按下Delete键将它们从游戏中移除。
提示
将游戏和 Weltmeister 都打开以检查你所做的更改是一个好习惯。如果由于某种原因,你添加的实体是损坏的,游戏拒绝加载,至少你知道问题出在你最后做的更改上。当然,你还有 Chrome 或 Firefox 的调试器,它们也会指引你走向正确的方向。
添加对象与添加实体不同。死对象,不能与之交互,只是一个图形的东西,可以简单地涂抹,例如,一块草地、一个喷泉或一堵城墙。这些对象的复杂交互可以完成,但只能通过实体来实现。在这里,我们将看看如何向关卡添加一个简单的对象,没有交互。
虽然关卡看起来相当整洁,但我们需要对其进行改头换面。让我们从图层菜单中选择草地图层。将鼠标悬停在地图上,按下空格键。一个图块集将出现;你可以通过再次按下空格键使其消失。如果这个图块集不适合你的屏幕,你可以将鼠标悬停在更中心的位置并在那里打开它,或者使用鼠标滚轮缩小。如果你没有滚轮,你可以使用Ctrl + -(减号)组合键缩小,使用Ctrl键和加号键(+)放大。现在你可以看到整个草地图块集。选择草地,通过点击并按住鼠标左键在所有地方涂抹。
提示
用单个图块涂抹大面积的小技巧是,首先只在地图上涂抹一个小区域。然后点击Shift +鼠标左键,选择来自关卡本身的这个新绘制的更大的图块区域。现在你可以用这个新选择的图块涂抹,以更少的时间覆盖更大的区域。
如果你想从给定的图层中删除某些东西,只需选择该特定图层中的一个空方块。如果你已经在某个位置有其他图层的图形,但不是你当前正在操作的图层,那个方块可以被视为空的。现在用这个空方块涂抹,先前选择的图块将神奇地消失。现在试着删除一些草地。
草地位于一切的底部。如果你有一个对象,任何对象,它总是在草地的上面,从来不在下面(除非在一些疯狂的鼹鼠世界)。为了实现这一点,你必须将你的草地图层拖到图层堆栈的底部。
让我们在场景中添加一些其他东西。我们还有我们创建的图层astonishinglayer
,准备好了,所以让我们用它画一棵树。为了一次性选择整棵树,通过点击Shift +鼠标左键组合键选择树。根据你放置图层的位置,树现在将始终出现在玩家的前面或后面。如果你将图层拖到列表的底部,甚至可能看不见。这是一个奇怪的结果,我们稍后会处理。保存你的关卡并重新加载,查看你的第一个关卡创意。
总结添加和删除实体和对象的过程,我们得出结论:
-
实体图层提供了所有游戏实体的选择
-
你可以将一些当前的实体添加到关卡中,然后保存并重新加载游戏
碰撞图层
碰撞图层是一个特殊的图层,在你从头开始打开 Weltmeister 时并不是预定义的。它是特殊的,因为它是一个不可见的图层,标记着不可通过的区域。例如,如果你通过使用图形图层在地图上画一堵墙,所有的实体都可以穿过它,就好像它根本不存在一样。如果你想要一堵真正能够阻止玩家和他的敌人的墙,就在碰撞图层上画一条线。
你的游戏还在打开;尝试画一堵墙(或者其他任何物体),然后在层次的底部穿过它。你会发现很容易穿过看起来很坚固的东西。选择collision图层,如果还没有完成,将其拖到列表的顶部,并确保其visibility选项已打开。现在所有的瓷砖都清晰可见,你会发现底部墙上没有瓷砖。将鼠标悬停在层次的画布上,按空格键以打开碰撞瓷砖集。选择一个方块,在墙上画一条线。删除碰撞块就像删除图形一样。选择地图上的一个区域(按住Shift键或不按住)没有碰撞块,并使用这个选择来删除那些存在的碰撞块。保存层次并重新加载游戏。现在再试着穿过墙;这已经变得相当不可能了;为此欢呼!
总结前面的过程:
-
在 Weltmeister 中选择collision图层
-
用它画一些瓷砖
-
保存并重新加载游戏,看看如果你想走到你画的碰撞瓷砖的地方会发生什么
连接两个不同的层次
现在我们知道了如何通过添加一些图形,比如草地、树木、玩家和一些敌人来构建一个层次,是时候看看层次是如何连接的了。
为此,将内部层次加载到 Weltmeister 中。内部层次位于建筑物内部(你没想到这一点,是吧?)。就像我们对myfirstepiclevel
所做的那样,我们需要在main.js
脚本中更改对loadlevel()
函数的调用,如下面的代码片段所示。然而,这次,层次本身已经包含在main.require
脚本中。
this.loadLevel(LevelInside);
同样,不要忘记大写字母。
加载 Weltmeister 和游戏本身,看看是否一切都设置正确了。
在 Weltmeister 中,通过选择entities图层查看层次的实体。如果你无法清楚地看到地图中存在的实体,请随意关闭其他图层,方法是点击它们的白色方块。或者你可以在悬停在地图上时按空格键,以打开实体选择菜单。和往常一样,我们有一个玩家实体,所以我们可以在地方四处移动,但是在菜单中,你应该注意到一些额外的实体,比如Void,Trigger和Levelchange:
-
Void实体是一个相当简单的实体;它只是一个带有名称和一些坐标的盒子
-
Trigger实体将在特定类型的实体(如玩家)与其碰撞时触发与其链接的任何其他实体的代码。
-
LevelChange实体将使游戏加载另一个层次
通过巧妙地组合这三个实体,你可以连接层次,所以让我们来做吧:
-
确保entities图层是顶部之一,这样你就可以看到你添加的东西。
-
首先选择Trigger实体,并将其放在靠近门的地图上。一开始它只是一个小方块,所以把它做得大一点,以适应出口。你可以通过选择方框,将鼠标移动到其边缘,直到看到双箭头(双箭头符号),然后拖动它使其变大(就像你在 PC 上调整任何窗口对象的大小一样)。在选择大小时,你的目标是在玩家想要使用门出去时检测到他。
-
现在添加一个Levelchange实体。如果选择Levelchange实体,您将在右侧看到其属性。目前,这只是地图上的位置(x 和 y 坐标)和其尺寸,以防您重新调整了框的形状。通过在键框中输入
name
,为Levelchange实体命名为ToOutside。按Enter键确认。现在您将看到该实体具有额外的属性(名称),其值为ToOutside。只有通过给它一个名称,它才能被唯一标识,这就是我们需要的。我们还需要告诉它需要加载哪个关卡。添加键level,值为outside,然后按Enter键。 -
Trigger和Levelchange实体现在都在关卡中,但它们尚不知道彼此的存在;如果我们希望它们合作,这一点非常重要。
-
返回到触发器实体并给它一个目标。您可以通过在键框中输入
target.1
,值为ToOutside来实现。注意单词target
后面的句点(.
);没有它,它将无法工作。现在按Enter键,看着两个漂亮的方块如何通过一条白线连接在一起,如下图所示。Trigger实体现在知道它是Levelchange实体;当玩家触摸到它时,它将被触发。
保存并加载关卡。将您的玩家走向触发器位置;Levelchange实体的位置是无关紧要的。如果一切顺利,现在您应该能够通过走向门来进入下一个关卡!
奇怪的是,当您进入外部世界时,并没有被放置在建筑物旁边。即使对于一个视频游戏来说,这也太奇怪了。此外,当试图打开门时,没有办法回到室内,您将永远被困在外面,除非重新加载。
这是因为在外部关卡中没有添加spawnpoint、Trigger或Levelchange实体。我们将弥补这一点,但首先让我们在内部关卡中添加一个出生点。
为了做到这一点,我们需要Void实体。将Void实体添加到关卡中,并将其放在门前,但是超过触发器。将其放得太靠近(或者在上面)触发器会导致玩家被击退到外面。虽然制作一个永恒的循环,让玩家在关卡之间来回击退是很有趣的,但是永恒的循环(就像除以零一样)有可能摧毁世界。将Void实体命名为insideSpawn
。选择Levelchange实体并添加键spawn,值为OutsideSpawn。
我们已经完成了内部关卡,现在需要将外部关卡设置为其镜像相反。因此,再次添加Void、Levelchange和Trigger实体。将Void实体命名为OutsideDoor
,因为Levelchange实体将寻找它。将Levelchange实体命名为ToInside
,并将触发器指向它。还要向Levelchange实体添加Level和spawn属性。这些属性的值分别为Inside和InsideDoor。
保存并重新加载游戏。如果一切顺利,您现在应该能够像专业人士一样在两个关卡之间移动。
总结连接两个关卡的完整过程:
-
在 Weltmeister 中加载内部关卡
-
向关卡中添加三个实体,Trigger、Levelchange和Void
-
给每个实体命名
-
使触发器指向Levelchange实体
-
将这些信息添加到Levelchange实体中:它需要加载的关卡和它将要使用的出生点
-
保存内部关卡,加载外部关卡,并在那里重复练习
-
确保两个关卡都已保存并在浏览器中重新加载游戏
对象-可玩和不可玩角色
现在我们已经看过如何构建一个级别,是时候深入研究我们一直在玩的实体背后的代码了。虽然没有官方分类,但可以通过区分三种类型的实体来简化事情:死亡对象、不可玩角色和玩家实体本身。这三种类型的实体按复杂性和互动性逐渐增加排序。在本章的第一部分,我们看了游戏的图形层。纯粹的图形根本没有任何互动元素;它们只是作为稳定的元素存在。要从你正在玩(构建)的游戏中得到一些反馈,你需要实体。这些实体中最简单的是死亡对象,它们根本没有任何人工智能,但可以进行交互,例如,可以拾取的物品,如硬币和药水。我们已经调查过的一种实体类型是Trigger实体,它本身是不可见的,但可以放置在与图形相同的级别,并且可以指示游戏中将会发生的事情。岩浆的图形不会杀死你。但是,精心放置在岩浆下面的实体会告诉游戏摧毁进入该区域的一切。在复杂性方面稍微上升的是NPC(不可玩角色)。这些是你的敌人,你的朋友,你作为玩家将杀死或保护的一切,或者如果你愿意的话,可以忽略。它们可以是毫无头脑的僵尸,也可以是复杂而非常精确的对手,比如国际象棋电脑。游戏中最后一个也是最复杂的实体就是你,或者至少是你的化身。可玩角色是迄今为止最多才多艺的角色,值得在本章后面进行详细阐述。在这样做之前,我们首先必须看一看是什么使 ImpactJS 实体成为它所是的。
ImpactJS 实体
为了解释实体的基础知识,最好先看一看死亡对象。这些实体没有像不可玩角色或玩家那样复杂的行为模式,但肯定比纯粹的图形复杂得多。
一个例子是Void实体,我们在本章前面设置级别转换时遇到的一个好朋友。在脚本编辑器中打开void.js
文件,这样我们就可以看一看。以下代码片段是Void实体的一个例子:
ig.module(
'game.entities.void'
)
.requires(
'impact.entity'
)
.defines(function(){
EntityVoid = ig.Entity.extend({
_wmDrawBox: true,_wmBoxColor: 'rgba(128, 28, 230, 0.7)',_wmScalable: true,size: {x: 8, y: 8},update: function(){}});
});
每个实体至少会调用ig.module
,requires()
和defines()
函数。
在ig.module
函数中,你将Void实体定义为一个模块。ig.module
函数调用定义了Void实体作为一个新模块。模块名称应该与脚本的名称相同。放在game
文件夹中的entities
文件夹中的void.js
文件将成为game.entities.void
文件。
requires()
函数将调用此实体所依赖的代码。像所有实体一样,虚空实体依赖于 Impact Engine 中的实体原型代码,因此被命名为impact.entity
。
defines()
函数使你能够定义这个特定模块的全部内容。看一看defines()
函数里面有什么。我们看到EntityVoid
模块被定义为实体类的扩展,如下所示:
EntityVoid = ig.Entity.extend({
在实体名称前始终添加Entity
,不要忘记大写字母。如果你不这样做,Weltmeister 就不会喜欢,你会收到一个错误消息,说它期望一个不同名称的实体。Weltmeister 将生成以下错误:
Void实体是一个特殊实体,因为它在游戏中是不可见的;这一点从代码并未指向media
文件夹中的某个图像就可以看出。相反,它有三个属性适用于 Weltmeister:_wmDrawBox
,_wmBoxColor
和_wmScalable
。_wm
前缀属性表明它们对 Weltmeister 很重要。
_wmDrawBox: true,
上一个代码片段告诉 Weltmeister 在将实体插入到级别时必须绘制一个框。将此属性设置为false
,则不会应用来自_wmBoxColor
属性的颜色。
_wmBoxColor: 'rgba(128, 28, 230, 0.7)',
上一个代码片段定义了此框的颜色,采用 RGBA 颜色方案。对于Void实体,目前颜色是紫色。
_wmScalable: true ,
上一个代码片段将允许您使框变大或变小。这对于像Trigger实体这样的事物特别有用,您可能在以前连接两个级别时将其转换为一个小但相当长的矩形。
size: {x: 8, y: 8},
在上一个代码片段中,size
属性是实体的默认大小。由于这个实体是可伸缩的,您可以在 Weltmeister 中进行更改。
update: function(){}
最后是update()
函数。每个实体每帧调用一次此函数,无论您是否明确提到调用此函数,如前面的代码片段所示。
尝试更改Void实体的默认参数并重新加载 Weltmeister,看看会发生什么。
Void实体是一个简单而有用的实体,但让我们面对现实,它也相当无聊。让我们看看更有趣的东西,比如硬币。假设您希望玩家每次拾取硬币时都变得更加富有。
以下是一个Coin实体示例:
为此,您将需要一个Coin实体,让我们在编辑器中打开coin.js
文件。与Void实体类似,它有一个名称(coin),需要impact.entity
库,是原型实体的扩展,并具有大小。然而,在以下代码中还有一些其他有趣的属性:
collides: ig.Entity.COLLIDES.NEVER,
type: ig.Entity.TYPE.B,
checkAgainst: ig.Entity.TYPE.A,
type
、collides
和checkAgainst
属性都与硬币在与其他实体碰撞时的行为有关。type
参数告诉游戏硬币在评估碰撞时属于类型B
。硬币实际上从不与任何东西发生碰撞,因为其collides
属性设置为NEVER
。这里的其他可能性是:LITE
、PASSIVE
、ACTIVE
和FIXED
。LITE
和PASSIVE
实体不会相互碰撞。FIXED
实体无法移动,LITE
实体可以被ACTIVE
实体移动。如果ACTIVE
实体与另一个ACTIVE
或PASSIVE
实体发生碰撞,则两个实体都会移动。
起初听起来有点棘手,但值得尝试。打开player.js
文件,并确保collides
属性设置为ACTIVE
。现在使用 Weltmeister 在游戏中添加一个硬币,靠近玩家的起点。通过在下面的示例中添加两个破折号(//
)将硬币的checkAgainst
属性注释掉:
//checkAgainst: ig.Entity.TYPE.A
如果将coin实体的模式设置为FIXED
,则无法移动硬币。当将其模式设置为PASSIVE
或ACTIVE
时,可以移动硬币,但会很困难,因为硬币会推回。然而,设置为LITE
属性的coin实体将非常容易移动。最后,当coin实体重新设置为NEVER
属性时,玩家会直接穿过硬币,就好像它不存在一样。我们使用 Weltmeister 向墙上添加碰撞瓦片;这些瓦片可以被视为FIXED
,因此不会被实体移动。
从checkAgainst
属性中删除破折号,以使其再次起作用,因为这告诉coin实体检查类型为A
的实体是否触碰它(玩家实体设置为A
)。
虽然Void实体是可见的,但硬币具有游戏内图形,并且它们位于AnimationSheet帧中。
animSheet: new ig.AnimationSheet('media/COIN.png',16,16),
这个AnimationSheet
帧实际上只是一个 16 像素的正方形图像,所以它并不能真正实现动画。为此,您需要一个至少包含两个不同图像的单个 PNG 文件。
然而,我们可以用第二个硬币替换这个硬币。通过将COIN.png
更改为COIN2.png
(保存并重新加载)来实现这一点。
每个实体的init()
函数将定义它们的标准属性。
init: function(x, y , settings){
this.parent(x,y,settings);
this.addAnim('idle',1,[0]);
}
由于coin实体没有太多属性,init()
方法相当空。
我们调用了父实体,这里只是entity
。this.addAnim()
函数是一个能够为 coin 添加动画的 impact 函数。它有三个输入:
-
实体的状态(
idle
) -
从一个动画切换到另一个动画的速度(
1
秒) -
它必须经过的图块集上的图像(图像
0
)
显然,由于只有一张图片,实际上没有动画。
check()
函数是每个实体非常有趣的一个方法。以下示例代码解释了check()
函数:
check: function(other){
ig.game.addCoin(); // give the player a coin when picked up
this.kill(); //disappear if you are picked up
}
它检查是否与另一个实体重叠,如果是,将执行函数中规定的操作。check()
方法与checkagainst
属性相关联;唯一相关的重叠将是其中声明的实体类型。在这种情况下,当玩家触碰到 coin 时,check()
函数将触发。这将导致触发ig.game.addCoin()
函数,然后使用this.kill()
函数将 coin 从游戏中移除。
死亡对象通常是非常简单的实体,只有几行代码,不可玩角色甚至有一个简单的 AI,而可玩角色则完全是另一回事。
总结可玩和不可玩角色的创建,我们可以得出结论:
-
与纯粹的图形相反,ImpactJS 实体是一个交互式游戏元素。
-
死亡对象是最不复杂的实体;Void和coin实体就是其中的两个例子。
-
Void实体在游戏中是不可见的,但在 Weltmeister 中是可见的,因为它具有特殊的 Weltmeister 属性。在本章的前面,我们曾将其用作生成点。
-
coin实体在游戏中是可见的,因为它有一个动画表。它也可以被玩家捡起,因为有碰撞检测。
-
碰撞检测可以采用多种形式:实体可以杀死、阻挡、推开,或者根据其碰撞属性简单地忽略彼此。
-
尝试玩弄Void和coin实体中解释的所有参数,看看会发生什么。
设置玩家控制
没有什么比实际玩家和他或她送入遗忘的敌人更有趣了。
如果你打开player.js
和enemy.js
文件,你会发现有很多关于这些实体需要讨论的内容。从动画到控制再到音效等等,它们确实很复杂。所有这些东西将在本章剩余的页面中逐渐揭示。但首先,ImpactJS 如何区分可玩和不可玩的角色呢?
你称一个实体为 player 并不会自动使其成为 player;ImpactJS 没有为这个实体保留名称,以识别什么可以被控制,什么不是由玩家控制的。这将非常有限,因为RTS(实时战略)游戏取决于同时移动不同可玩对象的能力。这意味着区分这两个实体的唯一元素是它们是否可控。
打开player.js
文件,滚动到以下代码:
if(ig.input.state('up') || ig.input.pressed('tbUp')){
this.vel.y = -100;
this.currentAnim = this.anims.up;
this.lastPressed = 'up';
}else if(ig.input.state('down') || ig.input.pressed('tbDown')){
this.vel.y = 100;
this.currentAnim = this.anims.down;
this.lastPressed = 'down';
}
else if(ig.input.state('left') || ig.input.pressed('tbLeft')){
this.vel.x = -100;
this.currentAnim = this.anims.left;
this.lastPressed = 'left';
}
else if(ig.input.state('right')||ig.input.pressed('tbRight')){
this.vel.x = 100;
this.currentAnim = this.anims.right;
this.lastPressed = 'right';
}
在这里,我们可以看到玩家实体将对输入做出反应。当输入命令up
时,角色将向上移动并显示动画。这些up
,down
,left
和right
状态不是 ImpactJS 的关键字。实际上,它们是在主脚本中定义的。打开main.js
文件,看一下以下代码:
if(!ig.ua.mobile){
ig.input.bind(ig.KEY.UP_ARROW, 'up');
ig.input.bind(ig.KEY.DOWN_ARROW,'down');
ig.input.bind(ig.KEY.LEFT_ARROW,'left');
ig.input.bind(ig.KEY.RIGHT_ARROW,'right');
// fight
ig.input.bind(ig.KEY.SPACE,'attack');
ig.input.bind(ig.KEY.CTRL,'block');
在这里,你可以看到哪个键与哪个输入状态相关联。还要注意键绑定之前的if
语句。首先要检查的是你是否在处理移动设备。这是因为 iPad 和 iPhone 上不存在 Space 键和方向箭头等键。尝试将攻击状态绑定到鼠标左键,而不是 Space 键,代码片段如下:
ig.input.bind(ig.KEY.MOUSE1,'attack');
所有可能的组合都可以在 ImpactJS 网站上找到。
保存并重新加载游戏,注意您的触发手指是如何从空格键移动到左鼠标按钮的。
请注意,这些初始键绑定定义在main.js
脚本的init()
函数中,而在player.js
脚本中的update
函数中等待输入。这是因为实际的键绑定只需要在游戏初始化时进行一次,而您的玩家需要始终受控制。update
函数在游戏经过完整的游戏循环时被调用,这与您的帧速率相同。假设您的帧速率为 60fps(每秒 60 帧),在这种情况下,update
函数将每秒检查用户输入 60 次。
处理移动设备时情况有些不同。由于几乎没有按键,您需要使用 HTML 对象添加虚拟按钮。
打开index.html
文件,并键入以下代码以添加虚拟按钮:
if(ig.ua.mobile){
// controls are different on a mobile device
ig.input.bindTouch( '#buttonLeft', 'tbLeft' );
ig.input.bindTouch( '#buttonRight', 'tbRight' );
ig.input.bindTouch( '#buttonUp', 'tbUp' );
ig.input.bindTouch( '#buttonDown', 'tbDown' );
ig.input.bindTouch( '#buttonJump', 'changeWeapon' );
ig.input.bindTouch( '#buttonShoot', 'attack' );
}
将 ImpactJS 游戏加载到浏览器时,实际加载的是这个页面,游戏本身只显示在页面内的 canvas 元素中。这意味着除了 canvas 元素之外,还可以添加其他东西,比如 HTML 按钮。由于每个按钮都可以用触摸板按下,通过巧妙使用这些按钮,可以为游戏添加无限数量的交互功能。您可以在index.html
文件中找到以下按钮定义,如下所示的 HTML 代码:
<div class="button" id="buttonLeft"></div>
<div class="button" id="buttonRight"></div>
<div class="button" id="buttonUp"></div>
<div class="button" id="buttonDown"></div>
<div class="button" id="buttonShoot"></div>
<div class="button" id="buttonJump"></div>
按钮是<div>
元素,其中div
是 division 的缩写。<div>
元素与 CSS 代码一起用于布局网页。在这种情况下,它们为我们提供了四个箭头,用于选择方向。
<div>
元素有几个属性;其中,id
属性对我们来说特别重要,因为它唯一标识了<div>
元素,并使我们能够链接到 JavaScript 代码。这可以在main.js
脚本中的bindTouch
方法中看到。
ig.input.bindTouch('#buttonLeft', 'tbLeft' );
它的第一个参数是<div>
元素的唯一 ID,前面加上#
符号;这样 JavaScript 就知道它需要查找一个 ID。第二个参数是我们称之为tbleft
(触摸绑定左)的输入状态。
如果您有 iPad 或其他移动设备,并且将游戏放在在线服务器上,您就可以在那里加载游戏。
现在输入键(无论是真正的键盘还是虚拟键)都绑定到了 ImpactJS 状态;这些状态可以用于跟踪玩家控制。当然,一个例子就是朝着某个方向移动。
总结设置玩家控制的程序:
-
控制一个实体是可玩角色和不可玩角色(NPC)之间的区别。
-
键盘和动作名称之间的链接在主脚本中定义一次。您应该尝试更改这些控件以适应您自己的偏好。
-
动作名称和实际执行动作之间的链接可以在玩家实体本身找到。
-
在移动设备上,您在某种程度上受限于触摸屏。可以使用 HTML
<div>
标签实现虚拟按钮。
位置、加速度和速度
一切都有位置,有些东西正在前往某个地方。在 ImpactJS 世界中,定位是通过 x 和 y 坐标以及第三个不太直观的 z 索引来完成的。
x 和 y 坐标是到达级别左上角的距离,以像素为单位。x 坐标是水平轴上任何对象的位置,从左到右计数。y 坐标是垂直轴上的位置,从上到下计数。对于习惯于查看图表的人来说,这个 y 坐标有点反直觉;y 坐标在底部始终为 0,在向上移动时会变得更高。请注意,级别的左上角并不总是与画布的左上角相同!你可以看到游戏的画布只是世界的窗口。这在策略游戏中非常明显,你永远看不到整个世界,通常会得到一个小地图,以便更快地从战斗到战斗中导航。
每个实体都有 x 和 y 坐标,当你使用 Weltmeister 时,你可以在地图上拖动实体时看到这种变化。在实体代码中,你可以像这样引用(和更改)它的位置:
this.pos.x = 100;
this.pos.y = 100;
如果你想让事物进行瞬间移动,这很好,但通常你只是希望它们移动得更微妙一些。为此,我们可以调整速度和加速度等属性。将速度设置为与0
不同的数字将使实体的位置随时间改变。设置加速度将随时间改变速度。
if(ig.input.state('up') || ig.input.pressed('tbUp')){
this.vel.y = -100;this.currentAnim = this.anims.up;
this.lastPressed = 'up';
}
我们在讨论玩家控制时已经看到了这段代码。this.vel.x = -100
命令将使玩家以每秒 100 像素的速度向上移动。因为正如我们之前看到的,需要将速度设置为负值才能向上移动,y 轴是反向的。速度可以分别设置为每个方向。例如,你可以创建一个区域,强风使英雄逆风时移动更慢,但在 90 度角下移动时不受影响,玩家甚至可能在风助下向后移动得更快。尝试使用以下代码更改速度来模拟来自北方的强风:
if(ig.input.state('up') || ig.input.pressed('tbUp')){
this.vel.y = -25;
this.currentAnim = this.anims.up;
this.lastPressed = 'up';
}
else if(ig.input.state('down') || ig.input.pressed('tbDown')){
this.vel.y = 400;
this.currentAnim = this.anims.down;
this.lastPressed = 'down';
}
加速度反过来影响了随时间的速度。加速度有点棘手,因为减速并不自然地停止,而是转向相反的方向,此时减速实际上变成了加速,反之亦然。为了引入加速度因素,我们插入以下代码:
if(ig.input.state('accelerate')){
this.accel.x = 1;
this.accel.y = 1;
}
if(ig.input.state('slow_down')){
this.accel.x = -1;
this.accel.y = -1;
}
为了确保加速不会使你的实体以光速前进,只要有足够的时间和按钮操作,你可以使用以下代码示例设置最大速度:
maxVel:{x:200,y:200},
尝试将此代码片段添加到player.js init()
函数或作为属性。如果你的风效果仍然存在,那么下风的效果应该比以前要弱一些。
除了 x 和 y 坐标,还有第三个维度在起作用。为了给游戏增加一些深度感,实体可以放置在彼此的前面。对于图形层,这可以通过在 Weltmeister Layers菜单中上下移动来简单地完成。在那里,你可以永久地将图层放在其他图层和所有实体的前面或后面。然而,实体之间的解决方式并不是在 Weltmeister 中设置的,而是通过它们各自的 z 索引。实体的 z 索引实际上是它在实体数组中的位置。为了更好地理解这意味着什么,看一下游戏 DOM 的以下 Firebug 表示:
在数组末尾的实体将由游戏的draw()
方法最后绘制。最后绘制意味着你将被绘制在所有其他实体的顶部,因此看起来就好像在它们的前面。所有新生成的实体都会附加到列表的末尾。实体越年轻,放在其他实体上方时就会显得越靠近。这可以通过手动设置 z 索引并在player.js
文件的main.js
更新函数中使用游戏的sortEntitiesDeferred()
方法来避免:
zIndex:999,
按照以下方式更新main.js
中的update()
函数:
ig.game.sortEntitiesDeferred() ;
你的玩家可以移动,但是它是如何如此优雅地移动而不是只是从 A 点滑向 B 点呢?这一切都与精灵和动画表有关。
总结位置、加速度和速度过程,我们得出:
-
每个实体都有一个位置、速度和加速度。
-
尝试改变玩家的速度以改变他/她的位置。
-
尝试改变加速度以改变速度,从而改变玩家的位置。
-
每个实体都有一个 z 坐标,它表示实体是在其他实体的前面还是后面绘制。尝试将玩家的 z 坐标更改为一个非常大的数字。现在可玩角色将被绘制在关卡中所有其他实体的后面。
游戏的图形:精灵和动画表
精灵是一种绘画,放在透明背景上,然后以能保持背景透明的文件格式保存,比如.png
或.gif
格式。例如,JPEG 不能有透明部分。拥有一个角色的绘画,比如一个带有核爪的红鲸鱼,是不错的。然而,对于动画,你需要更多这样的绘画,最好是从不同的角度。然后把所有这些绘画放在一个文件中(同样,不是.JPEG
格式),它们组成一个动画表。
好的精灵和动画表并不是那么容易获得的,而且你在互联网上找到的往往是有许可证的,禁止用于游戏发布。你可以自己画,也可以在诸如www.sprites4games.com这样的网站上购买。
动画表通常放在media
文件夹中,尽管这并不是强制性的,完全取决于你如何组织它们。
通过调用AnimationSheet()
方法,将动画表分配给一个实体,如下所示:
animSheet: new ig.AnimationSheet('media/player.png',32,48),
第一个参数是你的动画表的位置和名称。永远不要忘记,位置总是相对于其根文件夹指定的,现在应该是myfirstawesomegame
文件夹。它存储在 XAMP 文件结构的htdocs
文件夹中并不重要。第二和第三个参数分别是每个动画的宽度和高度(以像素为单位)。
现在动画表与玩家关联起来了,所有可能的状态都需要与一定的图像序列关联起来。实体的addAnim()
方法允许你将可能的状态与一定的图像序列关联起来,如下例所示:
this.addAnim('idle',1,[0]);
this.addAnim('down',0.1,[0,1,2,3,2,1,0]);
this.addAnim('left',0.1,[4,5,6,7,6,5,4]);
this.addAnim('right',0.1,[8,9,10,11,10,9,8]);
this.addAnim('up',0.1,[12,13,14,15,14,13,12]);
在玩家初始化(init()
函数)时,定义了一些序列并赋予了一个名称。最简单的是idle
。玩家什么也不做,只需要一张图片,就是在动画表的位置 0([0]
)上。所有的 JavaScript 数组都从索引 0 开始,ImpactJS 的动画表数组也是如此。一个 128 x 192 像素的动画表可以容纳 16 张 32 x 48 像素的图片,编号从 0 到 15。编号从表的左上角开始,到右下角结束,就像你读这本书的页面一样(也许除非你是中国人)。
向左走只需要三张不同的图片:向左看、伸出右腿和伸出左腿。在动画过程中,向左看在切换腿之间重复出现,这给人一种行走的印象,如果速度设置正确的话。这里在切换图片之间的速度设置为0.1
秒,相当匆忙。
尝试将空闲动画的速度设置为100
秒,将行走动画的速度设置为0.5
秒,如下例所示:
this.addAnim('idle',100,[0]);
this.addAnim('down',0.5,[0,1,2,3,2,1,0]);
请注意,将空闲动画的速度设置为100
秒并没有影响它,因为实际上没有真正的动画,它只是一个图像。但是,将行走之间的时间增加五倍确实有很大的视觉影响。玩家现在看起来像是在漂浮,有点像鬼魂。
最后,您需要使用所需的动画更新实体属性currentAnim
。使用用户输入更改速度和方向时,更新此实体属性与所需的动画会改变动画序列。
你也可以尝试玩这个。例如,尝试在玩家向左走时将动画设置为右,反之亦然。将这与相当缓慢的动画结合起来,哦是的,你在后退!
else if(ig.input.state('left') || ig.input.pressed('tbLeft')){
this.vel.x = -100;
this.currentAnim = this.anims.right;
this.lastPressed = 'right';
}
总结使用精灵和动画表提升游戏图形的过程,我们可以得出结论:
-
每个可见的实体都有一个动画表。动画表是实体可以看起来的所有不同方式的组合。尝试更改玩家实体的动画表。
-
动画序列将告诉游戏在执行某个动作时应该跟随哪些图像。玩弄动画的序列和速度可以创造出有趣的效果。尝试仅使用
addAnim()
方法复制一个幽灵或后退的角色。
生成、生命和死亡
每个生物都有一个开始、生命和死亡。说你几年前从你母亲的子宫中产生出来有点残酷。但在游戏术语中,这就是你所做的。
理论上,单个游戏中生成的实体数量是没有限制的;实际上,这受性能问题的限制,特别是在移动设备上。
让我们来看看一个经常生成和销毁的实体:抛射物。
当玩家感到扳机指头发痒时,抛射物就会由玩家生成。在player.js
的更新函数中,您会找到以下代码:
ig.game.spawnEntity('EntityProjectile',this.pos.x,this.pos.y,{direction:this.lastPressed})
生成是通过ig.game.spawnEntity
方法完成的。这个方法真正需要的是实体类型和需要生成的位置。第四个参数是一组额外的设置,您可能想要添加,这是可选的,但现在用于告诉子弹发射的方向。
任何东西都可以生成一个实体。与玩家生成抛射物的方式相同,Levelchange实体将生成玩家。在levelchange.js
文件中,您会找到以下代码:
if(spawnpoint) {
ig.game.spawnEntity(EntityPlayer, spawnpoint.pos.x,spawnpoint.pos.y);
ig.game.player = ig.game.getEntitiesByType( EntityPlayer )[0]
}
这段代码的作用是检测玩家想要前往的关卡中是否存在生成点,如果存在,则杀死可能存在的玩家。在 Weltmeister 中,您可以将玩家实体添加到关卡中;这样,您可以单独测试它,而不必经历所有可能出现在它之前的其他实体。这个预设的玩家实体被杀死,并在适当的生成点位置被新的玩家实体替换。然后ig.game.player
变量被设置为找到的第一个预设([0]
)玩家实体。最后一部分不是必需的,但有时直接链接到玩家实体是很方便的。
在这种情况下,抛射物本身并没有指定的生命值,但可以使用以下代码将其杀死:
if(this.lifetime <=100){this.lifetime +=1;}else{this.kill();}
在这里,抛射物只能存在 100 帧。您还可以使用真实计时器控制实体的寿命,或者当它击中可以造成伤害的东西时将其销毁。将值从100
更改为1000
,以大幅增加抛射物的射程。或者,您可以在抛射物中添加一个名为range
的新属性,并用这个属性替换寿命检查。在init()
函数中添加range
属性,如下所示:
this.range = 100;
在检查函数中,将值100
替换为this.range
:
if(this.lifetime <=this.range){this.lifetime+=1;}else{this.kill();}
恭喜!您的代码再次变得更加易读和灵活。
使用以下代码片段,当抛射物击中敌人时,也可以将其销毁:
check: function(other){
if(other.name == 'enemy'){other.receiveDamage(100,this);}
this.kill();
this.parent();
}
通过调用kill()
方法来杀死一个实体很简单,但如果健康值达到 0,实体的receiveDamage()
方法也会调用kill()
方法。
那么在这个弹丸检查函数中会发生什么呢?如果弹丸与敌人发生碰撞,它将受到100
的伤害,由this
(弹丸)造成。如果发生这种情况,弹丸将在这个过程中被摧毁。
在 ImpactJS 中,生成和死亡都是简单的事情,健康更是如此。当你用一种方法生成或杀死一个实体时,健康只是一个你可以随意设置和改变的属性。在player.js
文件中,如果添加了以下代码,你会看到玩家的健康值为400
:
health: 400,
扣除健康是通过receiveDamage()
方法内置到 Impact 引擎中的;你可以用相同的方法增加健康。尝试将receiveDamage()
方法中的伤害设置为负数,你就发明了治疗弹丸!
if(other.name == 'enemy'){other.receiveDamage(-100,this);}
总结生成、健康和死亡的完整过程,我们可以得出结论:
-
每个 ImpactJS 实体都可以生成、失去、获得健康并被杀死。
-
尝试改变弹丸实体的生成位置,使其生成离玩家更近或更远。
-
弹丸会对其他实体造成伤害;尝试颠倒效果以创建治疗箭。
摄像机视图
当你探索的世界很小很舒适时,随时保持概览是很容易的。但在更大的关卡和较小的屏幕上情况就不一样了。如果你的目标是为手机发布游戏,你必须掌握摄像机。
你的摄像机只是你进入世界的窗口。当你的世界很大时,你需要定期调整你的窗口以跟踪事物。有几种类型的摄像机,但最重要的两种是自由移动摄像机和自动摄像机。
然而,在深入研究摄像机之前,最好先看看 Impact 游戏中画布元素的设置方式。
游戏画布
如果你打开main.js
和html.index
,你应该能找到所有你需要的画布代码,因为这是一个高级游戏组件。在 HTML 文档的 body 标签中,你会找到包含游戏电影屏幕的画布。画布元素有一个名为"canvas"
的 ID,这使得可以通过以下代码将其与 JavaScript 链接起来:
<canvas id="canvas"></canvas>
在main.js
文件中,你可以找到ig
对象的main
方法。这个方法通过查找其 ID 将整个游戏与画布链接起来。如果 JavaScript 需要查找 HTML 的 ID,它总是以#
符号开头,如下面的例子所示:
ig.main('#canvas', OpenScreen, 60, 640, 480, 1);
ig.main()
方法有 6 个参数。第一个是画布 ID,然后是游戏的名称,如前面在main.js
文件中指定的。第三个参数表示游戏需要以每秒帧数运行;然而,这个参数已经过时,可能会在将来的版本中被完全移除。现在,引擎本身决定了最佳帧率,因此手动设置已经不可能了。
最后三个参数是画布的宽度和高度以及你想要使用的缩放值。缩放是一种特殊的东西,因为它会按你设定的因子放大一切。
尺寸为 640 x 480,缩放值为 1 的画布实际上是 640 x 480 像素大,其中的每个字符都保持其原始尺寸。然而,如果将缩放值设为2
,尺寸将乘以 2,游戏中的所有内容也将乘以 2。例如,如果你只有 640 x 480 像素可用,但几乎看不到你的主角,可以将尺寸除以 2,并将缩放值设置为2
,如下面的代码示例所示:
ig.main('#canvas', OpenScreen, 60, 320, 240, 2);
尝试将缩放值设置为6
,会导致极度眼睛疼痛和模糊。
总结画布特性,我们可以得出结论:
-
游戏画布是你进入游戏世界的窗口。
-
这个窗口的几个元素可以改变;大小和缩放是最重要的。尝试同时改变它们,以便完美地适应你自己的屏幕分辨率。
自由移动摄像机
自由移动摄像机,顾名思义,可以由玩家自己自由移动。这些视口通常在 RTS 游戏中使用,因为许多事情都在玩家的指挥下。例如,在著名的游戏《红色警戒》中,你有数十辆坦克、飞机、士兵和疯狂的潜艇四处游荡。优秀的玩家将它们分散在地图的各个地方,同时攻击各种目标。这类游戏中的摄像机控制比我们将要探索的简单介绍更复杂,但你得从某个地方开始。在main.js
文件中找到自由移动摄像机的代码:
var gameviewport= ig.game.screen;
if(ig.input.state('camera_right')) {gameviewport.x = gameviewport.x + 2;}
else if(ig.input.state('camera_left')) {gameviewport.x = gameviewport.x - 2;}
else if(ig.input.state('camera_up')) {gameviewport.y = gameviewport.y - 2;}
else if(ig.input.state('camera_down')) {gameviewport.y = gameviewport.y + 2;}
屏幕对象代表你可以看到的游戏部分,即前面提到的视口。在这里,屏幕被分配给一个名为gameviewport
的局部变量,以便可以用按钮进行操作。例如,每当玩家按下camera_right
按钮时,窗口向右移动 2 像素。
总结摄像机移动过程,我们可以得出结论:
-
自由移动摄像机只有在手动告知时才会调整窗口
-
你可以尝试在游戏中移动摄像机
自动跟随摄像机
制作一个自动跟随摄像机可能听起来更加困难,但实际上并不需要。我们可以看到在以下代码中添加自动跟随摄像机的简单过程:
var gameviewport= ig.game.screen;
var gamecanvas= ig.system;
var player = this.getEntitiesByType( EntityPlayer )[0];
gameviewport.x = player.pos.x - gamecanvas.width /2;
gameviewport.y = player.pos.y - gamecanvas.height /2;
这里引入了一个额外的元素:画布本身。ig.system
对象确保游戏循环,并负责输入。ig.system
对象通常通过ig.main()
函数调用,我们在查看画布时看到了,因此它接受相同的参数。这里它被分配给一个局部变量gamecanvas
,我们需要它来获取我们正在处理的视口的实际尺寸。玩家实体也被分配给一个局部变量player
。正如你可能已经注意到的,第一个玩家实体被取出(数组的索引 0)。因此,如果有多个玩家实体,只会关注第一个。这使它成为一个自动跟随摄像机,对于有多个可玩实体的游戏来说并不合适。
游戏窗口会不断更新玩家的位置(包括 x 和 y 轴),地图宽度除以 2。最后这个减法是为了保持玩家牢固地居中。尝试去掉最后这部分,看看会发生什么:
gameviewport.x = player.pos.x;
gameviewport.y = player.pos.y;
视口将被更新以保持玩家在屏幕上,但玩家被放置在左上角。它将始终位于左上角,因为 x 轴的坐标是从左到右计数,y 轴的坐标是从上到下增加的。
总结创建自动跟随摄像机的过程,我们可以得出结论:
-
自动跟随摄像机试图保持玩家在屏幕中央。
-
你可以尝试改变代码,使玩家保持在屏幕的左上角。
添加音乐和音效
有好游戏,也有真正令人难忘的游戏。任何游戏都可以凭借出色的游戏性和一些体面的图形自持;你并不总是需要音乐。《Minecraft》就是这类游戏的一个很好的例子;你并不是为了它清新的音乐而玩它。但对于那些玩过《塞尔达传说:时光之笛》和任何《最终幻想》的人来说,你知道音乐是锦上添花的。必须提前说一下,音乐在移动设备上有时可能会出现问题。同时播放两个声音通常是不可能的。这是一个相当基本的问题,因为背景音乐和音效总是重叠的。由于它在移动设备上的难以控制的特性,为了可重现性,我们只会在桌面版本中进行讨论。
有两种主要类型的声音:真正的音乐和音效。真正的音乐由作曲的歌曲组成;对于现代(和昂贵)的游戏来说,这些通常是管弦乐曲。音效是您的敌人的呻吟声,剑的撞击声,您的脚步声和一阵风的声音。如果您想得到一些真正的音乐,您可以自己创作或购买。当您需要音效时,您只需要准备一个音频录音机和您需要的声音列表,并与您最好的朋友之一组织一个录音会话。
播放背景音乐
在main.js
文件中,您应该找到以下代码:
var play_music = true;
var music = ig.music;
music.add("media/music/backgroundMusic.ogg");
music.volume = 0.0;
music.play();
您在这里看到的第一个重要元素是ig.music
,这是(您可能已经猜到的)负责所有音乐的对象。音乐数组形成了您想要使用的所有音乐的列表,添加歌曲的方式与您在任何数组末尾添加东西的方式相同,即使用.add()
方法。该方法只需要一个参数:您想要与其位置相对于游戏根文件夹的音乐文件。您可以使用音量属性设置音量。音量可以从值0
到1
。当然,您可以将音量设置为1
,只要您愿意,如果您不激活音乐,就不会有声音。这是通过.play()
方法完成的。尝试将音乐音量设置为 1 并重新加载游戏。
玩家是否想听您的音乐实际上应该取决于他或她。假设他们在上课时玩您的游戏;您不希望他们被抓到吧;那将是邪恶的。出于这个目的,您将在main.js
文件中找到以下代码:
if (ig.input.pressed('music_down')){ig.music.volume -= 0.1;}
if (ig.input.pressed('music_louder')){ig.music.volume += 0.1;}
if (ig.input.pressed('music_off')){ig.music.stop();}
它基本上检查您之前定义的声音按钮是否被按下,如果是,音量会增加,减少或完全关闭。
总结添加音乐和音效的整个过程,我们可以得出结论:
-
音乐可以以
.mp3
或.ogg
格式添加到游戏中 -
music
类对于整个音乐曲目特别有用,因为它具有几个等同于标准收音机的功能 -
您可以尝试更改音量并打开或关闭音乐
介绍音效
音乐是一个连续的东西,不是真正依赖于游戏事件(除非您的玩家几乎快死了,也许会有一些更紧张的音乐)。另一方面,音效可以添加到几乎任何东西上。
打开player.js
文件,并在其init()
函数中找到以下代码:
this.walksound = new ig.Sound('media/music/snowwalk.ogg');
this.walksound_status = false;
this.walksound.volume = 1;
另一个新对象ig.sound
将能够处理您提供的任何声音,包括背景音乐。然而,最好将您的音乐属性分配给ig.music
对象,因为您可以使用额外的选项来处理音乐曲目。例如,使用ig.music
对象,您可以随机播放曲目(.random
)或添加淡出效果(.fadeOut
),如果尚未包含在您的 MP3 文件中。
行走声音被添加为玩家实体(this
)的新声音,并且其音量设置为1
。我们有一个要添加的脚步声,但当他实际上没有在走路时听到脚步声并没有太多意义:
if(this.vel.x == 0 && this.vel.y == 0){
this.walksound.stop();
this.walksound_status = false;
}
else if(this.walksound_status == false){
this.walksound.play();
this.walksound_status = true;
}
当玩家不四处闲逛时,一切都是安静的。如果他再次开始走路,脚步声就会恢复。还有许多其他添加音效的例子,但现在我们将在此结束。
总结添加音效的完整过程,我们可以得出结论:
-
音效是通常只在发生某种动作时播放的短声音
-
默认情况下,音效只会播放一次
-
您可以尝试激活雪地行走音效
使用 Box2D 进行游戏物理
为了结束探索性章节,我们将看看 ImpactJS 的物理引擎:Box2D。物理引擎是游戏引擎,能够模拟地球上许多可见的力,如重力和压力力(冲击)。当然,最著名的带有物理引擎的游戏之一是愤怒的小鸟。在这个 2D 世界出现之前,物理在许多游戏中都得到了应用(例如《半条命》和甚至比这个更早的游戏)。然而,愤怒的小鸟应该是一个例子,说明一个简单的游戏(加上一个可观的营销机器)可以获得巨大的成功。
该引擎不是 Dominic(ImpactJS 的制作者)的发明,而是从 Flash ActionScript 移植到 JavaScript。因此,Impact 网站上并没有提供有关所有 Box2D 功能的完整描述(就像 Impact 引擎一样),但可以在以下网站上找到:www.box2dflash.org/docs/2.0.2/manual.php
。
然而,关于结合 ImpactJS 和 Box2D 的文档在最好的情况下是零碎的。在构建具有物理特性的游戏和没有物理特性的游戏时,您需要完全不同的思维方式,这也是为什么源代码与标准包是分开的原因。正如在第一章中提到的,启动您的第一个 Impact 游戏,您可以从购买 ImpactJS 时的可下载文件physics
中获取 Box2D 源代码。文件夹称为Box2D
应放置在plugins
文件夹下以继续进行。
在深入研究 Box2D 代码之前,加载一个游戏并按下Shift + F9键组合。您现在神奇地被传送到 Box2D 的奇异世界,在那里物体可以飞翔,重力使一切都回到原位。尝试推动硬币并看看它们如何对来自不同方向的有力头槌做出反应。
重力和力
如果您打开main.js
文件,您将遇到一个新的游戏定义。这次不是标准ig.game
函数的扩展,而是ig.Box2DGame
。是的,可以在单个文件中定义不同的游戏,通常使用此技术制作游戏结束屏幕、闪屏等,使用以下代码:
BouncyGame = ig.Box2DGame.extend({
gravity:3,
从一开始,我们可以将世界的重力定义为BouncyGame
变量的属性。随意更改它,并观察重力在游戏中产生的影响。重力也不一定需要是正向力。尝试将其设置为负数,如-100
,您将看到一切都被吸向天花板。
重力越大,您需要克服它的力就越大。使用重力值300
(或-300
),您的移动将受到左右的限制。
这可以在玩家实体本身中进行更改。打开boxPlayer.js
文件,找到玩家实体的特殊实例。特殊之处在于它不是普通玩家实体的扩展,而是另一个称为Box2DEntity
的实体,如下面的代码示例所示:
.requires(
'plugins.box2d.entity'
)
.defines(function(){
EntityBoxPlayer = ig.Box2DEntity.extend({
还要注意,我们需要包含 Box2D 实体。
正常的 Impact 引擎使用速度,而 Box2D 使用向量。正如您可能从物理学和数学中记得的那样,向量是具有方向和大小的线;让我们看看它是如何实现的:
if(ig.input.state('up')){
this.body.ApplyForce( new b2.Vec2(0,-200),this.body.GetPosition() );
}
例如,为了向上移动,您在身体的位置上施加力。如本例所示,您输出的力的大小为200
。我们将重力值更改为300
,因此我们没有足够的力量来克服 200 的力。尝试将其值设置为500
,您将能够逐渐克服重力。将其值设置为1000
,即使您仍然像砖块一样掉下来,通过按下上键来克服重力变得轻而易举。
总结重力和力的概念,我们可以得出结论:
-
Box2D 是一个物理引擎,不是 ImpactJS 的正式部分,但与之相当集成。
-
Box2D 是基于向量的。所有运动都以力和方向的组合进行转换。重力只是一个特例,始终具有垂直方向。
-
尝试改变游戏的重力,使物体向上浮动。
-
更改按下上按钮时施加在玩家身上的力。
碰撞影响和弹性
当撞击另一个物体(如硬币)时,它可能会被撞击力移动。您可能已经尝试过这样做。玩家施加的力被应用于硬币,它飞起来了。最终,硬币又被重力带到了静止,但您当然可以再次撞击它。
硬币也具有一定的弹性,在 Box2D 中被称为恢复。恢复的值可以在0
到1
的范围内设置。由于力随时间减小,物体永远不会以与其撞击墙壁时相同的速度弹回。您可以在boxcoin.js
文件中自行设置硬币的弹性如下:
This.restitution = 1;
尝试将恢复值设置为0
,看看硬币是否仍然会从墙壁上弹开。
这是对 Box2D 的一个非常简短的介绍。在下一章中,我们将从头开始构建一个小型 RPG。
总结碰撞影响和弹性的概念,我们可以得出结论:
-
在 Box2D 环境中两个物体之间的碰撞将导致每个物体对另一个物体施加一定的力
-
当撞击固体物体时,物体可以具有一定的弹性;这被称为恢复或弹性
-
您可以尝试更改硬币实体的恢复值,并观察弹性的细微差异
总结
本章的目的是通过探索一个预制示例,快速了解 Impact 游戏的每个重要组件。我们首先使用 Weltmeister 工具打开了一个现有的关卡,并深入了解了它是如何由图层和实体构建起来的。我们看了一个可玩角色以及它与不可玩角色的区别。通过调整一些实体参数,我们可以改变诸如生命值、移动速度甚至实体外观等内容。由于在大多数游戏中,您无法在单个屏幕上看到整个游戏场景,我们看了一下手动和自动跟随相机。我们添加了背景音乐和音效作为游戏氛围的一部分。
最后,我们简要地了解了 Box2D 物理引擎。虽然在本章中我们只是在调整参数,但在下一章中我们将从头开始构建一个游戏。
第三章:让我们建立一个角色扮演游戏
在上一章中,我们看了几个关键概念,并逐一放大它们,基本上忽略了它们的基本依赖关系。现在我们将逐步构建一个游戏。在本章中,我们将看看 RPG,而在第四章中,我们将深入研究侧面滚动游戏。
在本章中,我们将涵盖:
-
RPG 游戏格式及其可能的子格式
-
为玩家建立一个实际的探索级别,并将其与其他级别连接起来
-
向游戏中添加可玩实体、可杀但危险的敌人和中立的健谈角色
-
将您的玩家变成一个不可忽视的力量,通过添加武器和有用的物品
-
通过赋予敌人基本的人工智能,为玩家的敌人增加一些深度
-
跟踪游戏中的一些变化,比如收集硬币
-
通过让玩家与更强大的敌人对抗来结束游戏
RPG 游戏设置
在深入研究 RPG 游戏设置之前,最好看一看一些成功的 RPG,并看看我们可以从中学到什么。有一些很好的例子:《塞尔达传说》,《最终幻想》,《宝可梦》,《魔兽世界》,《泰比亚》,《博德之门》,《无冬之夜》等等。这个列表几乎是无穷无尽的。是什么让所有这些游戏如此成功呢?嗯,总是有营销因素,但没有游戏可以仅凭营销而获得永恒的名声。那么,他们对目标受众的独特游戏主张是什么?游戏评论员经常将他们的分数分配到几个类别,如游戏性、图形、声音等。这些都是有效的观点,但为什么不看看上瘾性呢?即使是最简单的游戏也可能会上瘾。如果你曾经去过拉斯维加斯,目睹了成堆的人在不需要任何技能的老虎机上玩了几个小时,你就会明白游戏心理学有些特别。
上瘾当然是很棒的,如果你提供一个免费游戏,并希望通过游戏内广告或重复订阅费用赚钱。另一种方法是使游戏引人入胜,但是有终点。这些是你实际上可以“完成”的游戏。它们通常在游戏的主角和反派之间有一个有趣的故事。当反派被打败时,游戏就结束了,作为玩家,你不太可能再去玩它。一个例子就是《最终幻想》系列中的每个游戏。
除了大型多人在线角色扮演游戏之外,大多数 RPG 都属于第二类。它们通常有一个迷人的故事和迷人的音乐。角色非常有趣和深刻。战斗系统非常直观,但足够复杂,以至于有人可以擅长或不擅长。真正优秀的游戏往往会给玩过的人留下深刻的印象,而且需要付出大量的工作才能完成。
这是看待 RPG 的标准方式。然而,这绝不应该阻止你刷新这个类型,并将其他类型的元素或全新的想法融入其中。例如,《无主之地》是 RPG 和射击游戏的混合体。它具有像大多数 RPG 那样的等级进展和武器增强。它有一个故事,同时仍然像射击游戏一样玩。
游戏不需要混合两种电脑类型。我的世界本质上是在电脑上玩乐高的乐趣。
归根结底,就是要找出自己最喜欢的或小时候非常喜欢的东西。找出其中的机制,并尝试在游戏中复制那种感觉。这当然比说起来容易得多。然而,有必要经历这个过程,因为建立游戏需要花费你的时间,如果它甚至不是你自己想玩的游戏,为什么别人会想要呢?
对于 RPG 游戏来说,通常会比简单找到原创的游戏组件更加复杂。RPG 视频游戏可以是一本好书和一部电影的结合,同时具有互动性的优点。如果你是一个很好的故事讲述者,或者认识一个,为什么不这样做呢?游戏并不需要难以或者图形完美才能吸引人们玩。一个很好的例子是《最终幻想 VII》,它在 1990 年代大获成功。2012 年,它以“优化图形”的形式重新发行。实际上并没有太大的区别;一个不经训练的眼睛不会立即注意到这种优化。但它仍然是一款很棒的游戏,尽管它无法与《上古卷轴》或《寓言》等游戏的复杂性和图形辉煌相竞争。
这就是你应该追求的目标:以尽可能少的复杂性打包你想要的核心乐趣,并添加快乐、柔和的图形。快乐的图形很棒。不,说真的,如果你想让你的游戏散发出黑暗和恐惧,那也可以,但除此之外,一定要考虑一些微笑的云和疯狂的动物。
构建一个 RPG 关卡
现在是时候开始组建我们自己的小型 RPG 了。我们将从零开始我们的旅程。以下是构建 RPG 关卡的步骤:
-
让我们复制我们在
第一章
文件夹中保留的新安装的ImpactJS
文件夹,并将其重命名为RPG
。将第三章
文件夹中的media
文件夹复制到你的RPG/media
文件夹中。这样至少我们有一些图形可以使用。 -
回到当你在浏览器中输入
localhost/RPG
时得到的**它起作用了!**屏幕。 -
让我们从打开 Weltmeister(
localhost/Weltmeister.html
)并绘制一个小关卡开始。 -
这一次你会注意到没有为你准备好任何东西。唯一可用的层是
entities
层,甚至连一个实体都没有。然而,一旦我们有了一些东西来填充世界,我们就可以画一个小的游戏场地来开始。 -
所以让我们添加另一个层(*±*符号)并将其命名为
grass
。让我们将瓷砖大小设置为16
,并在距离 1 像素处有一个 30 x 20 的区域。选择瓷砖集grass.png
,然后点击应用更改按钮,然后你就可以开始铺设草坪了。 -
如果你的绘图框没有完全居中,按住Ctrl键并移动鼠标直到它居中。如果由于某种原因它太大而无法适应你的屏幕,可以用鼠标滚轮缩小。
-
一旦我们把整个层都涂成绿色,我们就可以轻松地添加另一个层放在草坪上。但在这样做之前,将你的文件保存为
level1
。经常保存是一种美德。 -
在添加层时,你可以根据它们应该代表的内容进行命名和使用。例如,你可以有一个家具、植物和杂项物品的层。这是一个不错的工作方式,但你必须记住,一些层在视觉上会出现在你的玩家和怪物实体的前面,而其他层则会出现在它们的后面。即使是一个简单的墙最好也用两层来绘制。
提示
Weltmeister 不支持无限数量的层。为了保持层的数量可观,你可以为特定的关卡设置瓷砖集。例如,你有两个关卡设置:城市和地牢。两者都可以包含一把椅子,所以不要害怕在城市的瓷砖集上放置相同的椅子,也在不同的瓷砖集上构建你的地牢。重复信息会增加你的整体游戏大小,但可以减少单个关卡所需的层数。
我们的草地只叫grass
,因为我们不会有草地漂浮在玩家面前;因此我们不需要第二层草地。让我们创建两个新图层,分别叫做vegetation_back
和vegetation_front
。vegetation_back
必须在图层选择菜单中的entities
图层下面。vegetation_front
必须放在entities
图层上面。这两个新图层一起将构成地图上的所有植被。
选择图块集tree.png
,并为grass
图层使用相同的设置。
使用vegetation_front
图层绘制树的上部,使用vegetation_back
绘制下部。以下屏幕截图显示了不同的图层:
你的 Weltmeister Layers菜单中应该有以下图层:
如果你不知道任何东西的上部或下部应该是什么,想想你的玩家和/或敌人有多大。当走过树时,他们的头或脚不应该消失。为了避免玩家完全穿过树,我们需要另一个图层,碰撞图层。
在 Weltmeister 中添加一个名为collision
的图层。
不要忘记,你可以通过将图层拖到图层堆栈的顶部或关闭挡住视野的图层来在 Weltmeister 中使图层可见。在这种情况下,如果碰撞图层在堆栈的底部,grass
图层可能会挡住所有的视野。将collision
图层拖到顶部并在必要时打开和关闭它是非常有效的。设置图层的设置与以往一样。
使用collision
图层,在关卡周围绘制边界,这样就没有人可以逃跑了。还在树干下面或上面的分界线附近放一些碰撞方块,如下图所示:
所以我们创建了一个可行的环境。虽然不多,但这是一个开始。然而,为了加载关卡,我们需要对我们的main.js
脚本进行更改,如下面的代码片段所示:
.requires(
'impact.game',
'impact.font',
'game.levels.level1'
)
init: function() {
// Initialize your game here; bind keys etc.
this.loadLevel(LevelLevel1);
},
为了确保我们的游戏能找到关卡,我们需要在模块的.requires
部分包含它。我们需要以与任何文件相同的方式指向它,从我们的游戏根文件夹开始。唯一的区别是斜杠(/
)被点(.
)替换,而包含的文件本身总是被认为有.js
扩展名。例如,/game/levels/level1.js
变成了game.levels.level1
。
我们还需要在游戏启动时加载关卡,所以让我们在init()
函数中添加一个loadlevel()
方法。不要忘记,调用这个函数的参数总是以下形式:
大写字母的Level + Levelname
。其他任何形式都会导致游戏崩溃。
我们现在有一个加载的关卡,但它没有任何交互性;我们还没有玩家。尽管在屏幕上始终显示**it works!**相当激励,但也会轻微地阻碍我们的视野。所以让我们从main.js
中删除以下代码,然后继续使用以下代码来创建我们的player
实体:
var x = ig.system.width/2,
var y = ig.system.height/2;
this.font.draw( 'It Works!', x, y, ig.Font.ALIGN.CENTER );
总结前面的内容,步骤如下:
-
我们需要从头开始构建我们的游戏。因此,我们需要最初下载的 ImpactJS 文件。将它们放在服务器工作目录的一个单独文件夹中。还要测试一下是否收到了**it works!**消息。
-
将
chapter 3
文件夹的media
文件添加到你刚刚设置的文件夹中。 -
打开 Weltmeister 关卡编辑器并创建一个分层关卡。你需要一个碰撞图层,一个实体图层和三个图形图层。底部的图形图层将代表草地。其他两个图层代表所有其他在玩家前面或后面出现的对象。
-
绘制图形图层。
-
在你的
main
脚本中包含关卡文件。 -
从
main
脚本中删除it works!
消息。
添加可玩角色
为了从头开始构建我们的玩家,我们需要一个新的(并且是空的).js
文件。在你的代码编辑器中创建一个新文件,即使它是空的,也将其保存为entities
文件夹中的player.js
。
每个模块都是以相同的方式开始的。它由ig.module()
,ig.requires()
和ig.defines()
方法组成。对于一些模块,你不需要requires()
方法,但所有实体都需要,因为在这里你需要包含实体的impact
脚本,如下面的代码片段所示:
ig.module('game.entities.player')
.requires(
'impact.entity')
.defines( function(){
EntityPlayer = ig.Entity.extend({
});
});
我们将根据prototype
实体构建玩家。这个原型有几个属性(比如health
和velocity
)和几个方法(比如kill()
和receiveDamage()
)预定义。这样我们只需要用extend()
方法扩展原始版本,就可以创建我们的玩家了。
这里有一些规则。如果你的 JavaScript 文件叫做player.js
,你的实体将被称为Player
。你可以通过在其名称前面添加Entity
,将其分配给entity
原型的扩展,如前面的代码所示。
提示
任何与命名约定的偏离都将从 Weltmeister Entities菜单中移除实体。将实体添加到 Weltmeister 编辑器时,如果命名正确,加载游戏时命名错误将导致崩溃。
还不要忘记在main.js
中的requires()
方法中包含player
实体。只有当main
模块知道其存在时,模块才能被使用。以下代码显示了扩展.player
被分配给entities
文件夹:
'game.entities.player'
如果你现在用 Weltmeister 添加player
实体到游戏中,你会注意到什么也看不到。玩家还没有视觉表示,我们将在下面的代码中解决这个问题:
EntityPlayer = ig.Entity.extend({
size: {x:32,y:48},
health: 200,
animSheet: new ig.AnimationSheet('media/player.png', 32, 48 ), init: function( x, y, settings ) {
this.parent( x, y, settings );
// Add the animations
this.addAnim( 'idle', 1, [0] );
}
});
为了看到我们可玩角色的一瞥,我们需要添加一个动画表,它位于我们的media
文件夹中。如果你不想看到你的角色只是走来走去的话,动画表需要被分配正确的尺寸。我们还给实体分配了一个大小。动画实际上可以比实体的大小大。如果你不设置大小,你会发现你可以在 Weltmeister 中选择player
实体,但它的边界并不包含整个图像。这是因为默认大小是 16 x 16。大小是碰撞检测的相关属性。我们还给玩家一些生命值来开始。默认生命值是 10。
我们还面临着entity
原型的init()
方法。entity
原型已经有了自己的init()
函数,所以最好通过在init()
函数中调用parent()
方法来包含它。定义动画表并不会使实体动画化。你需要为动画表分配一个动作。在这里,空闲对应于表上的第一张图片。现在你可以安全地将你的玩家添加到地图上了。
太好了,我们的游戏中有了一个玩家!太糟糕了,它还不能移动。让我们现在来解决这个问题。
在main.js
脚本中,你需要在你的init()
方法中添加以下内容:
// move your character
ig.input.bind(ig.KEY.UP_ARROW, 'up');
ig.input.bind(ig.KEY.DOWN_ARROW,'down');
ig.input.bind(ig.KEY.LEFT_ARROW,'left');
ig.input.bind(ig.KEY.RIGHT_ARROW,'right');
这将确保你的箭头键绑定到一个输入状态。从现在开始,游戏将自动检查这些键中是否有任何一个被按下。由于我们在这里正在构建一个俯视游戏,我们需要能够朝任何方向行走。
在player.js
脚本中,需要在init()
函数中添加四个新的动画序列,如下面的代码片段所示:
this.addAnim('down',0.1,[0,1,2,3,2,1,0]);
this.addAnim('left',0.1,[4,5,6,7,6,5,4]);
this.addAnim('right',0.1,[8,9,10,11,10,9,8]);
this.addAnim('up',0.1,[12,13,14,15,14,13,12]);
虽然idle
的动画序列由一张图片组成,但现在我们需要为玩家可以行走的每个方向分配一个真正的序列。同样,0.1
值是图像之间的时间。
此外,你需要调用和扩展entity
原型的update()
函数。不要忘记在init()
和update()
函数之间加上逗号,否则会出错。
update: function(){
this.parent();
//player movement
if(ig.input.state('up')){
this.vel.y = -100;
this.currentAnim = this.anims.up;
}
else if(ig.input.pressed('down')) {
this.vel.y = 100;
this.currentAnim = this.anims.down;
}
else if(ig.input.state('left')){
this.vel.x = -100;
this.currentAnim = this.anims.left;
}
else if(ig.input.state('right')){
this.vel.x = 100;
this.currentAnim = this.anims.right;
}
else{
this.vel.y = 0;
this.vel.x = 0;
this.currentAnim = this.anims.idle;
}
}
update()
函数和init()
一样,是原型entity
的标准方法。因此,如果我们不想失去其 ImpactJS 实体核心功能,我们需要调用父函数。
对于每个输入状态,我们需要单独的行为,因此我们有这组if-then 操作符。请记住,由于我们将这段代码放在update()
函数中,它会在游戏每次更新循环时运行,即每帧一次。init()
函数只会被调用一次,也就是在玩家创建的时候。
在条件检查中,我们做了两件事:在相关轴上分配速度并添加动画。如果玩家什么也不做,那么两个方向上的速度也被设置为0
,因此玩家需要持续输入才能移动。
我们可以使用ig.input.pressed
而不是ig.input.state
。但这将导致我们的玩家不得不通过按钮来穿过关卡。因为每次他或她按下移动按钮时,玩家只会移动一小段距离然后立即停下。在 60 fps 和速度为 100 的情况下,玩家每次触摸只会移动 100/60 = 1.67 像素。尽管ig.input.pressed
当然有其优点,但以这种方式移动可能会让即使是最有耐心的玩家也感到恼火。
我们终于有了一个优雅移动的可玩角色!它甚至可以躲在我们之前创建的树后面。不过我们手头还有另一个问题,我们不能始终看到我们的玩家。你能想象一个玩家因为看不到自己的位置而被杀死的沮丧吗?我相信你可以,而且如果你过去玩过一些游戏,这种情况可能甚至发生过。不过,我们很幸运,因为一个跟随玩家四处走动的摄像头很容易实现,如下面的代码片段所示:
var gameviewport= ig.game.screen;
var gamecanvas= ig.system;
var player = this.getEntitiesByType( EntityPlayer )[0];
gameviewport.x = player.pos.x - gamecanvas.width /2;
gameviewport.y = player.pos.y - gamecanvas.height /2;
正如您在前面的代码中所看到的,两个重要的元素和玩家被分配给了一个本地变量。然后,视口坐标被设置为玩家的位置。如果您希望相机将玩家放在屏幕的左上角,您就不需要游戏画布。但当然,我们希望玩家居中,所以我们通过画布尺寸的一半来调整其位置。
重新加载浏览器后,您会注意到您终于可以走到屏幕底部和树下面。太好了!只是可惜这里没有什么可做的,所以下一步我们将引入一些敌对的东西。
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,并将其保存为
player.js
。 -
使用标准的 ImpactJS 模块代码设置
player.js
脚本。 -
在
main
脚本中包含player.js
。 -
为可玩角色添加动画表和序列,以便在 Weltmeister 中找到它。还为其提供健康和大小。
-
通过将键盘键绑定到
main
脚本中的输入状态来添加玩家控制。 -
将这些输入状态绑定到移动角色动作,通过操纵其速度。
-
通过引入额外的动画序列并在某些输入状态激活时调用它们,使移动看起来像是平滑的动画。
-
放置一个自动跟随玩家四处走动的摄像头。
引入一个可击败的对手
同样,我们将不得不从头开始,因此打开一个空的 JavaScript 文件,并将其保存为enemy.js
。
实体创建的开始总是相同的。设置您的Entity
文件并将enemy
实体添加到您的main
脚本中。
在main.js.requires
中添加以下代码:
'game.entities.enemy',
在enemy.js
中添加以下代码:
ig.module('game.entities.enemy')
.requires('impact.entity')
.defines(function(){
EntityEnemy = ig.Entity.extend({
});
});
添加前面的代码片段创建了我们的实体,我们可以通过 Weltmeister 将其添加到关卡中。不过它还是相当无用的,所以让我们首先使用以下代码添加一些图形:
size: {x:32,y:48},
animSheet: new ig.AnimationSheet('media/enemy.png',32,48),
init: function(x, y , settings){
this.addAnim('idle',1,[0]);
this.addAnim('down',0.1,[0,1,2,3,2,1,0]);
this.addAnim('left',0.1,[4,5,6,7,6,5,4]);
this.addAnim('right',0.1,[8,9,10,11,10,9,8]);
this.addAnim('up',0.1,[12,13,14,15,14,13,12]);
this.parent(x,y,settings);
}
现在我们可以将我们的第一个敌人添加到关卡中。不过它不会做太多事情,甚至你甚至可以穿过他走。这是因为实体之间还没有指定碰撞。
将以下代码添加到player
和enemy
实体作为属性。您可以使用旧的 JavaScript 表示法在init()
函数中添加它们,或者在文字表示法中在init()
上方添加,如下面的代码所示。
以下代码是用于玩家的:
collides: ig.Entity.COLLIDES.ACTIVE,
type: ig.Entity.TYPE.A,
checkAgainst: ig.Entity.TYPE.B,
以下代码是用于敌人entity
的:
collides: ig.Entity.COLLIDES.PASSIVE,
type: ig.Entity.TYPE.B,
checkAgainst: ig.Entity.TYPE.A,
现在我们可以像真正的恶霸一样推动我们的敌人在关卡中四处走动。您可能已经注意到玩家和敌人之间仍然有一些空间。这是因为实体的边界是矩形,远远超出了实际的绘图范围。当视觉上并非如此时,玩家被敌人击中是非常恼人的。为了纠正这种情况,我们需要将offset
引入为玩家属性。size
属性确定了实体周围的碰撞框的大小。offset
属性使您的碰撞框向右或向下移动几个像素。当然,您可以在一个点输入一个负数,它将向左和/或向上移动。我们需要结合这两个属性来为玩家制作一个新的碰撞框,使他更难被击中。但是,在继续之前,通过在main.js
脚本的requires()
方法中添加以下代码行来打开 ImpactJS 调试器是有用的:
'impact.debug.debug',
在开发过程中保持调试器打开是一个好习惯。当准备发布时,您可以再次删除此代码。让我们使用以下代码更改玩家和敌人的大小和偏移:
size: {x:18,y:40},
offset: {x: 7, y: 4},
实际图像大小为 32 x 48。我们将两个实体的大小都改为18
x 40
,偏移为7
x 4
。如果您在Entities选项卡上打开调试器并打开显示碰撞框,您会注意到大小的差异。您还可能注意到静态碰撞,例如我们添加到树中间的碰撞层的正方形不可见,因为它只显示实体的碰撞,如下面的截图所示:
没有设置碰撞框的完美规则。这完全取决于您的图像有多好地居中和对称,当涉及到碰撞时您有多宽容,以及前视和侧视之间的图像大小差异。在这里,我们选择将我们的宽度减小 14 像素(32-18)。为了保持框居中,偏移设置为差值的一半((32-18)/2 = 7)。相同的推理适用于 y 轴。
现在我们有了一个敌人。让我们杀了它!
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件并将其保存为
enemy.js
。 -
使用标准的 ImpactJS 模块代码设置
enemy.js
脚本。 -
在您的
main
脚本中包含enemy.js
。 -
添加一个动画表和几个动画序列,考虑到敌人可能行走的每个方向。
-
更改玩家和敌人的
碰撞
实体。它们需要能够检测到彼此的存在,以便敌人以后可以伤害玩家。 -
如果您还没有这样做,请通过在您的
main
脚本中包含它来打开 ImpactJS 调试器。目的是看到实体的碰撞框。
给玩家一些武器
我们喜欢我们的玩家武装起来,准备行动。让我们首先添加一个新的按键用于攻击。在main.js
中添加以下键绑定:
ig.input.bind(ig.KEY.MOUSE1,'attack');
在任何战斗情况下,造成伤害的是两个物体的碰撞。如果箭射中目标,造成伤害的是箭,而不是弓。同样的道理适用于核导弹。造成伤害的不是发射设施,而是核弹的爆炸冲击波与任何阻挡在其路径上的物体的碰撞。在这方面,我们可以说这里有三个实体在起作用:发射设施、核弹和其爆炸冲击波。如果你想区分空气压力和实际的大火,甚至可以再添加一个实体。所有这些只是为了展示在向游戏中添加武器时应该如何思考。哪种影响是相关的?在鸡和鸡发射器的情况下,鸡将成为一个实体,而发射器只是一个简单的绘图。
生成一个 projectile
对于我们的远程攻击,我们需要一个新的实体,我们将其称为projectile
。创建一个新的脚本,设置基础,将其保存为projectile.js
,并在main.js
中包含它。
在main.js
中包含以下代码:
'game.entities.projectile',
在projectile.js
中包含以下代码:
ig.module('game.entities.projectile')
.requires('impact.entity')
.defines( function(){
EntityProjectile = ig.Entity.extend({
size: {x:8,y:4},
vel: {x:100,y:0},
animSheetX: new ig.AnimationSheet('media/projectile_x.png',8,4),
animSheetY: new ig.AnimationSheet('media/projectile_y.png',4,8),
init: function(x, y , settings){
this.parent(x,y,settings);
this.anims.xaxis = new ig.Animation(this.animSheetX,1,[0]);
this.anims.yaxis = new ig.Animation(this.animSheetY,1,[0]);
this.currentAnim = this.anims.xaxis;
}
})
});
好吧,基础看起来似乎并不那么基础。这一次,我们有两个不同的动画表。箭往往比宽度长得多。因此,如果箭从左到右(或从右到左)射出,其尺寸与从上到下射出的箭不同。在定义动画表时,我们只需要一次定义每个图像的尺寸。然而,在这种情况下,我们需要两种不同的尺寸:8
x 4
和4
x 8
。实际上,在这种特殊情况下,还有另一种可能更简单的解决方案,涉及动画的角度。在编程语言中,通常有不同的方法来获得相同或类似的结果。然而,现在我们将使用多个动画表。
我们定义了两种不同的动画表。我们将它们命名为animSheetX
和animSheetY
,而不是在标准的animSheet
属性上初始化它们,以表示不同的轴。init()
函数不像Player
和Enemy
实体那样调用addAnim()
方法,因为它是按默认设置为animSheet
属性。相反,我们直接调用ig.animation
,可以传递我们自己的动画表。如果您想在 Weltmeister 中添加一个箭头,那么currentAnim
属性将默认给出 x 轴动画序列。
现在我们只需要让玩家生成箭。因此,我们需要在玩家的update()
函数中添加以下内容:
if(ig.input.pressed('attack')) {
ig.game.spawnEntity('EntityProjectile',this.pos.x,this.pos.y);
}
箭将在玩家的位置生成。
在这一点上运行游戏时,箭只能朝一个方向飞行:向右。这是因为我们的默认速度设置为每秒100
像素向右。而且我们的默认动画是箭头向右。
这并不完全是我们想要的。我们的敌人必须始终在我们的右侧,我们才能杀死他们。因此,让我们通过在init()
函数中添加以下代码来修改 projectile 代码:
if (this.direction == 'right'){
this.vel.x = this.velocity;
this.vel.y = 0;
this.currentAnim = this.anims.xaxis;
this.anims.xaxis.flip.x = false;
}
else if (this.direction == 'left'){
this.vel.x = -this.velocity;
this.vel.y = 0;
this.currentAnim = this.anims.xaxis;
this.anims.xaxis.flip.x = true;
}
else if (this.direction == 'up'){
this.vel.x = 0;
this.vel.y = -this.velocity;
this.currentAnim = this.anims.yaxis;
this.anims.yaxis.flip.y = false;
}
else if (this.direction == 'down'){
this.vel.x = 0;
this.vel.y = this.velocity;
this.currentAnim = this.anims.yaxis;
this.anims.yaxis.flip.y = true;
}
按照以下代码显示velocity
作为一个属性:
velocity: 100,
现在发生的是,如果箭头的方向是右、左、上或下,它将相应地调整其速度和动画。这里只有两个图像在起作用,一个箭头指向上方,一个指向右边,每个都在其单独的动画表中。我们可以向每个表中添加一个额外的图像,一个指向下的箭头,一个指向左边。这将是一个可行的解决方案,但在这里我们选择使用翻转属性。翻转基本上是制作动画的镜像图像,使箭头指向完全相反的方向。在使用翻转时,您必须确保翻转图像而不是使用单独的图像是有意义的。例如,如果您有一个从左到右奔跑的角色,并且希望使其从右到左奔跑,使用翻转是可以接受的。对于朝向您或远离您奔跑的角色,这并不起作用,因为您期望看到他们的正面或背面。
这一切都很好,但它的方向从哪里得到呢?让我们用默认值初始化方向,然后修改玩家,使其可以将自己的方向传递给抛射物。
将以下代码添加到projectile.js
:
direction: 'right',
对player.js
执行以下操作:
对于每个方向,添加一个名为lastpressed
的变量,其值与输入状态相同,如下面的代码片段所示,用于向右移动:
else if(ig.input.state('right')){
this.vel.x = 100;
this.currentAnim = this.anims.right;
this.lastpressed = 'right';
}
使用以下代码使spawnEntity
方法传递方向参数:
if(ig.input.pressed('attack')) {
ig.game.spawnEntity('EntityProjectile',this.pos.x,this.pos.y,{direction:this.lastpressed});
}
太棒了!我们现在的英雄可以像老板一样朝各个方向射箭。目前,我们的箭头仍然相当坚固,对我们幸运的敌人来说相当无害。它们只是击中我们关卡的边缘,永远停留在那里,或者直到游戏重新加载。
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,并将其保存为
projectile.js
。 -
设置
projectile.js
脚本。给它两个动画表。 -
将
projectile
脚本添加到main
脚本中。 -
更改玩家的
update
函数,以便玩家在激活attack
输入状态时可以生成一个抛射物。 -
根据玩家射击时面对的方向,调整抛射物的方向和动画。
-
确保在生成时将玩家的方向传递给
projectile
脚本。这是通过填写标准 ImpactJS 实体的可选参数:spawn
函数来完成的。
用抛射物造成伤害
我们可以使用以下代码使箭头在击中敌人或在空中一段时间后消失:
lifetime: 0,
update:function(){
if(this.lifetime<=100){this.lifetime +=1;}else{this.kill();}
this.parent();
}
在0
处初始化一个名为lifetime
的新属性,并在update()
函数中使用kill()
函数添加一个计数器,将使箭头在飞行了100
帧后消失。再次,不要忘记用逗号(,
)分隔init()
和update()
函数,否则文字表达式不会原谅您。
为了对敌人造成伤害,我们需要让箭头检查它是否遇到了敌人。我们将箭头设置为TYPE A
实体,就像player
实体一样,并让它检查TYPE B
实体,就像以下代码中的enemies
实体一样:
collides: ig.Entity.COLLIDES.NONE,
type: ig.Entity.TYPE.A,
checkAgainst: ig.Entity.TYPE.B,
通过添加check()
函数,我们可以使箭头检查它需要检查的每个实体(由checkAgainst
属性设置)。如果遇到类型为B
的实体,该实体将受到100
的伤害,如下面的代码片段所示:
check: function(other){
other.receiveDamage(100,this);
this.kill();
this.parent();
}
现在我们仍然没有解决箭头在关卡边缘或任何其他地图碰撞存在的地方露营的问题。所以让我们制作一些反弹的箭头!别担心,我们确保它们不会伤害玩家,因为它们只会检查类型为B
的实体,并且会直接穿过我们的玩家。
首先将bounciness
设置为1
,这意味着在反弹时保持所有速度,使用以下代码:
bounciness: 1,
现在我们只需要检查速度是否已经反转(如果箭已经反弹),并在必要时反转动画。当然,这需要在update()
函数中完成,如下面的代码片段所示,因为它可能随时发生:
if (this.vel.x< 0 &&this.direction == 'right'){this.anims.xaxis.flip.x = true;}
else if (this.vel.x> 0 &&this.direction == 'left'){this.anims.xaxis.flip.x = false;}
else if (this.vel.y> 0 &&this.direction == 'up'){this.anims.yaxis.flip.y = true;}
else if (this.vel.y< 0 &&this.direction == 'down'){this.anims.yaxis.flip.y = false;}
这是一个非常天真的检查,因为它依赖于箭的速度在反弹后仍然保持不变的假设。然而,为了保持示例简单,它将起作用。
我们甚至没有设置敌人的health
值,我们就已经能够伤害和杀死它了。这是因为默认情况下,实体的health
值被设置为10
。让我们更改这个属性,这样我们的敌人至少能够在第一次受到攻击时存活。
根据enemy.js
中的以下代码进行更改:
health: 200,
我们的敌人变得更难击败了,但并不是说他对我们构成挑战。是时候开始学习一些基本的AI或人工智能了。
总结前面的内容,步骤如下:
-
将项目的最大寿命添加到你的抛射物中,这样它就不会永远留在游戏中。
-
添加实体碰撞检测,使其能够与敌人碰撞。
-
设置抛射物的“检查”功能,使得当抛射物与敌人碰撞时,抛射物被摧毁,敌人受到伤害。
-
添加“弹性”以便它可以从墙上弹开。
-
设置敌人的
health
属性,使其不会被第一个抛射物击中。
用人工智能让你的 NPC 活起来
人工智能可能是游戏中最复杂的元素之一,如果不是最复杂的。顾名思义,AI 是人工或模拟的智能。游戏中的实体需要对玩家对它们或它们的环境所做的事情做出反应。在编写 AI 时,实际上是在尝试将人脑或更强大的东西放入计算机中。对于策略游戏,AI 可以决定游戏玩法的成败,因为它是在玩离线的小规模比赛时保持玩家参与的因素。对于其他类型的游戏,比如 2D 射击游戏,你可能会满足于敌人不仅仅只是向你开火。复杂的 AI 问题在于它需要考虑太多的参数,以至于一个程序员几乎无法理解。让我们将其分为三种类型:
-
单一策略 AI
-
多策略 AI
-
数据驱动 AI
策略是实体在特定情况下遵循的行为模式。当敌人健康时,它可以全力冲向你,但当受伤严重时,它会撤退并寻找一个安全的地方来治疗自己。这是使用两种不同策略的一个例子,而单一策略的敌人可能会一直攻击你,直到它死掉,不管自己的生命如何。
数据驱动 AI是完全不同的东西。它不是硬编码的行为,而是需要大量玩家数据,这些数据被上传到一个单一的位置。在那里,数据被处理,并且统计程序,如回归,决策树建模和神经网络被应用,以使 AI 在未来更加有竞争力。你得到的是一个学习实体,它变得越来越难击败,并根据模型的预测自动发明新的策略。对一些人来说,计算机能够学习和适应行为的想法可能相当可怕。然而,这是当今的现实,未来肯定会带来越来越聪明的 AI。计算机是否最终会像《终结者》和《黑客帝国》中的电影那样接管世界,还有待观察。
现在我们将忘记所有那些数据驱动的统计解决方案,只看一个单一策略的 AI。
在编写 AI 时,我们希望在决策和实际行为之间保持清晰的分工。你可以把它看作是人类大脑和身体之间的分工。大脑做出决定并向身体发送脉冲来执行动作。因此,我们将在一个单独的模块中编写我们的“大脑”,而敌人能够执行的动作将留在enemy
实体本身作为方法。
NPC 的行为
创建一个新的脚本,命名为ai.js
,并将其保存在plugins
文件夹下,如下面的代码片段所示:
ig.module('plugins.ai').
defines(function(){
ig.ai = ig.Class.extend({
})
})
我们首先定义我们全新的模块,我们的第一个插件。不要忘记在我们的main.js
中要求脚本,如下面的代码所示:
'plugins.ai',
AI 需要给实体下达命令。为了实现这一点,它们需要使用共同的语言。就像你的腿需要解释你的神经信号一样,我们的敌人需要在任何给定时间解释它需要执行的动作。我们在init()
函数中定义这些命令,如下面的代码片段所示:
init: function(entity){
ig.ai.ACTION = { Rest:0,MoveLeft:1,MoveRight:2,MoveUp:3,MoveDown:4,Attack:5,Block:6 };
this.entity = entity;
}
action
数组包含AI
模块可以发送的所有可能动作。init()
函数以它需要命令的实体作为输入。并不需要像前面的代码片段中所示那样为this.entity
分配一个实体(this.entity=entity;
),但这仅仅是确认this
不是实体本身,而是它的 AI。输入参数entity
不是分配给this
而是分配给this.entity
,这将使得可能拥有一个集体的ai
,也能够为整个敌人群体做出决策。这种集体 AI 或蜂群思维将在第五章中讨论,为你的游戏添加一些高级功能。
如果你现在在 Firefox 的 Firebug DOM 中查看,你可以看到AI
类作为ig
对象的一部分,它目前只包含我们刚刚编写的init()
函数。在编写代码时,跟踪 DOM 的演变是一个好主意。
现在我们已经定义了我们将发送的信号,让我们看看它们最终会到达哪里。打开enemy.js
脚本,并向其中添加以下update()
函数:
update: function(){
/* let the artificial intelligence engine tell us what to do */
var action = ai.getAction(this);
/* listen to the commands with an appropriate animation and velocity */
switch(action){
case ig.ai.ACTION.Rest:
this.currentAnim = this.anims.idle;
this.vel.x = 0;
this.vel.y = 0;
break;
case ig.ai.ACTION.MoveLeft:
this.currentAnim = this.anims.left;
this.vel.x = -this.speed;
break;
case ig.ai.ACTION.MoveRight :
this.currentAnim = this.anims.right;
this.vel.x = this.speed;
break;
case ig.ai.ACTION.MoveUp:
this.currentAnim = this.anims.up;
this.vel.y = -this.speed;
break;
case ig.ai.ACTION.MoveDown:
this.currentAnim = this.anims.down;
this.vel.y = this.speed;
break;
case ig.ai.ACTION.Attack:
this.currentAnim = this.anims.idle;
this.vel.x = 0;
this.vel.y = 0;
ig.game.getEntitiesByType('EntityPlayer')[0].receiveDamage(2,this);
break;
default:
this.currentAnim = this.anims.idle;
this.vel.x = 0;
this.vel.y = 0;
break;
}
this.parent();
}
我们可以将所有的行为写在单独的方法中,然后使用AI
命令来查看它们是否需要做些什么。然后,这些方法可以放在实体的update()
函数中,以保持其命令的最新状态。在这种情况下,我们不打算将这些行为分成方法。因为在这种情况下,事情并不太复杂,所有的行为代码都将适应update()
函数,而不会创建中间方法。
update()
函数现在由两个主要部分组成:调用 AI 模块来接收它需要执行的动作和实际执行动作。
通过调用ai.getAction()
方法,将动作存储在名为action
的局部变量中。然而,为了做到这一点,我们需要在敌人的requires
函数旁边添加 AI 到impact
实体代码中,如下面的代码片段所示:
.requires('impact.entity','plugins.ai')
还要给你的敌人一个速度参数,如下面的代码所示,因为 case 语句使用它来设置它们的移动:
speed:50
我们在AI
模块中定义的所有操作都在update()
函数中表示。为了使一系列案例检查更有效,每个操作的末尾都插入了一个 break。这样,一旦一个操作与案例匹配,它就会停止检查其他案例是否匹配。我们知道我们只想在每个给定时间给出一个命令,所以这是有道理的。由于update()
函数中的所有代码将在每秒调用 60 次,如果游戏以 60 帧的帧速率运行,应尽可能高效地编写。我们的四个操作都是朝着正确的方向移动,然后我们有attack
和rest
。为了确保处理每种情况,设置了一个default
值。这样,如果敌人收到他不理解的命令,他就会原地不动。如果你愿意,你可以重写代码的default
部分,并用attack
案例覆盖它;这样,如果敌人不明白他需要做什么,他就会一直攻击;野蛮但有效。
如果敌人攻击,他会调用玩家的receive damage
函数。这很有趣,因为玩家的receive damage
方法可以在player.js
中被重写,以包含来自盔甲等的伤害减少。
然而,现在让我们看一下实际的大脑或决策本身。因此,我们需要回到我们的AI
模块。
总结前面的内容,结论如下:
-
实体的 AI 是其基于外部输入做出决策的能力,通常使用多种策略
-
在代码中,决策应尽可能与实际行为分开
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,并将其保存为
ai.js
。类比于人体,这个文件将包含关于大脑的一切。 -
将
ai.js
脚本设置为 ImpactJS 类扩展。 -
在你的
main
脚本中包括ai.js
。 -
定义将行为决策与实际行为绑定的语言。类比于人体,这些将是你的神经系统传输的电脉冲。
-
为敌人将遵循的每个命令构建实际的行为模式。类比于人体,这将是身体对某些神经冲动的反应。
-
包括调用 AI 命令的函数。类比于人体,这个函数调用将是神经本身。
NPC 的决策过程
我们刚刚看到 AI getAction()
方法被调用,但尚未完全解释。它的主要目的是在调用时返回一个动作。这里可能的动作是朝着某个方向移动、攻击、阻挡进攻或根本不移动。采取什么行动是由需要做出决定的enemy
实体与玩家之间的距离决定,如下面的代码所示:
getAction: function(entity){
this.entity = entity;
//by default do nothing
var playerList= ig.game.getEntitiesByType('EntityPlayer');
var player = playerList[0];
var distance = this.entity.distanceTo(player);
var angle = this.entity.angleTo(player);
var x_dist = distance * Math.cos(angle);
var y_dist = distance * Math.sin(angle);
var collision = ig.game.collisionMap ;
//if collision between the player and the enemy occurs
//collision.trace is the way ImpactJS simulates line of sight detection. This will be explained after this block of code.
var res = collision.trace( this.entity.pos.x,this.entity.pos.y,x_dist,y_dist,
this.entity.size.x,this.entity.size.y);
if( res.collision.x){
if(angle > 0){return this.doAction(ig.ai.ACTION.MoveUp);}else{return this.doAction(ig.ai.ACTION.MoveDown);}
}
if(res.collision.y){
if(Math.abs(angle) >Math.PI / 2){return this.doAction(ig.ai.ACTION.MoveLeft)}else{return this.doAction(ig.ai.ACTION.MoveRight);}
}
if(distance < 30){
//decide between attacking, blocking or just being lazy //
var decide = Math.random();
if(decide < 0.3){return this.doAction(ig.ai.ACTION.Block);}
if(decide < 0.6){return this.doAction(ig.ai.ACTION.Attack);}
return this.doAction(ig.ai.ACTION.Rest);
}
if( distance > 30 && distance < 300) {
//if you can walk in a straight line: go for it
if(Math.abs(angle) <Math.PI / 4){ return this.doAction(ig.ai.ACTION.MoveRight); }
if(Math.abs(angle) > 3 * Math.PI / 4) {return this.doAction(ig.ai.ACTION.MoveLeft);}
if(angle < 0){return this.doAction(ig.ai.ACTION.MoveUp);}
return this.doAction(ig.ai.ACTION.MoveDown);
}
return this.doAction(ig.ai.ACTION.Rest);
}
将此函数添加到AI
模块中。就像init()
函数一样,它以实体作为输入参数。一系列局部变量被计算出来,以决定需要采取什么路径才能到达玩家。敌人需要知道与玩家的距离和朝向玩家的角度。使用collision.trace()
方法计算碰撞。这个方法的输入是实体的position
、size
和到目标的distance
,在这种情况下是玩家。在这里,你不应该把碰撞看作真正的物理碰撞,而应该把它看作视线。res.x.collision
应该被解释为“如果我在屏幕上水平看,玩家是否在视线中?”
以下截图显示了敌人的视线:
如果是这样,就不再需要上下移动。对于 y 轴和左右移动也是同样的道理。这只是为了向你展示这个函数是如何工作的,省略了前两个if
语句,并且res
变量的计算仍然会得到相同的结果,因为接下来的两个if
语句的逻辑。
在此之后检查敌人和玩家之间的距离。如果敌人足够接近可以攻击(这在30
像素处硬编码),敌人就会攻击。这个截止点可以通过读取敌人的实际范围并使用它来代替30
来改变。此外,敌人每帧有一次攻击的机会;这样一秒钟就会有 60 次攻击。你有没有被一秒钟内被剑击中 60 次?那很疼。我们可以通过增加敌人什么都不做的机会来降低这个频率。通过改变这两件事,代码可能看起来像以下的代码片段:
if(distance <entity.range){
var decide = Math.random();
if(decide < 0.3){return this.doAction(ig.ai.ACTION.Block);}
if(decide < 0.02){return this.doAction(ig.ai.ACTION.Attack);}
return this.doAction(ig.ai.ACTION.Rest);
}
当然,你需要改变实际造成的伤害,因为 2 点伤害对于一个有 200 点生命值的玩家来说可能并不那么令人印象深刻或具有挑战性。以下代码片段显示了伤害的变化:
ig.game.getEntitiesByType('EntityPlayer')[0].receiveDamage(40,this);
当敌人和玩家之间的距离为 300 时,敌人会朝着玩家移动。如前所述,它使用角度来决定首先朝哪个方向前进。在所有其他情况下,AI 建议实体休息。所以如果玩家很远,敌人就不会攻击。这样你就可以避免被所有敌人同时攻击。如果你的速度更快,你甚至可以逃跑。
还有一件小事。你可能已经注意到,一个动作不会立即返回,而是通过doAction()
方法发送。以下代码片段显示了如何做到这一点:
doAction: function(action){
this.lastAction = action;
return action;
},
这个方法也被添加到AI
模块中,只用于存储实体执行的最后一个动作。你可以不用这个函数,但是跟踪上一次执行的动作通常很方便。这个功能的应用在这个简短的 AI 教程中没有展示出来。
如果你在这一点重新加载游戏,你应该有一个真正试图杀死你的敌人,而不仅仅是像一块石头一样被动。
总结前面的内容,步骤如下:
-
调用大脑行动是通过我们的
getAction()
函数完成的。这个函数以需要做出决定的实体作为输入参数,并返回一个命令或一个动作。这个函数内部的逻辑可以像你喜欢的那样简单或复杂。在这个例子中,与玩家的距离是决定需要采取的行动的最重要因素。 -
使用
line of sight
ImpactJS 函数来确定敌人是否能看到玩家。 -
AI 应该做的是完全主观的事情;尝试添加你自己的命令和行为模式。
拾取物品来帮助你的玩家
现在我们的敌人反击了,我们可能需要一些额外的帮助,比如pickup
物品和额外的武器。
一个有用的pickup
物品将是一个即时的healthpotion
实体,这样我们就可以从受到的伤害中恢复。
用药水治疗你的玩家
让我们建立一个名为healthpotion
的实体,并将其包含在main
脚本main.js
中,如下所示:
'game.entities.healthpotion',
在healthpotion.js
脚本中包含以下代码:
ig.module('game.entities.healthpotion')
.requires('impact.entity')
.defines( function(){
EntityHealthpotion = ig.Entity.extend({
size: {x:32,y:32},
collides: ig.Entity.COLLIDES.NONE,
type: ig.Entity.TYPE.B,
checkAgainst: ig.Entity.TYPE.A,
animSheet: new ig.AnimationSheet('media /healthpotion.png',20,25),
init: function(x, y , settings){
this.parent(x,y,settings);
this.addAnim('idle',1,[0]);
},
check: function(other){
other.receiveDamage(-500,this);
this.kill();
}
})
});
healthpotion
实体是一个非常直接的实体。除了检测玩家是否触碰它,然后治疗玩家之外,它没有真正的行为。
有趣的是receiveDamage()
方法如何使用负伤害来治疗目标。这种生命药水在拾取时使用;它不总是这样,有些事情可以通过gameinfo
数组来计算。
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,并将其保存为
healthpotion.js
。 -
使用标准的 ImpactJS 模块代码设置
healthpotion.js
脚本。 -
在你的
main
脚本中包含healthpotion.js
脚本。 -
添加一个动画表和一个序列。
-
设置
collision
实体,以便它在玩家触碰它时能够检测到。 -
使用
receivedamage()
函数并带有负伤害;这将治愈玩家而不是处理伤害。让healthpotion
实体销毁自身。
用硬币变得富有
coin
实体是我们可能想要计数的物品的一个例子。它与healthpotion
实体几乎相同,除了名称、动画表和check
函数不同,如下所示:
check: function(other){
ig.game.addCoin();
his.kill();
}
不再治疗玩家,而是应用了一个名为addCoin()
的方法。这个函数还没有起作用,所以你可以把这行代码放在注释中,直到我们在“为玩家反馈保持得分”部分改变它。
首先让我们解决另一个问题。如果你用 Weltmeister 向游戏中添加了coin
和healthpotion
实体,你可能已经注意到你实际上可以通过射击它们来杀死healthpotion
和coin
实体。如果你不喜欢这种行为,可以通过给每个实体一个唯一的名称来修复,如下面的代码所示:
name: "player",
你可以在检查函数中检查它,就像下面的代码所示的那样:
check: function(other){
if (other.name == "player"){
//ig.game.addCoin();
this.kill();
}}
现在让我们让我们的得分系统起作用。
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,并将其保存为
coin.js
。 -
用标准的 ImpactJS 模块代码设置
coin.js
文件。 -
在你的
main
脚本中包含coin.js
。 -
添加一个动画表和一个序列。
-
设置
collision
实体,以便它在玩家触碰它时能够检测到。 -
当触碰
player
实体时,coin
实体必须销毁自身并调用addcoin()
函数,该函数会向游戏信息系统发送反馈。该函数将在本章后面定义,所以在实现时打开它。
为玩家反馈保持得分
跟踪一些东西的数量就是将它留在当前加载的游戏之外。这样它可以在关卡之间甚至在游戏之间传递。将以下内容添加到main.js
中MyGame
定义的上面:
GameInfo = new function(){
this.coins = 0;
this.score = 0;
},
GameInfo.coins
和GameInfo.score
现在将跟踪我们收集了多少硬币和我们当前的得分。
然而,我们确实需要两个函数,它们实际上会增加这些游戏属性。因此,让我们在main.js
脚本的MyGame
定义中添加这些函数:
addCoin: function(){
GameInfo.coins += 1; //add a coin to the money
},
increaseScore: function(points){
GameInfo.score +=points;
},
现在你可以放心地将ig.game.addCoin()
方法从注释中取出,而不用担心游戏崩溃。此外,我们可以在敌人死亡时调用increaseScore
函数。为此,我们需要更改enemy.js
脚本中敌人的kill
函数,如下面的代码片段所示:
kill: function(){
ig.game.increaseScore(100);
this.parent();
}
正如你所看到的,我们通过添加this.parent()
代码行来保留原始函数,但是在它之前添加了增加得分的代码。
我们不需要局限于只能上升的东西。我们可以限制英雄拥有的抛射物数量,并对其进行计数。将初始抛射物数量添加到GameInfo
数组中,如下面的代码片段所示:
this.projectiles = 10;
我们需要两个新的函数,我们可以像为addCoin()
和increaseScore()
一样将它们添加到MyGame
中。添加这两个函数的代码如下:
addProjectile: function(nbr_projectiles){
GameInfo.projectiles +=nbr_projectiles;
},
substractProjectile: function(){
GameInfo.projectiles -=1;
}
我们的player
实体的新攻击代码将如下代码片段所示:
if(ig.input.pressed('attack')) {
if (GameInfo.projectiles> 0){ ig.game.spawnEntity('EntityProjectile',this.pos.x,this.pos.y,{direction:this.lastpressed});
ig.game.substractProjectile();
}
}
首先我们检查是否有足够的抛射物,然后发射一个后,从我们的原始堆栈中减去一个projectile
实体。
太棒了!但是我们如何补给?我们可以为此目的创建另一个pickup
物品,如下面的代码所示:
ig.module('game.entities.pickupprojectile')
.requires('impact.entity')
.defines( function(){
EntityPickupprojectile = ig.Entity.extend({
size: {x:8,y:4},
collides: ig.Entity.COLLIDES.NONE,
type: ig.Entity.TYPE.B,
name: "pickupprojectile",
checkAgainst: ig.Entity.TYPE.A,
animSheet: new ig.AnimationSheet('media /projectile_x.png',8,4),
init: function(x, y , settings){
this.parent(x,y,settings);
this.addAnim('idle',1,[0]);
},
check: function(other){
if (other.name == "player"){
ig.game.addProjectile(10);
this.kill();
}}
})
});
在你的游戏中添加一些这样的东西,你就能像真正的兰博一样射穿一切!
这个GameInfo
数组还有许多其他用途,但是如何好好利用它就取决于你了。
总结前面的内容,步骤如下:
-
一些信息需要保留在实际游戏之外,以便在游戏结束后使用和存储。这些额外信息保存在
main
脚本中定义的gameinfo
数组中。 -
创建
gameinfo
数组,并保留一个位置来存储收集的硬币数量和玩家实现的总分数。 -
构建
addcoin()
和increasescore()
函数。addcoin()
在调用时将硬币数量增加一枚。increasescore()
可以接受一个数字输入参数,这是需要添加到总分数的分数。 -
激活
coin
实体中的addcoin()
函数。 -
覆盖敌人的
kill
方法以整合increasescore()
函数。 -
使用相同的逻辑,创建
addProjectile()
和substractprojectile()
函数。 -
更改
player
实体代码。这样它将检查玩家在变得可能发射之前有多少投射物。当发射投射物时,从剩余弹药中减去一个projectile
实体。 -
利用您学到的关于
pickup
物品的一切,制作一个可以补充玩家弹药供应的pickup
投射物。
从一个区域过渡到另一个区域
在第二章中,详细解释了如何进行 RPG 的地图过渡,介绍 ImpactJS。在本节中,我们将简要回顾一些要点。
正如您可能记得的,我们使用了三个实体文件的组合来构建级别之间的网关。将trigger
、levelchange
和void
实体添加到entities
文件夹中,并在main
脚本中包含它们,如下面的代码片段所示:
'game.entities.levelchange',
'game.entities.trigger',
'game.entities.void',
要连接到一个级别,我们首先需要构建一个级别。以下屏幕截图显示了我们应该连接到的endgame
级别:
这个级别是endgame
内容;它很快将展示这个小 RPG 的危险老板。不要忘记将其包含在main.js
中,如下面的代码片段所示:
'game.levels.level1',
'game.levels.endgame',
现在所有必要的组件都准备好了,以与第二章中所示的方式连接级别,介绍 ImpactJS。
当玩家走过时,使用trigger
实体触发levelchange
实体。void
实体用作spawn
位置。
这里需要指出一件事。当玩家从一个区域(级别)移动到另一个区域时,他的健康值会被重置为默认值,因为levelchange
脚本会生成一个新的玩家。可以通过将health
值移动到加载新级别之前的独立变量数组中,或者通过更改levelchange
脚本本身来避免这种情况。第二个选项在下面的代码片段中显示。打开levelchange.js
找到以下代码:
ig.game.player = ig.game.getEntitiesByType( EntityPlayer )[0];
var health = ig.game.player.health;
ig.game.loadLevel( ig.global['Level'+levelName] );
if(this.spawn){
var spawnpoint = ig.game.getEntityByName(this.spawn);
if(spawnpoint)
{
ig.game.spawnEntity(EntityPlayer, spawnpoint.pos.x, spawnpoint.pos.y);
ig.game.player = ig.game.getEntitiesByType( EntityPlayer )[0];
ig.game.player.health = health;
}
}
在实际加载level
实体之前,health
值被存储到一个本地变量health
中,然后重新分配给新生成的玩家。同样的操作也可以应用到任何属性,或者可以对player
实体进行临时复制,然后覆盖新生成的实体。
总结前面的内容,步骤如下:
-
从
第二章
文件夹中复制trigger
、levelchange
和void
实体,并将它们放入entities
文件夹中。 -
在
main
脚本中包含所有三个实体。 -
使用这三个实体进行级别过渡,如第二章中所示,介绍 ImpactJS。
-
更改
levelchange
实体,以便玩家的健康状况在级别加载之间暂时存储。
NPC 和对话
在许多 2D RPG 中,史诗般的故事仅通过文本来讲述。玩家在击败游戏之前与各种 NPC(非玩家角色)进行互动。敌人也是 NPC,但在大多数情况下,NPC 被视为通过给出提示、任务和物品来帮助英雄达到目标的非敌对角色。我们将在下一节介绍这样一个和平的生物,并让他说话。
对话气球
为此,我们将使用一个文本气球,将其视为一个独立的实体。让我们准备一个新的 JavaScript 文件,并将其命名为textballoon.js
,使用以下代码:
ig.module('game.entities.textballoon'
)
.requires('impact.entity','impact.game'
)
.defines( function(){
});
我们将再次需要让我们的main
脚本知道它的存在,所以将'game.entities.textballoon'
添加到main
脚本中。
在这个文件中,我们不仅会定义我们的textballoon
实体,还会定义一个内部类,我们将在textballoon
实体中使用:WordWrap
。WordWrap
是由 ImpactJS 论坛上一个名为 Kingsley 的人发明的类,所有的感谢应该归给他。这再次证明,在论坛上查找是一个好主意。有人可能已经做了你打算做的事情。WordWrap
以这样一种方式组织输入的文本,以便你可以将其放在诸如对话气球之类的对象上。我们可以在我们的任何 JavaScript 文件中定义这个类,但由于它仅被我们的textballoon
实体使用,将脚本放置如下所示是有意义的:
WordWrap = ig.Class.extend({
text:"",
maxWidth:100,
cut: false,
init:function (text, maxWidth, cut) {
this.text = text;
this.maxWidth = maxWidth;
this.cut = cut;
},
wrap:function(){
var regex = '.{1,' +this.maxWidth+ '}(\\s|$)' + (this.cut ? '|.{' +this.maxWidth+ '}|.+$' : '|\\S+?(\\s|$)');
return this.text.match( RegExp(regex, 'g') ).join( '\n' );
}
}),
WordWrap
类是通用 Impact 类的扩展,就像我们的AI
模块一样。实际上,它是一个函数,它接受三个参数:一段文本,一行文本的最大宽度,以及函数是否应该按字符或单词截断。当创建一个新的WordWrap
类时,这三个参数被分配给本地参数,如init()
函数中所示。
然而,最重要的是WordWrap
类的wrap
方法。它只包含两行代码,但却完成了所有的工作。在第一行中,构建了一个正则表达式,然后在第二行中进行解释和返回。正则表达式是一种灵活的方式,用于识别指定的文本字符串。这里不涵盖文本模式识别代码的工作原理,因为它不在本书的范围内。
现在我们已经有了textballoon
实体的最重要功能,我们可以使用以下代码构建textballoon
实体本身:
EntityTextballoon = ig.Entity.extend({
pos:{x:0,y:0},// a default position
size:{x:100,y:50},// the default size
lifeTime:200,// show the balloon for 200 frames
//media used by text balloon
font : new ig.Font('media/font.png'),// the font sheet
animSheet: new ig.AnimationSheet('media/gui_dialog.png',100,50),// the animation
wrapper : null,// place holder
init: function(x,y,settings){
this.zIndex = 1000;// always show on top
this.addAnim('idle',1,[0]);// the default graphic
this.currentAnim = this.anims.idle;
this.parent(x,y,settings);// defaults
this.wrapper = new WordWrap('Epicness awaits you!',20);//we only have one text so use it as a default
},
});
balloon
实体不过是一个带有文本的图像,在生成时显示在所有其他内容的顶部(zIndex = 1000
)。在我们的balloon
实体的Init()
方法中,使用WordWrap()
函数将文本包装到正确的尺寸。有趣的是,这里如何初始化字体(font: new ig.Font('media/font.png')
)。将要使用的字体已经存在于我们的media
文件夹中,格式为.png
,为了将其分配给我们的本地变量字体,使用了一个新的 impact 方法:ig.Font()
。与 Word 中的字体不同,这里有一个预定义的颜色和大小。如果您想为 ImpactJS 游戏制作自己的字体,可以在以下链接上找到免费的字体工具:
impactjs.com/font-tool/
还有一个名为lifeTime
的变量,它将跟踪balloon
实体被解散之前剩余的帧数。这个检查是在update()
函数中进行的,如下面的代码所示:
update:function(){
this.lifeTime = this.lifeTime -1;// counter for the lifetime
if(this.lifeTime< 0){this.kill();}// remove the balloon after 200 frames
this.parent();// defaults
},
在每一帧中,生命周期减少一次。当lifeTime
值达到0
时,balloon
实体被销毁。更智能的气球计时器可以通过计算应该阅读的文本量并调整阅读时间来实现,但这只是一个简单的例子。
我们需要的最后一件事是实体的draw()
方法。draw()
就像update()
函数一样,每一帧都会被调用,但它专门用于需要显示的内容,如下面的代码片段所示:
draw:function(){
this.parent();// defaults
var x = this.pos.x - ig.game.screen.x + 5;// x coordinate draw position
var y = this.pos.y - ig.game.screen.y + 5;// y coordinate draw position
this.font.draw(this.wrapper.wrap(),x, y,ig.Font.ALIGN.LEFT);// put it on the screen
}
所有实体都有一个draw
方法,并且会自动调用。我们现在将看一下它,因为我们的气泡需要稍作调整。在draw()
函数中,首先调用其父函数,然后定位并绘制需要显示在气泡顶部的文本。这里事情的顺序非常重要。如果你首先绘制文本并在最后放置this.parent();
,那么文本将首先被写入,然后是气泡。一旦我们有一个实体来生成我们的balloon
实体,你可以尝试这样做;现在你应该得到一个空的对话气泡。以下截图显示了一个完全功能的对话气泡:
现在我们有一个完全功能的对话气泡,是时候介绍一个想和我们说话的实体了:NPC
实体。
总结前面的内容,结论如下:
-
许多游戏中都有友好的生物在周围走动,并为玩家提供提示。
-
一个说话的角色由友好的
NPC
实体和其对话气泡组成,可以被视为一个单独的实体。此外,我们使用了一个wordwrap()
函数,它将保持句子在对话气泡的边界内。
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,并将其保存为
textballoon.js
。 -
将
wordwrap()
函数作为ImpactJS
类的扩展。 -
使用标准的 ImpactJS 模块代码设置
textballoon.js
文件。 -
在你的
main
脚本中包含textballoon.js
。 -
添加一个动画表,一个动画序列,一个大小和一个默认位置。
-
将 z-index 属性设置为一个较高的数字,这样对话气泡总是显示在其他实体的顶部。
-
使用
wordwrap()
函数来转换你选择的文本,并将其添加为对话气泡的属性。 -
如果你想为你的游戏制作自己的字体,请使用 ImpactJS 字体工具将其转换为 Impact 可以使用的文件。字体工具位于以下网址:
impactjs.com/font-tool/
。 -
更改对话气泡的
update
函数,以便它能够跟踪自对话气泡生成以来经过了多少时间。update
函数还将在预设的帧数过去时关闭对话气泡。 -
覆盖默认的
draw
函数,使其能够在对话气泡本身上绘制你的文本。
添加一个说话的非玩家角色
创建一个新的脚本并将其保存为Talkie.js
。Talkie
将是我们可爱的 NPC 的名称,如下面的代码所示:
ig.module('game.entities.Talkie')
.requires('impact.entity')
.defines(function(){
EntityTalkie = ig.Entity.extend({
})
});
与任何常规实体一样,Talkie
脚本属性在init()
函数之前或之中被定义,具体取决于你是否希望以文字表示法编写它们,如下面的代码所示:
size: {x:80,y:40},
offset:{x:-5,y:0},
// how to behave when active collision occurs
collides: ig.Entity.COLLIDES.PASSIVE,
type: ig.Entity.TYPE.B,
checkAgainst: ig.Entity.TYPE.A,
name: 'Talkie',
talked:0,
Anim:'idle', times:200,
// where to find the animation sheet
animSheet: new ig.AnimationSheet('media/Talkie.png',32,48),
init: function(x, y , settings){
this.addAnim('idle',3,[0,1]);
this.addAnim('Talk',0.2,[0,1,2,1]);
this.currentAnim = this.anims.idle;
this.parent(x,y,settings);
},
Talkie
有两种状态,要么他什么也不做(idle
),要么他在说话(Talk
),他的动画会相应地改变。他应该只在气泡存在时保持在Talk
状态,因此使用以下代码设置一个定时器来使气泡与 Talkie 的动画同步:
update: function(){
if(this.times>=0 &&this.Anim == 'Talk'){
if(this.times == 200){this.currentAnim = this.anims.Talk;}
this.times = this.times -1;
}
if(this.times == 0){
this.currentAnim = this.anims.idle;
this.times = -1;
}
this.parent();
},
动画保持在200
帧的位置;完成后,Talkie 返回到他的空闲状态。
Talkie
需要检查玩家是否在附近,这样他就可以开始说话。当玩家靠近时,textballoon
实体被生成,Talkie 将不会再说话。ig.game.sortEntitiesDeferred()
通过其 z 值重新排序游戏中的实体;这样你就可以确保气球显示在顶部。以下代码用于此目的:
check: function(other){
if(this.talked == 0){
this.Anim = 'Talk';
this.talked = 1;
ig.game.spawnEntity('EntityTextBalloon',this.pos.x - 10,this.pos.y - 70,null);
ig.game.sortEntitiesDeferred();
}
}
现在我们的 Talkie 代码已经完成,尝试将他添加到其中一个关卡,并靠近他。一个气球应该弹出,上面写着史诗般的等待着你!
Talkie 是正确的,因为我们几乎到达了游戏的结尾。
总结前面的内容,步骤如下:
-
现在我们需要一个能够向玩家传递消息的角色。我们将称这个角色为
Talkie
。 -
打开一个新的 JavaScript 文件,并将其保存为
Talkie.js
。 -
使用标准的 ImpactJS 模块代码设置
Talkie.js
文件。 -
在你的
main
脚本中包含Talkie.js
。 -
为 Talkie 添加动画表、动画序列、大小、名称和其他几个属性。
-
添加一个
talked
属性,用于跟踪 Talkie 是否已经说过话。还有一个times
属性,表示 Talkie 需要看起来像在说话的帧数。对话动画显示的时间跨度最好等于对话气球的寿命。 -
调整
update
函数使对话动画起作用。 -
覆盖
check
函数和碰撞检测,以便在玩家触摸 Talkie 时生成一个textballoon
实体,如果他之前还没有说过话。
最终战斗
通常游戏以盛大的结局结束;一个强大的 boss,你需要杀死他才能获得永恒的名声!
让我们来看看最终的Boss
实体:
ig.module('game.entities.Boss')
.requires('plugins.ai','game.entities.enemy')
.defines(function(){
EntityBoss = EntityEnemy.extend({
name: 'Boss',/* Let's call him the Boss*/
health: 300, /* he has more health than an ordinary enemy*/
speed:80, /* The default speed is higher than an enemy*/
animSheet: new ig.AnimationSheet('media/enemyboss.png',32,48)
/* different animation sheet for the Boss */
receiveDamage: function(amount,from){
/* override the default because we want an end screen (or animation) */
/* the boss is stronger then everyone, so he doesn't get damaged that fast */
amount = amount / 2;
if(this.health - amount <= 0){
//ig.system.setGame(GameEnd); /*we want an end screen (or animation)*/
}
/* update the health status */
this.health = this.health - amount;
}
})
});
在这种情况下,Boss
实体只是一个强大的敌人。没有必要精确复制和粘贴enemy
实体,并在它们共享的元素上分别调整代码。通过扩展enemy
类并只填写差异,效率更高。我们的 boss 有另一个名字,更多的生命值,更快的速度,外观不同,并且受到的伤害更少。为了能够建立在原始的enemy
实体之上,你需要在其require
函数中包含它。由于敌人已经建立在 ImpactJS entity
类之上,所以不再需要包含impact.entity
。
此外,我们需要告诉projectile
实体也可以击中Boss
实体,如下面的代码片段所示:
check: function(other){
if (other.name == "enemy" || other.name == "Boss"){
other.receiveDamage(100,this);
this.kill();
this.parent();
}
}
在projectile.js
中,if
语句被调整以适应我们的Boss
实体。你可能已经注意到我们的敌人死亡会触发游戏结束。我们将在第五章中研究这一点和开场画面,为你的游戏添加一些高级功能。你可以在endgame
级别中添加一个Boss
实体并为荣耀而战!
总结前面的内容,结论如下:
-
最终 boss 通常是玩家需要击败才能完成游戏或阶段的期待已久的对手。他通常拥有更多的生命值,造成更多的伤害,因此通常比普通敌人更难击败。
-
我们可以通过扩展
enemy
类来创建boss
实体,以基于常规敌人的角色。
总结前面的内容,步骤如下:
-
打开一个新的 JavaScript 文件,将其保存为
Boss.js
。 -
通过扩展
enemy
类设置Boss.js
文件。 -
在你的
main
脚本中包含Boss.js
。 -
更改所有需要区分 boss 和普通敌人的属性。这包括生命值、伤害、速度,甚至护甲。护甲可以通过覆盖
receivedamage()
函数来实现伤害减少。 -
覆盖
receivedamage()
函数,确保在 boss 死亡时调用游戏结束。这个 GameEnd 在第五章中有解释,为你的游戏添加一些高级功能,所以现在可以关闭它。 -
调整
projectile
实体,使其也对Boss
实体造成伤害,而不仅仅是对enemy
实体。
总结
在本章中,我们能够从头开始构建自己的俯视游戏。为了做到这一点,我们使用 ImpactJS Weltmeister 构建了关卡,并添加了一个可控制的角色,称为player。通过添加智能敌人和击败它们的武器,游戏变得更具挑战性。我们能够通过引入友好的 NPC 来为游戏增加一些深度。最后一个元素是保持得分,以便为玩家提供一些关于他或她表现如何的反馈。