ImpactJS-HTML5-游戏开发-全-

ImpactJS HTML5 游戏开发(全)

原文:zh.annas-archive.org/md5/441DA316F62E4350E9115A286AB618B0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

今天你在阅读本书,是因为你想制作视频游戏。你希望构建自己的视频游戏,可以在人们的浏览器以及他们的智能手机和平板电脑上运行。所有这些今天都是可能的,尽管这并非一直如此。你现在能够相对轻松地构建自己的游戏,原因有两个:HTML5ImpactJS

前言

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)代表ApacheMySQLPHPPerl。您可以根据下载和安装的版本在 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 控制面板页面了。

安装 XAMPP 服务器

在此面板中,您可以看到服务器的不同组件,可以打开和关闭。您需要确保至少 Apache 组件正在运行。其他组件也可以打开,但 Apache 对于运行游戏是绝对必要的。

现在转到您的浏览器。在本书的过程中,我们将使用 Chrome 和 Firefox 浏览器。但是,建议还安装最新的 Internet Explorer 和 Safari 浏览器进行测试。在地址栏中简单地输入localhost。Localhost 是本地安装服务器的默认位置。您是否看到以下XAMPP 启动屏幕

安装 XAMPP 服务器

恭喜,您已成功设置了自己的本地服务器!

已知问题是IISInternet Information Services)占用了您必需的端口。您可能需要禁用或甚至删除它们,以便为 XAMPP 释放端口。

对于MAMPM代表Mac),可能需要指定端口 8888 才能正常工作。因此,输入localhost: 8888而不是只输入localhost

总结前面的内容,步骤如下:

  1. 下载并安装 XAMMP。

  2. 打开控制面板并启动 Apache。

  3. 在地址栏中输入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文件夹中应该有libmediatools子文件夹,以及index.htmlWeltmeister.html文件。

时间进行一次小测试!只需在浏览器中输入localhost/myfirstawesomegame

“它起作用了!”的消息现在应该让你心中充满了喜悦!如果它没有出现在屏幕上,那么肯定出了大问题。如果你没有收到这条消息,请确保所有文件都存在并且位于正确的位置。

ImpactJS 带有一个名为Box2D物理引擎。检查一下你的文件夹结构中是否有这个文件夹。如果没有,你可以通过你下载 Impact 引擎时得到的个人下载链接下载一个包含引擎的演示游戏。这个演示游戏叫做Biolab Disaster,你应该能在这里找到box2d文件夹。如果没有,Dominic(ImpactJS 的创造者)还提供了一个名为physics的单独文件夹。由于 Box2D 是标准引擎的一个插件,最好在你的lib文件夹中搜索plugins文件夹,并将box2d文件夹放在这里。

安装游戏引擎:ImpactJS

总结前面的内容,步骤如下:

  • 购买 ImpactJS 许可证并下载其核心脚本

  • 将所有必要的文件放在服务器目录中新创建的名为myfirstawesomegame的文件夹中。

  • 在地址栏中输入localhost/myfirstawesomegame,打开你的浏览器。

  • 下载 Box2D 插件并将其添加到你自己服务器上的plugins文件夹中

选择一个脚本编辑器

我们现在已经让服务器运行起来,并安装了 ImpactJS 游戏引擎,但我们还没有工具来实际编写游戏代码。这就是脚本编辑器的用武之地。

为了选择适合你需求的正确代码编辑器,最好区分纯编辑器和 IDE。IDE集成开发环境既是脚本编辑器又是编译器。这意味着在一个程序中你可以改变和运行你的游戏。另一方面,脚本编辑器只是用来改变脚本。它不会显示输出,但在大多数情况下,会在你即将发生语法错误时提醒你。虽然编辑器会显示你 JavaScript 代码中的语法错误,但实际执行代码会显示逻辑错误,并给你一些(漂亮的)东西看。

对于 ImpactJS,有一个名为 AppMobi 的 IDE,它是免费的,但收费额外服务。使用 AppMobi 的替代方案是你刚刚安装的 XAMPP 服务器。

脚本编辑器,即使是非常好的脚本编辑器,通常也是免费的。在选择你喜欢的之前,你应该检查一些好的脚本编辑器,比如Eclipsenotepad++komodo editsublime edit 2。特别是对于 Mac,有一个名为Textmate的编辑器,它经常被使用,但不是免费的。当然还有Xcode,官方的苹果开发者编辑器。

所有这些脚本编辑器都会检查你在 JavaScript 代码中所犯的错误,但它们不会检查 ImpactJS 特定的代码。为此,你可以制作自己的脚本颜色编码包,或者从那些花时间构建的人那里下载一个。

下载并安装之前提到的一些脚本编辑器,并选择你最喜欢的。所有这些都可以很好地完成任务,只是个人偏好的问题。

运行预制游戏

是时候在你的电脑上开始运行游戏了。为了做到这一点,你需要书中附带的文件。这些文件可以从以下网站下载:

www.PacktPub.com/support

现在你应该已经准备好了。复制第一章的可下载材料,启动你的第一个 Impact 游戏。用 Packt Publishing 下载页面上的index.htmlmain.js脚本替换 ImpactJS 库附带的index.htmlmain.js脚本。还要用提供的mediaentitieslevels文件夹覆盖你电脑上的文件夹。

返回浏览器,重新加载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 和普通文字在以下方面有所不同:

  1. JSON 键是用双引号括起来的字符串。

  2. JSON 用于数据传输。

  3. 您不能将函数分配为 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。

Firebug - Firefox 调试器

Chrome 调试器

对于 Chrome,您甚至不需要下载插件,因为它已经捆绑了 JavaScript 控制台。此控制台可以在选项 | 额外 | JavaScript 控制台下找到,并且非常方便使用。您还可以通过右键单击网页并选择检查元素来打开控制台。

Chrome 调试器

Chrome 调试器(也称为 Chrome 开发者工具)有八个选项卡,其中有四个对调试 Impact 游戏特别有用。

元素选项卡允许您检查页面的 HTML 代码,甚至可以立即对其进行编辑。这样,您可以更改游戏的画布大小。但是,请注意,更改仅适用于加载的网页;它们不会保存到您的 HTML 或 JavaScript 文件中。

Resources标签中,您可以查找有关您的本地存储的信息。本地存储对于构建游戏并不是必需的,但它是一个用于保存高分和类似内容的很酷的功能。

Sources标签非常有用,因为它允许您检查和更改(再次是临时的)您的 JavaScript 代码。您可以在这个标签中找到您的 DOM,就像在 Firefox 中一样。代码可以手动暂停,也可以通过使用条件断点来暂停。例如,如果您的角色可以获得经验,您可以在升级时暂停游戏,看看所有变量是否都取得了您期望的值。

调试器最重要的部分是Console标签。控制台显示了您的错误所在,甚至指示了发生错误的 JavaScript 文件和行。控制台非常灵活,可以在任何其他标签打开时调用。这样,您可以在Sources标签中检查代码,如果有错误,可以通过单击右下角的X图标来调用控制台。打开SourcesConsole标签后,调试变得轻而易举。

其他四个标签是NetworkTimelineProfilesAudits标签。它们很有用,但您将花费大部分时间与SourcesConsole组件一起打开。

在本书的过程中,Firebug 和 Chrome 调试器可以互换使用。

如果启用了缓存,更改游戏并重新加载您的 Web 浏览器通常是不够的。只要您的游戏被缓存,您就无法百分之百确定您是否在评估游戏的最新版本还是浏览器在内存中锁定的先前版本。在开发游戏时,关闭缓存是明智的选择。在 Firefox 中,可以通过下载和使用一个执行此操作的插件来完成。在 Chrome 中,这只是 Chrome 开发者工具本身的一个选项。当调试器打开时,单击右下角的齿轮符号以打开设置。在General标签下,您可以禁用缓存,如下面的屏幕截图所示:

Chrome debugger

调试可以在单个浏览器中完成,但明智的做法是至少在四个浏览器上测试您的游戏是否运行顺畅,例如 Chrome,Safari,Internet Explorer 和 Firefox。使用这四个浏览器,您至少可以覆盖 95%的浏览器使用率。

如果您想为某些设备进行测试,那么测试也是必要的。这可以通过拥有您希望您的游戏在其上运行的设备之一(例如 iPad,iPhone,HTC,Galaxy 等)并在one.com等网络托管公司的帮助下将您的游戏在线上发布来完成。或者,您可以使用 AppMobi,该工具具有设备查看器,用于此目的。

测试游戏的另一个好方法是使用模拟器。模拟器是一种模拟实际智能手机的程序。这一切都很好,但让我们看一个实际的例子。

使用 Chrome 和 Firebug 进行调试的练习

在前面的章节中,我们已经让游戏开始运行了。现在让我们看看如果真的出了问题会发生什么(假设到目前为止一切都很顺利)。

首先,我们需要一些有缺陷的代码文件。因此,从debugging tutorial文件夹中复制main.jsplayer.jsprojectile.jsenemy.js脚本,并用这些脚本替换旧的脚本。main.js应该位于您的game文件夹中,而enemy.js可以在entities文件夹中找到。

现在,您的特殊(即有错误的)脚本已经就位,是时候重新启动游戏了。重新加载您的浏览器,并确保缓存是空的,否则不会显示错误。

游戏无法完全加载,如下面的加载栏所示:

在使用 Chrome 和 Firebug 进行调试的练习中

这可能经常发生在你开发新游戏时。例如,如果不同 JavaScript 文件的依赖关系错误,就会经常发生这种情况。要查看现在发生了什么,请打开 Chrome 调试器。

使用 Chrome 和 Firebug 进行调试的练习

转到控制台选项卡,查看错误消息:i 未定义 main.js:51。在编辑器中打开main.js脚本,并转到指定的行号。果然,如下代码所示,有一些问题:

i.input.bind(ig.KEY.UP_ARROW, 'up');
ig.input.bind(ig.KEY.DOWN_ARROW,'down');

没有叫做i的对象,这应该像其他的一样是ig

现在我们解决了这个问题,再次加载游戏。加载成功!太棒了!但这并不意味着一切都没有错误。打开调试器,看看是否还有其他问题困扰着你的游戏。目前没有,所以让我们继续探索。

如果一切“顺利”,你的游戏应该在你想向左走的时候卡住。

使用 Chrome 和 Firebug 进行调试的练习

你会收到消息,无法读取未定义的属性'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 目前没有指示错误。确保检查所有关卡,因为不同的关卡通常会有其他可能出现错误的实体。但是,现在让我们开始杀死一些敌人!

你可能已经注意到,摧毁这些讨厌的生物相当困难。我们不再有真正的错误,但也许其他事情并没有按计划进行。我们似乎无法杀死它,所以要么我们造成的伤害不够,要么它的生命值非常高。让我们深入了解可能涉及的两个实体:projectileenemy。我们应该检查projectile实体而不是player实体,因为尽管玩家射击了抛射物,但是造成伤害的是抛射物。枪不杀人,子弹杀人。打开projectile.jsenemy.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选项卡。现在按顺序打开iggameentities文件夹。您将看到一个编号列表,数字是entities数组中特定实体的位置。现在您可以打开一些数字,直到找到生命值为19800的敌人,如下面的截图所示:

使用 Chrome 和 Firebug 进行调试的练习

敌人被埋在其他实体的列表中,但通过他的属性我们可以看出这里发生了什么。我们射击了两次,现在它的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面板:

  1. .requires()函数调用所有需要执行代码的脚本。并非每个模块都需要这样做,但main.js脚本将始终至少需要impact库。

  2. 在这个函数调用中,您将希望添加impact.debug.debug脚本,它(正如您猜到的)调用lib/impact/debug文件夹中的debug.js脚本。

  3. 保存main.js脚本,并在 Chrome 中重新运行localhost/myfirstawesomegame

  4. 如果一切按计划进行,您现在应该在浏览器底部看到一个名为impact.debug的新工具栏。

  5. 调试器有三个选项卡:背景地图实体性能,以及右上角的四个关键指标。

  6. 从左到右,这些指标如下:

  • 运行游戏一帧所需的毫秒数。

  • 游戏的每秒帧数指示器。

  • 已经发生的绘制次数。如果您正在进行对话,这将包括角色。

  • 在右侧,您会找到当前游戏中的实体数量。

虽然这些指标快速向您展示了需要考虑的最重要的事情,但如下图所示的三个选项卡更深入地展示了:

使用 ImpactJS 进行调试

如果选择背景地图,您将看到游戏拥有的所有图形图层。假设您想让您的史诗角色在树前奔跑;您会期望树的一部分消失在角色的后面,而不是相反。当角色移动到树后面时,您希望它被树隐藏。因此,您至少需要两个图层才能绘制出这样的树。一个图层在玩家前面(很可能是树梢),另一个在玩家后面(树干)。

在调试器的部分中,您可以打开和关闭图层。如果图层设置为预渲染,您将能够看到图层的块。在以下截图中,检查和碰撞被打开,而其他选项被关闭:

使用 ImpactJS 进行调试

使用实体选项卡,您可以打开和关闭几个有趣的指标。如果您打开显示碰撞框,您将能够看到您的角色周围的红色框以及几个(不可见的)实体,它们不断检查碰撞。这些红色框指示触发点碰撞的边界。这很重要,因为如果围绕您的英雄角色的碰撞框比图像大得多,他可能无法再通过门,或者会神秘地被远处的敌人击中。在编写代码时,您可以自己设置这些碰撞框的大小,从而产生一些有趣的效果,比如只能通过射中眼球来杀死 boss。

当您打开显示速度时,您应该让角色四处走动。现在您将看到一条伸出在他前面的线,这是他当前移动速度的指示。

通过显示名称和目标,您可以看到所有命名实体及其目标。这是一个有趣的功能,但对于您的目的,最好使用 ImpactJS 级别编辑器(Weltmeister)。

最后,性能选项卡向您展示了浏览器为运行游戏需要执行的不同任务所付出的努力,如下图所示:

使用 ImpactJS 进行调试

在上一个图表上可以看到两条水平线:33ms16ms。这些线对应大约 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

无法直接连接到移动设备。但是,您可以向任何拥有移动设备的人发送链接。这样,您的朋友可以在安装了AppMobi applab的情况下测试您的最新创作。

总的来说,AppMobi 易于入门,并且在整个发布游戏的过程中值得考虑,尽管在开发过程中您几乎完全是靠自己。

PhoneGap

PhoneGap(以前称为Cordova)是另一个用于开发移动本机应用程序的 XDK。

PhoneGap 可以与 AppMobi 相比较,就功能而言,但 AppMobi 非常直观,更适合新手。PhoneGap 使您能够构建本地操作系统的应用程序,集成PayPal,并使用推送通知。

如下截图所示,PhoneGap 提供了一种构建您的应用程序以分发到不同渠道的方法:

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.jsindex.html文件以及entitieslevelspluginsmedia文件夹。

我们现在已经准备好去探索 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脚本的代码中进行更改:

  1. 在您首选的脚本编辑器中打开main.js脚本。

  2. main.js脚本中,您将看到对loadLevel()函数的调用。

this.loadLevel(LevelLevel1);

注意

这个调用位于游戏的init()函数中(ig.game.init)。这意味着main.js脚本将在初始化(即init)时做的第一件事情之一是加载Level1关卡。显然,我们不再需要这样做了,因为现在我们有自己的关卡叫做myfirstepiclevel。为了让游戏知道它必须包含这个关卡,您需要将它添加到.requires()函数中,如下面的代码行所示:

'game.levels.level1',
'game.levels.myfirstepiclevel',
  1. 还要更改对loadLevel()函数的调用,使其调用myfirstepiclevel关卡,而不是Level1,如下面的代码片段所示:
this.loadLevel(LevelMyfirstepiclevel1);

提示

正如您可能已经注意到的,您总是需要在实际级别名称之前加上Level一词。此外,您将始终需要用大写字母写Level和您的级别名称。如果违反其中任何一个,将导致游戏加载时发生严重崩溃。在实际级别名称之前加上Level一词是一个相当奇怪的约定,特别是因为loadlevel()等函数被设计为期望一个级别文件。可能在未来的 ImpactJS 版本中会删除这个强制前缀。但是目前,如果未在实际级别名称之前插入Level一词或者用大写字母写Level和您的级别名称,将导致显示以下错误:

在 Weltmeister 中创建、加载和保存级别

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,您将需要一个自动跟随可玩角色的视口来展示您的级别。这个视口相当容易整合,将在本章后面进行解释。按照以下步骤创建一个新的瓷砖集:

  1. 尝试通过单击图层选择菜单顶部的加号(+)号来创建一个新图层。

  2. 为图层输入一个名称;比如astonishinglayertree,随便你喜欢什么。

  3. 现在从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 时碰撞图层的最佳位置。这是因为碰撞图层本身在玩游戏时实际上没有图形,所以它不会遮挡其他任何东西。

图层和 z 轴

总结我们遇到的细节,我们得出结论:

  • 一个关卡由具有诸如图块大小、距离以及是否为collision图层等属性的不同图层组成

  • 使用Layers菜单中的(+)号添加一个新图层,并将其命名为astonishinglayer

  • 将图块集media/tree.png添加到图层中。将其尺寸设置为30 x 20,将其图块大小设置为32

  • 尝试玩弄图层上的所有属性,包括将图层上下拖动

  • 每次调整参数后,保存关卡并在浏览器中重新加载游戏

添加和移除实体和物体

有三种大类型的图层:entitiescollision,和其他任何图层。对于实体和死物体,实体和图形图层是感兴趣的。

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图层查看层次的实体。如果你无法清楚地看到地图中存在的实体,请随意关闭其他图层,方法是点击它们的白色方块。或者你可以在悬停在地图上时按空格键,以打开实体选择菜单。和往常一样,我们有一个玩家实体,所以我们可以在地方四处移动,但是在菜单中,你应该注意到一些额外的实体,比如VoidTriggerLevelchange

  • Void实体是一个相当简单的实体;它只是一个带有名称和一些坐标的盒子

  • Trigger实体将在特定类型的实体(如玩家)与其碰撞时触发与其链接的任何其他实体的代码。

  • LevelChange实体将使游戏加载另一个层次

通过巧妙地组合这三个实体,你可以连接层次,所以让我们来做吧:

  1. 确保entities图层是顶部之一,这样你就可以看到你添加的东西。

  2. 首先选择Trigger实体,并将其放在靠近门的地图上。一开始它只是一个小方块,所以把它做得大一点,以适应出口。你可以通过选择方框,将鼠标移动到其边缘,直到看到双箭头(双箭头符号),然后拖动它使其变大(就像你在 PC 上调整任何窗口对象的大小一样)。在选择大小时,你的目标是在玩家想要使用门出去时检测到他。

  3. 现在添加一个Levelchange实体。如果选择Levelchange实体,您将在右侧看到其属性。目前,这只是地图上的位置(x 和 y 坐标)和其尺寸,以防您重新调整了框的形状。通过在键框中输入name,为Levelchange实体命名为ToOutside。按Enter键确认。现在您将看到该实体具有额外的属性(名称),其值为ToOutside。只有通过给它一个名称,它才能被唯一标识,这就是我们需要的。我们还需要告诉它需要加载哪个关卡。添加键level,值为outside,然后按Enter键。

  4. TriggerLevelchange实体现在都在关卡中,但它们尚不知道彼此的存在;如果我们希望它们合作,这一点非常重要。

  5. 返回到触发器实体并给它一个目标。您可以通过在键框中输入target.1,值为ToOutside来实现。注意单词target后面的句点(.);没有它,它将无法工作。现在按Enter键,看着两个漂亮的方块如何通过一条白线连接在一起,如下图所示。Trigger实体现在知道它是Levelchange实体;当玩家触摸到它时,它将被触发。连接两个不同的关卡

保存并加载关卡。将您的玩家走向触发器位置;Levelchange实体的位置是无关紧要的。如果一切顺利,现在您应该能够通过走向门来进入下一个关卡!

奇怪的是,当您进入外部世界时,并没有被放置在建筑物旁边。即使对于一个视频游戏来说,这也太奇怪了。此外,当试图打开门时,没有办法回到室内,您将永远被困在外面,除非重新加载。

这是因为在外部关卡中没有添加spawnpointTriggerLevelchange实体。我们将弥补这一点,但首先让我们在内部关卡中添加一个出生点。

为了做到这一点,我们需要Void实体。将Void实体添加到关卡中,并将其放在门前,但是超过触发器。将其放得太靠近(或者在上面)触发器会导致玩家被击退到外面。虽然制作一个永恒的循环,让玩家在关卡之间来回击退是很有趣的,但是永恒的循环(就像除以零一样)有可能摧毁世界。将Void实体命名为insideSpawn。选择Levelchange实体并添加键spawn,值为OutsideSpawn

连接两个不同的关卡

我们已经完成了内部关卡,现在需要将外部关卡设置为其镜像相反。因此,再次添加VoidLevelchangeTrigger实体。将Void实体命名为OutsideDoor,因为Levelchange实体将寻找它。将Levelchange实体命名为ToInside,并将触发器指向它。还要向Levelchange实体添加Levelspawn属性。这些属性的值分别为InsideInsideDoor

连接两个不同的关卡

保存并重新加载游戏。如果一切顺利,您现在应该能够像专业人士一样在两个关卡之间移动。

总结连接两个关卡的完整过程:

  • 在 Weltmeister 中加载内部关卡

  • 向关卡中添加三个实体,TriggerLevelchangeVoid

  • 给每个实体命名

  • 使触发器指向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.modulerequires()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 将生成以下错误:

ImpactJS 实体

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,

typecollidescheckAgainst属性都与硬币在与其他实体碰撞时的行为有关。type参数告诉游戏硬币在评估碰撞时属于类型B。硬币实际上从不与任何东西发生碰撞,因为其collides属性设置为NEVER。这里的其他可能性是:LITEPASSIVEACTIVEFIXEDLITEPASSIVE实体不会相互碰撞。FIXED实体无法移动,LITE实体可以被ACTIVE实体移动。如果ACTIVE实体与另一个ACTIVEPASSIVE实体发生碰撞,则两个实体都会移动。

起初听起来有点棘手,但值得尝试。打开player.js文件,并确保collides属性设置为ACTIVE。现在使用 Weltmeister 在游戏中添加一个硬币,靠近玩家的起点。通过在下面的示例中添加两个破折号(//)将硬币的checkAgainst属性注释掉:

//checkAgainst: ig.Entity.TYPE.A

如果将coin实体的模式设置为FIXED,则无法移动硬币。当将其模式设置为PASSIVEACTIVE时,可以移动硬币,但会很困难,因为硬币会推回。然而,设置为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()方法相当空。

我们调用了父实体,这里只是entitythis.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 实体是一个交互式游戏元素。

  • 死亡对象是最不复杂的实体;Voidcoin实体就是其中的两个例子。

  • Void实体在游戏中是不可见的,但在 Weltmeister 中是可见的,因为它具有特殊的 Weltmeister 属性。在本章的前面,我们曾将其用作生成点。

  • coin实体在游戏中是可见的,因为它有一个动画表。它也可以被玩家捡起,因为有碰撞检测。

  • 碰撞检测可以采用多种形式:实体可以杀死、阻挡、推开,或者根据其碰撞属性简单地忽略彼此。

  • 尝试玩弄Voidcoin实体中解释的所有参数,看看会发生什么。

设置玩家控制

没有什么比实际玩家和他或她送入遗忘的敌人更有趣了。

如果你打开player.jsenemy.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时,角色将向上移动并显示动画。这些updownleftright状态不是 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.jshtml.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()方法。该方法只需要一个参数:您想要与其位置相对于游戏根文件夹的音乐文件。您可以使用音量属性设置音量。音量可以从值01。当然,您可以将音量设置为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 中被称为恢复。恢复的值可以在01的范围内设置。由于力随时间减小,物体永远不会以与其撞击墙壁时相同的速度弹回。您可以在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 关卡的步骤:

  1. 让我们复制我们在第一章文件夹中保留的新安装的ImpactJS文件夹,并将其重命名为RPG。将第三章文件夹中的media文件夹复制到你的RPG/media文件夹中。这样至少我们有一些图形可以使用。

  2. 回到当你在浏览器中输入localhost/RPG时得到的它起作用了!屏幕。

  3. 让我们从打开 Weltmeister(localhost/Weltmeister.html)并绘制一个小关卡开始。

  4. 这一次你会注意到没有为你准备好任何东西。唯一可用的层是entities层,甚至连一个实体都没有。然而,一旦我们有了一些东西来填充世界,我们就可以画一个小的游戏场地来开始。

  5. 所以让我们添加另一个层(+-符号)并将其命名为grass。让我们将瓷砖大小设置为16,并在距离 1 像素处有一个 30 x 20 的区域。选择瓷砖集grass.png,然后点击应用更改按钮,然后你就可以开始铺设草坪了。

  6. 如果你的绘图框没有完全居中,按住Ctrl键并移动鼠标直到它居中。如果由于某种原因它太大而无法适应你的屏幕,可以用鼠标滚轮缩小。

  7. 一旦我们把整个层都涂成绿色,我们就可以轻松地添加另一个层放在草坪上。但在这样做之前,将你的文件保存为level1。经常保存是一种美德。

  8. 在添加层时,你可以根据它们应该代表的内容进行命名和使用。例如,你可以有一个家具、植物和杂项物品的层。这是一个不错的工作方式,但你必须记住,一些层在视觉上会出现在你的玩家和怪物实体的前面,而其他层则会出现在它们的后面。即使是一个简单的墙最好也用两层来绘制。

提示

Weltmeister 不支持无限数量的层。为了保持层的数量可观,你可以为特定的关卡设置瓷砖集。例如,你有两个关卡设置:城市和地牢。两者都可以包含一把椅子,所以不要害怕在城市的瓷砖集上放置相同的椅子,也在不同的瓷砖集上构建你的地牢。重复信息会增加你的整体游戏大小,但可以减少单个关卡所需的层数。

我们的草地只叫grass,因为我们不会有草地漂浮在玩家面前;因此我们不需要第二层草地。让我们创建两个新图层,分别叫做vegetation_backvegetation_frontvegetation_back必须在图层选择菜单中的entities图层下面。vegetation_front必须放在entities图层上面。这两个新图层一起将构成地图上的所有植被。

选择图块集tree.png,并为grass图层使用相同的设置。

使用vegetation_front图层绘制树的上部,使用vegetation_back绘制下部。以下屏幕截图显示了不同的图层:

Building an RPG level

你的 Weltmeister Layers菜单中应该有以下图层:

Building an RPG level

如果你不知道任何东西的上部或下部应该是什么,想想你的玩家和/或敌人有多大。当走过树时,他们的头或脚不应该消失。为了避免玩家完全穿过树,我们需要另一个图层,碰撞图层。

在 Weltmeister 中添加一个名为collision的图层。

不要忘记,你可以通过将图层拖到图层堆栈的顶部或关闭挡住视野的图层来在 Weltmeister 中使图层可见。在这种情况下,如果碰撞图层在堆栈的底部,grass图层可能会挡住所有的视野。将collision图层拖到顶部并在必要时打开和关闭它是非常有效的。设置图层的设置与以往一样。

使用collision图层,在关卡周围绘制边界,这样就没有人可以逃跑了。还在树干下面或上面的分界线附近放一些碰撞方块,如下图所示:

Building an RPG level

所以我们创建了一个可行的环境。虽然不多,但这是一个开始。然而,为了加载关卡,我们需要对我们的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 );

总结前面的内容,步骤如下:

  1. 我们需要从头开始构建我们的游戏。因此,我们需要最初下载的 ImpactJS 文件。将它们放在服务器工作目录的一个单独文件夹中。还要测试一下是否收到了it works!消息。

  2. chapter 3文件夹的media文件添加到你刚刚设置的文件夹中。

  3. 打开 Weltmeister 关卡编辑器并创建一个分层关卡。你需要一个碰撞图层,一个实体图层和三个图形图层。底部的图形图层将代表草地。其他两个图层代表所有其他在玩家前面或后面出现的对象。

  4. 绘制图形图层。

  5. 在你的main脚本中包含关卡文件。

  6. 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实体构建玩家。这个原型有几个属性(比如healthvelocity)和几个方法(比如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;

正如您在前面的代码中所看到的,两个重要的元素和玩家被分配给了一个本地变量。然后,视口坐标被设置为玩家的位置。如果您希望相机将玩家放在屏幕的左上角,您就不需要游戏画布。但当然,我们希望玩家居中,所以我们通过画布尺寸的一半来调整其位置。

重新加载浏览器后,您会注意到您终于可以走到屏幕底部和树下面。太好了!只是可惜这里没有什么可做的,所以下一步我们将引入一些敌对的东西。

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,并将其保存为player.js

  2. 使用标准的 ImpactJS 模块代码设置player.js脚本。

  3. main脚本中包含player.js

  4. 为可玩角色添加动画表和序列,以便在 Weltmeister 中找到它。还为其提供健康和大小。

  5. 通过将键盘键绑定到main脚本中的输入状态来添加玩家控制。

  6. 将这些输入状态绑定到移动角色动作,通过操纵其速度。

  7. 通过引入额外的动画序列并在某些输入状态激活时调用它们,使移动看起来像是平滑的动画。

  8. 放置一个自动跟随玩家四处走动的摄像头。

引入一个可击败的对手

同样,我们将不得不从头开始,因此打开一个空的 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);
}

现在我们可以将我们的第一个敌人添加到关卡中。不过它不会做太多事情,甚至你甚至可以穿过他走。这是因为实体之间还没有指定碰撞。

将以下代码添加到playerenemy实体作为属性。您可以使用旧的 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 轴。

现在我们有了一个敌人。让我们杀了它!

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件并将其保存为enemy.js

  2. 使用标准的 ImpactJS 模块代码设置enemy.js脚本。

  3. 在您的main脚本中包含enemy.js

  4. 添加一个动画表和几个动画序列,考虑到敌人可能行走的每个方向。

  5. 更改玩家和敌人的碰撞实体。它们需要能够检测到彼此的存在,以便敌人以后可以伤害玩家。

  6. 如果您还没有这样做,请通过在您的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 44 x 8。实际上,在这种特殊情况下,还有另一种可能更简单的解决方案,涉及动画的角度。在编程语言中,通常有不同的方法来获得相同或类似的结果。然而,现在我们将使用多个动画表。

我们定义了两种不同的动画表。我们将它们命名为animSheetXanimSheetY,而不是在标准的animSheet属性上初始化它们,以表示不同的轴。init()函数不像PlayerEnemy实体那样调用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});
}

太棒了!我们现在的英雄可以像老板一样朝各个方向射箭。目前,我们的箭头仍然相当坚固,对我们幸运的敌人来说相当无害。它们只是击中我们关卡的边缘,永远停留在那里,或者直到游戏重新加载。

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,并将其保存为projectile.js

  2. 设置projectile.js脚本。给它两个动画表。

  3. projectile脚本添加到main脚本中。

  4. 更改玩家的update函数,以便玩家在激活attack输入状态时可以生成一个抛射物。

  5. 根据玩家射击时面对的方向,调整抛射物的方向和动画。

  6. 确保在生成时将玩家的方向传递给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人工智能了。

总结前面的内容,步骤如下:

  1. 将项目的最大寿命添加到你的抛射物中,这样它就不会永远留在游戏中。

  2. 添加实体碰撞检测,使其能够与敌人碰撞。

  3. 设置抛射物的“检查”功能,使得当抛射物与敌人碰撞时,抛射物被摧毁,敌人受到伤害。

  4. 添加“弹性”以便它可以从墙上弹开。

  5. 设置敌人的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 的演变是一个好主意。

NPC 的行为

现在我们已经定义了我们将发送的信号,让我们看看它们最终会到达哪里。打开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 帧的帧速率运行,应尽可能高效地编写。我们的四个操作都是朝着正确的方向移动,然后我们有attackrest。为了确保处理每种情况,设置了一个default值。这样,如果敌人收到他不理解的命令,他就会原地不动。如果你愿意,你可以重写代码的default部分,并用attack案例覆盖它;这样,如果敌人不明白他需要做什么,他就会一直攻击;野蛮但有效。

如果敌人攻击,他会调用玩家的receive damage函数。这很有趣,因为玩家的receive damage方法可以在player.js中被重写,以包含来自盔甲等的伤害减少。

然而,现在让我们看一下实际的大脑或决策本身。因此,我们需要回到我们的AI模块。

总结前面的内容,结论如下:

  • 实体的 AI 是其基于外部输入做出决策的能力,通常使用多种策略

  • 在代码中,决策应尽可能与实际行为分开

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,并将其保存为ai.js。类比于人体,这个文件将包含关于大脑的一切。

  2. ai.js脚本设置为 ImpactJS 类扩展。

  3. 在你的main脚本中包括ai.js

  4. 定义将行为决策与实际行为绑定的语言。类比于人体,这些将是你的神经系统传输的电脉冲。

  5. 为敌人将遵循的每个命令构建实际的行为模式。类比于人体,这将是身体对某些神经冲动的反应。

  6. 包括调用 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()方法计算碰撞。这个方法的输入是实体的positionsize和到目标的distance,在这种情况下是玩家。在这里,你不应该把碰撞看作真正的物理碰撞,而应该把它看作视线。res.x.collision应该被解释为“如果我在屏幕上水平看,玩家是否在视线中?”

以下截图显示了敌人的视线:

NPC 的决策过程

如果是这样,就不再需要上下移动。对于 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 教程中没有展示出来。

如果你在这一点重新加载游戏,你应该有一个真正试图杀死你的敌人,而不仅仅是像一块石头一样被动。

总结前面的内容,步骤如下:

  1. 调用大脑行动是通过我们的getAction()函数完成的。这个函数以需要做出决定的实体作为输入参数,并返回一个命令或一个动作。这个函数内部的逻辑可以像你喜欢的那样简单或复杂。在这个例子中,与玩家的距离是决定需要采取的行动的最重要因素。

  2. 使用line of sight ImpactJS 函数来确定敌人是否能看到玩家。

  3. 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数组来计算。

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,并将其保存为healthpotion.js

  2. 使用标准的 ImpactJS 模块代码设置healthpotion.js脚本。

  3. 在你的main脚本中包含healthpotion.js脚本。

  4. 添加一个动画表和一个序列。

  5. 设置collision实体,以便它在玩家触碰它时能够检测到。

  6. 使用receivedamage()函数并带有负伤害;这将治愈玩家而不是处理伤害。让healthpotion实体销毁自身。

用硬币变得富有

coin实体是我们可能想要计数的物品的一个例子。它与healthpotion实体几乎相同,除了名称、动画表和check函数不同,如下所示:

check: function(other){
  ig.game.addCoin();
  his.kill();
}

不再治疗玩家,而是应用了一个名为addCoin()的方法。这个函数还没有起作用,所以你可以把这行代码放在注释中,直到我们在“为玩家反馈保持得分”部分改变它。

首先让我们解决另一个问题。如果你用 Weltmeister 向游戏中添加了coinhealthpotion实体,你可能已经注意到你实际上可以通过射击它们来杀死healthpotioncoin实体。如果你不喜欢这种行为,可以通过给每个实体一个唯一的名称来修复,如下面的代码所示:

name: "player",

你可以在检查函数中检查它,就像下面的代码所示的那样:

check: function(other){
  if (other.name == "player"){
  //ig.game.addCoin();
  this.kill();
}}

现在让我们让我们的得分系统起作用。

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,并将其保存为coin.js

  2. 用标准的 ImpactJS 模块代码设置coin.js文件。

  3. 在你的main脚本中包含coin.js

  4. 添加一个动画表和一个序列。

  5. 设置collision实体,以便它在玩家触碰它时能够检测到。

  6. 当触碰player实体时,coin实体必须销毁自身并调用addcoin()函数,该函数会向游戏信息系统发送反馈。该函数将在本章后面定义,所以在实现时打开它。

为玩家反馈保持得分

跟踪一些东西的数量就是将它留在当前加载的游戏之外。这样它可以在关卡之间甚至在游戏之间传递。将以下内容添加到main.jsMyGame定义的上面:

GameInfo = new function(){
  this.coins = 0;
  this.score = 0;
},

GameInfo.coinsGameInfo.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数组还有许多其他用途,但是如何好好利用它就取决于你了。

总结前面的内容,步骤如下:

  1. 一些信息需要保留在实际游戏之外,以便在游戏结束后使用和存储。这些额外信息保存在main脚本中定义的gameinfo数组中。

  2. 创建gameinfo数组,并保留一个位置来存储收集的硬币数量和玩家实现的总分数。

  3. 构建addcoin()increasescore()函数。addcoin()在调用时将硬币数量增加一枚。increasescore()可以接受一个数字输入参数,这是需要添加到总分数的分数。

  4. 激活coin实体中的addcoin()函数。

  5. 覆盖敌人的kill方法以整合increasescore()函数。

  6. 使用相同的逻辑,创建addProjectile()substractprojectile()函数。

  7. 更改player实体代码。这样它将检查玩家在变得可能发射之前有多少投射物。当发射投射物时,从剩余弹药中减去一个projectile实体。

  8. 利用您学到的关于pickup物品的一切,制作一个可以补充玩家弹药供应的pickup投射物。

从一个区域过渡到另一个区域

在第二章中,详细解释了如何进行 RPG 的地图过渡,介绍 ImpactJS。在本节中,我们将简要回顾一些要点。

正如您可能记得的,我们使用了三个实体文件的组合来构建级别之间的网关。将triggerlevelchangevoid实体添加到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实体进行临时复制,然后覆盖新生成的实体。

总结前面的内容,步骤如下:

  1. 第二章文件夹中复制triggerlevelchangevoid实体,并将它们放入entities文件夹中。

  2. main脚本中包含所有三个实体。

  3. 使用这三个实体进行级别过渡,如第二章中所示,介绍 ImpactJS

  4. 更改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实体中使用:WordWrapWordWrap是由 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()函数,它将保持句子在对话气泡的边界内。

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,并将其保存为textballoon.js

  2. wordwrap()函数作为ImpactJS类的扩展。

  3. 使用标准的 ImpactJS 模块代码设置textballoon.js文件。

  4. 在你的main脚本中包含textballoon.js

  5. 添加一个动画表,一个动画序列,一个大小和一个默认位置。

  6. 将 z-index 属性设置为一个较高的数字,这样对话气泡总是显示在其他实体的顶部。

  7. 使用wordwrap()函数来转换你选择的文本,并将其添加为对话气泡的属性。

  8. 如果你想为你的游戏制作自己的字体,请使用 ImpactJS 字体工具将其转换为 Impact 可以使用的文件。字体工具位于以下网址:impactjs.com/font-tool/

  9. 更改对话气泡的update函数,以便它能够跟踪自对话气泡生成以来经过了多少时间。update函数还将在预设的帧数过去时关闭对话气泡。

  10. 覆盖默认的draw函数,使其能够在对话气泡本身上绘制你的文本。

添加一个说话的非玩家角色

创建一个新的脚本并将其保存为Talkie.jsTalkie将是我们可爱的 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 是正确的,因为我们几乎到达了游戏的结尾。

总结前面的内容,步骤如下:

  1. 现在我们需要一个能够向玩家传递消息的角色。我们将称这个角色为Talkie

  2. 打开一个新的 JavaScript 文件,并将其保存为Talkie.js

  3. 使用标准的 ImpactJS 模块代码设置Talkie.js文件。

  4. 在你的main脚本中包含Talkie.js

  5. 为 Talkie 添加动画表、动画序列、大小、名称和其他几个属性。

  6. 添加一个talked属性,用于跟踪 Talkie 是否已经说过话。还有一个times属性,表示 Talkie 需要看起来像在说话的帧数。对话动画显示的时间跨度最好等于对话气球的寿命。

  7. 调整update函数使对话动画起作用。

  8. 覆盖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实体,以基于常规敌人的角色。

总结前面的内容,步骤如下:

  1. 打开一个新的 JavaScript 文件,将其保存为Boss.js

  2. 通过扩展enemy类设置Boss.js文件。

  3. 在你的main脚本中包含Boss.js

  4. 更改所有需要区分 boss 和普通敌人的属性。这包括生命值、伤害、速度,甚至护甲。护甲可以通过覆盖receivedamage()函数来实现伤害减少。

  5. 覆盖receivedamage()函数,确保在 boss 死亡时调用游戏结束。这个 GameEnd 在第五章中有解释,为你的游戏添加一些高级功能,所以现在可以关闭它。

  6. 调整projectile实体,使其也对Boss实体造成伤害,而不仅仅是对enemy实体。

总结

在本章中,我们能够从头开始构建自己的俯视游戏。为了做到这一点,我们使用 ImpactJS Weltmeister 构建了关卡,并添加了一个可控制的角色,称为player。通过添加智能敌人和击败它们的武器,游戏变得更具挑战性。我们能够通过引入友好的 NPC 来为游戏增加一些深度。最后一个元素是保持得分,以便为玩家提供一些关于他或她表现如何的反馈。

第四章:让我们建立一个侧向滚动游戏

在本章中,我们将使用 ImpactJS 和 Box2D 构建一个非常基本的侧向滚动游戏。Box2D 是一个开源的 C++物理引擎。使用它,重力和摩擦力会被模拟,就像你在愤怒的小鸟游戏中看到的那样。虽然不是完全集成,但经过足够的努力,Box2D 可以在 ImpactJS 游戏中使用。就像上一章一样,游戏将从头开始构建。主要区别在于使用物理引擎和侧向滚动游戏设置。

在本章中,我们将涵盖:

  • 侧向滚动游戏

  • 使用 Box2D 与 ImpactJS

  • 使用 ImpactJS Weltmeister 构建一个侧向滚动关卡

  • 引入一个可玩的角色

  • 在侧向滚动游戏中添加一些敌人

  • 为玩家配备子弹和炸弹

  • 使用人工智能使敌人更聪明

  • 创建玩家可以拾取的物品

  • 保持得分并在每次敌人死亡时添加分数

  • 连接两个不同的侧向滚动关卡

  • 以强大的敌人结束游戏

侧向滚动游戏设置

侧向滚动视频游戏是一种从侧面角度观看的游戏,玩家通常在玩过程中从左到右移动。屏幕基本上是从一侧滚动到另一侧,无论是从左到右还是其他方向,因此得名侧向滚动。著名的侧向滚动游戏有 2D 马里奥、索尼克、大金刚、旧版洛克人、超级任天堂和 Gameboy 版的银河战士游戏,以及古老但成功的双战龙。

这种类型的大多数游戏都有一个长的关卡,英雄需要通过战斗或避开怪物和死亡陷阱找到自己的路。到达关卡的结尾后,通常除了重新开始该关卡之外,没有其他回头的办法。《银河战士》在这方面有些奇怪,因为它是最早的侧向滚动游戏之一,拥有一个你可以像在标准角色扮演游戏(RPG)中一样探索的巨大世界。《银河战士》为侧向滚动游戏的新思维方式奠定了基础;你需要在虚拟的数英里长的洞穴中找到自己的路,偶尔会发现自己回到起点。《梦幻城堡》是另一个例子,这是一个使用中世纪背景的侧向滚动冒险游戏。

侧向滚动游戏设置

既然我们已经了解了侧向滚动游戏是什么,让我们开始用 ImpactJS 构建一个。

为 Box2D 准备游戏

在我们正式开始之前,我们需要确保所有文件都正确放置:

  1. 从我们在第一章中准备的原始 ImpactJS 可下载文件夹中复制一份,启动你的第一个 Impact 游戏。或者,你也可以再次下载一个新的,并将其放在 XAMPP 服务器的htdocs目录中。给你的文件夹起一个名字;让我们完全原创,叫它chapter4。其他名字也可以。

  2. 从 ImpactJS 网站下载物理演示,并转到其plugins文件夹。在这里,你应该找到Box2D插件。创建你自己的plugins文件夹,并将Box2D扩展放在那里。

  3. 通过在浏览器中访问localhost/chapter4来测试一切是否正常。它正常工作!消息应该再次等待着你。

  4. 此外,我们还需要更改一些 Box2D 核心文件。Box2D 不是 ImpactJS 的产品,而是在开发 JavaScript 等效版本之前为基于 C++的游戏而发明的。然后,Dominic Szablewski(ImpactJS 的创造者)将这个 JavaScript 版本与 ImpactJS 集成。然而,存在一些缺陷,其中一个是错误的碰撞检测。因此,我们需要用一个修正了这个问题的适应文件来替换其中一个原始文件。从可下载的chapter4文件夹中获取game.jscollision.js脚本,并将它们放在本地的Box2D文件夹中。collision.js脚本得益于提供该脚本的 Abraham Walters。

  5. chapter4文件夹的媒体文件复制到本地的media文件夹中。

  6. 我们需要对主脚本进行调整。我们的游戏将不再是标准 Impact 游戏类的扩展。

MyGame = ig.Game.extend({ 
  1. 相反,它将是修改后的 Box2D 版本的扩展。因此,请确保更改以下代码片段:
MyGame = ig.Box2DGame.extend({
  1. 我们需要在main.js脚本的开头包含 Box2D 的game文件才能使用这个扩展。
.requires(
  'impact.game',
  'impact.font',
  'plugins.box2d.game'
)
  1. 最后,为了测试一切是否正常,我们需要加载一个带有碰撞层的关卡。这是因为 Box2D 需要碰撞层来创建它的世界环境和边界。没有关卡,你将遇到一个错误,看起来像这样:Preparing the game for Box2D

  2. 为此,从chapter4文件夹的level子文件夹中复制testsetup.js脚本,并将其放在本地的levels文件夹中。将关卡添加到所需的文件中。

'game.levels.testsetup'
  1. 在主脚本的init()方法中插入一个loadlevel()函数。
init: function() {
    this.loadLevel( LevelTestsetup );
  },
  1. 在浏览器中重新加载游戏,你应该会看到it works!的消息。现在你已经看到了它,可以从代码中删除它。它在主脚本的draw()方法中。
  var x = ig.system.width/2,
    y = ig.system.height/2;
    this.font.draw( 'It Works!', x, y, ig.Font.ALIGN.CENTER);

太好了!我们现在应该已经准备就绪了。我们要做的第一件事是建立一个小关卡,以便有一个属于自己的游乐场。

构建一个横向滚动的关卡

为了构建一个关卡,我们再次需要依赖于 ImpactJS Weltmeister:

  1. 在浏览器中打开 Weltmeister localhost/chapter4/Weltmeister.html。我们没有任何实体可以玩耍,所以现在我们要添加的只是一些图形和一个碰撞层。这个碰撞层特别重要,因为 Box2D 扩展代码将寻找它,缺少它将导致游戏崩溃。可以说,对于 ImpactJS 来说,Box2D 仍处于起步阶段,这样的小 bug 是可以预料到的。

  2. 添加一个层并将其命名为collision;Weltmeister 将自动识别它为碰撞层。

  3. 将其瓷砖大小设置为8,层尺寸设置为100 x 75。现在我们有一个 800 x 600 像素的画布可以使用。

  4. 现在在边缘画一个框,这样我们就有了一个封闭的环境,没有实体可以逃脱。当重力开始作用时,这将非常重要。没有坚实的地面,你肯定会得到一些意外的结果。

  5. 现在添加一个新的层,将其命名为background。我们将使用一张图片作为这个关卡的背景。

  6. media文件夹中选择church.png文件作为图块集。我们的图片是 800 x 600 像素,所以它应该恰好适合我们用碰撞层创建的区域。将瓷砖大小设置为100,层尺寸设置为8 x 6。在画布上绘制教堂的图片。

  7. 将你的关卡保存为level1

太好了,我们现在有了一个基本的关卡。虽然它很空,但一些额外的障碍会很好。只需按照以下步骤添加一些障碍:

  1. 添加另一个名为platforms的层。

  2. 使用tiles.png文件作为图块集。它们设计简单,但可以作为任何你想构建的平台的基本构件。将瓷砖大小设置为8,尺寸设置为100 x 75,与碰撞层完全相同。

  3. 在开始绘制平台之前,打开与碰撞层链接选项。这样,你就不需要事后用碰撞层追踪平台。如果你不希望平台的每个部分都是固体的,当然可以暂时关闭链接,绘制瓷砖,然后重新打开链接;链接不是事后建立的。

  4. 在关卡中添加一些浮动平台;按照你的内心欲望来决定它们应该是什么样子。

  5. 当你觉得舞台已经准备好时保存你的关卡。

  6. 将关卡添加到你的main.js脚本的require()函数中。

.requires(
  'impact.game',
  'impact.font',
  'plugins.box2d.game',

  'game.levels.testsetup',
  'game.levels.level1'
)
  1. 确保在开始时加载名为level1的关卡,而不是我们的testsetup关卡,通过改变loadLevel()函数的参数。
    init: function() {
    // Initialize your game here; bind keys etc.
    this.loadLevel( LevelLevel1 );
  },

构建一个侧面滚动关卡

现在是时候向游戏中添加一个可玩实体,这样我们就可以发现我们刚刚创建的令人惊叹的关卡了。

可玩角色

由于我们正在使用 Box2D,我们将不使用标准的 ImpactJS 实体,而是使用适应版本。特别是实体在 Box2D 世界中移动的方式是使一切变得不同的地方。在标准的 ImpactJS 中,这是将你的角色图像沿着某个方向移动几个像素的非常简单的过程。然而,Box2D 使用力;因此,为了移动,你需要克服重力甚至空气摩擦。但让我们先建立一个基本实体:

  1. 打开一个新的 JavaScript 文件,并将其保存为entities文件夹中的player.js

  2. 添加基本的 Box2D 实体代码如下:

ig.module(
  'game.entities.player'
)
.requires(
  'plugins.box2d.entity'
)
.defines(function(){
  EntityPlayer = ig.Box2DEntity.extend({
  });
});
  1. 正如你所看到的,术语entity是 Box2D 实体的扩展,因此需要 Box2D 实体插件文件。再次确保遵守命名约定,否则你的玩家实体将不会出现在 Weltmeister 中。

  2. 'game.entities.player'参数添加到main.js脚本中。

如果你在进行这些修改后访问 Weltmeister,你会发现玩家在你的实体层中。尽管目前它只是一个不可见的正方形,你无法控制它。是时候通过添加一个动画表来改变他的不可见性了。

EntityPlayer = ig.Box2DEntity.extend({
  size: {x: 16, y:24},
  name: 'player',
  animSheet: new ig.AnimationSheet( 'media/player.png', 16, 24 ),
  init: function( x, y, settings ) {
    this.parent( x, y, settings );
    this.addAnim( 'idle', 1, [0] );
    this.addAnim( 'fly', 0.07, [1,2] );
  } 
});

通过上面的代码块,我们给玩家指定了大小和名称;但更重要的是,我们添加了图形。动画表只包含两个图像,一个是玩家站立不动时的图像,另一个是玩家飞行时的图像。这并不多,但对于一个简单的游戏来说足够了。侧面滚动游戏在需要图形方面有相当大的优势。理论上,你只需要两张图像来代表一个角色;也就是说,一个是角色静止不动时的图像,另一个是角色在运动时的图像。而对于一个俯视游戏,你至少需要六张图像来完成同样的事情。这是因为,除了侧视图,你还需要一个角色背面和正面的图像。因此,如果你为玩家开火添加一个动画,这将导致侧面滚动游戏需要额外绘制一张图像,而俯视游戏需要三张图像。很明显,如果你只有有限的资源来获取你的图形,侧面滚动游戏更好。

现在我们可以将玩家添加到游戏中并且他实际上是可见的,但我们还没有对他有任何控制。

玩家控制是在两个地方完成的,即主脚本和玩家脚本。在主脚本中,将控制添加到游戏的init()方法中。

init: function() {
    // Bind keys
    ig.input.bind(ig.KEY.LEFT_ARROW, 'left' );
    ig.input.bind( ig.KEY.RIGHT_ARROW, 'right' );
    ig.input.bind( ig.KEY.X, fly);
//Load Level
    this.loadLevel( LevelLevel1 );
  },

在玩家脚本中,我们需要改变我们的update()函数,这样玩家就可以对我们的输入命令做出反应。

update: function() {
  // move left or right
  if( ig.input.state('left') ) {
    this.body.ApplyForce( new b2.Vec2(-20,0),this.body.GetPosition() );
    this.flip = true;
  }
  else if( ig.input.state('right') ) {
    this.body.ApplyForce( new b2.Vec2(20,0),this.body.GetPosition() );
    this.flip = false;
  }
  // jetpack
  if( ig.input.state('fly') ) {
    this.body.ApplyForce( new b2.Vec2(0,-60),this.body.GetPosition() );
    this.currentAnim = this.anims.fly;
  }
  else {
    this.currentAnim = this.anims.idle;
  }
  this.currentAnim.flip.x = this.flip;
  this.parent();
}

在 Box2D 中,实体有一个额外的属性,即身体。为了移动身体,我们需要对其施加力。这正是当我们使用身体的ApplyForce()方法时发生的事情。我们在某个方向上施加一个力,因此我们实际上使用一个向量。向量的使用正是 Box2D 的全部内容。只要我们保持右、左或飞行按钮按下,力就会被施加。然而,当释放时,实体并不会立即停止。不再施加进一步的力,但需要一定的时间来消耗施加力的效果;这与我们在前几章中使用的速度有很大的不同。

如果你把玩家添加到关卡中,确保他在左上角的某个平台上。左上角是默认可见的,我们还没有一个适应性视口来跟随我们的玩家。准确地说,他现在并不需要一个平台来站立,因为我们的世界没有重力。让我们解决这个问题。在main.js脚本中添加重力属性到你的游戏,如下所示:

MyGame = ig.Box2DGame.extend({
  gravity: 100,

让我们带我们的玩家进行一次测试飞行,好吗?

可玩角色

你可能已经注意到,即使他飞行得相当顺利,我们的喷气背包青蛙遇到的任何固体物体都会使他旋转。也许你实际上不希望发生这种情况。特别是当他的头朝下时,他的喷气背包火焰朝上。现在,如果激活喷气背包仍然导致向上推力,那么喷气背包火焰朝上就没有太多意义。因此,我们需要解决他的稳定性问题。这可以通过在水平轴上固定身体来实现。将以下代码添加到青蛙的update()函数中:

this.body.SetXForm(this.body.GetPosition(), 0);

现在玩家的身体被固定在 0 度角朝向 x 轴。尝试将其更改为 45;现在你有一个疯狂的青蛙,即使面向左,他的身体也始终向右倾斜飞行。

现在我们有一个飞行和稳定的青蛙。只可惜当我们向右移动一点或重力把我们带到关卡的底部时,我们就看不到他了。现在绝对是引入一个跟随摄像头的时候了。为此,我们需要对游戏的update()函数进行修改,如下所示:

update: function() {
    this.parent();
    var player = this.getEntitiesByType( EntityPlayer )[0];
    if( player ) {
      this.screen.x = player.pos.x - ig.system.width/2;
      this.screen.y = player.pos.y - ig.system.height/2;
    }
},

玩家被放在一个局部变量中,并且每帧检查其位置以更新屏幕的位置。因为我们从玩家的位置中减去视口大小的一半,所以我们的玩家被整齐地保持在屏幕中央。如果不减去这部分,玩家将保持在屏幕的左上角。

保存所有修改并在你创建的关卡周围飞行;尽情享受宁静,因为很快敌对势力将搅乱这个地方。

让我们快速回顾一下我们关于 Box2D 实体以及如何使用它制作可玩角色的内容。Box2D 实体不同于 ImpactJS 实体,Box2D 利用向量来移动。向量是方向和力的组合:

  • 打开一个新的 JavaScript 文件,并将其保存为player.js

  • 插入标准的 Box2D 实体扩展代码。

  • 在主脚本中包含玩家实体。

  • 为玩家添加动画。还利用flip属性,它可以在垂直轴上翻转图像,并为侧向滚动游戏剪切所需的角色图形的一半。

  • 添加玩家控制,使其能够向左、向右和向上移动。注意力是如何施加在身体上以便移动的。一旦输入按钮被释放,不再施加力,实体将继续前进并完全停止,一旦力完全消散或者他撞到一个固体墙壁。

  • 将重力引入游戏的属性。由于重力是一个不断向下的恒定力量,它会将一切拉向它遇到的第一个固体物体,除非提供一个相反的力。对于我们的飞行青蛙,他的喷气背包是对抗重力的反作用力。

  • 我们的青蛙目前还不知道如何稳定地飞行。将他固定在水平轴上,这样他每次撞到固体物体时就不会旋转。

  • 最后,我们需要一个摄像机来跟踪我们的位置。在游戏的update()函数中加入自动跟随摄像机。

添加一个小敌人

我们需要一些对手,一些我们可以在拥有武器后击落的东西。因此,让我们介绍一些更多的青蛙!这次是敌对的:

  1. 打开一个新文件,保存为enemy.js

  2. 将以下代码插入文件中。这是在 Weltmeister 中获得我们敌人表示所需的最小代码。因此,它已经包括了动画表。

ig.module(
  'game.entities.enemy'
)
.requires(
  'plugins.box2d.entity'
)
.defines(function(){
EntityEnemy = ig.Box2DEntity.extend({
size: {x: 16, y:24},
name: 'enemy',
animSheet: new ig.AnimationSheet( 'media/enemy.png', 16, 24),
init: function( x, y, settings ) {
  this.parent( x, y, settings );
  // Add the animations
  this.addAnim( 'idle', 1, [0] );
  this.addAnim( 'fly', 0.07, [1,2] );
  }
})
});
  1. 在我们的main.js脚本中需要敌人实体。
'game.entities.enemy'
  1. 使用 Weltmeister 在关卡中添加敌人。

由于我们的敌人目前相当无助,我们也可以将他从平台上击落。

添加一个小敌人

在正常的 ImpactJS 代码中,我们必须为此设置碰撞变量,否则玩家和敌人青蛙会直接穿过彼此。在 Box2D 中,这是不必要的,因为碰撞会自动假定,并且我们的飞行青蛙撞到每个可移动对象时都会施加力。

由于我们已经有了重力,一个很好的替代方法是在关卡顶部生成敌人。在游戏的init()函数中添加spawnEntity()函数。敌人将在那里生成,并且重力会将其拉到底部。

this.loadLevel( LevelLevel1 );
this.spawnEntity('EntityEnemy',300,30,null);

确保spawnEntity()函数在关卡加载后使用,否则会出错。一旦敌人有了自己的智能,在关卡顶部生成敌人就会更有意义。它们会下落,要么落到最底部,要么直到它们到达一个平台,在那里它们会等待玩家并攻击它。

一旦我们为红色青蛙提供了一些基本的人工智能,我们将把它变成一个真正讨厌的生物。然而,让我们首先通过向游戏添加一些武器来做好准备。

让我们简要回顾一下我们是如何创建我们的敌人的:

  • 打开一个新的 JavaScript 文件,保存为enemy.js

  • 插入标准的 Box2D 实体扩展,附加动画表,并添加动画序列

  • 在主脚本中包含敌人实体

  • 使用 Weltmeister 和spawnentity()方法在关卡中添加敌人

引入强大的武器

武器很棒,特别是如果它们受到重力的影响,或者如果它们可以对其他实体施加一些力。我们将在这里看两种类型的武器,即抛射物和炸弹。

发射抛射物

抛射物将是我们对手青蛙的主要武器,所以让我们从设置基础开始:

  1. 打开一个新的 JavaScript 文件,保存为projectile.js,放在entities文件夹中。

  2. 使用以下代码片段添加基本的 Box2D 实体代码,包括动画表和序列:

ig.module(
  'game.entities.projectile'
)
.requires(
  'plugins.box2d.entity'
)
.defines(function(){
  EntityProjectile = ig.Box2DEntity.extend({
  size: {x: 8, y: 4},
  lifetime:60,
  name: 'projectile',
  animSheet: new ig.AnimationSheet( 'media/projectile.png', 8, 4),
  init: function( x, y, settings ) {
    this.parent( x, y, settings );
    this.addAnim( 'idle', 1, [0] );
  }
});
});
  1. 除了名称、大小和执行动画所需的元素之外,我们已经包括了一个名为lifetime的属性。每个抛射物都以60lifetime开始。当它达到0时,我们将使其消失并杀死子弹。这样我们就不会在一个游戏中得到过多的实体。每个实体都需要自己的计算,一次在屏幕上有太多实体可能会显著降低游戏的性能。可以使用 ImpactJS 调试器来跟踪这种性能,通过在主脚本中包含'impact.debug.debug'命令来打开它。

  2. game.entities.projectile脚本添加到main.js脚本中。

现在我们可以通过 Weltmeister 向游戏中添加抛射物。然而,手动添加对我们来说没有太大用处。让我们改变玩家的代码,这样我们的青蛙就可以生成抛射物。首先,在主脚本中将'shoot'状态绑定到一个键。

ig.input.bind(ig.KEY.C, 'shoot' );

然后将以下代码添加到玩家的update()函数中。

if(ig.input.pressed('shoot') ) {
  var x = this.pos.x + (this.flip ? -0 : 6);
  var y = this.pos.y + 6;
  ig.game.spawnEntity( EntityProjectile, x, y, {flip:this.flip} );
}

生成弹丸需要在特定位置完成,并且必须指向特定方向,要么向左,要么向右。我们任意地将生成点的 y 坐标设置为比我们的玩家位置低 6 像素;我们也可以将其设置为 10、20 或 200 像素。不过,在最后一种情况下,子弹看起来会生成在玩家下方,这会显得相当不寻常。不过,我们不要忘记玩家的位置总是在其图像的左上角。鉴于我们的青蛙的高度为 24 像素,看起来就好像子弹是从嘴里射出来的,这对于一只青蛙来说相当酷。x 坐标是另一回事。如果青蛙面向左,我们不调整生成坐标;如果他面向右,我们将其调整 6 像素。有关玩家是否翻转的信息不仅用于调整生成坐标。它还作为一个可选的输入参数传递给弹丸本身。这里将用它来确定它应该面向和飞向哪一边。在发射子弹时,你可能会注意到青蛙被击退了一点,有点像枪的后坐力。这是因为青蛙最初占据了子弹生成时的位置。如果你想避免这种酷炫的效果,你只需要让子弹离青蛙远一点。如果你此时加载游戏,你会注意到你的子弹生成了,但没有飞走。这是因为我们没有告诉子弹在生成时应该这样做。

将以下两行代码添加到弹丸的init()函数中将纠正这种情况。

this.currentAnim.flip.x = settings.flip;
var velocity = (settings.flip ? -10 : 10);
this.body.ApplyImpulse( new b2.Vec2(velocity,0),
this.body.GetPosition() );

在生成弹丸时,我们现在应用的是冲量而不是力。ApplyImpulse()ApplyForce()函数之间存在显著的区别。ApplyForce()函数在物体上施加一个恒定的力,而ApplyImpulse()函数只施加一次,但是突然。你可以将其比作推动一块石头与跑向它并用你所有的力量和动量撞击它。现实生活中的子弹与我们在这里尝试模拟的方式相同;它被一个小爆炸甩开,之后再也没有被推动。局部变量var.velocity用于调整子弹的方向,就像动画取决于settings.flip参数的值一样。如果flip属性的值为 false,子弹将面向右并向右飞行。如果flip属性的值为 true,动画将翻转,使其面向左。因为速度取负数,子弹也会向左飞行。

我们仍然可以调整 y 轴上的冲量,目前设置为0。输入一个负数将使我们的青蛙向上射击,就像一门防空炮。输入一个正数将使他向下射击,就像一架轰炸机。尝试调整这个值,看看效果。

我们的弹丸仍然在屏幕上徘徊,因为我们还没有充分利用我们的lifetime属性。

让我们修改update()函数,以限制我们子弹的寿命。

update: function(){
  this.lifetime -=1;
  if(this.lifetime< 0){this.kill();}
  this.parent();
}

每当游戏通过更新循环,也就是每帧一次,弹丸的剩余寿命就会减少 1。在每秒 60 帧的游戏中,给定一个总寿命值为 60,子弹在生成后有 1 秒的寿命。

我们可以用它向敌人射击,并且实际上用子弹的力量将他们推开,但我们还没有真正伤害到他们。要实现这一点,我们需要检查是否击中了敌人。

check: function(other){
  other.receiveDamage(10);
  this.kill();
}

添加这个修改后的check()函数,这将使弹丸在自毁之前造成伤害,是不够的。尽管碰撞是由 Box2D 自动处理的,但check()函数工作所需的参数并没有。我们需要做一些其他的事情:

  1. 通过添加TYPE属性,告诉敌人它是 B 型实体。
type: ig.Entity.TYPE.B,
  1. 使用checkAgainst属性使抛射物检查与 B 类型实体的碰撞。
checkAgainst: ig.Entity.TYPE.B,
  1. 现在保存并重新加载游戏。你现在可以杀死那些讨厌的红色青蛙了。

尝试将你的玩家设为 B 类型实体。现在你的子弹会杀死你。这是因为我们让它们生成在我们的青蛙已经占据的空间中。正如我们之前看到的,这也是为什么我们在发射子弹时有这种后坐力效应的原因。然而,这次不仅仅是后坐力;它实际上可以杀死玩家。所以我们最好不要让我们的玩家成为 B 类型实体,或者我们应该让我们的子弹生成得离得更远,失去后坐力效应。拥有一些可以自卫的东西是很好的,即使其他青蛙现在还不构成太大的威胁。在让它们活过来之前,我们很快要看一下更爆炸性的东西,一个炸弹。

在转向炸弹之前,让我们再快速看一下我们是如何引入我们的主要武器——子弹的:

  • 我们需要枪,很多枪。

  • 打开一个新的 JavaScript 文件,并将其保存为projectile.js

  • 插入标准的 Box2D 实体扩展,附加一个动画表,并添加动画序列。还添加一个lifetime属性,用来跟踪子弹在游戏中应该停留多久。

  • 在主脚本中包含抛射实体。

  • 在主脚本中为射击输入状态添加一个键绑定。

  • 当玩家点击射击按钮时,让我们的飞行青蛙产生一个抛射物。

  • 给子弹添加一个冲量,这样它就可以真正飞起来,而不仅仅是掉到地上。

  • 检查子弹在空中的时间,并在超过预设寿命时将其销毁。

  • 让子弹检查敌人。如果遇到敌人,它应该造成伤害并自杀。

  • 尝试让子弹杀死玩家,但不要保持这种状态。

制造一个真正的炸弹

制造炸弹的基础与制造抛射物的基础相同,实际上,它们与创建任何实体的基础相同:

  1. 打开一个新的 JavaScript 文件,并将其保存为bomb.jsentities文件夹中

  2. 添加基本的 Box2D 实体代码,动画表和序列如下:

ig.module(
  'game.entities.bomb'
)
.requires(
  'plugins.box2d.entity'
)
.defines(function(){
EntityBomb = ig.Box2DEntity.extend({
  size: {x: 24, y: 10},
  type: ig.Entity.TYPE.A,
  checkAgainst: ig.Entity.TYPE.B,
  animSheet: new ig.AnimationSheet( 'media/bomb.png', 24, 10 ),
  lifespan: 100,
  init: function( x, y, settings ) {
    this.parent( x, y, settings );
    // Add the animations
    this.addAnim( 'idle', 1, [0] );
    this.currentAnim = this.anims.idle;
  }
});
});
  1. 这次我们已经给我们的炸弹一个类型和一个用于造成伤害的检查类型

  2. game.entities.bomb参数作为所需实体放入main.js脚本

现在我们有一个炸弹,我们可以把它放在任何我们想要的关卡中。我们可以在我们的关卡天花板附近添加一些炸弹,这样它们在关卡加载时会掉下来。那将是很棒的,因为会有一个真正的爆炸。我们将把这个爆炸作为一个单独的方法引入,只有我们的炸弹才能使用。

explosion:
function(minblastzone,maxblastzone,blastdamage,blastforcex,blastforcey){
  varEnemyList= ig.copy(ig.game.entities);
  var i = 0;
  //check every entity
  while(typeofEnemyList[i] != 'undefined'){
    Enemy = EnemyList[i];
    //calculate distance to entity
    distance = Math.sqrt((this.pos.x - Enemy.pos.x)*(this.pos.x -Enemy.pos.x) + (this.pos.y - Enemy.pos.y)*(this.pos.y -Enemy.pos.y));
    //adjust blastdirection depending on entity position
    if(this.pos.x - Enemy.pos.x< 0){adjustedblastforcex =blastforcex}
    else{adjustedblastforcex = - blastforcex}
    if(this.pos.y - Enemy.pos.y< 0){adjustedblastforcey = blastforcey}
    else{adjustedblastforcey = - blastforcey}//if within blastzone: blow up the targetif(minblastzone< distance && distance <maxblastzone){Enemy.body.ApplyImpulse(newb2.Vec2(adjustedblastforcex,adjustedblastforcey),this.body.GetPosition());
      Enemy.receiveDamage(blastdamage,this);}
      i++;
    }
}

就像init()update()check()方法一样,我们现在将explosion()方法插入到炸弹实体中,以便它今后能够使用。explosion()方法接受五个参数:

  1. 最小爆炸区域:如果一个实体距离比这更近,他将不会受到影响。这对于炸弹来说并没有太多意义,除非它允许你在一个炸弹中使用几次爆炸。这反过来又使得在目标靠近炸弹时造成更大的伤害,而在目标远离炸弹时造成更小的伤害成为可能。

  2. 最大爆炸区域:距离最大爆炸区域以外的一切都不会受到爆炸的影响。

  3. 爆炸伤害:这是实体在爆炸区域内会受到的伤害。

  4. Blastforcex:这是应用于受影响实体的 x 轴冲量。它将决定目标会向右或向左飞多远。

  5. Blastforcey:这是应用于受影响实体的 y 轴冲量。它将决定目标会飞多高。显然,如果目标在炸弹下方爆炸,它会将目标向下推,而不是向上。

explosion() 方法的工作方式如下。所有实体都被复制到一个本地变量中。然后依次检查这些实体,看看它们距离炸弹有多远。这里计算的距离是欧几里得距离。在计算欧几里得距离或普通距离时,你应用毕达哥拉斯定理。这个定理规定,如果已知三角形的另外两边的长度,就可以计算出一个直角三角形的任意一边的长度。公式是 a² + b² = c²,其中 c 是三角形的最长边。根据不幸的目标是位于炸弹的右侧还是左侧,上方还是下方,力的方向会进行调整。最后,函数检查距离是否在爆炸区域的范围内。如果是这样,就对目标施加伤害和冲量。在这一点上,实体要么死亡,要么飞向空中;无论哪种情况都不是好消息。

仅仅添加这个 explosion() 方法是没有用的,直到我们真正使用它。因此,我们需要修改我们的 update() 方法,以便在炸弹寿命结束时引爆我们的炸弹。

update: function(){
  //projectiles disappear after 100 frames
  this.lifespan -= 1;
  if(this.lifespan< 0){
    this.explosion(0,40,70,200,100);
    this.explosion(40,200,20,100,50);
    this.kill();
  }
  this.parent();
},

寿命部分的工作方式与弹丸中的方式完全相同。然而,在这种情况下,我们不仅仅调用 kill() 函数,而是使用我们新开发的爆炸两次。我们可以只调用一次函数,并将爆炸范围值设置在 0 到 200 之间。正如前面提到的,我们现在的优势在于高伤害和靠近炸弹的压力之间的区分,以及低伤害和远离炸弹的压力。从技术上讲,我们可以使用任意数量的爆炸;每一个都需要计算时间。不过,你可以决定你想要多少个爆炸。

在实际测试游戏中的爆炸之前,确保为所有实体分配健康值。它们是否能够承受爆炸的伤害将取决于你是否给予它们足够的健康值。由于默认值设置为 10,它们将不会飞走,而是立即死亡,如果使用前面的数字。因此,让我们通过在它们各自的 init() 函数之前添加此属性来给我们的玩家和敌人一个健康值为 100。

health: 100

作为最后的修饰,我们可以让炸弹在接触到敌对青蛙之一时爆炸。

check: function(other){
    other.receiveDamage(30);
    this.explosion(0,40,70,200,100);
    this.explosion(40,200,20,100,50);
    this.kill();
}

我们已经确保炸弹通过设置 checkAgainst 属性检查与 B 类型实体的接触。直接受到这块金属的伤害设置为 30。这之后是爆炸本身,它将造成 70 分的伤害,因为敌人离得很近。第二波爆炸影响到更远的一切,然后炸弹最终自毁。

现在我们有一个可以放置在关卡中任何位置并且效果很好的炸弹。然而,如果我们的玩家自己也能生成一个炸弹,那就更好了。在接下来的步骤中,我们简单地重复了我们在弹丸中所做的操作,使玩家自己生成一个炸弹:

  1. 将一个键盘按钮分配给炸弹输入状态,如下行代码所示:
ig.input.bind(ig.KEY.V, 'bomb');
  1. 修改玩家的 update() 函数,以便玩家现在可以使用以下代码生成炸弹:
if (ig.input.pressed('bomb')){
  var x = this.pos.x + (this.flip ? 0 : 8 );
  var y = this.pos.y + 25;
  ig.game.spawnEntity(EntityBomb,x,y, {flip:this.flip});
}
  1. 这里定义的生成坐标与我们在弹丸中所做的不同。 y 坐标非常重要;它设置为 25,因为我们的飞行青蛙的高度为 24 像素。这样炸弹总是生成在飞行青蛙的正下方。

  2. 将以下代码添加到炸弹的 init() 函数中,以便它接受 flip 参数,以知道生成时应该面向哪一侧。

this.currentAnim.flip.x = settings.flip;
  1. 保存、重新加载,并炸掉那些红色的青蛙!不过要小心,炸弹也可能杀死你。Building an actual bomb

炸弹是我们最大的武器;让我们快速回顾一下我们是如何构建它的:

  • 打开一个新的 JavaScript 文件,并将其保存为 bomb.js

  • 插入标准的 Box2D 实体扩展,附加动画表,并添加动画序列。添加一个寿命属性,用于跟踪炸弹爆炸前剩余的时间,如果没有被触碰敌人而过早引爆。

  • 在主脚本中包括炸弹实体。

  • 在关卡中添加一个炸弹。

  • 介绍explosion()方法;这是一个自定义函数,模拟爆炸的伤害和力量效果。

  • 更改update()函数,使炸弹在时间到时爆炸。

  • 使用check()函数检测与敌人的碰撞并立即引爆。

  • 为炸弹分配一个键盘快捷键。

  • 调整玩家的update()函数,使玩家命令时会生成一个炸弹。

  • 使炸弹翻转到玩家所看的方向。

  • 玩得开心,尽情地炸青蛙!

人工智能

是时候让我们的红色青蛙变得更聪明一点,这样他们至少有一点机会对抗我们新开发的武器库。在第三章中,让我们建立一个角色扮演游戏,我们完全按照书本上的方法做到了这一点,通过将决策与行为分开。我们为决策制定了一个单独的人工智能(AI)文件,而实际行为始终在实体的update()函数中。

这一次,我们将保持非常简单,直接将所有人工智能放在敌人的update()方法中。这将证明,即使是简单的人工智能也可以看起来相当聪明。

让我们用以下代码修改我们的敌人的update()函数:

update: function(){
  var players = ig.game.getEntitiesByType('EntityPlayer');
  var player = players[0];
  // both distance on x axis and y axis are calculated
  var distanceX = this.pos.x - player.pos.x;
  var sign = Math.abs(distanceX)/distanceX;
  var distanceY = this.pos.y - player.pos.y;
  //try to move without flying, fly if necessary
  var col = ig.game.collisionMap.trace( this.pos.x, this.pos.y,player.pos.x, player.pos.y,16,8 );
  if (Math.abs(distanceX) < 110){
    var fY = distanceY> 0 ? -50: 0;
    this.body.ApplyForce( new b2.Vec2(sign * -20,fY),this.body.GetPosition() );
    if(distanceY>= 0){this.currentAnim = this.anims.fly;}
    else{this.currentAnim = this.anims.idle;}
  }
  this.body.SetXForm(this.body.GetPosition(), 0);
  if (distanceX> 0){this.currentAnim.flip.x = true;}
  else{this.currentAnim.flip.x = false;}
  this.parent();
  }

将此函数插入到敌人实体中,将使他试图抓住玩家。但它是如何工作的呢?首先,玩家实体保存在函数的本地变量中,称为player。计算敌人和玩家之间的水平距离和垂直距离。sign变量用于确定青蛙应该向左飞还是向右飞。他总是向上飞;如果他需要下降,因为玩家在他下面,他将让重力发挥作用。在飞行时,飞行动画是活动的,否则使用空闲动画,即使在水平移动时也是如此。

青蛙的身体固定在 x 轴上,以防止他旋转,就像玩家一样。最后,根据玩家相对于敌人的位置,动画会翻转到左侧或右侧。

现在我们有一只青蛙,如果我们离他足够近,他会跟着我们走。现在我们需要他对玩家造成一些伤害:

  1. 确保敌人的类型和需要检查的类型分别填写为 B 和 A。还引入一个名为cooldowncounter的新敌人属性,如下所示:
type: ig.Entity.TYPE.B,
checkAgainst: ig.Entity.TYPE.A,
cooldowncounter: 0,
  1. cooldowncounter属性将跟踪自上次青蛙能够造成伤害以来经过了多少帧。

  2. cooldowncounter属性必须计数,因此将其添加到update()函数中:

this.cooldowncounter ++;
  1. 扩展check()函数,以检查自上次攻击以来是否已经过了足够的帧数,并允许青蛙进行攻击,如下所示:
check: function(other){
  if (this.cooldowncounter> 60){
    other.receiveDamage(10,this);
    this.cooldowncounter = 0;
  }
}

青蛙现在将能够在玩家身上使用其恶毒的近战攻击。无论青蛙在近距离对玩家造成的攻击是什么,每次击中玩家都会降低玩家的健康值 10 点。现在玩家肯定需要避开这些恶毒的生物,以免健康值迅速下降。我们需要给玩家一些额外的东西,让他能够在这场屠杀中生存下来。

人工智能是使敌人值得对抗的原因。与我们在第三章中提到的不同,让我们建立一个角色扮演游戏,它并不总是需要变得复杂。让我们快速看一下我们如何为横向滚动游戏实现了人工智能:

  • 更改update()函数,使敌人现在可以朝着玩家飞行。这个新的update()函数是敌人青蛙的人工智能。与第三章中的让我们建立一个角色扮演游戏不同,这次决策和行为都包含在同一段代码中。

  • 引入一个冷却计数器,用于跟踪自上次攻击以来的帧数。还要确保敌人实体是 B 类型,并检查它是否接触到 A 类型的实体。玩家应该是 A 类型的实体。

  • 通过将其添加到修改后的update()函数中,使cooldown属性在每帧过去时增加 1 的值。

  • check()函数中加入攻击,使青蛙成为不可忽视的力量。

拾取物品

我们的小飞行青蛙现在正式可以被那些讨厌的红色青蛙杀死。这对他来说不是好消息,我们需要提供一种方式来补充失去的健康。这是通过使用拾取物品来实现的,也就是,当接触到玩家时会消失但在过程中提供有益效果的实体。

在我们添加实际的拾取物品之前,它将以补充健康的板条箱的形式出现,让我们先在游戏中添加一个普通的板条箱。

添加一个普通板条箱

我们的板条箱将作为我们可以发明的所有类型的板条箱的原型。执行以下步骤创建板条箱:

  1. 打开一个新文件并将其保存为crate.js

  2. 将板条箱代码添加到文件中。

ig.module(
  'game.entities.crate'
)
.requires(
  'plugins.box2d.entity'
)
.defines(function(){
EntityCrate = ig.Box2DEntity.extend({size: {x: 8, y: 8},
  health: 2000,
  name: 'crate',
  type: ig.Entity.TYPE.B,checkAgainst: ig.Entity.TYPE.A,
  animSheet: new ig.AnimationSheet( 'media/crate.png', 8, 8),
  init: function( x, y, settings ) {
    this.addAnim( 'idle', 1, [0] );
    this.parent( x, y, settings );
  }
});
});
  1. 这段代码非常简单,因为板条箱只是一个无生命的物体。尽管是一个坚固的无生命物体,因为它的健康值为2000。通过给予板条箱如此多的健康,它能够经受住多次炸弹爆炸。

  2. 保存文件并在 Weltmeister 中添加一些到你的游戏中。当然,在释放爆炸之前,试着堆叠几个板条箱。添加一个普通板条箱添加一个普通板条箱

现在我们有了标准的板条箱;制作一个治疗板条箱只需要几个步骤,因为我们将在普通板条箱的基础上构建它。

在看看我们的治疗板条箱之前,让我们快速看看我们是如何制作普通板条箱的:

  • 创建一个新文件并将其保存为crate.js

  • 实现标准的 Box2D 实体代码

  • 保存并使用 Weltmeister 向游戏中添加一些板条箱

实现一个治疗板条箱

现在我们有了基本的原型板条箱,我们只需要在其基础上构建,以创建健康板条箱。执行以下步骤来构建健康板条箱:

  1. 打开一个新文件并将其保存为crate.js

  2. 为其添加healthcrate特定的代码。健康板条箱是普通板条箱的扩展,不是一个 Box2D 实体;因此,我们只需要指出健康板条箱与普通板条箱的区别所在:

ig.module(
  'game.entities.healthcrate'
)
.requires('game.entities.crate'
).defines(function(){
EntityHealthcrate = EntityCrate.extend({
  name: 'healthcrate',
  animSheet: new ig.AnimationSheet( 'media/healthcrate.png', 8, 8),
  check: function(other){
    if(other.name == 'player'){
      other.health =  other.health + 100;
      this.kill();
    }
  }
})
});
  1. 它有另一个名称和动画表。此外,它将治疗玩家并在治疗玩家后销毁自己。

  2. 使用以下代码将板条箱添加到主脚本中,这样你的游戏就知道它在那里。

'game.entities.healthcrate'
  1. 保存并添加一个板条箱到游戏中以查看其效果。实现一个治疗板条箱

这个板条箱通过提供100健康值来治疗玩家,如下截图所示。因此,玩家的健康值总是比游戏开始时更高。这只是一个选择;你可以通过实现健康上限来改变这一点,以确保治疗不会使玩家比初始状态更强大。

实现一个治疗板条箱

记住你可以随时用带有 Firebug 附加组件的 Firefox 打开并查找文档对象模型DOM)中的玩家属性。在拾取板条箱之前,我们的玩家的健康值为 100,拾取后上升到 200。

治疗板条箱比普通板条箱要复杂一些。让我们再次看看我们制作治疗板条箱所采取的步骤:

  • 创建一个新文件并将其保存为healthcrate.js

  • 扩展先前构建的生命箱,而不是一个 Box2D 实体。只添加健康箱与原始箱不同的参数。这包括一个check()函数,用于查看玩家是否触摸到它。

  • 保存并添加一个生命箱到游戏中使用 Weltmeister。

  • 在 DOM 中检查您的生命箱是否实际增加了玩家的生命值。

保持得分

在游戏中跟踪分数是一件相当简单的事情。为了实现一个系统,在其中每次杀死一个敌人时都会保持并增加分数,我们需要三样东西:

  1. 我们需要一个在游戏本身范围内并且可以被视为某种开销变量的变量。
.defines(function(){
GameInfo = new function(){
 this.score = 0;
},
MyGame = ig.Box2DGame.extend({
  1. 这非常重要,因为正如我们将在第五章中看到的那样,为您的游戏添加一些高级功能,开始和结束屏幕实际上是正在加载的不同游戏。当新游戏加载到内存中时,旧游戏被丢弃,它的所有变量也被丢弃。这就是为什么我们需要一个存在于游戏之外的变量。

  2. 这个函数用于增加一定数量的分数。这个函数允许是游戏本身的一个方法。只需将其插入到MyGame文件的其他主要函数下面的主脚本中。

increaseScore: function(points){
  //increase score by certain amount of points
  GameInfo.score +=points;
}
  1. 我们覆盖了敌人的kill()函数,如所示,因此青蛙不仅死亡,而且还为我们提供了额外的分数。
  kill: function(){
    ig.game.increaseScore(100);
    this.parent();
  }

从现在开始,每当红蛙死亡时,我们都会得到额外的 100 分,并且这些分数会安全地保存在一个变量中,只要我们不刷新页面,它们就不会被删除。然后,我们可以稍后使用这个变量,在游戏结束时向我们的玩家提供一些反馈,告诉他表现得好还是差。

保持得分对于几乎任何游戏来说都是非常重要的组成部分。这是一种挑战玩家重玩游戏并在其中表现更好的方式。而且实现起来也不是太困难;让我们看看我们做了什么:

  • 在当前游戏之外创建一个变量,并将变量命名为score

  • 添加一个可以直接操作我们的score变量的游戏函数

  • 敌人死亡时调用该函数,将分数添加到整体玩家得分中。

从一个级别过渡到另一个级别

为了实现地图过渡,您首先需要第二个级别。您可以自己制作一个,或者从本章的可下载文件中复制一个。您还需要触发器结束levelchange实体。将这两个实体复制到entities文件夹中,并将名为level 2的级别复制到本地计算机上的levels文件夹中。或者,您可以自己设计第二个级别,并使用随 Impact 许可证提供的触发器实体。触发器实体不是实际引擎的一部分;它可以在 ImpactJS 网站的可下载示例中找到。

levelchange实体中,我们将进行以下代码更改:

ig.module(
  'game.entities.levelchange'
)
.requires(
  'impact.entity'
)
.defines(function(){ 
EntityLevelchange = ig.Entity.extend({
  _wmDrawBox: true,
  _wmBoxColor: 'rgba(0, 0, 255, 0.7)',
  _wmScalable: true,
  size: {x: 8, y: 8},
  level: null,
  triggeredBy: function( entity, trigger ) {
    if(this.level) { 
      varlevelName = this.level.replace(/^(Level)?(\w)(\w*)/, function( m, l, a, b ) {
        return a.toUpperCase() + b;
        });
      var oldplayer = ig.game.getEntitiesByType( EntityPlayer )[0];
      ig.game.loadLevel( ig.global['Level'+levelName] );
      var newplayer = ig.game.getEntitiesByType( EntityPlayer )[0];
      newplayer = oldplayer;
    }
  },
  update: function(){}
});
});

正如您可能注意到的那样,它与我们在 RPG 中使用的不同,主要有两个方面:

  • 它不考虑使用生成点。对于大多数横向卷轴游戏,实际上并不需要使用生成点。这是因为一旦完成了一个级别,您只能通过重新玩它来返回到它。因此,我们不需要每个级别多个生成点,只需要一个生成点。然而,如果我们只需要一个生成点,不使用我们在之前章节中使用的 Void 实体会更容易。相反,我们只需将玩家实体放在级别内的某个位置,级别将始终从那里开始。

  • levelchange实体的第二个更改是我们对玩家实体的备份。在加载关卡之前,我们将玩家实体复制到一个名为oldplayer的本地变量中。一旦游戏加载,就会创建一个新的可玩角色;这是我们手动添加到 Weltmeister 中的level 2。然后我们将这个新玩家分配给另一个名为newplayer的本地变量。通过用oldplayer覆盖newplayer,我们可以继续使用旧的青蛙进行游戏。如果玩家被允许保留先前获得的补充武器或生命值,这可能很重要。

现在我们所需要做的就是在level 1中正确设置triggerlevelchange实体,这样我们就有了一个体面的关卡过渡。应该按照以下步骤进行:

  1. 一旦triggerlevelchange实体出现在entities文件夹中,就将它们都添加到主脚本中。一旦你创建或复制了level 2,也将level 2添加到脚本中。
'game.levels.level2',
'game.entities.trigger',
'game.entities.levelchange'
  1. 使用 Weltmeister 将triggerlevelchange实体放入level 1

  2. 使用 Weltmeister 为levelchange实体添加一个值为tolevel2name属性和一个值为level2level属性。

  3. 使用 Weltmeister 为trigger实体添加一个名为target.1的属性,值为tolevel2

  4. 仔细检查你的第二个关卡中是否有一个玩家实体,并且这个关卡的名称是level2

  5. 保存你所做的所有更改,并重新加载游戏进行测试。一定要尝试在使用关卡过渡之前收集一个生命值箱。一旦你到达level2,你的生命值增加应该会持续。

如果你从可下载文件中复制了level2,请注意星星的移动速度比飞船慢,而飞船的移动速度又比其他一些飞船慢。这是因为这三个图层的距离。如果你打开 Weltmeister,你会发现星星图层的距离值为5,最接近的星船的值为2,其他飞船的值为3。使用距离可以为视差游戏带来非常好的效果;明智地使用它们。

从一个级别过渡到另一个级别

如果只是单向进行关卡过渡,那么添加关卡过渡可以相对容易地完成。让我们回顾一下我们是如何做到这一点的:

  • 复制triggerlevelchange实体。

  • 构建或复制一个名为level2的第二个关卡。确保在关卡中添加一个玩家实体。

  • 在主脚本中包括新的关卡和triggerlevelchange实体。

  • level 1中添加一个triggerlevelchange实体,连接它们,并确保levelchange实体指向level2

  • 在设计关卡时,尝试使用图层的distance属性。这可以在横向滚动游戏中给你美丽的结果。

最后的战斗

每个好游戏都以一个具有挑战性的最终战斗结束,善良战胜邪恶,或者反之,由你决定。

为了进行一场具有挑战性的战斗,让我们创建一个单独的boss实体,比我们其他的青蛙更强大。

  1. 新建一个文件并将其保存为boss.js

  2. boss 将是我们正常敌人的扩展,所以让我们首先定义他与红色青蛙不同的特征。

ig.module(
  'game.entities.boss'
)
.requires(
  'game.entities.enemy'
)
.defines(function(){
  EntityBoss = EntityEnemy.extend({
  name: 'boss',
  size: {x: 32, y:48},
  health: 200,
  animSheet: new ig.AnimationSheet( 'media/Boss.png', 32,48 )
});
});
  1. 他的名字不同;但更重要的是,他的生命值更多,比其他青蛙要大得多。

  2. 使用以下代码将 boss 添加到你的主脚本中:

'game.entities.boss'
  1. 保存所有更改并将 boss 放入你的一个关卡中。

我们现在确实有一个更大的敌人,生命值更多,基本上做的事情和较小的一样。这并不会让 boss 战变得有趣,所以让我们赋予他像玩家一样发射子弹的能力。我们需要一个单独的子弹实体,因为我们的基本抛射物只能伤害 B 类型实体,而我们的玩家是 A 类型;另外我们可能希望它看起来有点不同:

  1. 新建一个文件并将其保存为bossbullet.js

  2. 这颗子弹将是普通子弹的直接扩展,除了类型检查和外观方式。编写以下代码来创建新的子弹实体:

ig.module(
  'game.entities.bossbullet'
)
.requires(
  'game.entities.projectile'
)
.defines(function(){
  EntityBossbullet = EntityProjectile.extend({
  name: 'bossbullet',
  checkAgainst: ig.Entity.TYPE.A,
  animSheet: new ig.AnimationSheet( 'media/bossbullet.png',8, 4 )
  });
});
  1. 我们需要进行最后一个修改,如下所示的代码,让 boss 发射自己的子弹:
update: function(){
  var players = ig.game.getEntitiesByType('EntityPlayer');
  var player = players[0];
  // both distance on x axis and y axis are calculated
  var distanceX = this.pos.x - player.pos.x;
  var sign = Math.abs(distanceX)/distanceX;
  var distanceY = this.pos.y - player.pos.y;
  //try to move without flying, fly if necessary
  if (Math.abs(distanceX) < 1000 &&Math.abs(distanceX)>100){
    var fY = distanceY> 0 ? -350: 0;
    this.body.ApplyForce( new b2.Vec2(sign * -50,fY),this.body.GetPosition() );
    if(distanceX>0){this.flip = true;}
    else {this.flip = false;}
    if (Math.random() > 0.9){
      var x = this.pos.x + (this.flip ? -6 : 6 );
      var y = this.pos.y + 6;
      ig.game.spawnEntity( EntityBossbullet, x, y,{flip:this.flip} );
    }
    if(distanceY>= 0){this.currentAnim = this.anims.fly;}
    else{this.currentAnim = this.anims.idle;}
  }
  else if (Math.abs(distanceX) <= 100){
    if(Math.random() > 0.9){
      var x = this.pos.x + (this.flip ? -6 : 6 );
      var y = this.pos.y + 6;
      ig.game.spawnEntity( EntityBossbullet, x, y,{flip:this.flip} );
    }
  }
  this.body.SetXForm(this.body.GetPosition(), 0);
  if (distanceX> 0){this.currentAnim.flip.x = true;}
  else{this.currentAnim.flip.x = false;}
  this.cooldowncounter ++;
  this.parent();
}
  1. boss 实体的update()函数与其他实体有三个主要区别:
  • 由于他是一个更大的生物,他需要更多的力量来移动。

  • 我们希望他用子弹造成伤害,这样他就不会试图进入近战范围。当他在 x 轴上的距离为 1000 像素时,他会接近。一旦距离为 100 像素,他就不会再靠近了。

  • 最后但并非最不重要的是,在每一帧中,他有 1/10 的几率发射一颗子弹。这平均每秒应该会导致 6 颗子弹,这是相当密集的火力。如果你非常不幸,他可以在一秒内向你发射多达 60 颗子弹。

Box2D 碰撞的一个相当好的效果是,作为玩家,你自己的子弹可以偏转 boss 的子弹。然而,这并不总是这样。Box2D 中的碰撞检测还不完美,有时两个实体可以直接穿过彼此。这也是为什么你应该确保你的外边界碰撞墙非常厚。否则,你的实体可能会飞出你的关卡,可能导致游戏崩溃。

最终战斗

击败 boss 角色应该结束游戏,并给玩家一个漂亮的胜利画面。死亡应该以游戏结束画面而不是游戏崩溃画面结束。这些以及许多其他事情将在第五章中得到解决,为您的游戏添加一些高级功能,在那里我们将更深入地研究一些更高级的功能,以增强您的游戏。

当游戏接近尾声时,玩家期望有一个高潮。这可以通过与一个值得的敌人进行一场史诗般的战斗来给他。这正是我们在本章早些时候所做的。boss 角色是玩家的终极敌人,也是他取得胜利的关键:

  • 打开一个新文件并将其保存为boss.js

  • 将 boss 角色的基本功能作为敌人实体的扩展。

  • 引入 boss 的子弹,也就是 boss 用来杀死玩家的抛射物。这是玩家自己使用的抛射物的扩展。

  • 升级 boss,使他能够利用他的致命新子弹。

  • 在游戏中添加一个 boss 并查看你是否能击败他。

总结

在本章中,我们了解了横向卷轴游戏,并看了一些著名的例子。我们使用了集成了 ImpactJS 的物理引擎 Box2D 构建了自己的横向卷轴游戏。

首先,我们使用 Weltmeister 建立了一个关卡,这样我们就可以用我们新创建的敌人和可玩角色来填充它们。我们添加了无生命的箱子,以完全展示 Box2D 的物理效果。为了武装玩家对抗暴力敌人,我们引入了拾取物品和两种有趣的武器,即子弹和炸弹。

我们的敌人在我们添加了轻微的人工智能后获得了生命。作为玩家的最终挑战,强大的 boss 被带到了场景中。这个敌人比普通敌人更强大,能够像玩家一样发射子弹。为了击败每个敌人,玩家将获得额外的积分。

在下一章中,我们将探讨一些新概念,比如处理数据,并深入一些我们已经接触过的功能,比如调试人工智能。

第五章:为您的游戏添加一些高级功能

在之前的章节中,我们看到了如何设置工作环境,看了 Impact 引擎,甚至构建了两种类型的游戏。现在是时候看一些有趣的额外内容了。

为了测试本章涵盖的元素,最好要么下载第五章文件夹中的代码材料,要么直接在我们设计的游戏中构建第三章中的游戏,让我们建立一个角色扮演游戏。由于本章我们不会使用 Box2D 扩展,一些东西将与第四章中的侧面卷轴游戏不兼容,让我们建立一个侧面卷轴游戏。在本章中,我们将涵盖:

  • 制作开始和胜利画面

  • 额外的调试可能性和引入定制的 ImpactJS 调试面板

  • 使用 cookie 和 lawnchair 应用程序保存数据,并将 Excel 文件转换为有用的游戏数据

  • 在第三章的角色扮演游戏(RPG)中的一些额外游戏功能,让我们建立一个角色扮演游戏

  • 通过鼠标移动角色

  • 智能生成位置

  • 添加基本对话

  • 显示玩家的生命值条

  • 通过集体智慧扩展人工智能(AI)

  • 实施 Playtomic 进行游戏分析

开始和游戏结束画面

当玩家开始游戏时,你可能希望他看到的第一件事是一个闪屏。这个屏幕通常包含游戏的名称和其他有趣的信息;通常包含一些关于游戏故事或控制的信息。在游戏结束时,你可以有一个胜利画面,告诉玩家他在排行榜上的得分有多高。

在代码方面,可以通过在实际游戏旁边引入新的游戏实例来实现。每个屏幕:开始、游戏结束和胜利都是 ImpactJS 游戏类的直接扩展。让我们首先创建一个开始画面。

游戏的开始画面

为了制作一个漂亮的开场画面,我们需要一个背景图片和我们信任的main.js脚本:

  1. 打开main.js脚本并插入以下代码:
OpenScreen = ig.Game.extend({
  StartImage : new ig.Image('media/StartScreen.png'),
  init:function(){
  if(ig.ua.mobile){
    ig.system.setGame(MyGame);
  }
    ig.input.bind(ig.KEY.SPACE,'LoadGame');
  },
  init:function(){
    if(ig.ua.mobile){ig.input.bindTouch( '#canvas','LoadGame' );}
    else {ig.input.bind(ig.KEY.SPACE,'LoadGame');}
  },
  1. 开场画面是ig.Game函数的扩展,就像我们的游戏一样。事实上,当我们完成这里的工作后,我们将有四个游戏实例:一个真正的游戏称为MyGame,另外三个游戏,它们只是作为开始、胜利或游戏结束画面。这可能有点反直觉,因为你可能期望这些画面是同一个游戏的一部分。实际上,这绝对是真的。然而,在代码中,将这些画面转换为单独的游戏类扩展更方便。

  2. OpenScreen代码的这一部分中,我们首先定义了我们将要显示的图像:StartScreen.png

  3. 最后,我们将空格键绑定到一个名为LoadGame的动作状态,如下所示:

  update:function(){
    if(ig.input.pressed('LoadGame')){
      ig.system.setGame(MyGame);
    }
  },
  1. 现在我们可以通过按空格键加载游戏,但我们仍然需要在屏幕上实际显示一些东西。

  2. 我们可以通过操纵任何 ImpactJS 类的draw()函数来可视化事物,如下面的代码片段所示:

  draw: function(){
    this.parent();
    this.StartImage.draw(0,0);
    var canvas = document.getElementById('canvas');
    if(canvas.getContext){
      var context = canvas.getContext('2d');
      context.fillStyle = "rgb(150,29,28)";
      context.fillRect (10,10,100,30);
    }
    var font = new ig.Font('media/font.png');
    font.draw('player:' + GameInfo.name,10,10);
  }
}),
  1. draw()函数将绘制我们在初始化OpenScreen函数时指定的背景图像。这样做后,它还会添加一个小的红色矩形,我们将在其中打印玩家的名字(如果有的话)。我们将在本章后面查看游戏数据时,获取这个名字并存储它以供以后使用。目前,GameInfo.name变量是未定义的,将会像开始新游戏一样显示出来。

  2. 为了确保我们全新的开场画面实际上被使用,我们需要在我们的ig.main函数调用中用OpenScreen函数替换MyGame游戏类实例,如下面的代码行所示:

ig.main( '#canvas', OpenScreen, 60, 320, 240, 2 );

现在我们有了一个开场画面!添加游戏结束画面和胜利画面的过程非常相似。在制作这些其他画面之前,让我们快速回顾一下我们刚刚做的事情:

  • 我们确保media文件夹中有背景图像

  • 我们添加了OpenScreen函数作为一个新的游戏实例

  • 我们绑定了空格键,以便用来加载实际游戏

  • 我们设置了Draw()函数,以便它可以显示背景,甚至以后还可以显示玩家的名字

  • 我们在OpenScreen函数窗口中初始化了我们的画布,而不是在MyGame游戏类实例中

胜利和游戏结束画面

胜利画面是游戏实体的一个相对简单的扩展。对于我们想要显示的每种类型的画面,该过程几乎是相同的。要设置胜利画面,请按照以下步骤进行:

  1. 打开game.js文件,并添加我们的新GameEnd游戏类,如下所示:
GameEnd = ig.Game.extend({
  EndImage : new ig.Image('media/Winner.png'),

  init:function(){
    if(ig.ua.mobile){ig.input.bindTouch( '#canvas','LoadGame' );}
    else {ig.input.bind(ig.KEY.SPACE,'LoadGame');}
  },
  1. 我们需要初始化的是我们将要显示的图像和一个用于重新开始游戏的键。

  2. 与开始画面类似,我们使用空格键加载新游戏。我们通过在update函数中添加以下if语句来不断检查空格键是否被按下:

  update:function(){
    if(ig.input.pressed('LoadGame')){
      ig.system.setGame(MyGame);
    }
  },
  1. 我们需要使用以下代码绘制实际的游戏结束图像,并放置文本HIT SPACE TO RESTART。这样我们就确保玩家不会刷新浏览器而是使用空格键。
  draw: function(){
    this.parent();
    var font = new ig.Font('media/font.png');
    this.StartImage.draw(0,0);

  if(ig.ua.mobile){
    font.draw('HIT THE SCREEN TO RESTART:',100,100);
  }
else font.draw('HIT SPACE TO RESTART:',100,100);
  }
}),
  1. 当玩家到达游戏结束时,需要显示胜利画面。在我们的情况下,这将是当 boss 实体被击败时。打开boss.js文件,并按照以下代码更改kill()方法,以便在他死亡时加载胜利画面:
kill: function(){
  ig.game.gameWon();
}
  1. kill()方法中,我们调用了gameWon()函数,这是我们当前游戏的一个方法,但尚未定义。

  2. 打开game.js文件,并将gameWon()方法添加为MyGame文件的一个新方法,如下所示。

gameWon: function(){
  ig.system.setGame(GameEnd);
}
  1. 目前,引入一个额外的中间函数来调用胜利画面可能看起来有点无聊。然而,一旦我们开始处理游戏数据,这将开始变得有意义。最终,这个函数不仅会调用胜利画面,还会保存玩家的得分。使用中间函数比直接将ig.system.setGame()函数添加到玩家实体中是一种更干净的编程方式。

注意

游戏结束画面可以是胜利画面的确切等价物,只是使用另一张图像,并且是由玩家的死亡而不是 boss 的触发。

  1. 如下所示,在game.js文件中添加gameOver函数:
gameOver = ig.Game.extend({
  gameOverImage : new ig.Image('media/GameOver.png'),
  init: function(){
    ig.input.bind(ig.KEY.SPACE,'LoadGame');
  },
  update:function(){
    if(ig.input.pressed('LoadGame')){
      ig.system.setGame(MyGame);
    }
  },
  draw: function(){
    this.parent();
    var font = new ig.Font('media/font.png');
    this.gameOverImage.draw(0,0);
    font.draw('HIT SPACE TO RESTART',150,50);
  }
}),
  1. 通过使用以下代码调整他的kill()方法,确保gameOver函数在玩家死亡时被触发:
kill: function(){
    ig.game.gameOver();
}
  1. 再次调用中间函数来处理实际画面加载。这个函数需要作为MyGame游戏类实例的一个方法添加。

  2. game.js脚本中,将gameOver()方法添加到MyGame游戏类实例中,如下所示:

gameOver: function(){
  ig.system.setGame(gameOver);
},

这些都是非常基本的开始和游戏结束画面,它们表明可以通过使用ig.game类作为起点来完成。对于胜利和游戏结束画面,一个好主意是显示排行榜或在游戏过程中收集的其他有趣信息。

当游戏通过添加高级功能变得更加复杂时,调试变得越来越重要,以应对这些增加的复杂性。我们现在将看看我们可以使用哪些高级调试选项。然而,在我们这样做之前,让我们快速回顾一下胜利和游戏结束画面:

  • 我们制作了两个新的游戏实例,作为胜利和游戏结束画面

  • update函数被调整以监听空格键,而draw函数被调整以显示背景图像和HIT SPACE TO RESTART消息

  • 老板和玩家实体的功能被调整以触发胜利和游戏结束屏幕

  • 我们使用了名为gameOver()gameWon()的中间函数,因为我们希望稍后调整它们,以便触发 lawnchair 应用程序来存储分数

更高级的调试选项

在第一章中,启动你的第一个 Impact 游戏,我们看了如何使用浏览器进行调试以及 ImpactJS 调试面板提供了什么。在这里,我们将进一步制作一个新的 ImpactJS 调试面板。这段代码由 Dominic 在他的 ImpactJS 网站上提供,但很多人忽视了这个功能,尽管它非常有用。

在第一章中,启动你的第一个 Impact 游戏,我们还谈到了逻辑错误,这是一种非常难以找到的错误,因为它不一定会在浏览器调试控制台中生成错误。为了应对这些错误,程序员经常使用一种称为单元测试的方法。基本上,这涉及到预先定义每段代码的期望结果,将这些期望结果转化为条件,并测试输出是否符合这些条件。让我们看一个简短的例子。

单元测试的简短介绍

我们的 ImpactJS 脚本中最基本的组件之一是函数。我们的一些函数返回值,其他函数直接改变属性。假设我们有一个名为dummyUnitTest()的函数,它接受一个参数:functioninput

dummyUnitTest: function(inputnumber){
  var outputnumber= Math.pow(inputnumber,2);
  return null; // can cause an error in subsequentfunctions,comment out to fix it
  return outputnumber;
}

inputnumber变量可以是任何数字,但我们的函数将inputnumber变量转换为outputnumber变量,然后返回它。inputnumber变量的平方应该始终返回一个正数。所以我们至少可以说两件事关于我们对这个函数的期望:输出不能为 null,也不能为负数。

我们可以通过添加专门用于检查特定条件的assert函数来对这个函数进行单元测试。assert函数检查一个条件,当条件为假时,它会将消息写入控制台日志。控制台元素本身具有这个函数,当调试模块被激活时,ImpactJS 也有这个函数。ig.assert()函数是Console.assert()函数的 ImpactJS 等价物。记住,通过在main.js文件中包含'impact.debug.debug'来激活 ImpactJS 调试。使用ig.assert函数优于console.assert()函数。这是因为在准备启动游戏时,通过简单地关闭 ImpactJS 调试模块来摆脱ig类消息。控制台类的方法,如console.assert()调用需要单独关闭。一般来说,assert()函数看起来像这样:

ig.assert(this.dummyUnitTest('expected')==='expected','you introduced a logical error you should retrieve the same value as the input');

对于我们的具体示例,我们可以执行几个测试,如下所示的代码:

ig.assert(typeof argument1 === 'number','the input is not a number');
ig.assert(typeof argument2 === 'number','the output is not a number');
ig.assert(typeof argument2 >= 0,'the output is negative');
ig.assert(typeof argument2 != null,'the output is null);

我们可以继续,这种方法并不是没有过度的缺陷。但一般来说,当你计划构建一个非常复杂的游戏时,单元测试可以通过减少你寻找逻辑错误源的时间来极大地帮助你。例如,在这种情况下,如果我们的输出是一个负数,函数本身不会失败;也许大部分依赖于这个函数的代码也不会失败,但在链条的某个地方,会有问题。在引入所有这些依赖关系的同时,一个函数建立在另一个函数之上,依此类推,单元测试是完全合理的。

ig.assert()ig.log()函数旁边还有另一个有趣的函数。它是console.log()函数的 ImpactJS 等价物,将始终写入日志,而不检查特定条件。这对于在不必在文档对象模型DOM)中寻找的情况下关注敌人的健康状况非常有用。

让我们在继续使用我们自己的 ImpactJS 调试面板之前,快速回顾一下单元测试的内容:

  • 单元测试是关于预见您期望代码组件执行的操作,并返回和检查输出的有效性。

  • 我们使用ig.assert()console.assert()函数来检查某些条件,并在违反条件时向日志打印消息。

将您自己的调试面板添加到 ImpactJS 调试器

如前所述,通过简单地在main.js文件中包含'impact.debug'语句来激活调试面板。开始新游戏时,面板会最小化显示在屏幕底部,只需点击即可完全显示。

让我们开始构建我们自己的面板,这将使我们能够在玩游戏时激活和停用实体。这样我们就可以在游戏中毫无阻碍地通过最凶猛的敌人,通过冻结它们的位置。让我们开始吧:

  1. 打开一个新文件,将其保存为MyDebugPanel.js

  2. 在文件中插入以下代码:

ig.module(
  'plugins.debug.MyDebugPanel'
)
.requires(
  'impact.debug.menu',
  'impact.entity',
  'impact.game'
)
.defines(function(){
ig.Game.inject({
  loadLevel: function( data ) {
    this.parent(data);
    ig.debug.panels.fancypanel.load(this);
  }
})
})
  1. 在我们实际定义面板之前,我们将在两个 ImpactJS 核心类中注入代码:GameEntity。注入代码就像扩展一样,只是我们不创建一个新类。原始代码被其扩展版本所替换。在前面的代码中,我们告诉核心loadlevel()函数也要加载我们的面板,这将被称为Fancy panel

  2. 然后,通过在核心实体代码中添加一个新属性_shouldUpdate来升级,如下所示:

ig.Entity.inject({
  _shouldUpdate: true,update: function() {if( this._shouldUpdate ) {this.parent();}
  }
});
  1. 当为 true 时,实体的update方法将被调用,这也是默认方法。但是,当为 false 时,update()函数将被绕过,并且实体不会执行任何实际操作。

  2. 现在让我们来看看面板本身。我们可以看到面板中包含以下代码:

MyFancyDebugPanel = ig.DebugPanel.extend({
  init: function( name, label ) {
    this.parent( name, label ); 
    this.container.innerHTML = '<em>Entities not loadedyet.</em>';
  },
}
  1. 我们的花哨面板被初始化为 ImpactJS 面板的扩展,称为DebugPanel。调用this.parent函数将确保向面板提供一个 DIV 容器,以便它可以在 HTML5 中显示。如果游戏中没有实体,容器将不包含任何内容,因此会放置一条消息。例如,这将是我们的开始和结束屏幕的情况。由于this.container.innerHTML函数将保存面板的内容,因此在开始屏幕中打开面板应该会显示消息Entities not loaded yet

为了显示先前的消息,我们应该在this.container.innerHTML函数中添加以下代码:

load: function( game ) {
  this.container.innerHTML = '';
    for( var i = 0; i < game.entities.length; i++ ) {
      var ent = game.entities[i];
      if( ent.name ) {
        var opt = new ig.DebugOption( 'Entity ' + ent.name, ent,'_shouldUpdate' );
        this.addOption( opt );
        this.container.appendChild(document.createTextNode('health: '+ ent.name + ' :' +ent.health));
      }
    }
},
  1. 在加载级别时,我们的面板将填充游戏中的所有实体,并提供关闭它们的update()函数的选项。还会显示它们的健康状况。addOption()函数使得可以在需要时从 true 切换到 false,并反之。它接受两个参数:一个标签和需要在 true 和 false 之间交替的变量。

  2. 这些最后的函数并没有用于我们特定的面板,但仍然很有用。以下代码解释了先前的函数:

ready: function() {
  // This function is automatically called when a new gameis created.
  // ig.game is valid here!
},
beforeRun: function() {
  // This function is automatically called BEFORE eachframe is processed.
},
afterRun: function() {
  // This function is automatically called AFTER each frameis processed.
}
});
  1. load()ready()beforeRun()afterRun()函数之间的主要区别在于它们在游戏中被调用的时刻。根据您的需求,您将使用一个,另一个或者组合。我们使用了load()方法,它在加载级别时被调用。但对于其他面板,您可能希望使用其他方法。

  2. 最后一步,我们实际上将定制面板添加到我们的标准面板集中,如下所示:

ig.debug.addPanel({
  type: MyFancyDebugPanel,
  name: 'fancypanel',
  label: 'Fancy Panel'
});
  1. 重新加载游戏,看看您的新面板。尝试冻结您的敌人!您会注意到敌人仍然会面对玩家,但不会朝向他移动。这是因为我们禁用了它们的update()方法,但没有禁用它们的draw()方法。

现在我们将继续使用游戏数据,但让我们首先看一下我们刚刚涵盖的内容:

  • ImpactJS 有一个非常有趣的调试器,您可以设计自己的面板。

  • 通过在主脚本中包含'impact.debug.debug'命令来激活 ImpactJS 调试器。

  • 我们通过扩展 ImpactJS 的DebugPanel类制作了自己的面板。我们自己的面板需要让我们能够将任何实体冻结在位置上,这样我们就可以无阻碍地探索我们的关卡。

  • 利用一种称为注入的技术;我们改变了我们的核心实体类,以便调试面板可以控制每个实体的update函数。

  • 最后,我们将我们的调试面板添加到标准设置中,以便随时可用。

处理游戏数据

处理数据对于游戏构建可能是至关重要的。简单的游戏不需要显式的数据管理。然而,当我们开始研究那些包含对话或保持高分的游戏时,理解数据处理就成为一个重要的话题。我们将讨论两件事:

  • 将数据引入游戏

  • 存储在游戏中生成的数据

对于后者,我们将看看解决问题的两种不同方式:cookie 和 lawnchair 应用程序。

首先让我们看看如果我们想要在 NPC 和玩家之间的对话中引入数据,我们需要做些什么。

向游戏添加数据

如前所述,RPG 游戏通常充满了玩家和多个非玩家角色(NPC)之间的对话。在这些对话中,玩家在回答时会有几个选项。这方面的代码机制可能会变得非常复杂,我们将在本章后面详细介绍,但首先我们需要实际的句子。我们可以在诸如 Excel 之类的应用程序中准备这些句子。

向游戏添加数据

设置 RPG 对话是一门艺术;有许多方法可以做到这一点,每种方法都有其优缺点。创建一个体面的对话设置和流程,甚至是数据库方面的,是一个超出本书范围的讨论。在这里,我们将尽量简单,并与两个表一起工作:一个用于 NPC 可以说的所有事情,另一个用于玩家可以回答的事情。我们游戏中对话的流程将如下:

  1. NPC 说了些什么。NPC 可以说的一切都有一个名为NPC_CONVO_KEY的唯一键。

  2. 玩家将被呈现一组可能的答案。每组都有一个名为REPLY_SET_KEY的键。除此之外,虽然我们不会使用它,但每个答案都有自己的唯一键,我们称之为UNIQUE_REPLY_KEY。即使你现在不使用它们,拥有主键也是一个很好的做法。

  3. 玩家选择其中一个答案。答案有一个外键,指向 NPC。我们将这个外键命名为NPC_CONVO_KEY

  4. 使用NPC_CONVO_KEY,NPC 知道接下来该说什么,我们已经完成了循环。这将继续进行,直到对话被突然中止或自然结束。

实际的句子保存在变量PC_SPEECHNPC_SPEECH中。

我们可以在 Excel 文档中轻松准备我们的数据,但我们仍需要将其导入到我们的游戏中。我们将使用转换器,例如以下网站上的转换器:shancarter.com/data_converter/

只需将数据从 Excel 复制粘贴到转换器中,并选择JSON-Column Arrays,即可将数据转换为 JSON 格式文档。

一旦以这种格式存在,我们所需要做的就是将数据复制粘贴到单独的模块中。以下代码是我们的 Excel 数据转换为 JSON 后的样子:

ig.module('plugins.conversation.npc_con')
.defines(function(){
npc_con=/*JSON[*/{
  "NPC_CONVO_KEY":[1,2,3,4,5,6,7],
  "NPC_SPEECH":["Hi, are you allright?","That is great! Bye now!","Ow, why? What is wrong?","You are mean!","Ow. You should see the doctor, he lives in the green house a bitfurther. Good luck!","Please explain. Maybe I can help you?","Bye!"],
  "REPLY_SET_KEY":[1,0,3,0,0,6,0]
}
});

我们将数据以 JSON 格式存储,就像 Weltmeister 对级别文件所做的那样。以下代码是玩家的语音数据转换为 JSON 后的样子:

ig.module( 'plugins.conversation.pc_con' )
.defines(function(){
pc_con=/*JSON[*/{
  "UNIQUE_REPLY_KEY":[1,2,3,4,5,6,7,8],
  "REPLY_SET_KEY":[1,1,1,3,3,3,6,6],
  "PC_SPEECH":["Yes","No","Go away","I am sick","I am sick of you","You know, stuff.","I will be fine! Bye!","Get lost! "],
  "NPC_CONVO_KEY":[2,3,4,5,4,6,7,4]
}
});

现在剩下的就是将数据放入我们的游戏目录,并在main.js文件中包含这两个文件:

'plugins.conversation.npc_con',
'plugins.conversation.pc_con',

如果您重新加载游戏,您应该能够在 Firebug 应用程序中探索您新引入的数据,如下面的屏幕截图所示:

向游戏添加数据向游戏添加数据

现在我们已经看了如何引入数据,让我们来看一下两种在玩家计算机上存储数据的方法,首先是使用 cookie。但首先让我们总结一下我们在这里所做的事情:

  • 设置对话是一门艺术,本章不会深入探讨

  • 我们在 Excel 或等效应用程序中设置了一个简单的对话

  • 这个 Excel 表格被转换为 JSON 格式的文档。您可以使用在线转换器来做到这一点,比如shancarter.com/data_converter/

  • 我们将新的 JSON 编码数据转换为 ImpactJS 模块

  • 最后,我们在我们的主脚本中包含了这两个新创建的数据模块

Cookie 不过是存储在浏览器中的一段字符串数据,许多网站用它来跟踪访问者。如果您使用 Google Analytics,您可能知道 Google 提供了一个脚本,为每个访问者放置了几个不同的 cookie。Google Analytics 并不是唯一以这种方式工作的程序。在一天愉快的上网之后,您的浏览器中充满了 cookie;其中一些将在几个月内保留,直到最终删除自己。

在用户的浏览器中存储玩家姓名和最高分等信息是有意义的;您不需要从您这边进行存储,因此不需要 PHP 或 SQL 编码。缺点是如果玩家决定清理浏览器,数据将丢失。此外,在使用 cookie 时与玩家之间没有真正的一对一关系。一个人可以有多个设备,甚至每个设备可以有多个浏览器。因此,建议对您总是从头开始重玩的游戏使用 cookie。对于需要玩家大量时间投入的游戏来说,这显然不适用;例如,大型多人在线角色扮演游戏(MMORPGs)往往是如此。对于这些更高级的游戏,使用帐户和服务器端数据库是正确的方式。

让我们按照以下步骤构建一个能够存储玩家姓名的 cookie 插件,这样我们可以在重新开始游戏时检索它:

  1. 打开一个新文件,将其保存为cookie.js。插入基本的类扩展代码如下:
ig.module('plugins.data.cookie').
  defines(function(){
    ig.cookie = ig.Class.extend({
    userName : null,
    init: function(){
      this.checkCookie();
  },
  1. 我们首先将我们的 cookie 插件定义为 ImpactJS 类扩展。我们知道它以后将需要存储用户名,所以让我们用值null来初始化它。我们的新 DOM 对象创建时,第一件事就是调用checkCookie()函数。checkCookie()函数将检查是否已经存在存储了相同用户名的 cookie。当然这里有两种可能性:存在或不存在。如果不存在,需要提示并存储名称。如果用户名以前已存储,可以检索出来。

  2. 将 cookie 放置在位置上是使用setCookie()函数完成的,如下面的代码所示:

setCookie: function(c_name,value,exdays){
  var exdate=new Date();
  exdate.setDate(exdate.getDate() + exdays);
  var c_value=escape(value) + ((exdays==null) ? "" : ";expires="+exdate.toUTCString());
document.cookie=c_name + "=" + c_value;
},
  1. 这个函数接受三个参数:
  • c_name:它需要存储的变量的名称,即用户名

  • value:用户名的值

  • exdays:cookie 允许存在的天数,直到它应该从浏览器中删除自己

  1. setcookie()函数用于检查输入数据的有效性。该值被转换,因此业余黑客更难插入有害代码而不是名称。然后将数据存储在document.cookie变量中,这是 DOM 的一部分,它存储所有 cookie,并在关闭页面时不会丢失。深入研究document.cookie变量的工作方式将使我们走得太远,但它的行为非常奇特。如前面的代码片段所示,将值分配给document.cookie变量不会用新分配的值替换已经存在的值。相反,它将添加到堆栈的其余部分。

  2. 如果有setCookie()函数,当然也有getCookie()函数,如下面的代码片段所示:

getCookie: function(c_name){
  var i,x,y,ARRcookies=document.cookie.split(";");
  for (i=0;i<ARRcookies.length;i++){
    x=ARRcookies[i].substr(0,ARRcookies[i].indexOf("="));
    y=ARRcookies[i].substr(ARRcookies[i].indexOf("=")+1);
    x=x.replace(/^\s+|\s+$/g,"");
    if (x==c_name){
      return unescape(y);
    }
  }
},
  1. 前面的代码将解码转换后的 cookie 并返回它。它的唯一输入参数是您要查找的变量的名称。

  2. 在编程中,特别是在 Java 中,很常见使用setget函数的组合来更改属性。因此,根据这种编程逻辑,例如health属性应该始终具有setHealth()getHealth()函数。直接更改参数有优点和缺点。直接更改属性的主要优点是实用主义;事情保持简单和直观。一个很大的缺点是维护代码的有效性的挑战。如果任何地方都可以随意更改任何实体的任何属性,如果失去了对事物的视野,就会出现严重问题。

  3. checkCookie()函数通过使用getCookie()函数检查浏览器中是否存在用户名:

checkCookie :function(){
  var username=this.getCookie("username");
  if (username!=null && username!=""){
  this.setUserName(username);
  }
  else {
    username=prompt("Please enter your name:","");
    if (username!=null && username!=""){
      this.setCookie("username",username,365);
    }
  }
},
  1. 如果存在 cookie,则使用获取的用户名作为输入参数调用setUserName()函数。如果没有 cookie,则提示玩家插入他/她的名字,然后使用setCookie()函数存储。

  2. getUserName()setUserName()函数在本示例中保持相对基本,如下面的代码所示:

getUserName: function(){
  return this.userName;
},
setUserName: function(userName){
  if(userName.length > 10){alert("username is too long");}
  else { this.userName = userName; }
}
  1. setUsername()getUsername()函数可以通过直接使用checkCookie()setCookie()函数来获取或设置this.username命令来省略。然而,正如前面所说的,使用setget语句是一种良好的编程实践,无论何时需要更改属性。正如在setUserName()函数中所看到的,这些函数可以内置一些额外的检查。虽然getCookie()setCookie()函数确保数据以无害的方式存储和适当获取,但setUserName()getUserName()函数可以用于检查其他约束,例如名称长度。

  2. 现在我们已经完成了我们的 cookie 扩展,我们实际上可以利用它。打开main.js文件,并将以下两行添加到GameInfo类中:

this.cookie = new ig.cookie();//ask username or retrieve ifnot set
this.userName = this.cookie.getUserName();//store theusername
  1. GameInfo类非常适合这个;我们希望在游戏实例之外保持可用的所有内容都需要在GameInfo类中收集。尽可能将数据组件与游戏逻辑分离是保持代码清晰和易于理解的一种方式,当游戏变得更加复杂时。

  2. 我们的第一行代码将创建一个ig.cookie数组,并立即检查用户名是否存在。如果不存在,将出现提示,并在玩家填写提示警报后存储该名称。

  3. 第二行简单地将用户名传递给我们在第三章中首次遇到的GameInfo对象,让我们建立一个角色扮演游戏。您可能还记得,我们在本章的开头使用了GameInfo.name变量,但它是未定义的。现在它将被设置为null,直到玩家给出他的名字,并且以后用于他玩的每个游戏。使用 cookie 在玩家的计算机上存储数据

最初,玩家的名字将是未知的,并且在屏幕上将显示null,如前一个截图所示。

使用 cookie 在玩家的计算机上存储数据

然而,玩家被提示在窗口中填写他或她的名字,如前一个截图所示。

使用 cookie 在玩家的计算机上存储数据

因此,真实姓名将如前一个截图所示地显示在屏幕上。

虽然您应该能够绕过使用 cookie,但还有另一种存储数据的方式,可能更多功能和易于使用:lawnchair。lawnchair 应用程序利用 HTML5 本地存储,也称为 DOM 存储。在转向 lawnchair 应用程序之前,我们将快速了解如何在不使用 lawnchair 应用程序的情况下使用 HTML5 本地存储:

  • Cookie 是一种在玩家浏览器中存储数据的方式。许多网站使用它们,包括网络分析平台 Google Analytics。Cookie 对于短时间内反复玩的游戏很有用,而不适用于需要长时间存储许多东西的复杂游戏。

  • 我们可以通过创建一个cookies插件来实现使用 cookie。一旦激活了这个插件,它将检查是否已经存在 cookie,如果没有找到,则放置一个。

  • 在这个例子中,我们使用 cookie 来存储和检索玩家的名字,如果没有 cookie,我们首先要求他填写。

  • 重点放在使用set()get()函数上。这些函数是 Java 中的标准做法,是一种有用的技术,可以在代码中保持对事物的视野,并检查任何属性的有效性,即使代码变得更加复杂。

本地存储

本地存储,也称为 DOM 存储,是 HTML5 的一个功能,允许您在用户的计算机上保存信息。它几乎在所有方面都优于 cookie,但是旧版浏览器不支持它。使用本地存储相当简单,如下面的代码片段所示:

ig.module('plugins.data.local').
defines(function(){
  ig.local = ig.Class.extend({
    setData: function(key, data){
      localStorage.setItem(key, data);
    },
    getData: function(key){ 
      return localStorage.getItem(key);
    }
  });
})

这个插件并不是必需的,以便使用本地存储。它只是一个扩展,使用getset技术来检查数据的有效性。您可以通过在main.js脚本中包含'plugins.data.local'命令并调用setData()getData()函数来使用该插件。

Ls = new ig.local(); //localstorage
  Ls.setData("name","Davy");
  alert(Ls.getData("name"));

现在我们来快速看一下如何一般使用本地存储;让我们看看 lawnchair 应用程序提供了什么。

使用 lawnchair 作为存储数据的多功能方式

lawnchair 应用程序是在客户端存储数据的免费且非常专业的解决方案。它能够以多种方式存储数据,并且 ImpactJS 的插件已经准备就绪。让我们看看如何使用 lawnchair 应用程序来存储数据:

  1. 从以下网站下载 lawnchair 应用程序:brian.io/lawnchair/,或者您可以在github.com/jmo84/Lawnchair-plugin-for-ImpactJS上下载适用于 ImpactJS 的版本。

  2. 将文件放入您的plugin文件夹中。在这个例子中,它们被放在名为dataLawnchair的单独子文件夹中。但是,只要确保相应地更改代码,您可以自由使用任何结构。

  3. 在您的main.js文件中包含impact-plugin文件,如下面的代码所示:

'plugins.data.lawnchair.impact-plugin',
  1. 通过使用新获得的ig.Lawnchair()方法,将存储元素添加到您的GameInfo类中,如下面的代码行所示:
this.store = new ig.Lawnchair({adaptor:'dom',table:'allscores'},function() { ig.log('teststore is ready'); }),

ig.Lawnchair()方法接受两个输入参数:

  • 第一个参数是最重要的,实际上是一个数组。在这个数组中,您需要指定两件事情:您想要使用哪种方法来存储所有内容,以及您想要创建的数据存储的名称。第一个变量称为adaptor,因为 lawnchair 应用程序使用适配器模式技术来决定接下来需要发生什么。lawnchair 应用程序编程非常高效,通过使用模式立即变得明显。适配器模式本质上是一段代码,将您自己的代码链接到 lawnchair 应用程序的存储系统。没有这种模式,要与实际的 lawnchair 应用程序源代码进行通信将会非常困难。在这里,我们选择将其保存为永久 DOM 存储,但也可以选择其他选项,如 Webkit-SQLite。

注意

Webkit-SQLite 与永久 DOM 存储不同,它更像是一个常规数据库,但是在客户端的本地存储上运行。例如,像其他数据库一样,您可以使用 SQL 查询 Webkit-SQLite 存储。

  • 第二个输入参数是可选的。在这里,您可以放入需要在准备好store变量时执行的函数。这是放置日志消息的完美位置。
  1. 现在我们的存储元素已经准备就绪,只需调用store.save()方法存储任何您想要的数据。假设我们想要存储玩家的分数。为此,我们可以向GameInfo类添加一个执行相同操作的方法。
this.saveScore = function(){
  this.store.save({score:this.score});
}
  1. saveScore()函数可以添加到我们构建胜利和游戏结束屏幕时创建的gameOver()gameWon()方法中,如下所示:
gameOver: function(){
 GameInfo.saveScore();
  ig.system.setGame(gameOver); 
},
gameWon: function(){
 GameInfo.saveScore();
  ig.system.setGame(GameEnd); 
}
  1. 当玩家死亡或赢得比赛时,他的分数将使用 lawnchair 永久 DOM 方法保存。永久 DOM 并不意味着 DOM 永久保存在用户的 PC 上;这只是本地存储的另一个名称。

  2. 我们需要能够做的最后一件重要的事情是检索数据。为此,我们向GameInfo类引入了三个新函数:

  • 如果输入参数是实际数字,setScore()函数将把输入参数保存为GameInfo.score类,如下面的代码所示:
this.setScore = function(score){
  if(typeof score == 'number')
  this.score = score;
}; 
  • getScore()方法将只返回存储在GameInfo.score类中的分数值,如下面的代码所示:
this.getScore = function() {
  return this.score;
};

注意

setScore()getScore()似乎并不太重要,但正如在查看 cookies 概念时所解释的,使用setget语句对数据有效性进行检查是有用的。

  • GameInfo.getSavedScore()方法是GameInfo.saveScore()方法的镜像相反,如下面的代码所示:
this.getSavedScore = function(){
  this.store.get('score',function(score){GameInfo.setScore(score.value) });
  return this.getScore();
};
  1. getSavedScore()方法利用setScore()函数将GameInfo.score类设置为从存储中提取的数字,然后使用getScore()方法返回此分数,其中可以对数据有效性进行一些额外的测试。

  2. 现在,您可以随时检索最后达到的分数!

  3. 我们可以调整我们的开屏,以便通过将以下代码行添加到其draw()函数中显示最后达到的分数。

font.draw('last score: ' + GameInfo.getSavedScore(), 10,20); 

玩家的最后得分如下截图所示:

使用 lawnchair 作为存储数据的多功能方式

关于数据存储的足够了,让我们快速了解一下 cookies、本地存储以及使用本地存储的更多灵活的方式:lawnchair 之间的区别。

存储大小 过期日期 信息安全
Cookies 非常有限 固定 可以在 URL 中看到,并将被发送到接收服务器和返回到本地计算机。
本地存储 会话或无限 存储在本地计算机上,没有任何东西发送到服务器和从服务器返回。
lawnchair 取决于所选的技术 存储在本地计算机上,没有任何东西发送到服务器和从服务器返回。

简而言之,本地存储是保存数据的新方法。你仍然可以使用 cookies,但是新的隐私规则规定你必须在使用它们之前征得许可。

总结完整的数据存储概念,我们得出结论:

  • lawnchair 应用程序是一个可自由下载的代码包,可以处理所有客户端存储需求。它可以使用多种方法保存,如永久 DOM 存储或 Webkit-SQLite。

  • 推荐的可下载代码包位于github.com/jmo84/Lawnchair-plugin-for-ImpactJS,因为它带有一个 ImpactJS 插件。

  • 利用 lawnchair 存储系统包括包含库并将我们的GameInfo类的变量初始化为 lawnchair 应用程序的对象。然后我们可以通过使用this对象来存储和检索数据,因为它继承了所有的 lawnchair 方法。

RPG 的额外功能

在这一部分,我们将看一些额外的功能,这些功能可能对于像我们在第三章中设计的 RPG 游戏特别有用,让我们建立一个角色扮演游戏。首先,我们将通过鼠标点击实现角色移动,这对于移动游戏特别有用,因为触摸屏幕相当于点击鼠标。然后我们将添加一个智能生成点。这个生成点首先检查生成实体是否会导致碰撞,并相应地调整其生成坐标。第三个元素是玩家和非玩家角色(NPC)之间的对话。最后一个附加功能是基本的头顶显示(HUD),允许玩家跟踪他们的健康状况。

通过鼠标点击移动玩家

直到现在,我们通过键盘箭头键移动我们的玩家。这是非常直观的,但有时是不可能的。如果你在 iPad 或其他移动设备上打开游戏,由于没有箭头键,你无法移动你的角色。在这种情况下,如果我们的角色只需朝着我们在屏幕上触摸的位置走就更有用了。在 ImpactJS 中,鼠标点击和触摸被视为相同的东西,这取决于设备。因此,通过鼠标点击实现移动自动导致了移动触摸设备。要使玩家通过点击鼠标或触摸屏幕移动,需要按照以下步骤进行:

  1. main.js文件中,将鼠标点击绑定到名为'mouseclick'的动作。
ig.input.bind(ig.KEY.MOUSE1, 'mouseclick');
  1. 打开player.js文件并添加一些额外的初始变量。一旦我们开始使用即将添加的鼠标功能,我们将需要这个。
name: "player",
movementspeed : 100,
mousewalking : 0,
takemouseinput : 0,
animSheet: new ig.AnimationSheet|( 'media/player.png', 32, 48 ),
  1. 如果movementspeed变量还不是一个"player"属性,确保现在添加它。mousewalking命令是一个标志变量;值为1表示玩家必须按鼠标点击的命令行走。当鼠标被点击并且目标坐标被计算后,takemouseinput变量的值被设置为1,然后立即返回到0。没有这个变量,可能会通过鼠标位置来操纵你的角色,而不是单击一次。这是一个选择的问题;通过鼠标位置而不是鼠标点击来操纵可以成为有效和直观的控制方案的一部分。

  2. 使用以下代码将mousemovement()方法添加到"player"实体:

mousemovement: function(player){
if (player.mousewalking == 1 && player.takemouseinput == 1){
  player.destinationx = ig.input.mouse.x + ig.game.screen.x;
  player.destinationy = ig.input.mouse.y + ig.game.screen.y;
  player.takemouseinput = 0;
}
else if(player.mousewalking == 1){
  var distancetotargetx = player.destinationx - player.pos.x - (player.size.x/2) ;
  var distancetotargety = player.destinationy - player.pos.y -(player.size.y/2) ;
  if (Math.abs(distancetotargetx) > 5 ||Math.abs(distancetotargety) > 5){
    if (Math.abs(distancetotargetx) > Math.abs(distancetotargety)){
      if (distancetotargetx > 0){
        player.vel.x = player.movementspeed;
        var xydivision = distancetotargety / distancetotargetx;
        player.vel.y = xydivision * player.movementspeed;
        player.currentAnim = player.anims.right;
        player.lastpressed = 'right';
      }
      else{
        player.vel.x = -player.movementspeed;
        var xydivision = distancetotargety /Math.abs(distancetotargetx);
        player.vel.y = xydivision * player.movementspeed;
        player.currentAnim = player.anims.left;
        player.lastpressed = 'left';
      }
      }
    else{
      if (distancetotargety > 0){
        player.vel.y = player.movementspeed;
        var xydivision = distancetotargetx / distancetotargety;
        player.vel.x = xydivision * player.movementspeed;
        player.currentAnim = player.anims.down;
        player.lastpressed = 'down';
      }
      else{
        player.vel.y = -player.movementspeed;
        var xydivision = distancetotargetx /Math.abs(distancetotargety);
        player.vel.x = xydivision * player.movementspeed;
        player.currentAnim = player.anims.up;
        player.lastpressed = 'up';
      }
      }
    }
  else{
    player.vel.y = 0;
    player.vel.x = 0;
    player.mousewalking = 0;
    player.currentAnim = player.anims.idle;
  }
}
},
  1. 这个函数的长度可能有点令人生畏,但实际上相同的逻辑被重复了几次。该函数基本上有两个功能:它可以设置目的地坐标,也可以使玩家朝着目标移动。在大多数情况下,不需要计算新的目标。因此,第一个检查是是否需要使用新的目的地。为此,player.takemouseinputplayer.mousewalking变量都需要为true。在计算目标位置坐标时,对游戏屏幕的位置进行了修正。

  2. 然后,函数继续进行实际的移动;是否进行移动由player.mousewalking变量的值(TrueFalse)设置。

  3. 如果玩家需要行走,实际距离将被计算到目标的 x 和 y 轴,并存储在本地变量distancetotargetxdistancetotargety中。当目标在任一轴上与玩家相距 5 像素时,玩家将不会移动。

  4. 然而,如果距离大于 5 像素,玩家将以线性方式朝着目标移动。为了确保玩家以预设的移动速度移动,他将在剩余距离最大的轴上这样做。假设玩家在 x 轴上离目标很远,但在 y 轴上不那么远。在这种情况下,他将以 x 轴上的预设移动速度移动,但在 y 轴上的速度小于预设移动速度。此外,他将面向左或右,而不是上或下。

  5. 两个最重要的触发变量:player.mousewalkingplayer.takemouseinput的初始值为0;当鼠标点击被注册时,它们需要被设置为1。我们在update()函数中执行此操作,如下面的代码所示:

if( ig.input.pressed('mouseclick')){
this.mousewalking = 1;
this.takemouseinput = 1;
}
  1. 我们刚刚确保游戏在每个新帧都会检查鼠标是否被点击。

  2. 如果我们现在通过添加对mousemovement()方法的调用来调用我们的更新函数,玩家将在屏幕上注册鼠标点击的地方行走。

mousemovement();
  1. 当然,我们的键盘控件仍然存在,这将导致问题。为了使两种控制方法都能正常工作,我们只需要在按下键盘上的任意一个键时,将player.mousewalking变量的值设置为0,如下面的代码所示,用于上箭头键:
if(ig.input.state('up')){
  this.mousewalking = 0;
  this.vel.y =this.movementspeed;
  this.currentAnim = this.anims.up;
  this.lastpressed = 'up';
}
  1. 需要不断使用以下代码来检查player.mousewalking变量的值是否为0。如果不是,我们的旧控制系统将立即停止移动,因为没有注册键盘输入。
Elseif(this.mousewalking == 0){
  this.vel.y = 0; 
  this.vel.x = 0;
  this.currentAnim = this.anims.idle;
}
  1. 最后,保存您的文件并重新加载游戏。

现在,您应该能够通过在屏幕上的任何位置单击鼠标来四处走动。如果玩家遇到障碍物,您可能会注意到轻微的航向调整。但是,如果障碍物太大,玩家就不够聪明去绕过它。作为玩家,您需要自己避开障碍物。

让我们看看如何创建一个智能的生成位置。但在这样做之前,让我们回顾一下刚刚讨论的内容:

  • 能够通过鼠标点击移动玩家是一个有趣的功能,尤其是在移动到移动设备时,因为在那里键盘不是一个选项。在 ImpactJS 中,鼠标的点击被视为与触摸 iPad 屏幕相同。

  • 目前,我们的玩家可以使用键盘四个方向键移动,因此我们需要实现同时使用键盘方向键和鼠标的可能性。所有这些调整将在玩家实体内进行。

  • 我们引入了一个名为mousemovement()的新方法,该方法在玩家的update函数中被重复调用。在任何时候,我们的方法都会检查是否给出了通过鼠标点击移动的命令,如果是,将移动玩家到所需位置。

  • 除了添加这个新方法,我们还需要调整旧的移动代码,以便允许同时使用箭头键和新实现的鼠标点击移动。

添加智能生成位置

在 Weltmeister 中构建关卡时,可以立即将敌对实体添加到关卡本身。这很好,但有时增加一些不可预测性会增加游戏的重玩价值。这可以通过添加智能生成来实现:在随机位置生成敌人,但考虑到其他实体和碰撞层的碰撞。为了做到这一点,我们需要按照以下步骤创建一个新的插件:

  1. 创建一个新文件,并将其保存为spawnlocations.js

  2. 'plugins.functions.spawnlocations'命令添加到你的main.js文件中。

  3. 创建一个ig.spawnlocations变量,作为 ImpactJS 类的扩展,如下面的代码所示:

ig.module('plugins.functions.spawnlocations').defines(function(){
  ig.spawnlocations = ig.Class.extend({
  });
})
  1. 添加spawnIf()方法,这是一个回调函数,如下面的代码所示。当满足某些条件时,它可以再次调用自身。
spawnIf: function(x, y)
{
  if (this.CollisionAt(x,y) || this.getEntitiesAt(x,y)){
    var x1 = x + Math.round(Math.random())*10;
    var x2 = x + Math.round(Math.random())*10;
    this.spawnIf(x1,x2); //recursion
  }
  ig.game.spawnEntity('EntityEnemy', x, y);
},
  1. spawnIf()函数接受一个 x 和 y 的起始坐标,并检查是否与碰撞层或实体发生碰撞。如果是这种情况,原始坐标将在两个轴上的随机像素数上进行调整。然后,这些新坐标将被重新提交给spawnIf()函数,直到找到一个空闲位置。一旦不再检测到碰撞,敌人就会在那个位置生成。它需要的CollisionAt()getEntitiesAt()函数也是spawnlocations类的一部分。

  2. getEntitiesAt()函数将检测与需要生成的敌人重叠的实体。以下代码描述了getEntitiesAt()函数应用的检测过程:

getEntitiesAt: function(x, y)
{
  var n = ig.game.entities.length;
  var ents = [];
  for (var i=0; i<n; i++)
  {
    var ent = ig.game.entities[i],
    x0 = ent.pos.x,
    x1 = x0 + ent.size.x,
    y0 = ent.pos.y,
    y1 = y0 + ent.size.y;
    if (x0 <= x && x1 > x && y0 <= y && y1 > y)
      return true;
  }
  return false;
},
  1. 逐个检查实体,以查看它们是否重叠,使用它们的位置、宽度和高度。如果与单个实体重叠,循环将被中止,getEntitiesAt()函数将返回值true。如果没有检测到重叠,它将返回值false

  2. 虽然getEntitiesAt()函数检查与其他实体的可能碰撞,CollisionAt()函数检查敌人是否会与碰撞层重叠,如下面的代码片段所示:

CollisionAt: function(x,y)
{
  var Map = ig.game.collisionMap;
  var ent = new EntityEnemy();
  var res = Map.trace( x, y, x+ ent.size.x,y + ent.size.y,ent.size.x,ent.size.y ); // position, distance, size
  // true if there is a collision on either x or y axis 
  return res.collision.x || res.collision.y;
}
  1. 最重要的功能是collisionMap方法的trace()函数。trace()函数将检查x坐标值和xent.size.x变量坐标值之和之间,或者y坐标值和yent.size.y变量坐标值之和之间是否有东西。最后两个参数是实体的size。这通常用于检查轨迹,但我们用它来检查特定位置。如果在 x 轴或 y 轴上发生碰撞,CollisionAt()函数将返回值truespawnIf()函数将需要寻找新的生成位置。

  2. 我们需要做的最后一件事是实际生成一个敌人。我们可以在main.js文件的MyGame中使用以下代码来实现:

var spaw = new ig.spawnlocations();
spaw.spawnIf(100,200);
  1. 如果有空闲空间,敌人现在将在这些坐标生成,否则,坐标将被调整,直到找到合适的位置。

现在我们在游戏中添加了智能生成点,是时候转向一个相对复杂的游戏元素:对话。然而,在开始对话过程之前,让我们快速回顾一下我们刚刚做的事情:

  • 智能生成点的目的是找到一个敌人生成的开放空间。为此,需要检查游戏中已有的实体和关卡的碰撞层。

  • 我们构建了一个包含三个部分的插件:

  • 一个回调函数,将调整坐标直到找到一个合适的位置,并随后生成敌人。它利用了我们生成点类中的其他两个函数。

  • 必须检查潜在与其他实体的重叠的函数。

  • 检查与碰撞层的重叠的函数。

  • 现在可以通过初始化一个新的生成点并使用其spawnIf()方法将新的敌人放入游戏世界来向游戏添加敌人。

介绍基本对话

许多角色扮演游戏(RPG)中有玩家和一些不可玩角色(NPC)之间的对话。在本节中,我们将介绍一种将简单对话添加到游戏中的方法。主要前提是我们在本章前面为游戏添加的对话数据。我们需要构建一个包含可以由玩家选择的对话菜单,具体步骤如下。我们可爱的 NPC Talkie 将作为我们的合作伙伴,玩家不仅在 Talkie 说话时有几个回答选项,而且 NPC 还会根据玩家想说的话做出反应,开启新的选项。这个循环应该能够一直进行,直到所有选项耗尽或对话被突然中止:

  1. 打开一个新文件,并将其保存为menu.js,放在plugins文件夹的conversation子文件夹中。

  2. 在你的main.js文件中添加一个'plugins.conversation.menu'命令。

  3. 创建一个window.Menu类,作为 ImpactJS 类的扩展,如下面的代码所示:

ig.module(
  'plugins.conversation.menu'
)
.defines(function(){
  window.Menu = ig.Class.extend({
    init: function(_font,_choice_spacing,_choices,_entity){
      this.selectedChoice = 0;
      this.cursorLeft = ">>";
      this.cursorRight = "<<";
      this.cursorLeftWidth =_font.widthForString(this.cursorLeft);
      this.cursorRightWidth =_font.widthForString(this.cursorRight);
      var i,labeled_choice;
      for(i=0;i<_choices.length;i++){
        _choices[i].labelWidth =_font.widthForString(_choices[i].label);
      } 
      this.font = _font;
      this.choices = _choices;
      this.choice_spacing = _choice_spacing;
      this.entity = _entity;
      this.MenubackgroundMenubackground = newig.Image('media/black_square.png');
      this.Menubackground.height = this.choices.length *this.choice_spacing;
    }
  }
},
  1. 我们的菜单init()函数将需要四个输入变量;我们将把它们都转换为menu属性,以便它们在我们的menu方法中可用;这四个输入变量如下:
  • _font:这是我们将使用的字体

  • _choice_spacing:这是我们希望在屏幕上显示的每个选择之间的间距

  • _choices:这是玩家在对话特定部分拥有的选择数组

  • _entity:这是需要与玩家交谈的 NPC;在这种情况下,将是Talkie

  1. 我们的init()方法包含一些其他重要的变量,如下所示:
  • this.selectedChoice:这是将存储当前选定选择的数组索引的变量。它被初始化为值0,这始终是任何数组的第一个元素,因此也是玩家的第一个选项。this.selectedChoice变量很重要,因为符号<<>>将显示在当前选定选项的两侧,作为视觉辅助。

  • this.cursorLeftthis.cursorRight:它们是存储视觉辅助符号<<>>的变量。

  • this.cursorLeftWidththis.cursorRightWidth:它们是存储所选字体的<<>>符号的长度的变量,以便在实际在屏幕上绘制选择时可以考虑到这一点。

  • _choices[i].labelWidth:这个局部变量存储了为每个选择计算出的宽度。计算出的宽度然后存储在菜单属性数组choices[i].labelWidth中。cursorLeftWidthcursorRightWidth变量将用于确定在屏幕上绘制选项时的屏幕定位。

  • this.Menubackground:这个变量将保存一个黑色的正方形,作为背景,以便对话的白色字符始终可读,无论当前级别的外观如何。背景会根据最长选项的长度和选项的数量自适应。这样就不会占用比绝对必要更多的空间。

  1. draw()方法包含所有菜单逻辑,因此我们将使用以下代码分块讨论它:
draw: function(_baseX, _baseY){
  var _choices = this.choices;
  var _font = this.font;
  var i,choice,x,y;
  if (this.choices.length > 0){
    var Menubackground = newig.Image('media/black_square.png');
    Menubackground.height = this.choices.length *this.choice_spacing;
    Menubackground.width = 1;
    for(var k=0;k<_choices.length;k++){
      choice = _choices[k];
      if(this.font.widthForString(choice.label)>Menubackground.width){
        Menubackground.width =this.font.widthForString(choice.label);
      }
    }
  Menubackground.width = this.Menubackground.width +this.cursorLeftWidth + this.cursorRightWidth + 16;
  Menubackground.draw(_baseX-this.Menubackground.width/2,_baseY);
  };
}
  1. draw()函数的第一个主要功能是调整菜单的背景,使其始终足够大,以适应不同的句子,给定所选择的字体。这种逻辑,以及其他逻辑,实际上可以存储在update()函数中,而不是draw()函数中。这是一个选择问题,您当然可以根据自己的意愿重写menu类。最重要的共同属性是draw()update()函数都在每一帧中被调用。在下面的代码中,我们可以查看draw()函数的功能:
for(i=0;i<_choices.length;i++){
  choice = _choices[i];
  choice.labelWidth = _font.widthForString(choice.label);
  y = _baseY + i * this.choice_spacing + 2;
  _font.draw(choice.label, _baseX, y,ig.Font.ALIGN.CENTER);
  if (this.selectedChoice === i){
    x = _baseX - (choice.labelWidth / 2) -this.cursorLeftWidth - 8;
    _font.draw(this.cursorLeft, x, y - 1);
    x = _baseX + (choice.labelWidth / 2) + 8;
    _font.draw(this.cursorRight, x, y - 1);
  }
}
  1. 现在确定文本的位置,并将每个选项写在屏幕上。检查当前选择的选项。这个选项被<<>>符号包围,以使玩家意识到他即将做出的选择。为了添加这些功能,我们将查看以下代码:
if(ig.input.pressed('up')){
  this.selectedChoice--;
  this.selectedChoice = (this.selectedChoice < 0) ? 0 :this.selectedChoice;
}
else if(ig.input.pressed('down')){
  this.selectedChoice++;
  this.selectedChoice = (this.selectedChoice >=_choices.length) ?_choices.length-1 : this.selectedChoice;
}
else if(ig.input.pressed('interact')){var chosen_reply_key = _choices[this.selectedChoice].npcreply();ig.game.spawnEntity('EntityTextballoon',this.entity.pos.x -10,this.entity.pos.y - 70,{wrapper:npc_con.NPC_SPEECH[chosen_reply_key]});
  this.choices =_choices[this.selectedChoice].changechoices(chosen_reply_key);
}
  1. 玩家有三个选项:他可以按上箭头、下箭头或键盘上的交互按钮;最后的动作状态对应Enter键。在这里,我们将解释如何在常规桌面上实现这一点。尝试为移动设备实现这一点是一个很好的练习:
  • 如果激活了'up'输入状态,则'up'状态当前应该绑定到键盘的上箭头,并且所选选项向上移动一个位置。在数组中,这意味着一个具有较低索引的元素。但是,如果达到索引中的位置 0,它就不能再往下走了,因为这是第一个选项。在这种情况下,它会停留在第一个选项。

  • 使用下箭头键向下移动菜单时使用相同的逻辑。

  • 如果'interact'状态尚未绑定到Enter键,请通过在main.js文件中添加ig.input.bind( ig.KEY.ENTER, 'interact' );命令来绑定。玩家通过按下Enter键来做出选择。使用npcreply()函数,NPC 知道该说什么,并将生成一个包含他回复的文本气球。根据这个回复,this.choices函数将填充新的供玩家选择的选项。

  1. 菜单由不同的项目组成;每个单独的选项对应一个单独的菜单项。使用以下代码将此菜单项类添加到menu.js文件中:
window.MenuItem = ig.Class.extend({
  init: function(label,NPC_Response){
    this.label = label;
    this.NPC_Response = NPC_Response;
    this.entity = entity;
    },
  });
});
  1. 菜单项使用以下两个输入参数进行初始化:
  • 标签,这是一个选择或选项的实际文本。

  • NPC_Response,这是 NPC 回复的主键。有了这个键,就可以查找 NPC 需要回答的内容,并为玩家构建新的选项。

  1. npcreply()方法使用NPC_Response键(如下面的代码所示)查找 NPC 在我们在本章前面构建的NPC_CON数组中将要给出的回复的数组编号:
npcreply: function(){
  for(var i= 0;i<=npc_con.NPC_CONVO_KEY.length; i++){
    if (npc_con.NPC_CONVO_KEY[i] == this.NPC_Response){
    return i;
    }
  }
},
  1. 你可能还记得,我们的整个对话只有两个数组:
  • NPC_CON:这个数组包含了 NPC 要说的一切

  • PC_CON:这个数组包含了玩家可以说的一切

  1. 在菜单代码中,该键存储在一个名为chosen_reply_key的局部变量中,然后以以下两种方式重新使用:
  • 使 NPC 回复

  • 通过将其作为参数输入到changechoices()方法来构建新的选项

  1. 最后,changechoices()方法接受 NPC 所说的内容(如下面的代码所示),并通过遍历我们在本章前面构建的PC_CON数组来构建新的选项。
changechoices: function(chosen_reply_key){
  var choices =  []
  for(var k= 0;k<=pc_con.REPLY_SET_KEY.length; k++){
    if (pc_con.REPLY_SET_KEY[k] ==npc_con.REPLY_SET_KEY[chosen_reply_key]){
      choices.push(new MenuItem(pc_con.PC_SPEECH[k],pc_con.NPC_CONVO_KEY[k]));
    }
  }
return choices;
}

对话是一个循环,理论上可以永远进行下去。然而,我们仍然需要一个开始。我们可以通过在Talkie NPC 本身中初始化我们的Talkie NPC 菜单的一些选项来实现这一点。这是一个非常实用的方法,但与此对话插件的整个实现一样,您可以自由地根据自己的意愿进行调整和扩展。

在我们开始与他交谈之前,我们仍然需要调整我们的Talkie实体:

  1. 打开talkie.js文件,并将以下代码添加到文件中作为属性:
var i;
this.choices = [
new MenuItem(pc_con.PC_SPEECH[0],pc_con.NPC_CONVO_KEY[0],this),
new MenuItem(pc_con.PC_SPEECH[1],pc_con.NPC_CONVO_KEY[1],this),
new MenuItem(pc_con.PC_SPEECH[2],pc_con.NPC_CONVO_KEY[2],this)
];
var menufont = new ig.Font('media/04b03.font.png');
this.contextMenu = new Menu(menufont,8,this.choices,this);
  1. 我们现在为 Talkie 添加了一个对话菜单,并将其初始化为PC_CON数组的前三个选项。

  2. 现在我们需要一个函数来检查 Talkie 是否被实际选择。否则,如果我们同时引入多个 NPC,就会出现冲突。为了检查 Talkie 是否被实际选择,我们编写以下代码:

checkSelection:function(){
  this.mousecorrectedx = ig.input.mouse.x + ig.game.screen.x;
  this.mousecorrectedy = ig.input.mouse.y + ig.game.screen.y;
  return (
    (this.mousecorrectedx >= this.pos.x && this.mousecorrectedx <=this.pos.x+this.animSheet.width)&& (this.mousecorrectedy >= this.pos.y && this.mousecorrectedy <=this.pos.y+this.animSheet.height)
    );
  },
}
  1. 该函数将检查鼠标点击的位置,并校正其在游戏屏幕上的位置。如果我们的级别完全适合视口,则不需要校正,但这几乎永远不是这种情况,因此需要进行校正。该函数返回一个truefalse值。如果实体被选择,则返回值为true,如果没有选择,则返回false

  2. 在我们的update()方法中,我们现在可以检查鼠标点击,并使用以下代码查看 Talkie 是否被实际选择:

if( ig.input.pressed('mouseclick') ) {
  this.contexted = this.checkSelection();
}
  1. 如果是这样,我们将设置它全新的属性contextedtrue。如果没有选择 Talkie,contexted将被设置为false

  2. 如果Talkie实体被点击并且有菜单可用,它将在Talkie实体下方绘制以下代码:

draw: function() {
  if(this.contexted && this.contextMenu){
    this.contextMenu.draw(this.pos.x+(this.animSheet.width/2)-ig.game.screen.x,this.pos.y+(this.animSheet.height)-ig.game.screen.y);
  }
this.parent();
},
  1. 现在 Talkie 已经准备好交谈了!一定要尝试设置自己的对话,并在游戏中看到它展开。

在我们继续讨论一些高级 AI 之前,我们将添加一个漂亮的条形图,直观地显示玩家的生命值。但在这样做之前,我们将首先回顾一下对话插件:

  • 我们想要在玩家和 NPC 之间建立一段对话。为此,我们将利用本章早些时候导入的数据和一个名为Menu的新插件。

  • Menu插件由两部分组成:菜单本身和菜单中的选项。我们将两者都创建为ImpactJS类的扩展。

  • 设置了Menu插件和菜单项之后,我们友好的 NPC Talkie 需要进行一些额外的调整。当玩家用鼠标点击Talkie实体时,应该出现一个带有几个选项的菜单。当选择其中一个选项时,Talkie 会回复。为了显示回复,我们利用了在第三章中创建的对话气泡,让我们建立一个角色扮演游戏

  • 整个对话是一个循环,当玩家或 NPC 用完句子,或者玩家走开时,循环结束。

添加基本的头顶显示

我们的玩家有生命值,但他不知道自己在任何给定时间剩下多少。因为作为玩家,了解自己剩下多少生命值是如此重要,所以我们将在屏幕上显示这一点,作为数字和生命条。为此,我们使用以下步骤构建自己的 HUD 插件:

  1. 打开一个新文件,并将其保存为hud.js,放在plugin文件夹的hud子文件夹下。

  2. 'plugins.hud.hud'命令添加到main.js脚本中。

  3. 首先在新的plugin文件中插入以下代码:

ig.module('plugins.hud.hud').
defines(function(){
  ig.hud = ig.Class.extend({ 
    canvas  : document.getElementById('canvas'), //get the canvas
    context : canvas.getContext('2d'),
    maxHealth  : null,
    init: function(){
      ig.Game.inject({
        draw: function(){
          this.parent();
          // draw hud if there is a player
          if(ig.game.getEntitiesByType('EntityPlayer').length  !=0){
            if (this.hud){
            this.hud.number();
            this.hud.bar();
            } 
          }
        }
      })
    }, 
  }
}
  1. 像往常一样,我们基于 ImpactJS 类定义一个新类。我们初始化两个变量:canvas 和 context,这将允许我们查看游戏是否正在被查看。此外,我们以值null初始化一个maxHealth变量。然而,与通常的条件不同,我们使用了注入技术,就像我们构建调试面板时所做的那样。在扩展代码时,您创建原始代码的新实例,并为其提供新名称。它在所有方面都是原始代码的副本,唯一的区别是您添加的额外代码。但在注入时,您修改原始代码。在这种情况下,我们覆盖了游戏的draw()函数。this.parent()函数指向我们以前的draw()函数,因此已经存在的所有内容都被保留。我们添加的是检查玩家实体是否存在。如果玩家在游戏中,将绘制 HUD。我们的 HUD 由两部分组成:数字和生命条。

  2. number函数将绘制一个黑色并略微透明的矩形,其中健康值将可见,使用以下代码:

number: function(){ 
  if(!this.context) return null;
  var player =ig.game.getEntitiesByType('EntityPlayer')[0];
  // draw a transparant black rectangle 
  var context = this.canvas.getContext('2d');
  context.fillStyle = "rgb(0,0,0)";
  context.setAlpha(0.7); //set transparency 
  context.fillRect(10,10,100,30);
  //draw text on top of the rectangle 
  context.fillStyle = "rgb(255,255,255)";
  context.font = "15px Arial";
  context.fillText('health: ' + player.health,20,30);
  //font used is the default canvas font
  context.setAlpha(1);
  return null;
},
  1. 在我们的number()函数的第一部分中,我们定义并绘制了矩形。由于它需要位于数字下方,所以需要先绘制它。与以前不同的是,我们直接使用 canvas 元素的属性在屏幕上绘制。例如,字体不需要使用 ImpactJS 的ig.font函数来设置。如下所示,您可以通过直接访问画布并设置画布的font属性来将字符写入屏幕。我们在这里使用的画布属性非常简单,列举如下:
  • fillstyle: 此属性将设置颜色

  • font: 此属性将设置字体

  • setAlpha(): 此属性将设置透明度,值为1表示完全不透明,值为0表示完全透明

  • fillRect(): 此属性将在给定位置以给定宽度和高度向屏幕绘制一个矩形

  • fillText(): 此属性将在屏幕上的特定位置绘制文本

  1. 我们的生命条功能的工作方式与数字功能类似,如下面的代码所示:
bar: function(){
  if(!this.context) return null;
  var player = ig.game.getEntitiesByType('EntityPlayer')[0];
  // draw a transparant black rectangle 
  var h = 100*Math.min(player.health / this.maxHealth,100);
  var context = this.canvas.getContext('2d');
  context.fillStyle = "rgb(0,0,0)";
  context.setAlpha(0.7);
  context.fillRect(10,50,100,10);
  //either draw a blue or red rectangle on top of theblack one var color = h < 30 ? "rgb(150,0,0)" :"rgb(0,0,150)";
  context.fillStyle = color;
  context.setAlpha(0.9);
  context.fillRect(10,50,h,10);
  context.setAlpha(1);
  return null;
},
  1. 在这里,我们在彼此之上绘制了两个矩形。底部的矩形始终是黑色的,并且略微透明。顶部的矩形要么是蓝色的,要么是红色的,这取决于玩家剩余的健康程度。如果玩家的健康值为30或更高,条将是蓝色的,否则将是红色的,表示即将死亡。

  2. 黑色透明底部条的大小始终相同,但其宽度取决于玩家开始游戏时的健康状况。我们可以使用setMaxHealth()方法来捕获这一点,如下面的代码所示:

setMaxHealth: function(health){
  this.maxHealth = health;
}
  1. 现在我们所需要做的就是初始化一个 HUD,并使用我们的setMaxHealth()方法提供玩家的健康值。将以下代码添加到main.js文件中:
MyGame = ig.Game.extend({
  font: new ig.Font( 'media/04b03.font.png' ),ai: new ig.general_ai(),
 hud: new ig.hud(),
  init: function() {
    this.loadLevel(LevelLevel1);
 var player = ig.game.getEntitiesByType('EntityPlayer')[0];
 this.hud.setMaxHealth(player.health);
  }
}
  1. 重新加载游戏时,我们现在应该有一个蓝色的生命条,并指示我们还剩下100生命值,如下面的屏幕截图所示:添加基本 HUD

  2. 然而,与敌人进行了一场小战斗后,我们可以通过我们的红色生命条看到,现在是时候去找医生了,如下面的屏幕截图所示:添加基本 HUD

现在我们已经看过了一些有趣的扩展内容第三章,让我们建立一个角色扮演游戏,让我们重新审视我们的人工智能,并引入新的复杂性。在继续之前,让我们快速回顾一下我们构建 HUD 的方式:

  • HUD 或抬头显示器提供了玩家几个关键指标的快速视图,这有助于玩家取得成功。在射击游戏中,这显示了他还剩多少弹药,总共和当前弹夹中的数量。它还可以指示其他物品或他的总得分。在这里,我们允许他使用经典的生命条来跟踪他的健康状况。

  • hud插件是 ImpactJS 类的扩展,有两个元素:数字和有颜色的条。它们在hud插件内部有各自的方法。您可以通过添加代表其他可跟踪统计数据的新方法来扩展hud插件。

  • 在构建 HUD 时,我们使用canvas属性作为使用 ImpactJS 类(如ig.font)的替代方法。

人工智能:集体意识

在第三章中,让我们建立一个角色扮演游戏,我们已经涵盖了 AI 以及为什么行为应该与决策过程分开。我们也已经看过策略,但只应用了单一策略:攻击。在这里,我们将建立一个补充的智能层,决定哪个实体将遵循哪种策略。因为决策过程考虑了同一级别中的所有敌人,我们称之为集体意识智能。这与蜂巢的女王或战场上的将军非常相似,他们决定谁应该攻击,谁应该留在原地。我们在集体意识中决定的策略被发送到我们在第三章中放置的 AI,那里它被解释并转化为行为。行为命令又被发送到实体本身,然后实体根据它们行动。让我们使用以下步骤创建我们的ai插件:

  1. 打开一个新文件,将其保存为general_ai.js

  2. main.js文件中插入'plugins.ai.general_ai'类。

  3. ig.general_ai类创建为 ImpactJS 类扩展。通常,类general_ai.js已经按照以下代码创建:

ig.module('plugins.ai.general_ai').
defines(function(){
  ig.general_ai = ig.Class.extend({
    init: function(){
      ig.ai.STRATEGY = { Rest:0,Approach:1};
  },
}
  1. 我们首先要做的是定义可能的策略。在这里,我们只会发布两种策略:ApproachRest

  2. getStrategy()函数位于我们的集体意识决定保留它的地方,它是我们的 AI 将调用以接收策略的函数。这个策略又通过以下代码转化为行为:

getStrategy: function(ent){
  // part 1: get player and list of enemies
  var playerList = ig.game.getEntitiesByType('EntityPlayer');
  var player = playerList[0];
  var EnemyList = ig.game.getEntitiesByType('EntityEnemy');
  // part 2: store distance to player if that enemy has enoughhealth to attack
  var distance =  [];
  for(var i = 0;i < EnemyList.length; i++){
    //for every enemy > 100 health: put in array
    EnemyList[i].health > 100 ?distance.push(EnemyList[i].distanceTo(player)) : null;
  }
  // part 3: decide on strategy: attack or stay put?var Mindist = Math.min.apply(null,distance);
  var strategy = (ent.distanceTo(player)===Mindist ||distance.length === 0) ? ig.ai.STRATEGY.Approach:ig.ai.STRATEGY.Rest;
  return strategy;
}
  1. getStrategy()方法包含我们整个集体意识逻辑,并由三个主要部分组成:
  • 首先,敌人列表和玩家实体分别分配给本地变量。

  • 然后,这些本地变量被用来计算每个敌人与玩家之间的距离,对于那些具有超过 100 生命值的敌人。每个生命值低于 100 的敌人都被认为是虚弱的,太害怕攻击。通过为每个敌人添加个性,可以使这段代码变得更加复杂。例如,我们可以初始化每个敌人的courage属性,填充一个在我们敌人的生命范围内的随机数;在我们的情况下,这是0200。这样我们可以通过将当前生命值与勇气进行比较来决定某个敌人是否足够勇敢地攻击,而不是与固定值进行比较。当然,你可以尝试这个方法;它为游戏增加了深度和不可预测性。

  • 最后,所有足够勇敢攻击的敌人都将根据它们与目标的距离进行比较,只有最接近目标的敌人才会攻击。其他人将被分配Rest策略,只有当它们成为周围最近的敌人时才会攻击。作为玩家,你仍然应该小心。如果他们中没有一个感到足够强大来单独攻击,他们将联合起来一起攻击。

  1. 在我们之前构建的 AI 中,我们现在需要使用以下代码调用getStrategy()函数:
getAction: function(entity){
this.entity = entity;
if(ig.game.ai.getStrategy(entity) == ig.ai.STRATEGY.Approach){

  1. 如果策略是Approach,AI 将将其转化为适当的动作。
return this.doAction(ig.ai.ACTION.Rest);
  1. 如果策略是其他的,它会立即转化为Rest动作。因为我们只有这两种策略,所以这是有意义的。如果你有更多的策略,你将需要更多的检查。

现在我们已经扩展了我们的 AI 以包含策略,是时候来看一下本章的最后一部分了:使用 Playtomic 实现游戏分析。在继续之前,让我们快速回顾一下集体意识 AI:

  • 集体意识是一个高层决策机构,将向游戏中的不同实体发布策略。这是一种使它们作为一个团体而不是一群无组织的个体行动的方式。

  • 在第三章让我们建立一个角色扮演游戏中,我们有决策过程,这被转化为行为。现在我们有了一个策略,这转化为个体决策,然后转化为行为。

  • 集体意识插件与我们在第三章让我们建立一个角色扮演游戏中构建的 AI 是分开的。这样我们仍然可以通过只进行少量代码更正来返回我们的个体主义 AI。

  • 集体意识逻辑遵循三个主要步骤:

  • 获取关卡内的所有敌人和玩家。

  • 检查每个敌人的健康值,看看他是否是一个适合攻击的候选人。

  • 从这些可行的敌人中选择一个离玩家最近的敌人让他攻击。敌人将如何执行这次攻击并不是由总体 AI 指定的;这是个体 AI 的决定。

实施 Playtomic 进行游戏分析

Playtomic 可以被视为游戏的 Google Analytics。你可以标记游戏的某些部分,并检查它们是否经常被使用。例如,如果你的游戏中有一个隐藏关卡,你可以通过标记这个隐藏关卡的loadlevel()函数来查看它被多少不同的玩家发现了多少次。然后你就可以确定它可能太容易或太难被发现,然后相应地调整你的游戏。但这只是你可以应用游戏统计的众多方式之一。然而,你需要意识到标记你的游戏会在一定程度上影响其性能。因此,标记代码的每一寸可能并不像预期的那样富有成效。此外,你将留下大量的数据需要分析,这可能是一项艰巨的任务。

除了为你提供游戏使用情况的见解外,Playtomic 还允许你在他们的服务器上存储一些东西,比如得分,你可以将其转化为排行榜。

如果这一切听起来对你来说都不错,那么请务必前往playtomic.com/创建一个免费账户。

然而,需要适当地警告一下。Playtomic 仍处于起步阶段,这意味着会有一些错误或不合逻辑的选择。例如,默认的保存得分到排行榜的做法是不覆盖第一个,即使新的得分更高。这对于排行榜来说是没有意义的,即使文档中也指出默认设置应该允许得分覆盖。与 Playtomic 服务器的连接会减慢游戏加载速度,并且经常会因为没有建立稳定连接而丢失数据。

但即使在实施、服务器速度和文档中存在缺陷,如果你想要收集有关你的游戏的见解,Playtomic 还是值得一看的。以下截图描述了 Playtomic 收集的数据及其表示:

实施 Playtomic 进行游戏分析

为了实施 Playtomic,你需要做一些事情:

  1. 创建一个 Playtomic 账户并获取你的数据传输凭据。你需要这些来建立与他们服务器的连接。

  2. index.html文件中,我们需要包含 Playtomic 源脚本,如下面的代码所示。当然,要检查最新版本是什么,在安装时。在撰写本书时,它是 2.2 版本,但这些东西发展得很快。

<body>
  <canvas id="canvas"></canvas>
  <script type="text/javascript"src="img/playtomic.v2.2.min.js"></script>
</body>
  1. 打开一个新文件,并将其保存为PlayTomic.js,放在plugins文件夹的data子文件夹下。在这里,我们将放置我们需要与 Playtomic 一起工作的函数。

  2. 将此插件文件包含在我们的main.js脚本中,如下面的代码行所示:

'plugins.data.PlayTomic'
  1. 使用以下代码定义PlayTomic插件模块:
ig.module('plugins.data.PlayTomic').
defines(function(){
// module to store and retrieve things with Playtomic
ig.PlayTomic= ig.Class.extend({
userName : null,
success: true,
scores: null,
init: function(){
  ig.log('Trying to start Playtomic...');
  try{
 Playtomic.Log.View( 951388, 'b05b606fc66742b9','f41f965c47a14bcfa7adee84eff714', document.location );
    //your login credentials
    Playtomic.Log.Play();//game start
    ig.log('loading Playtomic success ...')//could connectmessage
  }
  catch(e){
    this.success = false; //could not connect
    ig.log('Failed loading Playtomic ...')//could notconnect message
  }
},
  1. 我们的新 Playtomic 类将负责在 Playtomic 服务器上保存玩家的分数。但是,首先需要建立与服务器的连接;这是在init()函数中完成的。在实现和测试 Playtomic 设置时,在关键时刻插入日志消息非常有用。您需要在上述代码的突出部分填写自己的连接凭据。

  2. 一旦我们建立了连接,我们就需要发送数据。由于我们要保存分数,我们需要一个saveScore方法,如下面的代码所示:

saveScore: function(name, score1){
  var score = {Name: name, Points: score1};
  Playtomic.Leaderboards.Save(score,'highscores',this.submitComplete,{allowduplicates:true});
},
  1. Playtomic类有一个leaderboards属性,您可以使用其save()方法保存玩家的分数。您需要指定要保存到高分榜中并添加分数的值。您可以在 Playtomic 网站的leaderboards设置中自己命名表格,如下截图所示:Implementing Playtomic for game analytics

  2. 我们添加了一个可选函数,用于在提交成功时给我们反馈。在使用 Playtomic 时,强烈建议跟踪所有发送和接收的数据。作为最后一个参数,我们将允许在排行榜上重复,以便一个人可以在榜单上有多个分数。

  3. submitComplete()函数只是我们跟踪特定数据传输是否成功的一种方式:

submitComplete: function( response ){
  if( response.Success ){
    ig.log( 'Successfully Logged!' ); //submit success
    ig.log( response );
  }
  else{
    ig.log( 'Unable to Save High Score!' ); //submit fail
  }
},
  1. 现在,我们唯一需要做的就是集成我们的PlayTomic分析,如下所示的代码,使用我们为使用 lawnchair 应用程序保存分数而构建的GameInfo.saveScore()函数:
this.PlayTom = new ig.PlayTomic();
this.saveScore = function(){
  this.store.save({score:this.score});
  if(this.PlayTom.success){
    try{
    //service sometimes failes to load
      this.PlayTom.saveScore(this.userName,this.score);}
      catch(e){
        ig.log("Could not load to Playtomic");
      }
    }
  }
}
  1. 我们的saveScore()方法现在不仅通过 lawnchair 应用程序在本地保存分数,还将结果发送到 Playtomic 服务器,在那里它将被放入排行榜中,如下截图所示:Implementing Playtomic for game analytics

Playtomic 还有很多内容没有涵盖到,但这将由您自行发现。通过这个简单的介绍,您应该已经有信心开始自己的游戏分析了。不过,请注意隐私规定适用且不断变化。最好在保留游戏统计数据时征得玩家的许可,并确保在实现 Playtomic 代码时考虑到这一点。

总结介绍 Playtomic 在我们的游戏中的完整过程,我们得出结论:

  • Playtomic 是移动游戏的谷歌分析工具,免费且相对容易实现。

  • 在创建 Playtomic 帐户后,您需要的第一件事是连接到他们的脚本,该脚本可以包含在您的index.html文件中。

  • 需要建立与 Playtomic 服务器的连接。这是使用您的帐户凭据完成的,尽管您可以使用示例代码中的凭据进行测试。

  • 本介绍的目标是将游戏平台上的分数发送到 Playtomic 服务器,以便在排行榜中表示。为此,我们制作了自己的 Playtomic 插件。

摘要

在本章中,我们看了一些您可以在游戏中做的更高级的事情,并将它们应用到我们在第三章中设计的 RPG 游戏中。

我们构建了一个介绍、胜利和游戏结束的屏幕,并让我们的游戏提示玩家的名字,以便在介绍屏幕上显示。

我们深入研究了如何通过单元测试调试代码,并制作了自己的 ImpactJS 调试面板。然后,我们看了一下处理数据的方法以及在玩家设备上存储数据的方法。RPG 增加了一些有趣的元素,比如通过鼠标点击移动玩家的方法,智能生成点,NPC 对话和生命条。

我们通过引入高层次的策略决策来增强我们的人工智能,比如集体智慧。最后,我们看了一下 Playtomic 以及如何将玩家分数发送到 Playtomic 数据库。

在下一章中,我们将看一看音乐和音效。目标是获得开始制作你的第一款游戏所需的基本声音和音乐。

第六章:音乐和音效

音乐和音效就像蛋糕上的樱桃:当正确实施时,它们可以极大地改善游戏体验,但如果没有,至少你还有蛋糕。现在的大预算游戏总是伴随着原创和精美的歌曲和曲调。游戏音乐领域在过去几十年里已经发展壮大,有许多作曲家致力于制作游戏音乐。

以下是一些伟大作曲家的名单:

  • 近藤浩治(马里奥和塞尔达系列)

  • 植松伸夫(《最终幻想系列》)

  • 中村正人(索尼克、合金装备固、《银河战士 Prime》系列)

  • 迈克尔·贾奇诺(《使命召唤》、《荣誉勋章:联合突击》)

  • 比尔·布朗(《命令与征服将军》、《敌领土》、《彩虹六号》)

  • 杰瑞米·索尔(《上古卷轴》系列、《星球大战:旧共和国》、《全面毁灭》、《无冬之夜》、《博德之门》、《公会战争》、《英雄连》、《普特普特》)

这些人确实知道如何制作令人惊叹的音乐,为游戏体验增添了难以置信的附加值。这些游戏中使用的曲调通常变得与游戏本身一样具有标志性和令人难忘。如果你观看一部非常古老的电影,你会注意到它们使用的音乐和音效要比现在少得多,这使得它们对我们许多人来说几乎无法观看。尝试从任何最近的电影中剥离所有的背景音乐,你可能会发现它看起来乏味,即使故事内容保持不变。对于许多游戏来说也是如此,特别是对于冒险游戏来说,精心谱写的背景音乐非常重要,因为它有助于将你带入故事情节中。

同样,没有音效和威胁性音乐的恐怖游戏几乎是不可想象的。曲调和音效对于营造场景的氛围至关重要。一个很好的例子是著名的游戏《生化危机》。在这款僵尸游戏中,即使 20 分钟内什么都没有发生,你仍然会时刻保持警惕。正是声音和威胁性的音乐让你本能地不愿意打开下一扇门。因此,在选择音乐和音效之前,考虑一下你希望玩家在玩游戏时产生的感觉。对于唤起感觉来说,没有什么比完美选择的音乐和声音更有影响力了。

在本章中,我们将看一下一些游戏音乐的来源,除了这些相当昂贵的作曲家。我们将简要介绍一下 FL Studio,它可以用来创作你自己的音乐。最后,我们将在 ImpactJS 中整合音乐和音效。

制作或购买音乐

如果你决定要为你的游戏添加一些音乐,问题仍然是要么自己制作,要么购买。似乎作为一个 2D 游戏开发者,你需要了解一点点所有的东西:你必须能够理解游戏心理学,实际编写游戏程序,为其制作图形,甚至创作其音乐。听起来你需要成为一个全能的人才才能完成这样的壮举。然而,在图形设计和音乐领域进行教育可能是浪费时间。虽然成为一个通才是一个很好的特点,但考虑一下为你的游戏创作音乐需要多少时间,而不是从别人那里购买。

在本章中,这两种选择都得到了支持。首先,让我们看看一些可以为你提供音乐和音效的网站。

购买曲调和音效

如果你需要一些游戏音乐,你可以像杰瑞米·索尔一样雇佣一位个人作曲家。然而,假设你没有数百万美元的预算,以下网站可能会有所帮助:

www.craze.se

Craze上,可以找到各种类型的音乐。这些歌曲可以提前听,并且价格从每首 15 美元到 60 美元不等。它们也可以作为包购买,这将大大降低总成本。

如果你正在寻找一个价格相对更实惠的供应商,你可以看看以下链接中的Lucky Lion Studios

www.luckylionstudios.com

大多数曲目售价为 5 美元。他们接受定制委托,并且甚至会区分购买定制项目的独家或非独家权利,从而让您在定制任务上节省成本。

最后,如果您正在寻找一些免费音乐,可以在以下链接找到Nosoapradio

www.nosoapradio.us

这个网站拥有一切;您可以随意收听和下载超过 300 首曲目(超过 12 小时的音乐),而且完全免费使用。该网站甚至提供了一个种子文件的追踪器,让您一次性下载 1GB 的音乐。这是一个很棒的网站,如果您希望有一些音乐作为占位符,甚至发布一个真正的游戏。

还有一些网站可以购买音效:

  • Pro sound effects允许您以每个效果 5 美元的价格从各种不同的声音中购买,链接如下:

www.prosoundeffects.com

您还可以购买特定主题的整个音效库,例如动物声音。这些套餐的价格范围可以从 40 美元到 15000 美元不等。

  • Radish patch每小时以 45 美元的价格提供定制工作,还以 8 美元或 80 美元的价格出售预制音效,具体取决于您的计划。链接如下:

www.radish-patch.com

如果您计划销售超过 5000 份游戏,他们将收取每个音效 80 美元,而不是 8 美元。

  • 列表中还有一个免费网站供您使用,链接如下:

www.mediacollege.com/downloads/sound-effects/

Media college提供了大量免费的声音效果,涵盖了各种主题。他们唯一要求的是,如果您喜欢他们提供的内容,可以捐赠一些费用。

与优质音乐不同,音效并不难制作。您只需要一个所需声音的列表,一个体面的录音机,一点空闲时间(也许还有一些疯狂的朋友来帮助您制作)。因此,在决定是自己制作还是购买音效时,建议自己制作,除非您需要一些真正优质的效果。

现在让我们来看看 FL Studio 的基础知识。

使用 FL Studio 制作基本曲调

FL Studio是一款数字音频工作站,以前被称为 FruityLoops。以下是 FL Studio 的标志:

使用 FL Studio 制作基本曲调

FL Studio 不是免费软件,而是一个可以从他们的网站下载的演示版本:

www.fl-studio.en.softonic.com

FL Studio 被认为是目前最完整的虚拟工作室。但是,FL Studio 目前尚不适用于 Linux。

对于 Linux 用户,LMMS可能是一个不错的(免费)但功能较弱的替代品。以下是 LMMS 的标志:

使用 FL Studio 制作基本曲调

您可以从以下链接下载 LMMS:

lmms.sourceforge.net/download.php

由于本书的目的不是深入了解音乐制作,因此这里只涵盖了 FL Studio 的基础知识。

打开 FL Studio 时,首先注意到的是顶部菜单栏,如下面的截图所示:

使用 FL Studio 制作基本曲调

我们大致可以区分三个主要部分。在左侧是您可以期望任何程序都具有的所有菜单:文件工具视图选项等。栏的中间提供了快速访问播放、停止和其他与您正在处理的歌曲直接相关的按钮。在右侧,我们可以找到一些快速访问按钮,用于 FL Studio 的重要元素。

创建新文件时,FL Studio 允许您从模板开始,这对于初学者来说非常方便。

使用 FL Studio 制作基本曲调

例如,Basic with limiter将立即为用户提供鼓线的不同元素。这样,您就不需要自己找出基本组件。FL Studio 的五个最重要的元素的快速访问按钮从左到右依次是:播放列表、步进序列器、钢琴卷、文件浏览器和混音器,如下面的屏幕截图所示:

使用 FL Studio 制作基本曲调

如果您打开步进序列器,您会注意到您的第一个序列Pattern 1已经预定义了四个元素:KickClapHatSnare。如下列表所述,这四个元素构成了您鼓线的基础。

  • Kick可以比作您的大鼓。

  • Clap是拍子的近似。Clap(也称为 tala)本身是印度古典音乐中用于任何作品的节奏模式的术语。

  • Snare代表较小的鼓。

  • Hat是您鼓线的钹。

以下屏幕截图显示了Pattern 1序列:

使用 FL Studio 制作基本曲调

在您的模式中,每个乐器都有一系列矩形。通过单击一个矩形,您告诉 FL Studio 在该特定点激活这个乐器。右键单击突出显示的矩形将再次关闭它。FruityLoop studio 中的几乎所有内容都是通过左键单击打开或添加的,而右键单击用于关闭或删除。尝试通过在特定时间间隔激活一些乐器来制作出声音不错的鼓线。

创建了一个模式后,可以将其添加到播放列表。播放列表控制台将保存项目中所有音乐的所有部分,如下面的屏幕截图所示:

使用 FL Studio 制作基本曲调

您的所有模式可以根据您使用它们的方式同时或顺序地进行排队或播放。在播放列表控制台中左键单击一个位置,基本上是在该位置绘制一个模式。右键单击一个模式将其删除。要更改模式,您当前正在放置的下拉框位于播放列表控制台的右上角。

FL Studio 为用户提供了各种乐器、音效,甚至预制音乐和一些语音效果,如下面的屏幕截图所示:

使用 FL Studio 制作基本曲调

所有这些资源都可以通过文件浏览器访问。从这里,您可以向您的序列构建器添加乐器,例如合成器或吉他。每种声音类型都有不同的符号,如下面的屏幕截图所示,甚至可以在浏览器中预览(或提前听到)预制音乐:

使用 FL Studio 制作基本曲调

添加预先编排的旋律可以让您快速制作出一首相当不错的歌曲,然后可以将其合并到您的游戏中。

如果您已经向您的序列构建器添加了乐器,比如合成器,请尝试打开其钢琴卷控制台,如下面的屏幕截图所示:

使用 FL Studio 制作基本曲调

钢琴卷控制台允许您定义乐器需要演奏的每个音符。对于一些乐器,比如鼓,这并不总是必要的,但对于其他乐器来说,绝对建议在钢琴卷控制台中制作自己的小曲调。您可以在与鼓线相同的模式中进行,或者您可以开始一个不同的模式,在那里释放您的创造力,如下面的屏幕截图所示:

使用 FL Studio 制作基本曲调

最终,您创建的每一首音乐都应该最终进入播放列表。使用不同的音轨是保持对同时发生的所有事情的良好视图的关键。如果您忘记将不同的乐器类别分配到不同的音轨中,不用担心,在播放列表窗口中有一个拆分它们的选项。

在某个时候,您会想要听听您的不同音轨一起播放时的声音。为此,您需要从模式切换到歌曲模式,如下图所示:

使用 FL Studio 制作基本曲调

如果您觉得需要对不同的乐器进行一些额外的调整,这就是混音器控制台发挥作用的地方。混音器控制台允许您更改音量、平衡和特殊效果,如下图所示:

使用 FL Studio 制作基本曲调

向音乐添加特殊效果或滤镜可以快速为您提供所需的额外触感。有许多预设的滤镜可供选择,它们都可以单独进行调整。如果您正在寻找一个快速解决方案,当然可以将它们保留在默认设置并进行操作。

在这四个元素中的每一个:序列器、播放列表、钢琴卷、和混音器中,都有一些模板和/或默认设置可用。如果您不想花太多精力来创建自己的音乐,请务必寻找这些。您可以使用已经存在的内容,稍作调整,很快就可以拥有自己的配乐!

当您完成第一首歌曲时,您可能不仅想要保存它,还想将其导出为.mp3.ogg文件。

使用 FL Studio 制作基本曲调

同样,不要忘记将项目切换到歌曲模式,而不是模式模式,否则您只会导出当前选择的模式。

一旦歌曲被导出,您可以在 ImpactJS 中使用您刚刚创建的内容。

将背景音乐添加到您的游戏中

背景音乐是您希望始终播放的东西。很多游戏会在情况变得艰难时,将音乐从平静变为更加热烈的音轨。所有这些都可以使用if条件来在您的主代码中或专门用于管理播放列表的单独文件中完成。

ImpactJS 有两个重要的类负责您想要使用的所有声音:ig.musicig.soundig.music是我们需要的背景音乐。假设您想要将您的音乐添加到第三章的项目中,让我们建立一个角色扮演游戏或第四章的项目中,让我们建立一个横向卷轴游戏。将以下代码添加到main.jsMyGame定义的init()函数中:

init: function() {
  this.loadLevel(LevelLevel1);
  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');
  ig.input.bind(ig.KEY.MOUSE1,'attack');
  var music = ig.music;
  music.add("media/music/background.*");
  music.volume = 1.0;
  music.play();
},

请注意,我们将我们的歌曲添加为background.*,而不是background.oggbackground.mp3。这样游戏就知道它需要查找所有名为background的文件,而不管它们的扩展名是什么。由于我们在media文件夹中创建了一个单独的music文件夹,这里不应该有命名冲突。使用background.*不仅方便(一行代码而不是两行),而且对于系统使用music文件也是有帮助的。有时这将是.mp3,有时是.ogg;至少现在可以自动确定要使用的music文件。Chrome 现在似乎更喜欢 WebM 而不是.mp3.ogg,但仍然可以使用.mp3.ogg。另一方面,Firefox 更喜欢使用.ogg,而不使用.mp3

ig.music本身就是一种播放列表,具有多个功能。使用add()方法将在播放列表的末尾添加另一首歌曲。您可以用几乎无限数量的歌曲填充这个列表。music.volume方法设置了您的歌曲音量,范围从01music.play()方法将激活播放列表中的第一首歌曲。前面的代码不仅会激活您的歌曲,而且会无限循环播放,因为这是默认设置。除了简单启动循环的方法之外,还有许多其他函数。

fadeout(time)将使您的歌曲在您指定的时间内淡出。当歌曲的音量达到0时,它将调用stop()方法,停止歌曲的播放。在 ImpactJS 中有您在常规收音机上期望的一切。您可以使用pause()next()方法,以及looprandom属性使歌曲循环和随机播放。另一个有趣的属性是currentIndex,因为它将返回当前播放歌曲在播放列表中的位置。这在管理歌曲顺序并在必要时切换歌曲时非常有用。

当发生某个动作时播放声音

ig.music非常适合用于音乐,因为它与基本媒体播放器有许多共同的功能。对于播放音乐,ig.music是最佳选择,而对于播放音效,您应该使用ig.sound以获得最佳效果。

声音效果并不是持续活动的,而是只在执行某些动作时发生。比如说,当玩家发射抛射物时,我们希望听到枪声。我们需要在玩家的init()方法中添加声音,这样它就可以作为资源使用。

player.js中使用以下代码添加this.gunshotsound

init: function( x, y, settings ) {
  this.parent( x, y, settings );
  // Add the animations
  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]);
  //set up the sound
  this.gunshotsound = new ig.Sound('media/sounds/gunshot.*');
  this.gunshotsound.volume = 1;
},

然后,通过在player.js中添加以下代码,我们实际上播放了抛射物发射时的声音。

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();
    this.gunshotsound.play();
  }
}

ig.music中,歌曲被添加到播放列表中,声音是通过调用ig.sound的新实例来启动的。当只有一首歌曲被添加到音乐播放列表时,默认情况下它会永远循环。这对于使用ig.sound启动的音效并不适用,因为它没有loop属性,因此在调用.play()方法时,声音只会播放一次。ig.sound具有.enabled属性,默认设置为true。将其设置为false将为游戏停用所有声音和音乐。这很有用,因为一些移动设备在需要同时播放两种不同的声音时仍然存在问题。同时播放两种不同的声音是非常常见的,特别是如果您已经在播放背景音乐。通过使用 Ejecta,ImpactJS 的直接画布解决方案,可以解决这个问题。代码保持不变,但是 Ejecta 目前只支持 iPhone 和 iPad,不支持 Android 或 Windows 设备。

在游戏中使用声音文件的技巧

优化声音文件意味着保持简短和简单。大多数游戏都有短小的歌曲,不会太过于显眼,因此不会总是被注意到。即使不被注意到的歌曲仍然会影响情绪,而且不会显得太过重复。为了优化目的,有一些事情您一定要注意:

  • 保持歌曲简短,并且只在需要时将其加载到内存中。

  • 准备相同的歌曲,分别以.ogg.mp3格式,这样需要播放的系统可以选择最有效的扩展名。

  • 使用以下代码在目标发布游戏的移动设备上双重检查您的声音是否有效。如果没有,请确保在这些设备上关闭所有声音,直到能够使声音在这些设备上正常工作为止。

if(ig.ua.mobile){
  ig.music.add("media/music/backgroundMusic.ogg");
  ig.music.play();
}
  • 这不仅仅是一种优化,更是一种用户友好的措施,但请确保允许玩家关闭音乐和音效。最好是分开两者:有些玩家喜欢听枪声,但不喜欢你的音乐。如果你使用游戏分析,请确保跟踪这些变化,以便了解哪种类型的歌曲是可以接受的,哪种是不可以接受的。

总结

在本章中,我们讨论了音乐和音效作为在游戏中营造氛围的重要元素。我们讨论了是否应该购买或创建音乐以及你可以在哪里找到它:免费或付费。我们使用 FL Studio 创建了自己的基本背景音乐,并将其添加到了我们的游戏中。最后,我们总结了在 ImpactJS 中使用音乐的一些建议。

在下一章中,我们将看一下图形。我们将检查是购买还是制作它们更好,以及如何使用 Inkscape 或 Photoshop 创建图形。

第七章:图形

你可以有完美运行的脚本,但如果没有东西可看,就没有游戏。图形很重要,我们将在这里探讨如何获得它们并在 ImpactJS 中实现它们。在本章中,你将学到:

  • 不同类型的图形

  • 在决定是制作还是购买图形时你应该考虑什么

  • 如何使用免费工具 Inkscape 制作矢量图形

  • 如何利用 Adobe Photoshop 将现实变成游戏

自数字游戏开始以来,游戏图形一直在不断发展。快速浏览一下《太空战争!》及其街机版本《计算机空间》,《乒乓球》,《枪战》,以及许多其他古老的游戏。你会注意到的第一件事不是游戏玩法的不同,而是缺乏图形的华丽。更快的计算机和专用图形处理器的发展使得游戏变得越来越漂亮。当然,有一个普遍的趋势朝着更多的现实主义发展:我们能让游戏看起来多像现实生活而不会把我们的处理器烧毁?这有点像绘画的发展。画家们倾向于追求更多的细节,更好地逼近现实生活中所见的东西。这是一个挑战,直到他们开始使用光学透镜将图像直接反射到画布上。然后他们只需要描绘和上色。艺术家们开始寻找在画布上表达自己的新方法,因为完美不再是成功的保证。几个世纪后,当图形完美达到时,世界看到了像毕加索的《格尔尼卡》和爱德华·蒙克的《尖叫》这样的绘画。这两者都远非接近完美的现实主义;但它们都有一些东西能吸引人们。

在游戏世界中似乎正在发生类似的事情。最近的游戏已经证明我们可以非常接近现实,一些游戏开发者已经开始寻找更原创的外观。例如,任天堂从未努力接近提供逼真的图形,但他们在制作优秀游戏方面的技能在全世界都受到尊敬。这是因为他们明白,在玩家心中激起某种感觉比展示玩家从屏幕上看到的东西更重要。

看看 1995 年发行的超级任天堂游戏《耀西岛》。这里描绘的场景远非现实。然而,只要玩上 10 分钟,你就会充满快乐的感觉。一切看起来都是如此快乐和闪闪发光,色彩明亮而快乐。当它们不打算杀死你时,动物甚至云朵都会用真诚的快乐微笑着看着你。

《塞尔达传说:风之杖》于 2003 年发布,是最早使用卡通渲染图形的大型游戏之一。卡通渲染或卡通风格的图形看起来就像是手绘的。卡通渲染已经被许多其他非常成功的游戏使用,如《无主之地》和《大神》。

之前的例子是 3D 游戏,但你现在正在阅读这篇文章就证明了制作游戏并不仅仅是关于图形。许多年前,游戏成功地从 2D 过渡到 3D。即使我们心爱的马里奥也能够出色地完成这个过渡。3D 游戏通常被认为比 2D 游戏更令人愉悦。然而,你现在正在准备制作一个 2D 游戏。这证明了漂亮的图形对传达某种感觉很重要,但你可以以任何你希望的形式来传达这种感觉,就像艺术本身一样。

制作/购买图形

在制作游戏时,我们需要购买或制作自己的图形吗?我们至少有幸在这方面有选择。对于 3D 游戏,自己制作图形的选择通常受到开发团队规模的限制。对于 2D 游戏,自己完成所有工作的选择仍然是一个现实的选择。如果你没有预算购买精灵和瓷砖集,你有三个主要选项来创建你的图形:

  • 像素艺术

  • 矢量艺术

  • 使用 Photoshop 创造现实

在这三个选项中,逐像素绘制你的角色和场景是最雄心勃勃的选择。优秀的艺术家可以用这种方法得到非常好的结果,但即使是最有经验的像素艺术家也会花费数小时来绘制几个角色和瓷砖集。有一些工具可以帮助你将自己的绘画技能转移到电脑上,比如数字绘画笔和软件:Adobe Photoshop 或其免费的对应物 GIMP。如果你对绘画没有任何经验,也没有强烈的冲动去投入精力学习,那就干脆不要尝试。

第二个选择是矢量图形设计。矢量图形与像素艺术不同,因为图形是由线条和基本形状构建而成,而不是单独的点。这些线条和形状可以自由缩放到更高或更低的分辨率,而对于像素艺术来说,这可能非常困难。从基本形状如矩形、圆形和线条构建图形需要一种不同于常规绘画的洞察力。制作图形的先决条件基本上是从需要稳定的手转变为对物体和生物的分析视角。以《愤怒的小鸟》中的鸟为例。它们的基本形状是一个圆,眼睛放在中心的圆形上。它们的喙略呈圆形三角形,它们的眉毛和尾巴只是一堆矩形。如果你从这种更分析的角度看这些愤怒的小鸟,那么自己画一个就会变得更容易。如果你觉得自己有一些分析洞察力,即使你的绘画技能只是普通水平,只要你付出足够的努力,你就可以制作自己的瓷砖集。本章将简要介绍如何做到这一点。

最后一个选择更像是一个快速解决方案。通过拍摄物体的照片并将其转换为瓷砖集,你可以迅速获得一些图形。虽然对于 3D 游戏来说,接近真实场景是非常困难的,但对于 2D 游戏来说,这实际上是最简单的方法。当然,这里的主要问题是,如果使用调整后的图片,你很难在竞争对手中脱颖而出,这在推广游戏时是一个真正的缺点。尽管如此,这些图形看起来很好,这是一种快速而廉价的获取图形的方式。

购买图形的地方

尽管 2D 游戏相当普遍,但并没有很多公司专门为业余游戏开发者提供瓷砖集。大多数游戏艺术家要么为游戏公司工作,要么按照客户的要求工作,这往往对于业余时间开发游戏的人来说太昂贵了。

然而,有一些价格实惠的 2D 游戏图形制作商,例如www.sprites4games.com。他们有一些免费的精灵可用,但他们尤其因其美丽而实惠的定制作品而备受赞誉。

从随机网页下载免费瓷砖集时,存在两个主要问题:

  • 瓷砖集非常不完整,因此实际上无法用它们来创建整个游戏。

  • 免费瓷砖集的另一个问题是它们实际上并不是免费的。它们经常是从现有游戏中剥离出来的,重新使用它们是违法的。

例如,在www.spritedatabase.net,你可以下载整个游戏的瓷砖集。但实际上使用它们来发布你自己的游戏可能会导致因侵犯版权而被起诉。

有时你也可以在更大的艺术和照片网站上找到瓷砖集,比如www.shutterstock.com。问题在于混乱;在所有这些其他图片中找到实际的游戏图形是很困难的。如果你最终找到了一些,你将面临与免费瓷砖集相同的问题:不完整。在那时,你可以联系艺术家并请求更多的图形,但那又变成了定制工作,这往往会变得相当昂贵。

矢量图形介绍

现在我们已经看过了不同的选项,让我们深入了解其中一个:创建我们自己的矢量图形。有几种有趣的工具可以帮助你。Adobe Illustrator 是市场上最好的之一。然而,在这里我们将使用一个稍微不那么先进但免费的工具:Inkscape。你可以在他们的网站上下载 Inkscape:[www.inkscape.org/download/](http:// www.inkscape.org/download/)。

一旦我们在计算机上安装了 Inkscape,我们就可以开始制作一个机器人角色。

有几种方法可以绘制自己的角色或物体。真正的艺术家使用钢笔工具来完成,如下面的截图所示:

矢量图形简介

这是一个非常多才多艺的绘图工具,它使您能够绘制直线和最完美对称的曲线。然而,在这个简短的初学者教程中,我们将限制自己使用基本形状,如矩形和圆来构建我们的小机器人,如下图所示:

矢量图形简介

这实际上将是一个小机器人,因为我们希望它的大小与我们一直在使用的角色相同:48 x 32 像素。尽管矢量图形是可伸缩的,但最好还是按照要使用的比例来工作。在处理这些小分辨率时,实际上看到你要填充的像素是有意义的。您可以通过在“视图”选项卡下打开“网格”选项来实现这一点。此外,您需要在放大的图片和实际大小之间切换;这样你就可以看到你实际上要在游戏中放入多少细节。放大和缩小可以使用鼠标的 Ctrl 键和滚轮来完成;此外,通过按键盘上的“1”键,可以简单地以 1:1 的比例查看所有内容。

矢量图形简介

当我们看我们想要构建的机器人时,可以注意到一些重要的东西:头部被放大了。通常,人类的头部大小应该是人体的八分之一或七分之一。在低分辨率下绘制时,头部大约应该是身体大小的三分之一到一半。这是非常不现实的,但至少你能看到一些面部特征,比如眼睛和嘴巴。这种大头风格被称为千变,意思是日语中的“矮个子”;它非常适合小动画。

让我们首先看一下我们需要的基本形状。这似乎不过是一些矩形(圆角和普通的)和两个椭圆形的眼睛,如下面的截图所示:

矢量图形简介

矩形的角可以通过选择普通矩形并在下面的面板中更改其角的半径来轻松圆角,如下面的截图所示:

矢量图形简介

椭圆形只不过是一个拉长的圆。您可以在任何方向拉伸任何形状,并在必要时旋转或倾斜它,如下面的截图所示:

矢量图形简介

在处理矢量图形时,最好有不同的图层来处理不同的动画。例如,如果我们想让我们的机器人行走,我们需要它的一只胳膊和腿伸出,然后是另一只胳膊和腿。从动画的角度来看,将身体和手臂和腿放在单独的图层中是有意义的。身体的形状在移动时不会改变,而肢体的形状会改变。

现在我们有了基本的形式,让我们专注于颜色。在低分辨率下工作时,最好是有很大的对比度。你可以通过选择一个接近白色和一个接近黑色的颜色来实现这一点,从而调整亮度。或者,你可以选择使用两种互补颜色。当两种颜色互补时,它们是彼此的对立面,当它们相邻时产生最大的对比度。因此,在选择颜色时,引入色轮是有用的。在这个色轮上,彼此相对的颜色被认为是互补颜色。例如,黄色的补色是紫色,如下图所示:

矢量图形介绍

我们的机器人将是灰色和黑色。为了给它上色,我们只需要右键单击鼠标按钮,选择填充和描边,并用我们喜欢的颜色填充它。

矢量图形介绍

此外,我们可以通过在我们的圆圈中切换到不完整的弧线来给我们的机器人的眼睛增加一些额外的细节,使用下图中显示的面板。

矢量图形介绍

我们的机器人现在有了一个可识别的形式,甚至有了这个小眼睛细节。这些细节的问题在于,当实际玩游戏时,它们并不总是可见,如下图所示,我们的机器人角色的最小化形式;找到合适的细节量可能会有些棘手。

矢量图形介绍

我们可以像下图所示在他的头上加上天线,虽然很小,但仍然是可识别的;最终,这是你需要考虑的每一个细节。让我们在角色的下图所示的绘画中加入一点阴影。我们可以通过将填充改为渐变图案而不是均匀填充来实现这一点。

矢量图形介绍

此外,通过使用这些渐变阴影图案添加一些额外的形式,我们可以使设计看起来更加逼真。作为练习,你可以为角色空闲时添加自己的动画。例如,一个人会吸气和呼气,使他的胸部上下起伏。为了描绘这一点,你可以添加一张额外的图像,使游戏感觉更加生动。最终,我们得到了我们的最终机器人。教它如何行走只是把最好的一面展现出来,然后当然是另一面。如果你在一个图层中工作,可以通过选择它们并按下Home键将一条腿和一只胳膊移到前面来完成。按下End键将选定的手臂放在其他形式的后面。或者,你可以使用对象菜单来实现同样的事情。不过,理想情况下,你会希望使用不同的图层,因为这样会让生活变得更加容易。然而,在这里我们不会深入到那个层面的细节。

矢量图形介绍

机器人看起来好像要离开他的小画面,直接走向你,就像前面的图表所示的那样。要得到一个完整的角色,你至少需要为一个侧面视图和背面视图做同样的事情。一旦你有经验,这可以很快完成。然而,有一个更快的方法来获得图形。不过,你可能只想在图纸准备好之前使用它们作为占位符,这仍然是一个不错的选择。这个选择是使用 Adobe Photoshop 进行真实生活图片。

使用 Adobe Photoshop 创建你自己的头像

曾经梦想过在自己的游戏中四处走动吗?现在你可以了!你只需要一个相机和类似 Adobe Photoshop 的工具。虽然我们将使用 Adobe Photoshop,但市面上有很多免费的替代品可以胜任。甚至浏览器的解决方案也相当不错。Pixlr 就是一个很好的例子。它可以在www.pixlr.com找到。

我们将从各个相关方向拍摄一堆照片开始。最好在均匀着色的屏幕前拍摄;简单的白色毯子或墙壁也可以。如果您的背景与您想捕捉的人容易区分,那么将他或她从图片中减去将更容易。我们可以使用快速选择工具来做到这一点,如下面的截图所示:

使用 Adobe Photoshop 创建您自己的头像

在将人与背景分离后,我们可以简单地将图片放入一个带有透明背景的新文件中,甚至可以添加一些效果,以赋予它更加超现实的触感,如下面的截图所示:

使用 Adobe Photoshop 创建您自己的头像

不要局限于 Adobe Photoshop 所提供的功能。有一些很好的网站可以以你难以想象的方式转换你的图片。其中一个网站是[www.befunky.com](http:// www.befunky.com)。

在这里,我们可以选择在我们的图片上释放卡通效果,使人几乎无法辨认,同时产生出这种漂亮的单色风格,如下面的截图所示:

使用 Adobe Photoshop 创建您自己的头像

您将不得不为所有的图片重复这个过程,这可能会耗费相当多的时间。然而,这比自己绘制它们要快得多。还要记住,被动对象只需要一张图片。需要实际动画表的游戏角色代表了大部分的工作量。

现在我们有了个人精灵,让我们来看看动画表本身。如果您没有适合的照片,现在是时候去让别人在白墙前拍几张照片了。在视频游戏中看到自己有点奇怪,所以试试看吧。

将您的作品添加到 RPG

为了从个人精灵到完全成熟的动画表中,所需做的就是将它们整齐地放在一个文件中。

将您的作品添加到 RPG

在开始之前,您需要考虑您的图片需要多大。在这个例子中,它们的尺寸为 32 x 96 像素。在早期章节中,我们的角色尺寸为 32 x 48 像素。拥有比我们当前示例拉伸得更少的图纸是更可取的,因为它们将使游戏玩起来更容易。操纵一个尺寸为圆形或正方形的角色要比操纵一个又长又细的角色容易得多。然而,在这种情况下,我们的测试人员又长又瘦,我们希望保持他的样子。

实际上,在单个动画表上安排图片是一项精密的工作,因此建议使用图片的坐标。Adobe Fireworks 在设置坐标时非常直观。任何图片程序都可以胜任;甚至可以在 MS Paint 中完成。当然还有其他选择。精灵表生成器将使排列精灵并将它们保存为瓷砖集合变得更加容易。您还可以选择在 Fireworks 中使用一些 JavaScript 排列代码来自动化定位过程。但是,这里不会对这些主题进行详细阐述。

当您最终设置好自己的表格时,就该将其引入游戏中了。将文件保存为player.png,并在之前章节中的代码和表格中进行替换。

animSheet: new ig.AnimationSheet( 'media/player.png', 32, 96 ), 
init: function( x, y, settings ) {
  this.parent( x, y, settings );
  // Add the animations
  this.addAnim( 'idle', 1, [4] );
  this.addAnim('down',0.1,[3,4,3,5]);
  this.addAnim('left',0.1,[0,1,0,2]);
  this.addAnim('right',0.1,[6,7,6,8]);
  this.addAnim('up',0.1,[9,10,9,11]);
}

我们的动画序列非常短。对于每个视角,我们在静止和移动右腿或左腿之间切换。如果我们的角色是完全对称的,那么图表可能会更小。那样的话,我们只需要左右行走的动画,然后通过翻转图像来获得另一个动画,就像在前面的章节中所看到的那样。

在 HTML5 中使用图形的提示

为了结束本章,让我们回顾一下在 HTML5 中使用图形的一些要点:

  • 尽量保持动画图表尽可能小。没有必要复制某些精灵;如果必要,动画序列允许多次引用同一个精灵。还要知道,每个浏览器都有不同的图像大小限制,尽管你必须相当粗心才能达到这个限制。

  • 使用支持透明背景的文件格式。PNG 文件就可以胜任。JPG 无法保存透明背景,而会将其解释为纯白色。

  • 尽量使用对称图形。这样你可以翻转图像,使角色从左到右行走,反之亦然,使用相同的图像。这也减少了你需要的精灵数量,从而减少了制作它们的工作量。

  • 在 ImpactJS 中使用背景地图时,预渲染它们可能会很有用。背景地图与常规级别图层不同,它是通过您在脚本中提供的代码数组绘制的,而不是标准的 JSON 编码级别文件。这样就可以设置重复的背景。

var backgroundarray= [
  [1,2,6],
  [0,3,5],
  [2,8,1],
];
var background = new ig.BackgroundMap( 32, backgroundarray,'media/grass.png' );
  • 预渲染背景将使系统创建块,这是一组瓷砖。选择预渲染将需要更多的 RAM,因为需要将更大的块保留在内存中,但会加快绘图过程;这样设备的处理器上的负担就会减少。知道你有这个选项,并根据你认为 RAM 还是处理能力将成为瓶颈,你可以选择通过使用 ImpactJS 的.prerender属性来预渲染背景或不预渲染。此外,你可以设置块的大小来微调两种资源之间的平衡:
background.preRender = true;
background.chunksize = 4096;

总结

图形是任何游戏的重要元素,因为它们是游戏所代表的一切的可视化。尽管游戏中的图形确实趋向于更加逼真,但这并不是获得良好游戏体验的绝对要求。我们讨论了是否应该制作或购买图形,以及在哪里可以以实惠的价格购买定制图形。如果决定创建自己的图形,我们区分了三个重要选项:像素图形、矢量图形,以及使用 Adobe Photoshop。跳过第一个选项,我们快速了解了如何使用 Inkscape 开发矢量图形,并使用 Adobe Photoshop 将自己添加到游戏中。本章以一些关于在游戏中使用图形的提示结束。在下一章中,我们终于可以向世界展示我们的游戏,因为我们将把它部署到从常规网站到 Google Play 等多个分发渠道。

第八章:调整您的 HTML5 游戏以适应分发渠道

当您的游戏终于准备好供全世界观看时,是时候考虑可能的分发渠道了。您想让人们在网站上的浏览器中玩游戏,还是作为 Web 应用程序?也许您希望他们在平板电脑或智能手机上玩游戏,无论是在浏览器中还是作为应用程序。在本章中,我们将探讨其中几种不同的选择以及成功实施所需的工作。

在本章中,您将学到:

  • 为网络浏览器准备您的游戏

  • 为移动网络浏览器做适应

  • 将您的游戏发布为 Google Chrome 网络应用程序

  • 将游戏转换为 Android 应用程序

  • 使您的游戏在 Facebook 上可玩

  • 实施 AppMobi 的直接画布

为网络浏览器准备您的游戏

在开发过程中,您一直在 Web 浏览器中测试您的游戏。那么您的本地服务器和公共或生产服务器之间有什么区别呢?

在向公众发布您的游戏之前,您需要对其进行烘烤。烘烤游戏不过是压缩代码。这有两个优点:

  1. 压缩代码将比未压缩代码更快地加载到浏览器中。更短的加载时间总是一个很大的优势,特别是对于第一次玩您的游戏的人。这些人还不知道您的游戏有多棒,不想浪费时间看加载条。

  2. 烘烤后的代码也更难阅读。所有不同的模块,整齐地排列在单独的文件中,现在都在一个文件中与 ImpactJS 引擎一起。这使得普通用户很难从浏览器中复制和粘贴你宝贵的源代码,然后在自己的游戏中使用。然而,这并不能防止那些真正知道自己在做什么的人;代码并没有加密,只是压缩了。

用您下载的 ImpactJS 引擎一起的烘烤游戏的工具。在游戏的root目录中的tools文件夹中,您应该有四个文件:bake.batbake.phpbake.shjsmin.php。按照以下步骤来烘烤您的游戏:

  1. 用文本编辑器打开bake.bat文件,您会找到以下行:
php tools/bake.php %IMPACT_LIBRARY% %GAME% %OUTPUT_FILE%
  1. php更改为 XAMPP 或 WAMP 服务器中php.exe文件的目录。对于默认的 XAMPP 安装,这一行现在将如下所示:
C:/xampp/php/php.exe tools/bake.php %IMPACT_LIBRARY% %GAME% %OUTPUT_FILE%
  1. 保存并关闭bake.bat文件,然后双击运行它。在 Windows 上,一个命令窗口将打开,并且game.min.js脚本将被创建在游戏的root目录中,如下面的屏幕截图所示:为网络浏览器准备您的游戏

game.min.js脚本现在包含了我们所有的代码。现在我们需要做的就是更改游戏root目录中的index.html文件,使其寻找game.min.js而不是impact.jsmain.js脚本。

打开index.html文件,找到以下代码:

<script type="text/javascript" src="img/impact.js"></script>
<script type="text/javascript" src="img/main.js"></script>

用我们新的紧凑版本的代码替换以前的代码,如下面的代码片段所示:

<script type="text/javascript" src="img/game.min.js"></script>

现在,您可以剥离游戏文件夹中除index.htmlgame.min.js之外的所有代码文件,并将其上传到您的服务器。如果您购买了自己的网络空间,您可以使用免费的 FTP 程序,如FileZilla来完成此操作。

我们的游戏现在已经准备好分发了,通过将其加载到 Web 服务器,您已经可以让任何人使用。但是,我们还没有考虑移动设备上的浏览器。在我们研究这个问题之前,让我们快速回顾一下。

总结前面的内容,结论如下:

  • 在向公众发布我们的游戏之前,我们应该对其进行烘烤。烘烤游戏基本上就是压缩源代码。烘烤有两个重要优点:

  • 游戏加载到浏览器中的速度更快。

  • 代码变得更难阅读,因此更不容易被盗。然而,代码并没有加密,因此对于一个专注的人来说,解除烘烤仍然相当容易。

  • 为了烘烤游戏,我们在运行之前更改bake.bat文件。这个过程创建了一个game.min.js脚本。

  • 在将游戏上传到服务器之前,我们在index.html文件中包含game.min.js而不是main.jsimpact.js

为移动 Web 浏览器准备我们的游戏

如果您考虑到人们可能使用智能手机玩游戏,您已经实现了触摸屏控制。这方面的例子可以在第五章中找到,为您的游戏添加一些高级功能。然而,有时这还不够。您希望玩家能够像在电脑上一样在智能手机上进行操作。为了实现这一点,我们可以引入虚拟按钮。这些虚拟按钮是屏幕上的区域,它们将表现得就像它们是常规键盘键一样。我们可以在index.html文件中使用CSS(层叠样式表)代码创建这些按钮。我们可以为玩家的每个动作创建按钮。在我们的角色扮演游戏中,他需要能够向各个方向行走和射击。在侧面卷轴游戏中,他可以向左或向右移动,飞行和射击。让我们假设我们将飞行与向上移动分开。以下屏幕截图显示了我们的按钮图块:

为移动 Web 浏览器准备我们的游戏

以下是创建虚拟按钮的步骤:

  1. 打开index.html文件,并在 canvas 的 CSS 代码下面添加以下代码。如果您使用的是 ImpactJS 引擎示例提供的index.html文件,则该文件应该已经包含 canvas 的以下样式代码。另外,第三章中的让我们建立一个角色扮演游戏和第四章中的让我们建立一个侧面卷轴游戏index.html文件都包含 canvas 的以下 CSS 代码:
.button {
  background-image: url(media/iphone-buttons.png);
  background-repeat: no-repeat;
  width: 192px;
  height: 32px;
  position: absolute;
  bottom: 0px;
}
-webkit-touch-callout: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-text-size-adjust: none;#buttonLeft {
  position: absolute;
  top: 50%;
  left: 10%;
  width: 32px;
  background-position: -32px;
  height: 32px;
  visibility:hidden;
}
  1. 首先,我们定义了完整的按钮面板。它的高度为 32 像素,宽度为 192 像素(六个按钮,每个 32 像素宽)。

  2. 在这个按钮中,我们可以分别定义六个不同的部分。在这里,你可以看到左按钮的 CSS 代码。其他五个按钮使用完全相同的代码,除了它们的背景位置,因为这是它们在iphone-buttons.png图像上的位置。因此,例如,对于左按钮,位置是-32,对于右按钮,位置是0,对于上按钮,位置是-64,因为它是第三行。webkit命令是为了保持布局整洁,如预期的那样。如果没有提供这些命令,用户可能会意外地通过点击屏幕改变缩放或颜色。

  3. 然而,我们只希望我们的按钮显示在移动设备上。因此,让我们在index.html文件中用一小段 JavaScript 代码来控制,如下面的代码片段所示:

<script type="text/javascript">
  <!--//test if it is a mobile device-->
varisMobile= {
    Android: function() {
      return navigator.userAgent.match(/Android/i) ? true : false;
    },
    BlackBerry: function() {
      return navigator.userAgent.match(/BlackBerry/i) ? true : false;
    },
    iOS: function() {
      return navigator.userAgent.match(/iPhone|iPad|iPod/i) ? true : false;
    },
    Windows: function() {
      return navigator.userAgent.match(/IEMobile/i) ? true : false;
    },
    any: function() {
      return (isMobile.Android() || isMobile.BlackBerry() || isMobile.iOS() || isMobile.Windows());
    }
  };
  function mobileButtons(){
    <!-- show the mobile buttons -->
    if(isMobile.any()){
      document.getElementById('buttonLeft').style.visibility = 'visible';
      document.getElementById('buttonRight').style.visibility = 'visible';
      document.getElementById('buttonUp').style.visibility = 'visible';
      document.getElementById('buttonDown').style.visibility = 'visible';
      document.getElementById('buttonShoot').style.visibility = 'visible';
      document.getElementById('buttonJump').style.visibility = 'visible';
    }
  };
</script>
  1. 在这个脚本的第一部分中,我们定义了本地变量isMobile。如果检测到移动设备,则设置为true,否则设置为false。在第二部分中,如果isMobiletrue,则将 CSS 对象的可见性设置为visible。请记住,在index.html的 CSS 部分中创建它们时,它们的可见性被设置为hidden

  2. 在我们的index.html文件中,唯一剩下的事情就是将这些按钮作为<div>元素添加到我们的canvas元素旁边,如下面的代码所示:

<body onLoad='mobileButtons()'>
  <div id="game">
    <canvas id="canvas"></canvas>
 <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>
</body>

index.html文件现在有按钮,只有在检测到移动设备时才会显示,但这还不足以使我们的游戏准备就绪。为此,我们需要调整我们的main.js脚本。

  1. 打开main.js,并将以下代码添加到game实例的init()方法中:
if(ig.ua.mobile){
  // controls are different on a mobile device
  ig.input.bindTouch( '#buttonLeft', 'Left' );
  ig.input.bindTouch( '#buttonRight', 'Right' );
  ig.input.bindTouch( '#buttonUp', 'Up' );
  ig.input.bindTouch( '#buttonDown', 'Down' );
  ig.input.bindTouch( '#buttonJump', 'changeWeapon' );
  ig.input.bindTouch( '#buttonShoot', 'attack' );
  //alert('control setup');
}else{
  //initiate background music
  var play_music = true;
  var music = ig.music;
  music.add("media/music/backgroundMusic.ogg");
  music.volume = 0.0;
  music.play();
}
  1. 如果检测到移动设备,则虚拟按钮将绑定到游戏输入状态。因此,例如,buttonLeft元素将绑定到输入状态Left

  2. else语句中的代码会打开背景音乐(如果有的话)。正如在第六章中所述,音乐和音效,一些移动设备不允许声音重叠。因此,对于移动设备,关闭背景音乐是明智的,这样它就不会与其他音效重叠。这可能不会永远是一个问题,但现在考虑这些声音问题是明智的。

  3. 我们还需要调整我们的画布大小,以便它适合智能手机或 iPad 的屏幕。替换默认的画布调用:

ig.main('#canvas', OpenScreen, 60, 320, 320,2);

使用以下代码替换默认的画布调用:

if( ig.ua.iPad ) {
  ig.main('#canvas', MyGame, 60, 240, 160, 2);
}
else if( ig.ua.mobile ) {
  ig.main('#canvas', MyGame, 60, 160, 160, 2);
}
else {
  ig.main( '#canvas', OpenScreen, 60, 320, 320, 2 );
}
  1. 所有这些只是使用不同的画布尺寸初始化游戏,以便它适合 iPad(或其他平板电脑)和智能手机等较小屏幕。此外,这里跳过了介绍屏幕;这是一个选择,你可以在移动设备上留下它。您还可以为更多设备调整画布大小。这里只显示了 iPad 和所有其他移动设备,但当然还可以进行更多的区分。

万岁!您的游戏现在已经准备好移动设备使用了!在将其上线之前不要忘记进行烘烤;移动互联网不像常规 Wi-Fi 那样快,因此使您的文件更小在这里绝对很重要。

接下来,我们将看看如何为Chrome 网络商店制作移动网络应用,但首先让我们快速回顾一下如何为移动浏览器准备我们的游戏。

总结前面的内容,结论如下:

  • 如果我们希望玩家在移动设备上有良好的游戏体验,我们需要调整游戏界面以适应这一点。我们通过添加虚拟按钮来实现这一点。

  • 虚拟按钮的视觉方面是使用 CSS 和index.html中的图像文件创建的。我们可以根据游戏是在移动设备上玩还是在其他设备上玩来使按钮可见或隐藏。

  • 在我们游戏的main脚本中,我们需要将这些按钮绑定到游戏动作状态,以便获得与键盘相同的功能。

  • 此外,我们可以更改游戏屏幕分辨率和大小,使其更适合玩家使用的设备。

将游戏转化为谷歌 Chrome 网络商店的网络应用

网络应用是在浏览器中运行的应用程序,而不是在移动设备的操作系统上运行。要在谷歌 Chrome 网络商店发布网络应用,您需要一个谷歌 Chrome 开发者帐户,这需要支付一次性费用 5 美元。您需要一个谷歌站长帐户来确认谷歌提供给您的链接的所有权。此外,为了不使事情变得更加困难,最好获得一个免费的 AppMobi 帐户。您可以在他们的网站上做到这一点:www.appmobi.com。AppMobi 是一个非常有趣的初学者工具,有三个主要原因:

  1. 他们简化了将游戏推送到多个不同的分发渠道的过程。

  2. 他们对您的应用程序或游戏的前 10,000 个用户不收费,这样您可以先赚钱,然后再要求您分一杯羹;这的确是一个非常有吸引力的定价方案。

  3. ImpactJS XDK(跨环境开发工具包)通过创建人工视口来帮助将游戏适应不同的移动设备。它包含许多其他有用的功能,如模拟位置检测。

AppMobi 便于为以下平台构建游戏版本:iOS、Android、AppUp、亚马逊、Nook、Facebook、Chrome、Mozilla 和 hostMobi(他们自己的云主机服务)。

订阅后,您可以安装他们的 ImpactJS XDK 进行开发。安装 XDK 后,它将在 Chrome 浏览器中变得非常易于访问,并在您的地址栏旁边显示一个插件图标,如下面的屏幕截图所示:

将游戏转化为谷歌 Chrome 网络商店的网络应用

您可以登录到 appHub:AppMobi 控制面板,以访问其所有服务。我们现在特别感兴趣的是构建一个谷歌 Chrome 游戏。以下是构建 Chrome 游戏的步骤:

  1. 首次登录时,您需要通过单击以下截图中显示的按钮向您的控制中心添加一个新游戏:将游戏转变为谷歌浏览器网络商店的网络应用

  2. 为游戏命名并以压缩格式上传到服务器,如下截图所示:将游戏转变为谷歌浏览器网络商店的网络应用

  3. 您将看到 AppMobi 允许您为不同的分发渠道准备文件,如下截图所示:将游戏转变为谷歌浏览器网络商店的网络应用

  4. 在我们能够构建一个 Chrome ready文件之前,我们需要通过按下PROMOTE按钮将我们的文件推广到生产,如下截图所示:将游戏转变为谷歌浏览器网络商店的网络应用

  5. 我们想要为 Chrome 构建一个游戏,所以检查您仍然存在的问题。很可能您只需要为游戏添加一个图标。但是您需要在构建游戏之前执行此操作,如下截图所示:将游戏转变为谷歌浏览器网络商店的网络应用

  6. 如果一切顺利,您应该能够下载一个production文件,然后需要使用以下截图中显示的按钮将其上传到 Chrome 网络商店:将游戏转变为谷歌浏览器网络商店的网络应用

  7. 现在是时候将文件上传到 Chrome 网络商店了。但是在这样做之前,打开您刚从 AppMobi 网站下载的压缩文件夹,并确保 Chrome 图标的名称与 AppMobi 添加的manifest.json文件中所述的名称完全相同。这是一个已知的问题,Chrome 不会接受不一致的命名。

  8. 如果您是第一次上传,您将收到一条消息,说您需要验证谷歌提供给您的域名所有权。为了做到这一点,您必须将谷歌允许您下载的带有标记的 HTML 文件插入到您首次上传到 AppMobi 的捆绑包中,并重新上传您的游戏到 AppMobi,这次在压缩的捆绑包中包含验证文件。在 AppMobi 中,使用UPDATE QA按钮上传新文件。之后不要忘记推广到生产。将游戏转变为谷歌浏览器网络商店的网络应用

  9. 在谷歌站长工具中,您需要添加谷歌提供的链接并进行验证,如下截图所示:将游戏转变为谷歌浏览器网络商店的网络应用

  10. 现在您可以重新上传到 Chrome 网络商店,并填写所有必要的元素。您需要添加游戏的详细描述、定价方案和截图,使用以下截图中显示的按钮:将游戏转变为谷歌浏览器网络商店的网络应用

如果一切顺利,您将能够将您的游戏作为网络应用进行测试,并将其添加到您的谷歌 Chrome 中。如果出现问题,AppMobi 有大量关于其服务的文档和如何使用它们的信息。

现在我们有了一个网络应用,但是我们可以通过大致相同的过程来获得一个真正的应用程序。在本书中,我们将以 Android 为例。在这之前,让我们快速回顾一下。

总结前面的内容,结论如下:

  • 网络应用是在浏览器中运行的应用程序,而不是直接在设备的操作系统上运行。谷歌浏览器在其谷歌浏览器网络商店中提供此类网络应用。在商店发布需要支付一次性 5 美元的订阅费。

  • AppMobi 提供了一种构建 Web 应用和应用的简单方法。注册是免费的,但一旦游戏取得一定的成功,就需要付费。

  • 烘烤好的游戏需要被压缩并上传到 AppMobi 服务器。在这里,AppMobi 会对其进行适配,然后你可以重新下载并上传到 Chrome 开发者账户。

  • 谷歌会提供一个链接,你需要重新上传到 AppMobi 并通过谷歌站长账户进行验证。

  • 链接验证通过后,你可以重新上传游戏到 Web 商店,并填写游戏描述等详细信息。

  • 在提交应用程序进行审核和发布到公众之前,你可以在浏览器中测试你的游戏。

将游戏推送到 Android 的谷歌应用商店

现在我们知道了如何构建 Web 应用,让我们在谷歌应用商店上构建一个真正的移动应用。我们将再次利用我们的 AppMobi 账户来完成这项工作。但是,此外,你还需要一个谷歌开发者账户,每年需支付 25 美元,用于在谷歌应用商店发布你的游戏。以下是将游戏推送到谷歌应用商店的步骤:

  1. 使用上传到AppMobi appHub上传游戏包或上传一个新的包。

  2. Android选项卡下选择构建,并解决你仍然存在的任何问题。如果你成功构建了 Chrome 商店的 Web 应用程序,那么只剩下一个问题:设置谷歌云消息传递。为此,你需要一个谷歌项目 ID和一个API 密钥;你需要从你的开发者账户中获取这两者。

  3. play.google.com/apps/publish/signup注册开发者账户,或者如果你已经有账户就登录。

  4. 转到你的Google APIs控制台并创建一个新项目。你可以从code.google.com/apis/console/选择你的项目 ID。

  5. 服务部分启用Android 的谷歌云消息传递,如下截图所示:将游戏推送到 Android 的谷歌应用商店

  6. 在控制中心的API 访问部分选择创建新的服务器密钥。创建新服务器后,你也会收到 API 密钥。

  7. 返回到 AppMobi appHub,在那里填写项目 ID 和 API 密钥。你现在已经设置好了推送消息。下面的截图显示了推送消息设置完成后的屏幕:将游戏推送到 Android 的谷歌应用商店

  8. 你的应用现在应该准备好构建了。点击立即构建按钮,下载apk文件,如下截图所示:将游戏推送到 Android 的谷歌应用商店

  9. 现在你需要做的就是将这个文件上传到你的开发者控制台。谷歌将要求你填写名称、描述,并添加一些截图。完成这些步骤后,你就可以开始了。

在将你的应用提交到应用商店进行审核之前,建议在多部移动设备上测试从 AppMobi 下载的构建文件是否流畅运行。你可以通过将文件上传到你自己的网站并用智能手机下载来完成测试。或者,你可以使用免费的云存储服务,如Dropbox,将文件从云端传输到你想要测试的任何设备上。

现在我们已经构建了应用和 Web 应用,我们将深入研究在Facebook上发布你的游戏的方法。在这之前,让我们快速回顾一下。

总结前面的内容,结论如下:

  • 使用 AppMobi 构建应用与构建 Web 应用的过程几乎相同。但是,为了将你的游戏发布为应用,你需要一个谷歌开发者账户,每年需支付 25 美元。

  • 如果你还没有将压缩的游戏文件上传到 AppMobi appHub,请先这样做。确保从 Google APIs 获得项目 ID 和 API 密钥。

  • 构建您的android文件并将其上传到您的开发人员帐户,然后可以将其发送进行审查。但在这样做之前,请务必在几部 Android 移动设备上测试您的游戏。

在 Facebook 上提供您的游戏

AppMobi 可以用于构建 Facebook 应用程序,但 Facebook 还允许另一种选项来展示您的游戏。您需要一个 Facebook 开发人员帐户与您的 Facebook 帐户配套使用。目前没有订阅费。您可以转到以下链接获取您的 Facebook 开发人员帐户:

developers.facebook.com

如果您已经在自己的网站上运行游戏,Facebook 允许您在您的网站上设置游戏的视口。

以下是使您的游戏在 Facebook 上可用的步骤:

  1. 在您的帐户的应用程序部分,通过单击以下按钮创建一个新应用程序:在 Facebook 上提供您的游戏

  2. 填写Facebook 应用部分,如下面的屏幕截图所示。如果您的游戏也可以在移动设备上查看,还可以填写移动网络部分。确保沙盒模式打开,直到您彻底测试了所有内容。在 Facebook 上提供您的游戏

  3. 转到应用详细信息页面,在那里您需要填写有关您的游戏的一些基本信息:类别、描述和一些屏幕截图。一旦准备好,您可以通过单击以下按钮之一来预览您的游戏:在 Facebook 上提供您的游戏

  4. 尝试返回您自己的个人资料页面,您将在应用程序列表中找到您的游戏,如下面的屏幕截图所示。单击它以玩游戏并对您自己的 Facebook 游戏进行测试。在 Facebook 上提供您的游戏

这不是将游戏发送到 Facebook 的唯一方法。您可以使用 AppMobi 制作一个真正的 Facebook 应用程序。但是,一旦您的游戏完成并存储在 Web 服务器的某个位置,这是一个非常快速的方法将其放在 Facebook 上。这种方法还有一个很大的优势:游戏仍然存储在您控制的服务器上,Facebook 仅提供一个视口。这意味着如果 Facebook 更改了某些内容,这对您的游戏的兼容性几乎没有影响,您不必在任何地方更改代码。

作为本章的最后一个主题,我们将快速查看 AppMobi 的直接画布实现。这是一个有趣的概念,因为它允许游戏运行速度比以往快得多。但是,首先让我们回顾一下。

总结前面的内容,结论如下:

  • 有几种方法可以将您的游戏带到 Facebook。由于我们已经使用 AppMobi 构建应用程序,我们将研究视口解决方案。

  • 您需要将游戏放在服务器上并拥有免费的 Facebook 开发人员帐户。

  • 转到应用部分,并创建一个具有普通画布和/或移动 URL 的新应用程序。还填写所有应用程序详细信息。

  • 在发布之前彻底测试您的游戏。您可以在您自己的个人 Facebook 页面的应用程序之间找到您的游戏。

使用 AppMobi 进行直接画布游戏加速

HTML5 游戏很棒,因为 HTML 和 JavaScript 可以被任何浏览器解释,并且转换为应用程序相当简单。易于“部署”是一个很大的优势,但它也带来了一个相当大的劣势。画布元素为了实际渲染游戏所需的资源可能是惊人的,一旦您想要同时使用许多实体,系统延迟很容易就会出现。在游戏体验中,很少有比这更糟糕的事情,这就像看幻灯片一样。但是,有一些技巧可以改善这一点,比如在第七章 图形中建议的预渲染图形。

如果你想利用直接画布提供的性能提升,实现起来相当简单。但是,首先你需要为 AppMobi ImpactJS XDK 准备好你的代码。以下是实现直接画布加速的步骤:

  1. 转到 Chrome Web Store 并安装 Impact XDK 扩展。

  2. 在 XDK 中,登录你的 AppMobi 账户并添加一个新项目。在 XAMPP(或 WAMP)库中选择你游戏的root文件夹。以下截图显示了开始新项目的按钮:使用 AppMobi 实现直接画布游戏加速

  3. XDK 会警告你尚未在游戏中包含 AppMobi 库,因此你将无法使用 AppMobi 命令。按照弹出窗口建议的方式,将以下代码复制到剪贴板中:使用 AppMobi 实现直接画布游戏加速

  4. 打开你的index.html文件,并将脚本粘贴到文档的head部分。现在你的游戏已经准备好在 Impact XDK 中查看,当需要时你可以添加 AppMobi 命令,如下面的代码片段所示:

<!-- the line below is required for access to the appMobi JS library -->
<script type="text/javascript" charset="utf-8" src="img/appmobi.js"></script>
<script type="text/javascript" language="javascript">
  // This event handler is fired once the AppMobilibraries are ready
  function onDeviceReady() {
    //use AppMobi viewport to handle device resolution differences if you want
    //AppMobi.display.useViewport(768,1024);
    //hide splash screen now that our app is ready to run
    AppMobi.device.hideSplashScreen();
  }
  //initial event handler to detect when appMobi is ready to roll
  document.addEventListener("appMobi.device.ready",onDeviceReady,false);
</script>

现在我们的游戏在 XDK 中运行。然而,我们还没有直接画布加速。

  1. 在你游戏的root文件夹中创建一个名为index.js的新脚本,并添加以下代码:
AppMobi.context.include( 'lib/impact/impact.js' );
AppMobi.context.include( 'lib/game/main.js' );
  1. 打开index.html并将AppMobi命令添加到onDeviceReady()事件监听器中。以下代码将加载index.js脚本:
functiononDeviceReady() {
  AppMobi.device.hideSplashScreen();
 AppMobi.canvas.load("index.js");
}
  1. 删除包括你的游戏和 impact 引擎脚本的以下script标签:
<script type="text/javascript" src="img/impact.js"></script>
<script type="text/javascript" src="img/main.js"></script>
  1. 删除以下的canvas元素:
<body>
 <canvas id="canvas"></canvas>
</body>
  1. 打开main.js脚本,并将以下内容添加到所需脚本的列表中:
'plugins.dc.dc'
  1. 如果你的代码中有画布样式的引用,请从中删除。例如:ig.system.canvas.style.width = '320px'

  2. 最后,删除你可能已经实现的触摸事件绑定,并用 AppMobi 版本替换它们。<div>元素可以留在index.html文件中,但你需要附加其他事件。例如,对于shoot按钮<div>元素:

onTouchStart="AppMobi.canvas.execute('ig.input.actions[\'shoot\']=true;ig.input.presses[\'shoot\']=true;');" onTouchEnd="AppMobi.canvas.execute('ig.input.delayedKeyup.push( \'shoot\' )');"

恭喜!你现在已经成功实现了直接画布加速!当在 Impact XDK 中玩游戏时,你可能会注意到画布元素的轮廓已经消失,如下面的截图所示:

使用 AppMobi 实现直接画布游戏加速

总结

本章的目标是提供在多种方式发布游戏时所需的技术准备。首先,我们看了一下烘焙游戏代码的过程,这可以缩短加载时间并使源代码不那么易读。烘焙应该在分发游戏之前进行。然后我们深入研究了通过实现触摸控制来适应移动浏览器的游戏。将游戏转换为 Web 应用程序或 Android 应用程序是通过 AppMobi 完成的。在发布到 Facebook 时,你有几个选项,我们深入研究了其中一个。在这个解决方案中,你自己的网站充当实际平台,而 Facebook 仅提供一个视口。在移动设备上,运行游戏时处理能力和内存使用可能是真正的问题。因此,我们看了 AppMobi 的直接画布实现。通过摆脱普通的 HTML 画布元素,可以消除大量的开销处理,大大减少必要的资源。

在下一章中,我们将看看作为 HTML5 游戏开发者的赚钱选择,希望能把爱好变成工作。

第九章:用你的游戏赚钱

在本章中,我们将快速了解 HTML5 游戏开发赚钱的选项。制作游戏可以纯粹是一种爱好,也可以是一种职业。然而,后者要求你制作一些非常独特和成功的游戏,因为竞争非常激烈。因此,提供一个独特的游戏主张,并得到健康的营销支持,似乎是大多数成功游戏开发者的选择。在本章中,我们将涵盖:

  • 进入游戏开发时你有的一些战略选择

  • 在安卓和苹果的应用市场赚钱

  • 游戏内广告选项及其在 HTML5 游戏中的应用

  • MarketJS 作为向出版商出售发行权的途径

你的游戏开发策略

如果你想制作一款游戏来赚钱,那么在开始制作之前,有几件事情是必须考虑的。你需要问自己的第一个问题可能是:我要为谁制作游戏?你是针对所有能玩游戏的人,还是想要针对非常特定的人群并满足他们的游戏需求?这就是广泛和小众定位之间的区别。大多数塔防游戏都是非常广泛的目标游戏,你需要建造具有不同属性的塔来抵御敌军。例如《俄罗斯方块》、《宝石迷阵》、《扫雷》和大多数轻盈的益智游戏。《愤怒的小鸟》是另一个例子,它因其简单性、可爱的图形和大量巧妙的营销而受到广泛受众的欢迎。

总的来说,休闲游戏似乎吸引大众的原因有以下几个因素:

  • 简单为主:大多数玩家在短短几分钟内就能适应游戏。

  • 几乎没有知识先决条件:你不需要已经了解一些背景故事或在这些类型的游戏中有经验。

  • 即使投入的练习时间较少,休闲玩家也往往表现不错。即使你一开始就表现不错,你仍然可以变得更好。一个你无法通过重玩变得更好的游戏不会长久。值得注意的例外是像轮盘赌和老丨虎丨机这样的机会游戏,它们确实会上瘾;但这是出于其他原因,比如赢钱的机会。

建造休闲游戏的主要优势在于几乎每个人都是你游戏的潜在用户。因此,可实现的成功可能是巨大的。《魔兽世界》是一款游戏,多年来已经从相当激烈和小众的游戏转变为更加休闲的游戏。他们之所以这样做,是因为他们已经吸引了大多数普通玩家,并决定说服大众,即使你一般不怎么玩游戏,也可以玩《魔兽世界》。试图取悦所有人的缺点是竞争的数量。在众多游戏中脱颖而出是非常困难的。特别是如果你没有一个强大的营销机器来支持它。

一个很好的例子是任何一款根据电影制作的游戏。《星际迷航》、《星球大战》、《指环王》等游戏,大多针对已经看过并喜欢这些电影的人。小众游戏也可能是小众,因为它们只针对特定的玩家群体。例如,喜欢玩第一人称射击游戏(FPS)的人,每天都这样做。实质上,小众游戏具有以下特点(请注意,它们与休闲或广泛定位的游戏相对):

  • 陡峭的学习曲线:掌握需要许多小时的专注游戏。

  • 需要一些游戏知识或经验。例如《星球边际 2》这样的在线射击游戏,你至少需要一些以前的射击游戏经验,因为你将与知道自己在做什么的人对抗。

  • 你玩游戏的次数越多,你获得的有用奖励就越多。经常玩游戏通常会获得使你在游戏中更强大的物品,从而加强了你通过更多游戏而变得更好的事实。

《星际争霸》是暴雪在 1998 年发布的一款游戏,即使有了续作《星际争霸 2》,它仍然在今天的比赛中被玩家们玩耍。原版《星际争霸》等游戏完全可以在 HTML5 中构建,并在浏览器或智能手机上运行。当《星际争霸》发布时,平均台式电脑的性能远不及今天许多智能手机。从技术上讲,道路是开放的;然而复制相同的成功水平又是另一回事。

瞄准游戏玩家群体的优势在于你可以在他们的生活中占据独特的位置。也许你的目标群体并不多,但由于游戏是专门为他们打造的,因此更容易吸引并保持他们的注意。此外,这并不意味着因为你有一个明确的目标,玩家就不能从意想不到的角落进入。你从未想过会玩你的游戏的人仍然可能喜欢你所做的事情。正是因为这个原因,了解你的玩家是如此重要,这也是像 Playtomic 这样的工具存在的原因。

利基营销的劣势是显而易见的:你的游戏很不可能超越一定的范围;它可能永远不会成为世界上最受欢迎的游戏之一。

你将要开发的游戏类型是一个选择,你在每款游戏中投入的细节数量是另一个选择。你可以尽可能多地努力去打造一款游戏。实质上,一款游戏永远不会完成。游戏总是可以有额外的关卡、彩蛋或其他精彩的细节。在规划游戏时,你必须决定你将使用霰丨弹丨枪还是狙击手的开发策略。

在霰丨弹丨枪策略中,你会快速开发和发布游戏。每款游戏仍然有一个独特的元素,应该使其与其他游戏有所区别:UGP(独特游戏命题)。但是在霰丨弹丨枪策略下发布的游戏并不提供很多细节;它们并不完善。

采用霰丨弹丨枪策略的优势有很多:

  • 低开发成本;因此每款游戏代表着低风险

  • 快速上市允许利用世界事件作为游戏背景

  • 你可以同时在市场上推出多款游戏,但通常你只需要一款成功的游戏来支付其他游戏的开支

  • 短小的游戏可以免费提供给公众,但通过出售附加内容(如关卡)来实现盈利

然而,当你采用这种策略时,并不全是美好的。有几个原因你不会选择霰丨弹丨枪策略:

  • 一个感觉不完整的游戏成功的机会比一个完美的游戏要小。

  • 将游戏投放市场不仅测试了某个概念是否可行,还使其暴露于竞争对手,他们现在可以开始制作副本。当然,你有先发优势,但并不像可能的那么大。

  • 你必须始终小心,不要在市场上乱丢垃圾,否则你可能会毁了自己作为开发者的名声。

然而,不要混淆。霰丨弹丨枪策略并不是建造平庸游戏的借口。你发布的每款游戏都应该有独特的特色——没有其他游戏有的东西。如果一个游戏没有新的特色,为什么有人会选择它而不是其他所有游戏呢?

当然,还有狙击策略,它涉及构建一个体面且经过深思熟虑的游戏,并在市场上以最大的关怀和支持发布。这是苹果等分发商敦促开发者做的事情,也是有充分理由的——你不希望你的应用商店里充斥着糟糕的游戏,对吧?一些其他游戏分发商,比如 Steam,在允许分发的游戏方面更加挑剔,使得散弹策略几乎不可能。但这也是最成功的游戏开发者使用的策略。看看 Rockstar(GTA 系列的开发者)、Besthesda(上古卷轴系列的开发者)、Bioware(质量效应系列的开发者)、暴雪(魔兽世界系列的开发者)等开发者。这些都不是小角色,但他们在市场上的游戏并不多。开发高质量游戏并希望它们能够成功显然是有风险的。为了开发一个真正了不起的游戏,你还需要时间和金钱。如果你的游戏销售不佳,这对你或你的公司来说可能是一个真正的问题。即使对于 HTML5 游戏来说,情况也可能如此,特别是因为设备和浏览器变得越来越强大。当运行游戏的设备变得更强大时,游戏本身通常会变得更加复杂,开发时间也会更长。

我们已经看了进入游戏开发业务时需要做出的两个重要选择。现在让我们来看一下允许你通过游戏赚钱的分发渠道,但在此之前让我们总结一下我们刚刚讨论过的主题:

  • 在开始开发游戏之前,决定你想要针对的目标群体非常重要。

  • 广泛的目标实际上根本就不是目标。它是关于让尽可能多的人能够接触和喜欢游戏。

  • 利基定位是深入研究和关注特定群体,并开发适合他们特定游戏需求的游戏。

  • 在开发和发布游戏时,有两种主要策略:散弹和狙击。

  • 在散弹策略中,你会快速发布游戏。每个游戏仍然具有其他游戏所不具备的独特元素,但它们不像可能那样精心制作和打磨。

  • 采用狙击策略,你只开发少量游戏,但每个游戏在发布时已经完美,并且只需要在发布补丁时进行轻微的打磨。

通过游戏应用赚钱

如果你将你的游戏制作成应用程序,你可以选择多个分发渠道,比如 Firefox 市场、IntelAppUp 中心、Windows Phone 商店、亚马逊应用商店、SlideMe、Mobango、Getjar 和苹果 Appsfire。但目前市场上最受欢迎的玩家是 Google Play 和 iOS 应用商店。iOS 应用商店不应与 Mac 应用商店混淆。iPad 和 Mac 有两个不同的操作系统,iOS 和 Mac OS,因此它们有不同的商店。游戏可以在 iOS 商店和 Mac 商店发布。还可能会有一些混淆,例如 Google Play 和 Chrome Web 商店。Google Play 包含所有适用于搭载谷歌安卓操作系统的智能手机的应用程序。Chrome Web 商店允许你向 Google Chrome 浏览器添加应用程序。因此有很多分发渠道可供选择,我们将简要介绍一下 Google Play、iOS 应用商店和 Chrome Web 商店。

谷歌 Play

谷歌 Play 是安卓的默认应用商店,也是 iOS 应用商店最大的竞争对手。

如果你想成为安卓应用开发者,需要支付 25 美元的费用,并且必须阅读开发者分发协议。

作为进入费和签署协议的回报,他们允许您使用他们的虚拟货架并享有所有的好处。您可以随意设置价格,但是对于您出售的每款游戏,Google 将收取约 30%的费用。可能会进行一些地理价格歧视。因此,您可以在比利时将价格设定为 1 欧元,而在德国收取 2 欧元。您可以随时更改价格;但是,如果您免费发布游戏,就无法回头了。之后,该应用的唯一变现方式是允许游戏内广告、出售附加内容或创建可以用真钱购买的游戏内货币。

引入用真钱购买的游戏内货币可能是一种非常吸引人的格式。这种变现方案的一个非常成功的例子可以在《蓝精灵》游戏中找到。在这个游戏中,您可以建立自己的蓝精灵村庄,包括大蓝精灵、蓝精灵和大量的蘑菇。您种植更多庄稼并建造新房子,您的城市会变得更大,但这是一个缓慢的过程。为了加快速度,您可以用真钱购买特殊的浆果,从而可以建造独特的蘑菇和其他东西。这种变现方案变得非常受欢迎,正如在《英雄联盟》、《星球边境 2》、《坦克世界》等游戏中所显示的那样。对于 Google Play 应用,这种应用内支付系统得到了 Android 的 Google Checkout 的支持。

此外,Google 允许您访问有关游戏的一些基本统计数据,例如玩家数量和他们玩游戏的设备,如下图所示:

Google Play

这样的信息可以让您重新设计游戏以提高成功率。例如,您可以注意到某个设备的独立用户数量并不多,尽管它是非常受欢迎的设备,被许多人购买。如果是这种情况,也许您的游戏在这种特定的智能手机或平板电脑上看起来不太好,您应该对其进行优化。

所有应用的最大竞争对手和发起者都是 iOS 应用商店,所以让我们来看看这个。

iOS 应用商店

iOS 应用商店是第一个这样的应用商店,在撰写本书时,它仍然拥有最大的收入。

要在 iOS 应用商店发布应用,您需要订阅 iOS 开发者计划,每年费用为 99 美元,几乎是 Google Play 的四倍。实际上,它们提供的东西与 Google Play 几乎相同;正如您在这个简短的列表中所看到的:

  • 您可以自行定价,并获得销售收入的 70%

  • 您可以每月收到无需信用卡、托管或营销费用的付款

  • 有支持和充分的文档可供您开始

更重要的是,以下是 Google Play 和 iOS 应用商店之间的区别:

  • 如前所述,注册 Google Play 更便宜。

  • 苹果的筛选过程似乎比 Google Play 更严格,这导致了更长的时间才能进入市场,甚至有更高的可能性根本无法进入市场。

  • Google Play 包含退款选项,允许购买您的应用的人在 24 小时内卸载应用或游戏后获得退款。

  • 如果您希望您的游戏能够利用一些 Android 核心功能,这是可能的,因为该平台是开源的。另一方面,苹果对其 iOS 平台非常保护,并且不允许应用程序具有相同级别的灵活性。这个元素对于游戏来说可能还不那么重要,但对于那些确实希望利用这种自由的非常创新的游戏来说可能很重要。

  • iOS 覆盖的人数比 Android 多,尽管当前趋势表明这种情况可能在不久的将来会发生变化。

  • 购买苹果设备的人和使用安卓系统智能手机或平板电脑的用户之间似乎存在显著差异。苹果粉丝对在应用上花钱的门槛似乎比安卓用户低。总的来说,iPad 和 iPhone 比其他平板电脑和智能手机更昂贵,吸引了那些对设备花更多钱没有问题的人。这种目标群体的差异似乎让安卓游戏开发者更难从他们的游戏中赚钱。

提示

如果你的游戏在 Safari 浏览器上运行,并不意味着它已经准备好被 iOS 应用商店接受。将你的游戏转换为本地应用需要一些额外的准备。同样的情况也适用于 Chrome 浏览器和 Google Play 商店。从浏览器游戏转换为应用可以使用 AppMobi,就像在第八章中展示的那样,调整你的 HTML5 游戏到分销渠道

在这里我们将讨论的最后一个销售应用的选择是 Chrome 网络商店。

Chrome 网络商店

Chrome 网络商店不同于 Google Play 和 iOS 应用商店,它提供的是专门为 Chrome 浏览器而不是移动设备的应用。

Chrome 商店提供网络应用。网络应用就像你在 PC 上安装的应用程序,只不过网络应用安装在你的浏览器中,大多数是使用 HTML、CSS 和 JavaScript 编写的,就像我们的 ImpactJS 游戏一样。关于 Chrome 商店值得注意的第一件事是发布应用的一次性 5 美元入场费。如果这本身还不够好,那么销售应用的交易费仅为 5%。这与 Google Play 和 iOS 应用商店有着显著的不同。如果你已经为自己的网站开发了一款游戏,并将其打包为安卓和/或苹果的应用,你也可以在 Chrome 网络商店上发布。将你的 ImpactJS 游戏转换为 Chrome 商店的网络应用可以使用 AppMobi,但 Google 本身提供了如何手动操作的详细文档。

网络应用的最大好处之一是简化了权限流程。假设你的网络应用需要用户的位置才能运行。而 iPad 应用每次需要位置数据时都会请求权限,网络应用只在安装时请求一次。

此外,它提供与 Google Play 相同的功能和支付方式。例如,还有一个包括免费试用版本的选项,也就是所谓的免费增值。免费增值模式是指允许免费下载演示版本,并提供升级到完整版本的选项。《蓝精灵》游戏也使用了免费增值模式,尽管有所不同。整个游戏是免费的,但玩家可以选择用真钱购买一些原本需要花费大量时间才能获得的东西。在这种免费增值模式中,你为方便和独特物品付费。例如,在《星际争霸 2》中,获得某个狙击步枪可能需要你花费几天或 10 美元,这取决于你选择如何玩免费增值游戏。

如果你计划为安卓发布 ImpactJS 游戏,那么在 Chrome 网络商店发布也是毫无理由不这样做的。

总之,让我们快速回顾一下:

  • iOS 应用商店是唯一的应用商店的时代早已一去不复返;现在有许多可供选择的应用商店,包括 Firefox Marketplace、Intel AppUp Center、Windows Phone Store、Amazon Appstore、SlideMe、Mobango、Getjar、Appsfire、Google Play 等。

  • 目前最大的应用商店是 Google Play 和 iOS 应用商店。它们在几个方面有很大的不同,其中最重要的是:

  • 订阅费

  • 筛选流程

  • 吸引的受众类型

  • Chrome 网络商店销售的是像普通应用一样的网络应用,但只能在 Chrome 浏览器中使用。

  • Chrome 网络商店便宜且易于订阅。你一定要试试在这个平台上发布你的游戏。

游戏内广告

游戏内广告是另一种赚钱的方式。游戏内广告是一个不断增长的市场,目前已经被主要公司使用;巴拉克·奥巴马在他 2008 年和 2012 年的竞选中也使用了游戏内广告,如下游戏内截图所示:

游戏内广告

有一种趋势是更加动态的游戏内广告。游戏制造商确保游戏中有广告空间,但实际的广告内容是后来决定的。根据对你的了解,这些广告可以随后变化,变得与你作为玩家和现实消费者相关。

当刚开始制作游戏时,游戏内广告并不那么引人注目。大多数知名的在线游戏内广告商甚至不希望他们的广告出现在初创游戏中。

Google AdSense 的要求如下:

  • 游戏玩法:每天至少 500,000 次

  • 游戏类型:仅限基于 Web 的 Flash

  • 集成:必须具备 SDK 集成的技术能力

  • 流量来源:80%的流量必须来自美国和英国

  • 内容:适合家庭和面向 13 岁及以上的用户

  • 分发:必须能够报告嵌入目的地并控制游戏的分发位置

另一家大竞争对手 Ad4Game 的要求也不轻松:

  • 至少每天有 10,000 个独立访客

  • 不接受子域和博客

  • Alexa 排名应该低于 400,000

  • 不允许成人/暴力/种族主义内容

如果你刚开始,这些先决条件并不是好消息。不仅因为你需要如此多的玩家才能开始广告,而且因为目前所有的支持都是针对 Flash 游戏。HTML5 游戏目前还没有得到充分支持,尽管这可能会改变。

幸运的是,有一些公司允许你开始使用广告,即使你每天没有 10,000 个访问者。Tictacti 就是其中之一。

再次强调,几乎所有的支持都是针对 Flash 游戏的,但他们确实为 HTML5 游戏提供了一个选项:预滚动。预滚动简单地意味着在你开始游戏之前会出现一个带有广告的屏幕。预滚动广告的集成非常简单,不需要对游戏进行更改,只需要对你的index.html文件进行更改,就像 Tictacti 的以下示例一样:

//You can use publisherId 3140 and tagTypedemoAPI for testing purposes however the ads will not be credit to you.
<html>
<head>
  <title>Simple Ad</title>
</head>
<body>
<script type="text/javascript"src="img/t3widgets.js"></script><script type="text/javascript">
  var publisherId = "3140";var tagType = "jsGameAPI";var agencyUniqueId= "0";var playerWidth = "600";//The Game widthvar playerHeight = "400";//The Game heightvar t3cfg = {wrapperUrl: 'engine/game/3170/tttGameWrapper.swf',
config: { enableDM: false, tttPreloader: false, bgcolor: "#000000", engineConnectorType: 7 , externalId:agencyUniqueId},
    onClose:function(){document.location="http://www.tictacti.com";}
    //Called after the ad is closed. In the Demo after 30 seconds.};
  TicTacTi.renderWidget(publisherId, tagType, playerWidth ,playerHeight , t3cfg);
</script>
</body>
</html>

在将其添加到游戏的index.html文件时,填写你自己的发布者 ID,基本上就可以开始了。

Tictacti 类似于 Google Analytics,它还为你提供了一些关于游戏网站上广告的相关信息,如下图所示:

游戏内广告

然而,要小心,预滚动广告是最具侵入性和恼人的广告之一。从技术上讲,它甚至不算是游戏内广告,因为它是在你玩游戏之前运行的。如果你的游戏还没有建立足够的声誉,无法说服玩家忍受广告才能开始游戏,就不要选择这个选项。给你的游戏一些时间来建立声誉,然后再让玩家忍受这些。

最后一个选择是通过 MarketJS 出售你的实际分发权。但首先让我们简要回顾一下游戏内广告:

  • 游戏内广告是一个不断增长的市场。甚至巴拉克·奥巴马也利用游戏内广告牌来支持他的竞选活动。

  • 有一种趋势是更加动态的游戏内广告——利用你的位置和人口统计信息来调整游戏中的广告。

  • 目前,即使是最容易接触到的在线游戏广告公司也专注于 Flash 游戏,并要求有很多独立访问者才能允许你展示他们的广告。Tictacti 是一个值得注意的例外,因为它的先决条件低,实施简单;尽管广告目前仅限于预滚动广告。

  • 始终要先为你的游戏建立良好的声誉,然后再允许广告。

使用 MarketJS 出售分发权

我们在本章中将要调查的最后一个选项是出售你的游戏分发权。你仍然可以通过将游戏发布到所有应用商店和你自己的网站上赚钱,但要被注意到变得越来越困难。只有当人们知道游戏存在时,质量才能获胜,因此制作一款好游戏有时是不够的——你需要营销。如果你是一个有很好的游戏创意和技能的初学者游戏开发者,那很好,但营销可能不是你的菜。这就是 MarketJS 发挥作用的地方。

MarketJS 充当游戏开发者和游戏出版商之间的中介。

一旦你有了游戏,程序就很简单:

  1. 你可以在他们的网站www.marketjs.com上注册。

  2. 将游戏上传到你自己的网站或直接上传到 MarketJS 服务器。

  3. 发布你的游戏供出版商查看。你可以设置一些选项,比如最适合你的价格和合同类型。你有五个合同选项:

  • 完整分发合同:将你的游戏所有的分发权出售。

  • 独家分发合作伙伴合同:在这里,你限制自己与一个分销商合作,但仍保留游戏的权利。

  • 非独家合同:在这里,任何分销商都可以购买你游戏的使用权,但只要你愿意,你可以继续出售权利。

  • 收入分成:在这里,你可以协商如何分配游戏产生的收入。

  • 定制合同:这基本上可以有任何条款。如果你还不确定你想从你的游戏中得到什么,你可以选择这个选项。在填写你的合同偏好的网页部分如下截图所示:

使用 MarketJS 出售分发权

发布演示后,就是等待出版商发现它,被它的壮丽所震撼,并提出与你合作的要约。

MarketJS 对游戏领域的重大贡献在于让游戏开发者专注于开发游戏。其他人负责营销方面,这是完全不同的一回事。

MarketJS 还提供了一些有趣的统计数据,比如他们网站上游戏的平均价格,如下图所示。这让你对是否应该把游戏开发作为一种生活方式或者继续把它作为一种爱好有了一些见解。

使用 MarketJS 出售分发权

根据 MarketJS,非独家权利的价格平均在 500 到 1000 美元之间,而售出游戏的独家权利价格在 1500 到 2000 美元之间。如果你能在这个价格范围内制作一款体面的游戏,那么你已经准备好了:

  • MarketJS 是一家将游戏分销商和开发者联系在一起的公司。他们专注于 HTML5 游戏,所以如果你是一名初创的 ImpactJS 游戏开发者,他们是一个很好的选择。

  • 他们不需要订阅费,并且有一个简单的流程将你的游戏变成一个带有价格标签的展示品。

总结

在本章中,我们已经看了一些重要的元素,考虑了你的游戏开发策略。你想采取散弹枪策略,在短时间内开发大量游戏吗?还是你会使用狙击手策略,只开发一些精心制作的游戏?你还需要决定你希望吸引的受众群体。你可以选择制作一款受到所有人喜爱的游戏,但竞争激烈。

在应用商店赚钱是可能的,但对于 Android 和 Apple 来说,有注册费。如果你决定开发应用程序,不妨试试 Chrome 网络应用商店(它运行网络应用)。

游戏内广告是资助你的努力的另一种方式,尽管大多数提供在线游戏服务的公司对此有很高的先决条件,并且更多地支持 Flash 游戏而不是更新的 HTML5 游戏。

最有前途的变现模式之一是免费模式。玩家可以自由玩游戏,但他们需要为额外的内容付费。这是一个容易被接受的模式,因为对于不愿意花钱的人来说,游戏基本上是免费的,而且也没有烦人的广告。

游戏内广告和免费模式的组合也是可能的:被广告打扰的人支付费用,作为回报,他们将不再受到打扰。

最后一个选择是通过与 MarketJS 合作出售你的发行权,将营销方面留给其他人。他们专注于 HTML5 游戏,这个选择对于初学者游戏开发者来说特别有用,因为他们在营销游戏方面可能会遇到困难。

我们现在已经到达了书的结尾,涵盖了大量的信息 - 从设置服务器的基础知识,到开发 ImpactJS 游戏,再到自己创作的分发。感谢你阅读了所有这些。

我希望能让整个过程更容易理解,并为你开始创作自己的游戏甚至可能靠此谋生提供最后的推动。有时,开发游戏可能会令人沮丧,因为魔鬼常常隐藏在细节中。你可能会发现自己经常咒骂屏幕,但请记住这不是电脑的错,只要有足够的决心,你总能找到解决方案。如果你有时感到迷茫,ImpactJS 网站有一个充满了非常乐于助人的人的论坛,我非常鼓励你利用它,在那里分享你的想法、问题和想法。当你关闭这本书,开始制作你的游戏时,不要忘记制定计划并以非常有条理的方式工作将是成功的关键因素,并可能避免许多不眠之夜。逐步进行变更和改进,并始终检查一切是否仍然按预期运行是正确的方式,可以交付一个完美运行的游戏。但是,尽管有组织和有条理的思维所带来的所有好处,还有另一个最重要的东西:你的想象力。

要有原创性,不要受到已有内容的限制,你无疑会创造出一款让数百万人喜欢的游戏,甚至可能经得起时间的考验。

posted @ 2024-05-24 11:11  绝不原创的飞龙  阅读(8)  评论(0编辑  收藏  举报