扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
用GameCanvas类处理Graphics对象
本文,我将介绍Graphics对象类在游戏中被应用的主要方面。在Tumbleweed游戏中,我要画一个穿越草原的牛仔。在设计中,我将玩家的得分情况显示在屏幕底端,而游戏时间显示在顶端(为使样例不至太过复杂,我仅仅在玩家使用玩规定的时间后才自动终止游戏)。因为牛仔独自在走,我的想法是让他的背景向右或者向左滚动(否则在这个狭小的屏幕上他好象根本没走多远),而时间和分数的显示不动。
为了实现这个设计,我用JumpCanvas类来画屏幕顶端和底端稳定不动的显示条,而动态有趣的图形是用LayerManager类来实现的。
JumpCanva类创建时,你必须先分析要使用的显示屏幕。有一些显示屏幕信息来源于Graphics对象,一些来自于display对象,还有一些直接来源于GameCanvas类的方法。这些信息用来计算对象应当显示的位置,包括计算显示区域的尺寸,LayerManager子类(JumpManager)将在这些区域重复显示对象。如果你坚持维护Java“一处写就,处处运行”的特性,基于动态屏幕尺寸而不是基于常量尺寸设置屏幕显示区域显然更方便合理。
当然,如果从一种目标显示设备转向另一种游戏需要巨大的代码更改量,显然应该根据不同设备显示维护多个游戏版本,运行时根据显示设备信息调用游戏的不同版本,而不是将所有代码置于一个版本中。
在样例游戏中,如果实际显示的设备与游戏中定义的显示设备差别太大,将有一个异常抛出以便Jump类捕获并且显示警告给玩家。为了显得更专业,你应当确保给玩家的警告信息明确标明他针对当前的设备应该下载的版本号。
也许有点多余,但我后面仍然会告诉大家我认为顶端和底端区域合适的尺寸,paint(Graphics g)方法将描绘出顶端的白色和底端的绿色,g.drawString()方法用来记录使用的时间和分数。(不用问我为什么游戏的草原中都是颜色相同的绿草和风滚草;唯一的解释是我对Java的了解当然远甚于西部荒原。)
LayerManager类
一个MIDP游戏中富有乐趣的图形对象常常是用javax.microedition.lcdui.game.Layer类的子类来展示。背景层是javax.microedition.lcdui.game.TiledLayer的实例化,游戏的主人公(和他的敌人)是javax.microedition.lcdui.game.Sprite的实例,这两个类都是Layer的子类。LayerManager类用来组织所有的图形对象层,追加你的图层对象到管理器LayerManager的顺序,决定了他们运行时被描绘的次序(后进先出,最先追加的最后显示)。规则是上面的图层将覆盖下面的图层,不过你可以通过创建包含透明区域的图象文件(上层)来部分地显示下层图层中你想要的部分。
LayerManager类实际中最有用的也许是你可以创建一个远大于显示屏幕的图形对象,然后选择它将在屏幕中被显示的部分显示出来。你可以想象事先做出一幅巨大的、精心描绘的图画,然后用一张纸去覆盖它,而纸上有一个方孔在图画上自由移动。巨幅的整张图画代表你保存在LayberManager中的显示信息,而方孔是任一给定时刻在屏幕上显示图画的窗口。游戏代码设计中,一个比实际屏幕大得多的虚拟显示屏幕对于通常运行在小屏幕显示设备上的游戏是极其重要的,它将为你节省大量的时间和工作。
举个例子,比如你的游戏中包含主人公在一个复杂的地牢中摸索前进的场景,最麻烦的是你必须处理两个独立坐标系统。GameCanva的图形对象有一个坐标系,但是根据LayerManager坐标系的要求,图形中的不同图层又需要被置于LayerManager中。所以,一定要牢记LayerManager.paint(Graphics g, int x, int y)方法根据GameCanvas的坐标系在屏幕上描绘图层,而LayerManager.setViewWindow(int x, int y, int width, int height)方法根据LayerManager坐标系的要求设置LayerManager的可视矩形的显示属性。
在样例中,我设计了一个简单的背景(仅仅是一系列重复显示的草丛),但我想让牛仔从右向左行走的时候总是显示在屏幕中央,所以我需要不断改变LayerManager的图形可显示区域。这项工作是通过在LayerManager的子类JumpManager类中方法paint(Graphics g)中调用setViewWindow(int x, int y, int width, int height)方法完成的。更准确地说,逻辑是这样的:GameThread中的主循环调用JumpCanvas.checkKeys()来检查按键状态,并且通知JumpManager类牛仔此时应该向左、向右还是应该跳跃了。JumpCanva类通过调用setLeft(boolean left)或者jump()方法将信息传至JumpManager。如果信息表明牛仔此时应该向左走(向右与之类似),那么当GameThead调用JumpCanvas告诉JumpManager继续的时候(循环的下一步),JumpManager就告诉牛仔对象向左移动一个象素,同时通过向左移动视窗一个象素来保持牛仔在屏幕中央。你可以通过增加字段myCurrentLeftX的值(这个作为X坐标值传至setViewWindow(int x,int y,int width,int height)),接着调用myCowboy.advance(gameTicks, myLeft)来实现这两个动作。
当然,我可以不移动牛仔,也不把他追加到LayerManager,而只是独立地描绘他,这样也可以确保牛仔在屏幕中央。但显然通过将所有的移动对象置于相对静止的一系列图层中然后将视窗集中在牛仔对象上,借此保持所有对象的运动轨迹的方法要更容易。在通知牛仔前进的同时,风滚草对象Sprites和青草对象TiledLayer也也同时移动,然后检测牛仔是否被风滚草撞倒(在以下章节里我将说明实现这个功能的更多细节)。在移动了所有游戏对象后,JumpManager类调用wrap()方法来检查是否前端窗口到达了背景窗口的边缘,如果是,移动所有的有些对象以便视窗能继续在任一方向无限显示,接着JumpCanvas类重画每一个对象,再次执行游戏主循环。
Wrap()方法需要多说几句。很不幸,如果你想让你的简单背景不确定地重复显示,LayerManager类并不提供现成的隐藏方法. 当传至方法setViewWindow(int x, int y, int width, int height)的坐标参数值大于Integer. MAX_VALUE 时,LayerManager的图形区域将被隐藏,但这似乎对我们的想法没有任何帮助。因此,你必须写自己的函数来避免牛仔Sprite对象离开背景区域。
样例中,背景草丛总是在间隔数值Grass.TILE_WIDTH*Grass.CYCLE后被重画,所以任何时候视窗的X坐标值(myCurrentLeftX)都是背景图形宽度的整数倍。这样每次重画时,我都可以将视窗移回到显示中心,并且同时在相同的方向上移动Sprites对象,这样显然能平滑地阻止牛仔到达背景边界。
Listing 4 JumpManager.java.
|
Sprite实例(以下简称Sprite)都代表填充了一个图象的图形对象。仅仅包含一个图象是Sprite和TiledLayer类之间的本质区别,TiledLayer类的实例代表一个由多个可被操作的图象覆盖的区域(Sprite类有其他额外的特征,但每次它操作一个图象而不是用图象填充图形区域是最显而易见的特点)。所以Sprite通常被用来实现小的、活动的游戏对象(比如你的太空船和将与之相撞的小行星),而TiledLayer更多地被用来做活动的背景图形。Sprite一个很酷的特性是,尽管Sprite一次只能表示一个图象,但它能很容易地在不同情景下显示不同的图象,实现由一系列图象构成的动画效果。
样例游戏中,牛仔在跑动时有三个不同的姿势图象,在跳跃时还有另外一个。一个给定的Sprite需要的所有图象都应当保存在一个图象文件中(为了找到图象文件被调用的地方,Class.getResource()方法中将图象文件地址以相同的格式作为参数在jar文件中查找)。多帧图象保存在一个简单Image对象中显然让我们感觉轻松不少,因为我们不必操作不同的Image对象以便决定Sprite在一个给定时刻显示哪一个Image对象。风滚草图象文件包含三帧图象,当它们顺序显示时实现了一个滚动的动画效果。
在某一时刻应该选择哪一帧显示,这个很难计算,取决于你的感受。首先,如果你的图象文件包含了多个图象,你要用constructor来构建一个Sprite,定义你计划的Sprite的长度和宽度(用象素)。而Image的长度和宽度应当是你准备传到constructor的长度和宽度参数的整数倍。换句话说,计算机会均匀地将Image分割成若干按你的尺寸定义的矩形。正如前边的例子看到的那样,你可以在它的横向和纵向分别设置子图象,为了方便,你甚至可以在一个多行多列的网格中设置它们。然后,定义各个独立帧,对它们进行编号,顺序是从左上角顶点开始,然后向右,接着是下一行,就象你通常的阅读习惯一样。最后,选择某一帧显示的时候,直接将帧编号作为参数传入并调用setFrame(int sequenceIndex)方法就可以了。
Sprite类有一些为了实现动画效果的附加支持,比如允许你用setFrameSequence(int[] sequence) 方法定义帧的显示顺序。如Listing 5所示,我为牛仔定义了一个帧顺序{1,2,3,2},而风滚草是{0,1,2}(注意对于风滚草,我定义的顺序恰恰是默认的帧顺序,所以不用特意在代码中设置)。要想将活动的Sprite从某一帧移动到下一帧,需要调用nextFrame()(与之相反,可以用prevFrame())。这样处理对类似我的风滚草这样的动画效果显然是很方便的,所有的有效帧都会被用到,而对于牛仔的活动这样处理有点不便,因为只有一个Image或者Images都不在动画的帧顺序之内。这是因为一旦一个帧顺序被设置,传至setFrame(int sequenceIndex) 方法的参数只能表示用于帧顺序的索引,而不是帧本身的索引。那意味着一旦我设置好牛仔的帧顺序{3,2,1,2},如果我调用setFrame(0)方法,将返回帧编号1,setFrame(1)返回2,setFrame(2)返回3,而setFrame(3)返回2。但当牛仔跳起时,我想让它返回0,然而这一帧已经无效了。所以当我的牛仔跳起时,我必须在调用setFrame(0)前设置帧顺序为空(null),然后将帧顺序设置回{1,2,3,2}。Listing 5 jump()和advance(int tickCount,boolean left)方法中显示这个逻辑。
除了通过调用不同的显示帧改变你的Sprite显示外,你也可以通过直接的转换比如旋转或者镜象来展现不同的Sprite。Listing 5 和Listing 6样例中的牛仔和风滚草活动都是可左可右,所以我这里用了镜象转换。
一旦开始镜象转换,你必须首先保存Sprite参考象素的轨迹,因为你转换你的Sprite后,参考象素就是为改变前的象素。你可能以为如果你的Sprite图象是一个正方形,转换后图象将会呆在屏幕中同样的位置。事实不是这样。举例来说明。想象一个面朝左方的站立的人,他的参考象素可以定义为他的脚趾顶端。在旋转90度后,他所处的位置恰好就象他被拌倒向前摔倒后的姿势。很明显,转换后你的参考象素(脚趾顶端)的位置事先已被设置为和以前相同。如果你想让Sprite转换后继续占据屏幕上同样的区域,那么你应当首先调用defineReferencePixel(int x, int y)方法,并且设置你的Sprite参考象素为Sprite中心,正如我在Listing 5中对牛仔的设置那样。(另一个避免Sprite转换后失去位置的技巧是在转换前调用getX()和getY()方法获得Sprite左上角的绝对坐标,然后在转换后调用setPosition()设置左上角坐标为以前的位置。)
搞明白defineReferencePixel(int x, int y) 中引用的坐标与Sprite的顶端角相关,但是传至setRefPixelPosition(int x, int y) 告诉Sprite的参考坐标位置的坐标参数是根据屏幕坐标来的。更准确地说,如果Sprite是直接被绘制到Canvas,则传至setRefPixelPosition(int x, int y) 的坐标参数指的是Canvas的坐标系统,但如果Sprite是被一个LayerManager类绘制,这些参数应该根据LayerManager坐标系来确定。(这些坐标系统如何协调工作在前已有论述,请参考。)
在那些用来设置、获得参考象素或者设置、获得左上角位置象素的方法中,调用的坐标参数指的是对应的Canvas或者LayberManager坐标系上的坐标参数。还有需要注意的是如果你要进行多个转换,稍后的转换应用于它的前一个图象,而不是它的当前状态。换句话说,如果我对某一行应用了两次镜象转换setTransform(TRANS_MIRROR) ,第二次转换并不能将Image转换回最初的位置;它仅仅重复镜象转换的动作。如果你打算将一个转换后的Sprite返回到它转换前的位置,需要调用setTransform(TRANS_NONE) 方法,具体请参考Cowboy.advance(int tickCount, boolean left) 方法的前几行。
Layer类(包括Sprite和TiledLayer)的另一个伟大的特征是支持你用相对距离而不是绝对距离来设置对象位置。如果你的Sprite需要移动3个象素,无论它当前的位置坐标是多少,你只需调用move(int x,int y)方法就可以,而不是与之相反的调用setRefPixelPosition(int x, int y) 方法设置Sprite的新坐标来实现。更有用的是collidesWith()方法,它允许你检测是否一个Sprite抢占了另一个Sprite,或者TiledLayer,甚至Image的区域。这个方法的结果保存了许多比较后获得的参数,很容易看到,特别是当你传递一个象素级参数”true”后,如果他们的不透明象素有重叠之处,它将认为这两个图层发生冲突。
在风滚草游戏中,在所有Sprites向前移动后,我检测了是否牛仔与任何风滚草发生冲突(这发生在被JumpManager.advance(int gameTicks)调用的Cowboy.checkCollision(Tumbleweed tumbleweed)方法中)。当然因为如果当前的风滚草处于不可视状态,校验函数会返回False,所以我可以避免再花时间校验牛仔与这些不可视风滚草的冲突。除此以外,在很多情况下,通过只和可能发生冲突的对象进行校验,你还可以进一步提高程序的效率。注意样例游戏中我没有庸人自绕地检查是否风滚草彼此之间或者检查是否任何对象和背景草丛发生冲突,那显然与游戏无关。在你进行象素级别的冲突检测时,你要保证你的图象有一个透明的背景。(这通常也是很有用的,以避免你的Sprite不会用背景颜色在另一个Sprite或者Image上画一个丑陋的矩形)。
Listing 5 Cowboy.java.
|
象前面提到的,TiledLayer与Sprite类很相似,只是TiledLayer类包含多列,每一列被一套独立的图象帧绘制。另一个区别是TiledLayer类中缺乏大多数功能性相关的方法;TiledLayer没有转换,参考象素,甚至帧顺序。
当然, 同时管理多个图象使事情变得复杂,我将用TiledLayer的子类Grass来说明TiledLayer类的使用。该类在屏幕背景上显示一排前后随风舞动的绿草。为了使画面更加有趣,某些列拥有舞动的绿草,而另一些是低矮的草丛,仅仅是一层覆盖地面的绿色。
创建TiledLayer的第一步是决定你需要的格子的行列数。如果你不想让你的图层看上去是简单的矩形,那不是问题,因为没有用到的网格默认设置为空,这就避免了它们和别的图象一样排列。在我的样例中,如Listing 7所示,我只安排了一行,而列数是根据屏幕的宽度计算得到。
一旦你设置了要用到的行数和列数,你就可以用一个饰片来填充网格,方法是setCell(int col, int row, int tileIndex) ,“Sprite类”章中解释了tileIndex参数。如果你希望某些网格被活动图象填充,你需要通过调用createAnimatedTile(int staticTileIndex)方法创建一个活动饰片,该方法将返回装饰片的索引给你的新的饰片。你尽可以多做几个活动饰片,但要记住,如果你想让网格同时显示同样的动画,必须每一个饰片都要能在多个网格中使用。
在我的例子中,我只创建了一个活动饰片并且一直重用它,因为我想让我的所有活动绿草同时摇曳。网格被设置在Listing 7 Grass的创建方法中。为让饰片动起来,你无须使用象Sprite用到的事先定义好的帧顺序,所以你必须通过setAnimatedTile(int animatedTileIndex, int staticTileIndex)方法设置帧。这个方法设置了当前所有包含给定活动饰片的帧,从而,所有包含该活动饰片的网格相对应的animatedTileIndex将被变成当前参数staticTileIndex指定的图象。为了使动画更改变得简单,增加自己的帧顺序功能是必要的;请参考Grass.advace(int tickCount)方法看这个功能是如何实现的。
Listing 7. Grass.java
|
现在,你已经完整地看到了一个简单但是基础的游戏,它说明了如何使用javax.microedition.lcdui.game包中的所有类。风滚草样例游戏还特别展示了如何充分利用MIDP2.0的图形和动画特性。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。