[翻译]Akihabara 指南,第二部分:精灵的移动

原文:http://bostongamejams.com/akihabara-tutorials/akihabara-tutorial-part-2-moving-a-sprite/

Akihabara 指南,第二部分:精灵的移动

Darius Kazemi 编写于 2010 年五月 21 日

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

在这个部分中,我们将向你演示如何渲染精灵到屏幕上,并且移动它。这个指南包含精灵的渲染、输入、基本的游戏对象创建和控制。我们将在第一部分创建的基础上进行开发,所以将假设你已经完成了第一部分的指南。

成品

这里查看本课结束后的成果。按“Z”键跳过标题屏幕,然后使用方向键控制圈点移动。

整体思路

现在,我们将开始编写实际的游戏代码,我们需要回头看看,考虑一下视频游戏的屏幕是怎么样的。

游戏是由相互作用的对象组成:在像《太空侵略者》这样的游戏中,对象包括玩家的飞船、敌人、护盾和子弹。然后,我们设定对象行为的规则。一个敌人的对象可能包含代码,说“向前和向后移动我,当碰到屏幕边缘时消失。在一个随机的时间间隔中,向前进方向上射击。”

对象是游戏的核心组件,但是只有一大堆对象并不是游戏。我们需要将这些对象放在世界中,并提供一个整体的概念:“在玩家的四个方向上放置 10 个敌人。”;“玩家对象死亡三次后游戏结束。”

Akihabara 是一个基于对象的游戏引擎。在指南的这一部分,我们将尝试创建第一个玩家对象,定义它的行为,然后将它放入游戏世界中。在这个部分的指南中,不会有其他相互作用的对象或者游戏的整体概念出现——我们刻意略过这些在以后的指南中会出现的内容。

玩家对象

我们的玩家对象将会包含一些定义。我们会通过指定一个精灵来定义对象的外观(一个 2D 的图形,等一下详细说),并且设置它的控制方式,以及如何移动。

精灵

精灵是图片的梦幻般的称呼,或者说用代码来控制的一组图像。它是关联到游戏对象的图形,并且是这个对象的图形化描述。

我们的精灵将使用的图像是这个蓝色的圆形:playerSprite.png。这是一个 PNG 文件:Akihabara 可以处理透明的 PNG 文件,所以它可以正确渲染含有透明部分的 PNG,所以你可以穿过去看到背景。虽然图像本身是 16 x16 像素的方块,在我们的游戏中它将是一个圆形。下载并保存到 index.html 的同一目录下,跟第一部分中的其他 PNG 文件一样。

为了在游戏中使用这个精灵,我们需要像其他图像一样单独加载它。打开第一部分的代码,添加下面的代码到 loadResources 紧接着加载 logo.png 文件的那行下面。

	gbox.addImage('logo', 'logo.png');
	 
	// ** 下面是第二部分代码 **
	 
	// 在这里添加我们的圆形主角
	gbox.addImage('playerSprite', 'playerSprite.png');
	 
	// 在这里剪切精灵层,设置格子的尺寸,每行的精灵数和间隙。
	gbox.addTiles({
	  id:      'playerTiles', // 设置一个唯一的 ID,以便将来调用
	  image:   'playerSprite', // 使用上面加载的‘精灵’图像
	  tileh:   16,
	  tilew:   16,
	  tilerow: 1,
	  gapx:    0,
	  gapy:    0
	});

首先,我们调用第一部分已经学过的 gbox.addImage 加载 PNG 文件。

然后调用 gbox.addTiles。许多 2D 游戏引擎支持精灵表(sprite sheet),它是一张大图,含有若干相同尺寸、按格网排列的小图。精灵图被剪切,并通过定义网格的尺寸和每行、每列的格子数量建立瓦片地图(tile map)。游戏引擎可以通过瓦片的编号访问到每个瓦片——2D 的动画可以定义为一格一个接一个的显示瓦片。

gbox.addTiles 函数需要知道图片和其拆分方式。你可以单独指定每个瓦片的高和宽,以及每行希望有多少个瓦片。我们的圆形是 16×16 像素,所以我们设置 tileh 和 tilew (高和宽)为 16。在这个例子中我们仅仅处理一个格,所以我们让它每行只用一个瓦片。gapx 和 gapy 变量定义每个格之间有多少偏移量(或者说需要忽略的“空白”),这里我们设为 0。

过一会我们定义玩家对象时,会使用我们定义的 playerTiles ID 来访问。

玩家对象的结构

定义一个对象的代码是非常冗长的,所以我们首先看一下定义一个对象的大致结构,然后再讨论一些特殊的实现。下面是部分从我们的对象中摘抄出的部分代码,包括了 addPlayer 函数。先参照注释阅读一遍代码,应当是相当清晰的。

	function main() {
	  // 第一部分的代码放在这里
	}
	 
	// 添加玩家对象的包裹函数——这是我们良好并且清晰的游戏的主要代码
	function addPlayer() {
	  // gbox.addObject 用变量和方法创建一个游戏中的新对象。在这里我们创建了玩家。
	  gbox.addObject({
	  // id 指向指定的对象,group 用于渲染的分组,tileset 指定了图像的来源
	    id: 'playerid',
	    group: 'player',
	    tileset: 'playerTiles',
	 
	  // 包含了对象第一次被创建时的初始化函数。在这里玩家对象只出现一次,要么是游戏开始,要么是玩家死亡然后重来。
	    initialize: function() {
	  // ...
	    },
	 
	    // first 函数是一个单步函数。它在每帧运行处理碰撞。被称为 first 是因为在渲染前执行,这样我们计算出新的位置和动作,<strong>然后</strong>渲染对象
	    first: function() {
	      // ...
	    },
	 
	    // blit 函数说明了游戏绘制圆的时候发生了什么。所有跟渲染和绘制有关的内容都放在了这里
	    blit: function() {
	      // ...
	    },
	  }); // 玩家的 gbox.addObject 结束
	} // 结束 addPlayer()

在 main 函数后面我们创建了addPlayer 函数用于封装,以保证我们的游戏主要代码清晰。这个函数在内部调用 gbox.addObject 进行了大量的设置。

最开始要做的是定义一些基本的识别变量。首先,为 玩家对象指定了 id,这样可以在其他函数中使用它。然后设置了组 ID,用于渲染的顺序。这让我们实现了“确保在背景组之上绘制玩家组的所有内容”,所以就不会发生玩家消失在背景之后的事情。然后我们设置了对象的 tileset 为‘playerTiles’,这是之前我们 tilemap 的图像的 ID。

剩下的部分分为三个函数:initialize, first 和 blit。

initialize 函数包含的所有代码在对象创建时运行一次以后再也不执行了。这通常包括如最大生命值、初始速度和对象的任何可以进行“设置”的内容。

first 函数在游戏图像绘制前的每一帧执行。游戏帧是一个用于计算和更新对象行为的小的时间片段。我们将对象的所有行为放在这个函数内,例如检查玩家的输入、检查一个对象是否同其他对象碰撞、根据当前的速度更新位置,更新人工智能等等。许多我们认为的“游戏编程”的内容都在这个函数内实现。

blit 函数包括对象的所有渲染代码。如果必须对在屏幕上显示图像做一些处理,就是在这里进行。跟 first 函数一样,在每帧都会调用,只不过是紧接着 first 函数。这是因为我们希望首先更新自己的位置,然后在屏幕上用已经更新过的位置绘制精灵。

下面的三个章节我们将讨论在这三个函数里放些什么内容。

initialize 函数

在这个例子中,我们的函数是空白的:

	initialize: function() {
	  // Toys 是用于指定确切样式的帮助函数。
	  // 为了创建顶视图,我们使用“topview”这个 Akihabara 提供的帮助函数实现它。
	 
	  // 这里我们只是调用并初始化玩家对象。
	  toys.topview.initialize(this, {
	  });
	},

上面的代码我们告诉游戏使用 toys.topview 类型初始化玩家对象,所有需要的初始化在 toys.topview 中完成。这对于下面的 first 和 blit 是很方便的。

first 函数

在 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);
	},

toys.topview.controlKeys 函数是一个帮助函数,用于映射每个方向上的加速度到控制键。在这个例子中,我们希望映射到方向键,所以这个函数最终实现了这个功能:是的,左是左,右是右。但是有时,需要编程实现这些功能。

当我们调用 toys.topview.handleAccellerations (是的,这里的函数名拼写有错误),我们是在告诉游戏引擎如果没有在指定方向上的加速度,则减速到 0。如果我们不使用这个函数,我们的玩家对象会在冰面上一样。在这里我们必须实现摩擦力。

toys.topview.applyForces 函数提供了根据最后的位置,以及当前的速度和加减速度确定新的位置。这是实际的移动发生的位置。

blit 函数

在 blit 函数中要做的事情是清除屏幕,然后根据新的位置绘制玩家对象。

	blit: function() {
	  // 清除屏幕。
	  gbox.blitFade(gbox.getBufferContext(),{});
	 
	  // 渲染当前的精灵……不要担心这里接下来的内容。我们将使用默认的绘制函数处理 tileset、帧信息、坐标、精灵是否进行切换,摄像机信息和 alpha 透明值。
	  gbox.blitTile(gbox.getBufferContext(), {
	    tileset: this.tileset,
	    tile:    this.frame,
	    dx:      this.x,
	    dy:      this.y,
	    fliph:   this.fliph,
	    flipv:   this.flipv,
	    camera:  this.camera,
	    alpha:   1.0
	  });
	},

gbox.blitFade 函数清除屏幕——调用 gbox.getBufferContext 获得当前的屏幕缓冲并传递进去,所以它知道要清理什么内容。

向 gbox.blitTile 函数传递了渲染这个瓦片的对象的信息。就像第一部分解释过的那样,“this”意味着我们正在写代码的那个对象,所以我们最终传递了当前的 tileset,当前的 x/y 坐标,和关于这个瓦片是否被垂直和水平翻转的信息(默认不进行),摄像机的信息,和 alpha 透明值 1(意味着完全不透明)。虽然我们传递了几乎 this.* 的所有数据,但只是传递了默认的值,对于现在来说足够了。

main 函数的一些组成

现在,我们需要对 main 函数设置一些内容,然后就大功告成了!

首先要做的是设置玩家对象的渲染层。在定义玩家对象的时候,我们指定这个对象到‘player’组。这里是 main 函数的首行调用 gbox.setGroups 添加 ‘player’ 组到内部的组存储的数组。

	function main() {
	  // ** 在第二部分我们在这行之后添加了‘player’ **
	  gbox.setGroups(['player', 'game']);

gbox.setGroups 函数告诉游戏用什么顺序渲染物体。物体按照 setGroups 列出的从左到右的顺序进行渲染。通过在最开始添加‘player’组,玩家对象将会首先被渲染,但是在‘game’组的内容将会被渲染在其之上。这样做的好处是‘game’组包括类似“Game Over”的消息将会显示在玩家的精灵上。

现在,我们需要剔除默认的难度选择,以及 ”Let’s begin!” 消息。为了实现这个,我们只要重写这些函数,使其什么也不做。在定义 maingame 之后的代码中,编写这些重写:

	maingame = gamecycle.createMaingame('game', 'game');
	  // ** 下面是第二部分代码 **
	 
	  // 屏蔽默认的难度选择菜单;在我们的指南中不需要这个
	  maingame.gameMenu = function() { return true; };
	 
	  // 屏蔽默认的“get ready”屏幕;在我们的指南中不需要这个
	  maingame.gameIntroAnimation = function() { return true; };

在 main 函数的结束,我们调用 gbox.go 去执行游戏,我们需要通过向游戏世界放置玩家对象初始化游戏。

	  // maingame.initializeGame 是游戏对象和行为定义的地方。这是你的游戏代码存在的地方!
	  maingame.initializeGame = function() {
	    addPlayer();
	  };
	 
	  gbox.go();
	}

maingame.initializeGame 函数中的代码设置游戏。跟之前的代码类似的方法,这里像《太空入侵者》那样设置玩家和所有的敌人。在这个代码中所要做的所有事情就是调用 addPlayer 函数初始化玩家对象。因为我们没有对玩家对象定义任何的坐标,默认的坐标是 (0,0),相对于屏幕的顶部和左边。

嘿,你已经可以移动了!

现在你可以保存你的 HTML 文件,并在兼容的浏览器中打开;你应该看到类似这样的内容。你会看到来自于第一部分的标题屏幕。按 Z 键跳过标题屏幕,蓝色的圆形会显示在屏幕上。并可以通过键盘上的方向键进行控制。

Akihabara 指南目录