[翻译]Akihabara 指南,第三部分:基本的地图

原文:http://bostongamejams.com/akihabara-tutorials/akihabara-tutorial-part-3-basic-mapping/

Akihabara 指南,第三部分:基本的地图

Darius Kazemi 编写于 2010 年 06 月 14 日

这是系列指南的第三部分,我们将向你演示如何使用基于 HTML5 和 JavaScript 的 Akihabara 框架来编写 8 向射击游戏。Akihabara 是一个利用 HTML5 功能帮助创建游戏的 Javascript 库。使用 HTML5 编写游戏最棒的事情是你可以在任何平台、任何支持 HTML5 的浏览器运行它。这包括 Chrome,Firefox,Safari 和 iPhone/iPad,WebOS 设备上的 WebKit 浏览器,或者其他移动平台。

在这一部分,我们将向你演示如何在游戏中创建一个包含可见的背景的地图,地图中的“墙”是可碰撞的,这样我们在第二部分中实现的玩家对象碰到的时候,就会停下来。在快速开发游戏的时候 Akihabara 的一个最好的功能之一,就是创建一个基本的地图的过程是相当的容易。所以,这部分将是一个简短的指南,不过我们仍然会花一些时间用于理清这个基本地图的核心思想上面。而且,我们添加了碰撞,所以玩家对象可以撞到墙上去。

成品

本次课程结束后,你会得到类似这样的一个游戏。按下 Z 越过标题屏幕,然后使用方向键移动。注意你是如何同墙碰撞的!

定义地图

在 Akihabara 中,定义地图是非常容易的。你只需要调用 help.asciiArtToMap 函数,然后用 ASCII 绘制出来!我们将在 main 函数后面,写一个叫做 load_map 的函数。看起来是这样的:

<br />
	function loadMap() {<br />
	  return help.asciiArtToMap([<br />
	&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;,<br />
	&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xxxxxxxxxxxxxxxx            xx        xx&quot;,<br />
	&quot;xxxxxxxxxxxxxxxx            xx        xx&quot;,<br />
	&quot;xx                          xx        xx&quot;,<br />
	&quot;xx                          xx        xx&quot;,<br />
	&quot;xx                          xx        xx&quot;,<br />
	&quot;xx                          xx        xx&quot;,<br />
	&quot;xx                          xx        xx&quot;,<br />
	&quot;xx                          xx        xx&quot;,<br />
	&quot;xx          xxxxxxxx   xxxxxxxxxxxxxxxxx&quot;,<br />
	&quot;xx          xxxxxxxx   xxxxxxxxxxxxxxxxx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xxxxxxxx                              xx&quot;,<br />
	&quot;xxxxxxxx                              xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx            xxxxxxxxxxxxxxxxxx      xx&quot;,<br />
	&quot;xx            xxxxxxxxxxxxxxxxxx      xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xx                                    xx&quot;,<br />
	&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;,<br />
	&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;,<br />
	    ], [ [null, ' '], [0, 'x'] ])<br />
	}<br />

help.asciiArtToMap 函数接受两个参数。第一个传递的是持久化的描绘地图的字符数组——很容易看出,我们使用“x”和“ ”(空格)字符绘制了地图。然后,我们传递了一个翻译数组,一个数组的数组,格式类似

<br />
[ [null, char1], [0, char2], [1, char3] ]<br />

你至少需要一个 null 的条目,紧接着数字条目的对应想要渲染的瓦片。null 条目映射 char1(在这个例子里是“ ”(空格))到空白位置。接下来的映射对应地图的 tilesheet 部分。在上面的例子中,我们映射“x”到 tilesheet 中编号0(第一个)的瓦片。在这个例子中,我们只有一个瓦片,所以不需要继续定义更多的瓦片了。

加载地图的瓦片

在 Akihabara 引擎中,地图只是游戏世界中的一个对象,跟第二部分那个可移动的精灵一样。跟可移动的玩家对象一样,需要渲染一个图像,所以我们将从加载
用于渲染墙的图片开始。我们使用的图片叫做 map_pieces.png,你需要系在并保存在 index.html 相同目录。

<br />
	function loadResources() {<br />
	...<br />
	  // ** 这些代码在 gbox.addFont() 之前 **<br />
	  // 添加提供“墙”的地图精灵表<br />
	  gbox.addImage('map_spritesheet', 'map_pieces.png');</p>
<p>	  // 瓦片集合从精灵表中获得<br />
	  gbox.addTiles({<br />
	    id:      'map_pieces',<br />
	    image:   'map_spritesheet',<br />
	    tileh:   8,<br />
	    tilew:   8,<br />
	    tilerow: 1,<br />
	    gapx:    0,<br />
	    gapy:    0<br />
	  });<br />

main() 方法的改动

下面,我们将更新 main 方法。首先要做的是修改 main 函数中的 gbox.setGroups 调用,添加用于渲染地图本身的组。我们将把这个组叫做‘background’并将其放到数组定义的第一个,这样它会被首先渲染。这样做的原因是我们希望 ‘player’ 和 ‘game’ (玩家精灵和 UI 元素)组在墙的上层被渲染。

<br />
	function main() {<br />
	  // ** 在第三部分中,我们在下面的行中添加了 ‘background’ **<br />
	  gbox.setGroups(['background', 'player', 'game']);<br />

接着,我们跳转到 maingame.initializegame 函数。在紧接着 addPlayer 调用的位置,我们添加 addMap 调用。现在还没有定义这个函数,但是就像 addPlayer 添加玩家对象到游戏世界一样,addMap 将会添加地图。由于这个在 maingame.initializegame 内调用,在游戏第一次加载的时候,会被执行一次。

<br />
	maingame.initializeGame = function() {<br />
	  // 来自于第二部分的代码……<br />
	  addPlayer();</p>
<p>	  // 这里我们创建了背景对象,它在每次游戏世界被渲染的时候,被绘制在‘background’层。<br />
	  addMap();<br />
	};<br />

在完成了 maingame.initializeGame 函数的定义后,我们定义 map 函数本身。地图是一个包含三个信息的结构体:tileset,是我们在 loadResources 中定义的地图的瓦片集合的名字;map,实际上通过调用上面的 load_map 函数从 ASCII 艺术中转换地图信息;然后还有 tileIsSolid 函数,内建的碰撞代码,判断哪个瓦片是可碰撞的,哪个不是。例如,草地的瓦片不可碰撞,而墙的瓦片则可碰撞。

<br />
	// 这里我们定义了地图,包括 tileset,实际的地图数据,以及碰撞的帮助函数<br />
	map = {<br />
	  tileset: 'map_pieces', // 指定使用 loadResources 函数中创建的‘map_pieces’瓦片</p>
<p>	  // 这里加载了 ASCII 码定义的地图的内容,用数组的形式定义了整数编号作为类型的地图瓦片<br />
	  // 每个‘类型’对应 tileset 的一个精灵。例如地图瓦片有类型 0,则使用地图瓦片(上面定义的‘map_pieces’)中的第一个精灵<br />
	  // 如果地图瓦片有类型 1,则使用地图瓦片中的第二个精灵,以此类推。<br />
	  // 同时,需要注意类型 null 也是被接受的,不使用地图瓦片中的任何精灵<br />
	  map: loadMap(),</p>
<p>	  // 如果检测到对象‘obj’所在的瓦片‘t’是墙的话,这个函数返回 true,所以……<br />
	  tileIsSolid: function(obj, t) {<br />
	    return t != null; // 如果不是空地,就是墙<br />
	  }<br />
	}<br />

(补充一下:虽然在调用 addMap 之后才定义地图,但是要记得 addMap 只是一个函数定义!在 main 函数结束前的 gbox.go 调用时,才会真正执行。所以,安顺序说:我们告诉游戏准备好调用 addMap,然后定义了地图,然后 addMap 才真正执行。)

在定义了地图之后,我们调用 help.finalizeTilemap,用来计算并且正确设置 map.h 和 map.w,也就是地图的高和宽。

<br />
	// 这个函数通过统计瓦片的数量,来计算地图完整的宽高。<br />
	map = help.finalizeTilemap(map);<br />

当我们执行 gbox.createCanvas 时,传递 map.h 和 map.w 设定地图画布的尺寸。画布是地图实际绘制的位置,所以接下来调用 gbox.blitTilemap 真正在画布上绘制地图。

<br />
	  // 地图最终的宽高被计算出来的时候,就可以创建适合于地图的画布了。就叫画布为“map_canvas”。<br />
	  gbox.createCanvas('map_canvas', { w: map.w, h: map.h });</p>
<p>	  // 这个函数从“map”对象中获取地图,并将其绘制到“map_canvas”。这样地图就在渲染管道中了。<br />
	  gbox.blitTilemap(gbox.getCanvasContext('map_canvas'), map);</p>
<p>	  gbox.go();<br />
	}<br />

然后,跟之前一样在 main 函数结束前调用 gbox.go。

添加地图对象

现在,在 addPlayer 函数定义之前,定义 addMap 函数。addMap 函数向游戏世界添加地图。之前的代码中,我们告诉 Akihabara 在游戏初始化时,玩家对象已经被添加后调用。下面是 addMap:

<br />
	// 这是用于添加地图对象的函数——以确保游戏主要代码优质且简明<br />
	function addMap() {<br />
	  gbox.addObject({<br />
	    id:    'background_id', // 这是对象 ID<br />
	    group: 'background',    // 我们使用之前‘setGroups’调用中创建的‘backround’组</p>
<p>	    // blit 函数说明了游戏绘制圆的时候发生了什么。所有跟渲染和绘制有关的内容都放在了这里<br />
	    blit: function() {<br />
	      // 首先,擦除整个屏幕。blitFade 填充制指定的矩形区域(在这个例子中,就是屏幕)<br />
	      gbox.blitFade(gbox.getBufferContext(), { alpha: 1 });</p>
<p>	      // 由于我们在 main 函数中已经将将 tilemap 渲染到了‘map_canvas’,现在可以将‘map_canvas’绘制到屏幕。<br />
	      // ‘map_canvas’仅仅是 tilemap 的一个图片,在这里渲染,以确保在每帧都被重绘。<br />
	      gbox.blit(gbox.getBufferContext(), gbox.getCanvas('map_canvas'), { dx: 0, dy: 0, dw: gbox.getCanvas('map_canvas').width, dh: gbox.getCanvas('map_canvas').height, sourcecamera: true });<br />
	    }<br />
	  });<br />
	}<br />

这跟玩家对象相比是非常简单的。我们定义了对象 ID;将其指定它到‘background’组,以便让其在其他所有对象渲染前被渲染;然后我们定义了 blit 函数,用于擦除屏幕和绘制地图。blit 函数绘制 map_canvas 的内容——还记得吗,我们在 main 函数中保存了一个地图的图像在 map_canvas 中。 所以我们只需要确保在每帧重绘这个图像。这就是地图对象!

修复……呃,问题

对于已经讨论过擦除屏幕,这里必须要澄清一下。我们在第二部分的指南里范了个错误。为了方便起见,我们让你在玩家对象的 blit 函数中擦除屏幕。这绝对是错误的。像你在上面看到的那样,背景是最佳的擦除屏幕的地方。这是因为它是第一个被渲染的,所以如果期望擦除上一屏幕,则直接渲染背景以及其上的所有内容。否则,我们会在玩家对象中将背景(也就是地图)也擦除,这是我们所不希望的。

我们需要从第二部分的旧的玩家对象的代码中删除下面的两行:

<br />
	// 擦除屏幕<br />
	gbox.blitFade(gbox.getBufferContext(), {});<br />

对这个真实抱歉啊!

添加碰撞

我们要感谢 Akihabara,对玩家对象添加碰撞是相当简单的。首先要做的是重定义碰撞盒,一个不可见的套在玩家对象外面的盒子,用来计算碰撞的发生。新的代码添加在 tileset 定义之后。

<br />
	function addPlayer() {<br />
	  gbox.addObject({<br />
	    id:      'playerid',    // 对象引用的 ID<br />
	    group:   'player',       // 渲染组<br />
	    tileset: 'playerTiles', // tileset 表示图像的来源</p>
<p>	    // ** 第三部分的新代码 **<br />
	    // 我们重写了对象的 colh 属性的默认值。“colh”表示碰撞的高度,这个高是我们的碰撞盒的高度。<br />
	    colh:gbox.getTiles('playerTiles').tileh,<br />

colh 属性是顶视图对象的默认属性。它表示“碰撞高度”,也就是碰撞盒的高度。对象同时也有 colw(碰撞盒宽度)和 colx 以及 coly (碰撞盒的 x/y 偏移量)。

我们重写 toys.topview 对象的 colh 的值,是因为 colh 的默认值被设定为精灵高度的一半。这是因为顶视图通常使用在《塞尔达》模式的游戏,它们的碰撞盒只有精灵的下半部分,上半部分可以遮盖在其后的布景上。因此,我们需要设置 colh 为默认的瓦片高度, 这样对于我们的圆型,碰撞盒可以想象成这个圆的外切正方形。

在我们的初始化函数中,设置了玩家的初始位置为 (20, 20) 代替默认的 (0, 0)。这样做是因为在 (0, 0) 有瓦片存在,所以如果玩家从这个坐标开始,他会被永远卡住!

<br />
	initialize: function() {</p>
<p>	  // 这里我们初始化对象,也就是玩家。<br />
	  toys.topview.initialize(this, {});</p>
<p>	  // ** 第三部分的新代码 **<br />
	  // 然后设置玩家的新的初始位置。<br />
	  this.x = 20;<br />
	  this.y = 20;<br />
	},<br />

最后在 first 函数中,我们在改变应用到对象后检查碰撞。

<br />
	first: function() {<br />
	  // Toys.topview.controlKeys 设置主要的控制按键。在这个例子中,我们使用映射到英文名字的方向键。<br />
	  // 在这个函数中,它根据方向应用加速度。<br />
	  toys.topview.controlKeys(this, { left: 'left', right: 'right', up: 'up', down: 'down' });</p>
<p>	  // 这个添加了加速度的控制,这样当没有加速度的时候就会停下来,否则游戏控制就会像 Asteroids 那样。<br />
	  toys.topview.handleAccellerations(this);</p>
<p>	  // 这告诉物理引擎产生实际的影响<br />
	  toys.topview.applyForces(this);</p>
<p>	  // ** 第三部分的新代码 **</p>
<p>	  // 这里我们通过 colx、coly、colh 和 colw 参数来设置碰撞盒的边缘。因为我们的精灵是圆的,所以容差设置为 6。<br />
	  // 容差设置为 6 让我们的对象圆角的感觉更好一些,而不会过于尖锐的感觉。<br />
	  // 我们通过尝试和错误得到这个数值——通常来说,容差应当是 0 到你的精灵的宽或者高的一半的某个数值。<br />
	  // 当然,在代码中它被拼写为 “tollerance”!<br />
	  toys.topview.tileCollision(this, map, 'map', null, { tollerance: 6, approximation: 3 });<br />
	},<br />

我们将碰撞检测放到移动应用到对象后进行,是因为 tileCollision 函数通过检测象是否跟地图上的固有瓦片重叠。如果是,则立刻让对象离开瓦片。最基本的就是设置回对象移动前的位置,这样结果看起来就像我们期望的那样。

tileCollision 函数本身得到玩家对象、地图对象、一个叫做‘map’的字符串(我们也不确定它具体应该是做什么的),和无须关心的 null 值,以及容差[sic]和误差。

参数容差决定了碰撞检测算法的严格程度。通常,容差决定了碰撞盒的边缘有多“圆滑”。我们测试了一大堆数,最后决定使用 6。

参数误差决定了碰撞检测算法的精确程度。通常来说,越小的数字,碰撞检测越精密。注意:越精密的算法,需要越多的资源。然后,我们通过不断尝试和错误得到 3。要小心的是误差设置为 0 或者更小的值会让你的游戏挂掉!

撞进墙:来自于传统视频游戏 Breakout

你现在可以保存 HTML 文件,然后在合适的浏览器里打开;你应当看到这样的内容。你会看到第一部分的标题屏幕。按 Z 键跳过标题屏幕,一个蓝色的圆会显示在屏幕上,被漂亮的迷宫包围。用方向键控制这个圆,并且享受撞墙的快乐吧!

Akihabara 指南目录

2 thoughts on “[翻译]Akihabara 指南,第三部分:基本的地图”

Leave a Reply

Your email address will not be published. Required fields are marked *