扫描关注游侠网
热门资讯排行
精品手机游戏
热门游戏专题
赛车竞速RAC(英文)
动作游戏ACT(英文)
角色扮演RPG(中文)
小游戏在线玩[书摘]Android手机游戏开发实例
发表于 15:20|
来源机械工业出版社|
作者机械工业出版社
摘要:本文节选于机械工业出版社推出的《Android应用开发揭秘》一书,作者为杨丰盛。 本书内容丰富全面,较为详细地讲解了Android框架、Android组件、用户界面开发、游戏开发、数据存储、多媒体开
&本文节选于机械工业出版社推出的一书,作者为杨丰盛。本书内容丰富全面,较为详细地讲解了Android框架、Android组件、用户界面开发、游戏开发、数据存储、多媒体开发和网络开发等基础知识,而且还深入阐述了传感器、语音识别、桌面组件开发、Android游戏引擎设计、Android应用优化、OpenGL等高级知识。另外,本书还全面介绍了如何利用原生的C/C++(NDK)和Python、Lua等脚本语言(Android Scripting Environment)来开发Android应用,并以迭代的方式重现了各种常用的Android应用和经典Android游戏的开发全过程。
第11章 Android游戏开发实例
手机游戏被业内人士称为继短信之后的又一座&金矿&。从2005年开始,全球手机游戏市场一直以每年40%的速度增长,2006年,手机游戏掘金9亿美元,成为超过无线音乐和娱乐内容服务的最大应用。据预测,2010年中国手机游戏市场总销售额将达到95.27亿元,从2003年到2010年的市场销售额年均复合增长率为62.4%。可以看出,手机游戏行业将经历一个黄金发展时期。3G的普及、手机显示性能的提高、手机屏幕的扩大和较高的分辨率,解决了传统手机游戏行业的很多弊端,同时Android也采用了键盘形式的按键,可以完成更复杂的操作。Android虽然基于Java,但是其虚拟机是基于寄存器的(这与普通的Java基于堆栈不同),速度得到了很大的提升,这为Android平台上开发游戏提供了坚强的后盾。
11.1 &手机游戏开发简介
在这里首先要说的是大家不要认为开发游戏很难,开发游戏其实比开发一些应用更简单,为什么呢?因为游戏的本质就是在屏幕上不断地显示和更新图片,只不过不是胡乱地更新,而是根据程序逻辑来控制,比如要实现一个主角在地图上移动,那么只需要将编辑好的地图先绘制到屏幕上去,然后再将主角绘制上去,剩下的便是逻辑判断了。再如,要让主角移动,当我们按下了前进键时,只需要将主角绘制的位置向前移动就可以达到目的了(在碰撞检测之后主角能够移动的情况下)。但同时也不要就因为这些就认为开发一款游戏很简单,一款完整的游戏需要多方面的知识,比如游戏的创意、背景、故事情节、游戏音效、游戏风格、游戏类型、运行速度、适配机型等。 而且,游戏开发是需要策划、美工、程序、测试的协同工作和默契配合完成的。下面我们将详细地介绍游戏开发的流程。
1.游戏策划
一款游戏的策划是最重要的,它将直接决定该游戏的后期制作和玩家的喜好程度。在早期的游戏开发过程中,或许只是一个或者多个代码工作者将自己封闭起来狂写一段时间的代码就可以产生一款很不错的游戏,但现在如果还是这样,写出来的游戏多半不会受到用户的欢迎。目前游戏行业飞速发展,市面上各种各样的游戏简直是数不胜数,如何才能开发一款受广大用户喜爱的游戏呢?这就需要我们在策划之前确定市场的需求,比如,将很有中国特色的棋牌游戏&斗地主&拿到美国去销售,就算你做得很好,也许美国人根本就不玩这种游戏。再比如,国家明明禁止了***游戏和***,我们要是开发出这样的游戏能行吗?肯定不能,所以首先要确定目标客户。
在明确了目标客户之后,是否需要考虑游戏的类型呢?从大的方面来说,游戏是单机的还是联网的?是单人的还是多人的?是动作类型的还是角色扮演类型的?等等。在确定了游戏的类型之后,还需要考虑游戏的可玩性,总不可能说游戏玩家十多分钟就通关了,这就需要包括游戏的难度设置、关卡控制以及后期的版本控制。
现在我们就可以根据上面的目标客户和游戏类型来定义游戏的风格了,比如想以三国为题材做一个即时战略游戏,就不可能定义为现代风格,游戏中就不会出现坦克、飞机的道具。同时这也体现了游戏的真实性,但是游戏本身是一个虚幻的东西,玩家就是需要将自己放到虚拟的世界中,所以也不能过度真实,让玩家觉得枯燥乏味。风格确定之后,还可以根据游戏的风格来配置游戏音效。
由于玩家经常会接触到很多游戏,所以他在玩游戏时会对一些没有新意的游戏感到厌倦,&反正我玩过类似的游戏,没什么好玩的&。如果得到玩家这样的评论,不用我说,大家都知道这款游戏的成败了。虽然游戏创新并不是一件很容易的事情,但是为了吸引玩家,我们不得不大胆地创新。
这些问题都解决了,就可以准备写策划案了。在写策划案的同时还需要考虑到美工和程序在技术上的实现以及硬件的支持,不能设计技术达不到的效果。了解了这些问题,开始编写策划案。
其实策划是一个非常广泛的领域,有很多东西需要自己在实践中证明,这里只是列举了常用的、值得注意的地方。
美工在拿到策划文档之后,需要根据策划文档的描述来发挥自己的想象力,在保持和策划文档一致的情况下,进行创新。同样,为了保证美工的图片适合程序的要求,还需要多和程序员进行交流,以确保程序员能够很清楚地理解自己的设计,使游戏效果达到最好。
程序员在得到文档和资源后并不能马上打开编辑器,新建工程开始写代码,而是要仔细查看文档和资源,根据这些来确定所要使用的知识和所要实现的功能,然后构建一个整体的框架。这个整体的框架很重要,一个优秀的程序员会在框架的设计上花很多时间,因为一个好的框架可以使后面的开发、调试等更加简单,同时一个好的框架还能提高游戏的运行效率。为了保证质量,每个程序员写的程序都有Bug,所以我们需要不断地测试、修改,再测试、再修改,从而给玩家一个最好的体验,希望大家都能养成这个很好的习惯。
到目前为止,Android的游戏还很少。Android Market从日开始运营,从Goolge公布的数字来看,Android Market中的应用程序数量达到了5000个,和App Store比起来还是有明显的差距。下面是Android Market上面几款游戏的效果,如图11-1所示。
图11-1 Android平台游戏
在本书第5章我们学习了Android平台开发游戏的基础知识,本章我们将通过在Android平台完成一个风靡全球的经典游戏《魔塔》来学习Android游戏开发。该游戏包括了游戏开发中的大部分技术,包括地图、精灵、图层、音效、存档等。下面是本章将完成的游戏在Android上的运行效果,如图11-2所示。
图11-2 《魔塔》游戏效果
11.2 &游戏框架设计
前面已经介绍了框架在游戏开发中的重要地位,如何才能实现一个适合该游戏的框架呢?首先我们需要了解游戏的内容,从图11-2中可以大致看出游戏中包括了地图、主角、整个屏幕界面,显示了地图和主角的属性,地图上还有道具,至少需要一个视图来显示,并且需要更新界面的显示和一个控制游戏逻辑及事件的类。下面我们来构建该游戏的整体框架。
我们知道,在Android中要显示一个视图类就必须继承自View类,View类中包括一个最主要的绘制方法onDraw和一些事件的处理,比如onKeyDown、onKeyUp等。当然在构建这个视图类时还可以加入自己的一些抽象方法,比如资源回收(reCycle)、刷新(refurbish)等。有了这些内容,下面构建一个用于显示游戏界面的视图类GameView,当然该游戏也不只是这么一个界面,还有菜单、道具商店、战斗等,所以这个类应该是一个抽象类(abstract),可以被各个界面使用。GameView类的代码如代码清单11-1所示。
代码清单11-1&&第11章\MagicTower\src\com\yarin\android\MagicTower&GameView.java片段&public&abstract&class&GameView&extends&View&&{&&&&&&public&GameView(Context&context)&&&&&&{&&&&&&&&&&super(context);&&&&&&}&&&&&&&&&&&&&&&&&&protected&abstract&void&onDraw(Canvas&canvas);&&&&&&&&&&&&&&&&&&public&abstract&boolean&onKeyDown(int&keyCode);&&&&&&&&&&&&&&&&&&public&abstract&boolean&onKeyUp(int&keyCode);&&&&&&&&&&&&&&protected&abstract&void&reCycle();&&&&&&&&&&&&&&&&protected&abstract&void&refurbish();&&}&&
有了视图类来显示界面,还要控制当前屏幕显示哪一个界面,甚至对界面进行一些逻辑上的处理,这时就可以构建一个整个游戏逻辑的MainGame类,在该类中需要根据不同的游戏状态来设置屏幕需要显示的视图,如代码清单11-2所示,这是MainGame类中控制界面显示的一段代码。
代码清单11-2&&controlView方法&GameView&m_GameView&=&null;&...&public&void&controlView(int&status)&{&&&&&&&&&&if(m_status&!=&status)&&&&&{&&&&&&&&&if(m_GameView&!=&null)&&&&&&&&&{&&&&&&&&&&&&&m_GameView.reCycle();&&&&&&&&&&&&&System.gc();&&&&&&&&&&&&&&&&&&&&&&&&&}&&&&&}&&&&&&&&&&freeGameView(m_GameView);&&&&&&&&&&&&&&&switch&(status)&&&&&{&&&&&case&yarin.GAME_SPLASH:&&&&&&&&&m_GameView&=&new&SplashScreen(m_Context,this);&&&&&&&&&break;&&&&&case&yarin.GAME_MENU:&&&&&&&&&m_GameView&=&new&MainMenu(m_Context,this);&&&&&&&&&break;&&&&&case&yarin.GAME_HELP:&&&&&&&&&m_GameView&=&new&HelpScreen(m_Context,this);&&&&&&&&&break;&&&&&case&yarin.GAME_ABOUT:&&&&&&&&&m_GameView&=&new&AboutScreen(m_Context,this);&&&&&&&&&break;&&&&&case&yarin.GAME_RUN:&&&&&&&&&m_GameView&=&new&GameScreen(m_Context,m_MagicTower,this,true);&&&&&&&&&break;&&&&&case&yarin.GAME_CONTINUE:&&&&&&&&&m_GameView=new&GameScreen(m_Context,m_MagicTower,this,false);&&&&&&&&&break;&&&&&}&&&&&&&&&&setStatus(status);&}&
同样,还需要得到当前要显示的视图对象,以便被其他类使用,因此这里我们实现一个getMainView方法来取得当前视图对象。代码如下:
&public&static&GameView&getMainView()&&{&&&&&return&m_GameV&}&
在创建和控制了视图显示之后,要让游戏能够动起来,需要开启一个线程来实时更新视图显示界面并刷新视图。下面我们将为游戏开启一个主线程,可以通过MainGame.getMainView()方法来取得当前显示的视图界面,然后根据不同的界面来进行游戏更新,本例中使用了postInvalidate()方法来刷新一个界面,如代码清单11-3所示。
代码清单11-3&&第11章\MagicTower\src\com\yarin\android\MagicTower\ThreadCanvas.java片段&public&class&ThreadCanvas&extends&View&implements&Runnable&{&&&&&private&String&m_Tag&=&&ThreadCanvas_Tag&;&&&&&public&ThreadCanvas(Context&context)&{&&&&&&&&&super(context);&&&&&}&&&&&&&&&&protected&void&onDraw(Canvas&canvas)&{&&&&&&&&&if&(MainGame.getMainView()&!=&null)&{&&&&&&&&&&&&&MainGame.getMainView().onDraw(canvas);&&&&&&&&&}else{&&&&&&&&&&&&&Log.i(m_Tag,&&null&);&&&&&&&&&}&&&&&}&&&&&&&&&&public&void&start(){&&&&&&&&&Thread&t&=&new&Thread(this);&&&&&&&&&t.start();&&&&&}&&&&&&&&&&public&void&refurbish(){&&&&&&&&&if&(MainGame.getMainView()&!=&null)&{&&&&&&&&&&&&&MainGame.getMainView().refurbish();&&&&&&&&&}&&&&&}&&&&&public&void&run(){&&&&&&&&&while&(true)&{&&&&&&&&&&&&&try{&&&&&&&&&&&&&&&&&Thread.sleep(yarin.GAME_LOOP);&&&&&&&&&&&&&}catch&(Exception&e)&{&&&&&&&&&&&&&&&&&e.printStackTrace();&&&&&&&&&&&&&}&&&&&&&&&&&&&refurbish();&&&&&&&&&&&&&&postInvalidate();&&&&&&&&&&}&&&&&}&&&&&&&&&&boolean&onKeyDown(int&keyCode)&{&&&&&&&&&if&(MainGame.getMainView()&!=&null)&{&&&&&&&&&&&&&MainGame.getMainView().onKeyDown(keyCode);&&&&&&&&&}else{&&&&&&&&&&&&&Log.i(m_Tag,&&null&);&&&&&&&&&}&&&&&&&&&return&true;&&&&&}&&&&&&&&&&boolean&onKeyUp(int&keyCode)&{&&&&&&&&&if&(MainGame.getMainView()&!=&null)&{&&&&&&&&&&&&&MainGame.getMainView().onKeyUp(keyCode);&&&&&&&&&}else{&&&&&&&&&&&&&Log.i(m_Tag,&&null&);&&&&&&&&&}&&&&&&&&&return&true;&&&&&}&}&
在完成了这些模块之后,就需要通知一个Activity来显示界面,这里我们是在ThreadCanvas中控制界面的显示,所以使用setContentView方法来显示一个ThreadCanvas类对象即可,当然按键事件的处理也就可以调用ThreadCanvas类来处理,ThreadCanvas类会和MainGame一起配合来找到我们所指定的界面,代码清单11-4为MagicTower类的处理。
代码清单11-4&&第11章\MagicTower\src\com\yarin\android\MagicTower\MagicTower.java&&public&class&MagicTower&extends&Activity&{&private&ThreadCanvas&mThreadCanvas&=&null;&public&void&onCreate(Bundle&savedInstanceState)&{&&&&&super.onCreate(savedInstanceState);&&&&&&&&&&setTheme(android.R.style.Theme_Black_NoTitleBar_Fullscreen);&&&&&requestWindowFeature(Window.FEATURE_NO_TITLE);&&&&&getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,&&&&&&WindowManager.LayoutParams.FLAG_FULLSCREEN);&&&&&new&MainGame(this);&&&&&mThreadCanvas&=&new&ThreadCanvas(this);&&&&&setContentView(mThreadCanvas);&}&protected&void&onPause(){&&&&&super.onPause();&}&protected&void&onResume(){&&&&&super.onResume();&&&&&mThreadCanvas.requestFocus();&&&&&mThreadCanvas.start();&}&public&boolean&onKeyDown(int&keyCode,&KeyEvent&event)&{&&&&&mThreadCanvas.onKeyDown(keyCode);&&&&&return&false;&}&public&boolean&onKeyUp(int&keyCode,&KeyEvent&event)&{&&&&&mThreadCanvas.onKeyUp(keyCode);&&&&&return&false;&&&&&}&}&
到这里我们基本完成了一个游戏的整体框架,后面的所有视图类界面显示都只需要继承自我们自定义的抽象类GameView,然后在MainGame中判断和更改当前的游戏状态,程序便自动找到我们需要更新和释放的视图类进行操作。下面我们将通过该游戏的菜单显示界面来熟悉该框架的使用,菜单界面如图11-3所示。
图11-3 《魔塔》菜单界面
要通过GameView 类来实现该菜单界面,首先需要创建一个MainMenu类来继承自GameView类,代码如下:
public&class&MainMenu&extends&GameView&{&&&&&&}&&
既然继承自GameView,就需要实现其中的抽象方法。绘制界面很简单,大家可以参考本书第5章的内容,完成界面的绘制,在绘制好界面和处理好按键事件之后,只需要在MainGame中判断当前状态即可。如果是菜单界面,那么将GameView对象构建为MainMenu类型,ThreadCanvas便可以通过MainGame.getMainView()取得当前的MainMenu界面来进行更新,当有事件触发时便自动找到MainMenu中事件相关的方法进行处理。这里需要说明的是,当我们选择了一个菜单项之后,应用程序的界面将跳转到另一个界面,比如选择了&关于&界面,将跳转到&关于&界面中,这时就需要通过&mMainGame.controlView(yarin.GAME_ABOUT);&来改变当前状态为&关于&状态,当然以后任何界面的设计都可以继承自GameView这样来进行。图11-4是游戏&关于&界面的效果,具体实现参见本书所附源代码:第11章\MagicTower。
图11-4《魔塔》&关于&页面
当然,一个好的游戏框架需要根据具体的游戏类型和内容来进行设计,以方便开发,提高开发效率,使得开发的游戏运行效果更好。现在我们可以点击菜单界面上的&新游戏&来进入游戏的主题开发了。大家可能都知道,J2ME中在MDIP2.0后面的版本多出了一个专门为开发游戏提供的Game包,本章我们将通过在Android中实现并使用这个包来完成《魔塔》。笔者在这里先实现了Game包中的几个重要的类,包括Layer、TiledLayer、Sprite、LayerManager。这样就可以使用这个包来轻松地完成以后的游戏开发了。没有学习过J2ME的读者不用着急,下面我们将介绍如何使用这个包开发游戏。具体实现参见本书所附源代码:第11章\MagicTower\src\javax\microedition\lcdui\game。
11.3 &地图设计
地图的设计在游戏中非常重要,我们不可能让美工做出一张完整的大地图,然后在程序中直接使用,这样做出来的游戏将会很大,一般的手机不一定能成功地装载这么大的图片。通常游戏中的地图是多个小块组成的一个完整的大地图,而组成这些小块的数据一般可以使用一个二维数组来存储,然后通过程序以最快的方式将这些地图数据对应的小块映射到屏幕上组成一幅完整的地图。当然,这些数据也不是我们从键盘上一个一个地输入进去的,一般情况下先由程序员做一个地图编辑器,在这个地图编辑器中用鼠标点击再保存,或者是从网络上下载一些成熟的编辑器,比如用mappy这样的工具生成地图,再用脚本语言为mappy写一个应该保存成什么格式的程序。通常地图分为45度角、侧视角和俯视角等。如图11-5所示,是编辑地图时的一个截图。
图11-5 &地图编辑
编辑好地图之后,可以得到一个保存了地图数据的二维数组,如下面代码所示:
private&byte[][]&map={&6,11,6,6,&6,1,6,6,&6,6,1,6,&6,6,1,6};&
其中,6、1、11这些代表了不同的图块,我们只需要将这些数据所对应的图块绘制到屏幕上即可。这里将使用前面所实现的TiledLayer类来显示地图。
1.首先创建一个TiledLayer对象
创建TiledLayer对象需要使用其构造函数:
public&TiledLayer(&int&columns,&int&rows,&Bitmap&image,&&&&&&&&&&&&&&&&int&tileWidth,&int&tileHeight)&&
参数columns和rows分别表示地图的列数和行数,即我们构建的地图数组的行数和列数;参数image表示地图所有图块的一张图片,即我们在编辑地图时所使用的图片;参数tileWidth和tileHeight分别表示每个图片的宽度和高度。
2.设置地图数据
创建了TiledLayer对象后,可以通过如下方法来设置该地图使用的地图数据:
public&void&setCell(int&col,&int&row,&int&tileIndex)&
参数col和row分别表示在屏幕上要显示的列数和行数;参数tileIndex则表示在地图上显示图块的值。比如,如下代码通过一个循环设置了所有要显示的行和列的图块索引值:
floorMap=new&TiledLayer(TILE_NUM_COL,TILE_NUM_ROW,bmap,TILE_WIDTH,&TILE_HEIGHT);&for&(int&i&=&0;&i&&&TILE_NUM;&i++)&{&&&&&int[]&colrow&=&getColRow(i);&&&&&floorMap.setCell(colrow[0],&colrow[1],&floorArray[curFloorNum][i]);&}&
3.绘制地图
通过上面的设置,绘制地图就非常简单了,只需要调用TiledLayer中的paint方法就可以将地图绘制到屏幕上。代码如下:
floorMap.paint(canvas);&
下面我们来看看TiledLayer中的paint方法是如何实现的,如代码清单11-5所示。
代码清单11-5&第11章\MagicTower\src\com\yarin\android\MagicTower\TiledLayer.java片段&public&final&void&paint(Canvas&canvas)&{&&&&&if&(canvas&==&null)&{&&&&&&&&&&throw&new&NullPointerException();&&&&&}&&&&&if&(visible)&{&&&&&&&&&int&tileIndex&=&0;&&&&&&&&int&ty&=&this.y;&&&&&&&for&(int&row&=&0;&row&&;&row++,&ty&+=&cellHeight)&{&&&&&&&&&&&&int&tx&=&this.x;&&&&&&&&&&&&int&totalCols&=&cellMatrix[row].&&&&&&&&&&&&for&(int&column&=&0;&column&&;&column++,&tx&+=&cellWidth)&{&&&&&&&&&&&&&&&&tileIndex&=&cellMatrix[row][column];&&&&&&&&&&&&&&&&if&(tileIndex&==&0)&{&//&transparent&tile&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&}&else&if&(tileIndex&&)&{&&&&&&&&&&&&&tileIndex&=&getAnimatedTile(tileIndex);&&&&&&&&&&&&&&&&}&&&&&&&&&&&&drawImage(canvas,&tx,&ty,&sourceImage,&&&&&&&&&&&&&&&&tileSetX[tileIndex],&tileSetY[tileIndex],&cellWidth,&cellHeight);&&&&&&&&&&&&&&&&&}&&&&&&&&&&&}&&&&&&&}&}&//绘制一个图片&private&void&drawImage(Canvas&canvas,&int&x,&int&y,&&&&&&&&&&&&&Bitmap&bsrc,&int&sx,&int&sy,&int&w,&int&h)&{&&&&&Rect&rect_src&=&new&Rect();&&&&&rect_src.left&=&sx;&&&&&rect_src.right&=&sx&+&w;&&&&&rect_src.top&=&sy;&&&&&rect_src.bottom&=&sy&+&h;&&&&&Rect&rect_dst&=&new&Rect();&&&&&rect_dst.left&=&x;&&&&&rect_dst.right&=&x&+&w;&&&&&rect_dst.top&=&y;&&&&&rect_dst.bottom&=&y&+&h;&&&&&//按指定的裁剪矩形来绘制图片&&&&&//分别是屏幕上的矩形和图片上要绘制的矩形裁剪&&&&&canvas.drawBitmap(bsrc,&rect_src,&rect_dst,&null);&&&&&rect_src&=&null;&&&&&rect_dst&=&null;&}&
代码清单11-5就是用一个循环来将我们设置的地图数组和图片进行对应,绘制到对应的屏幕上,在Android中按指定矩形裁剪图片绘制的方法如下:
void&android.graphics.Canvas.drawBitmap(Bitmap&bitmap,&Rect&src,&Rect&dst,&Paint&paint)&
参数bitmap是要绘制的图片,参数src和dst分别是图片上的裁剪区域和屏幕上对应的裁剪区域,参数paint设置了画笔的属性。更多方法可以参见我们实现的Game包中的TiledLayer类。
11.4 &主角设计
游戏中的主角在这里称为&精灵&,当然精灵包括的范围很广,不仅仅是主角,还有NPC、道具等。既然是精灵,必然有很多动画,比如,主角在向4个方向移动时分别对应4个不同的动画,动画本身就是将图片一帧一帧地连接起来,循环地播放每一帧形成的。在一些大的游戏中同样可以使用自己写的精灵编辑器去编辑精灵,将精灵拆成很多部分,然后再组合起来,这样可以节省大量的空间。这里可以使用我们实现的Sprite类来完成《魔塔》中的主角,Sprite是个用来显示图像的类,该类和TiledLayer的区别是:Sprite是由一个图像(可以有好几帧,但是一次只有一个显示)组成的(当然Sprite还有其他的特性,每次只能使用一个图像而不是多个图像来填充屏幕是它的最主要特征),因而Sprite 被用来定义一些小的有动作的游戏对象(比如飞船和小行星对撞),TiledLayer 更常被用来构造生动的背景。下面我们看看如何使用这个 Sprite类。
1.构建Sprite对象
在该Sprite类中提供了3个构造方法,分别是:
public&Sprite(Bitmap&image)&&public&Sprite(Bitmap&image,&int&frameWidth,&int&frameHeight)&&public&Sprite(Sprite&s)&
参数image为精灵的图片,参数frameWidth和frameHeight分别设置了精灵图片的每一帧的宽度和高度,参数s表示通过一个精灵来创建另一个精灵。构造Sprite 类的时候需要指定精灵的高度和宽度(像素值)。 图像的高度和宽度必须分别是精灵的高度和宽度的整数倍。换句话说,要能正好让电脑把图像按照精灵的大小划分成几个类,通过上面的例子也看到了,这些帧是横着排还是竖着排,抑或横竖都有,排成一个方阵,都无所谓。接着就可以指定帧数了, 左上方是编号0,然后从左到右、从上到下依次排列。可以使用setFrame(int sequenceIndex) 选择哪一帧被显示,只要把它的编号作为参数传递即可。
2.Sprite属性
我们使用的TiledLayer类可以自动根据精灵的位置来判断地图绘制的位置,因此可以使用如下方法来设置精灵的位置:
public&void&setRefPixelPosition(int&x,&int&y)&
参数x和y是精灵的位置。更多的方法可以参考我们实现的Game包中的Sprite类。
注意 &Sprite的编号是从0开始的, 但是 TiledLayer却是从1开始的(笔者开始因为没注意得到了一个IndexOutOfBoundsException 异常)。在TiledLayer中,序号0表示一个空白的元素 (比如在某个位置你什么都不想画,那就把它设置成0)。Sprite只由一个单元组成, 所以如果你想让它不显示这个单元,简单地设置成 setVisible(false)就可以了,因而Sprite不需要一个特殊的编号来表示空白的单元。
3.碰撞检测
精灵类提供了以下3个碰撞检测函数:
public&final&boolean&collidesWith(TiledLayer&t,&boolean&pixelLevel)&&public&final&boolean&collidesWith(Sprite&s,&boolean&pixelLevel)&&public&final&boolean&collidesWith(Bitmap&image,&int&x,&int&y,boolean&pixelLevel)&
上面的3个函数分别表示精灵和TiledLayer的碰撞、精灵和精灵的碰撞、精灵和图片的碰撞。参数pixelLevel表示使用像素检测还是矩形检测。矩形检测只需要将精灵对应成相应的矩形范围来进行检查,这种检测速度很快,但不是很准确,对于碰撞要求不高的游戏可以使用它。同时还可以将一个Sprite***成很多矩形来使用矩形检测以提高准确性。而像素检测则比较准确,但是速度必然会减慢。
4.精灵旋转和镜像
一般在做游戏时,都需要使用旋转和镜像。比如,在做一个飞机游戏时,飞机有几个方向的图片,这样会增加游戏开发出来的包的大小,所以可以制作一个方向的图片,然后通过精灵的旋转方法来将图片在各个方向进行旋转。这里我们只实现了90&的倍数旋转,分别是我们在Sprite类中定义的常量:TRANS_NONE、TRANS_ROT90、TRANS_ROT180、TRANS_ROT270、TRANS_MIRROR、TRANS_MIRROR_ROT90、TRANS_MIRROR_ROT180、TRANS_MIRROR_ROT270。我们可以通过setTransform方法来传输上面这些常量,设置Sprite的旋转和镜像。当然,可以查看该方法的具体实现来更深入地理解Sprite的旋转和镜像。
5.主角对话
《魔塔》中的主角可能和NPC对话来获得一些信息,进行游戏。《魔塔》中勇士和仙子的对话如图11-6所示。
图11-6 &勇士和仙子的对话
从图11-6中可以看出,它是通过一个浮动的对话框来显示对话内容,这个对话框只是一个矩形框,然后在右边绘制出对应的NPC的头像即可。这里的对话内容可以通过前面介绍的TextUtil类来实现自动换行。如代码清单11-6所示,即可实现一个游戏中的对话框的效果。
代码清单11-6&&dialog方法&public&void&dialog()&{&&&&&int&x,&y,&w,&h;&&&&&w&=&yarin.SCREENW;&&&&&h&=&yarin.MessageBoxH;&&&&&x&=&0;&&&&&y&=&(yarin.SCREENH&-&yarin.MessageBoxH)&/&2;&&&&&if&(task.curTask2&%&2&==&0)&&&&&{&&&&&&&&&drawDialogBox(IMAGE_DIALOG_HERO,&x,&y,&w,&h);&&&&&}&&&&&else&&&&&{&&&&&&&&&drawDialogBox(curDialogImg,&x,&y,&w,&h);&&&&&}&&&&&tu.DrawText(mcanvas);&}&private&void&drawDialogBox(int&imgType,&int&x,&int&y,&int&w,&int&h)&{&&&&&Paint&ptmPaint&=&new&Paint();&&&&&ptmPaint.setARGB(255,Color.red(BACK_COLOR),&Color.green(BACK_COLOR),&Color.&&&&&&blue(BACK_COLOR));&&&&&yarin.fillRect(mcanvas,&x,&y,&w,&h,&ptmPaint);&&&&&Bitmap&img&=&getImage(imgType);&&&&&yarin.drawRect(mcanvas,&x,&y,&w,&h,&ptmPaint);&&&&&if&(img&!=&null)&&&&&{&&&&&&&&&if&(imgType&==&IMAGE_DIALOG_HERO)&&&&&&&&&{&&&&&&&&&&&&&yarin.drawImage(mcanvas,&img,&x,&y&-&64);&&&&&&&&&}&&&&&&&&&else&&&&&&&&&{&&&&&&&&&&&&&yarin.drawImage(mcanvas,&img,&yarin.SCREENW&-&40,&y&-&64);&&&&&&&&&}&&&&&}&&&&&ptmPaint&=&null;&}&
6.战斗界面
当主角和怪物发生碰撞时,就会发生战斗,这时需要一个界面来显示战斗效果,首先看看《魔塔》中的战斗界面的效果,如图11-7所示。
图11-7 &战斗界面
在这里,战斗界面很简单,就是分别显示玩家和怪物的头像以及属性,包括生命、攻击、防御。下面我们来看看战斗界面的绘制,如代码清单11-7所示。
代码清单11-7&&第11章\MagicTower\src\com\yarin\android\MagicTower\&FightScreen.java片段&protected&void&onDraw(Canvas&canvas)&{&&&&&mcanvas&=&&&&&&int&tx,&ty,&tw,&&&&&&tw&=&yarin.SCREENW;&&&&&th&=&yarin.MessageBoxH;&&&&&tx&=&0;&&&&&ty&=&(yarin.SCREENH&-&yarin.MessageBoxH)&/&2;&&&&&showMessage();&&&&&if&(!isFighting)&&&&&{&&&&&&&&&tu.DrawText(mcanvas);&&&&&}&&&&&else&&&&&{&&&&&&&&&yarin.drawImage(canvas,&orgeImage,&0,&ty&+&(th&-&GameMap.TILE_&&&&&&WIDTH)/2,GameMap.TILE_WIDTH,GameMap.TILE_WIDTH,orgeSrcX,orgeSrcY);&&&&&&&&&yarin.drawImage(canvas,&heroImage,&(tw&-&GameMap.TILE_WIDTH),&ty&+&&&&&&&&&&(th&-&GameMap.TILE_WIDTH)&/&2,&GameMap.TILE_WIDTH,&GameMap.TILE_&&&&&&&&&WIDTH,&0,&0);&&&&&&&&&paint.setColor(Color.WHITE);&&&&&&&&&&&&&&&&&&{&&&&&&&&&&&&&tx&=&40;&&&&&&&&&&&&&ty&=&(yarin.SCREENH&-&yarin.MessageBoxH)&/&2&+&5;&&&&&&&&&&&&&yarin.drawString(canvas,&&生命:&&+&orgeHp,&tx,&ty,&paint);&&&&&&&&&&&&&yarin.drawString(canvas,&&攻击:&&+&orgeAttack,&tx,&ty&+&&&&&&&&&&&&&&yarin.TextSize,&paint);&&&&&&&&&&&&&yarin.drawString(canvas,&&防御:&&+&orgeDefend,&tx,&ty&+&2&&&&&&&&&&&&&&*&yarin.TextSize,&paint);&&&&&&&&&}&&&&&&&&&&&&&&&&&&{&&&&&&&&&String&string&=&&&;&&&&&&&&&&&&&ty&=&(yarin.SCREENH&-&yarin.MessageBoxH)&/&2&+&5;&&&&&&&&&&&&&string&=&hero.getHp()&+&&:生命&;&&&&&&&&&&&&&yarin.drawString(canvas,string,(tw-40-paint.measureText&&&&&&&&&&&&&(string)),&ty,&paint);&&&&&&&&&&&&&string&=&hero.getAttack()&+&&:攻击&;&&&&&&&&&&&&&yarin.drawString(canvas,&string,&(tw&-&40&-&paint.&&&&&&&&&&&&&measureText(string)),&ty&+&yarin.TextSize,&paint);&&&&&&&&&&&&&string&=&hero.getDefend()&+&&:防御&;&&&&&&&&&&&&&yarin.drawString(canvas,string,(tw-40-paint.measureText&&&&&&&&&&&&&(string)),&ty&+&2&*&yarin.TextSize,&paint);&&&&&&&&&}&&&&&}&&&&&tick();&}&&public&void&showMessage()&{&&&&&int&x&=&0;&&&&&int&y&=&(yarin.SCREENH&-&yarin.MessageBoxH)&/&2;&&&&&int&w&=&yarin.SCREENW;&&&&&int&h&=&yarin.MessageBoxH;&&&&&Paint&ptmPaint&=&new&Paint();&&&&&ptmPaint.setARGB(255,&0,&0,&0);&&&&&yarin.fillRect(mcanvas,&x,&y,&w,&h,&ptmPaint);&&&&&ptmPaint&=&null;&}&
7.Sprite绘制
上面设置了精灵的一些属性,要将Sprite显示在屏幕上同样很简单,下面我们看看Sprite类中是如何将图片绘制到屏幕上的,具体实现如代码清单11-8所示。
代码清单11-8&&Sprite.java片段&public&final&void&paint(Canvas&canvas)&&&&&&{&&&&&if&(canvas&==&null)&&&&&&{&&&&&&&&&throw&new&NullPointerException();&&&&&}&&&&&&&&&&if&(visible)&&&&&&{&&&&&&&&&drawImage(canvas,this.x,this.y,sourceImage,&&&&&&&&&&&&&&&&frameCoordsX[frameSequence[sequenceIndex]],&&&&&&&&&&&&&&&&&frameCoordsY[frameSequence[sequenceIndex]],&&&&&&&&&&&&&&&&&&&&srcFrameWidth,&&&&&&&&&&&&&&&&&&&&srcFrameHeight);&&&&&&&&&}&&&&&}&
代码清单11-8中通过visible来检测是否需要显示当前精灵,如果需要显示我们才绘制,绘制时只需要指定要绘制精灵的位置、精灵的图片、当前帧在图片上的偏移量、帧的宽度和高度。
本节介绍了Sprite的使用,以及如何在Android平台实现Sprite类。该类中还有很多方法来帮助我们开发,大家可以参考本书所附源代码:第11章\MagicTower\src\javax\microedition\lcdui\game\Sprite.java。
11.5 &图层管理器
前面学习了使用TiledLayer来绘制地图,使用Sprite来绘制主角,可以分别使用它们的paint方法来显示地图和精灵,但是我们是否发现TiledLayer和Sprite类都继承自一个抽象类Layer(图层)呢?也就是说,不管是地图还是精灵,都包含在图层这个类中,所以为了方便管理和维护这些图层,我们构建一个专门用来管理图层的图层管理器LayerManager。
抽象类Layer的实现很简单,只是包括了图层的位置(x,y)、图层的宽度和高度(width,height)以及一个控制是否显示图层的布尔变量visible。下面我们来看看Layer类的实现,如代码清单11-9所示。
代码清单11-9&&第11章\MagicTower\src\com\yarin\android\MagicTower\Layer.java&public&abstract&class&Layer&{&&&&&&&&&&int&x;&&&&&&int&y;&&&&&&int&&&&&&&int&&&&&&&&&&&&boolean&visible&=&true;&&&&&&&&&&Layer(int&width,&int&height)&{&&&&&&&&&setWidthImpl(width);&&&&&&&&&setHeightImpl(height);&&&&&}&&&&&&&&&&&&&public&void&setPosition(int&x,&int&y)&{&&&&&&&&&this.x&=&x;&&&&&&&&&this.y&=&y;&&&&&}&&&&&&&&&&public&void&move(int&dx,&int&dy)&{&&&&&&&&&&&x&+=&&&&&&&&&&y&+=&&&&&&}&&&&&&&&&&public&final&int&getX()&{&&&&&&&&&return&x;&&&&&}&&&&&&&&&&public&final&int&getY()&{&&&&&&&&&return&y;&&&&&}&&&&&&&&&&public&final&int&getWidth()&{&&&&&return&&&&&&}&&&&&&&&&&public&final&int&getHeight()&{&&&&&return&&&&&&}&&&&&&&&&&public&void&setVisible(boolean&visible)&{&&&&&&&&this.visible&=&&&&&&}&&&&&&&&&&public&final&boolean&isVisible()&{&&&&&&&&&return&&&&&&}&&&&&&&&&&public&abstract&void&paint(Canvas&canvas);&&&&&&&&&&void&setWidthImpl(int&width)&{&&&&&&&&&&if&(width&&&0)&{&&&&&&&&&&&&&throw&new&IllegalArgumentException();&&&&&&&&&}&&&&&&&&this.width&=&&&&&&}&&&&&&&&&&void&setHeightImpl(int&height)&{&&&&&&&&&if&(height&&&0)&{&&&&&&&&&&&&&throw&new&IllegalArgumentException();&&&&&&&&&}&&&&&&&&&this.height&=&&&&&&}&}&
代码清单11-9中的方法都很简单,就是设置(获得)图层的一些属性,下面我们来创建一个图层管理器LayerManager。首先确定图层管理器需要包括的成员变量:一个视窗的x和y,宽度和高度,一个Layer的数组(用来保存所有的图层),一个变量(用来保存实际图层的数量)。下面我们来看看这个图层管理器的具体实现,如代码清单11-10所示。
代码清单11-10&&第11章\MagicTower\src\com\yarin\android\MagicTower\LayerManager.java&public&class&LayerManager&{&&&&&public&LayerManager()&{&&&&&&&&&setViewWindow(0,&0,&Integer.MAX_VALUE,&Integer.MAX_VALUE);&&&&&}&&&&&&&&&&public&void&append(Layer&l)&{&&&&&&&&&removeImpl(l);&&&&&&&&&&addImpl(l,&nlayers);&&&&&}&&&&&&&&&&public&void&insert(Layer&l,&int&index)&{&&&&&&&&&if&((index&&&0)&||&(index&&&nlayers))&{&&&&&&&&&&&&&throw&new&IndexOutOfBoundsException();&&&&&&&&&}&&&&&&&&&removeImpl(l);&&&&&&&&&addImpl(l,&index)&&&&&}&&&&&&&&&&public&Layer&getLayerAt(int&index)&{&&&&&&&&&if&((index&&&0)&||&(index&&=&nlayers))&{&&&&&&&&&&&&&throw&new&IndexOutOfBoundsException();&&&&&&&&&}&&&&&&&&&return&component[index];&&&&&}&&&&&&&&&&public&int&getSize()&{&&&&&&&&&return&&&&&&}&&&&&&&&&&public&void&remove(Layer&l)&{&&&&&&&&&removeImpl(l);&&&&&}&&&&&&&&&&public&void&paint(Canvas&canvas,&int&x,&int&y)&{&&&&&&&&&canvas.translate(x&-&viewX,&y&-&viewY);&&&&&&&&&&&&&&&&&&canvas.clipRect(viewX,&viewY,&viewX+viewWidth,&viewY+viewHeight);&&&&&&&&&for&(int&i&=&&--i&&=&0;)&{&&&&&&&&&&&&&Layer&comp&=&component[i];&&&&&&&&&&&&&if&(comp.visible)&{&&&&&&&&&&&&&&&&&comp.paint(canvas);&&&&&&&&&&&&&}&&&&&&&&&}&&&&&&&&&canvas.restore();&&&&&&&&&canvas.translate(-x&+&viewX,&-y&+&viewY);&&&&&}&&&&&&&&&&public&void&setViewWindow(int&x,&int&y,&int&width,&int&height)&{&&&&&&&&&if&(width&&&0&||&height&&&0)&{&&&&&&&&&&&&&throw&new&IllegalArgumentException();&&&&&&&&&}&&&&&&&&&viewX&=&x;&&&&&&&&&viewY&=&y;&&&&&&&&&viewWidth&=&&&&&&&&&&viewHeight&=&&&&&&}&&&&&&&&&&private&void&addImpl(Layer&layer,&int&index)&{&&&&&&&&&if&(nlayers&==&component.length)&{&&&&&&&&&&&&&Layer&newcomponents[]&=&new&Layer[nlayers&+&4];&&&&&&&&&&&&&System.arraycopy(component,&0,&newcomponents,&0,&nlayers);&&&&&&&&&&&&&System.arraycopy(component,&index,&newcomponents,&index&+&1,&&&&&&&&&&&&&nlayers&-&index);&&&&&&&&&&&&&component&=&&&&&&&&&&}&else&{&&&&&&&&&&&&&System.arraycopy(component,index,component,&index&+&1,&nlayers&&&&&&&&&&&&&-&index);&&&&&&&&&}&&&&&&&&&component[index]&=&&&&&&&&&&nlayers++;&&&&&}&&&&&&&&private&void&removeImpl(Layer&l)&{&&&&&&&&&if&(l&==&null)&{&&&&&&&&&&&&&throw&new&NullPointerException();&&&&&&&&&}&&&&&&&&&for&(int&i&=&&--i&&=&0;)&{&&&&&&&&&&&&&if&(component[i]&==&l)&{&&&&&&&&&&&&&&&&&remove(i);&&&&&&&&&&&&&}&&&&&&&&&}&&&&&}&&&&&&&&&&private&void&remove(int&index)&{&&&&&&&&&System.arraycopy(component,&index&+&1,&component,&index,&nlayers&&&&&&&&&-&index&-&1);&&&&&&&&&component[--nlayers]&=&null;&&&&&}&&&&&&&&&&private&int&&&&&&&&&&&&private&Layer&component[]&=&new&Layer[4];&&&&&&&&&&private&int&viewX,&viewY,&viewWidth,&viewH&&}&
从代码清单11-10中可以看出,只需要将所有图层(包括地图、主角)一起添加到图层管理器中,然后设置视图查看时的位置及大小,调用图层管理器的paint方法就可以绘制出图层。绘制的顺序是按添加的反顺序,即先添加的后绘制,大家一定要注意这一点,以免图层被覆盖之后显示不出来。因此,我们只需要在《魔塔》的GameScreen类中创建一个图层管理器(LayerManager),将地图和主角都添加进去,代码如下:
LayerManager&layerManager&=&new&LayerManager();&layerManager.append(hero);&layerManager.append(gameMap.getFloorMap());&
然后在要绘制的地方设置视窗的位置和大小,再调用paint方法就可以绘制出所有的图层,代码如下:
layerManager.setViewWindow(scrollX,&scrollY,&winWidth,&winHeight);&layerManager.paint(canvas,&borderX,&borderY);&
到这里我们基本完成了前面所说的Game包的内容,简单地说就是一个图层和一个图层管理器。如果我们使用这个Game来开发游戏,就可以节省很多的时间,提高开发效率。同样,我们的实现方式是按J2ME中MIDP2.0的Game包来进行的,所以大家在移植J2ME的程序时可以很轻松地解决两个平台不同的绘图方式。大家还可以实现一个MIDP2.0中的GameCanvas来使移植J2ME的游戏更加方便。将两个甚至多个平台的优点融合到一起,可以让我们开发出更优秀的、更受欢迎的游戏。
11.6 &游戏音效
音效在游戏开发中占据重要地位,这一点可以通过经典的RPG游戏《仙剑奇侠传》来说明,该游戏之所以能够成功就在于故事情节的逼真,让玩家很容易将自己融入其中,随着游戏的节奏喜怒哀乐。游戏开发的最高境界就是能带动玩家的情绪,我们设想一下,要是《仙剑奇侠传》没有音效,会是一个什么样的情况呢?可能总是感觉缺少什么一样,玩家不会如此轻易地进入游戏的情节。
开发游戏时,人们常常忽视游戏的音效。开发者往往把主要精力花费在游戏的图像和动画等方面,而忽视了背景音乐和声音效果。当他们意识到这一点时,通常为时已晚。这种做法显然是不正确的,因为好的游戏音效和音乐可以使玩家融入游戏世界,产生共鸣。音效的作用还不仅限于此。如果没有高超的游戏音效的映衬,再好的图像技巧也无法使游戏的表现摆脱平庸,对玩家也没有足够的吸引力。首先我们将游戏中的音效分为如下几类:背景音乐、剧情音乐、音效(动作的音效、使用道具音效、辅助音效)等。背景音乐一般需要一直播放,而剧情音乐则只需要在剧情需要的时候播放,音效则是很短小的一段,比如挥刀的声音、怪物叫声等。下面我们为《魔塔》加入两个背景音乐,一个是菜单背景音乐,一个是游戏中的背景音乐。
(1)首先准备两个符合游戏剧情的背景音乐放到res\raw文件夹下面。
(2)创建一个CMIDIPlayer类,控制音乐播放。
我们知道在Android中是通过MediaPlayer来播放音乐的,所以在CMIDIPlayer类中需要构建一个MediaPlayer对象,通过MediaPlayer.create来装载音乐文件,具体实现如代码清单11-11所示。
代码清单11-11&&第11章\MagicTower\src\com\yarin\android\MagicTower\CMIDIPlayer.java&public&class&CMIDIPlayer&{&public&MediaPlayer&&playerM&public&MagicTower&&&magicTower&&=&null;&public&CMIDIPlayer(MagicTower&magicTower)&{&&&&&this.magicTower&=&magicT&}&&public&void&PlayMusic(int&ID)&{&&&&&FreeMusic();&&&&&switch&(ID)&&&&&{&&&&&case&1:&&&&&&&&&&&&&&&&&&playerMusic&=&MediaPlayer.create(magicTower,&R.raw.menu);&&&&&&&&&&&&&&&&&&playerMusic.setLooping(true);&&&&&&&&&try&&&&&&&&&{&&&&&&&&&&&&&&&&&&playerMusic.prepare();&&&&&&&&&}&&&&&&&&&catch&(IllegalStateException&e)&&&&&&&&&{&&&&&&&&&e.printStackTrace();&&&&&&&&&}&&&&&&&&&catch&(IOException&e)&&&&&&&&&{&&&&&&&&&e.printStackTrace();&&&&&&&&&}&&&&&&&&&&&&&&&&&&playerMusic.start();&&&&&&&&&break;&&&&&case&2:&&&&&&&&&playerMusic&=&MediaPlayer.create(magicTower,&R.raw.run);&&&&&&&&&playerMusic.setLooping(true);&&&&&&&&&try&&&&&&&&&{&&&&&&&&&playerMusic.prepare();&&&&&&&&&}&&&&&&&&&catch&(IllegalStateException&e)&&&&&&&&&{&&&&&&&&&e.printStackTrace();&&&&&&&&&}&&&&&&&&&catch&(IOException&e)&&&&&&&&&{&&&&&&&&&e.printStackTrace();&&&&&&&&&}&&&&&&&&&playerMusic.start();&&&&&&&&&break;&&&&&&&&&}&}&&public&void&FreeMusic()&{&&&&&if&(playerMusic&!=&null)&&&&&{&&&&&&&&&playerMusic.stop();&&&&&&&&&playerMusic.release();&&&&&}&}&&public&void&StopMusic()&{&&&&&if&(playerMusic&!=&null)&&&&&{&&&&&&&&&playerMusic.stop();&&&&&}&}&}&
从代码清单11-11中可以看出,通过PlayMusic加上ID来确定播放什么音乐,通过StopMusic来停止正在播放的音乐,当程序退出时调用FreeMusic方法来释放播放音乐产生的资源。
(3)创建&是否开启音效&界面。
游戏中我们不可能强制玩家接受要播放的音乐,所以需要设计一个界面来供玩家选择是否开启音乐,这个界面这里设置得很简单,就是绘制了一段字符串,如图11-8所示。
图11-8 音效选择界面
点击了&是&(OK键)就是需要播放音乐,而点击&否&则直接进入游戏,因此点击了&是&需要开启音乐,然后进入菜单界面,播放音乐代码如下:
mCMIDIPlayer.PlayMusic(1);&
mCMIDIPlayer是我们构建的一个CMIDIPlayer类的对象。在进入游戏时,又需要播放另一首背景音乐,和上面的代码一样,只需要通过参数的ID来设置要播放的音乐。
(4)释放资源。
当退出游戏时,需要释放资源,这时调用CMIDPlayer类的FreeMusic方法来释放资源,代码如下:
mCMIDIPlayer.FreeMusic();&
当然,在一般的游戏中,控制音效的界面也不只是这一个,可以在主菜单界面里设置音效,同样还可以在游戏中通过一个弹出菜单等,来控制音效,但是实现方式都相同,大家可以自己将其完善,尽可能地方便用户随时控制音乐开关。
11.7 &游戏存档
手机游戏最大的优点就是不受时间和地点等因素的限制,玩家随时随地掏出手机即可玩游戏,但会由于一些突发的事件让玩家不得不终止游戏,但是玩家又正玩得高兴,不想在下次进入游戏时又重新开始,这就体现了游戏存档的重要性。游戏存档就是将玩家当前游戏的进度等信息存储下来,在玩家再次进入游戏时可以通过读取上次的存档来接着上次的进度继续游戏。我们知道,在应用程序中可以通过一个变量或者其他容器来存储一个值供我们使用,但是退出了游戏这个值也就随之消失了。所以我们需要一个能够持久地存储数据的功能。实际上,在本书第6章已经介绍了如何让数据能够持久地存储,我们在开发游戏存档时可以选择一种适合游戏的存储方式来存储当前的游戏进度。下面我们将为《魔塔》添加一个游戏存档的功能。
1.明确需要存储的数据
既然要存储数据,首先就需要确定游戏中哪些数据是需要存储的,下面我们来分析《魔塔》中需要存储的数据。首先,为了再次游戏能够顺利地装载上次的进度,需要保存主角的一些属性(包括位置、生命、攻击、防御等),还需要保存当前地图的一些属性(比如行、列、当前层数),同样还需要保存对话的相关内容,最后需要保存游戏的整个地图数据(每一层)。不要忘了,还有当前的音乐状态也需要存储。当然,每个游戏需要存储的数据肯定是不一样的,我们必须在保证存储完整性的前提下尽可能地存储少量的数据。
2.保存数据
可以根据数据的多少和操作的难度来选择一个合适的存储方式,这里我们选择了存储到一个文件中,在下一次进入游戏之后通过读取这个文件的内容来装载上次的进度。存储过程是:获取要存储的数据&将数据打包到Properties中&将Properties写入到文件中。代码清单11-12存储了《魔塔》游戏中的数据。
代码清单11-12&&save方法&boolean&save()&{&&&&&&&&&&int&col&=&hero.getRefPixelX()&/&GameMap.TILE_WIDTH;&&&&&int&row&=&hero.getRefPixelY()&/&GameMap.TILE_HEIGHT;&&&&&byte[]&r1&=&hero.encode();&&&&&byte[]&r2={(byte)gameMap.curFloorNum,(byte)&gameMap.reachedHighest,&(byte)&&&&&&row,&(byte)&col,&(byte)&hero.getFrame()&};&&&&&byte[]&r3&=&task.getTask();&&&&&&&&&&Properties&properties&=&new&Properties();&&&&&&&&&&properties.put(&music&,&String.valueOf(mMainGame.mbMusic));&&&&&properties.put(&r1l&,&String.valueOf(r1.length));&&&&&properties.put(&r2l&,&String.valueOf(r2.length));&&&&&properties.put(&r3l&,&String.valueOf(r3.length));&&&&&for&(int&i&=&0;&i&&&r1.&i++)&&&&&{&&&&&&&&&properties.put(&r1_&&+&i,&String.valueOf(r1[i]));&&&&&}&&&&&for&(int&i&=&0;&i&&&r2.&i++)&&&&&{&&&&&&&&&properties.put(&r2_&&+&i,&String.valueOf(r2[i]));&&&&&}&&&&&for&(int&i&=&0;&i&&&r3.&i++)&&&&&{&&&&&&&&&properties.put(&r3_&&+&i,&String.valueOf(r3[i]));&&&&&}&&&&&&&&&&for&(int&i&=&0;&i&&&GameMap.FLOOR_NUM;&i++)&&&&&{&&&&&&&&&byte&map[]&=&gameMap.getFloorArray(i);&&&&&&&&&for&(int&j&=&0;&j&&&map.&j++)&&&&&&&&&{&&&&&&&&&&&&&properties.put(&map_&+i+&_&+j,&String.valueOf(map[j]));&&&&&&&&&}&&&&&}&&&&&&&&&&try&&&&&{&&&&&&&&&&&&&&&&&&FileOutputStream&stream=magicTower.openFileOutput(&save&,&Context.&&&&&&&&&MODE_WORLD_WRITEABLE);&&&&&&&&&&&&&&&&&&properties.store(stream,&&&);&&&&&}&&&&&catch&(FileNotFoundException&e)&&&&&{&&&&&&&&&return&false;&&&&&}&&&&&catch&(IOException&e)&&&&&{&&&&&&&&&return&false;&&&&&}&&&&&return&true;&}&
3.装载数据
通过上面的方式,我们将所有的数据存储到了一个名为&save&的文件中,那么再次进入游戏时,就需要读取这个文件,获取上次的游戏进度。读取文件的流程如下:打开文件&将文件流装载进properties中&通过properties.get方法得到指定标签的数据&将得到的数据赋值给应用程序中对应的变量。代码清单11-13是我们读取《魔塔》的存档文件save的方法load。
代码清单11-13&&load方法&boolean&load()&{&&&&&&&&&&Properties&properties&=&new&Properties();&&&&&try&&&&&{&&&&&&&&&&&&&&&&&&FileInputStream&stream&=&magicTower.openFileInput(&save&);&&&&&&&&&&&&&&&&&&properties.load(stream);&&&&&}&&&&&catch&(FileNotFoundException&e)&&&&&{&&&&&&&&&return&false;&&&&&}&&&&&catch&(IOException&e)&&&&&{&&&&&&&&&return&false;&&&&&}&&&&&&&&&&mMainGame.mbMusic&=&Byte.valueOf(properties.get(&music&).toString());&&&&&byte[]&r1&=&new&byte[Byte.valueOf(properties.get(&r1l&).toString())];&&&&&byte[]&r2&=&new&byte[Byte.valueOf(properties.get(&r2l&).toString())];&&&&&byte[]&r3&=&new&byte[Byte.valueOf(properties.get(&r3l&).toString())];&&&&&for&(int&i&=&0;&i&&&r1.&i++)&&&&&{&&&&&&&&&r1[i]&=&Byte.valueOf(properties.get(&r1_&&+&i).toString());&&&&&}&&&&&for&(int&i&=&0;&i&&&r2.&i++)&&&&&{&&&&&&&&&r2[i]&=&Byte.valueOf(properties.get(&r2_&&+&i).toString());&&&&&}&&&&&for&(int&i&=&0;&i&&&r3.&i++)&&&&&{&&&&&&&&&r3[i]&=&Byte.valueOf(properties.get(&r3_&&+&i).toString());&&&&&}&&&&&hero.decode(r1);&&&&&gameMap.curFloorNum&=&r2[0];&&&&&gameMap.reachedHighest&=&r2[1];&&&&&hero.setFrame(r2[4]);&&&&&task.setTask(r3);&&&&&for&(int&i&=&0;&i&&&GameMap.FLOOR_NUM;&i++)&&&&&{&&&&&&&&&byte[]&map&=&new&byte[GameMap.TILE_NUM];&&&&&&&&&for&(int&j&=&0;&j&&&map.&j++)&&&&&&&&&{&&&&&&&&&&&map[j]=Byte.valueOf(properties.get(&map_&+i+&_&+j).toString());&&&&&&&&&}&&&&&&&&&gameMap.setFloorArray(i,&map);&&&&&}&&&&&gameMap.setMap(gameMap.curFloorNum);&&&&&hero.setRefPixelPosition(r2[3]*GameMap.TILE_WIDTH&+&GameMap.TILE_WIDTH&/&2,&&&&&&r2[2]&*&GameMap.TILE_HEIGHT&+&GameMap.TILE_HEIGHT&/&2);&&&&&return&true;&}&
上面两个方法分别实现了保存和装载,现在就要在需要保存游戏的地方调用save()方法,然后在需要读取的地方调用load()方法。我们可以看出,一个游戏的存档并不是很难,关键在于找到需要保存的数据,选择存储的方式。为了方便玩家,我们尽可能地为每个游戏实现存档功能,并且在退出游戏时,不管玩家是否保存都将自动保存下来,以避免游戏进度的丢失,为玩家提供更好、更多、更方便的服务。
11.8 &小结&
本章我们通过实现一个较为典型的游戏学习了Android平台游戏开发的相关知识。相信通过本章的学习,大家都明白了我们在本章开始时所说的&其实游戏开发并不难&。由于在前面的章节中我们介绍了有关图形绘制和操作的一些知识,本章把重点放在游戏框架、如何实现以及游戏开发流程上。关于该游戏的具体实现大家可以参考本书所附源代码,这个游戏能够运行,但是并不是一个完整的游戏,因为我们将游戏中的道具商城系统、跳跃、查看怪物属性等功能留给大家学习之后练习。本章所讲述的内容基本上包括了游戏开发中经常使用的技术,大家在学习了本章后一定能够顺利地完成剩余的小部分。当然这里需要说明一下,游戏开发重在创新,玩家需要挑战,所以在开发游戏之前大家一定要选择一个非常具有创意的题材。
推荐阅读相关主题:
网友评论有(0)
CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
相关热门文章