寫個(gè)簡(jiǎn)單的飛機(jī)游戲玩玩
來源:程序員人生 發(fā)布時(shí)間:2015-01-16 08:57:10 閱讀次數(shù):2616次
寫個(gè)簡(jiǎn)單的飛機(jī)游戲玩玩
侯亮
1 概述
前些天看了《Android游戲編程之從零開始》1書中1個(gè)簡(jiǎn)單飛機(jī)游戲的實(shí)現(xiàn)代碼,1時(shí)手癢,也寫了1個(gè)練練手。雖然我的本職工作其實(shí)不是寫游戲,不進(jìn)程序員或多或少都有編寫游戲的情結(jié),那就寫吧,Just for fun!游戲的代碼部份我基本上全部重寫了,至于游戲的圖片資源嘛,我老實(shí)不客氣地全拿來復(fù)用了1下,呵呵,希望李華明先生不要見怪啊。
在Android平臺(tái)上,SurfaceView就足以應(yīng)付所有簡(jiǎn)單游戲了。固然我說的是簡(jiǎn)單游戲,如果要寫復(fù)雜游戲,恐怕還得使用各種游戲引擎,不過游戲引擎不是本文關(guān)心的重點(diǎn),對(duì)我寫的簡(jiǎn)單游戲來講,用SurfaceView就能夠了。
飛機(jī)游戲的1個(gè)小特點(diǎn)是,畫面總是在變動(dòng)的,這固然是句空話,不過卻能引出1個(gè)關(guān)鍵的設(shè)計(jì)核心,那就是“幀流”。幀流的最典型例子大概就是電影啦,我們知道,只要膠片按每秒鐘24幀(或更高)的速率播放,人眼就會(huì)誤以為看到了連續(xù)的運(yùn)動(dòng)畫面。飛機(jī)游戲中的運(yùn)動(dòng)畫面大體也是這樣顯現(xiàn)的,因此游戲設(shè)計(jì)者必須設(shè)計(jì)出1條平滑的幀流,并且?guī)室銐蚩臁?/p>
從技術(shù)上說,我們可以在1個(gè)線程中,構(gòu)造1個(gè)不斷繪制“幀”的while循環(huán),并在每次畫好幀后,調(diào)用Thread.sleep()睡眠適合的時(shí)間,這樣就能夠?qū)崿F(xiàn)1個(gè)相對(duì)平滑的幀流了。
另外一方面,游戲的邏輯也是可以融入到幀流里的,也就是說,每次畫好幀后,我們可以調(diào)用1個(gè)類似execLogic()的函數(shù)來履行游戲邏輯,從而(間接)產(chǎn)生新的幀。而游戲邏輯又可以劃分成多個(gè)子邏輯,比如關(guān)卡背景邏輯、敵人行動(dòng)邏輯、玩家飛機(jī)邏輯、子彈行動(dòng)邏輯、碰撞邏輯等等,這個(gè)我們后文再細(xì)說。
大概說起來就是這么多了,現(xiàn)在我們逐一來看游戲設(shè)計(jì)中的細(xì)節(jié)。
2 平滑的幀流
我們先寫個(gè)全屏顯示的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));
}
}
這個(gè)Activity的主視圖是PlaneGameView類,它繼承于SurfaceView。
public class PlaneGameView extends SurfaceView implements Callback, Runnable
1旦surface創(chuàng)建成功,我們就啟動(dòng)1個(gè)線程,這個(gè)線程負(fù)責(zé)運(yùn)作幀流。
@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()函數(shù)的代碼以下:
@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)); // 睡眠適合的時(shí)間!
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
畫幀、游戲邏輯、適合的sleep,1氣呵成。為了便于計(jì)算,此處我采取了每秒20幀的幀率,所以每幀平均50毫秒,而且由于畫幀和履行游戲邏輯都是需要消耗時(shí)間的,所以適合的sleep()動(dòng)作應(yīng)當(dāng)寫成:Thread.sleep(50 - (end - start))。
3 GameManager
3.1 整合游戲中所有元素
為了便于管理,我設(shè)計(jì)了1個(gè)GameManager管理類。這個(gè)類究竟是干甚么的呢?簡(jiǎn)單地說,它整合了游戲中的所有元素,目前有:
- 繪制關(guān)卡背景;
- 所有敵人;
- 爆炸殊效;
- 所有子彈、炮彈;
- 玩家(player)飛機(jī);
- 游戲信息面板;
固然,以后還可以再擴(kuò)大1些東西,它們的機(jī)理是接近的。
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的總模塊關(guān)系示意圖以下:

既然在“幀流”線程里最重要的動(dòng)作是drawFrame()和execLogic(),那末GameManager類也必須提供這兩個(gè)成員函數(shù),這樣幀流線程只需直接調(diào)用GameManager的同名函數(shù)便可。
3.2 GameManager的畫幀動(dòng)作
幀流線程的drawFrame()函數(shù),其代碼以下:
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()函數(shù)以下:
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);
}
不過是調(diào)用所有游戲角色的drawFrame()而已。
每一個(gè)游戲角色有自己的存活期,在其存活期中,可以通過drawFrame()向canvas中的適合位置繪制相應(yīng)的圖片。示意圖以下:

在上面的示意圖中,兩個(gè)enemy的生存期都只有5幀,當(dāng)幀流繪制到上圖的紫色幀時(shí),會(huì)先繪制enemy_1的第1幀,而后繪制enemy_2的第5幀,最后繪制player確當(dāng)前幀。(固然,這里我們只是簡(jiǎn)單論述原理,大家如有興趣,可以再在這張圖上添加其他的游戲元素。)繪制終了后的終究效果,就是屏幕展現(xiàn)給用戶的終究畫面。
每一個(gè)游戲角色都非常清楚自己當(dāng)前應(yīng)當(dāng)如何繪制,而且它通過履行自己的子邏輯,決定出下1幀該如何繪制,這就是游戲中最重要的畫幀流程。
3.3 GameManager管理所有的子邏輯
其實(shí),游戲的整體運(yùn)作是由兩個(gè)方面帶動(dòng)的,1個(gè)是“軟件內(nèi)部控制”,主要控制所有“非player角色”的移動(dòng)和動(dòng)作,比如每一個(gè)enemy下1步移動(dòng)到哪里,如何發(fā)射子彈等等;另外一個(gè)是“用戶操作”,主要控制“player角色”的移動(dòng)和動(dòng)作(這部份我們放在后文再說)。在前文所說的幀流線程里,是通過調(diào)用GameManager的execLogic()來完成所有“軟件內(nèi)部控制”的,其代碼以下:
public void execLogic()
{
mCurStage.execLogic();
mEnemyMgr.execLogic();
mPlayer.execLogic();
mPlayerBulletsMgr.execLogic();
mEnemyBulletsMgr.execLogic();
mExplodeMgr.execLogic();
mGameInfoPanel.execLogic();
execCollsionLogic(); // 碰撞邏輯
}
從上面代碼就能夠看出,GameManager所管理的子邏輯大概有以下幾個(gè):
- 關(guān)卡運(yùn)作子邏輯
- 所有敵人的運(yùn)作子邏輯
- 玩家角色的子邏輯
- 玩家發(fā)射的子彈的子邏輯
- 敵人發(fā)射的子彈的子邏輯
- 管理爆炸效果的子邏輯
- 游戲信息面板的子邏輯
- 碰撞子邏輯
4 游戲子邏輯
4.1 關(guān)卡運(yùn)作子邏輯――GameStage
我們先看前面execLogic()函數(shù)里的第1句:mCurState.execLogic(),這個(gè)mCurState是GameStage類型的,這個(gè)類主要保護(hù)當(dāng)前關(guān)卡的相干數(shù)據(jù)。目前這個(gè)類非常簡(jiǎn)單,只保護(hù)了關(guān)卡背景圖和本關(guān)enemy的出現(xiàn)順序表。

4.1.1 關(guān)卡背景圖由StageBg類處理
1般來講,飛機(jī)游戲的背景是不斷轉(zhuǎn)動(dòng)的。為了實(shí)現(xiàn)轉(zhuǎn)動(dòng)效果,我們可以繪制1張比屏幕長(zhǎng)度更長(zhǎng)的圖片,并首尾相接地循環(huán)繪制它。

在StageBg里,mBackGroundBmp1和mBackGroundBmp2這兩個(gè)域其實(shí)指向的是同1個(gè)位圖對(duì)象,之所以寫成兩個(gè)域,是為了代碼更容易于瀏覽。另外,mBgScrollSpeed用于表示背景轉(zhuǎn)動(dòng)的速度,我們可以通過修改它,來體現(xiàn)飛行的速度。
4.1.2 關(guān)卡中的敵人的出場(chǎng)安排
GameStage的另外一個(gè)重要職責(zé)是向游戲的主控制器(GameManager)提供1張表示敵人出場(chǎng)順序的表,為此它提供了getEnemyMap()函數(shù):
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;
}
該函數(shù)返回的2維數(shù)組,表達(dá)的就是敵人的出場(chǎng)順序和出場(chǎng)位置。我們目前是這樣安排的,將屏幕均分為9列,每列的特定位置對(duì)應(yīng)2維數(shù)組中的1個(gè)整數(shù),當(dāng)數(shù)值為0時(shí),表示此處沒有敵人;當(dāng)數(shù)值為1到3之間的整數(shù)時(shí),分別代表此處將出現(xiàn)哪一種敵人。現(xiàn)在我們只有3種敵人:DUCK,F(xiàn)LY,PIG。



這1關(guān)卡只有1個(gè)BOSS,其類型為3型,對(duì)應(yīng)上面的PIG。我們可以看到,它只會(huì)在上面出場(chǎng)表的最后1行出現(xiàn)1次。
4.2 EnemyManager
關(guān)卡里的所有敵人最好能統(tǒng)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列表中會(huì)記錄關(guān)卡里產(chǎn)生的所有敵人,當(dāng)敵人被擊斃以后,程序會(huì)把相應(yīng)的Enemy對(duì)象從這張表中刪除。mEnemyMap記錄的其實(shí)就是前文所說的敵人的出場(chǎng)順序表。另外,為了便于創(chuàng)建Enemy對(duì)象,我們可以先創(chuàng)建1個(gè)EnemyFactory對(duì)象,并記入mEnemyFactory域。
另外,我們還需要管理所有Enemy發(fā)出的子彈,我們?yōu)镋nemyManager添加了mBulletsMgr域,意思很簡(jiǎn)單,往后每一個(gè)Enemy發(fā)射子彈時(shí),其實(shí)都是向這個(gè)BulletsManager添加子彈對(duì)象。與此同理,我們還需要1個(gè)記錄爆炸效果的爆炸管理器,那就是mExplodeMgr域。每當(dāng)1個(gè)Enemy被擊斃時(shí),它會(huì)向爆炸管理器中添加1個(gè)爆炸效果對(duì)象。
4.2.1 drawFrame()
EnemyManager的繪制動(dòng)作很簡(jiǎn)單,只需遍歷1下所記錄的Enemy列表,調(diào)用每一個(gè)Enemy對(duì)象的drawFrame()函數(shù)便可。
@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()
履行邏輯的動(dòng)作也差不多,都需要遍歷Enemy列表:
@Override
public void execLogic()
{
execAddEnemyLogic(); // 添加enemy的地方!
Iterator<Enemy> itor = mEnemyList.iterator();
while (itor.hasNext())
{
Enemy b = itor.next();
b.execLogic(); // 履行每一個(gè)enemy的execLogic。
}
// EnemyManager還需要負(fù)責(zé)清算“已死亡”的enemy
itor = mEnemyList.iterator();
while (itor.hasNext())
{
Enemy b = itor.next();
if (b.isDead())
{
itor.remove();
}
}
}
請(qǐng)注意,EnemyManager的execLogic()在1開始會(huì)調(diào)用execAddEnemyLogic()函數(shù),由于我們總需要1個(gè)地方添加關(guān)卡里的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個(gè)mEnemyCounter計(jì)數(shù)器,來控制添加enemy的頻率。幀流里每活動(dòng)1幀,耗時(shí)大概50毫秒(由于我們?cè)O(shè)的幀率是20幀/秒),那末24幀大概會(huì)耗時(shí)24 * 50 = 1200毫秒。也就是說,每過1.2秒,我們就會(huì)向EnemyManager里添加1行enemy。至于這1行里具體有甚么類型的enemy,是由mEnemyMap[ ]數(shù)組決定的。
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;
}
}
代碼很簡(jiǎn)單,先利用EnemyFactory根據(jù)不同的enemyType,創(chuàng)建相應(yīng)的enemy對(duì)象。然后為每一個(gè)enemy設(shè)置重要的關(guān)聯(lián)對(duì)象,比如mBulletsMgr、mExplodeMgr、mPlayer。這是由于enemy總是要發(fā)子彈的嘛,那末它每發(fā)1顆子彈,都要向“子彈管理器”里添加子彈對(duì)象。同理,當(dāng)enemy爆炸時(shí),它也會(huì)向“爆炸管理器”里添加1個(gè)爆炸效果對(duì)象。又由于enemy常常需要瞄準(zhǔn)玩家發(fā)射子彈,那末它就需要知道玩家的位置信息,因此setTarget(mPlayer)也是必要的。
接著我們將enemy對(duì)象添加進(jìn)EnemyManager的mEnemyList列表中。另外還需要為不同enemy設(shè)置不同的初始信息,比如初始位置、運(yùn)行速度等等。
4.3 BulletsManager
游戲中所有的子彈,不論是enemy發(fā)射的,還是玩家發(fā)射的,都必須添加進(jìn)“子彈管理器”加以保護(hù)。只不過為了便于處理,我們把enemy和玩家發(fā)射的子彈分別放在了不同的BulletsManager里。這就是為何在GameManager里,會(huì)有兩個(gè)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的同名函數(shù)很像。在execLogic()中,每當(dāng)發(fā)現(xiàn)1顆子彈已報(bào)廢了,就會(huì)把它從mBulletsList列表里刪除。嗯,用isDead()來表達(dá)子彈是不是報(bào)廢了好像不太貼切,不過大家應(yīng)當(dāng)都能夠理解吧,呵呵。
BulletsManager還得向外提供1個(gè)getBullets()函數(shù),以便外界進(jìn)行碰撞判斷。這個(gè)我們?cè)诤笪脑偌?xì)說。
4.4 ExplodeManager
爆炸效果管理器和子彈管理器的邏輯代碼差不多,所以我們就不貼它的execLogic()和drawFrame()的代碼了。
每一個(gè)爆炸效果會(huì)對(duì)應(yīng)1個(gè)Explode對(duì)象。由于爆炸效果1般都會(huì)表現(xiàn)為動(dòng)畫,所以Explode內(nèi)部必須記錄下自己當(dāng)前該繪制哪1張圖片了。在我們的程序里,爆炸資源圖以下:

這張爆炸圖會(huì)在Explode對(duì)象構(gòu)造之時(shí)傳入,而且外界會(huì)告知Explode對(duì)象,爆炸圖中總共有幾幀。Explode的構(gòu)造函數(shù)以下:
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();
}
每當(dāng)ExplodeManager遍歷履行每一個(gè)Explode對(duì)象的execLogic()時(shí),會(huì)改變當(dāng)前應(yīng)當(dāng)繪制的幀號(hào)。這樣當(dāng)游戲總幀流活動(dòng)時(shí),爆炸效果也就動(dòng)起來了。Explode的execLogic()函數(shù)以下:
public void execLogic()
{
mCurFrameIdx++;
if (mCurFrameIdx >= mTotalFrame)
{
mState = STATE_DEAD;
}
}
具體繪制爆炸幀時(shí),我們只需把爆炸圖中與mCurFrameIdx對(duì)應(yīng)的那1部份畫出來就能夠了,這就必須用到clipRect()。Explode的drawFrame()函數(shù)以下:
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開始計(jì)算的srcRect,表示的就是和mCurFrameIdx對(duì)應(yīng)的繪制部份。
其實(shí),不光是爆炸效果,我們的每類Enemy都是具有自己的動(dòng)畫的。它們的繪制機(jī)理和爆炸效果1致,我們就不贅述了。下面只貼出3類Enemy的角色動(dòng)畫圖:



4.5 Player
現(xiàn)在我們來看玩家控制的角色――Player類。它和Enemy最大的不同是,它是直接由玩家控制的。玩家想把它移到甚么地方,他就得乖乖地移到那個(gè)地方去,為此它必須能夠處理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;
}
注意,為了保證良好的用戶體驗(yàn),我們需要在用戶點(diǎn)擊屏幕之時(shí),先計(jì)算1下手指導(dǎo)擊處和Player對(duì)象當(dāng)前所在位置之間的偏移量,以后在處理ACTION_MOVE時(shí),還需用x、y減去偏移量。這樣,就不會(huì)出現(xiàn)Player對(duì)象從舊位置直接跳變得手指導(dǎo)擊處的情況。
4.5.2 碰撞判斷
現(xiàn)在我們來講說碰撞處理。在飛機(jī)游戲里,1種典型的碰撞情況就是被子彈擊中啦。對(duì)Player來講,它必須逐一判斷敵人發(fā)出的子彈,看自己是不是已和某個(gè)子彈密切接觸,如果是的話,那末Player就得減血,如果沒血可減了,就算被擊斃了。
對(duì)簡(jiǎn)單的游戲而言,我們只需判斷子彈所占的Rect范圍是不是和Player所占的Rect范圍有交集,如果是的話,就能夠認(rèn)為產(chǎn)生碰撞了。固然,為了增加1點(diǎn)兒趣味性,我們是用1個(gè)比Player Rect更小的矩形來和子彈Rect比對(duì)的,這樣可以出現(xiàn)1點(diǎn)兒子彈和Player擦身而過的驚險(xiǎn)效果。
在GameManager的execLogic()的最后1步,會(huì)調(diào)用execCollsionLogic()函數(shù)。該函數(shù)的代碼以下:
private void execCollsionLogic()
{
mPlayer.doWithCollision(mEnemyBulletsMgr);
mEnemyMgr.doWithCollision(mPlayerBulletsMgr);
}
意思很簡(jiǎn)單,Player需要和所有enemy發(fā)出的子彈進(jìn)行比對(duì),而每一個(gè)enemy需要和Player發(fā)出的子彈比對(duì)。我們只看Player的doWithCollision()函數(shù),代碼以下:
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());
}
}
}
其中那個(gè)effectRect就是比Player所占矩形更小1點(diǎn)兒的矩形啦。我們遍歷BulletsManager中的每一個(gè)子彈,1旦發(fā)現(xiàn)哪一個(gè)子彈和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;
}
}
}
如果寫得復(fù)雜1點(diǎn)兒的話,不同enemy發(fā)出的子彈的威力應(yīng)當(dāng)是不1樣的。不過在本游戲中,每顆子彈的威力都定為1了。也就是說,傳入doCollide()的power參數(shù)的值總為1。每次碰撞時(shí),Player就減1滴血(mMyHP -= power),然后立即跳變到STATE_ADJUST狀態(tài)或STATE_EXPLODE狀態(tài)。
另外一方面,enemy和Player發(fā)出的子彈也有類似的判斷,只是判斷條件更加寬松1些,這樣可以給玩家增加1點(diǎn)兒射擊的爽快感,呵呵。關(guān)于這部份的代碼我們就不重復(fù)貼了。
4.5.3 被擊中后的閃爍效果
Player需要完成的另外一個(gè)效果是被擊中后,閃爍1段很短的時(shí)間,在這段時(shí)間內(nèi),它會(huì)暫時(shí)處于無敵狀態(tài),這樣做可以免玩家出現(xiàn)被多顆子彈同時(shí)擊中而被瞬殺的情況。為此我們?cè)O(shè)計(jì)了1個(gè)“調(diào)劑狀態(tài)”,就是我們剛剛看到的STATE_ADJUST狀態(tài)啦。
1旦Player被擊中,只要它的mMyHP(血值)沒有減到0,那末它立即跳變到STATE_ADJUST。在這類狀態(tài)下,我們不再每次都繪制Player圖片了,而是隔1幀繪制1次,這樣就能夠到達(dá)閃爍的效果了。固然這個(gè)狀態(tài)的保持時(shí)間很短,我們會(huì)記錄1個(gè)mAdjustCounter計(jì)數(shù)變量,每次履行execLogic()會(huì)給這個(gè)計(jì)數(shù)器加1,直到加到6,我們就從STATE_ADJUST狀態(tài),跳變回普通狀態(tài)(STATE_ALIVE狀態(tài))。
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
飛機(jī)游戲還需要1個(gè)簡(jiǎn)單的“信息顯示板”,來顯示1些重要的信息。在本游戲中,我只顯示了Player的剩余血量(每滴血用1個(gè)紅心表示),大家有興趣可以再添加玩家分?jǐn)?shù)等信息。
我們?cè)O(shè)計(jì)的信息顯示板是GameInfoPanel,它的邏輯非常簡(jiǎn)單:
public void execLogic()
{
mPlayerHP = mPlayer.getHP();
}
只是簡(jiǎn)單地記錄1下Player的血量而已。
繪制時(shí),它根據(jù)所記錄的血量值繪制相應(yīng)的紅心圖片就能夠了:
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 尾聲
至此,我們已把這個(gè)小游戲的主要設(shè)計(jì)方面都講到了。固然,由于這個(gè)游戲只是我為了好玩而寫的1個(gè)demo程序,所以肯定有很多地方其實(shí)不完備,這個(gè)我想大家也是可以理解的。那末就先說這么多吧。最后讓我們來貼兩張游戲截圖,樂呵1下。

生活不易,碼農(nóng)辛苦
如果您覺得本網(wǎng)站對(duì)您的學(xué)習(xí)有所幫助,可以手機(jī)掃描二維碼進(jìn)行捐贈(zèng)