Android 游戏开发.docx
《Android 游戏开发.docx》由会员分享,可在线阅读,更多相关《Android 游戏开发.docx(23页珍藏版)》请在冰豆网上搜索。
Android游戏开发
Android游戏开发——基础游戏框架
作者:
宋云
前言:
一直在稀里糊涂的做着游戏开发,也没有什么像样的成品。
虽然如此,但是在每次的开发过程中总是有所体会,再根据以往的编程经验,一个非常基本的游戏框架,就这样写出来了。
理论上用此框架是可以制作很多的游戏了,开发者只需要关注游戏逻辑就可以了。
但是,需要强调的是理论上!
读者需要认真思考,我的框架中一定有很多错误的。
毕竟该框架还过于简单,或许框架根本就不需要很复杂,因为大多数事情Android的API里面都做好了,其实开发工作已经很简单了。
那么,我们就当做一个Android游戏的框架只有这么简单,请跟着我进入我的游戏框架的世界吧!
PS:
需要学习Android游戏开发基础的朋友,可以阅读本人所写的Android游戏开发基础一文,简要的介绍了要进行Android游戏开发所需要掌握的最基本的技术。
阅读地址:
目录
一、框架结构1
1.1精灵Spirit1
1.2窗口GameWindow3
1.3视图GameView5
1.4游戏中的GameActivity6
1.5游戏线程GameThread7
1.6子状态类MiniGameThread8
二、框架进阶9
2.1发给Activity的消息9
2.2接口的妙用10
2.3精灵的碰撞检测11
2.4关于多平台游戏框架开发的思路11
三、使用该游戏框架实例11
一、框架结构
游戏框架?
!
什么是游戏框架?
!
框架里面要完成什么样的任务?
一连串的问题曾经不断的在我的头脑中思索,久久想不出答案。
后来我想到,一个游戏框架也许就是游戏中最基本的元素。
而一个框架,实现了使用框架的开发者,在编写游戏的过程中只需要考虑游戏逻辑,而不需要考虑怎样绘制、怎样传递系统消息等平台相关的问题。
是的,这样一个框架是最优秀的,我虽然不敢说我的游戏框架已经达到了完全不需要考虑游戏运行平台,但是我敢说它已经很接近了,我甚至感觉我自己已经触摸到了多平台游戏框架开发的那扇门,也许再有两年,三年我也可以开发一个像Unity3D那样的,至少是2D游戏的一套API库。
那是一件多么有成就感的事情啊。
独立游戏开发者的路,非常的艰辛,我真诚的希望此篇文章能够帮助还在摸索游戏开发的朋友。
能给您带来哪怕一丁点的启示,我也是十分满足了。
1.1精灵Spirit
其实AndroidAPI中有精灵这个类,但是用着不是很喜欢自己写一个吧虽然可能不如它写的好,但是有了这个经验,再做一些其他平台上的扩展时,就方便了。
我们先来想一想,这个精灵写好之后应该是什么样子的。
比如我们构造精灵对象的时候,构造函数是什么样的?
首先应该确定精灵动画的素材,一般都是一张图片里面有每一帧动画的内容。
那么一定要把这个图片资源给精灵,那么当然也要告诉精灵这张图片资源的布局是几帧乘以几帧的。
还要告诉精灵他的初始位置,显示动画的频率,那么好,构造函数就出来了:
publicSpirit(float x,
float y,
int w,
int h,
Bitmap bitmap,
long uptime)
参数:
x-相对于上层窗口的偏移坐标x;relativetofatherwindow'spositionX
y-相对于上层窗口的偏移坐标y;relativetofatherwindow'spositionY
w-位图横向单位数目;thenumberoflandscapeunitinbitmap
h-位图纵向单位数目;thenumberofportraitunitinbitmap
bitmap-动画位图;theanimationbitmap
uptime-更新每一帧的时间单位是毫秒;thetimetoupdateframebymillisecond
构造函数里没有什么运算,都是对参数的一些赋值。
这些参数一般都选取为这个类必不可少的参数,每一个对象都一定会用到这几个参数,那么作为构造函数需要初始化的内容是非常合适的。
精灵的图片素材里面是一帧一帧的信息,每一帧的格式是一样的,所以通过数字可以确定这一帧在图片中的位置,例如0帧的位置,左上角是(0*w,0*h),右下角的位置是(0*w+w,0*h+h))。
因此要显示的动画内容可以用一个数组赋值。
因此,publicvoidsetAnimations(int[] animations),赋值动画内容的函数是需要的。
这个函数在每次需要显示动画或者更改要显示的动画之前调用即可。
一个新创建的精灵对象不一定就需要显示具体的动画内容,再者很多精灵频繁的变化动画内容,因此这个函数是很需要的。
在绘制精灵是,一个非常值得考虑的问题就是优先级别,哪个精灵先绘制哪个后绘制,也就是说哪些精灵是可以被另一些精灵所覆盖的。
因此需要有优先级别这个参数level,默认为0。
显示精灵动画时,有些情况需要重复的显示动画;有些需要显示完一次动画精灵就消失;有的则需要定格在某一帧。
因此告诉精灵要开始显示动画时必须通知精灵这两个信息。
publicvoidplay(boolean loop,
int endState)
参数:
loop-决定动画是否循环
endState-决定动画的终止状态STOP表示停止在最终帧OVER表示不显示动画
如同所有的游戏开发中常用到的两个函数一样,精灵也需要这两个函数。
即
publicvoidupdate(long time)函数还有publicvoiddraw(Canvas canvas)函数。
update控制精灵数据的更新,也就是根据显示频率更新帧数,并且判断循环绘制、终止、定格状态;draw绘制精灵到上层容器的画布上。
这两个函数的源码就像这样:
publicvoidupdate(longtime){
if(state!
=PLAYING)return;
nowTime+=time;
if(nowTime>updateTime){
frame++;
if(frame>=animations.length){
if(loop){
frame=0;
}else{
frame--;
state=this.endState;
}
}
nowTime=0;
drwR=this.getDrawRect();
}
}
随着时间更新帧数,如果循环则从0帧继续播放,否则定格在终止帧,设置当前状态为play函数中给出的endState,如果是OVER则不再绘制不再更新数据,上层容易获取到精灵的状态时OVER则可以释放精灵资源;如果是STOP则绘末帧不再更新数据。
其中getDrawRect()是通过当前帧运算源矩形位置的函数。
publicvoiddraw(Canvascanvas){
if(state==PLAYING||state==STOP){
canvas.drawBitmap(bitmap,drwR,dstR,null);
}
}
privateRectgetDrawRect(){
Rectrect=newRect();
intf=animations[frame];
intr=f/w;//运算在第几行
intc=f%w;//运算在第几列
rect.left=c*w_size;
rect.top=r*h_size;
rect.right=rect.left+w_size;
rect.bottom=rect.top+h_size;
returnrect;
}
这样,精灵最主要的东西已经都讲完了,相信读者也能够自己将没有写出的参数和方法补齐。
1.2窗口GameWindow
窗口GameWindow这个类的提出,是为了模拟应用中一个个窗口的实现。
例如游戏中会出现的技能窗口,背包窗口,各种信息窗口,对话窗口等等。
随着Window类的实现,发现此类也可以适用于相耦合精灵的显示。
例如一个玩家有其形象,随着装备的变化精灵的显示也会有不同的变化,并且在使用技能、动作等时也许还会有一些特效。
那么将一个玩家、他的武器、装备、特效都放到一个窗口中,这一堆得精灵就被组合在一起了。
显示也更方便,只需要更改此窗口的偏移坐标即可。
对于一个窗口位置和尺寸是必须的,背景可有可无,没有背景的背景则默认是ARGB=0x00000000,即透明的黑色。
对于背景的设置用drawColor方法比较好,制定为清屏CLEAR模式。
同时有其他需求也可以是XOR等等模式。
构造方法如下:
publicGameWindow(float x,
float y,
float w,
float h,
Bitmap background)
参数:
x-相对上层View的偏移坐标x;relativetofatherview'spositionX
y-相对上层View的偏移坐标y;relativetofatherview'spositionY
w-窗口的宽;thewidthofwindow
h-窗口的高;theheightofwindow
background-窗口的背景位图;thebackgroundofwindow(canbenull)
对于窗口,同样有非常重要的level属性,来决定窗口的优先等级。
下面来讲解一下窗口中管理精灵等级的方法。
假设一个窗口中最多只有100个精灵。
那么精灵用一个数组存储,Spiritspirits[]=newSpirit[100]。
根据优先级和建时间将精灵排序,在数组前面的精灵优先进行数据更新,并且显示在更上方。
也就是说按照顺序更新精灵,但是按照倒序显示精灵。
每次向Window对象中添加精灵使用插入排序方法,具体代码如下:
publicbooleanaddSpirit(Spiritspirit){
if(getSpiritsNum()==spirits.length)returnfalse;
for(inti=0;iif(spirits[i]==null){
spirits[i]=spirit;
returntrue;
}
if(spirit.getLevel()>=spirits[i].getLevel()){
for(intj=spirits.length-1;j>i;j--){
spirits[j]=spirits[j-1];
}
spirits[i]=spirit;
returntrue;
}
}
returnfalse;
}
返回true表示插入成功,返回false表示失败原因是该窗口已经存在上限100个精灵。
作为一个窗口,最主要的功能已经介绍完了,下面扩展一个附加功能,在平时使用过的应用里窗口都是可以拖拽移动位置的,因此这个窗口也要有同样的功能。
因此要写一个检测拖拽信息的类checkTouchEvent,在上级的View的TouchEvent函数中调用次方法,具体的checkTouchEvent方法实现如下:
publicbooleancheckTouchEvent(MotionEventevent){
for(inti=0;iif(spirits[i]!
=null){
if(spirits[i].checkListener(event))returntrue;
}
}
intaction=event.getAction();
intp=event.getPointerCount();
floatx=event.getX(p-1);
floaty=event.getY(p-1);
RectFrect=newRectF(offX,offY,offX+width,offY+height);
if(GameView.maskAction(action)==MotionEvent.ACTION_DOWN){
if(rect.contains(x,y)){
downX=x;
downY=y;
returntrue;
}
}elseif(GameView.maskAction(action)==MotionEvent.ACTION_UP){
if(rect.contains(x,y)){
returntrue;
}
}elseif(GameView.maskAction(action)==MotionEvent.ACTION_MOVE){
if(canDrag&&rect.contains(x,y)){
floatdx=x-downX;
floatdy=y-downY;
downX=x;
downY=y;
offX+=dx;
offY+=dy;
returntrue;
}
}
returnfalse;
}
第一个循环是将消息按照正序传递给精灵数组,如果这个消息是属于精灵的那么将不对本身进行判断检测。
窗口的绘制也是需要重点强调的部分,首先为了体现出窗口的特性,我们创建一个缓存位图并且获得起相应的画布
buffBitmap=Bitmap.createBitmap((int)width,(int)height,
Bitmap.Config.ARGB_8888);
buffCanvas=newCanvas(buffBitmap);
绘制的时候先绘制到buffCanvas上然后将buffBitmap再绘制到上级的画布上。
其次清屏的时候要使用drawColor函数,采用CLEAR模式否则两窗口的重叠部分将不能正确显示。
具体drawColor方式还有很多其他的模式,感兴趣的读者可以自行查阅AndroidAPI文档。
绘制代码如下:
publicvoiddraw(Canvascanvas){
buffCanvas.drawColor(0,PorterDuff.Mode.CLEAR);
if(background!
=null){
buffCanvas.drawBitmap(background,0,0,null);
}
for(inti=spirits.length-1;i>=0;i--){
if(spirits[i]!
=null)spirits[i].draw(buffCanvas);
}
buffCanvas.save(Canvas.ALL_SAVE_FLAG);
buffCanvas.restore();
canvas.drawBitmap(buffBitmap,offX,offY,null);
}
1.3视图GameView
GameView类既是SurfaceView的衍生类,其内容与本人之前写过的《Andorid游戏开发基础》中介绍SurfaceView时所写的衍生类GameSurfaceView基本相同,唯一不同的就是这里的GameView是在Window的基础上开发,其结构更加简单只需要管理Window数组,而不需要管理精灵的过程。
SurfaceView类这里就不再介绍了,基础的内容请查阅《Andorid游戏开发基础》一文。
基于Window基础之上,那么就不用关心精灵的绘制与更新了。
View内包含一个Window数组即可,假设一个View内最多不会超过10个Window,声明Window数组:
GameWindowwindows[]=newGameWindow[10]。
排序方式与Window中的Spirit相同,根据优先等级和创建时间排序,添加窗口时采用插入排序,添加过程函数如下:
publicbooleanaddWindow(GameWindowwindow){
if(getWindowsNum()==windows.length)returnfalse;
for(inti=0;iif(windows[i]==null){
windows[i]=window;
returntrue;
}
if(window.getLevel()>=windows[i].getLevel()){
for(intj=windows.length-1;j>i;j--){
windows[j]=windows[j-1];
}
windows[i]=window;
returntrue;
}
}
returnfalse;
}
如果返回false则添加失败,说明当前View已经存在满10个窗口。
正序更新逆序绘制。
删除过程如下,当然Window类中Spirit中删除的过程也是这样的:
publicbooleanremoveWindow(GameWindowwindow){
for(inti=0;iif(windows[i]==window){
for(intj=i;jwindows[j]=windows[j+1];
}
windows[windows.length-1]=null;
returntrue;
}
}
returnfalse;
}
1.4游戏中的GameActivity
一个GameActivity类需要做的事情很少,最主要的事情就是加载一个GameView并且启动一个游戏主线程就可以了,附带的判定一下设备屏幕的分辨率等级,以备动态适应设备尺寸之用。
声明的参数如下:
publicstaticfloatREAL_WIDTH=480f;
publicstaticfloatREAL_HEIGHT=320f;
publicstaticintSCREEN_LEVEL=0;
publicstaticintLOW=0;
publicstaticintMIDDLE=1;
publicstaticintHIGH=2;
构造函数中设置了无标题和全屏并且判断了屏幕分辨率等级:
//无标题
requestWindowFeature(Window.FEATURE_NO_TITLE);
//全屏模式
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
REAL_WIDTH=this.getWindowManager().getDefaultDisplay().getWidth();
REAL_HEIGHT=this.getWindowManager().getDefaultDisplay().getHeight();
SCREEN_LEVEL=getScreenLevel(REAL_WIDTH,REAL_HEIGHT);
其中getScreenLevel函数就是判断了一下分辨率是240x320的还是320x480的还是480x800哪个等级的。
而GameView的初始化已经设定交给GameThread处理GameThead的启动交给GameActivity的具体子类中的构造函数。
1.5游戏线程GameThread
有了以上的设计,我们可以想象一下我们游戏的主线程需要是什么样子的。
一个游戏我们只需要1个Activity和1个GameView那么我们需要多个GameWindow每个GameWindow中有多个Spirit。
整个框架的结构,看似就这么简单。
那么如果我定义了一个主线程类,这个类初始化的时候必然要保存该应用的Activity对象,再定义一个GameView并且设置为主要视图,同时我还初始化了资源管理类和数据管理类。
GameThread作为线程,我采用状态控制的方法来管理程序的运行。
publicvoidrun(){
//TODOAuto-generatedmethodstub
while(controlLoop){
try{
Thread.sleep(150);
}catch(InterruptedExceptione){
//TODOAuto-generatedcatchblock
e.printStackTrace();
}
if(state==INIT){
init();
state=BEGIN_OTHER;//实际设置成其他状态
//gv.removeWindow(logoWindow);//进入下一状态需要做的清理工作
}elseif(state==BEGIN_OTHER){//其他状态时要做的事情,每一个其他状态都一个开始的准备,这里进行准备的调用
//调用OTHER的开始准备
state=OTHER;
}elseif(state==OTHER){//如果其他状态需要进行数据更新,在这里进行
}
}
}
一个线程所有要做的事情,都要写在run函数中,基于状态控制的线程,需要先写一个死循环(最好又一个只有在停止程序是才会被赋值为true的参数控制,这样更安全可控)然后根据不同的状态执行不同的代码。
作为游戏来讲,第一个状态必然是INIT在这个状态的时候,初始化所有素材和数据。
因此每一个子状态(对于游戏来讲就是每一个使用不同GameWindow的界面都需要写一个初始化函数,以供INIT状态调用),同时在每一个子状态要执行之前需要有一些准备活动,因为这些子状态可能被重复多次从其他状态转移过来(例如游戏中经常遇到的重新开始操作,或者从游戏中转到主菜单,或者在游戏