從零開始Android遊戲編程(第二版) 第八章 地圖的設計和實現

來源:互聯網
上載者:User
第八章 地圖的設計和實現

這本來是第十章,前面計劃還有兩章的內容,一是跟第四章一樣,完成一個Asteroid遊戲作為小結,總結一下前面講過的Sprite的用法,並示範NPC和子彈的處理方法。但是,在寫第七章的最後一個例子的時候,把本來簡單的碰撞檢測的例子擴充了一下,加入了NPC和子彈,基本和Asteroid的功能差不多了,所以就把原定的Asteroid砍掉了。另外一章是講解程式的生命週期,內容相對簡單,但考慮到上一章講Sprite時已經引入了TiledLayer,如果接著講地圖應該會更連貫些,所以把生命週期向後移了一章。

那麼,就先讓我們看看地圖的設計和實現。

如果我們需要的地圖很小又很少,完全可以將整個地圖畫在一張圖片上。但是如果地圖很多,繪製和管理地圖的工作就會很麻煩,這時我們就需要用到另外一種技術——圖塊(Tile)。所謂Tile,就是將地圖中的公用元素提取出來,然後在顯示的時候組合這些元素形成完整的地圖,這就是本章要介紹的主要內容。

如是組成坦克大戰地圖的所有元素(16x16像素):

接下來讓我們看一幅遊戲中的情境:

可以看到,整個遊戲情境就是由上面那些Tile構成的。這樣,擺在我們面前的任務就很簡單了:將地圖依照Tile的大小分成若干格,將對應的Tile填到格子中。

在前面講Sprite的時候,我們知道可以將組合在一張圖片中的關鍵楨編號,以後就可以通過編號來使用這個楨(參看上一章楨動畫的相關內容),這種方法也同樣適用於Tile。而2D地圖很容易讓我們想到二維數組。也就是說我們可以將Tile的編號放到二維數組中,這樣我們就可以通過曆遍數組元素,像顯示Sprite那樣將整張地圖一塊一塊的顯示出來。

以上面的那張遊戲為例,一共13行13列,我們可以定義一個13x13的二維數組(因為地圖上有空白地區,所以我們將Tile的編號從1開始,用0表示空白)。

讓我們找到GameView_Old.java,增加成員變數map[][]:

int[][] map = {

{0,0,0,2,0,0,0,2,0,0,0,0,0},

{0,1,0,2,0,0,0,1,0,1,0,1,0},

{0,1,0,0,0,0,1,1,0,1,2,1,0},

{0,0,0,1,0,0,0,0,0,2,0,0,0},

{3,0,0,1,0,0,2,0,0,1,3,1,2},

{3,3,0,0,0,1,0,0,2,0,3,0,0},

{0,1,1,1,3,3,3,2,0,0,3,1,0},

{0,0,0,2,3,1,0,1,0,1,0,1,0},

{2,1,0,2,0,1,0,1,0,0,0,1,0},

{0,1,0,1,0,1,1,1,0,1,2,1,0},

{0,1,0,1,0,1,1,1,0,0,0,0,0},

{0,1,0,0,0,1,1,1,0,1,0,1,0},

{0,1,0,1,0,1,6,1,0,1,1,1,0},

};

在資源中增加tile.png

在建構函式中初始化bitmap對象

res = context.getResources();

bmp = BitmapFactory.decodeResource(res, R.drawable.tile);

在onDraw中繪圖

//用來顯示圖塊的Rect對象

Rect src = new Rect(0, 0, 0, 16);

Rect dst = new Rect();

for(int i=0; i

for(int j=0; j

//根據Tile的編號得到對應的位置

src.left = (map[i][j]-1) * 16;

src.right = src.left + 16;

//根據地圖上的編號計算對應的螢幕位置

dst.left = j * 16;

dst.right = dst.left + 16;

dst.top = i * 16;

dst.bottom = dst.top + 16;

canvas.drawBitmap(bmp, src, dst, paint);

}

}

最後不要忘了修改Main.java中的setContentView

GameView_Old gameView;

/** Called when the activity is first created. */

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

gameView = new GameView_Old(this);

setContentView(gameView);

}

好了,運行一下程式看看結果:

對比一下原圖,除了基地四周的磚塊之外兩者並無二致,可以說我們的Tile地圖實踐基本成功。

現在我們知道了使用Tile顯示地圖的原理,實際上,我們不需要每次都很麻煩的寫那麼多代碼,還記得前面說過的TiledLayer嗎?其中早已封裝了上述操作。不僅如此,TiledLayer還能顯示動態地圖呢。下面就讓我們看一看TiledLayer的基本用法。其實,它與Sprite的用法非常相似(我們需要將TiledLayer.java加入到項目中,請使用本章附帶程式的TiledLayer.java檔案,上一章的TiledLayer並不能正確工作):

這次讓我們使用GameView,把剛剛的數組map拷貝到GameView中,並聲明一個TiledLayer類型變數

//背景

TiledLayer backGround;

在建構函式中初始化TiledLayer

// 背景圖

backGround = new TiledLayer(13, 13, BitmapFactory.decodeResource(res,

R.drawable.tile), 16, 16);

TiledLayer的建構函式有5個參數,分別是地圖的行列數(是以Tile為單位的),包含Tile的bitmap對象,Tile的寬度和高度。

通過setCell方法將定義在數組中的Tile編號傳遞給TiledLayer。

for(int i=0; i

for(int j=0; j

backGround.setCell(i, j, map[i][j]);

}

}

最後,只需要在run函數中調用paint方法,就可以將TiledLayer顯示出來了。當然,你可以像控制Sprite那樣控制TiledLayer顯示的位置。

backGround.paint(c);

讓我們看一下啟動並執行效果

下面讓我們來學習如何?動態地圖。所謂動態地圖跟前面講到的楨動畫是一個道理,就是迴圈顯示幾個關鍵楨。讓我們看一下前面Tiles的圖片

我們會發現有兩張水域的圖片,這就是為動態地圖準備的,組合起來之後應該會有如下的效果:

那麼,我們如何在TiledLayer中實現動態地圖呢?TiledLayer為我們準備了這樣幾個函數:

createAnimatedTile:建立動態圖塊。很多人會迷惑於這個函數的名字,說是建立動態圖塊,可是建立在哪兒啊?建立出來怎麼用呢?只有天知道。其實,這個函數的主要功能也就是為動態圖塊分配了一個儲存結構。你不調用它還會報錯,調用了其實也沒什麼用。createAnimatedTile返回一個動態圖塊的編號,從-1開始依次遞減,第一次調用返回-1,這樣就分配了一個編號為-1的動態圖塊。第二次調用會返回-2,依次類推。這個傳回值一般沒有用,因為我們做地圖的時候肯定已經確定了動態圖塊的位置,這個編號早就寫到了數組中了。以後你就可以通過這個編號來控制相應的圖塊。函數有一個參數,指定一個Tile的編號,動態圖塊最初顯示的就是這個Tile。而正是通過改變這個Tile來實現動畫的。請看下面代碼:

backGround.createAnimatedTile(4);

我們初始化了一個動態圖塊,編號是-1,參數4表示當前顯示Tile順序圖表中的第四個Tile,就是第一張水域的圖片。

setAnimatedTile:動態圖塊的內容就是使用這個函數改變的。函數的第一個參數是動態圖塊的編號,如剛剛的-1。第二個參數是Tile的編號。

說到這裡,大家應該清楚動態地圖的用法了吧:

首先在地圖數組中確定需要顯示動態圖塊的位置,填入相應的編號,例如我們將上一張地圖的第三行增加三塊水域

int[][] map = {

{0,0,0,2,0,0,0,2,0,0,0,0,0},

{0,1,0,2,0,0,0,1,0,1,0,1,0},

{0,1,-1,-1,-1,0,1,1,0,1,2,1,0},

{0,0,0,1,0,0,0,0,0,2,0,0,0},

{3,0,0,1,0,0,2,0,0,1,3,1,2},

{3,3,0,0,0,1,0,0,2,0,3,0,0},

{0,1,1,1,3,3,3,2,0,0,3,1,0},

{0,0,0,2,3,1,0,1,0,1,0,1,0},

{2,1,0,2,0,1,0,1,0,0,0,1,0},

{0,1,0,1,0,1,1,1,0,1,2,1,0},

{0,1,0,1,0,1,1,1,0,0,0,0,0},

{0,1,0,0,0,1,1,1,0,1,0,1,0},

{0,1,0,1,0,1,6,1,0,1,1,1,0},

};

然後在GameView的建構函式中初始化TiledLayer,除了在setCell之前調用createAnimatedTile之外,沒有其他區別。

最後就是在run函數中調用setAnimatedTile,不斷地改變圖塊了

if(backGround.getAnimatedTile(-1) == 4) {

backGround.setAnimatedTile(-1, 5);

} else {

backGround.setAnimatedTile(-1, 4);

}

backGround.paint(c);

來讓我們看一下啟動並執行效果

其實筆者覺得TiledLayer完全也可以像Sprite那樣使用楨序列和nextFrame來實現動態效果,似乎更易用一些,有興趣的讀者可以自己修改TiledLayer實現這個功能。

到這裡,我們已經掌握了顯示地圖的方法,但是,這個地圖還不能真正運用到我們的遊戲中。讀者肯定也看到了,在TiledLayer的第一個例子中,我們的坦克可以穿牆而過,顯然,這個地圖還缺少最基本的功能——阻擋。

有一種簡單的方案可以實現阻擋,讓我們看一下Sprite類,其中有一個方法:

public final boolean collidesWith(TiledLayer t, boolean pixelLevel)

檢測Sprite與TiledLayer的碰撞。這種檢測是以Tile為單位的,當與Sprite重合的Tiles編號不為0時函數返回true,否則返回false。

下面讓我們做一個小例子測試一下:

開啟GameView_Old,增加一個Sprite類型的成員變數

// 主角

Sprite player;

在建構函式中初始化player

// 初始化主角

player = new Sprite(BitmapFactory.decodeResource(res,

R.drawable.player1), 16, 16);

player.setFrameSequence(new int[] { 0, 1 });

在onDraw中繪製player

player.paint(c);

backGround.paint(c);

在onKeyDown中控制Sprite的運動

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

// TODO Auto-generated method stub

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP:

x = player.getX();

y = player.getY();

player.move(0, -16);

if(!player.collidesWith(backGround, false)) {

y -= 16;

}

player.setTransform(Sprite.TRANS_NONE);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

x = player.getX();

y = player.getY();

player.move(0, 16);

if(!player.collidesWith(backGround, false)) {

y += 16;

}

player.setTransform(Sprite.TRANS_ROT180);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_LEFT:

x = player.getX();

y = player.getY();

player.move(-16, 0);

if(!player.collidesWith(backGround, false)) {

x -= 16;

}

player.setTransform(Sprite.TRANS_ROT270);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_RIGHT:

x = player.getX();

y = player.getY();

player.move(16, 0);

if(!player.collidesWith(backGround, false)) {

x += 16;

}

player.setTransform(Sprite.TRANS_ROT90);

player.setPosition(x, y);

break;

}

postInvalidate(); // 通知系統重新整理螢幕

return super.onKeyDown(keyCode, event);

}

可以看到,坦克只能在空白地區運動,這回不能上牆了。

但是這種方法還是比較粗糙的,很多時候不能實現我們的目的,比如坦克大戰中,水和磚頭是坦克不能通過的,但是掩體是可以通過的,還有子彈可以通過水域,這時候還是檢測Tile的編號來的準確些。就是說,我們事先確定好那些編號的Tile可以通過,哪些不能。然後模仿collidesWith方法根據Tank的位置取得它下一步要到達的Tile的編號,並判斷坦克是否被阻擋。

讓我們用這個方案重寫onKeyDown方法(為了簡化教程,我們假設每次player和tile都是完全重合的):

首先我們先定義一個函數用來判斷Tank是否可以通過

// 判斷坦克是否可以通過

private boolean tankPass(int x, int y) {

// 不超過地圖範圍

if (x 12 * 16 || y 12 * 16) {

return false;

}

int tid = map[y / 16][x / 16];

if (tid == 1 || tid == 2 || tid == -1)

return false;

return true;

}

然後在onKeyDown中運用新的方案

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

// TODO Auto-generated method stub

switch (keyCode) {

case KeyEvent.KEYCODE_DPAD_UP:

x = player.getX();

y = player.getY();

player.move(0, -16);

if (tankPass(player.getX(), player.getY())) {

y -= 16;

}

player.setTransform(Sprite.TRANS_NONE);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_DOWN:

x = player.getX();

y = player.getY();

player.move(0, 16);

if (tankPass(player.getX(), player.getY())) {

y += 16;

}

player.setTransform(Sprite.TRANS_ROT180);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_LEFT:

x = player.getX();

y = player.getY();

player.move(-16, 0);

if (tankPass(player.getX(), player.getY())) {

x -= 16;

}

player.setTransform(Sprite.TRANS_ROT270);

player.setPosition(x, y);

break;

case KeyEvent.KEYCODE_DPAD_RIGHT:

x = player.getX();

y = player.getY();

player.move(16, 0);

if (tankPass(player.getX(), player.getY())) {

x += 16;

}

player.setTransform(Sprite.TRANS_ROT90);

player.setPosition(x, y);

break;

}

postInvalidate(); // 通知系統重新整理螢幕

return super.onKeyDown(keyCode, event);

}

現在讓我們運行一下看看吧,這回終於能夠達到了我們想要的效果,我們的坦克正藏在掩體下面,並且它不能通過牆和水域。

到此為止,關於地圖的內容就講解完畢了。這些內容並不複雜,首先介紹了使用Tile組成地圖的原理,然後介紹了TiledLayer已經動態地圖,最後示範了阻擋的實現方法。本章的大部分例子使用了GameView_Old,並沒有使用遊戲迴圈,也沒有涉及到地圖的平滑滾動,在以後需要的時候會補充這部分知識。

本章樣本程式http://u.115.com/file/f12827d8db

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.