1. 程式人生 > >夜靜水寒魚不食 滿船空載月明歸

夜靜水寒魚不食 滿船空載月明歸

寫個簡單的飛機遊戲玩玩

侯亮

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

其中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的當前幀。(當然,這裡我們只是簡單闡述原理,大家如有興趣,可以再在這張圖上新增其他的遊戲元素。)繪製完畢後的最終效果,就是螢幕展示給使用者的最終畫面。

         每個遊戲角色都非常清楚自己當前應該如何繪製,而且它通過執行自己的子邏輯,決定出下一幀該如何繪製,這就是遊戲中最重要的畫幀流程。

3.3  GameManager管理所有的子邏輯

        其實,遊戲的整體運作是由兩個方面帶動的,一個是“軟體內部控制”,主要控制所有“非player角色”的移動和動作,比如每個enemy下一步移動到哪裡,如何發射子彈等等;另一個是“使用者操作”,主要控制“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()函式裡的第一句:mCurState.execLogic(),這個mCurState是GameStage型別的,這個類主要維護當前關卡的相關資料。目前這個類非常簡單,只維護了關卡背景圖以及本關enemy的出現順序表。

4.1.1   關卡背景圖由StageBg類處理

一般來說,飛機遊戲的背景是不斷滾動的。為了實現滾動效果,我們可以繪製一張比螢幕長度更長的圖片,並首尾相接地迴圈繪製它。

 

         在StageBg裡,mBackGroundBmp1和mBackGroundBmp2這兩個域其實指向的是同一個點陣圖物件,之所以寫成兩個域,是為了程式碼更易於閱讀。另外,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時,表示此處沒有敵人;當數值為1到3之間的整數時,分別代表此處將出現哪種敵人。現在我們只有3種敵人:DUCK,FLY,PIG。

  

          這一關卡只有一個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);
    }
}<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()在一開始會呼叫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設定重要的關聯物件,比如mBulletsMgr、mExplodeMgr、mPlayer。這是因為enemy總是要發子彈的嘛,那麼它每發一顆子彈,都要向“子彈管理器”裡新增子彈物件。同理,當enemy爆炸時,它也會向“爆炸管理器”裡新增一個爆炸效果物件。又因為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()中,每當發現一顆子彈已經報廢了,就會把它從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()時,會改變當前應該繪製的幀號。這樣當遊戲總幀流流動時,爆炸效果也就動起來了。Explode的execLogic()函式如下:
public void execLogic()
{
    mCurFrameIdx++;
    if (mCurFrameIdx >= mTotalFrame)
    {
        mState = STATE_DEAD;
    }
}

         具體繪製爆炸幀時,我們只需把爆炸圖中與mCurFrameIdx對應的那一部分畫出來就可以了,這就必須用到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();
}

一開始計算的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時,還需用x、y減去偏移量。這樣,就不會出現Player物件從舊位置直接跳變到手指點選處的情況。

4.5.2   碰撞判斷

        現在我們來說說碰撞處理。在飛機遊戲裡,一種典型的碰撞情況就是被子彈擊中啦。對於Player來說,它必須逐個判斷敵人發出的子彈,看自己是否已和某個子彈親密接觸,如果是的話,那麼Player就得減血,如果沒血可減了,就算被擊斃了。

        對於簡單的遊戲而言,我們只需判斷子彈所佔的Rect範圍是否和Player所佔的Rect範圍有交集,如果是的話,就可以認為發生碰撞了。當然,為了增加一點兒趣味性,我們是用一個比Player Rect更小的矩形來和子彈Rect比對的,這樣可以出現一點兒子彈和Player擦身而過的驚險效果。

         在GameManager的execLogic()的最後一步,會呼叫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所佔矩形更小一點兒的矩形啦。我們遍歷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狀態。

         另一方面,enemy和Player發出的子彈也有類似的判斷,只是判斷條件更加寬鬆一些,這樣可以給玩家增加一點兒射擊的爽快感,呵呵。關於這部分的程式碼我們就不重複貼了。

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);
    }
}<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

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程式,所以肯定有很多地方並不完備,這個我想大家也是可以理解的。那麼就先說這麼多吧。最後讓我們來貼兩張遊戲截圖,樂呵一下。

        

相關推薦

滿空載

寫個簡單的飛機遊戲玩玩 侯亮 1      概述         前些天看了《Android遊戲程式設計之從零開始》一書中一個簡單飛機遊戲的實現程式碼,一時手癢,也寫了一個練練手。雖然我的本職工作並不是寫遊戲,不過程式設計師或多或少都有編寫遊戲的情結,那就寫吧,Jus

TabLayout 滿全屏問題(android.support.design.widget.TabLayout)

<android.support.design.widget.TabLayout android:id="@+id/tab_layout" android:layout_width=“match_parent” android:layout_height=“wrap_content”

曾經滄海難為 除卻巫山是雲

各位投資朋友大家好,歡迎收聽《搶財貓股票課堂》,我是你們的老朋友波哥。   今天我們聊聊 “曾經滄海難為水 除卻巫山不是雲” 這個話題   在股市裡我們經常遇到一種情況:當你初步有意識按照自己交易模式積累了幾筆成功的交易,然後你尋找下一個標的的時候, 你

PAT 1050 螺旋矩陣 (題 然鵝)(又名:鹹魚和妖孽題糾纏清的故事)

原題在此 題意簡單,寫題暴力。 但是曲折萬分,請諸位大佬當笑話看吧……。 請看版本一。 #include <iostream> #include <cmath> #include <string> #include <queu

題 提取重複的整數 (queue的練習)

題目描述: 輸入一個int型整數,按照從右向左的閱讀順序,返回一個不含重複數字的新的整數。 輸入描述: 輸入一個int型整數。 輸出描述: 按照從右向左的閱讀順序,返回一個不含重複數字的新的整數。 輸入樣例: 9876673 輸出樣例: 37689 解題思路:

Scrollview佈局滿

專案中出現在ScrollView下的控制元件加了marginBottom="xdp"後發現並不是在螢幕底端 解決: 加入fillViewPort="true"即可 <ScrollView xmlns:android="http://schemas.android.

有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個後每個又生一對兔子,假如兔子都死,問每個的兔子總數為多少?

package src pac spa scanner span warnings warning resource 分析: 第一個月-----------------1 第二個月-----------------1 第三個月-----------------2 第四個月-

為什麽if else 語句裏能用函數聲定義函數,而可以用函數表達式定義函數

java 關鍵字 {} 作用 關系 另一個 else 語法 出錯 在《JavaScript高級程序設計》第三版第7章函數表達式部分講到,定義函數有兩種方式:一種是函數聲明,另一種就是函數表達式。函數聲明的語法是這樣的。function functionName(arg0,

python實現滿二叉樹遞循環

location pre tar 頂點 遞歸循環 int tle 計算 個數 一、二叉樹介紹點這片文章 二叉樹及題目介紹 例題: 有一顆滿二叉樹,每個節點是一個開關,初始全是關閉的,小球從頂點落下, 小球每次經過開關就會把它的狀態置反,這個開關為關時,小球左跑,為開時右跑。

java經典題丨有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個後每個又生一對兔子,假如兔子都死,問每個的兔子總對數為多少?

兔子問題,習題練習: public class Rubbit { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("請輸入月份");

夫君子之行,以修身,儉以養德,非淡泊無以志,非寧靜無以致遠.

一、使用者切換    "$":普通使用者提示符          "#":root使用者提示符       1.普通使用者到root:     方式一:命令:su然後輸入root密碼         此種方式只是切換了root身份,但Shell環境仍是普通使用者的S

古典問題:有一對兔子,從出生後第3個起每個都生一對兔子, 小兔子長到第三個後每個又生一對兔子 ,假如兔子都死,問每個的兔子總數為多少

思路分析:   月份          兔子數                  說明   1      1(對)            從開始有一對兔子   2      1   3      1+1       原本有一對  從第三個月開始 生了一對 一共是兩對兔

有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個後每個又生一對兔子,假如兔子都死,問每個的兔子對數為多少?

   private static int fun(int n){            if(n==1 ||n==2)               return 1;            else               return fun(n-1)+fun(n-2

有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個後每個又生一對兔子,假如兔子都死,問每個的兔子對數為多少?(遞迴,裴波那契數列)

/** * @Desc:古典問題:有一對兔子,從出生後第3個月起每個月都生一對兔子,小兔子長到第三個月後每個月又生一對兔子, * 假如兔子都不死,問每個月的兔子對數為多少? 程式分析: 兔子的規

古典問題:有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個後每個又生一對兔子,假如兔子都死,問每個的兔子總數為多少?

 第一種方法:import java.util.Scanner; public class Rab{ public static void main(String[]args){ int month; System.out.println("請輸入養殖兔子的月份

古典問題:有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個 後每個又生一對兔子,假如兔子都死,問每個的兔子總數為多少?

/*1.古典問題:有一對兔子,從出生後第3個月起每個月都生一對兔子,小兔子長到第三個月 後每個月又生一對兔子,假如兔子都不死,問每個月的兔子總數為多少?*/ //下一個數為前兩個數之和 1 1 2 3 5 8 13 (第一種方法)#include<stdio.h&

Python中求有一對兔子,從出生後第3個起每個都生一對兔子,,假如兔子都死,問每個的兔子總數為多少?

1.兔子的規律為數列1,1,2,3,5,8,13,21... a=1 b=1 print(a) print(b) for i in range(10): a=a+b print(a) b=a+b print(b) 第一個月跟第二個月一樣 

有一隻兔子,從出生後第3個起每個都生一隻兔子,小兔子長到第三個後每個又生一隻兔子,假如兔子都死,問每個的兔子總數為多少?

這是一道斐波拉契數列題目,很自然會想到使用遞迴f(n)=f(n-1)+f(n-2),但是使用遞迴的方式 會導致很多重複計算,因此,可以用第二種方法:用組數儲存已經計算過的數值,當後面計算需 要使用前面的值時,可以直接從陣列內取,方法如下: packag

題目:古典問題:有一對兔子,從出生後第3個起每個都生一對兔子,小兔子長到第三個後每個又生一對兔子,假如兔子都死,問每個的兔子對數為多少? 程式分析: 兔子的規律為數列1,1,2,3,5,

兔子問題: 別人提供的方法: 遞迴: public class Prog1{ public static void main(String[] args){ int n = 10; System.out.println("第"+n+"個月兔子總數為"+fun(n));