原文: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 的函数。看起来是这样的:
function loadMap() { return help.asciiArtToMap([ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xx xx", "xx xx", "xx xx", "xx xx", "xxxxxxxxxxxxxxxx xx xx", "xxxxxxxxxxxxxxxx xx xx", "xx xx xx", "xx xx xx", "xx xx xx", "xx xx xx", "xx xx xx", "xx xx xx", "xx xxxxxxxx xxxxxxxxxxxxxxxxx", "xx xxxxxxxx xxxxxxxxxxxxxxxxx", "xx xx", "xx xx", "xx xx", "xxxxxxxx xx", "xxxxxxxx xx", "xx xx", "xx xxxxxxxxxxxxxxxxxx xx", "xx xxxxxxxxxxxxxxxxxx xx", "xx xx", "xx xx", "xx xx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", ], [ [null, ' '], [0, 'x'] ]) }
help.asciiArtToMap 函数接受两个参数。第一个传递的是持久化的描绘地图的字符数组——很容易看出,我们使用“x”和“ ”(空格)字符绘制了地图。然后,我们传递了一个翻译数组,一个数组的数组,格式类似
[ [null, char1], [0, char2], [1, char3] ]
你至少需要一个 null 的条目,紧接着数字条目的对应想要渲染的瓦片。null 条目映射 char1(在这个例子里是“ ”(空格))到空白位置。接下来的映射对应地图的 tilesheet 部分。在上面的例子中,我们映射“x”到 tilesheet 中编号0(第一个)的瓦片。在这个例子中,我们只有一个瓦片,所以不需要继续定义更多的瓦片了。
加载地图的瓦片
在 Akihabara 引擎中,地图只是游戏世界中的一个对象,跟第二部分那个可移动的精灵一样。跟可移动的玩家对象一样,需要渲染一个图像,所以我们将从加载
用于渲染墙的图片开始。我们使用的图片叫做 map_pieces.png,你需要系在并保存在 index.html 相同目录。
function loadResources() { ... // ** 这些代码在 gbox.addFont() 之前 ** // 添加提供“墙”的地图精灵表 gbox.addImage('map_spritesheet', 'map_pieces.png'); // 瓦片集合从精灵表中获得 gbox.addTiles({ id: 'map_pieces', image: 'map_spritesheet', tileh: 8, tilew: 8, tilerow: 1, gapx: 0, gapy: 0 });
main() 方法的改动
下面,我们将更新 main 方法。首先要做的是修改 main 函数中的 gbox.setGroups 调用,添加用于渲染地图本身的组。我们将把这个组叫做‘background’并将其放到数组定义的第一个,这样它会被首先渲染。这样做的原因是我们希望 ‘player’ 和 ‘game’ (玩家精灵和 UI 元素)组在墙的上层被渲染。
function main() { // ** 在第三部分中,我们在下面的行中添加了 ‘background’ ** gbox.setGroups(['background', 'player', 'game']);
接着,我们跳转到 maingame.initializegame 函数。在紧接着 addPlayer 调用的位置,我们添加 addMap 调用。现在还没有定义这个函数,但是就像 addPlayer 添加玩家对象到游戏世界一样,addMap 将会添加地图。由于这个在 maingame.initializegame 内调用,在游戏第一次加载的时候,会被执行一次。
maingame.initializeGame = function() { // 来自于第二部分的代码…… addPlayer(); // 这里我们创建了背景对象,它在每次游戏世界被渲染的时候,被绘制在‘background’层。 addMap(); };
在完成了 maingame.initializeGame 函数的定义后,我们定义 map 函数本身。地图是一个包含三个信息的结构体:tileset,是我们在 loadResources 中定义的地图的瓦片集合的名字;map,实际上通过调用上面的 load_map 函数从 ASCII 艺术中转换地图信息;然后还有 tileIsSolid 函数,内建的碰撞代码,判断哪个瓦片是可碰撞的,哪个不是。例如,草地的瓦片不可碰撞,而墙的瓦片则可碰撞。
// 这里我们定义了地图,包括 tileset,实际的地图数据,以及碰撞的帮助函数 map = { tileset: 'map_pieces', // 指定使用 loadResources 函数中创建的‘map_pieces’瓦片 // 这里加载了 ASCII 码定义的地图的内容,用数组的形式定义了整数编号作为类型的地图瓦片 // 每个‘类型’对应 tileset 的一个精灵。例如地图瓦片有类型 0,则使用地图瓦片(上面定义的‘map_pieces’)中的第一个精灵 // 如果地图瓦片有类型 1,则使用地图瓦片中的第二个精灵,以此类推。 // 同时,需要注意类型 null 也是被接受的,不使用地图瓦片中的任何精灵 map: loadMap(), // 如果检测到对象‘obj’所在的瓦片‘t’是墙的话,这个函数返回 true,所以…… tileIsSolid: function(obj, t) { return t != null; // 如果不是空地,就是墙 } }
(补充一下:虽然在调用 addMap 之后才定义地图,但是要记得 addMap 只是一个函数定义!在 main 函数结束前的 gbox.go 调用时,才会真正执行。所以,安顺序说:我们告诉游戏准备好调用 addMap,然后定义了地图,然后 addMap 才真正执行。)
在定义了地图之后,我们调用 help.finalizeTilemap,用来计算并且正确设置 map.h 和 map.w,也就是地图的高和宽。
// 这个函数通过统计瓦片的数量,来计算地图完整的宽高。 map = help.finalizeTilemap(map);
当我们执行 gbox.createCanvas 时,传递 map.h 和 map.w 设定地图画布的尺寸。画布是地图实际绘制的位置,所以接下来调用 gbox.blitTilemap 真正在画布上绘制地图。
// 地图最终的宽高被计算出来的时候,就可以创建适合于地图的画布了。就叫画布为“map_canvas”。 gbox.createCanvas('map_canvas', { w: map.w, h: map.h }); // 这个函数从“map”对象中获取地图,并将其绘制到“map_canvas”。这样地图就在渲染管道中了。 gbox.blitTilemap(gbox.getCanvasContext('map_canvas'), map); gbox.go(); }
然后,跟之前一样在 main 函数结束前调用 gbox.go。
添加地图对象
现在,在 addPlayer 函数定义之前,定义 addMap 函数。addMap 函数向游戏世界添加地图。之前的代码中,我们告诉 Akihabara 在游戏初始化时,玩家对象已经被添加后调用。下面是 addMap:
// 这是用于添加地图对象的函数——以确保游戏主要代码优质且简明 function addMap() { gbox.addObject({ id: 'background_id', // 这是对象 ID group: 'background', // 我们使用之前‘setGroups’调用中创建的‘backround’组 // blit 函数说明了游戏绘制圆的时候发生了什么。所有跟渲染和绘制有关的内容都放在了这里 blit: function() { // 首先,擦除整个屏幕。blitFade 填充制指定的矩形区域(在这个例子中,就是屏幕) gbox.blitFade(gbox.getBufferContext(), { alpha: 1 }); // 由于我们在 main 函数中已经将将 tilemap 渲染到了‘map_canvas’,现在可以将‘map_canvas’绘制到屏幕。 // ‘map_canvas’仅仅是 tilemap 的一个图片,在这里渲染,以确保在每帧都被重绘。 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 }); } }); }
这跟玩家对象相比是非常简单的。我们定义了对象 ID;将其指定它到‘background’组,以便让其在其他所有对象渲染前被渲染;然后我们定义了 blit 函数,用于擦除屏幕和绘制地图。blit 函数绘制 map_canvas 的内容——还记得吗,我们在 main 函数中保存了一个地图的图像在 map_canvas 中。 所以我们只需要确保在每帧重绘这个图像。这就是地图对象!
修复……呃,问题
对于已经讨论过擦除屏幕,这里必须要澄清一下。我们在第二部分的指南里范了个错误。为了方便起见,我们让你在玩家对象的 blit 函数中擦除屏幕。这绝对是错误的。像你在上面看到的那样,背景是最佳的擦除屏幕的地方。这是因为它是第一个被渲染的,所以如果期望擦除上一屏幕,则直接渲染背景以及其上的所有内容。否则,我们会在玩家对象中将背景(也就是地图)也擦除,这是我们所不希望的。
我们需要从第二部分的旧的玩家对象的代码中删除下面的两行:
// 擦除屏幕 gbox.blitFade(gbox.getBufferContext(), {});
对这个真实抱歉啊!
添加碰撞
我们要感谢 Akihabara,对玩家对象添加碰撞是相当简单的。首先要做的是重定义碰撞盒,一个不可见的套在玩家对象外面的盒子,用来计算碰撞的发生。新的代码添加在 tileset 定义之后。
function addPlayer() { gbox.addObject({ id: 'playerid', // 对象引用的 ID group: 'player', // 渲染组 tileset: 'playerTiles', // tileset 表示图像的来源 // ** 第三部分的新代码 ** // 我们重写了对象的 colh 属性的默认值。“colh”表示碰撞的高度,这个高是我们的碰撞盒的高度。 colh:gbox.getTiles('playerTiles').tileh,
colh 属性是顶视图对象的默认属性。它表示“碰撞高度”,也就是碰撞盒的高度。对象同时也有 colw(碰撞盒宽度)和 colx 以及 coly (碰撞盒的 x/y 偏移量)。
我们重写 toys.topview 对象的 colh 的值,是因为 colh 的默认值被设定为精灵高度的一半。这是因为顶视图通常使用在《塞尔达》模式的游戏,它们的碰撞盒只有精灵的下半部分,上半部分可以遮盖在其后的布景上。因此,我们需要设置 colh 为默认的瓦片高度, 这样对于我们的圆型,碰撞盒可以想象成这个圆的外切正方形。
在我们的初始化函数中,设置了玩家的初始位置为 (20, 20) 代替默认的 (0, 0)。这样做是因为在 (0, 0) 有瓦片存在,所以如果玩家从这个坐标开始,他会被永远卡住!
initialize: function() { // 这里我们初始化对象,也就是玩家。 toys.topview.initialize(this, {}); // ** 第三部分的新代码 ** // 然后设置玩家的新的初始位置。 this.x = 20; this.y = 20; },
最后在 first 函数中,我们在改变应用到对象后检查碰撞。
first: function() { // Toys.topview.controlKeys 设置主要的控制按键。在这个例子中,我们使用映射到英文名字的方向键。 // 在这个函数中,它根据方向应用加速度。 toys.topview.controlKeys(this, { left: 'left', right: 'right', up: 'up', down: 'down' }); // 这个添加了加速度的控制,这样当没有加速度的时候就会停下来,否则游戏控制就会像 Asteroids 那样。 toys.topview.handleAccellerations(this); // 这告诉物理引擎产生实际的影响 toys.topview.applyForces(this); // ** 第三部分的新代码 ** // 这里我们通过 colx、coly、colh 和 colw 参数来设置碰撞盒的边缘。因为我们的精灵是圆的,所以容差设置为 6。 // 容差设置为 6 让我们的对象圆角的感觉更好一些,而不会过于尖锐的感觉。 // 我们通过尝试和错误得到这个数值——通常来说,容差应当是 0 到你的精灵的宽或者高的一半的某个数值。 // 当然,在代码中它被拼写为 “tollerance”! toys.topview.tileCollision(this, map, 'map', null, { tollerance: 6, approximation: 3 }); },
我们将碰撞检测放到移动应用到对象后进行,是因为 tileCollision 函数通过检测象是否跟地图上的固有瓦片重叠。如果是,则立刻让对象离开瓦片。最基本的就是设置回对象移动前的位置,这样结果看起来就像我们期望的那样。
tileCollision 函数本身得到玩家对象、地图对象、一个叫做‘map’的字符串(我们也不确定它具体应该是做什么的),和无须关心的 null 值,以及容差[sic]和误差。
参数容差决定了碰撞检测算法的严格程度。通常,容差决定了碰撞盒的边缘有多“圆滑”。我们测试了一大堆数,最后决定使用 6。
参数误差决定了碰撞检测算法的精确程度。通常来说,越小的数字,碰撞检测越精密。注意:越精密的算法,需要越多的资源。然后,我们通过不断尝试和错误得到 3。要小心的是误差设置为 0 或者更小的值会让你的游戏挂掉!
撞进墙:来自于传统视频游戏 Breakout
你现在可以保存 HTML 文件,然后在合适的浏览器里打开;你应当看到这样的内容。你会看到第一部分的标题屏幕。按 Z 键跳过标题屏幕,一个蓝色的圆会显示在屏幕上,被漂亮的迷宫包围。用方向键控制这个圆,并且享受撞墙的快乐吧!
Leave a Reply