文档章节

写个简单的飞机游戏玩玩

悠然红茶
 悠然红茶
发布于 2015/01/11 23:10
字数 5307
阅读 1280
收藏 12

写个简单的飞机游戏玩玩

 

侯亮

 

 

1      概述

    前些天看了《Android游戏编程之从零开始》一书中一个简单飞机游戏的实现代码,一时手痒,也写了一个练练手。虽然我的本职工作并不是写游戏,不过程序员或多或少都有编写游戏的情结,那就写吧,Just for fun!游戏的代码部分我基本上全部重写了,至于游戏的图片资源嘛,我老实不客气地全拿来复用了一下,呵呵,希望李华明先生不要见怪啊。

    在Android平台上,SurfaceView就足以应付所有简单游戏了。当然我说的是简单游戏,如果要写复杂游戏,恐怕还得使用各种游戏引擎,不过游戏引擎不是本文关心的重点,对于我写的简单游戏来说,用SurfaceView就可以了。

    飞机游戏的一个小特点是,画面总是在变动的,这当然是句废话,不过却能引出一个关键的设计核心,那就是“帧流”。帧流的最典型例子大概就是电影啦,我们知道,只要胶片按每秒钟24帧(或者更高)的速率播放,人眼就会误以为看到了连续的运动画面。飞机游戏中的运动画面大体也是这样呈现的,因此游戏设计者必须设计出一条平滑的帧流,并且帧率要足够快。

    从技术上说,我们可以在一个线程中,构造一个不断绘制“帧”的while循环,并在每次画好帧后,调用Thread.sleep()睡眠合适的时间,这样就可以实现一个相对平滑的帧流了。

    另一方面,游戏的逻辑也是可以融入到帧流里的,也就是说,每次画好帧后,我们可以调用一个类似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


         一旦surface创建成功,我们就启动一个线程,这个线程负责运作帧流。

@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,一气呵成。为了便于计算,此处我采用了每秒20帧的帧率,所以每帧平均50毫秒,而且因为画帧和执行游戏逻辑都是需要消耗时间的,所以合适的sleep()动作应该写成:Thread.sleep(50 - (end - start))

3      GameManager

3.1 整合游戏中所有元素

为了便于管理,我设计了一个GameManager管理类。这个类到底是干什么的呢?简单地说,它整合了游戏中的所有元素,目前有:

  • 绘制关卡背景;
  • 所有敌人;
  • 爆炸特效;
  • 所有子弹、炮弹;
  • 玩家( player )飞机;
  • 游戏信息面板;

当然,以后还可以再扩展一些东西,它们的机理是接近的。

        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);
        }
    }
}


其中GameManagerdrawFrame()函数如下:

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的当前帧。(当然,这里我们只是简单阐述原理,大家如有兴趣,可以再在这张图上添加其他的游戏元素。)绘制完毕后的最终效果,就是屏幕展示给用户的最终画面。

         每个游戏角色都非常清楚自己当前应该如何绘制,而且它通过执行自己的子逻辑,决定出下一帧该如何绘制,这就是游戏中最重要的画帧流程。

 

3.3 GameManager管理所有的子逻辑

其实,游戏的整体运作是由两个方面带动的,一个是“软件内部控制”,主要控制所有“非player角色”的移动和动作,比如每个enemy下一步移动到哪里,如何发射子弹等等;另一个是“用户操作”,主要控制“player角色”的移动和动作(这部分我们放在后文再说)。在前文所说的帧流线程里,是通过调用GameManagerexecLogic()来完成所有“软件内部控制”的,其代码如下:

public void execLogic() 
{
    mCurStage.execLogic();
    mEnemyMgr.execLogic();
    mPlayer.execLogic();
    mPlayerBulletsMgr.execLogic();
    mEnemyBulletsMgr.execLogic();
    mExplodeMgr.execLogic();
    mGameInfoPanel.execLogic();
    execCollsionLogic();		// 碰撞逻辑
} 


         从上面代码就可以看出,GameManager所管理的子逻辑大概有以下几个:

  • 关卡运作子逻辑
  • 所有敌人的运作子逻辑
  • 玩家角色的子逻辑
  • 玩家发射的子弹的子逻辑
  • 敌人发射的子弹的子逻辑
  • 管理爆炸效果的子逻辑
  • 游戏信息面板的子逻辑
  • 碰撞子逻辑

4      游戏子逻辑

4.1 关卡运作子逻辑——GameStage

我们先看前面execLogic()函数里的第一句:mCurState.execLogic(),这个mCurStateGameStage类型的,这个类主要维护当前关卡的相关数据。目前这个类非常简单,只维护了关卡背景图以及本关enemy的出现顺序表。

 

4.1.1   关卡背景图由StageBg类处理

一般来说,飞机游戏的背景是不断滚动的。为了实现滚动效果,我们可以绘制一张比屏幕长度更长的图片,并首尾相接地循环绘制它。

 

         StageBg里,mBackGroundBmp1mBackGroundBmp2这两个域其实指向的是同一个位图对象,之所以写成两个域,是为了代码更易于阅读。另外,mBgScrollSpeed用于表示背景滚动的速度,我们可以通过修改它,来体现飞行的速度。

 

4.1.2   关卡中的敌人的出场安排

GameStage的另一个重要职责是向游戏的主控制器(GameManager)提供一张表示敌人出场顺序的表,为此它提供了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;
}


该函数返回的二维数组,表达的就是敌人的出场顺序和出场位置。我们目前是这样安排的,将屏幕均分为9列,每一列的特定位置对应二维数组中的一个整数,当数值为0时,表示此处没有敌人;当数值为13之间的整数时,分别代表此处将出现哪种敌人。现在我们只有3种敌人:DUCKFLYPIG


  

         这一关卡只有一个BOSS,其类型为3型,对应上面的PIG。我们可以看到,它只会在上面出场表的最后一行出现一次。

4.2 EnemyManager

关卡里的所有敌人最好能统一管理,所以我编写了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对象,我们可以先创建一个EnemyFactory对象,并记入mEnemyFactory域。

    另外,我们还需要管理所有Enemy发出的子弹,我们为EnemyManager添加了mBulletsMgr域,意思很简单,日后每个Enemy发射子弹时,其实都是向这个BulletsManager添加子弹对象。与此同理,我们还需要一个记录爆炸效果的爆炸管理器,那就是mExplodeMgr域。每当一个Enemy被击毙时,它会向爆炸管理器中添加一个爆炸效果对象。

4.2.1   drawFrame()

EnemyManager的绘制动作很简单,只需遍历一下所记录的Enemy列表,调用每个Enemy对象的drawFrame()函数即可。

@Override
public void drawFrame(Canvas canvas)
{
    Iterator<Enemy> itor = mEnemyList.iterator();
    
    while (itor.hasNext())
    {
        Enemy b = itor.next();
        b.drawFrame(canvas);
    }
} 


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();   
        }
    }
} 


         请注意,EnemyManagerexecLogic()在一开始会调用execAddEnemyLogic()函数,因为我们总需要一个地方添加关卡里的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++;
    }
}


我们用一个mEnemyCounter计数器,来控制添加enemy的频率。帧流里每流动一帧,耗时大概50毫秒(因为我们设的帧率是20/秒),那么24帧大概会耗时24 * 50 = 1200毫秒。也就是说,每过1.2秒,我们就会向EnemyManager里添加一行enemy。至于这一行里具体有什么类型的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 = -1 * enemy.getHeight();
        enemy.setInitInfo(enemyCenterX, enemyCenterY, 8);
        break;
        
    case EnemyFactory.ENEMY_TYPE_PIG:
        enemyCenterX = GlobalInfo.screenW / 2;
        enemyCenterY = -1 * enemy.getHeight();
        enemy.setInitInfo(enemyCenterX, enemyCenterY, 8);
        break;
        
    default:
        break;
    }
}


代码很简单,先利用EnemyFactory根据不同的enemyType,创建相应的enemy对象。然后为每个enemy设置重要的关联对象,比如mBulletsMgrmExplodeMgrmPlayer。这是因为enemy总是要发子弹的嘛,那么它每发一颗子弹,都要向“子弹管理器”里添加子弹对象。同理,当enemy爆炸时,它也会向“爆炸管理器”里添加一个爆炸效果对象。又因为enemy常常需要瞄准玩家发射子弹,那么它就需要知道玩家的位置信息,因此setTarget(mPlayer)也是必要的。


接着我们将enemy对象添加进EnemyManagermEnemyList列表中。另外还需要为不同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()中,每当发现一颗子弹已经报废了,就会把它从mBulletsList列表里删除。嗯,用isDead()来表达子弹是否报废了好像不太贴切,不过大家应该都能够理解吧,呵呵。

         BulletsManager还得向外提供一个getBullets()函数,以便外界进行碰撞判断。这个我们在后文再细说。

4.4 ExplodeManager

爆炸效果管理器和子弹管理器的逻辑代码差不多,所以我们就不贴它的execLogic()drawFrame()的代码了。

         每个爆炸效果会对应一个Explode对象。因为爆炸效果一般都会表现为动画,所以Explode内部必须记录下自己当前该绘制哪一张图片了。在我们的程序里,爆炸资源图如下:

 

这张爆炸图会在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()时,会改变当前应该绘制的帧号。这样当游戏总帧流流动时,爆炸效果也就动起来了。ExplodeexecLogic()函数如下:

public void execLogic()
{
    mCurFrameIdx++;
    if (mCurFrameIdx >= mTotalFrame)
    {
        mState = STATE_DEAD;
    }
} 


         具体绘制爆炸帧时,我们只需把爆炸图中与mCurFrameIdx对应的那一部分画出来就可以了,这就必须用到clipRect()ExplodedrawFrame()函数如下:

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();
}


一开始计算的srcRect,表示的就是和mCurFrameIdx对应的绘制部分。


         其实,不光是爆炸效果,我们的每一类Enemy都是具有自己的动画的。它们的绘制机理和爆炸效果一致,我们就不赘述了。下面只贴出三类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;
}

         注意,为了保证良好的用户体验,我们需要在用户点击屏幕之时,先计算一下手指点击处和Player对象当前所在位置之间的偏移量,以后在处理ACTION_MOVE时,还需用xy减去偏移量。这样,就不会出现Player对象从旧位置直接跳变到手指点击处的情况。

4.5.2   碰撞判断

现在我们来说说碰撞处理。在飞机游戏里,一种典型的碰撞情况就是被子弹击中啦。对于Player来说,它必须逐个判断敌人发出的子弹,看自己是否已和某个子弹亲密接触,如果是的话,那么Player就得减血,如果没血可减了,就算被击毙了。

对于简单的游戏而言,我们只需判断子弹所占的Rect范围是否和Player所占的Rect范围有交集,如果是的话,就可以认为发生碰撞了。当然,为了增加一点儿趣味性,我们是用一个比Player Rect更小的矩形来和子弹Rect比对的,这样可以出现一点儿子弹和Player擦身而过的惊险效果。

         GameManagerexecLogic()的最后一步,会调用execCollsionLogic()函数。该函数的代码如下:

private void execCollsionLogic()
{
    mPlayer.doWithCollision(mEnemyBulletsMgr);
    mEnemyMgr.doWithCollision(mPlayerBulletsMgr);
}


意思很简单,Player需要和所有enemy发出的子弹进行比对,而每个enemy需要和Player发出的子弹比对。我们只看PlayerdoWithCollision()函数,代码如下:

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所占矩形更小一点儿的矩形啦。我们遍历BulletsManager中的每个子弹,一旦发现哪个子弹和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;
        }
    }
}


         如果写得复杂一点儿的话,不同enemy发出的子弹的威力应该是不一样的。不过在本游戏中,每颗子弹的威力都定为1了。也就是说,传入doCollide()power参数的值总为1。每次碰撞时,Player就减一滴血(mMyHP -= power),然后立即跳变到STATE_ADJUST状态或STATE_EXPLODE状态。

         另一方面,enemyPlayer发出的子弹也有类似的判断,只是判断条件更加宽松一些,这样可以给玩家增加一点儿射击的爽快感,呵呵。关于这部分的代码我们就不重复贴了。

4.5.3   被击中后的闪烁效果

Player需要完成的另一个效果是被击中后,闪烁一段很短的时间,在这段时间内,它会暂时处于无敌状态,这样做可以避免玩家出现被多颗子弹同时击中而被瞬杀的情况。为此我们设计了一个“调整状态”,就是我们刚刚看到的STATE_ADJUST状态啦。

         一旦Player被击中,只要它的mMyHP(血值)没有减到0,那么它立即跳变到STATE_ADJUST。在这种状态下,我们不再每次都绘制Player图片了,而是隔一帧绘制一次,这样就可以达到闪烁的效果了。当然这个状态的维持时间很短,我们会记录一个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);
    }
}

4.6  GameInfoPanel

飞机游戏还需要一个简单的“信息显示板”,来显示一些重要的信息。在本游戏中,我只显示了Player的剩余血量(每滴血用一个红心表示),大家有兴趣可以再添加玩家分数等信息。

         我们设计的信息显示板是GameInfoPanel,它的逻辑非常简单:

public void execLogic()
{
    mPlayerHP = mPlayer.getHP();
}


只是简单地记录一下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      尾声

至此,我们已经把这个小游戏的主要设计方面都讲到了。当然,因为这个游戏只是我为了好玩而写的一个demo程序,所以肯定有很多地方并不完备,这个我想大家也是可以理解的。那么就先说这么多吧。最后让我们来贴两张游戏截图,乐呵一下。

    

 

© 著作权归作者所有

悠然红茶
粉丝 343
博文 20
码字总数 106144
作品 0
西安
高级程序员
私信 提问
加载中

评论(7)

y
yxwhxh
大神,你是用什么画图软件啊,内功好强大
世界是我平的
博主我也是新手求带
rj43
rj43
强!16
noday
noday
厉害
booksoul
booksoul
好啦,貌似代码不多,我就从这个游戏开始吭吧,我是新手我怕谁?!!
悠然红茶
悠然红茶 博主
有同学要求分享一下本文对应的源代码,链接如下:
http://www.oschina.net/code/snippet_174429_50594
抬头一片星空
看起来有些吃力,博主功力深厚啊,看了之后,自己都能写出来,最重要的是里面的东西能讲析的如此详细
python写简单的猜数字游戏

最近在学python,学到控制流程要写一个猜数字游戏。不经想起小时候三色台的一个综艺节目,里面也有个猜数字游戏,于是就想写个简单的自己玩玩也好。 规则:[0-100]随机生成一个数字,然后在猜...

PM肥子
2017/03/13
0
0
用 Python 实现打飞机,让子弹飞吧!

所用技术和软件 python 2.7 pygame 1.9.3 pyCharm 准备工作 安装好 pygame 在第一次使用 pygame 的时候,pyCharm 会自动 install pygame。 下载好使用的素材。 技术实现 初始化 pygame 首先要...

猫咪编程
2018/07/21
80
0
HTML5演示碰撞及基本弹幕的实现

0、演示在此:http://runjs.cn/detail/y8w993if 1、框架搭建 为了方便演示,我搭建了一个简单的游戏“框架”,框架包含描述游戏状态的部分及逻辑部分。 一个弹幕游戏至少应当包含“自机”及“...

lxrmido
2014/01/17
3.1K
3
Android 游戏开发之飞行射击类游戏原理实现

1.地图滚动的原理实现 举个简单的例子吧,同学们都坐过火车吧,坐火车的时候都遇到过自己的火车明明是停止的但是旁边铁轨的火车在向后行驶,会有一种错觉感觉自己的火车是在向前 行驶吧,呵呵...

无鸯
2011/10/03
2.3K
0
Android游戏开发之飞行射击类游戏原理实现(二十)

Android游戏开发之飞行射击类游戏原理实现 雨松MOMO原创文章如转载,请注明:转载自雨松MOMO的博客原文地址:http://blog.csdn.net/xys289187120/article/details/6673940 1.地图滚动的原理实...

彭博
2012/03/09
141
0

没有更多内容

加载失败,请刷新页面

加载更多

为什么Netty的FastThreadLocal速度快

前言 最近在看netty源码的时候发现了一个叫FastThreadLocal的类,jdk本身自带了ThreadLocal类,所以可以大致想到此类比jdk自带的类速度更快,主要快在什么地方,以及为什么速度更快,下面做一...

ksfzhaohui
27分钟前
5
0
资治通鉴解析:无论什么条件,要挟权力做出承诺,都会被清算

电影《满城尽带黄金甲》里有句经典的名言“朕赐给你的,才是你的。朕不给你的,你不能抢。”之所以这段话有名,核心的就是,它揭示了这样一个权力心思:无论什么情况,权力的行使,都不愿意受...

太空堡垒185
31分钟前
4
0
CSS技巧之向下箭头

本文转载于:专业的前端网站➫CSS技巧之向下箭头 思路: 使用◇符号(可在输入法的软键盘找到该符号),使用定位选择位置,并隐藏溢出的上半部分 细点: 1.使用i标签的楷体属性把◇变大 2.给i...

前端老手
47分钟前
2
0
SpringCloud alibaba微服务之NACOS多环境配置整合

前言 伴随着spring cloud alibaba 登上主板以后,我就去了解下感觉还是蛮不错的。说实话第一次看见Nacos好长一段时间连读法都不知道...(/nɑ:kəʊs/)。按照官方的话说Nacos是:一个更易于...

攻城狮-飞牛
50分钟前
4
0
tcpdump

tcpdump -A -s0 port 21011 -i any (1)tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型 (2)-i eth1 : 只抓经过接口eth1的包 (3)-t : 不显...

mskk
55分钟前
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部