本章要點(diǎn):
本章我們將通過(guò)在Android平臺(tái)完成一個(gè)小游戲《小魚(yú)快跑》來(lái)學(xué)習(xí)Android游戲開(kāi)發(fā)。
6.1. 游戲策劃
在開(kāi)發(fā)一款游戲時(shí),我們的第一階段是進(jìn)行游戲策劃,這是一項(xiàng)比編寫(xiě)代碼更為重要的工作。在這個(gè)過(guò)程中,我們需要確定這款游戲的基本特征,比如是游戲類(lèi)型、用戶(hù)類(lèi)型、游戲風(fēng)格以及游戲可玩性等等,這將直接決定該游戲的后期制作和玩家的喜好程度。
游戲的開(kāi)發(fā)過(guò)程已經(jīng)隨著游戲行業(yè)的飛速發(fā)展被賦以更多的元素。在游戲開(kāi)發(fā)的初期,優(yōu)秀的作品往往只是代碼工作者短期的投入,不需要前期的準(zhǔn)備、調(diào)查等。但是目前游戲行業(yè)的狀況已經(jīng)不同于當(dāng)年,游戲平臺(tái)的多元化、用戶(hù)類(lèi)型的增加以及市場(chǎng)競(jìng)爭(zhēng)的激烈化等都需要我們將正規(guī)的商業(yè)流程納入游戲的開(kāi)發(fā)當(dāng)中,如果我們依然按照早期的開(kāi)發(fā)模式應(yīng)對(duì)當(dāng)前的游戲市場(chǎng),那么開(kāi)發(fā)出的游戲?qū)?huì)由于缺少一方面或多方面的特性而無(wú)法引起用戶(hù)的興趣。那么如何才能開(kāi)發(fā)一款受廣大用戶(hù)喜愛(ài)的游戲呢?這就需要我們?cè)诓邉澲斑M(jìn)行調(diào)查,首先確定市場(chǎng)的需求和目標(biāo)客戶(hù)。在明確了目標(biāo)客戶(hù)之后,是否需要考慮游戲類(lèi)型呢?從大的方面來(lái)說(shuō),游戲是單機(jī)的還是聯(lián)網(wǎng)的?是單人的還是多人的?是動(dòng)作類(lèi)型的還是角色扮演類(lèi)型的?等等。在確定了游戲的類(lèi)型之后,還需要考慮游戲的可玩性,總不可能說(shuō)游戲玩家十多分鐘就通關(guān)了,這就需要包括游戲的難度設(shè)置、關(guān)卡控制以及后期的版本控制。現(xiàn)在我們就可以根據(jù)上面的目標(biāo)客戶(hù)和游戲類(lèi)型來(lái)定義游戲的風(fēng)格了,比如想以三國(guó)為題材做一個(gè)即時(shí)戰(zhàn)略游戲,就不可能定義為現(xiàn)代風(fēng)格,游戲中就不會(huì)出現(xiàn)坦克、飛機(jī)的道具。同時(shí)這也體現(xiàn)了游戲的真實(shí)性,但是游戲本身是一個(gè)虛幻的東西,玩家就是需要將自己放到虛擬的世界中,所以也不能過(guò)度真實(shí),讓玩家覺(jué)得枯燥乏味。風(fēng)格確定之后,還可以根據(jù)游戲的風(fēng)格來(lái)配置游戲音效。由于玩家經(jīng)常會(huì)接觸到很多游戲,所以他在玩游戲時(shí)會(huì)對(duì)一些沒(méi)有新意的游戲感到厭倦,反正我玩過(guò)類(lèi)似的游戲,沒(méi)什么好玩的。如果得到玩家這樣的評(píng)論,那么大家都知道這款游戲的成敗了。雖然游戲創(chuàng)新并不是一件很容易的事情,但是為了吸引玩家,我們不得不大膽地創(chuàng)新。
這些問(wèn)題都解決了,就可以準(zhǔn)備寫(xiě)策劃案了。在寫(xiě)策劃案的同時(shí)還需要考慮到美工和程序在技術(shù)上的實(shí)現(xiàn)以及硬件的支持,不能設(shè)計(jì)技術(shù)達(dá)不到的效果。了解了這些問(wèn)題,開(kāi)始編寫(xiě)策劃案。
其實(shí)策劃是一個(gè)非常廣泛的領(lǐng)域,有很多東西需要自己在實(shí)踐中證明,這里只是列舉了常用的、值得注意的地方。
6.2.游戲資源
在策劃階段完成了游戲的前期準(zhǔn)備之后,我們就可以按照策劃文檔開(kāi)始游戲的實(shí)施。首先準(zhǔn)備游戲的資源,比如音效、界面,這些在公司內(nèi)都由專(zhuān)門(mén)的人士負(fù)責(zé),他們需要根據(jù)策劃文檔的描述來(lái)發(fā)揮自己的想象力,在保持和策劃文檔一致的情況下,進(jìn)行創(chuàng)新。同樣,為了保證美工的圖片適合程序的要求,還需要多和程序員進(jìn)行交流,以確保程序員能夠很清楚地理解自己的設(shè)計(jì),使游戲效果達(dá)到好。
6.3.游戲開(kāi)發(fā)
程序員在得到文檔和資源后并不能馬上打開(kāi)編輯器,新建工程開(kāi)始寫(xiě)代碼,而是要仔細(xì)查看文檔和資源,根據(jù)這些來(lái)確定所要使用的知識(shí)和所要實(shí)現(xiàn)的功能,然后構(gòu)建一個(gè)整體的框架。這個(gè)整體的框架很重要,一個(gè)優(yōu)秀的程序員會(huì)在框架的設(shè)計(jì)上花很多時(shí)間,因?yàn)橐粋(gè)好的框架可以使后面的開(kāi)發(fā)、調(diào)試等更加簡(jiǎn)單,同時(shí)一個(gè)好的框架還能提高游戲的運(yùn)行效率。為了保證質(zhì)量,每個(gè)程序員寫(xiě)的程序都有Bug,所以我們需要不斷地測(cè)試、修改,再測(cè)試、再修改,從而給玩家一個(gè)好的體驗(yàn)。
《小魚(yú)快跑》包括了游戲開(kāi)發(fā)中的大部分技術(shù),包括背景、精靈、圖層、音效等。下面是本章將完成的游戲在Android上的運(yùn)行效果。
6.3.1.游戲框架設(shè)計(jì)
前面已經(jīng)介紹了框架在游戲開(kāi)發(fā)中的重要地位,如何才能實(shí)現(xiàn)一個(gè)適合該游戲的框架呢?首先我們需要了解游戲的內(nèi)容,游戲中包括了地圖、主角、整個(gè)屏幕界面,顯示了地圖和主角的屬性,地圖上還有道具,至少需要一個(gè)視圖來(lái)顯示,并且需要更新界面的顯示和一個(gè)控制游戲邏輯及事件的類(lèi)。下面我們來(lái)構(gòu)建該游戲的整體框架。
在Android中要顯示一個(gè)視圖類(lèi)就必須繼承自View類(lèi),在本例中我們使用SurfaceView,SurfaceView可以直接從內(nèi)存或者DMA等硬件接口取得圖像數(shù)據(jù),因此是個(gè)非常重要的繪圖容器類(lèi)。在Android開(kāi)發(fā)中,布局資源通過(guò)setContentView被設(shè)置在res文件夾下,刷新率通過(guò)UI主線(xiàn)來(lái)控制。但是在游戲開(kāi)發(fā)中,這種刷新率遠(yuǎn)遠(yuǎn)不能滿(mǎn)足我們的需求,所以我們應(yīng)該自己掌控刷新率,當(dāng)然Android也想到了這一點(diǎn),所以提供了一個(gè)類(lèi)SurfaceView,使用SurfaceView我們可以通過(guò)自己的線(xiàn)程去控制屏幕的刷新頻率,以達(dá)到游戲中的效果。SurfaceView中包括一主要的繪制方法onDraw和一些事件的處理,本例中我們將所有顯示在屏幕上的對(duì)象通過(guò)屬性的不同歸類(lèi)到不同的圖層,并通過(guò)哈希表來(lái)存儲(chǔ)這些圖層,后通過(guò)圖層之間的順序、圖層中對(duì)象的順依次在屏幕上繪制。在構(gòu)建這個(gè)類(lèi)時(shí)還可以加入我們自己的一些方法,比如更新圖層(updatePicLayer)、對(duì)圖層的其他操作(removeDrawablePic、updateLayrIds)等。在SurfaceView中,整個(gè)圖層的繪制全部都是基于SurfaceView來(lái)實(shí)現(xiàn)的,我們可以從SurfaceView來(lái)獲得圖層并對(duì)其進(jìn)行繪制。有了這些內(nèi)容,下面構(gòu)建一個(gè)用于顯示游戲界面的視圖類(lèi)MainSurface。MainSurface類(lèi)的代碼如代碼清單6-1所示:
代碼清單6-1.MainSurface.java
public class MainSurface extends SurfaceView implements SurfaceHolder.Callback {
/**
* 修改圖層的操作定義
*/
//更新圖層
private final static int CHANGE_MODE_UPDATE = 0;
//添加元素到圖層
private final static int CHANGE_MODE_ADD = 1;
//刪除元素從圖層
private final static int CHANGE_MODE_REMOVE = 2;
// 圖片的圖層分布
private HashMap<Integer, ArrayList<Drawable>> picLayer =new HashMap<Integer, ArrayList<Drawable>>();
// 修改后的圖片的圖層分布,這里根據(jù)操作分為了兩個(gè)圖層,分別是添加的元素,和刪除的元素
private HashMap<Integer, ArrayList<Drawable>> addPicLayer = new HashMap<Integer, ArrayList<Drawable>>(),removePicLayer = new HashMap<Integer, ArrayList<Drawable>>();
// 是否修改過(guò)圖層
private boolean changeLayer = false;
private int picLayerId[] = new int[0]; // 定義一個(gè)圖層ID,加速獲取圖層繪制(省去了從map中獲取各個(gè)圖層排序問(wèn)題)
private Paint paint; // 畫(huà)筆
private OnDrawThread odt; // 屏幕繪制線(xiàn)程,用于控制繪制幀數(shù),周期性調(diào)用onDraw方法
private Typeface typeface;
public MainSurface(Context context) {
super(context);
typeface = Typeface.createFromAsset(context.getAssets(),"texttype/WhatsHappened.ttf");
this.getHolder().addCallback(this);
paint = new Paint();
paint.setTypeface(typeface); // 設(shè)置Paint的字體
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
paint.setTextSize(15); // 根據(jù)不同分辨率設(shè)置字體大小
paint.setColor(Color.WHITE);
odt = new OnDrawThread(this);
}
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
// TODO Auto-generated method stub
}
public void surfaceCreated(SurfaceHolder arg0) {
// TODO Auto-generated method stub
odt.start();
 nbsp; }
public void surfaceDestroyed(SurfaceHolder arg0) {
paint = null;
typeface = null;
picLayerId = null;
picLayer = null;
addPicLayer = null;
removePicLayer = null;
}
@Override
/**
* 繪圖方法,這個(gè)方法是由線(xiàn)程控制,周期性調(diào)用的
*/
public void onDraw(Canvas canvas) {
//更新圖層內(nèi)容
updatePicLayer(CHANGE_MODE_UPDATE,0,null);
// 遍歷所有圖層,按圖層先后順序繪制
for (int id : picLayerId) {
for (Drawable drawable : picLayer.get(id)) {
drawable.onDraw(canvas, paint);
}
}
//繪制LOGO
canvas.drawText("farsight android game demo. by XiloerFan", 0, 20, paint);
}
/**
* 更新圖層,這里分為三種操作,分別是更新臨時(shí)圖層中的內(nèi)容到繪制圖層中,刪除繪制圖層中的元素,添加繪制圖層中的元素
* 這里加了個(gè)線(xiàn)程鎖,保證多線(xiàn)程下操作圖層的安全性
* @param mode 對(duì)繪制圖層的操作類(lèi)型,對(duì)應(yīng)當(dāng)前類(lèi)的CHANGE_MODE常量
* @param layerId 操作的圖層ID
* @param draw 操作的圖層元素
*/
private synchronized void updatePicLayer(int mode,int layerId,Drawable draw){
switch(mode){
//將臨時(shí)圖層中的內(nèi)容更新至繪制圖層中
case CHANGE_MODE_UPDATE:
//如果有修改
if(changeLayer){
//向圖層添加新的元素
for(Integer id:addPicLayer.keySet()){
for(Drawable d:addPicLayer.get(id)){
//如果要添加的元素所處圖層不存在,則創(chuàng)建這個(gè)圖層,并更新圖層ID數(shù)組
if(this.picLayer.get(id)==null){
this.picLayer.put(id, new ArrayList<Drawable>());
updateLayerIds(id);
}
this.picLayer.get(id).add(d);
}
}
addPicLayer.clear();
//刪除圖層中的元素
for(Integer id:removePicLayer.keySet()){
for(Drawable d:removePicLayer.get(id)){
try {
this.picLayer.get(id).remove(d);
} catch (Exception e) {
System.out.println("圖層內(nèi)容不存在:"+id);
}
}
}
removePicLayer.clear();
changeLayer = false;
}
break;
/**
* 無(wú)論是向繪圖圖層中添加還是刪除元素,都不是直接操作繪制圖層,都是存放在對(duì)應(yīng)的臨時(shí)圖層中,等待繪制方法繪制周期中將變化的內(nèi)容更新到繪制圖層中
* 保證多線(xiàn)程操作情況下的安全性
*/
//添加一個(gè)元素
case CHANGE_MODE_ADD:
ArrayList<Drawable> al = addPicLayer.get(layerId);
if(al==null){
al = new ArrayList<Drawable>();
addPicLayer.put(layerId, al);
}
al.add(draw);
changeLayer = true;
break;
//刪除一個(gè)元素
nbsp; case CHANGE_MODE_REMOVE:
ArrayList<Drawable> al1 = removePicLayer.get(layerId);
if(al1==null){
al1 = new ArrayList<Drawable>();
removePicLayer.put(layerId, al1);
}
al1.add(draw);
changeLayer = true;
break;
}
}
/**
* 將一個(gè)可繪制的圖放入圖層中
*
* @param layer
*圖層號(hào) 圖層號(hào)雖然是int,但是實(shí)際上只支持到byte,原因是圖層沒(méi)有必要那么多
* @param pic
&nbnbsp; *可繪制的圖
*/
public void putDrawablePic(int layer, Drawable pic) {
if(pic==null){
System.out.println("圖層內(nèi)容不能為空:對(duì)應(yīng)圖層:"+layer);
return;
}
updatePicLayer(CHANGE_MODE_ADD,layer,pic);
}
/**
* 將一個(gè)可繪制的圖從圖層中移除
*
* @param layer
* @param pic
*/
public void removeDrawablePic(int layer, Drawable pic) {
if(pic==null){
&nbsnbsp;System.out.println("圖層內(nèi)容不能為空:對(duì)應(yīng)圖層:"+layer);
return;
}
updatePicLayer(CHANGE_MODE_REMOVE,layer,pic);
}
/**
* 更新圖層Id
*
* @param newLayerId
*/
private void updateLayerIds(int newLayerId) {
// 初始化圖層
if (picLayerId.length == 0) {
picLayerId = new int[1];
picLayerId[0] = newLayerId; // 將新的圖層ID添加到初始化的圖層ID數(shù)組中
} else {
// 創(chuàng)建一個(gè)新的圖層數(shù)組,長(zhǎng)度比原來(lái)的大1位
int picLayerIdFlag[] = new int[picLayerId.length + 1];
for (int i = 0; i < picLayerId.length; i++) {
// 排序操作,如果新的圖層ID小于當(dāng)前圖層ID,講新的圖層ID插入其中
if (picLayerId[i] > newLayerId) {
for (int f = picLayerIdFlag.length - 1; f > i; f--) {
picLayerIdFlag[f] = picLayerId[f - 1];
}
picLayerIdFlag[i] = newLayerId;
break;
} else {
picLayerIdFlag[i] = picLayerId[i];
}
// 如果到了后,都沒(méi)有比新圖層ID大的,就將新的圖層ID存入后
if (i == picLayerId.length - 1) {
picLayerIdFlag[picLayerIdFlag.length - 1] = newLayerId;
}
}
// 將新的圖層ID數(shù)組覆蓋原有的
this.picLayerId = picLayerIdFlag;
}
}
}
在創(chuàng)建和控制了圖層顯示之后,要讓游戲能夠動(dòng)起來(lái),需要開(kāi)啟一個(gè)線(xiàn)程來(lái)實(shí)時(shí)更新圖層顯示界面并刷新。下面我們將為游戲創(chuàng)建一個(gè)繪圖線(xiàn)程,可以通過(guò)sh.lockCanvas(null)方法來(lái)取得當(dāng)前顯示的圖層,然后根據(jù)不同的圖層來(lái)進(jìn)行游戲更新。線(xiàn)程的開(kāi)啟在MainSurface繼承的接口SurfaceHolder.CallBack中體現(xiàn):SurfaceCreated圖層創(chuàng)建時(shí)開(kāi)啟。代碼清單6-2所示為繪制圖層的線(xiàn)程:
代碼清單6-2. 控制繪圖的線(xiàn)程
public class OnDrawThread extends Thread{
private MainSurface surface;
private SurfaceHolder sh;
private int drawSpeed; &nbnbsp; //每次繪制后的休息毫秒數(shù),這個(gè)值是根據(jù)常量中的繪制幀數(shù)決定的
public OnDrawThread(MainSurface surface){
super();
this.surface = surface;
sh = surface.getHolder();
drawSpeed = 1000/Constant.ON_DRAW_SLEEP;
}
public void run(){
super.run();
Canvas canvas = null;
while(GamingInfo.getGamingInfo().isGaming()){
try{
canvas = sh.lockCanvas(null);
if(canvas!=null){
surface.onDraw(canvas);
}
}catch(Exception e){
Log.e(this.getName(), e.toString());
e.printStackTrace();
}finally{
try{
if(sh!=null){
sh.unlockCanvasAndPost(canvas);
}
}catch(Exception e){
Log.e(this.getName(), e.toString());
}
}
try{
Thread.sleep(drawSpeed);
}catch(Exception e){
}
}
 nbsp; }
}
在完成了這些模塊之后,就需要通知一個(gè)Activity來(lái)控制游戲的運(yùn)行,游戲開(kāi)始(onCreate)、游戲重置(onResume)、游戲暫停(onPause)、事件處理(onTouchEvent)等。整個(gè)游戲界面的顯示通過(guò)繪圖線(xiàn)程來(lái)控制:當(dāng)創(chuàng)建一個(gè)MainSurface對(duì)象時(shí),在MainSurface的構(gòu)造方法里面創(chuàng)建一個(gè)繪圖線(xiàn)程odt,同時(shí)SurfaceHolder.Callback監(jiān)聽(tīng)到MainSurface的創(chuàng)建時(shí)自動(dòng)調(diào)用surfaceCreate方法,在surfaceCreate中開(kāi)啟線(xiàn)程,開(kāi)始以雙緩沖模式繪制圖層。代碼清單6-3為GameActivity類(lèi)的處理:
代碼清單6-3. GameActivity.java
public class GameActivity extends Activity {
private MainSurface surface;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.requestWindowFeature(Window.FEATURE_NO_TITLE);//設(shè)置屏幕顯示沒(méi)有title
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
@Override
protected void onResume() {
super.onResume();
init(); //開(kāi)始初始化
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
// TODO Auto-generated method stub
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
/**
* 創(chuàng)建一個(gè)線(xiàn)程來(lái)異步初始化游戲內(nèi)容
*/
&nbsnbsp; new Thread(new Runnable(){
public void run() {
//使用游戲初始化管理器初始化游戲
GameInitManager.getGameInitManager().init();
}
}).start();
}
}
/**
* 初始化操作
*/
&nbsnbsp; private void init(){
/**
* 初始化繪圖層
*/
GamingInfo.clearGameInfo();
GamingInfo.getGamingInfo().setGaming(true);
GamingInfo.getGamingInfo().setActivity(this);
//獲得手機(jī)的寬度和高度像素單位為px
DisplayMetrics dm = new DisplayMetrics();
this.getWindowManager().getDefaultDisplay().getMetrics(dm);
if(dm.widthPixels<dm.heightPixels){
GamingInfo.getGamingInfo().setScreenWidth(dm.heightPixels);
GamingInfo.getGamingInfo().setScreenHeight(dm.widthPixels);
}else{
GamingInfo.getGamingInfo().setScreenWidth(dm.widthPixels);
GamingInfo.getGamingInfo().setScreenHeight(dm.heightPixels);
}
surface = new MainSurface(this);
GamingInfo.getGamingInfo().setSurface(surface);
this.setContentView(surface);
}
@Override
protected void onPause() {
//停止游戲相關(guān)活動(dòng)
GameInitManager.getGameInitManager().stop();
//清除共享數(shù)據(jù)對(duì)象
GamingInfo.clearGameInfo();
super.onPause();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(GameInitManager.getGameInitManager().isIniting()){
return super.onTouchEvent(event);
}
//屏幕被觸摸
if(event.getAction()==MotionEvent.ACTION_DOWN){
//先看布局管理器是否有相應(yīng)
if(LayoutManager.getLayoutManager().onClick(event.getRawX(), event.getRawY())){
return true;
}
//發(fā)射子彈
CannonManager.getCannonManager().shot(event.getRawX(), event.getRawY());
return true;
}
return super.onTouchEvent(event);
}
}
到這里我們基本完成了一個(gè)游戲的整體框架,值得一提的是,整個(gè)程序采用MVC架構(gòu),后面的所有游戲?qū)ο蠖贾恍枰^承自我們自定義的實(shí)體類(lèi)DrawableAdapter,然后在相應(yīng)的管理類(lèi)中判斷和更改當(dāng)前的游戲狀態(tài),程序便自動(dòng)找到我們需要更新和釋放的游戲?qū)ο筮M(jìn)行操作。
6.3.2.背景設(shè)計(jì)
小魚(yú)快跑的游戲背景設(shè)計(jì)比較簡(jiǎn)單,僅僅是一張背景圖的顯示,沒(méi)有參與游戲邏輯的處理。代碼清單6-4為游戲背景的實(shí)體類(lèi):
代碼清單6-4. BackGround.java
public class BackGround extends DrawableAdapter{
private Bitmap background;
public void setCurrentPic(Bitmap background){
this.background = background;
}
public Bitmap getCurrentPic() {
// TODO Auto-generated method stub
return background;
}
public int getPicWidth() {
// TODO Auto-generated method stub
return background.getWidth();
}
public int getPicHeight() {
// TODO Auto-generated method stub
nbsp; return background.getHeight();
}
}
6.3.3.精靈設(shè)計(jì)
游戲中的對(duì)象稱(chēng)為精靈,當(dāng)然精靈的范圍很廣,包括NPC、道具等。既然是精靈,必然有很多動(dòng)畫(huà),比如,小魚(yú)在游動(dòng)和被捕時(shí)應(yīng)該有不同的動(dòng)畫(huà),動(dòng)畫(huà)本身就是將圖片一幀一幀地連接起來(lái),循環(huán)地播放每一幀形成的。一般做游戲會(huì)把精靈作為一個(gè)單獨(dú)的類(lèi),本例中由于我們使用MVC架構(gòu),因此精靈的屬性和活動(dòng)分開(kāi)操作。我們定義一個(gè)可繪制接口Drawable,用一個(gè)實(shí)體類(lèi)DrawableAdapter來(lái)實(shí)現(xiàn)該接口,表示精靈的實(shí)體,后用相應(yīng)的管理類(lèi)來(lái)實(shí)現(xiàn)精靈的活動(dòng)。Drawable是個(gè)用來(lái)顯示圖像的類(lèi),是由一個(gè)圖像(可以有好幾幀,但是一次只有一個(gè)顯示)組成的(當(dāng)然DrawableAdapter還有其他的特性,每次只能使用一個(gè)圖像而不是多個(gè)圖像來(lái)填充屏幕是它的主要特征)。下面我們看看Drawable和DrawableAdapter類(lèi):
代碼清單6-5.Drawable.java
public interface Drawable {
public Matrix getPicMatrix();//獲取圖片旋轉(zhuǎn)的矩陣表示
public Bitmap getCurrentPic();//獲取當(dāng)前動(dòng)作圖片的資源
public int getPicWidth();//返回圖片的寬度
public int getPicHeight();//返回圖片的高度
public void onDraw(Canvas canvas,Paint paint);//繪制的回調(diào)方法
}
代碼清單6-6.DrawableAdapter.java
public abstract class DrawableAdapter implements Drawable{
private Matrix matrix = new Matrix();
public Matrix getPicMatrix() {
// TODO Auto-generated method stub
return matrix;
}
public void onDraw(Canvas canvas, Paint paint) {
canvas.drawBitmap(this.getCurrentPic(),
this.getPicMatrix(), paint);
}
}
在Drawable類(lèi)中,getCurrentPic()得到游戲?qū)ο螽?dāng)前的動(dòng)作圖片,getPicMatrix()得到處理游戲?qū)ο蟮木仃嚒T贒rawableAdapter類(lèi)中,onDraw方法用來(lái)實(shí)現(xiàn)游戲?qū)ο蟮睦L制。
6.3.3.1.游戲?qū)ο髽?gòu)造
本游戲中的對(duì)象包括小魚(yú)、子彈、金幣等。下面我們以小魚(yú)為例介紹游戲?qū)ο蟮臉?gòu)造:
1. 常量、屬性定義
代碼清單6-7. Fish類(lèi)常量、屬性定義
/**
* 常量定義
*/
public static final int ROTATE_DIRECTION_LEFT = 1; //左轉(zhuǎn)
public static final int ROTATE_DIRECTION_RIGHT = 2; //右轉(zhuǎn)
/**
* 引用類(lèi)型屬性定義
*/
private FishInfo fishInfo; //當(dāng)前魚(yú)的細(xì)節(jié)配置信息
private Bitmap[] fishActs; //當(dāng)前魚(yú)的所有動(dòng)作
private Bitmap[] fishCatchActs; //當(dāng)前魚(yú)的所有被捕獲動(dòng)作
private PicActThread picActThread; // 創(chuàng)建當(dāng)前魚(yú)的動(dòng)作線(xiàn)程
/**
* 簡(jiǎn)單類(lèi)型屬性定義
*/
private int currentPicAct = 0; //當(dāng)前動(dòng)作索引值
private int currentCatchPicAct = 0; //當(dāng)前被捕捉動(dòng)作索引值
private boolean isAlive = true; //當(dāng)前魚(yú)是否活著
private float distanceHeadFishX; //距領(lǐng)頭魚(yú)X偏移量
private float distanceHeadFishY; //距領(lǐng)頭魚(yú)Y偏移量
private HeadFish headFish; //領(lǐng)頭魚(yú)
private boolean canRun; //魚(yú)是否可以移動(dòng)
private int[] fishOutlinePoint = new int[4]; //魚(yú)的外接矩形,x的小值,大值,Y的小值,大值
常量定義中定義小魚(yú)游動(dòng)的方向——左、右;引用類(lèi)型中,定義小魚(yú)的活動(dòng)信息——當(dāng)前魚(yú)的細(xì)節(jié)配置信息、當(dāng)前魚(yú)的所有動(dòng)作、當(dāng)前與的所有被捕動(dòng)作、控制當(dāng)前魚(yú)動(dòng)作的線(xiàn)程。
2. 事件處理
當(dāng)小魚(yú)被捕時(shí)觸發(fā)一個(gè)事件,程序由此做出響應(yīng)的處理——捕捉動(dòng)作變換、小魚(yú)總數(shù)變換等。
代碼清單6-8.Fish類(lèi)事件處理
/**
* 觸發(fā)已被捕捉事件的響應(yīng)方法
* 當(dāng)調(diào)用了這個(gè)方法,說(shuō)明這條魚(yú)已經(jīng)被捕捉了
*/
public void onCatched(Ammo ammo,final float targetX,final float targetY){
this.setAlive(false);
new Thread(new Runnable() {
public void run() {
try{
float fishX = getHeadFish().getFish_X()-getDistanceHeadFishX();
float fishY = getHeadFish().getFish_Y()-getDistanceHeadFishY();
GamingInfo.getGamingInfo().getFish().remove(Fish.this);
Thread.sleep(1800);
//調(diào)用增加分?jǐn)?shù)方法
ScoreManager.getScoreManager().addScore(getFishInfo().getWorth(), fishX, fishY);
Thread.sleep(200);
Fish.this.getPicActThread().stopPlay();
GamingInfo.getGamingInfo().getSurface().removeDrawablePic(Fish.this.getFishInfo().getFishInLayer(), Fish.this);
}catch(Exception e){
Log.e("Fish_onCatched", e.toString());
}
}
}).start();
}
3. 基本信息處理
代碼清單6-9.Fish類(lèi)基本信息處理
public Fish(){
}
public Fish(Bitmap[] fishActs,Bitmap[] fishCatchActs,FishInfo fishInfo){
this.fishActs = fishActs;
this.fishCatchActs = fishCatchActs;
this.fishInfo = fishInfo;
this.getPicMatrix().setTranslate(-500, -500);
}
//是否處于活動(dòng)狀態(tài)(在屏幕中游著)
public boolean isAlive() {
// TODO Auto-generated method stub
return isAlive;
}
//設(shè)置是否處于活動(dòng)狀態(tài)
public void setAlive(boolean isAlive) {
// TODO Auto-generated method stub
this.isAlive = isAlive;
}
/**
* 魚(yú)旋轉(zhuǎn)點(diǎn)的X坐標(biāo)
*/
public int getFishRotatePoint_X() {
return getCurrentPic().getWidth()/2;
}
/**
* 魚(yú)旋轉(zhuǎn)點(diǎn)的Y坐標(biāo)
*/
public int getFishRotatePoint_Y() {
return getCurrentPic().getHeight()/2;
}
public PicActThread getPicActThread() {
return picActThread;
}
public void setPicActThread(PicActThread picActThread) {
this.picActThread = picActThread;
}
public float getDistanceHeadFishX() {
return distanceHeadFishX;
}
public void setDistanceHeadFishX(float distanceHeadFishX) {
this.distanceHeadFishX = distanceHeadFishX;
}
public float getDistanceHeadFishY() {
return distanceHeadFishY;
}
public void setDistanceHeadFishY(float distanceHeadFishY) {
this.distanceHeadFishY = distanceHeadFishY;
}
/**
* 獲取所有的動(dòng)作數(shù)量
* @return
*/
public int getFishActs() {
// TODO Auto-generated method stub
if(isAlive()){
return fishActs.length;
}else{
return fishCatchActs.length;
}
}
/**
* 設(shè)置當(dāng)前動(dòng)作圖片的資源ID
* @param picId
*/
public void setCurrentPicId(int picId) {
if(isAlive()){
this.currentPicAct = picId;
}else{
this.currentCatchPicAct = picId;
}
}
public int getCurrentPicId() {
if(isAlive()){
return currentPicAct;
}else{
return currentCatchPicAct;
}
}
public Bitmap getCurrentPic() {
// TODO Auto-generated method stub
if(isAlive()){
return fishActs[currentPicAct];
}else{
return fishCatchActs[currentCatchPicAct];
}
}
public int getPicWidth() {
// TODO Auto-generated method stub
return getCurrentPic().getWidth();
}
public int getPicHeight() {
// TODO Auto-generated method stub
return getCurrentPic().getHeight();
}
/**
* 設(shè)置魚(yú)的所有動(dòng)作
* @param fishActs
*/
public void setFishActs(Bitmap[] fishActs) {
// TODO Auto-generated method stub
this.fishActs = fishActs;
}
/**
* 設(shè)置魚(yú)的所有被捕獲動(dòng)作
* @param fishCatchActs
*/
public void setFishCatchActs(Bitmap[] fishCatchActs) {
// TODO Auto-generated method stub
this.fishCatchActs = fishCatchActs;
}
public FishInfo getFishInfo() {
return fishInfo;
}
public void setFishInfo(FishInfo fishInfo) {
this.fishInfo = fishInfo;
}
/**
* 根據(jù)當(dāng)前魚(yú)獲取同類(lèi)魚(yú)實(shí)例
* @return
*/
public Fish getFish(){
return new Fish(this.fishActs,this.fishCatchActs,this.fishInfo);
}
/**
* 觸發(fā)捕捉事件的響應(yīng)方法
*/
public void onCatch(Ammo ammo,final float targetX,final float targetY){
// System.out.println("魚(yú)被捕捉了,但是沒(méi)有捕捉到");
}
public HeadFish getHeadFish() {
return headFish;
}
&nbsnbsp; public void setHeadFish(HeadFish headFish) {
this.headFish = headFish;
}
public int[] getFishOutlinePoint() {
return fishOutlinePoint;
}
public boolean isCanRun() {
return canRun;
}
public void setCanRun(boolean canRun) {
this.canRun = canRun;
}
6.3.3.2.游戲?qū)ο蠊芾?/p>
仍然以小魚(yú)為例,我們使用單例模式對(duì)魚(yú)對(duì)象進(jìn)行管理。以下只列出管理類(lèi)的屬性和方法的定義:
代碼清單6-10.FishManager.java
/**
* 魚(yú)的管理器
* @author Xiloerfan
*
*/
public class FishManager {
/**
* 單利模式
*/
private static FishManager fishManager;
private FishManager();
public static FishManager getFishMananger();
/**
* 根據(jù)名字保存所有魚(yú)的配置信息
*/
private HashMap<String,FishInfo> allFishConfig = new HashMap<String,FishInfo>();
/**
* 根據(jù)名字保存所有魚(yú)的動(dòng)作配置信息
*/
private HashMap<String,ActConfig[]> allFishActConfigs = new HashMap<String,ActConfig[]>();
/**
* 根據(jù)名字保存所有魚(yú)的捕獲動(dòng)作配置信息
*/
private HashMap<String,ActConfig[]> allFishCatchActConfigs = new HashMap<String,ActConfig[]>();
/**
* 根據(jù)名字緩存的魚(yú)的動(dòng)作圖片
*/
private HashMap<String,Bitmap[]> allFishActs = new HashMap<String,Bitmap[]>();
/**
* 根據(jù)名字緩存的魚(yú)的捕獲動(dòng)作圖片
*/
private HashMap<String,Bitmap[]> allFishCatchActs = new HashMap<String,Bitmap[]>();
/**
* 魚(yú)的種類(lèi)
*/
&nbsnbsp; private ArrayList<String> allFish = new ArrayList<String>();
/**
* 根據(jù)XML配置文件,初始化所有魚(yú)
* 這里的配置文件還沒(méi)定義,只是寫(xiě)在代碼里了,以后可以改成通過(guò)讀取配置文件來(lái)加載不同
* 資源的魚(yú)
* @param initXml
*/
/**
* 是否可以創(chuàng)建新的魚(yú)
* 這個(gè)值的改變?cè)谝韵聲?huì)發(fā)生:
* 每當(dāng)調(diào)用updateFish方法時(shí),會(huì)將這個(gè)值設(shè)置為false
* updateFish方法執(zhí)行完畢時(shí),會(huì)將這個(gè)值在改變回true
*/
private boolean createable = false;
/**
* 初始化管理器
* 這里會(huì)讀取fish文件夾下的FishConfig.plist文件,來(lái)加載所有其他配置信息
*/
public void initFish();
/**
* 根據(jù)魚(yú)的名字獲取一條魚(yú)的實(shí)例
* @param fishName
* @return
 nbsp; */
public Fish birthFishByFishName(String fishName);
/**
* 更新加載的魚(yú)
* @param fish
*/
public void updateFish(String []fish);
/**
* 獲取所有魚(yú)的名字
* @return
*/
&nnbsp; public ArrayList<String> getAllFishName();
/**
* 銷(xiāo)毀釋放資源
*/
public static void destroy();
/**
* 設(shè)置魚(yú)的動(dòng)作到管理器魚(yú)動(dòng)作結(jié)構(gòu)中
* @param fishName
* @param fishActs
* @return true:放置成功 false:放置失敗
*/
private boolean getFishByName(String fishName,HashMap<String,ActConfig> configs);
/**
* 獲取魚(yú)的游動(dòng)圖片集
* @param fishName
* @return
*/
private Bitmap[] getFishActByFishName(String fishName);
/**
* 獲取魚(yú)的被捕獲圖片集
* @param fishName
* @return
*/
private Bitmap[] getFishCatchActsByFishName(String fishName);
/**
* 初始化魚(yú)的配置信息
* @param config
*/
private void initFishInfo(String config);
/**
* 初始化魚(yú)的動(dòng)作信息
* @param configs 將解析出來(lái)的每個(gè)配置文件放入這個(gè)Map中
* @param fishActConfiges 所有的配置文件名稱(chēng)
*/
&nbnbsp; private void initFishAct(HashMap<String,ActConfig> configs,String fishActConfiges[]);
6.3.3.3.游戲?qū)ο筮壿嬏幚?/p>
游戲核心的部分是玩家體驗(yàn)的過(guò)程,而對(duì)這一過(guò)程的處理屬于游戲的邏輯部分。我們?cè)贏ndroid游戲開(kāi)發(fā)中通常使用線(xiàn)程來(lái)實(shí)現(xiàn)。
1.移動(dòng)魚(yú)群
a) 移動(dòng)
魚(yú)群的移動(dòng)在FishRunThread中實(shí)現(xiàn),下面是實(shí)現(xiàn)該功能的代碼:
代碼清單6-11.移動(dòng)魚(yú)群
/**
* 移動(dòng)魚(yú)群
*/
private void moveShoal(){
try{
if(fish.getShoal()==null){
return;
}
for(Fish fishFlag:fish.getShoal()){
if(!fishFlag.isCanRun()||!fishFlag.isAlive()){
continue;
}
fishFlag.getFishOutlinePoint()[0] = (int)(fish.getFishOutlinePoint()[0]-fishFlag.getDistanceHeadFishX());
fishFlag.getFishOutlinePoint()[1] = (int)(fish.getFishOutlinePoint()[1]-fishFlag.getDistanceHeadFishX());
fishFlag.getFishOutlinePoint()[2] = (int)(fish.getFishOutlinePoint()[2]-fishFlag.getDistanceHeadFishY());
fishFlag.getFishOutlinePoint()[3] = (int)(fish.getFishOutlinePoint()[3]-fishFlag.getDistanceHeadFishY());
fishFlag.getPicMatrix().setTranslate(fish.getFish_X()-fishFlag.getDistanceHeadFishX(), fish.getFish_Y()-fishFlag.getDistanceHeadFishY());
fishFlag.getPicMatrix().preRotate(fish.getCurrentRotate(),fishFlag.getFishRotatePoint_X(),fishFlag.getFishRotatePoint_Y());
}
}catch(Exception e){
nbsp; }
}
對(duì)魚(yú)群移動(dòng)的處理包括根據(jù)給定長(zhǎng)度走直線(xiàn)(goStraight)、旋轉(zhuǎn)魚(yú)的角度并移動(dòng)(rotateRightFish、rotateLeftFish)、設(shè)置魚(yú)的外接矩形(setFishOutlinePoint),具體實(shí)現(xiàn)見(jiàn)工程。
b)碰撞檢測(cè)
當(dāng)魚(yú)群超出屏幕邊界時(shí),程序也作出相應(yīng)的操作:isAtOut和checkFishAtOut用于檢測(cè),isAtOut判斷魚(yú)是否部分在屏幕外,checkFishAtOut檢測(cè)魚(yú)是否完全在屏幕外。setFishAtOut用于進(jìn)行當(dāng)魚(yú)群處于邊界外的操作,以下給出setFishOut函數(shù)的代碼:
代碼清單6-12. setFishAtOut
/**
* 處理魚(yú)出了邊界后的操作
*/
private void setFishAtOut() {
fishIsOut = true;
new Thread(new Runnable(){
public void run() {
try{
// TODO Auto-generated method stub
//如果領(lǐng)頭魚(yú)有魚(yú)群
for(Fish fishFlag:fish.getShoal()){
while(GamingInfo.getGamingInfo().isGaming()){
if(checkFishAtOut(fish,fishFlag)){
GamingInfo.getGamingInfo().getFish().remove(fishFlag);
GamingInfo.getGamingInfo().getSurface().removeDrawablePic(fishFlag.getFishInfo().getFishInLayer(), fishFlag);
fishFlag.getPicActThread().stopPlay();//停止動(dòng)作
break;
}
try{
Thread.sleep(10);
}catch(Exception e){
e.printStackTrace();
}
}
 nbsp; }
//讓魚(yú)群移動(dòng)線(xiàn)程停掉
setRun(false);
//通知魚(yú)群管理器,這條魚(yú)已經(jīng)離開(kāi)屏幕
if(GamingInfo.getGamingInfo().isGaming()){
GamingInfo.getGamingInfo().getShoalManager().notifyFishIsOutOfScreen();
}
}catch(Exception e){
LogTools.doLogForException(e);
}
}
}).start();
}
2.子彈
子彈的處理在ShotTread類(lèi)中實(shí)現(xiàn):
代碼清單6-13.ShotTread.java
public class ShotThread extends Thread {
private float targetX;
private float targetY;
private float currentX;
private float currentY;
private float ammoRotateX;
private float ammoRotateY;
private float speed_x; // 取一個(gè)近似值,代表每幀移動(dòng)的像素?cái)?shù)
private float speed_y;
private int ammo_speed = 1000 / Constant.ON_DRAW_SLEEP; // 子彈繪制速度,這個(gè)與屏幕刷新速度一樣
private Ammo ammo; //子彈
private boolean ammoActIsRun; //子彈動(dòng)畫(huà)是否播放
&nbsnbsp; public ShotThread(float targetX, float targetY, Ammo ammo,float fromX,float fromY) {
this.ammo = ammo;
currentX = fromX;
currentY = fromY;
ammoRotateX = ammo.getPicWidth()/2;
ammoRotateY = ammo.getPicHeight()/2;
this.targetX = targetX;
this.targetY = targetY;
float x = Math.abs(this.targetX - fromX); // 獲取目標(biāo)距離子彈始發(fā)的X坐標(biāo)長(zhǎng)度
float y = Math.abs(this.targetY - fromY); // 獲取目標(biāo)距離子彈始發(fā)的Y坐標(biāo)長(zhǎng)度
float len = (float) Math.sqrt(x * x + y * y); // 目標(biāo)和始發(fā)點(diǎn)之間的距離
float time = len / (Constant.AMMO_SPEED / Constant.ON_DRAW_SLEEP); &nbnbsp; // 計(jì)算目標(biāo)與始發(fā)之間子彈需要行走的幀數(shù)
speed_x = x / time; // 計(jì)算子彈沿X軸行進(jìn)的增量
speed_y = y / time; // 計(jì)算子彈沿Y軸行進(jìn)的增量
if (targetX < fromX) {
speed_x = -speed_x;
}
if (targetY < fromY) {
speed_y = -speed_y;
}
}
public void run() {
try{
//如果子彈幀數(shù)多于1,就播放子彈動(dòng)畫(huà)
if(ammo.getAmmoPicLenght()>1){
new Thread(this.playAmmoAct()).start();
}
// 計(jì)算子彈需要的旋轉(zhuǎn)角度
float angle = Tool.getAngle(targetX, targetY, currentX, currentY);
AmmoParticleEffect effect = ParticleEffectManager.getParticleEffectManager().getAmmoEffect();
int ammoRedius = ammo.getPicHeight()/2;//這個(gè)半徑的作用是用于計(jì)算子彈尾巴處出現(xiàn)粒子使用
effect.playEffect((float)(ammoRedius*Math.cos(Math.toRadians(angle+180)))+ammoRotateX,-(float)(ammoRedius*Math.sin(Math.toRadians(angle+180)))+ammoRotateY,currentX, currentY, speed_x, speed_y);
// 計(jì)算子彈的旋轉(zhuǎn)(原理與大炮一樣)
if (angle >= 90) {
angle = -(angle - 90);
} else {
angle = 90 - angle;
}
// 創(chuàng)建變換矩陣
 nbsp; Matrix matrix = ammo.getPicMatrix();
matrix.setTranslate(currentX, currentY);
matrix.preRotate(angle,ammoRotateX,ammoRotateY);
GamingInfo.getGamingInfo().getSurface()
.putDrawablePic(Constant.AMMO_LAYER, ammo); // 將子彈放入圖層,等待被繪制
// 根據(jù)增量移動(dòng)子彈
while (GamingInfo.getGamingInfo().isGaming()) {
while(!GamingInfo.getGamingInfo().isPause()){
matrix.reset();
matrix.setTranslate(currentX, currentY);
matrix.preRotate(angle,ammoRotateX,ammoRotateY);
currentX += speed_x;
currentY += speed_y;
effect.setEffectMatrix(currentX,currentY);
if (checkHit()) {
effect.stopEffect();
// 命中后刪除這個(gè)子彈
GamingInfo.getGamingInfo().getSurface()
.removeDrawablePic(Constant.AMMO_LAYER, ammo);
CatchFishManager.getCatchFishManager().catchFishByAmmo(currentX, currentY, ammo);
// 如果超出屏幕,從圖層中刪除子彈
GamingInfo.getGamingInfo().getSurface()
.removeDrawablePic(Constant.AMMO_LAYER, ammo);
this.ammoActIsRun = false;//停止子彈動(dòng)畫(huà)
break;
} else if (currentX - 100 >= GamingInfo.getGamingInfo().getScreenWidth()
|| currentX + 100 <= 0 || currentY + 100 <= 0) {
// 如果超出屏幕,從圖層中刪除子彈
effect.stopEffect();
GamingInfo.getGamingInfo().getSurface()
.removeDrawablePic(Constant.AMMO_LAYER, ammo);
this.ammoActIsRun = false;//停止子彈動(dòng)畫(huà)
break;
}
try {
Thread.sleep(ammo_speed);
} catch (Exception e) {
}
}
break;
}
}catch(Exception e){
LogTools.doLogForException(e);
}
}
private Runnable playAmmoAct(){
Runnable runnable = new Runnable(){
public void run() {
ammoActIsRun = true;
int picIndex = 0;
try {
while(GamingInfo.getGamingInfo().isGaming()){
while(!GamingInfo.getGamingInfo().isPause()&&ammoActIsRun){
ammo.setCurrentId(picIndex);
picIndex++;
if(picIndex==ammo.getAmmoPicLenght()){
picIndex=0;
}
Thread.sleep(200);
}
break;
}
} catch (Exception e) {
// TODO: handle exception
}
}
};
return runnable;
}
private boolean checkHit() {
try{
ArrayList<Fish> allFish = (ArrayList<Fish>)GamingInfo.getGamingInfo().getFish().clone();
for (Fish fish : allFish) {
if (currentX > fish.getFishOutlinePoint()[0]
&& currentX < fish.getFishOutlinePoint()[1]
&& currentY >gt; fish.getFishOutlinePoint()[2]
&& currentY < fish.getFishOutlinePoint()[3]) {
return true;
}
}
}catch(Exception e){
LogTools.doLogForException(e);
}
return false;
}
}
6.3.4. 游戲特效
除了游戲?qū)ο蟮奶幚恚覀冞添加了簡(jiǎn)單的粒子系統(tǒng)、水紋效果來(lái)增強(qiáng)玩家的游戲體驗(yàn)。
6.3.4.1.粒子系統(tǒng)
粒子系統(tǒng)主要體現(xiàn)子彈的粒子效果、漁網(wǎng)的粒子效果、金幣的粒子效果,子彈的粒子效果在AmmoParticleEffect類(lèi)中實(shí)現(xiàn),漁網(wǎng)的粒子效果在在NetParticleEffect類(lèi)中實(shí)現(xiàn),金幣粒子效果在GoldParticleEffect中實(shí)現(xiàn),粒子管理器是ParticleEffectManager類(lèi),以下只給出子彈粒子效果的實(shí)現(xiàn)代碼:
代碼清單6-14.NetParticleEffect.java
/**
* 子彈粒子效果
* @author Xiloer
*
*/
public class AmmoParticleEffect extends DrawableAdapter{
private static byte ADD = 1;
private static byte REMOVE = 2;
private static byte UPDATE = 3;
//粒子彩色圖
private Bitmap effectImgs[];
private ArrayList<Particle> effects = new ArrayList<Particle>();
private ArrayList<Particle> news = new ArrayList<Particle>();
private ArrayList<Particle> removes = new ArrayList<Particle>();
private int indexByDraw;//這個(gè)值用于繪制方法循環(huán)使用
private Particle particle;//這個(gè)值用于繪制方法循環(huán)使用
private boolean isPlay;//是否播放粒子效果
private float targetOffsetX,targetOffsetY;//距離當(dāng)前坐標(biāo)的偏移量,這兩個(gè)值加上currentX,currentY來(lái)得到粒子初始位置
private float currentX,currentY;
public AmmoParticleEffect(Bitmap effectImgs[]){
this.effectImgs = effectImgs;
}
/**
* 播放一次粒子效果
* @param x 粒子的生成位置X
* @param y 粒子的生成位置Y
* @param offX 粒子偏移量X 這兩個(gè)值是生成粒子時(shí)的行動(dòng)路線(xiàn),這個(gè)應(yīng)該和給定的物體的偏移量相反
* @param offY 粒子偏移量Y
*/
public void playEffect(float targetOffsetX,float targetOffsetY,float x,float y,float offX,float offY){
try{
isPlay = true;
this.targetOffsetX = targetOffsetX;
this.targetOffsetY = targetOffsetY;
startCreateEffectThread(x,y,offX,offY);
GamingInfo.getGamingInfo().getSurface().putDrawablePic(Constant.PARTICLE_EFFECT_LAYER, this);
}catch(Exception e){
LogTools.doLogForException(e);
}
}
private void updateEffect(byte mode,Particle p){
if(mode==ADD){
news.add(p);
}else if(mode==REMOVE){
removes.add(p);
}else if(mode == UPDATE){
if(news.size()>0){
effects.addAll(news);
news.clear();
}
if(removes.size()>0){
effects.removeAll(removes);
removes.clear();
}
}
}
/**
* 啟動(dòng)產(chǎn)生粒子的線(xiàn)程
*/
private void startCreateEffectThread(final float x,final float y,final float offX,final float offY){
this.currentX = x;
this.currentY = y;
new Thread(new Runnable() {
public void run() {
try{
while(GamingInfo.getGamingInfo().isGaming()){
while(!GamingInfo.getGamingInfo().isPause()&&isPlay){
updateEffect(ADD,new Particle(currentX,currentY,offX,offY,0.5f,effectImgs[(int)(Math.random()*effectImgs.length)]));
Thread.sleep((long)(Math.random()*201));
}
break;
}
}catch(Exception e){
LogTools.doLogForException(e);
}
 nbsp; }
}).start();
}
@Override
public void onDraw(Canvas canvas, Paint paint) {
updateEffect(UPDATE,null);
indexByDraw = 0;
while(GamingInfo.getGamingInfo().isGaming()){
while(!GamingInfo.getGamingInfo().isPause()&&isPlay&&indexByDraw<effects.size()){
particle = effects.get(indexByDraw);
canvas.drawBitmap(particle.effect, particle.matrix, paint);
indexByDraw++;
}
break;
}
}
/**
* 停止播放粒子
*/
public void stopEffect(){
this.isPlay = false;
GamingInfo.getGamingInfo().getSurface().removeDrawablePic(Constant.PARTICLE_EFFECT_LAYER, this);
}
/**
* 設(shè)置粒子位置
*/
public void setEffectMatrix(float currentX,float currentY){
this.currentX = currentX;
this.currentY = currentY;
Particle particle;
for(int i =0;i<effects.size();i++){
particle = effects.get(i);
particle.offX -=particle.offX*0.05f;
particle.offY -=particle.offY*0.05f;
particle.scale -=particle.scale*0.05f;
particle.currentX = particle.currentX+particle.offX;
particle.currentY = particle.currentY+particle.offY;
particle.matrix.setTranslate(particle.currentX, particle.currentY);
particle.matrix.preScale(particle.scale, particle.scale);
if(particle.scale<0.1){
updateEffect(REMOVE,particle);
}
}
}
public Bitmap getCurrentPic() {
// TODO Auto-generated method stub
return null;
}
public int getPicWidth() {
// TODO Auto-generated method stub
return 0;
}
public int getPicHeight() {
// TODO Auto-generated method stub
return 0;
}
/**
* 粒子對(duì)象
* @author Xiloer
*
*/
private class Particle{
private Bitmap effect;
/**
* 當(dāng)前粒子坐在坐標(biāo)X
*/
public float currentX;
/**
* 當(dāng)前粒子坐在坐標(biāo)Y
*/
public float currentY;
/**
* 偏移量X
*/
public float offX;
/**
* 偏移量Y
*/
public float offY;
/**
nbsp; * 縮放
*/
public float scale;//縮放基數(shù)
/**
* 粒子矩陣
*/
public Matrix matrix = new Matrix();
/**
*
* @param currentX
* @param currentY
* @param offX
* @param offY
*/
public Particle(float currentX,float currentY,float offX,float offY,float scale,Bitmap effect){
this.offX = offX;
this.offY = offY;
this.scale = scale;
this.currentX = currentX-effect.getWidth()/2*scale+targetOffsetX;
this.currentY = currentY-effect.getHeight()/2*scale+targetOffsetY;
this.matrix.setTranslate(this.currentX, this.currentY);
this.matrix.preScale(scale, scale);
this.effect = effect;
}
}
}
6.3.4.2. 水紋效果
水波紋的實(shí)現(xiàn)在3D游戲中比較復(fù)雜,涉及到各種光線(xiàn)的處理。在2D游戲中相對(duì)簡(jiǎn)單,我們只需要對(duì)圖片進(jìn)行簡(jiǎn)單的處理即可。小魚(yú)快跑中的水波紋在WaterRipper類(lèi)中實(shí)現(xiàn):
代碼清單6-15.水波紋
/**
* 水波紋
* @author Xiloer
*
*/
public class WaterRipple extends DrawableAdapter{
private Bitmap[] ripple;
private int currentId;
public WaterRipple(Bitmap[] ripple){
this.ripple = ripple;
}
public void setCurrentId(int currentId) {
this.currentId = currentId;
}
public Bitmap getCurrentPic() {
// TODO Auto-generated method stub
return ripple[currentId];
}
public int getPicWidth() {
// TODO Auto-generated method stub
nbsp; return getCurrentPic().getWidth();
}
public int getPicHeight() {
// TODO Auto-generated method stub
return getCurrentPic().getHeight();
}
}
6.3.5. 游戲音效
游戲中不可或缺的另一個(gè)方面就是音效,音效容易讓玩家將自己融入其中,隨著游戲的節(jié)奏喜怒哀樂(lè)。游戲開(kāi)發(fā)的高境界就是能帶動(dòng)玩家的情緒,如果游戲沒(méi)有音效,會(huì)是一個(gè)什么樣的情況呢?可能總是感覺(jué)缺少什么一樣,玩家不會(huì)如此輕易地進(jìn)入游戲的情節(jié)。好的游戲音效和音樂(lè)可以使玩家融入游戲世界,產(chǎn)生共鳴。音效的作用還不僅限于此。如果沒(méi)有高超的游戲音效的映襯,再好的圖像技巧也無(wú)法使游戲的表現(xiàn)擺脫平庸,對(duì)玩家也沒(méi)有足夠的吸引力。開(kāi)發(fā)游戲時(shí),人們常常忽視游戲的音效。開(kāi)發(fā)者往往把主要精力花費(fèi)在游戲的圖像和動(dòng)畫(huà)等方面,而忽視了背景音樂(lè)和聲音效果。當(dāng)他們意識(shí)到這一點(diǎn)時(shí),通常為時(shí)已晚,這種做法顯然是不正確的。
游戲中的音效可分為如下幾類(lèi):背景音樂(lè)、劇情音樂(lè)、音效(動(dòng)作的音效、使用道具音效、輔助音效)等。背景音樂(lè)一般需要一直播放,而劇情音樂(lè)則只需要在劇情需要的時(shí)候播放,音效則是很短小的一段,比如揮刀的聲音、怪物叫聲等。
《小魚(yú)快跑》中的音效存儲(chǔ)在res文件夾的raw下,管理類(lèi)分別在soundManager和musicManager實(shí)現(xiàn)。
6.4.小結(jié)
本章我們通過(guò)實(shí)現(xiàn)一個(gè)Android平臺(tái)下的小游戲《小魚(yú)快跑》學(xué)習(xí)了Android平臺(tái)游戲開(kāi)發(fā)的相關(guān)知識(shí)。本章所講述的內(nèi)容基本上包括了游戲開(kāi)發(fā)中經(jīng)常使用的技術(shù),由于在前面的章節(jié)中我們介紹了有關(guān)圖形繪制和操作的一些知識(shí),我們把重點(diǎn)放在游戲框架、如何實(shí)現(xiàn)以及游戲開(kāi)發(fā)流程上,關(guān)于該游戲的具體實(shí)現(xiàn)可以參考所附源代碼。