学了好一些Android知识,拿这个例子来统一下,并且熟悉下游戏整个框架是怎么做的:
我们要做的是一个类似雷电的飞行射击游戏,由于现在的智能手机都不配备数字键盘和方向键了,所以书本的代码照写是跟不上时代了,只有看懂的基础上再做修改,这样也表明你真是看懂了而不是一味看书照写的,从一个实例中学习是最有效的方法了,这篇博文的重点也不在于具体的代码,重点我打算放在整体框架,各个类之间的协调关系上,看下一个复杂的游戏到底是怎样划分成各个部分,而这些部分又是怎样关联起来的。
一般步骤:先要划分游戏状态:游戏菜单,游戏进行阶段,胜利阶段,失败阶段,每个阶段对应的视图都不同,这个是怎样实现的呢
首先整体框架是一个Activity,它的内容是我们自己写的SurfaceView,重点就在于我们写的SurfaceView了,之前学习这个View的时候也讲到它的一般框架,在SurfaceHolder的控制下,先create,在create里面创一个子线程来负责画面的刷新,并且执行逻辑部分,那么在子线程的run函数中调用的绘制View的函数里面我们加一个switch语句,针对不同游戏状态执行不同的draw()和logic()
public static final int GAME_MENU=0;//游戏菜单
public static final int GAMEING=1;//游戏中
public static final int GAME_WIN=2;//游戏胜利
public static final int GAME_LOST=3;//游戏失败
public static int GameStatus=GAME_MENU;
SurfaceView的运行过程:
构造函数:
public MySurfaceView(Context context)
{
super(context);
// TODO Auto-generated constructor stub
this.context=context;
sfh=getHolder();
sfh.addCallback(this);
}
↓↓↓↓↓↓
public void surfaceCreated(SurfaceHolder arg0)
{
// TODO Auto-generated method stub
thread=new MyThread();
flag=true;
thread.start();
}
↓↓↓↓↓↓
class MyThread extends Thread
{
@Override
public void run()
{
while(flag)
{
try
{
long start=System.currentTimeMillis();
myDraw();
logic();
long end=System.currentTimeMillis();
if(end-start<50)
{
Thread.sleep(50-(end-start));
}
} catch (Exception e)
{
// TODO: handle exception
}
}
}
}
↓↓↓↓↓↓
public void myDraw()
{
try
{
canvas=sfh.lockCanvas();
if(canvas!=null)
{
switch (GameStatus)
{
case GAME_MENU:
...
break;
case GAMEING:
...
break;
case GAME_WIN:
...
break;
case GAME_LOST:
...
break;
default:
break;
}
}
} catch (Exception e)
{
// TODO: handle exception
}finally{
if(canvas!=null)
{
sfh.unlockCanvasAndPost(canvas);
}
}
}
logic()也想mydraw()一样这样子处理,这样的框架下SurfaceView就能根据不同的游戏状态展示出不同的画面和执行不同的逻辑了!!
总体框架出来了就到各个部分了,每个部分的具体实现我也不打算详细讲,但是有一点是要注意的就是怎样把各个部分从框架拆出来单独搞这个思路一定要知道,做法就是为每个细节部分创造一个类,下面先以游戏菜单这部分为例讲下思路。
首先case GAME_MENU:这种情况下,创建的GameMenu类,从整体框架出发,针对于这个细节部分要实现的就是2处地方:mydraw()和logic(),那么只要在MySurfaceView中建一个GameMenu类对象,case GAME_MENU:的时候就调用GameMenu的draw()和logic()这样子做就行了,其他方面,例如触屏什么的也可以仿照mydraw一样做,对于MySurfaceView的onTouchEvent针对不同情况就调用不同的onTouchEvent就行,把参数传过去就行。
public void myDraw()
{
try
{
canvas=sfh.lockCanvas();
if(canvas!=null)
{
switch (GameStatus)
{
case GAME_MENU:
gameMenu.draw(canvas, paint);
break;
public boolean onTouchEvent(MotionEvent event)
{
// TODO Auto-generated method stub
switch (GameStatus)
{
case GAME_MENU:
gameMenu.onTouchEvent(event);
break;
这里给出GameMenu的完整代码:
package com.example.planeproject;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.MotionEvent;
public class GameMenu
{
private Bitmap bmpmenu,bmpbutton,bmpbutton_press;
private float btnX,btnY,sx,sy;
private boolean ispress;
public GameMenu(Bitmap bmpMenu,Bitmap bmpbutton,Bitmap bmpbutton_press)
{
this.bmpmenu=bmpMenu;
this.bmpbutton=bmpbutton;
this.bmpbutton_press=bmpbutton_press;
btnX=MySurfaceView.screenW/2-bmpbutton.getWidth()/2;
btnY=MySurfaceView.screenH-bmpbutton.getHeight();
sx=(float)MySurfaceView.screenW/(float)bmpmenu.getWidth();
sy=(float)MySurfaceView.screenH/(float)bmpmenu.getHeight();
ispress=false;
}
public void draw(Canvas canvas,Paint paint)
{
canvas.save();
canvas.scale(sx, sy, 0, 0);
canvas.drawBitmap(bmpmenu, 0, 0, paint);
canvas.restore();
if(!ispress)
canvas.drawBitmap(bmpbutton, btnX, btnY, paint);
else {
canvas.drawBitmap(bmpbutton_press, btnX, btnY, paint);
}
}
public void onTouchEvent(MotionEvent event)
{
float x=event.getX();
float y=event.getY();
if(event.getAction()==MotionEvent.ACTION_DOWN || event.getAction()==MotionEvent.ACTION_MOVE)
{
if(x>btnX && x<btnX+bmpbutton.getWidth())
{
if(y>btnY && y<btnY+bmpbutton.getHeight())
{
//Touch在按钮区域
ispress=true;
}
else {
ispress=false;
}
}
else
ispress=false;
}
if(event.getAction()==MotionEvent.ACTION_UP)
{
if(x>btnX && x<btnX+bmpbutton.getWidth())
{
if(y>btnY && y<btnY+bmpbutton.getHeight())
{
//Touch在按钮区域
ispress=false;
MySurfaceView.GameStatus=MySurfaceView.GAMEING;
}
}
}
}
}
这个类中可以学到的技巧就是怎样令你的图片适应屏幕大小:利用画布的伸缩函数,把屏幕大小和位图大小的比作为比例因子就行(这点书上是没的,不缩放,换台手机就比例失调的)
canvas.save();
canvas.scale(sx, sy, 0, 0);
canvas.drawBitmap(bmpmenu, 0, 0, paint);
canvas.restore();
同样的做法,游戏胜利和游戏失败都是展示一张图片,比较简单,实现draw()就行
比较复杂和技巧比较多的就是游戏进行中的画面,游戏进行过程中要处理的要素很多,很乱,包括主角,怪物(怪物又分几种),Boss,主角的子弹,怪物的子弹,Boss的子弹,主角碰到怪物,主角碰到怪物子弹,怪物碰到主角子弹,怪物产生爆炸效果等等。
首先不要乱,一样一样理好顺序:
主角(主角机,主角HP,主角的移动方式,Player类)----->怪物(把几种怪物写到同一个类中,Enemy类,可以增加一个type属性,用type来判断当前Enemy类是什么怪物,利用switch根据type的值来处理不同的逻辑,例如根据不同的type传不同的怪物位图,每次logic时执行不同的x,y坐标变化)------->处理怪物和主角的碰撞-------->添加子弹类--------->处理子弹与怪物,主角碰撞--------->添加爆炸类------------>在子弹与怪物碰撞的处理中添加上爆炸类实现爆炸效果---------->添加Boss类
整个流程就是这个样子了,具体的实现不打算详细讲了,讲讲实现时一些技巧吧:
(1)怎样实现人物的移动
利用canvas的clipRect弄一个剪裁区域,在那个剪裁区域不同播放不同帧的位图就有动画效果,在这个基础上在logic那里加上剪裁区域的坐标变化就能实现人物的移动,使用与主角,怪物,子弹,爆炸所有类的效果实现。
(2)怎样实现怪物AI(包括一次出几只怪,怪的种类,出新怪的时候旧怪还在的效果)
这个真是好好的技巧,我之前一直都是不会处理这个的,首先我们知道SurfaceView是展示东西,负责刷新画面的其实是另起的一个线程,线程每50ms运行一次,我们叫它做一帧,我们可以设一个计数器变量,一个int createEnemyTime,每一帧的时候计数器加1,如果检测到计数器的值等于createEnemyTime就产生怪。
怎么产生怪,又是另一个技巧了:首先设一个Vector<Enemy>对象,这个对象保存当前所有活着的怪,那次mydraw的时候循环一次这个vector,把里面每个Emeny对象的draw函数都调用一次,这样就能维持旧怪的存在,增加新怪当然就是往vector加对象了,至于怎加就看你自己的设计了
(3)怎样检测碰撞
首先矩形碰撞的检测:矩形1在矩形2左边,右边,上边,下边这4种情况就是没碰撞,除此之外都是碰撞
public boolean isCollsionWith(Enemy enemy)
{
float x2=enemy.x;
float y2=enemy.y;
int w2=enemy.frameW;
int h2=enemy.frameH;
if(!iscollsion)
{
if(x2<x && x2+w2<x)
{
return false;
}
else if(x2>x && x2>x+bmpplayer.getWidth())
{
return false;
}
else if(y2<y && y2+h2<y)
{
return false;
}
else if(y2>y && y2>y+bmpplayer.getHeight())
{
return false;
}
else
{
iscollsion=true;
return true;
}
}
else {
return false;
}
}
(4)怎样实现子弹类
子弹类就是简单的在相应的位置画出相应的子弹位图,再在logic那里修改子弹的位置为下一次的位置。在SurfaceView那里维护一个Vector<Bullet>对象,保存所有当前还在画面内的子弹,这样旧子弹就会依旧沿着原来的轨迹前进,新子弹也能顺利出发。
(5)怎样判断子弹与主角,怪物之间的碰撞
主角与怪物子弹Vector的每一个Bullet对象进行矩形碰撞检测,碰撞了就扣血,怪物和主角子弹Vector的每一个对象进行矩形碰撞检测,碰撞了往爆炸Vector里面增加爆炸对象播放动态效果,这里讲下子弹,怪物他们都可以通过坐标的位置来判断是否应该画来控制是否从vector中取出,爆炸类可以增加一个isPlayEnd标志来表明这个爆炸效果是否已经播放完毕,完了就从爆炸vector中取出,下一帧就不会画它了。
(6)BACK键的处理
当游戏失败,胜利或者游戏中按BACK键的时候应该把GameStatus改回菜单状态,如果是菜单状态下按back就把Activity 给finish掉,然后System.exit(0);结束程序
public boolean onKeyDown(int keyCode, KeyEvent event)
{
// TODO Auto-generated method stub
if(keyCode==event.KEYCODE_BACK)
{
if(mySurfaceView.GameStatus==mySurfaceView.GAMEING || mySurfaceView.GameStatus==mySurfaceView.GAME_WIN || mySurfaceView.GameStatus==mySurfaceView.GAME_LOST)
{
mySurfaceView.GameStatus=mySurfaceView.GAME_MENU;
mySurfaceView.initGame();
mySurfaceView.emenyArrayIndex=0;
mySurfaceView.isBoss=false;
}
else if(mySurfaceView.GameStatus==mySurfaceView.GAME_MENU)
{
MainActivity.this.finish();
System.exit(0);
}
}
return true;
}