从零开始 OPhoneOPhoneOPhoneOPhone游戏编程
作者:邢野 时间:2009-9-25
MSN:xingye(at)
目录
前言.............................................................................................................................................3
第一章 搭建 OPhone开发环境................................................................................................4
第二章 创建第一个 OPhone程序 Hello Tank.........................................................................8
第三章 显示文字和图片.........................................................................................................15
第四章 响应用户事件.............................................................................................................27
第五章 小结——扫雷游戏的实现.........................................................................................33
第六章 SurfaceView动画.......................................................................................................36
第七章 精灵、帧动画与碰撞检测.........................................................................................43
第八章 地图的设计和实现.....................................................................................................59
前言前言前言前言
什么是OPhoneOPhoneOPhoneOPhone
OPhone是基于 Linux、面向移动互联网的终端基础软件及系统解决方案。
OPhone SDK是专为 OPhone平台设计的软件开发套件,它包括 OPhone API,OPhone模拟
器,开发工具,示例代码和帮助文档(摘自 OPhone官方网站:
简而言之,OPhone是一个移动终端的操作系统,移动终端包括手机、MID、NetBook等等。
与其他领域的编程一样,OPhone编程并没有什么神秘之处,只需简单的学习就可以掌握大
部分的概念。剩下的就是尽情发挥你的想象力了。
写作本文的目的
为了普及 OPhone编程的基本知识,并通过复刻一个坦克大战游戏让读者了解 2D游戏编程
的简单思路。文中的程序结构和实现方法并非最优,希望能起到一个抛砖引玉的作用,让更
多的人加入到 OPhone开发的行列中来。
谁适合阅读本文
虽然本文叫做“从零开始 OPhone编程”,但并不能面对那些对编程一无所知的读者。实际
上,本文要求读者了解 java语言的基本知识,最好会使用 eclipse。在文章的每个章节都标
有难度,有能力的读者完全可以跳过相对容易的章节直接阅读自己感兴趣的内容。
本文的时效性
本文只适合当前版本的 OPhone SDK(),本文的代码、图片、链接可能会因时间推移而
失效。
第一章第一章第一章第一章 搭建搭建搭建搭建OPhoneOPhoneOPhoneOPhone开发环境开发环境开发环境开发环境
难度:容易
工欲善其技,必先利其器。我们要做的第一件事就是搭建 OPhone开发环境。本文只介绍
Windows下的安装方法,Linux下的安装方法请参考官方网站的介绍。
与 PC编程略有不同的是,OPhone的程序需要在模拟器中运行。因此,我们需要一个集成
开发环境,一个 SDK和一个模拟器。因为 OPhone编程使用 java语言,所以我们还需要 JDK,
最好使用安装版本 ( JDK 6 Update 16
Windows版即可。
集成开发环境我们选用 eclipse。目前我们只涉及到 OPhone Application 的开发,可以使用
到 的任意版本。如果以后要开发 OPhone Widget 那么最好选用
( OPhone官方网
站下载 OPhone SDK( SDK中已经包含了模拟器。
最佳的安装顺序是,首先安装先安装 JDK,然后把下载好的 eclipse压缩包解压。我一般会
把开发工具放在 D:\dev下面。最后安装 OPhone SDK。因为 OPhone SDK的安装包是 jar格
式,如果没有文件关联,是无法直接运行的。这时候需要使用命令行:
<JDK>\bin\java -jar
注意,如果 JDK的 bin目录已经加入 path中,可以不用写前面的目录。
全部安装完毕之后,还需要安装 eclipse插件。插件是用来扩展 eclipse功能的。开发 OPhone
用的插件叫 ADT(Android Developer Tools,因为 OPhone脱生于 google的 Android 系统,
所以使用 android的开发插件)。它可以帮助我们完成创建 OPhone项目,向模拟器部署并运
行程序,调试程序等工作。关于 ADT的功能,在后面使用中我们会逐渐熟悉。
安装 ADT的方法如下(以 为例):
启动 eclipse,选择菜单中的 Help -> Software Updates
点击 Add Site
点击 Archive…
找到 OPhone SDK安装目录下 tools\ophone\ (因为我已经安装好了 ADT,所以
出现了重复 URL的提示),点击 OK即可开始安装
ADT安装完毕后还要简单配置一下,打开菜单中的Window -> Preferences
找到 Android项,通过 Browse按钮指定 OPhone SDK的安装位置
至此为止,OPhone的安装环境就全部搭建完毕了。下一章节,我们会遇见经典的 helloworld,
下章见!
第二章第二章第二章第二章 创建第一个创建第一个创建第一个创建第一个OPhoneOPhoneOPhoneOPhone程序程序程序程序 HHHHelloelloelloello TankTankTankTank
难度:容易
现在开始,我们要真正写作 OPhone程序了。虽然前面安装过程那么复杂,但是写起程序来
却是非常简单。而且为了让大家有一个直观的认识,本文不会叙述大段的原理,而是在编码
的过程中渗透对原理、概念的讲解。
让我们打开 eclipse,选择菜单中的 File -> New -> Project…
选择 Android -> Android Project
下面需要我们输入项目的一些信息,因为我们要复刻经典游戏坦克大战,所以我们的程序就
取名 Tank
这样,一个Android项目就创建完成了,我们可以在 eclipse的 Package Explorer看到我们的
项目
托 ADT的福,虽然我们只输入了几个名字,但这个项目实际上已经可以运行了。右击项目
名,选择 RunAs -> AndroidApplication
不出意外的话,你会看到一个手机模拟器被启动,而我们刚刚建立的程序会被运行起来
如果你发现模拟器启动了,而程序并没有被运行,可能需要手工启动程序。这里我们用到一
个重要的工具 DDMS(Davlik Debug Manager)。运行 DDMS快捷方法是点击 eclipse右上角
的 Open Perspective,如果在弹出的列表中没有 DDMS,那么点击 Others
选择 DDMS
这样我们就打开了 DDMS界面,这个工具我们以后会经常用到。
刚刚说到模拟器启动了而程序并没有被运行,很可能是在模拟器启动过程中DDMS失去了
与模拟器的链接。解决方法很简单:
点击 Devices标签下的工具栏,选择 Reset adb
然后右击项目名称,RunAs -> AndroidApplication。
除了右击运行项目,还可以通过工具栏上的运行按钮启动程序
在运行按钮左边的是 Debug按钮,这两个我们以后也会经常用到。
现在我们已经有了第一个可以运行的 OPhone,虽然你可能对 ADT生成的一堆文件感到一头
雾水,也不知道程序界面上那一句“Hello World, Main”是从哪里来的,但是没关系,随着
本文的深入你会逐渐熟悉 OPhone项目的目录结构,程序设计的原则和方法,以及调试和部
署的方法。现在读者可以自己熟悉一下模拟器的操作,让我们下章再见。
第三章第三章第三章第三章 显示文字和图片显示文字和图片显示文字和图片显示文字和图片
难度:容易
从本章开始,读者就要编写代码了。按照作者的原则——少一些理论,多一些实践,代码中
可能会有跳跃的地方。但是请大家不要着急,随着学习的深入,你很快就会了解其中的奥秘 。
不过在开始之前,我们还是要先来理顺一下思路,看看完成一个坦克大战游戏需要哪些工作 :
首先,我们需要一个基本的程序,这个程序能够在 OPhone上运行;这个程序要能够显示图
形包括地图,主角和 NPC等等;程序能够接受用户的输入,控制主角移动;程序要能够控
制 NPC和子弹的移动;程序还能对各种事件做出判断,比如击中敌人,获得物品,胜利或
者失败。
现在我们就从基本程序开始,一步一步实现它。
首先,让我们看一下刚刚生成的文件目录
在源文件目录下,只有 和 两个文件,刚刚被我们命名成 的文件
就是程序的入口文件。而 是由插件来维护的资源定义文件,我们先不管它。
内容如下:
packagepackagepackagepackage ;
importimportimportimport ;
importimportimportimport ;
publicpublicpublicpublic classclassclassclass Main extendsextendsextendsextends Activity {
/** Called when the activity is first created. */
@Override
publicpublicpublicpublic voidvoidvoidvoid onCreate(Bundle savedInstanceState) {
(savedInstanceState);
setContentView();
}
}
很幸运,的代码非常之少,而且还有一段注释,以致我们很容易知道函数 onCreate
的作用,需要解释的只是 setContentView()。先不要管注释中提到的Activity和 setContentView
的参数 ,我们使用 setContentView的另一种形式:setContentView(View view)。
setContentView的作用是设定当前使用的视图即 View(依此理解,可以有很多个 View,需
要用哪个就可以把他作为 setContentView的参数显示出来)。View是一个非常重要的组件,
它可以用来显示文字,图片,也可以接收客户的操作,比如触摸屏,键盘等等,而我们的游
戏中正是需要绘图和交互,看来 View很符合我们的需要(但是请注意,使用 View并不是
我们的最终方案,原因会在后面说明。此处介绍 View是为了讲解基础的图形和用户控制)。
下面我们就要订制一个属于自己的 View,可以通过继承自系统提供的 View,并重载相关的
函数来实现。创建类的方法如下:
右击包名 New -> Class
我们将这个 View类命名为 GameView,并且由 继承
点击 Finish,一个 View类就创建好了。这里是第一次创建类,以后就不会有图片演示了,
请大家记住的这个方法。GameView创建好了,但是代码还有一些错误,这里介绍一下 eclipse
的使用技巧,将鼠标悬停在有错误的位置,或者将光标停在有错误的行,然后按 Ctrl+1键,
就会出现修改建议,大部分时候,使用修改建议都可以改正我们的错误,如图
可以看出来,刚刚的错误是因为没有创建构造函数,选择修改建议的第二项,增加一个构造
函数
publicpublicpublicpublic GameView(Context context) {
supersupersupersuper(context);
// TODOTODOTODOTODO Auto-generated constructor stub
}
我们的 View就创建好了。
回到,刚刚说了,只要将 View作为 setContentView的参数,这个 View就可以被
显示出来:
publicpublicpublicpublic voidvoidvoidvoid onCreate(Bundle savedInstanceState) {
(savedInstanceState);
setContentView(newnewnewnew GameView(thisthisthisthis));
}
现在让我们运行模拟器,看看程序变成什么样子了(启动模拟器的方法见第二章)。
不要意外,屏幕上就是一片空白,因为我们创建了一个 View,但是没有让它显示任何内容。
下面我们就会在 View上显示一段文字和一张图片。
让 View显示内容也很简单,只需要重载 View的 onDraw函数,把相应的语句写入 onDraw
中即可。打开 ,点击菜单 Source -> Override/Implement Method…
选中 onDraw点击 OK
下面这段代码就会被加入到程序当中,所有与显示有关的代码都会在这里面完成
@Override
protectedprotectedprotectedprotected voidvoidvoidvoid onDraw(CanvasCanvasCanvasCanvas canvascanvascanvascanvas) {
// TODOTODOTODOTODO Auto-generated method stub
(canvas);
}
这里我们遇到了又一个非常重要的类 Canvas,Canvas一般翻译成画布,所有的绘图操作都
是通过 Canvas中的函数来完成的,比如显示文字的函数 (),显示位图的函
数 (),以及各种绘制图形的函数如 (),()
等等。下面让我们显示一段文字在屏幕上:
protectedprotectedprotectedprotected voidvoidvoidvoid onDraw(Canvas canvas) {
// TODOTODOTODOTODO Auto-generated method stub
(canvas);
("坦克大战", 50, 50, newnewnewnew Paint());
}
坦克大战四个字已经出现在了屏幕上。让我们来详细看一下这条语句:
("坦克大战", 50, 50, newnewnewnew Paint());
第一个参数是要显示的文字,第二、第三个参数是文字在屏幕上的坐标,说到坐标得多讲两
句。在 2D编程中,屏幕坐标的原点是屏幕的左上角,横向向右增大,纵向向下增大,如上
图所示。最后一个参数是 Paint,通常翻译成画笔,它决定了文字或图形的颜色,字体,线
条粗细等等,后面用到相应属性的时候会详细介绍。那么这条语句就是在屏幕上(50,50)
的位置用缺省的画笔写出“坦克大战”四个字。另外如果 eclipse提示代码错误,不要忘了
用 Ctrl+1。
有了文字,下面就是图像了。显示图像比显示文字略微复杂一些,首先我们要准备一张位图 ,
图片必须是 png格式的,文件名只能是小写字母,数字和下划线。
然后将这张图片 copy到工程的 res/drawable目录下。可以直接在 eclipse的目录树中粘贴。
显示位图的函数是 (),drawBitmap有很多种形态,我们先看其中最简单
的一种
(bitmapbitmapbitmapbitmap, left, top, paint)
乍一看似乎和 drawText差不多,4个参数有三个都相同,但这第一个参数 bitmap要比文本
复杂得多。首先,他是一个 Bitmap类实例,因为我们现在还不需要这个类的其他功能,所
以不过多介绍 Bitmap,只考虑它是怎么来的。得到 Bitmap实例的方法也有很多种,这里只
介绍其中的一种
(resresresres, id);
此方法可以返回一个 bitmap实例,但是这个函数还需要两个参数 res和 id。res是 Resources
实例,而 id是一个整数,下面让我们分别了解这两个参数。res的地位跟 bitmap差不多,只
需要作为参数被使用,因此,只要得到实例就可以了,获得 Resources实例的方法如下:
res = ();
天哪,事情越来越复杂了,因为这段代码里面有多了一个陌生面孔 context。context是 Context
实例,Context通常翻译做上下文,这个名称似乎有点晦涩,他究竟是什么呢?让我们回头
看看写好的程序
publicpublicpublicpublic GameView(ContextContextContextContext contextcontextcontextcontext) {
supersupersupersuper(context);
// TODOTODOTODOTODO Auto-generated constructor stub
}
这时候我们有一个 context实例,继续朔源而上,在中
publicpublicpublicpublic voidvoidvoidvoid onCreate(Bundle savedInstanceState) {
(savedInstanceState);
setContentView(newnewnewnew GameView(thisthisthisthis));
}
原来,context指向Main类。好了,我们终于找到 res的源头了。还有另外一个分支第二个
参数 id。
(res, idididid);
id是一个整形,它到底是谁的id呢?我们还是得往前面找,还记得我们第一次见到函数
setContentView时什么样子么
setContentView();
对,他的参数是,后来被我们替换成了GameView实例。就是一个
整数。它被定义在文件中,我们前面讲过是由插件维护的资源定义文件。说到
这里大家应该猜到了吧。让我们打开文件
publicpublicpublicpublic finalfinalfinalfinal classclassclassclass R {
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal classclassclassclass attr {
}
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal classclassclassclass drawable {
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint battlecity=0x7f020000;battlecity=0x7f020000;battlecity=0x7f020000;battlecity=0x7f020000;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint icon=0x7f020001;
}
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal classclassclassclass layout {
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint main=0x7f030000;
}
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal classclassclassclass string {
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint app_name=0x7f040001;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint hello=0x7f040000;
}
}
果然,位图文件 在这里面也被分配了一个 id:,没错,就
是它了,这就是我们要找的 id。至此为止,我们终于可以使用 drawBitmap了。
对于一次创建,多次使用的资源,我们把他放到构造函数里面。增加了图形显示的 GameView
如下:
publicpublicpublicpublic classclassclassclass GameView extendsextendsextendsextends View {
BitmapBitmapBitmapBitmap bmpbmpbmpbmp;;;;
publicpublicpublicpublic GameView(Context context) {
supersupersupersuper(context);
// TODOTODOTODOTODO Auto-generated constructor stub
ResourcesResourcesResourcesResources resresresres ==== ();();();();
bmpbmpbmpbmp ==== (res,(res,(res,(res,
););););
}
@Override
protectedprotectedprotectedprotected voidvoidvoidvoid onDraw(Canvas canvas) {
// TODOTODOTODOTODO Auto-generated method stub
(canvas);
("坦克大战", 0, 50, newnewnewnew Paint());
((((bmpbmpbmpbmp,,,, 0,0,0,0, 100,100,100,100, newnewnewnew Paint());Paint());Paint());Paint());
}
}
运行效果
第四章第四章第四章第四章 响应用户事件响应用户事件响应用户事件响应用户事件
上一章介绍了如何显示文字和图片,一般来说,下一步就该讲到动画了。可是我们前面说了 ,
使用 View不是最终的选择,要实现动画还需要很多复杂的代码。相对来说,学习如何响应
用户事件要简单些。
本章前半部分讲解按键事件的响应,但是这也不是最终方案,因为实际上的手机可能没有硬
键盘,需要使用虚拟键盘,所以后半部分我们会讲解虚拟键盘的设计和实现。
同绘图一样,View也是通过回调函数来响应用户事件的。键盘事件的回调函数有多个,以
对应不同的事件,我们暂时只用到 onKeyDown,对应按键被按下的事件,其他函数以后用
到再介绍。让我们重载 onKeyDown(重载一个函数的方法前面章节有介绍):
@Override
publicpublicpublicpublic booleanbooleanbooleanboolean onKeyDown(intintintint keyCode, KeyEvent event) {
// TODOTODOTODOTODO Auto-generated method stub
returnreturnreturnreturn (keyCode, event);
}
onKeyDown有两个参数:keyCode和 event,通过 keyCode能判断是哪个键被按下,event
比较复杂,包含了这次按键更多的信息,我们暂时先不考虑它。
现在我们要通过按键控制主角向四个方向移动。所谓移动,就是将主角的图像在不同的位置
显示出来,也就是改变函数 drawBitmap中的第二、第三个参数。比如用户按下右方向键,
我们就把横坐标增加,这样下次显示出来的时候,主角就会往右一点。为了节约时间,我们
就把刚刚显示的图片 BattleCity 作为主角好了。首先定义两个全局变量 x 和 y,然后在
onKeyDown中改变 x、y的值,然后重绘 View。因为代码没有什么难度,所以不做讲解了。
publicpublicpublicpublic classclassclassclass GameView extendsextendsextendsextends View {
intintintint x=0, y=0;
……
@Override
protectedprotectedprotectedprotected voidvoidvoidvoid onDraw(Canvas canvas) {
……
(bmp, x, y, newnewnewnew Paint());
}
@Override
publicpublicpublicpublic booleanbooleanbooleanboolean onKeyDown(intintintint keyCode, KeyEvent event) {
// TODOTODOTODOTODO Auto-generated method stub
switchswitchswitchswitch(keyCode) {
casecasecasecase _DPAD_UP:
y -= 10;
breakbreakbreakbreak;
casecasecasecase _DPAD_DOWN:
y += 10;
breakbreakbreakbreak;
casecasecasecase _DPAD_LEFT:
x -= 10;
breakbreakbreakbreak;
casecasecasecase _DPAD_RIGHT:
x += 10;
breakbreakbreakbreak;
}
postInvalidate(); //通知系统重绘 View
returnreturnreturnreturn (keyCode, event);
}
}
完成后我们肯定很想测试一下,但是此时你会发现,按键根本没有任何反应。这就是我们要
特殊指出的地方。View被显示时,缺省情况下没有获得焦点,就是说,按键动作没有发送
给 View,所以需要在构造函数中增加一句
publicpublicpublicpublic GameView(Context context) {
……
setFocusable(truetruetruetrue);
}
再运行程序,看看图片是否按照我们的指令运动起来了。
前面说过,很多手机没有硬键盘,所以我们需要一个软键盘的解决方案。软键盘就是在屏幕
上显示一个键盘,然后响应用户的触摸屏操作,模拟成键盘操作。对于坦克大战,我们只需
要在屏幕上显示一个模拟的游戏手柄(显示图片的方法大家没有忘记吧,显示位置可以根据
模拟器自行调整):
在用户触摸模拟手柄上的方向键和开火键时进行相应的操作。我们拿方向键做演示,步骤如
下:首先确定四个方向键在屏幕上的区域(上图的红色方框),然后在触摸屏事件的响应函
数中判断事件是否发生在方向键区域中,最后如果事件发生在区域中进行相应的操作。
下面,我们引入一个非常有用的类 Rect(RectF与 Rect基本相同,不过以 float作为坐标参
数), rect是 rectangle的简写,顾名思义,这个类代表了一个矩形。Rect通过矩形 4个边来
定义这个矩形的范围。他们分别是 left,right,top,bottom,如图所示:
转化为屏幕坐标,top是矩形坐上角的纵坐标,left是矩形坐上角的横坐标,right是矩形右
下角的横坐标,buttom是右下角的纵坐标。有了 Rect我们就可以方便的表示虚拟手柄各个
键的位置。同时 Rect还提供了一些很有用的函数,其中 (x, y)能够判断点(x, y)
是否在矩形框中,正好是我们需要的。
现在我们就可以开始编码了,首先为虚拟键盘的方向键创建 Rect(可以用绘图工具测量坐
标):
Rect rKeyUp = newnewnewnew Rect(56,290,86,320);
Rect rKeyDown = newnewnewnew Rect(56, 350, 86, 380);
Rect rKeyLeft = newnewnewnew Rect(26, 320, 56, 350);
Rect rKeyRight = newnewnewnew Rect(86, 320, 116, 350);
然后重载触摸屏响应函数:
@Override
publicpublicpublicpublic booleanbooleanbooleanboolean onTouchEvent(MotionEvent arg0) {
// TODOTODOTODOTODO Auto-generated method stub
returnreturnreturnreturn (arg0);
}
下面我们要做的是,首先判断触摸屏操作是不是按下,如果是,取得坐标(x,y),然后判
断坐标所在的按键,做出相应的操作
@Override
publicpublicpublicpublic booleanbooleanbooleanboolean onTouchEvent(MotionEvent arg0) {
// TODOTODOTODOTODO Auto-generated method stub
ifififif (() == _DOWN) {
intintintint ax = (intintintint) ();
intintintint ay = (intintintint) ();
ifififif ((ax, ay)) {
y -= 10;
} elseelseelseelse ifififif ((ax, ay)) {
y += 10;
} elseelseelseelse ifififif ((ax, ay)) {
x -= 10;
} elseelseelseelse ifififif ((ax, ay)) {
x += 10;
}
postInvalidate(); //不要忘记刷新屏幕
}
returnreturnreturnreturn (arg0);
}
现在让我们运行一下,每次用鼠标点击模拟手柄的方向键,图片就会移动
至此为止,我们介绍了两种响应用户事件的手段,但是要真正完成对一个游戏的控制,还需
要更多的工作,后面还有深入的讲解。
第五章第五章第五章第五章 小结小结小结小结————————扫雷游戏的实现扫雷游戏的实现扫雷游戏的实现扫雷游戏的实现
目前,我们学习了如何建立 OPhone编程环境,如何显示文字和图片,如何响应用户事件。
作为总结,我们要运用这些知识实现一个扫雷游戏。
先说游戏规则:扫雷,就是在一个分成若干小格的矩形区域中发现隐藏的地雷,找到它,但
是不能触发它。每次翻开一个小格,如果下面是地雷,游戏就失败了。如果不是地雷,而它
的周围 8个格中有地雷,那么就会显示周围的地雷数。如果周围 8个格中没有地雷,那就是
空白的。如果你认为某一格是地雷可以用红旗标记它,正确标记了所有的地雷或者翻开了所
有不是地雷的格就取得胜利。
再说用户操作:在Windows中用户可以有三种操作,左键单击,右键单击,左右键同时单
击。左键可以翻开一个小格,可以触雷,如果点到了一个空白格,跟它相联的所有空白格都
会被打开。右键可以标记一个格,不会触雷,标记方式有两种,第一次单击用红旗标记,确
信此处有雷,再次单击红旗变成问号,表示可能有雷。左右键同时单击只在此种情况有效,
即一个格周围有雷,并且所有的雷都已被用红旗标记出来。此时单击此格会打开周围所有未
标记的格,此操作会触雷,就是说如果标记错了游戏就会失败,所以一定要小心使用。因为
在手机上没有右键,所以我们必须设计替代方案,一种方法是设计一个开关图标,点击打开
开关后,所有的操作都被认为是右键和左右键同时单击,另外还可以借助手机上的菜单键,
按住菜单键等同打开开关,松开等同关闭开关。为了方便游戏,我们可以同时实现两个方案 。
最后让我们分析一下程序的大体思路,从最直观的用户界面开始,我们需要根据游戏的显示
区域创建游戏地图,一个m行 n列的二维整形数组,数组中的一个值对应界面上的一个格,
不同的数值可以表示不同的状态,比如 0表示空白格,1表示此格周围有一个雷,2表示有
两个雷等等。因为雷的位置是随机的,所以这张地图需要在每次游戏开始的时候被初始化。
另外因为这张地图不能直接显示给用户看,所以我们还需要另一个同样大小的地图把它盖起
来。并且也用不同的数值来表示一个格是否被翻开,或者被标记等等。这样每次刷新屏幕我
们会根据地图上的数值在屏幕上对应的位置显示不同的图片,就是我们看到的游戏界面。这
种使用多层地图的技术在游戏中非常常见。这两个地图可以在同一个二维数组中,也可以分
开两个数组,为了便于理解,我们使用两个数组分别表示。另外,界面上还要显示当前剩余
的地雷数量和游戏时间。剩余地雷数是地雷总数减去用户标记的地雷数,可以是负值。游戏
时间是每次新游戏开始时启动的一个计时器,我们知道,如果要时间连贯显示,就必须不停
的自动刷新屏幕,但是回顾前面四章的内容,并没有讲解如何循环刷新屏幕,所以在这里会
介绍一种使用 Handler刷新屏幕的方法,这种方法比较简单,但是效率并不高,所以后面我
们还会介绍另外一种更有效的方法。
接着来看用户事件的响应,虽然用户也可以用键盘来操作,但那样就失去了扫雷游戏追求速
度的快感,所以我们只设计使用触摸屏的方案。当用户点击屏幕时,首先判断点击在哪一格 ,
再根据用户点击的方法,以及被点击格的状态,判断用户操作的结果,改变地图上相应格的
数值,刷新后用户操作的结果就会在屏幕上反映出来。
另外我们还需要一些游戏状态的标志,比如游戏胜利,失败或是正在游戏中。如果游戏胜利 ,
还需要存储记录,由于存储操作前面没有讲到,我们暂时放弃这个功能。
为了缩减篇幅,下面使用源代码直接讲解,源程序的 eclipse工程文件已经随本文一起提供
下载,这里就算是一个源程序导读(在源程序的 res/raw目录下有一首 mp3很好听哦)。
首先看
……
@Override
protectedprotectedprotectedprotected voidvoidvoidvoid onPause() {
/*/*/*/*
**** 在程序被挂起或者退出的时候改变游戏状态以结束游戏循环
*/*/*/*/
= _LOST;
();
}
……
这里我们对 onPause做一个说明,也是对 OPhone程序生命周期的一个简单介绍。详细内容
会随着文章的深入慢慢讲解。大家知道在 pc中,多个程序是可以同时运行的,即多进程。
在手机中,程序依然可以多进程运行,但是还有一些不同:首先是屏幕,当一个应用启动后 ,
他要独占屏幕。这样一些有打断功能程序运行起来后,当前的程序就不可见了,最简单的例
子就是来电。当你正在游戏,突然有电话打进来,来电程序会占领屏幕,你的游戏转到后台
运行(这时 onPause事件就会被触发);另一个不同的地方是,如果一个程序被转到后台太
长时间而没有再次被激活,那么系统会结束这个程序。在结束之前会触发相应的事件
(onSaveInstanceState,后面会介绍)。而与之对应的,当程序开始,触发 onCreate事件时,
我们需要检测当前程序是第一次运行还是被在后台销毁后重新运行,如果是重新运行,我们
需要装载程序销毁前的状态信息。
这里我们用 onPause的方法并不正确,因为程序挂起的时候不应该让游戏结束,但是为了简
化代码,我们暂时先这样做,后面会加以改进。
程序的主要内容在 中,随本章提供的源代码中有详细的注释,请大家自行阅
读源码。
第六章第六章第六章第六章 SurfaceViewSurfaceViewSurfaceViewSurfaceView动画动画动画动画
难度:中等
前面介绍的内容,还是比较简单的,应用这些知识,可以完成一些非实时游戏,比如井字棋
等,或者一些画面刷新不是很频繁、实时性不强的游戏,比如我们前面做的扫雷。但是我们
的目标是坦克大战,对操作的实时性要求比较高,更有很多的 NPC需要处理,绘图的工作
量也很大,所以我们要用一个新的视图类 SurfaceView 代替 View 来完成显示工作。
SurfaceView与 View有一些不同,但是我们只用其中的一个特性:在主线程之外的线程中向
屏幕上绘图。这样就可以避免在画图任务繁重的时候造成主线程阻塞,从而提高程序的反应
速度。
首先让我们重新定义一个 GameView 类,让他继承自 SurfaceView,并且要实现
接口。为什么要实现 Callback接口呢?因为使用 SurfaceView有一个
原则,所有的绘图工作必须得在 Surface被创建之后才能开始(Surface—表面,这个概念在
图形编程中常常被提到。基本上我们可以把它当作显存的一个映射,写入到 Surface的内容
可以被直接复制到显存从而显示出来,这使得显示速度会非常快),而在 Surface被销毁之前
必须结束。所以Callback中的 surfaceCreated和 surfaceDestroyed就成了绘图处理代码的边界 。
我们直接让 GameView类实现 Callback接口,使程序更简洁一些。
GameView被创建,并补充了构造函数之后就是这个样子(创建类和添加构造函数的方法前
面有介绍哦)
packagepackagepackagepackage ;
importimportimportimport ;
importimportimportimport ;
importimportimportimport ;
importimportimportimport ;
publicpublicpublicpublic classclassclassclass GameView extendsextendsextendsextends SurfaceView implementsimplementsimplementsimplements Callback {
publicpublicpublicpublic GameView(Context context) {
supersupersupersuper(context);
// TODOTODOTODOTODO Auto-generated constructor stub
}
publicpublicpublicpublic voidvoidvoidvoid surfaceChanged(SurfaceHolder arg0, intintintint arg1, intintintint arg2,
intintintint arg3) {
// TODOTODOTODOTODO Auto-generated method stub
}
publicpublicpublicpublic voidvoidvoidvoid surfaceCreated(SurfaceHolder arg0) {
// TODOTODOTODOTODO Auto-generated method stub
}
publicpublicpublicpublic voidvoidvoidvoid surfaceDestroyed(SurfaceHolder arg0) {
// TODOTODOTODOTODO Auto-generated method stub
}
}
这里我们有看到了一个新的类 SurfaceHolder,我们权且把它当作一个 Surface的控制器,用
它来操作 Surface。因为我们现在还不需要直接操作 Surface,所以我们不做深入讲解。而唯
一要使用的是 ,即为 SurfaceHolder添加回调函数。原因前面我已
经说明了,方法如下:
publicpublicpublicpublic GameView(Context context) {
supersupersupersuper(context);
// TODOTODOTODOTODO Auto-generated constructor stub
getHolder().addCallback(thisthisthisthis);
}
现在我们可以运行一下,跟第一次使用 View一样,界面上什么也没有。因为我们还没有编
写绘图的代码嘛。
前面说过,我们之所以使用 SurfaceView代替 View,是因为 SurfaceView可以在主线程之外
的线程中进行绘图操作,从而提高界面的反应速度。下面我们要做的就是创建一个用来绘图
的线程。不过在这之前我们可以先了解一些关于游戏循环的知识:
我们知道,一般的应用程序是用户驱动的,就是用户操作了,程序再来响应。而我们的游戏
呢,不管用户有没有操作,都会有一些变化,最明显的就是 npc会移动、发生世界事件等。
因此,我们可以说,游戏程序在一个无限循环当中,我们就把它叫做游戏循环。那么在游戏
循环中要做哪些工作呢?让我们用一个流程图来说明游戏循环的过程:
获取用户输入
移动主角
移动 NPC和子弹
碰撞检测
产生世界事件
这只是我们假设的流程,不同的游戏肯定会都有些变化。而且细节上会有更多的差别。
了解了游戏循环,下面的工作就是建立一个线程,线程中包含一个游戏循环,在游戏循环中
更新游戏的各种数据,并根据这些数据将游戏画面绘制在 Surface上最终显示给玩家。
创建线程的方法很简单,我们不需要知道 Thread的很多高级特性。只需要知道,在线程中
完成具体的工作需要重载 run()函数。线程通过 start()函数启动。然后就会执行 run()函数中
的内容,run()函数执行结束后线程就会终止。因此我们将游戏循环放在 run()函数中。通过
start()启动循环,并通过适当的方式结束循环进而结束整个线程。还要注意一点,所有对
Surface的操作都必须要保证同步,因此我们会使用Synchronized关键字,同步 SurfaceHolder。
增加了 GameThread后的代码如下:
publicpublicpublicpublic classclassclassclass GameView extendsextendsextendsextends SurfaceView implementsimplementsimplementsimplements Callback {
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal String tag = "GameView";
//声明GameThread类实例
GameThread gameThread;
publicpublicpublicpublic GameView(Context context) {
supersupersupersuper(context);
// TODOTODOTODOTODO Auto-generated constructor stub
//获取SurfaceHolder
SurfaceHolder surfaceHolder = getHolder();
//添加回调对象
(thisthisthisthis);
//创建GameThread类实例
gameThread = newnewnewnew GameThread(surfaceHolder);
}
publicpublicpublicpublic voidvoidvoidvoid surfaceChanged(SurfaceHolder arg0, intintintint arg1, intintintint arg2,
intintintint arg3) {
// TODOTODOTODOTODO Auto-generated method stub
(tag, "surfaceChanged");
}
publicpublicpublicpublic voidvoidvoidvoid surfaceCreated(SurfaceHolder arg0) {
// TODOTODOTODOTODO Auto-generated method stub
(tag, "surfaceCreated");
//启动gameThread
();
}
publicpublicpublicpublic voidvoidvoidvoid surfaceDestroyed(SurfaceHolder arg0) {
// TODOTODOTODOTODO Auto-generated method stub
(tag, "surfaceDestroyed");
//通过结束run()函数的方法结束gameThread,详见GameThread类的定义
= falsefalsefalsefalse;
}
/**
* GameThread的定义
* @author@author@author@author xingye
*
*/
classclassclassclass GameThread extendsextendsextendsextends Thread {
SurfaceHolder surfaceHolder;
//run()函数中控制循环的参数。
booleanbooleanbooleanboolean run = truetruetruetrue;
publicpublicpublicpublic GameThread(SurfaceHolder surfaceHolder) {
= surfaceHolder;
}
@Override
publicpublicpublicpublic voidvoidvoidvoid run() {
// TODOTODOTODOTODO Auto-generated method stub
intintintint i = 0;
whilewhilewhilewhile(run) {
(tag, "GameThread");
Canvas c = nullnullnullnull;
trytrytrytry {
synchronizedsynchronizedsynchronizedsynchronized (surfaceHolder) {
//我们在屏幕上显示一个计数器,每隔1秒钟刷新一次
c = ();
(255, 255, 255, 255);
("" + i++, 100, 100, newnewnewnew Paint());
(1000);
}
} catchcatchcatchcatch (Exception e) {
// TODOTODOTODOTODO Auto-generated catch block
();
} finallyfinallyfinallyfinally {
ifififif (c != nullnullnullnull) {
(c);
}
}
}
}
}
}
运行程序看一下效果
第七章第七章第七章第七章 精灵、帧动画与碰撞检测精灵、帧动画与碰撞检测精灵、帧动画与碰撞检测精灵、帧动画与碰撞检测
经过前几章的学习,大家对使用位图、接受用户控制应该已经有了初步的概念,也可以运用
这些知识完成简单的小游戏。这一章中,我们会为游戏中最重要的部分——图形处理建立一
个基本的框架,这还不是游戏引擎,但是其中很多方法可以为读者以后创建自己的游戏引擎
提供借鉴。这一章的涉及的内容比较多,既有 2D游戏的基础理论,又有复杂的代码。尤其
是代码部分,如果详细讲解,恐怕会占用很大的篇幅。所以我们只对关键的函数进行讲解,
以方便读者今后灵活运用这些代码(所有的源代码都与本章节内容一同提供下载)。
这个框架是完全依照MIDP中 包设计的:
Classes
GameCanvas
Layer
LayerManager
Sprite
TiledLayer
game包中有 5个类,其中 Layer(层)是一个抽象类,对图形显示作了基本的定义。以我们
的目标游戏《坦克大战》为例,在游戏中有这样一些图形元素:我方和敌方的坦克、坦克发
出的子弹、地面、墙体、水域掩体等。这些元素虽然外观不同,但是本质上却非常相似:都
是在特定位置以特定尺寸显示一个或一组位图,有些位图位置还会变动,Layer就定义了位
置,尺寸,显示等相关的功能。之所以叫做 Layer,与游戏中分层地图的概念有关,先让我
们了解一下什么是分层地图:还是说坦克大战,当我们的坦克行驶在普通地面上时,坦克的
图像肯定是覆盖了地面的图像,这样我们能看到坦克。当坦克行驶到掩体时,我们会发现,
掩体的图像覆盖了坦克的图像,如图所示:
掩体
坦克
地面
实际上,我们在程序中,只要首先显示地面的图像,然后显示坦克的图像,最后显示掩体的
图像(掩体图片是镂空的),就能达到这种效果,这就是分层地图。通常我们把最下面的叫
做地面层,中间的叫做物件层,最上面的叫做天空层。关于地图我们就讲这么多,这里只介
绍图形意义上的分层,是为了帮助大家理解 Layer一词的意义。关于地图的详细内容我们在
第十章会深入讲解。
首先让我们看一下抽象类 Layer的定义:
packagepackagepackagepackage ;
importimportimportimport ;
publicpublicpublicpublic abstractabstractabstractabstract classclassclassclass Layer {
intintintint x = 0; // Layer的横坐标
intintintint y = 0; // Layer的纵坐标
intintintint width = 0; // Layer的宽度
intintintint height = 0; // Layer的高度
booleanbooleanbooleanboolean visible = truetruetruetrue; // Layer是否可见
Layer(intintintint width, intintintint height) {
setWidthImpl(width);
setHeightImpl(height);
}
/**
* 设定Layer的显示位置
*
* @param@param@param@param x
* 横坐标
* @param@param@param@param y
* 纵坐标
*/
publicpublicpublicpublic voidvoidvoidvoid setPosition(intintintint x, intintintint y) {
= x;
= y;
}
/**
* 相对于当前的位置移动Layer
*
* @param@param@param@param dx
* 横坐标变化量
* @param@param@param@param dy
* 纵坐标变化量
*/
publicpublicpublicpublic voidvoidvoidvoid move(intintintint dx, intintintint dy) {
x += dx;
y += dy;
}
/**
* 取得Layer的横坐标
*
* @return@return@return@return 横坐标值
*/
publicpublicpublicpublic finalfinalfinalfinal intintintint getX() {
returnreturnreturnreturn x;
}
/**
* 取得Layer的纵坐标
*
* @return@return@return@return 纵坐标值
*/
publicpublicpublicpublic finalfinalfinalfinal intintintint getY() {
returnreturnreturnreturn y;
}
/**
* 取得Layer的宽度
*
* @return@return@return@return 宽度值
*/
publicpublicpublicpublic finalfinalfinalfinal intintintint getWidth() {
returnreturnreturnreturn width;
}
/**
* 取得Layer的高度
*
* @return@return@return@return 高度值
*/
publicpublicpublicpublic finalfinalfinalfinal intintintint getHeight() {
returnreturnreturnreturn height;
}
/**
* 设置Layer是否可见
*
* @param@param@param@param visible
* true Layer可见,false Layer不可见
*/
publicpublicpublicpublic voidvoidvoidvoid setVisible(booleanbooleanbooleanboolean visible) {
= visible;
}
/**
* 检测Layer是否可见
*
* @return@return@return@return true Layer可见,false Layer不可见
*/
publicpublicpublicpublic finalfinalfinalfinal booleanbooleanbooleanboolean isVisible() {
returnreturnreturnreturn visible;
}
/**
* 绘制Layer,必须被重载
*
* @param@param@param@param c
*/
publicpublicpublicpublic abstractabstractabstractabstract voidvoidvoidvoid paint(Canvas c);
/**
* 设置Layer的宽度
*
* @param@param@param@param width
*/
voidvoidvoidvoid setWidthImpl(intintintint width) {
ifififif (width < 0) {
throwthrowthrowthrow newnewnewnew IllegalArgumentException();
}
= width;
}
/**
* 设置Layer的高度
*
* @param@param@param@param height
*/
voidvoidvoidvoid setHeightImpl(intintintint height) {
ifififif (height < 0) {
throwthrowthrowthrow newnewnewnew IllegalArgumentException();
}
= height;
}
}
Layer的代码不多,根据函数名称就可以知道它的功能,主要是 Layer尺寸、位置的设定和
获取。其中最重要的方法 paint是虚方法,Layer图像就是通过这个方法显示出来的。因此继
承自 Layer的所有类都要实现这个方法。
Sprite(精灵)继承自 Layer,同时又增加了帧动画,图形变换和碰撞检测的功能。Sprite是
我们这一章重点介绍的内容。首先让我们了解一下精灵的概念。Sprite这个词在 2D游戏中
非常常见,一般指游戏中具有独立外观和属性的个体元素。如主角、NPC、宝箱、子弹等等 ,
这些都是精灵。
下面就让我们来创建 Sprite类并使其继承自 Layer。创建完毕时,IDE会提示必须实现 paint
方法。但是这时候我们会发现,paint方法要显示那些图形呢?没有。因此我们需要为 Sprite
增加一个 Bitmap类型变量,用来存放 paint要显示的图形。同时,我们要创建一个构造函数
用来初始化这个 Bitmap变量。
publicpublicpublicpublic Sprite(Bitmap image) {
supersupersupersuper((), ());
initializeFrames(image, (), (),
falsefalsefalsefalse);
initCollisionRectBounds();
(TRANS_NONE);
}
虽然这个构造函数只有几行,却涉及到不少的知识。super不用说了,initializeFrames是做
什么的呢?这就要提到帧动画的概念了。什么是帧动画呢?如下图,我们看到一组星星的图
片(4张 16x16的位图)
当我们在同一个位置以一定的时间间隔连续显示这几幅图片的时候就变成了这个样子
我们看到,星星在发光,这就是帧动画。即取得一个连续画面中的几个关键帧,在一定的时
间间隔下连续的显示这些帧从而形成动画,initializeFrames的功能就是初始化这些关键帧。
那么又为什么要初始化呢?通常情况下,我们为了节省空间也为了便于管理,会将一组动画
的多个帧保存在同一个图片文件中(如上图的星星)。这样一来每次显示的时候就不能显示
整张图片,而只能显示这个图片的一部分。因此,我们要计算每一帧在整张图片上的位置以
便正确显示。就让我们来看看 initializeFrames的定义
privateprivateprivateprivate voidvoidvoidvoid initializeFrames(Bitmap image, intintintint fWidth, intintintint
fHeight,
booleanbooleanbooleanboolean maintainCurFrame)
initializeFrames作了这样的工作,首先取得一个位图,然后根据用户设置的单一帧的宽度和
高度计算这个位图中包括多少帧。如刚刚我们看到的星星的图片(64x16像素),当单一帧的
宽度和高度与图片相同的时候,就只有一帧。但是当一帧的宽度和高度均为 16像素时,整
个图片就可以分为 4帧了。这时候,函数会计算每一帧的顶点坐标,如第一帧的顶点是(0,
0),第二帧的顶点是(16,0),并依次类推。这个函数还可以处理更复杂的情况,如下图:
函数会将计算好的各个帧的顶点横纵坐标分别保存在两个数组中( frameCoordsX[],
frameCoordsY[]),下次我们使用帧的序号访问各个帧时(在 paint中)就可以很快找到它的
所对应的位图区域了。
在这个 Sprite的构造函数中,initializeFrames使用了 ()和 ()作
为一帧的高度和宽度,所以这个精灵注定只能有一帧。Sprite的构造函数还有其他样式,如
publicpublicpublicpublic Sprite(Bitmap image, intintintint frameWidth, intintintint frameHeight)
这时候我们就可以指定帧的宽度和高度,定义具有多个帧的 Sprite了。
说完了为帧动画做初始化工作的 initializeFrames ,让我们来看下一个函数
initCollisionRectBounds。
privateprivateprivateprivate voidvoidvoidvoid initCollisionRectBounds() {
collisionRectX = 0;
collisionRectY = 0;
collisionRectWidth = ;
collisionRectHeight = ;
}
这个函数的代码不多,名字翻译过来就是“初始化碰撞矩形边缘”。由此引出另外一个游戏
中的重要概念——碰撞检测。在我们的目标游戏坦克大战中,碰撞检测可是少不了的,我们
的子弹击中敌方坦克就是一次碰撞,只有进行了碰撞检测才能够触发这次击中事件,不然我
们就没法消灭敌人的坦克了。2D游戏中的碰撞检测有几种,最简单的是矩形碰撞检测,复
杂一些的有多边形检测和像素检测等。这里我们只介绍一下矩形检测。如图:
坦克(Sprite)
子弹(Sprite)
我们取坦克和子弹的矩形外框,当这两个矩形重叠的时候就认为是碰撞了。函数
initCollisionRectBounds的功能就是设定 Sprite的矩形外框。同前面初始化帧的原理一样,我
们需要这个函数在多个帧组合成的图片中确定一帧的大小。
现在我们还剩下构造函数中最后一行代码了:
(TRANS_NONE);
这行代码设置了 Sprite图形的变换方式。变换一共有 8种,定义如下:
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_NONE = 0;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_ROT90 = 5;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_ROT180 = 3;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_ROT270 = 6;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_MIRROR = 2;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_MIRROR_ROT90 = 7;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_MIRROR_ROT180 = 1;
publicpublicpublicpublic staticstaticstaticstatic finalfinalfinalfinal intintintint TRANS_MIRROR_ROT270 = 4;
所谓变换,就是对图形进行旋转和镜像等操作,这就相当于增加了图形资源。如图:
因为要显示向 4个方向行驶的坦克,每个坦克都需要 4组图片。如果我们使用了旋转变换功
能,每个坦克只需要一组图片就够了,其他的图片完全可以由旋转获得。需要指出的是,旋
转是围绕参照点(reference pixel)进行的,如果没有使用函数 setRefPixelPosition设定参照
点,缺省情况下参照点就是(0,0)。因此如果我们使用参数 TRANS_ROT90旋转的话,图形
应该是这样
原(x,y)
新(x,y)
这时候有一点必须要特别提示一下,旋转之后,getX和 getY的返回值将发生变化。
讲到这里,这一章的理论实在是够多了,我们必须要总结一下:
首先这一章讲的是图形显示。我们依照 j2me中的 games包建立了两个类 Layer和 Sprite。重
点介绍了 Sprite(精灵)类相关的知识,包括桢动画、碰撞检测和旋转变换。下面我们来看
一组 Sprite的应用实例:
第一个例子,在屏幕上显示一个带有桢动画的 Sprite。
让我们拿出前面做过的 Tank的源代码,将 和 添加到源代码中。
(其中的 TiledLayer我们在讲解地图的时候再详细介绍)
将图片 拷贝到图形资源目录下
打开 ,在其中添加一个 Sprite类型变量 s,并在构造函数中初始化 s
//取得系统资源
Resources res = ();
//获取位图
Bitmap bmpBore = (res,
);
//创建一个Sprite对象,使用位图bmpBore,桢的宽度和高度都为16像素
s = newnewnewnew Sprite(bmpBore, 16, 16);
//显示在150,150位置
(150, 150);
//显示第0桢,第1桢和第3桢
(newnewnewnew intintintint[]{0,1,3});
在 GameThread的 run函数中显示这个 Sprite
synchronizedsynchronizedsynchronizedsynchronized (surfaceHolder) {
c = ();
//显示精灵
(c);
//显示下一桢
();
(200);
}
运行程序,我们可以看到一个小星星在屏幕上闪烁
第二个例子,Sprite的旋转变换。
依旧使用上一个程序,因为星星旋转起来根本没法分辨,所以这次我们使用系统为程序提供
的图标文件 。
首先我们定义一个 Sprite数组
Sprite[] ss = newnewnewnew Sprite[8];
并在构造函数中初始化这个数组
Bitmap bmpIcon = (res,
);
//创建Sprite,并设置为不同的旋转方式
forforforfor(intintintint i=0; i<8; i++){
ss[i] = newnewnewnew Sprite(bmpIcon);
ss[i].setTransform(i);
}
然后在 run函数中显示出来
forforforfor(intintintint i=0; i<8; i++) {
ss[i].setPosition(100, i*40);
ss[i].paint(c);
}
(200);
运行一下看看效果
最后让我们来做一个碰撞检测的例子,这个例子要稍稍复杂一些。先说一下设计思路:
这是一个小游戏,在一个 320x320的区域中,我方坦克在最中间。从上下左右 4个方向有敌
人坦克向中间进攻。我方坦克可以向四个方向发射子弹,使用方向键改变方向,使用空格键
发射,但同时最多只能发射两颗子弹。敌人坦克有三种类型,其中 是普通坦克, 的速
度是普通坦克的一倍, 速度与普通坦克相同,但是需要击中两次才能被摧毁。
首先我们需要引入几个图形文件
分别为子弹、敌方坦克、爆炸效果和我方坦克。
在 GameView中声明变量
// 背景色
Paint p = newnewnewnew Paint();
// 文字
Paint pntText = newnewnewnew Paint();
Sprite player; // 我方坦克
……
在 GameView的构造函数中做一些初始化工作
// 初始化我方坦克
player = newnewnewnew Sprite((res,
), 16, 16);
(newnewnewnew intintintint[] { 0, 1 });
……
增加按键响应事件
publicpublicpublicpublic booleanbooleanbooleanboolean onKeyDown(intintintint keyCode, KeyEvent event)
最后就是在 GameThread的 run函数中完成游戏逻辑了。具体内容请看本章的代码,代码中
有详细的注释。
第八章第八章第八章第八章 地图的设计和实现地图的设计和实现地图的设计和实现地图的设计和实现
这本来是第十章,前面计划还有两章的内容,一是跟第四章一样,完成一个 Asteroid游戏作
为小结,总结一下前面讲过的 Sprite的用法,并演示 NPC和子弹的处理方法。但是,在写
第七章的最后一个例子的时候,把本来简单的碰撞检测的例子扩充了一下,加入了 NPC和
子弹,基本和 Asteroid的功能差不多了,所以就把原定的 Asteroid砍掉了。另外一章是讲解
程序的生命周期,内容相对简单,但考虑到上一章讲 Sprite时已经引入了 TiledLayer,如果
接着讲地图应该会更连贯些,所以把生命周期向后移了一章。
那么,就先让我们看看地图的设计和实现。
如果我们需要的地图很小又很少,完全可以将整个地图画在一张图片上。但是如果地图很多 ,
绘制和管理地图的工作就会很麻烦,这时我们就需要用到另外一种技术——图块(Tile)。所
谓 Tile,就是将地图中的公共元素提取出来,然后在显示的时候组合这些元素形成完整的地
图,这就是本章要介绍的主要内容。
如下图是组成坦克大战地图的所有元素(16x16像素):
接下来让我们看一幅游戏中的场景:
可以看到,整个游戏场景就是由上面那些 Tile构成的。这样,摆在我们面前的任务就很简单
了:将地图依照 Tile的大小分成若干格,将对应的 Tile填到格子中。
在前面讲 Sprite的时候,我们知道可以将组合在一张图片中的关键桢编号,以后就可以通过
编号来使用这个桢(参看上一章桢动画的相关内容),这种方法也同样适用于 Tile。而 2D地
图很容易让我们想到二维数组。也就是说我们可以将 Tile的编号放到二维数组中,这样我们
就可以通过历遍数组元素,像显示 Sprite那样将整张地图一块一块的显示出来。
以上面的那张游戏截图为例,一共 13行 13列,我们可以定义一个 13x13的二维数组(因为
地图上有空白区域,所以我们将 Tile的编号从 1开始,用 0表示空白)。
让我们找到 ,增加成员变量map[][]:
intintintint[][] map = {
{0,0,0,2,0,0,0,2,0,0,0,0,0},
{0,1,0,2,0,0,0,1,0,1,0,1,0},
{0,1,0,0,0,0,1,1,0,1,2,1,0},
{0,0,0,1,0,0,0,0,0,2,0,0,0},
{3,0,0,1,0,0,2,0,0,1,3,1,2},
{3,3,0,0,0,1,0,0,2,0,3,0,0},
{0,1,1,1,3,3,3,2,0,0,3,1,0},
{0,0,0,2,3,1,0,1,0,1,0,1,0},
{2,1,0,2,0,1,0,1,0,0,0,1,0},
{0,1,0,1,0,1,1,1,0,1,2,1,0},
{0,1,0,1,0,1,1,1,0,0,0,0,0},
{0,1,0,0,0,1,1,1,0,1,0,1,0},
{0,1,0,1,0,1,6,1,0,1,1,1,0},
};
在资源中增加
在构造函数中初始化 bitmap对象
res = ();
bmp = (res, );
在 onDraw中绘图
//用来显示图块的Rect对象
Rect src = newnewnewnew Rect(0, 0, 0, 16);
Rect dst = newnewnewnew Rect();
forforforfor(intintintint i=0; i<13; i++) {
forforforfor(intintintint j=0; j<13; j++) {
//根据Tile的编号得到对应的位置
= (map[i][j]-1) * 16;
= + 16;
//根据地图上的编号计算对应的屏幕位置
= j * 16;
= + 16;
= i * 16;
= + 16;
(bmp, src, dst, paint);
}
}
最后不要忘了修改中的 setContentView
GameView_Old gameView;
/** Called when the activity is first created. */
@Override
publicpublicpublicpublic voidvoidvoidvoid onCreate(Bundle savedInstanceState) {
(savedInstanceState);
gameView = newnewnewnew GameView_Old(thisthisthisthis);
setContentView(gameView);
}
好了,运行一下程序看看结果:
对比一下原图,除了基地四周的砖块之外两者并无二致,可以说我们的 Tile地图实践基本成
功。
现在我们知道了使用 Tile显示地图的原理,实际上,我们不需要每次都很麻烦的写那么多代
码,还记得前面说过的 TiledLayer么?其中早已封装了上述操作。不仅如此,TiledLayer还
能显示动态的地图呢。下面就让我们看一看 TiledLayer的基本用法。其实,它与 Sprite的用
法非常相似(我们需要将 加入到项目中,请使用本章附带程序的
文件,上一章的 TiledLayer并不能正确工作):
这次让我们使用 GameView,把刚刚的数组map拷贝到 GameView中,并声明一个 TiledLayer
类型变量
//背景
TiledLayer backGround;
在构造函数中初始化 TiledLayer
// 背景图
backGround = newnewnewnew TiledLayer(13, 13,
(res,
), 16, 16);
TiledLayer的构造函数有 5个参数,分别是地图的行列数(是以 Tile为单位的),包含 Tile
的 bitmap对象,Tile的宽度和高度。
通过 setCell方法将定义在数组中的 Tile编号传递给 TiledLayer。
forforforfor(intintintint i=0; i<13; i++) {
forforforfor(intintintint j=0; j<13; j++) {
(i, j, map[i][j]);
}
}
最后,只需要在 run函数中调用 paint方法,就可以将 TiledLayer显示出来了。当然,你可
以像控制 Sprite那样控制 TiledLayer显示的位置。
(c);
让我们看一下运行的效果
下面让我们来学习如何实现动态地图。所谓动态地图跟前面讲到的桢动画是一个道理,就是
循环显示几个关键桢。让我们看一下前面 Tiles的图片
我们会发现有两张水域的图片,这就是为动态地图准备的,组合起来之后应该会有如下的效
果:
那么,我们如何在 TiledLayer中实现动态地图呢?TiledLayer为我们准备了这样几个函数:
createAnimatedTile:创建动态图块。很多人会迷惑于这个函数的名字,说是创建动态图块,
可是创建在哪儿啊?创建出来怎么用呢?只有天知道。其实,这个函数的主要功能也就是为
动态图块分配了一个存储结构。你不调用它还会报错,调用了其实也没什么用。
createAnimatedTile返回一个动态图块的编号,从-1开始依次递减,第一次调用返回-1,这样
就分配了一个编号为-1的动态图块。第二次调用会返回-2,依次类推。这个返回值一般没有
用,因为我们做地图的时候肯定已经确定了动态图块的位置,这个编号早就写到了数组中了 。
以后你就可以通过这个编号来控制相应的图块。函数有一个参数,指定一个 Tile的编号,动
态图块最初显示的就是这个 Tile。而正是通过改变这个 Tile来实现动画的。请看下面代码:
(4);
我们初始化了一个动态图块,编号是-1,参数 4表示当前显示 Tile序列图中的第四个 Tile,
就是第一张水域的图片。
setAnimatedTile:动态图块的内容就是使用这个函数改变的。函数的第一个参数是动态图块
的编号,如刚刚的-1。第二个参数是 Tile的编号。
说到这里,大家应该清楚动态地图的用法了吧:
首先在地图数组中确定需要显示动态图块的位置,填入相应的编号,例如我们将上一张地图
的第三行增加三块水域
intintintint[][] map = {
{0,0,0,2,0,0,0,2,0,0,0,0,0},
{0,1,0,2,0,0,0,1,0,1,0,1,0},
{0,1,-1,-1,-1,0,1,1,0,1,2,1,0},
{0,0,0,1,0,0,0,0,0,2,0,0,0},
{3,0,0,1,0,0,2,0,0,1,3,1,2},
{3,3,0,0,0,1,0,0,2,0,3,0,0},
{0,1,1,1,3,3,3,2,0,0,3,1,0},
{0,0,0,2,3,1,0,1,0,1,0,1,0},
{2,1,0,2,0,1,0,1,0,0,0,1,0},
{0,1,0,1,0,1,1,1,0,1,2,1,0},
{0,1,0,1,0,1,1,1,0,0,0,0,0},
{0,1,0,0,0,1,1,1,0,1,0,1,0},
{0,1,0,1,0,1,6,1,0,1,1,1,0},
};
然后在 GameView 的构造函数中初始化 TiledLayer,除了在 setCell 之前调用
createAnimatedTile之外,没有其他区别。
最后就是在 run函数中调用 setAnimatedTile,不断地改变图块了
ifififif((-1) == 4) {
(-1, 5);
} elseelseelseelse {
(-1, 4);
}
(c);
来让我们看一下运行的效果
其实笔者觉得TiledLayer完全也可以像Sprite那样使用桢序列和 nextFrame来实现动态效果,
似乎更易用一些,有兴趣的读者可以自己修改 TiledLayer实现这个功能。
到这里,我们已经掌握了显示地图的方法,但是,这个地图还不能真正运用到我们的游戏中 。
读者肯定也看到了,在 TiledLayer的第一个例子中,我们的坦克可以穿墙而过,显然,这个
地图还缺少最基本的功能——阻挡。
有一种简单的方案可以实现阻挡,让我们看一下 Sprite类,其中有一个方法:
publicpublicpublicpublic finalfinalfinalfinal booleanbooleanbooleanboolean collidesWith(TiledLayer t, booleanbooleanbooleanboolean
pixelLevel)
检测 Sprite与 TiledLayer的碰撞。这种检测是以 Tile为单位的,当与 Sprite重合的 Tiles编
号不为 0时函数返回 true,否则返回 false。
下面让我们做一个小例子测试一下:
打开 GameView_Old,增加一个 Sprite类型的成员变量
// 主角
Sprite player;
在构造函数中初始化 player
// 初始化主角
player = newnewnewnew Sprite((res,
), 16, 16);
(newnewnewnew intintintint[] { 0, 1 });
在 onDraw中绘制 player
(c);
(c);
在 onKeyDown中控制 Sprite的运动
@Override
publicpublicpublicpublic booleanbooleanbooleanboolean onKeyDown(intintintint keyCode, KeyEvent event) {
// TODOTODOTODOTODO Auto-generated method stub
switchswitchswitchswitch (keyCode) {
casecasecasecase _DPAD_UP:
x = ();
y = ();
(0, -16);
ifififif(!(backGround, falsefalsefalsefalse)) {
y -= 16;
}
(_NONE);
(x, y);
breakbreakbreakbreak;
casecasecasecase _DPAD_DOWN:
x = ();
y = ();
(0, 16);
ifififif(!(backGround, falsefalsefalsefalse)) {
y += 16;
}
(_ROT180);
(x, y);
breakbreakbreakbreak;
casecasecasecase _DPAD_LEFT:
x = ();
y = ();
(-16, 0);
ifififif(!(backGround, falsefalsefalsefalse)) {
x -= 16;
}
(_ROT270);
(x, y);
breakbreakbreakbreak;
casecasecasecase _DPAD_RIGHT:
x = ();
y = ();
(16, 0);
ifififif(!(backGround, falsefalsefalsefalse)) {
x += 16;
}
(_ROT90);
(x, y);
breakbreakbreakbreak;
}
postInvalidate(); // 通知系统刷新屏幕
returnreturnreturnreturn (keyCode, event);
}
可以看到,坦克只能在空白区域运动,这回不能上墙了。
但是这种方法还是比较粗糙的,很多时候不能实现我们的目的,比如坦克大战中,水和砖头
是坦克不能通过的,但是掩体是可以通过的,还有子弹可以通过水域,这时候还是检测 Tile
的编号来的准确些。就是说,我们事先确定好那些编号的 Tile可以通过,哪些不能。然后模
仿 collidesWith方法根据 Tank的位置取得它下一步要到达的 Tile的编号,并判断坦克是否
被阻挡。
让我们用这个方案重写 onKeyDown方法(为了简化教程,我们假设每次 player和 tile都是
完全重合的):
首先我们先定义一个函数用来判断 Tank是否可以通过
// 判断坦克是否可以通过
privateprivateprivateprivate booleanbooleanbooleanboolean tankPass(intintintint x, intintintint y) {
// 不超过地图范围
ifififif (x < 0 || x > 12 * 16 || y < 0 || y > 12 * 16) {
returnreturnreturnreturn falsefalsefalsefalse;
}
intintintint tid = map[y / 16][x / 16];
ifififif (tid == 1 || tid == 2 || tid == -1)
returnreturnreturnreturn falsefalsefalsefalse;
returnreturnreturnreturn truetruetruetrue;
}
然后在 onKeyDown中运用新的方案
@Override
publicpublicpublicpublic booleanbooleanbooleanboolean onKeyDown(intintintint keyCode, KeyEvent event) {
// TODOTODOTODOTODO Auto-generated method stub
switchswitchswitchswitch (keyCode) {
casecasecasecase _DPAD_UP:
x = ();
y = ();
(0, -16);
ifififif (tankPass((), ())) {
y -= 16;
}
(_NONE);
(x, y);
breakbreakbreakbreak;
casecasecasecase _DPAD_DOWN:
x = ();
y = ();
(0, 16);
ifififif (tankPass((), ())) {
y += 16;
}
(_ROT180);
(x, y);
breakbreakbreakbreak;
casecasecasecase _DPAD_LEFT:
x = ();
y = ();
(-16, 0);
ifififif (tankPass((), ())) {
x -= 16;
}
(_ROT270);
(x, y);
breakbreakbreakbreak;
casecasecasecase _DPAD_RIGHT:
x = ();
y = ();
(16, 0);
ifififif (tankPass((), ())) {
x += 16;
}
(_ROT90);
(x, y);
breakbreakbreakbreak;
}
postInvalidate(); // 通知系统刷新屏幕
returnreturnreturnreturn (keyCode, event);
}
现在让我们运行一下看看吧,这回终于能够达到了我们想要的效果,我们的坦克正藏在掩体
下面,并且它不能通过墙和水域。
到此为止,关于地图的内容就讲解完毕了。这些内容并不复杂,首先介绍了使用 Tile组成地
图的原理,然后介绍了 TiledLayer已经动态地图,最后演示了阻挡的实现方法。本章的大部
分例子使用了 GameView_Old,并没有使用游戏循环,也没有涉及到地图的平滑滚动,在以
后需要的时候会补充这部分知识。
前言
搭建OPhone开发环境