写个简单的飞机游戏玩玩
侯亮
1 概述
前些天看了《Android游戏编程之从零开始》1书中1个简单飞机游戏的实现代码,1时手痒,也写了1个练练手。虽然我的本职工作其实不是写游戏,不进程序员或多或少都有编写游戏的情结,那就写吧,Just for fun!游戏的代码部份我基本上全部重写了,至于游戏的图片资源嘛,我老实不客气地全拿来复用了1下,呵呵,希望李华明先生不要见怪啊。
在Android平台上,SurfaceView就足以应付所有简单游戏了。固然我说的是简单游戏,如果要写复杂游戏,恐怕还得使用各种游戏引擎,不过游戏引擎不是本文关心的重点,对我写的简单游戏来讲,用SurfaceView就能够了。
飞机游戏的1个小特点是,画面总是在变动的,这固然是句空话,不过却能引出1个关键的设计核心,那就是“帧流”。帧流的最典型例子大概就是电影啦,我们知道,只要胶片按每秒钟24帧(或更高)的速率播放,人眼就会误以为看到了连续的运动画面。飞机游戏中的运动画面大体也是这样显现的,因此游戏设计者必须设计出1条平滑的帧流,并且帧率要足够快。
从技术上说,我们可以在1个线程中,构造1个不断绘制“帧”的while循环,并在每次画好帧后,调用Thread.sleep()睡眠适合的时间,这样就能够实现1个相对平滑的帧流了。
另外一方面,游戏的逻辑也是可以融入到帧流里的,也就是说,每次画好帧后,我们可以调用1个类似execLogic()的函数来履行游戏逻辑,从而(间接)产生新的帧。而游戏逻辑又可以划分成多个子逻辑,比如关卡背景逻辑、敌人行动逻辑、玩家飞机逻辑、子弹行动逻辑、碰撞逻辑等等,这个我们后文再细说。
大概说起来就是这么多了,现在我们逐一来看游戏设计中的细节。
2 平滑的帧流
我们先写个全屏显示的Activity:
public class HLPlaneGameActivity extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(new PlaneGameView(this));
}
}
这个Activity的主视图是PlaneGameView类,它继承于SurfaceView。
public class PlaneGameView extends SurfaceView implements Callback, Runnable
1旦surface创建成功,我们就启动1个线程,这个线程负责运作帧流。
@Override
public void surfaceCreated(SurfaceHolder holder)
{
GlobalInfo.screenW = getWidth();
GlobalInfo.screenH = getHeight();
mSurfaceWorking = true;
mGameManager = new GameManager(getContext());
mGameThread = new Thread(this);
mGameThread.start();
}
mGameThread线程的核心run()函数的代码以下:
@Override
public void run()
{
while (mSurfaceWorking)
{
long start = System.currentTimeMillis();
drawFrame(); // 画帧!
execLogic(); // 履行所有游戏逻辑!
long end = System.currentTimeMillis();
try
{
if (end - start < 50)
{
Thread.sleep(50 - (end - start)); // 睡眠适合的时间!
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
画帧、游戏逻辑、适合的sleep,1气呵成。为了便于计算,此处我采取了每秒20帧的帧率,所以每帧平均50毫秒,而且由于画帧和履行游戏逻辑都是需要消耗时间的,所以适合的sleep()动作应当写成:Thread.sleep(50 - (end - start))。
3 GameManager
3.1 整合游戏中所有元素
为了便于管理,我设计了1个GameManager管理类。这个类究竟是干甚么的呢?简单地说,它整合了游戏中的所有元素,目前有:
- 绘制关卡背景;
- 所有敌人;
- 爆炸殊效;
- 所有子弹、炮弹;
- 玩家(player)飞机;
- 游戏信息面板;
固然,以后还可以再扩大1些东西,它们的机理是接近的。
GameManager的代码截选以下:
public class GameManager
{
private Context mContext = null;
private GameStage mCurStage = null;
private Player mPlayer = null;
private EnemyManager mEnemyMgr = null;
private BulletsManager mPlayerBulletsMgr = new BulletsManager();
private BulletsManager mEnemyBulletsMgr = new BulletsManager();
private ExplodeManager mExplodeMgr = null;
private GameInfoPanel mGameInfoPanel = null;
GameManager的总模块关系示意图以下:
既然在“帧流”线程里最重要的动作是drawFrame()和execLogic(),那末GameManager类也必须提供这两个成员函数,这样帧流线程只需直接调用GameManager的同名函数便可。
3.2 GameManager的画帧动作
帧流线程的drawFrame()函数,其代码以下:
public void drawFrame()
{
Canvas canvas = null;
try
{
canvas = mSfcHolder.lockCanvas();
if (canvas == null)
{
return;
}
mGameManager.drawFrame(canvas);
}
catch (Exception e)
{
// TODO: handle exception
}
finally
{
if (canvas != null)
{
mSfcHolder.unlockCanvasAndPost(canvas);
}
}
}
其中GameManager的drawFrame()函数以下:
public void drawFrame(Canvas canvas)
{
mCurStage.drawFrame(canvas);
mEnemyMgr.drawFrame(canvas);
mExplodeMgr.drawFrame(canvas);
mPlayerBulletsMgr.drawFrame(canvas);
mEnemyBulletsMgr.drawFrame(canvas);
mPlayer.drawFrame(canvas);
mGameInfoPanel.drawFrame(canvas);
}
不过是调用所有游戏角色的drawFrame()而已。
每一个游戏角色有自己的存活期,在其存活期中,可以通过drawFrame()向canvas中的适合位置绘制相应的图片。示意图以下:
在上面的示意图中,两个enemy的生存期都只有5帧,当帧流绘制到上图的紫色帧时,会先绘制enemy_1的第1帧,而后绘制enemy_2的第5帧,最后绘制player确当前帧。(固然,这里我们只是简单论述原理,大家如有兴趣,可以再在这张图上添加其他的游戏元素。)绘制终了后的终究效果,就是屏幕展现给用户的终究画面。
每一个游戏角色都非常清楚自己当前应当如何绘制,而且它通过履行自己的子逻辑,决定出下1帧该如何绘制,这就是游戏中最重要的画帧流程。
3.3 GameManager管理所有的子逻辑
其实,游戏的整体运作是由两个方面带动的,1个是“软件内部控制”,主要控制所有“非player角色”的移动和动作,比如每一个enemy下1步移动到哪里,如何发射子弹等等;另外一个是“用户操作”,主要控制“player角色”的移动和动作(这部份我们放在后文再说)。在前文所说的帧流线程里,是通过调用GameManager的execLogic()来完成所有“软件内部控制”的,其代码以下:
public void execLogic()
{
mCurStage.execLogic();
mEnemyMgr.execLogic();
mPlayer.execLogic();
mPlayerBulletsMgr.execLogic();
mEnemyBulletsMgr.execLogic();
mExplodeMgr.execLogic();
mGameInfoPanel.execLogic();
execCollsionLogic(); // 碰撞逻辑
}
从上面代码就能够看出,GameManager所管理的子逻辑大概有以下几个:
- 关卡运作子逻辑
- 所有敌人的运作子逻辑
- 玩家角色的子逻辑
- 玩家发射的子弹的子逻辑
- 敌人发射的子弹的子逻辑
- 管理爆炸效果的子逻辑
- 游戏信息面板的子逻辑
- 碰撞子逻辑
4 游戏子逻辑
4.1 关卡运作子逻辑――GameStage
我们先看前面execLogic()函数里的第1句:mCurState.execLogic(),这个mCurState是GameStage类型的,这个类主要保护当前关卡的相干数据。目前这个类非常简单,只保护了关卡背景图和本关enemy的出现顺序表。
4.1.1 关卡背景图由StageBg类处理
1般来讲,飞机游戏的背景是不断转动的。为了实现转动效果,我们可以绘制1张比屏幕长度更长的图片,并首尾相接地循环绘制它。
在StageBg里,mBackGroundBmp1和mBackGroundBmp2这两个域其实指向的是同1个位图对象,之所以写成两个域,是为了代码更容易于浏览。另外,mBgScrollSpeed用于表示背景转动的速度,我们可以通过修改它,来体现飞行的速度。
4.1.2 关卡中的敌人的出场安排
GameStage的另外一个重要职责是向游戏的主控制器(GameManager)提供1张表示敌人出场顺序的表,为此它提供了getEnemyMap()函数:
public int[][] getEnemyMap()
{
// ENEMY_TYPE_NONE = 0;
// ENEMY_TYPE_DUCK = 1;
// ENEMY_TYPE_FLY = 2;
// ENEMY_TYPE_PIG = 3;
int[][] map = new int[][] {
{0, 0, 0, 0, 1, 0, 0, 0, 0},
{0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 2, 1, 0, 0, 0, 1, 2, 0},
{0, 2, 2, 1, 0, 1, 2, 2, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 1, 1},
{0, 0, 0, 0, 0, 0, 1, 1, 1},
{0, 2, 2, 0, 0, 0, 2, 2, 0},
{0, 2, 2, 0, 0, 0, 2, 2, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 3, 0, 0, 0, 0},
};
return map;
}
该函数返回的2维数组,表达的就是敌人的出场顺序和出场位置。我们目前是这样安排的,将屏幕均分为9列,每列的特定位置对应2维数组中的1个整数,当数值为0时,表示此处没有敌人;当数值为1到3之间的整数时,分别代表此处将出现哪一种敌人。现在我们只有3种敌人:DUCK,FLY,PIG。
这1关卡只有1个BOSS,其类型为3型,对应上面的PIG。我们可以看到,它只会在上面出场表的最后1行出现1次。
4.2 EnemyManager
关卡里的所有敌人最好能统1管理,所以我编写了EnemyManager类。EnemyManager的定义截选以下:
public class EnemyManager implements IGameElement
{
private ArrayList<Enemy> mEnemyList = new ArrayList<Enemy>();
private int[][] mEnemyMap = null;
private int mCurLine = 0;
private int mEnemyCounter = 0;
private Context mContext = null;
private EnemyFactory mEnemyFactory = null;
private BulletsManager mBulletsMgr = null;
private ExplodeManager mExplodeMgr = null;
private Player mPlayer = null;
其中mEnemyList列表中会记录关卡里产生的所有敌人,当敌人被击毙以后,程序会把相应的Enemy对象从这张表中删除。mEnemyMap记录的其实就是前文所说的敌人的出场顺序表。另外,为了便于创建Enemy对象,我们可以先创建1个EnemyFactory对象,并记入mEnemyFactory域。
另外,我们还需要管理所有Enemy发出的子弹,我们为EnemyManager添加了mBulletsMgr域,意思很简单,往后每一个Enemy发射子弹时,其实都是向这个BulletsManager添加子弹对象。与此同理,我们还需要1个记录爆炸效果的爆炸管理器,那就是mExplodeMgr域。每当1个Enemy被击毙时,它会向爆炸管理器中添加1个爆炸效果对象。
4.2.1 drawFrame()
EnemyManager的绘制动作很简单,只需遍历1下所记录的Enemy列表,调用每一个Enemy对象的drawFrame()函数便可。
@Override
public void drawFrame(Canvas canvas)
{
Iterator<Enemy> itor = mEnemyList.iterator();
while (itor.hasNext())
{
Enemy b = itor.next();
b.drawFrame(canvas);
}
}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
4.2.2 execLogic()
履行逻辑的动作也差不多,都需要遍历Enemy列表:
@Override
public void execLogic()
{
execAddEnemyLogic(); // 添加enemy的地方!
Iterator<Enemy> itor = mEnemyList.iterator();
while (itor.hasNext())
{
Enemy b = itor.next();
b.execLogic(); // 履行每一个enemy的execLogic。
}
// EnemyManager还需要负责清算“已死亡”的enemy
itor = mEnemyList.iterator();
while (itor.hasNext())
{
Enemy b = itor.next();
if (b.isDead())
{
itor.remove();
}
}
}
请注意,EnemyManager的execLogic()在1开始会调用execAddEnemyLogic()函数,由于我们总需要1个地方添加关卡里的enemy吧。
private void execAddEnemyLogic()
{
mEnemyCounter++;
if (mEnemyCounter % 24 == 0)
{
if (mCurLine < mEnemyMap.length)
{
for (int i = 0; i < mEnemyMap[mCurLine].length; i++)
{
addEnemy(mEnemyMap[mCurLine][i], i, mEnemyMap[mCurLine].length);
}
}
mCurLine++;
}
}
我们用1个mEnemyCounter计数器,来控制添加enemy的频率。帧流里每活动1帧,耗时大概50毫秒(由于我们设的帧率是20帧/秒),那末24帧大概会耗时24 * 50 = 1200毫秒。也就是说,每过1.2秒,我们就会向EnemyManager里添加1行enemy。至于这1行里具体有甚么类型的enemy,是由mEnemyMap[ ]数组决定的。
addEnemy的代码以下:
private void addEnemy(int enemyType, int colIdx, int colCount)
{
Enemy enemy = null;
int enemyCenterX, enemyCenterY;
enemy = mEnemyFactory.createEnemy(enemyType);
if (null == enemy)
{
return;
}
enemy.setBulletsManager(mBulletsMgr);
enemy.setExplodeManager(mExplodeMgr);
enemy.setTarget(mPlayer);
mEnemyList.add(enemy);
switch (enemyType)
{
case EnemyFactory.ENEMY_TYPE_DUCK:
case EnemyFactory.ENEMY_TYPE_FLY:
int colWidth = (int)((double)GlobalInfo.screenW / colCount);
enemyCenterX = colWidth * colIdx + colWidth / 2;
enemyCenterY = ⑴ * enemy.getHeight();
enemy.setInitInfo(enemyCenterX, enemyCenterY, 8);
break;
case EnemyFactory.ENEMY_TYPE_PIG:
enemyCenterX = GlobalInfo.screenW / 2;
enemyCenterY = ⑴ * enemy.getHeight();
enemy.setInitInfo(enemyCenterX, enemyCenterY, 8);
break;
default:
break;
}
}
代码很简单,先利用EnemyFactory根据不同的enemyType,创建相应的enemy对象。然后为每一个enemy设置重要的关联对象,比如mBulletsMgr、mExplodeMgr、mPlayer。这是由于enemy总是要发子弹的嘛,那末它每发1颗子弹,都要向“子弹管理器”里添加子弹对象。同理,当enemy爆炸时,它也会向“爆炸管理器”里添加1个爆炸效果对象。又由于enemy常常需要瞄准玩家发射子弹,那末它就需要知道玩家的位置信息,因此setTarget(mPlayer)也是必要的。
接着我们将enemy对象添加进EnemyManager的mEnemyList列表中。另外还需要为不同enemy设置不同的初始信息,比如初始位置、运行速度等等。
4.3 BulletsManager
游戏中所有的子弹,不论是enemy发射的,还是玩家发射的,都必须添加进“子弹管理器”加以保护。只不过为了便于处理,我们把enemy和玩家发射的子弹分别放在了不同的BulletsManager里。这就是为何在GameManager里,会有两个BulletsManager的缘由:
private BulletsManager mPlayerBulletsMgr = new BulletsManager();
private BulletsManager mEnemyBulletsMgr = new BulletsManager();
BulletsManager的代码以下:
public class BulletsManager
{
private ArrayList<Bullet> mBulletsList = new ArrayList<Bullet>();
public void addBullet(Bullet bullet)
{
mBulletsList.add(bullet);
}
public void drawFrame(Canvas canvas)
{
Iterator<Bullet> itor = mBulletsList.iterator();
while (itor.hasNext())
{
Bullet b = itor.next();
b.drawFrame(canvas);
}
}
public void execLogic()
{
Iterator<Bullet> itor = mBulletsList.iterator();
while (itor.hasNext())
{
Bullet b = itor.next();
b.execLogic();
}
itor = mBulletsList.iterator();
while (itor.hasNext())
{
Bullet b = itor.next();
if (b.isDead())
{
itor.remove();
}
}
}
public ArrayList<Bullet> getBullets()
{
ArrayList<Bullet> bullets = (ArrayList<Bullet>)mBulletsList.clone();
return bullets;
}
}
从代码上看,它的drawFrame()和execLogic()和EnemyManager的同名函数很像。在execLogic()中,每当发现1颗子弹已报废了,就会把它从mBulletsList列表里删除。嗯,用isDead()来表达子弹是不是报废了好像不太贴切,不过大家应当都能够理解吧,呵呵。
BulletsManager还得向外提供1个getBullets()函数,以便外界进行碰撞判断。这个我们在后文再细说。
4.4 ExplodeManager
爆炸效果管理器和子弹管理器的逻辑代码差不多,所以我们就不贴它的execLogic()和drawFrame()的代码了。
每一个爆炸效果会对应1个Explode对象。由于爆炸效果1般都会表现为动画,所以Explode内部必须记录下自己当前该绘制哪1张图片了。在我们的程序里,爆炸资源图以下:
这张爆炸图会在Explode对象构造之时传入,而且外界会告知Explode对象,爆炸图中总共有几帧。Explode的构造函数以下:
public Explode(int explodeType, Rect rect, Bitmap explodeBmp, int totalFrame)
{
mType = explodeType;
mCurRect = new Rect(rect);
mExplodeBmp = explodeBmp;
mTotalFrame = totalFrame;
mFrameWidth = mExplodeBmp.getWidth() / mTotalFrame;
mFrameHeight = mExplodeBmp.getHeight();
}
每当ExplodeManager遍历履行每一个Explode对象的execLogic()时,会改变当前应当绘制的帧号。这样当游戏总帧流活动时,爆炸效果也就动起来了。Explode的execLogic()函数以下:
public void execLogic()
{
mCurFrameIdx++;
if (mCurFrameIdx >= mTotalFrame)
{
mState = STATE_DEAD;
}
}
具体绘制爆炸帧时,我们只需把爆炸图中与mCurFrameIdx对应的那1部份画出来就能够了,这就必须用到clipRect()。Explode的drawFrame()函数以下:
public void drawFrame(Canvas canvas)
{
Rect srcRect = new Rect(mCurFrameIdx * mFrameWidth, 0,
(mCurFrameIdx + 1)*mFrameWidth,
mFrameHeight);
canvas.save();
canvas.clipRect(mCurRect);
canvas.drawBitmap(mExplodeBmp, srcRect, mCurRect, null);
canvas.restore();
}
1开始计算的srcRect,表示的就是和mCurFrameIdx对应的绘制部份。
其实,不光是爆炸效果,我们的每类Enemy都是具有自己的动画的。它们的绘制机理和爆炸效果1致,我们就不赘述了。下面只贴出3类Enemy的角色动画图:
4.5 Player
现在我们来看玩家控制的角色――Player类。它和Enemy最大的不同是,它是直接由玩家控制的。玩家想把它移到甚么地方,他就得乖乖地移到那个地方去,为此它必须能够处理MotionEvent。
4.5.1 doWithTouchEvent()
public boolean doWithTouchEvent(MotionEvent event)
{
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
mOffsetX = x - mCurRect.left;
mOffsetY = y - mCurRect.top;
return true;
case MotionEvent.ACTION_UP:
mOffsetX = mOffsetY = 0;
return true;
case MotionEvent.ACTION_MOVE:
int curX = x - mOffsetX;
int curY = y - mOffsetY;
if (curX < 0)
{
curX = 0;
}
if (curY < 0)
{
curY = 0;
}
if (curX + mWidth > GlobalInfo.screenW)
{
curX = GlobalInfo.screenW - mWidth;
}
if (curY + mHeight > GlobalInfo.screenH)
{
curY = GlobalInfo.screenH - mHeight;
}
mCurRect.set(curX, curY, curX+mWidth, curY+mHeight);
return true;
default:
break;
}
return false;
}
注意,为了保证良好的用户体验,我们需要在用户点击屏幕之时,先计算1下手指导击处和Player对象当前所在位置之间的偏移量,以后在处理ACTION_MOVE时,还需用x、y减去偏移量。这样,就不会出现Player对象从旧位置直接跳变得手指导击处的情况。
4.5.2 碰撞判断
现在我们来讲说碰撞处理。在飞机游戏里,1种典型的碰撞情况就是被子弹击中啦。对Player来讲,它必须逐一判断敌人发出的子弹,看自己是不是已和某个子弹密切接触,如果是的话,那末Player就得减血,如果没血可减了,就算被击毙了。
对简单的游戏而言,我们只需判断子弹所占的Rect范围是不是和Player所占的Rect范围有交集,如果是的话,就能够认为产生碰撞了。固然,为了增加1点儿趣味性,我们是用1个比Player Rect更小的矩形来和子弹Rect比对的,这样可以出现1点儿子弹和Player擦身而过的惊险效果。
在GameManager的execLogic()的最后1步,会调用execCollsionLogic()函数。该函数的代码以下:
private void execCollsionLogic()
{
mPlayer.doWithCollision(mEnemyBulletsMgr);
mEnemyMgr.doWithCollision(mPlayerBulletsMgr);
}
意思很简单,Player需要和所有enemy发出的子弹进行比对,而每一个enemy需要和Player发出的子弹比对。我们只看Player的doWithCollision()函数,代码以下:
public void doWithCollision(BulletsManager bulletsMgr)
{
if (mState == STATE_EXPLODE || mState == STATE_DEAD)
{
return;
}
ArrayList<Bullet> bullets = bulletsMgr.getBullets();
Iterator<Bullet> itor = bullets.iterator();
int insetWidth = (int)((mCurRect.right - mCurRect.left) * 0.2);
int insetHeight = (int)((mCurRect.bottom - mCurRect.top) * 0.15);
Rect effectRect = new Rect(mCurRect);
effectRect.inset(insetWidth, insetHeight);
while (itor.hasNext())
{
Bullet b = itor.next();
Rect bulletRect = b.getRect();
if (effectRect.intersect(bulletRect))
{
b.doCollide();
doCollide(b.getPower());
}
}
}
其中那个effectRect就是比Player所占矩形更小1点儿的矩形啦。我们遍历BulletsManager中的每一个子弹,1旦发现哪一个子弹和effectRect有交集,就履行doCollide()。
private void doCollide(int power)
{
if (mState == STATE_ADJUST || mState == STATE_EXPLODE || mState == STATE_DEAD)
{
return;
}
if (power < 0)
{
// kill me directly
mState = STATE_EXPLODE;
}
else if (power > 0)
{
mMyHP -= power;
if (mMyHP <= 0)
{
mMyHP = 0;
mState = STATE_EXPLODE;
}
else
{
mState = STATE_ADJUST;
mAdjustCounter = 0;
}
}
}
如果写得复杂1点儿的话,不同enemy发出的子弹的威力应当是不1样的。不过在本游戏中,每颗子弹的威力都定为1了。也就是说,传入doCollide()的power参数的值总为1。每次碰撞时,Player就减1滴血(mMyHP -= power),然后立即跳变到STATE_ADJUST状态或STATE_EXPLODE状态。
另外一方面,enemy和Player发出的子弹也有类似的判断,只是判断条件更加宽松1些,这样可以给玩家增加1点儿射击的爽快感,呵呵。关于这部份的代码我们就不重复贴了。
4.5.3 被击中后的闪烁效果
Player需要完成的另外一个效果是被击中后,闪烁1段很短的时间,在这段时间内,它会暂时处于无敌状态,这样做可以免玩家出现被多颗子弹同时击中而被瞬杀的情况。为此我们设计了1个“调剂状态”,就是我们刚刚看到的STATE_ADJUST状态啦。
1旦Player被击中,只要它的mMyHP(血值)没有减到0,那末它立即跳变到STATE_ADJUST。在这类状态下,我们不再每次都绘制Player图片了,而是隔1帧绘制1次,这样就能够到达闪烁的效果了。固然这个状态的保持时间很短,我们会记录1个mAdjustCounter计数变量,每次履行execLogic()会给这个计数器加1,直到加到6,我们就从STATE_ADJUST状态,跳变回普通状态(STATE_ALIVE状态)。
public void execLogic()
{
if (mState == STATE_ALIVE)
{
doFireBulletLogic();
}
else if (mState == STATE_EXPLODE)
{
doExplode();
}
else if (mState == STATE_ADJUST)
{
doFireBulletLogic();
mAdjustCounter++;
if (mAdjustCounter > 6)
{
mState = STATE_ALIVE;
mAdjustCounter = 0;
}
}
}
public void drawFrame(Canvas canvas)
{
boolean shouldDraw = true;
if (mState == STATE_DEAD)
{
Log.d("Player", "mState == STATE_DEAD");
return;
}
else if (mState == STATE_ADJUST)
{
if (mAdjustCounter % 2 == 0)
{
shouldDraw = false;
}
}
else if (mState == STATE_EXPLODE)
{
// should draw
}
Log.d("Player", "mState == " + mState);
if (shouldDraw)
{
Rect src = new Rect(0, 0, mPlayerBmp.getWidth(), mPlayerBmp.getHeight());
canvas.drawBitmap(mPlayerBmp, src, mCurRect, null);
}
}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>
4.6 GameInfoPanel
飞机游戏还需要1个简单的“信息显示板”,来显示1些重要的信息。在本游戏中,我只显示了Player的剩余血量(每滴血用1个红心表示),大家有兴趣可以再添加玩家分数等信息。
我们设计的信息显示板是GameInfoPanel,它的逻辑非常简单:
public void execLogic()
{
mPlayerHP = mPlayer.getHP();
}
只是简单地记录1下Player的血量而已。
绘制时,它根据所记录的血量值绘制相应的红心图片就能够了:
public void drawFrame(Canvas canvas)
{
Rect src = new Rect(0, 0, mHPBmp.getWidth(), mHPBmp.getHeight());
Rect dest = new Rect();
for (int i = 0; i < mPlayerHP; i++)
{
dest.left = mRect.left + i * mHPiconWidth;
dest.top = mRect.top;
dest.right = dest.left + mHPiconWidth;
dest.bottom = dest.top + mHPiconHeight;
canvas.drawBitmap(mHPBmp, src, dest, null);
}
}
5 尾声
至此,我们已把这个小游戏的主要设计方面都讲到了。固然,由于这个游戏只是我为了好玩而写的1个demo程序,所以肯定有很多地方其实不完备,这个我想大家也是可以理解的。那末就先说这么多吧。最后让我们来贴两张游戏截图,乐呵1下。