6.1 游戏API简介
MIDP 2.0相对于1.0来说,最大的变化就是新添加了用于支持游戏的API,它们被放在javax.microedition.lcdui.game包中。游戏API包提供了一系列针对无线设备的游戏开发类。由于无线设备仅有有限的计算能力,因此许多API的目的在于提高Java游戏的性能,并且把原来很多需要手动编写的代码如屏幕双缓冲、图像剪裁等都交给API间接调用本地代码来实现。各厂家有相当大的自由来优化它们。
游戏API使用了MIDP的低级图形类接口(Graphics,Image,等等)。整个game包仅有5个Class:
GameCanvas
这个类是LCDUI的Canvas类的子类,为游戏提供了基本的“屏幕”功能。除了从Canvas继承下来的方法外,这个类还提供了游戏专用的功能,如查询当前游戏键状态的能力,同步图像输出;这些功能简化了游戏开发并提高了性能。
Layer
Layer类代表游戏中的一个可视化元素,例如Sprite或TiledLayer是它的子类;这个抽象类搭好了层(Layer)的基本框架并提供了一些基本的属性,如位置,大小,可视与否。出于优化的考虑,不允许直接产生Layer的子类(不能包外继承)。
LayerManager
对于有着许多Layer的游戏而言,LayerManager通过实现分层次的自动渲染,从而简化了游戏开发。它允许开发者设置一个可视窗口(View Window),表示用户在游戏中可见的窗口; LayerManager自动渲染游戏中的Layer,从而实现期望的视图效果。
Sprite
Sprite又称“精灵”,也是一种Layer,可以显示一帧或多帧的连续图像。但所有的帧都是相同大小的,并且由一个Image对象提供。Sprite通过循环显示每一帧,可以实现任意顺序的动画;Sprite类还提供了许多变换(翻转和旋转)模式和碰撞检测方法,能大大简化游戏逻辑的实现。
TiledLayer
TiledLayer又称“砖块”,这个类允许开发者在不必使用非常大的Image对象的情况下创建一个大的图像内容。TiledLayer有许多单元格构成,每个单元格能显示由一个单一Image对象提供的一组贴图中的某一个贴图。单元格也能被动画贴图填充,动画贴图的内容能非常迅速地变化;这个功能对于动画显示非常大的一组单元格非常有用,例如一个充满水的动态区域。
在游戏中,某些方法如果改变了Layer,LayerManager,Sprite和TiledLayer对象的状态,通常并不能立刻显示出视觉变化。因为这些状态仅仅存储在对象里,只有当随后调用我们自己的paint()方法时才会更新显示。这种模式非常适合游戏程序,因为在一个游戏循环中,一些对象的状态会更新,在每个循环的最后,整个屏幕才会被重绘。基于轮询也是现在视频游戏的基本结构。
6.2 GameCanvas的使用
GameCanvas类提供了基本的游戏用户接口。除了从Canvas继承下来的特性(命令,输入事件等)以外,它还提供了专门针对游戏的功能,比如后备屏幕缓冲和键盘状态查询的能力。
每个GameCanvas实例都会有一个为之创建的专用的缓冲区。因为每个GameCanvas实例都会有一个唯一的缓冲区。可以从GameCanvas实例获得其对应的Graphics对象,而且,只有对Graphics对象操作,才会修改缓冲区的内容。外部资源如其他的MIDlet或者系统级的通知都不会导致缓冲区内容被修改。该缓冲区在初始化时被填充为白色。
缓冲区大小被设置为GameCanvas的最大尺度。然而,当请求填充时,可被填充的区域大小会受限于当前GameCanvas的尺度,一个存在的Ticker,Command等等都会影响到GameCanvas的大小。GameCanvas的当前大小可以通过调用getWidth和getHeight获得。
一个游戏可能提供自己的线程来运行游戏循环。一个典型的循环将检查输入,实现游戏逻辑,然后渲染更新后的用户界面。以下代码演示了一个典型的游戏循环的结构:
// 从后备屏幕缓冲获得Graphics对象
Graphics g = getGraphics();
while (true) {
// 检查用户输入并更新位置,如果有需要
int keyState = getKeyStates();
if ((keyState & LEFT_PRESSED) != 0) {
sprite.move(-1, 0);
}
else if ((keyState & RIGHT_PRESSED) != 0) {
sprite.move(1, 0);
}
// 将背景清除成白色
g.setColor(0xFFFFFF);
g.fillRect(0,0,getWidth(), getHeight());
// 绘制Sprite(精灵)
sprite.paint(g);
// 输出后备缓冲区的内容
flushGraphics();
}
6.2.1 绘图
要创建一个新的GameCanvas实例,只能通过继承并调用父类的构造函数:
protected GameCanvas(boolean suppressKeyEvents),
这将使为GameCanvas准备的一个新的缓冲区也被创建并在初始化时被填充为白色。
为了在GameCanvas上绘图,首先要获得Graphics对象来渲染GameCanvas:
protected Graphics getGraphics()
返回的Graphics对象将用于渲染属于这个GameCanvas的后备屏幕缓冲区(off-screen buffer)。 但是渲染结果不会立刻显示出来,直到调用flushGraphics()方法;输出缓冲区的内容也不会改变缓冲区的内容,即输出操作不会清除缓冲区的像素。
每次调用这个方法时,都会创建一个新的Graphics对象;对于每个GameCanvas实例,获得的多个Graphics对象都将渲染同一个后备屏幕缓冲区。因此,有必要在游戏启动前获得并存储Graphics对象,以便游戏运行时能反复使用。
刚创建的Graphics对象有以下属性:
l 渲染目标是这个GameCanvas的缓冲区;
l 渲染区域覆盖整个缓冲区;
l 当前颜色是黑色(black);
l 字体和调用Font.getDefaultFont()返回的相同;
l 绘图模式为SOLID;
l 坐标系统的原点定位在缓冲区的左上角。
在完成了绘图操作后,可以使用flushGraphics()方法将后备屏幕缓冲区的内容输出到显示屏上。输出区域的大小与GameCanvas的大小相同。输出操作不会改变后备屏幕缓冲区的内容。这个方法会直到输出操作完成后才返回,因此,当这个方法返回时,应用程序可以立刻对缓冲区进行下一帧的渲染。
如果GameCanvas当前没有被显示,或者系统忙而不能执行输出请求,这个方法不做任何事就立刻返回。
6.2.2 键盘
如果需要,开发者可以随时调用getKeyStates方法来查询键的状态。getKeyStates()获取游戏的物理键状态。返回值的每个比特位都表示设备上的一个特定的键。如果一个键对应的比特位的值为1,表示该键当前被按下,或者自上次调用此方法后到现在,至少被按下过一次。如果一个键对应的比特位的值为0,表示该键当前未被按下,并且自上次调用此方法后到现在从未被按下过。这种“闭锁行为(latching behavior)”保证一个快速的按键和释放总是能够在游戏循环中被捕获,不管循环有多慢。下面是获取游戏按键的示例:
// 获得键的状态并存储
int keyState = getKeyStates();
if ((keyState & LEFT_KEY) != 0) {
positionX--;
}
else if ((keyState & RIGHT_KEY) != 0) {
positionX++;
}
调用这个方法的副作用是不能及时清除过期的状态。在一个getKeyStates调用后如果紧接着另一个调用,键的当前状态将取决于系统是否已经清除了上一次调用后的结果。
某些设备可能无法直接访问键盘硬件,因此,这个方法可能是通过监视键的按下和释放事件来实现的,这会导致getKeyStates可能滞后于当前物理键的状态,因为时延取决于每个设备的性能。某些设备还可能没有探测多个键同时按下的能力。
请注意,除非GameCanvas当前可见(通过调用Displayable.isShown()方法),否则此方法返回0。一旦GameCanvas变为可见,将初始化所有键为未按下状态(0)。
6.3 Sprite的使用
Sprite是一个基本的可视元素,可以用存储在图像中的一帧或多帧来渲染它;轮流显示不同的帧可以令Sprite实现动画。翻转和旋转等几种变换方式也能应用于Sprite使之外观改变。作为Layer子类,Sprite的位置可以改变,并且还能设置其可视与否。
6.3.1 Sprite帧
用于渲染Sprite的原始帧由一个单独的Image对象提供,此Image可以是可变的,也可以是不可变的。如果使用多帧,图像将按照指定的宽度和高度被切割成一系列相同大小的帧。正如下图所示,同一序列的帧可以以不同的排列存储,这取决于游戏开发者是否方便开发。
每一帧都被赋予一个唯一的索引号。左上角的帧被赋予索引号0。余下的帧按照行的顺序索引号依次递增(索引号从第一行开始,接着是第二行,以此类推)。getRawFrameCount()方法返回所有原始帧的总数。
6.3.2 帧序列
Sprite的帧序列定义了帧以什么样的顺序来显示。缺省的帧序列就是所有可用帧的顺序排列,因此,帧序列和对应的帧的索引号是一致的。这表示缺省的帧序列的长度和所有原始帧的总数是相等的。例如,如果一个Sprite有4帧,缺省的帧序列为{0, 1, 2, 3}。
可以使用setFrameSequence(int[] sequence)来为Sprite设置帧序列。当调用此方法时,将会复制sequence数组;因此,随后对参数sequence数组进行的任何更改均不会影响Sprite的帧序列。传入null将使Sprite的帧序列重置为缺省值。
开发者必须在帧序列中手动切换当前帧。可以调用setFrame(int),prevFrame()或者nextFrame()方法来完成。注意,这些方法是针对帧序列操作,而不是对帧的索引操作。如果使用缺省的帧序列,那么帧序列的索引和帧的索引是可互换的。
如果愿意,可以为Sprite定义任意的帧序列。帧序列必须至少包含一个元素,并且每个元素都必须是一个有效的帧的索引号。通过定义新的帧序列,开发者可以方便地以任意想要的顺序来显示Sprite的帧;帧可以重复,忽略,或者以相反的顺序显示,等等。
例如,下图显示了一个特定的序列如何被用于动画显示一个蚊子。帧序列被设计为蚊子振动翅膀3次,然后在下次循环前暂停一会儿。
每次调用nextFrame()方法就会更新显示,动画效果如下:
要创建一个静态的Sprite,可以调用public Sprite(Image image),通过提供的图像创建一个新的Sprite。如果要创建动态的Sprite,就必须使用public Sprite(Image image,int frameWidth, int frameHeight)。
帧的大小由frameWidth和frameHeight指定,所有帧的大小必须相等。可以在图像中水平、竖直或以方格形式排列。源图像的宽度必须是帧宽度的整数倍,高度必须是帧高度的整数倍。如果image的宽度或高度不是frameWidth或frameHeight的整数倍,将会抛出IllegalArgumentException异常。
6.3.3 Reference Pixel
作为Layer的一个子类,Sprite继承了很多方法来设置和获取位置,如setPosition(x,y),getX()和getY()。这些方法定义的位置均以Sprite视图区域的左上角像素点为依据。然而,在某些情况下,根据Sprite的其它像素点来定位Sprite更加方便,尤其是在Sprite上应用某些转换。
因此,Sprite包含一个参考像素点(reference pixel)的概念。参考像素点通过指定其在Sprite未经变换的帧内的某一点来定义,使用defineReferencePixel(x,y)方法。缺省的,参考像素点定义在帧的(0,0)像素点。如果有必要,参考像素点也可以定义在帧区域以外。
在这个例子中,参考像素点被定义在猴子悬挂的手上:
getRefPixelX()和getRefPixelY()方法可用于查询参考像素点在绘图坐标系统中的位置。开发者也可以调用setRefPixelPosition(x,y)方法来定位Sprite,使得参考像素点定义在绘图坐标系统中的指定位置。这些方法自动地适应任何应用在Sprite上的变换。
在这个例子中,参考像素点被定位在树枝末端的一点;Sprite的位置也改变了,使得参考像素点定位在这一点上,猴子看起来像挂在树枝上。
6.3.4 Sprite的变换
几种变换可应用于Sprite。可用的变换包括旋转几个90度加上镜像(沿垂直轴)。 Sprite的变换通过调用setTransform(transform)方法实现。
当应用一个变换时,Sprite被自动重新定位,使得参考像素点在绘图坐标系统中看起来是静止的。因此,参考像素点即为变换操作的中心点。因为参考像素点并未移动,getRefPixelX()和getRefPixelY()方法返回的值仍不变;但是,getX()和getY()方法可能改变以便反映出Sprite左上角位置的移动。
再次回到猴子的例子上来,当应用一个90度旋转后,参考像素点的位置仍然在(48, 22),因此使得猴子像是在沿着树枝飘着:
由于某些变换涉及到90度或270度旋转,其使用结果可能导致Sprite的宽度和高度互换。因此,调用Layer.getWidth()和Layer.getHeight()方法的返回值可能改变。
6.3.5 绘制Sprite
可以在任何时候通过调用paint(Graphics)方法来绘制Sprite。 Sprite将被绘制在Graphics对象上,根据Sprite保持的当前状态信息(如位置,帧,可视与否)。擦除Sprite通常是Sprite以外的类的责任。
厂商可以使用任何希望使用的技术(如硬件加速可以用于所有Sprite,或特定大小的Sprite,或者根本不使用硬件加速)来实现Sprite。对一些平台而言,特定大小的Sprite可能对于其它大小的Sprite更高效;厂商可以选择提供给开发者关于设备相关的这些特性。
6.3.6 碰撞检测
Sprite非常适合移动的物体,如游戏主角、敌人等等,在游戏中,可以使用Sprite提供的碰撞检测功能来简化游戏逻辑。
使用defineCollisionRectangle()定义用于碰撞检测的Sprite的矩形区域。此指定的矩形是相对于未经变换的Sprite的左上角,该区域将用于检测碰撞。对于像素级的碰撞检测,仅仅在这个碰撞检测区内部的像素点会被检查。缺省的,Sprite的碰撞检测区定位在(0,0),并与Sprite尺度相同。碰撞检测区也可以指定为大于或小于缺省的碰撞检测区;如果大于缺省的碰撞检测区,在Sprite之外的像素在像素级的碰撞检测时被认为是透明的。
要判断两个Sprite是否碰撞,或者与其他Layer是否碰撞,可以使用collidesWith()方法。如果使用像素级检测,仅当非透明像素重叠时,碰撞才被检测到。即第一个Sprite中的非透明像素和第二个Sprite中的非透明像素重叠时,碰撞才被检测到。仅仅那些包含在Sprite的碰撞检测区内的像素会被检测。
如果不使用像素级检测,这个方法就简单地检查两个Sprite的碰撞检测区矩形是否有重合。 如果对Sprite应用了变换,会进行相应的处理。注意,只有两个Sprite都可见时,才能检测碰撞。
6.4 Layer的使用
Layer是一个抽象类,表示游戏中的一个可视元素。上节中讲述的Sprite就是Layer的一种。每个Layer都有位置(取决于它的左上角在其容器中的位置),宽度,高度和可视与否。 Layer的子类必须实现一个paint(Graphics)方法,使得它们能够被渲染。如果该Layer可见。 Layer从它的左上角开始渲染,其当前坐标(x,y)是相对于原始的Graphics对象。当渲染Layer时,应用程序可以使用剪辑和坐标变换来控制并限制渲染的区域。实现此方法的子类有责任检查Layer是否可见,如果不可见,这个方法应该不做任何事。此外,调用此方法不应该改变Graphics对象的属性(剪辑区域,坐标变换,绘图颜色等等)。
Layer的位置坐标(x,y)通常都是相对于Graphics对象的坐标系统,该对象通过Layer的paint()方法传递。这个坐标系统被称为绘图坐标系统。一个Layer的初始位置是(0,0)。
6.4.1 TiledLayer
TiledLayer由一系列单元格组成,单元格可被一组贴图填充。这个类允许不必使用特别大的图像来创建大的虚拟层。这个技术在2D游戏中被广泛用于创建特别大的可卷动的背景。
贴图(Tiles)
贴图用于填充TiledLayer的单元格,由一个单一的可变或不可变的Image对象提供。图像被切割成一系列相同大小的贴图;贴图大小随Image一同指定。如下图所示,相同的一系列贴图可以以不同的方式存储,取决于对游戏开发者而言方便与否。
每个贴图都被赋予一个唯一的索引号。位于图像最左上角的贴图被赋予索引号1。剩下的贴图按照一行一行的顺序(首先是第一行,然后是第二行,以此类推)依次递增。这些贴图被视为静态贴图(static tiles),因为贴图和图像内容有固定的联系。
当实例化一个TiledLayer时,一组静态贴图就被创建了;也可以在任何时候调用setStaticTileSet(javax.microedition.lcdui.Image, int, int)方法来更新它们。
除了静态贴图外,开发者同样能够定义一系列动态贴图(animated tiles)。一个动态贴图就是一个虚拟的贴图,它与一个静态贴图动态地联系在一起;一个动态贴图的外观就是当时与之联系的静态贴图。
动态贴图允许开发者能非常容易地改变一组单元格的外观。对于用动态贴图填充的单元格而言,改变它们的外观仅仅需要简单地改变与动态贴图关联的静态贴图即可。此技术对于动画显示大的重复性区域非常有用,因为不需要显式地改变大量单元格的内容。
动态贴图可以通过调用createAnimatedTile(int)方法来创建,该方法返回一个索引号,用于标记新创建的动态贴图。动态贴图的索引号总是负数,并且也是连续的,起始值为-1。一旦被创建,与之关联的静态贴图可以通过调用setAnimatedTile(int, int)方法来改变。
单元格(Cells)
TiledLayer由相同大小的单元格组成;每行和每列的单元格数目在构造方法中指定,实际大小取决于贴图的大小。
每个单元格的内容由贴图索引号指定;一个正的贴图索引号代表一个静态贴图,一个负的贴图索引号代表一个动态贴图。索引号为0的贴图表示该单元格为空;为空的单元格是完全透明的,并且不会被TiledLayer绘制任何内容。缺省的,所有单元格都包含索引号为0的贴图。
可以通过调用setCell(int, int, int)和fillCells(int, int, int, int, int)方法改变单元格的内容。很多单元格可以包含同一个贴图;然而,一个单元格仅能包含一个贴图。下面的例子演示了如何使用TiledLayer来创建一个简单的背景。
在这个例子中,水的区域由动态贴图来填充,索引号为-1,该动态贴图在初始化时与一个索引号为5的静态贴图关联。可以简单地通过调用setAnimatedTile(-1, 7)方法来改变与之联系的静态贴图,从而实现整个水区域的动画效果。
渲染一个TiledLayer
可以手动调用paint()方法来渲染一个TiledLayer;也可以使用LayerManager对象自动渲染它。绘图方法将尝试渲染在Graphics对象的剪裁区域内的整个TiledLayer;从TiledLayer的左上角开始渲染,该点的当前坐标(x,y)相对于Graphics对象的原点。渲染区域可以通过设置Graphics对象的剪裁区域来控制。
6.4.2 LayerManager
LayerManager管理一系列的Layer。LayerManager简化了渲染每个Layer的过程,每个添加的Layer都将在正确的区域并以正确的顺序被渲染。
LayerManager维护一个顺序列表,以便管理如何追加、插入和删除Layer。一个Layer的索引号关联了它的Z轴位置(z-order);索引号为0的Layer最接近用户,索引号越大的Layer离用户越远。索引号永远是连续的,即,如果一个Layer被删除,后面的Layer的索引号都将调整使得索引号保持连续。
LayerManager类提供一些用于控制游戏中如何在屏幕上渲染Layer的功能。
可视窗口(view window)控制着可视区域及其在LayerManager的坐标系统中的位置。改变可视窗口的位置可以实现上下或左右滚动屏幕的效果。例如,如果想向右移动,简单地将可视窗口的位置右移。可视窗口的大小决定了用户的可视范围,通常它应该适合设备的屏幕大小。
在这个例子中,可视窗口被设置为85x85像素大小,并定位在LayerManager的坐标系统的(52, 11)点。每个Layer的位置都是相对于LayerManager的原点。
paint(Graphics, int, int)方法包含一个(x,y)坐标,控制可视窗口在屏幕中的显示位置。改变参数不会改变可视窗口的内容,仅仅简单地改变可视窗口在屏幕中被绘制的位置。注意到这个位置是相对于Graphics对象的原点而言的,因此它服从Graphics对象的变换属性。
例如,如果一个游戏在屏幕的最顶端显示分数,可视窗口可能在(17,17)点被渲染,确保有足够的空间来显示分数。
为了添加一个Layer,我们使用append()方法向这个LayerManager添加一个Layer。Layer将被添加到现有Layer列表的末尾,即有最大的索引号(离用户最远)。如果此Layer已存在,将在添加前首先被删除。
insert()方法与append()的区别在于可以指定Layer的索引号。如果此Layer已存在,将在添加前首先被删除。
获得指定位置的Layer可以调用getLayerAt(int index)方法。
渲染
LayerManager的paint()方法以降序的顺序来渲染每一个Layer,以保证实现正确的Z轴次序。完全在可视窗口之外的Layer将不被渲染。
此方法的另外两个参数决定了LayerManager的可视窗口相对于Graphics对象的原点在何处渲染。例如,一个游戏可能在屏幕上方显示分数,因此游戏的Layer就必须在这个区域下面,可视窗口可能在点(0, 20)处开始渲染。此位置相对于Graphics对象的原点,因此Graphics对象的坐标转换模式也会影响可视窗口在屏幕上渲染的位置。
Graphics对象的剪裁区域被设置为与位于(x,y)处的可视窗口的区域一致。 LayerManager将转换Graphics对象的坐标,使得点(x,y)与可视窗口在LayerManager中的坐标系统的位置一致。然后,Layer以一定的次序被渲染。在方法返回前,Graphics对象的坐标转换模式和剪裁区将重置为原先的值。
渲染会自动适应Graphics对象的剪裁区域和变换方式。这样,如果剪裁区域不够大,可视窗口仅有部分被渲染。
为了提升速度,这个方法可能忽略不可见的Layer,或者全部在Graphics对象剪裁区域以外的Layer。在调用Layer的paint()方法前,Graphics对象并不会重置为一个确定的状态。剪裁区域可能大于Layer的区域,因此,Layer必须自己保证渲染操作在其范围内进行。
6.5 一个示例
这里给出一个示例游戏的核心代码。这是一个著名的潜艇游戏的手机版本。由黄晔开发。这是一个良好的游戏入门范本,其中涉及到精灵的使用、Tiled的使用以及碰撞检测等运动类2D-Tile-based游戏常见的问题。希望通过对他的学习给你一些启示。为了配合本章API的讲解,我们省略的大部分的游戏周边代码,这里给出的仅仅是游戏的GameCanvas子类。并且这个类同时包含了游戏的主循环线程。用于教学是再好不过了。你可以在www.j2medev.com的文章区看到完整的游戏设计和源代码。
如你所见的,这不是一个产品质量的游戏。他教会你基本的内容,而不是全部。同样这里没有包括什么优化。一下是游戏的截图。另外本游戏所用的资源大多不属于作者,代码仅供非商业用途的学习参考。在此强调想要编译游戏你需要下载完整的源代码。
请着重注意代码中的一下方法:
l paintCanvas方法用于渲染;
l run方法用于游戏的主循环;
l 关于输入捕获的的一点说明是,这个游戏并没有屏蔽键盘事件,他混合使用了主动轮询用于潜艇运动,而开火则采用捕获方式。
评论