如何使用OPhone API繪製2D遊戲場景
遊戲開發, 2009-12-14 11:32:59
標籤 : 2D遊戲 地圖 OPhone API RPG ACT
地圖是遊戲中必不可少的一種預算元素,尤其是在RPG、ACT等類型的遊戲中作用更爲重要,一個漂亮的地圖效果和一個流暢的捲動速度會大大增加玩家的遊戲體驗。而遊戲中地圖滾動的重繪有多種算法,由於手機性能的限制和開發週期等其他非技術條件,需要根據情況靈活選擇所需的技術。本文將主要介紹如何使用OPhone API來繪製2D遊戲中的場景,也即地圖的繪製方法。
地圖繪製及滾動的常用算法
無縫圖片滾動畫法
最簡單的一種畫地圖方法,無需使用數組,只需要使用一張無縫的背景圖片,在屏幕上繪製兩次,以此來實現最簡單的地圖滾動效果和圖片的重複使用以節約資源。
如下圖,紅色虛線部分爲屏幕,使用一個偏移量在屏幕中錯開位置貼上兩次圖片,通過不斷改變偏移量的大小來實現動畫效果。
代碼舉例:
- //imgBack圖片對象
- //posX圖片在X軸方向上的偏移量
- canvas.drawBitmap(imgBack, -posX, 0, paint);
- canvas.drawBitmap(imgBack, imgBack.getHeight()+posX, 0, paint);
- if(posX==-imgBack.getHeight())
- posX=0;
優點與侷限:此算法非常簡單,由於是單張圖片反覆滾動生成的背景圖片,所以對於美術人員的限制較少,利於發揮,而且外觀效果好。但因爲不是地圖Tile組成的,資源複用率不高,只能用於生成不太複雜的地圖。而且由於沒有Tile的存在,無法針對不同的Tile計算碰撞。最終使得這種畫法只能用於繪製簡單屏幕背景圖片,而無法用在有複雜物理碰撞的地圖層。
裁剪區畫法
我們平時所玩的遊戲一般場景都是大於屏幕的尺寸的,也就是說在遊戲中的主角移動的時候,後面的地圖將會隨着主角的位置變化而發生移動,我們稱之爲地圖的卷軸效果。而對諸如RPG,ACT這類地圖場景比較大的類型的遊戲來說,地圖都不是一整張的背景圖直接使用,而是採用一種“拼接”的方式,這樣做既能節省內存的佔用,同時也能使圖片資源的利用率達到最大化。下圖就是2D遊戲常用的圖片樣式:
從圖中我們能夠看出,我們可以把整張圖片進行分割,並將分割後的圖片進行編號,如下所示:
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
11
|
12
|
13
|
14
|
15
|
爲每塊圖素編號之後,就可以設計自己的地圖了。這裏需要使用一種叫做“地圖編輯器”的工具軟件。我們這裏使用“mapwin”進行地圖的設計,使用步驟如下圖所示:
上面的四個輸入框分別代表地圖小塊的寬度和高度,以及我們要創建的整個場景的水平和垂直的地圖塊數,輸入後點擊“OK”如下圖所示:
下面需要引入一張圖片,引入方法爲“File——Import”,選取一張圖片並點擊確定,隨後就能看到如下的圖片:
剩下的工作想必你就可以想到了,用鼠標在右邊區域選取一個圖塊,然後將其放到左邊黑色區域中即可,拼接完的效果如下圖:
接下來要把地圖數據導出,導出放下如下圖:
最後我們需要的數據是這樣的:
const short ss_map0[10][10] = {
{ 1, 1, 1, 1, 1, 1, 1, 5, 1, 1 },
{ 10, 10, 10, 1, 1, 1, 1, 1, 1, 1 },
{ 8, 8, 8, 1, 1, 1, 1, 1, 1, 1 },
{ 9, 9, 9, 1, 1, 1, 1, 14, 15, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 16, 17, 1 },
{ 1, 1, 1, 6, 11, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 11, 1, 1, 1, 21, 1 },
{ 1, 4, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
};
實際上就是一個二維數組,數組中的數字即爲地圖塊的索引號。
使用二維數組保存地圖信息,另外有一張圖片素材,根據地圖數組的不同下標,配合public boolean clipRect(float left, float top, float right, float bottom,Region.Op op) 裁剪區方法,將對應的Tile顯示在正確的位置上。
如下圖所示,紅色虛線部分爲屏幕,紅色實線爲裁剪區,通過讀取地圖數組,將相應的位置設置爲裁剪區,並用將圖片素材相對於裁剪區偏移一定x,y位置的方法,使得要繪製的Tile正好對應出現在裁剪區中。
代碼舉例:
- // 繪製切割圖片
- public void drawClipImg(int XDest, int YDest, int Width, int Height,
- int XSrc, int YSrc, Bitmap img, Paint g,Canvas canvas)
- {
- canvas.clipRect(XDest, YDest, XDest + Width, YDest + Height,
- Region.Op.REPLACE);
- canvas.drawBitmap(img, XDest - XSrc, YDest - YSrc, g);
- canvas.clipRect(0, 0, Const.SCREEN_WIDTH, Const.SCREEN_HEIGHT,
- Region.Op.REPLACE);
- }
相對於前一種畫法,圖片資源的利用率提高了很多,可以繪製很複雜的地圖。由於Tile的存在,可以針對不同的Tile計算碰撞,可以用於地圖物理層的繪製。
最常見的地圖繪製優化——只繪製當前屏幕
上面的繪製方法都是將整個地圖的數據全部畫出來的,這樣做實際上也存在很大的浪費,因爲玩家實際上只能看見屏幕中的一塊區域,其他大部分的地圖即使被繪製也不能反映到屏幕上,反而因爲這個不必要的步驟大大增加了CPU的負擔,從而影響了遊戲的流暢程度。因此,在實際開發中,常用的優化方法就是隻繪製當前屏幕的地圖塊。代碼如下:
- //計算單元格起始位置下標
- int startIndexX =leftTopY/ MAP_TILE_SIZE;
- int startIndexY =leftTopX/ MAP_TILE_SIZE;
- //再使用上面得到的數據修改雙循環繪製的條件即可,
- for (int i = startIndexX; i < startIndexX +SCREEN_WIDTH / MAP_TILE_SIZE + 1; i++)
- for (int j = startIndexY; j < startIndexY +SCREEN_HEIGHT / MAP_TILE_SIZE + 1; j++)
卡馬克卷軸算法的引入
上面的算法雖然在一定程度上解決了地圖繪製的效率問題,但對於某些資源嚴重不足的手機,或者由於地圖塊比較小、循環次數過多的情況,仍然會造成畫圖時屏幕閃爍。因此,在這種情況下,仍然需要對上述算法做進一步的優化。
不論採用哪種優化算法,一個基本的思路就是儘量減少繪製的次數,從而減少對系統資源的消耗。卡馬克卷軸算法就是這樣算法的一個經典例子。
單方向卷軸
對於橫版遊戲來說,如果角色向右側移動,則地圖向左側滾動。由於角色每次移動若干個步長,因此地圖中新畫出的區域寬度也爲若干個像素,那麼如果讓系統重繪所有屏幕區域,很明顯,大部分區域都是和上一屏幕區域相同的,如此造成成了資源的浪費。而卡馬克算法的思路就是——如果上一次繪製過的地圖也能夠部分重用到本次地圖繪製上來就好了。那麼很容易想到在內存中建立一個和屏幕一樣大或略大的緩衝區即可很好的完成這個設想。
由上圖可以看到,區域B爲相同的地圖區域,這個區域在下一次屏幕重繪時,可以被重新利用。區域A是在下一次屏幕重繪中不被採用的區域,這區域應當被捨棄,但是如果稍微留意一下的話,不難發現區域A和區域C的面積大小其實居然是一樣的。
那麼如果建立一個和屏幕大小相同的緩衝,在其被捨棄掉的繪製區域A中畫上新的區域C,再把區域B和區域C拼合到屏幕上,是不是就能達到減少系統資源消耗的目的了呢?卡馬克卷軸的基本原理正是如此。
圖顯示了卡馬克卷軸的最基本原理,首先在內存中建立一塊和屏幕一樣大小(或略大)的緩衝區。然後在本應由於地圖移動而被捨棄掉的區域1上面繪製,由於地圖滾動而出現的新地圖區域。最後把兩個區域按照地圖的實際位置拼合到屏幕上。
雙軸滾動的卡馬克卷軸
對於俯視遊戲,或者有Y軸捲動的遊戲來說,單單一個方向的地圖捲動並不夠用。那麼如果是出現兩個方向的捲動會如何呢。不必擔心,上面的思路算法一樣能適應這種情況。
由上圖可以看到,區域D爲相同的地圖區域,這個區域在下一次屏幕重繪時,可以被重新利用。區域ABC是在下一次屏幕重繪中不被採用的區域,可以在這個3個區域上繪製上下一次需要重繪的區域A’B’C’。再將繪製好的四個區域拼合到屏幕的對應位置。
上圖顯示了雙軸滾動的卡馬克卷軸的基本繪製原理,需要特別注意的是:在緩衝區的繪製順序和在屏幕上拼合的順序是完全相反的。
卡馬克算法的實現
卡馬克卷軸緩衝畫法的一般步驟如下:
1. 初始化所有地圖數據,並且全屏繪製初始的地圖
2. 若人物移動,則調用攝像機算法,修正地圖偏移量
3. 地圖偏移量不滿足地圖的邊界條件,就重繪緩衝區
4. 重繪緩衝區
5. 後臺緩衝區的四個子區按照順序畫到屏幕上
地圖類——Map的設計
字段定義
- //地圖數據
- public byte mapData[][];
- //移動緩衝區的當前座標窗口
- public int sx,sy;
- //地圖圖片
- private Bitmap imgMap;
- public GameView m_View;
- //常量
- public final static int MAP_TILE_SIZE = 24;
- /** 緩衝區寬高,命名方式爲:Carmack width or height */
- private int bufWidth, bufHeight;
- /** 緩衝區寬的圖塊數,與高的圖塊數*/
- private int carTileWidth, carTileHeight;
- /** 屏幕寬高命名方式爲:screen width or height */
- private int scrWidth, scrHeight;
- /** 緩衝切割線,命名方式爲:Carmack x or y */
- private int carx, cary;
- /** 地圖在緩衝區的X 、Y偏移量,命名方式爲:map offset x or y */
- private int mapOffx, mapOffy;
- /** 緩衝區,命名方式爲:Carmack buffer */
- public Bitmap carBuffer;
- /** 緩衝區畫筆,命名方式爲:Carmack Graphics */
- private Canvas carGp;
- /** 緩衝區增大的大小(上下大小是一樣的) */
- private int buffSize;
- /** 圖片寬度的所切割的圖塊數量。 */
- private int imageTileWidth;
- /** 地圖圖片 */
- private Bitmap mapImage;
- Paint paint=new Paint();
- /** 地圖數組 */
- private byte mapArray[][];
- /** 圖塊大小,寬高一致 */
- private int tileSize;
- /** 圖塊的寬度數量,與高度數量 */
- private int tileW, tileH;
- /** 地圖的寬高 */
- private int mapLastx, mapLasty;
方法定義
- CarMapBuffer(int, int, int, int)構造器
- CarMapBuffer(int, int, int)構造器的代理
- setMap(Image, byte[][])設置地圖參數
- initBuffer()初始化繪製地圖
- scroll(int, int)捲動地圖算法
- updateBuffer(int, int)繪製緩衝區
- getIndexCarX()獲得切割線所在的圖塊索引X
- getIndexCarY()獲得切割線所在的圖塊索引Y
- getBufferCarX()獲得切割線在Buffer中的X位置
- getBufferCarY()獲得切割線在Buffer中的Y位置
- getIndexBuffLastX()獲得緩衝區後面的X索引
- getIndexBuffLastY()獲得緩衝區後面的Y索引
- getTitleHeight()獲得當前要繪製的圖塊高度的數量
- getTitelWidth()獲得當前要繪製的圖塊寬度的數量
- copyBufferX(int, int, int, int, int) 由於x方向捲動造成的重繪
- copyBufferY(int, int, int, int, int) 由於y方向捲動造成的重繪
- getMapX(int, int) 獲得地圖圖片的X座標偏移
- getMapY(int, int) 獲得地圖圖片的Y座標偏移
- paint(Graphics, int, int)將緩衝區的內容分成4塊依次拼合到屏幕上
- drawBuffer(Graphics, int, int)繪製緩衝區方法
- drawRegion(Graphics, Image, int, int, int, int, int, int, int, int)封裝的drawRegion()方法
- getGraphics()獲得緩衝區畫筆
- getImage()獲得緩衝區Image對象
步驟一的實現
初始化所有地圖數據,並且全屏繪製初始的地圖,代碼如下:
- /**
- * 初始化Buffer,全部地圖繪製在此方法中完成
- */
- private void initBuffer()
- {
- int x, y, cx, cy;
- for (int i = 0; i < carTileHeight; i++)
- {
- for (int j = 0; j < carTileWidth; j++)
- {
- x = getMapX(i, j);
- y = getMapY(i, j);
- cx = j * tileSize;
- cy = i * tileSize;
- m_View.drawClipImg(cx, cy, tileSize, tileSize, x, y, mapImage, paint, carGp);
- }
- }
- }
步驟二、三的實現
若人物移動,則調用攝像機算法,修正地圖偏移量,若偏移量在[0,maplast]移動範圍內移動,則有可能發生重繪
- /**
- * 卷軸滾動 *
- * @param x * X軸滾動
- * @param y * Y軸滾動
- */
- private void scroll(int x, int y)
- {
- try
- {
- x += mapOffx;
- y += mapOffy;
- // *************************************************
- // 邊界檢測
- if (x < 0 || y < 0)
- {
- return;
- }
- if (x > mapLastx)
- {
- mapOffx = mapLastx;
- return;
- }
- if (y > mapLasty)
- {
- mapOffy = mapLasty;
- return;
- }
- updateBuffer(x, y);
- // *************************************************
- }
- catch (ArrayIndexOutOfBoundsException e)
- {
- }
- }
步驟四的實現
重繪緩衝區,地圖的x方向捲動會造成列方向上的重繪(調用copyBufferX()方法),地圖的y方向上的捲動會造成行方向上的重繪(調用copyBufferY()方法)。updateBuffer()方法用於針對不同的四個方向上的捲動進行copyBuffer()參數的初始化。
- /**
- * 更新緩衝區 *
- * @param x * 緩衝區新的地圖X座標
- * @param y * 緩衝區新的地圖Y座標
- */
- private void updateBuffer(int x, int y)
- {
- mapOffx = x;
- mapOffy = y;
- // 右移
- if (x > carx + buffSize)
- {
- // while (carx < mapOffx - buffSize) {
- int indexMapLastX = getIndexBuffLastX();
- if (indexMapLastX < tileW)
- {
- copyBufferX(indexMapLastX, getIndexCarY(), getTileHeight(),
- getBufferCarX(), getBufferCarY());
- carx += tileSize;
- }
- // }
- }
- // 左移
- if (x < carx)
- {
- // do {
- carx -= tileSize;
- copyBufferX(getIndexCarX(), getIndexCarY(), getTileHeight(),
- getBufferCarX(), getBufferCarY());
- // } while (carx > mapOffx);
- }
- // 下移
- if (y > cary + buffSize)
- {
- // while (cary < mapOffy - buffSize) {
- int indexMapLastY = getIndexBuffLastY();
- if (indexMapLastY < tileH)
- {
- copyBufferY(getIndexCarX(), indexMapLastY, getTitelWidth(),
- getBufferCarX(), getBufferCarY());
- cary += tileSize;
- }
- // }
- }
- // 上移
- if (y < cary)
- {
- // do {
- cary -= tileSize;
- copyBufferY(getIndexCarX(), getIndexCarY(), getTitelWidth(),
- getBufferCarX(), getBufferCarY());
- // } while (cary > mapOffy);
- }
- }
重繪緩衝區的具體方法,該方法涉及到大量的座標運算,而且由於卡馬克點的存在經常會分成兩個區域分兩次進行重繪。見下圖:
下面以x方向捲動爲例舉例
- private void copyBufferX(int indexMapx, int indexMapy, int tileHeight,
- int destx, int desty)
- {
- int mapImagex, mapImagey, vy;
- // 拷貝地圖上面到緩衝的下面
- int timer=0;
- for (int j = 0; j < tileHeight; j++)
- {
- mapImagex = getMapX(indexMapy + j, indexMapx);
- mapImagey = getMapY(indexMapy + j, indexMapx);
- vy = j * tileSize + desty;
- m_View.drawClipImg(destx, vy, tileSize, tileSize, mapImagex, mapImagey, mapImage, paint, carGp);
- timer++;
- }
- // 拷貝地圖下面到緩衝的上面
- for (int k = tileHeight; k < carTileHeight; k++)
- {
- mapImagex = getMapX(indexMapy + k, indexMapx);
- mapImagey = getMapY(indexMapy + k, indexMapx);
- vy = (k - tileHeight) * tileSize;
- m_View.drawClipImg(destx, vy, tileSize, tileSize, mapImagex, mapImagey, mapImage, paint, carGp);
- timer++;
- }
- System.out.println("x:"+timer);
- }
步驟五的實現
將後臺緩衝區的四個子區按照順序畫到屏幕上:
- public void paint(Canvas g, int x, int y)
- {
- // 地圖在緩衝中的座標
- int tempx = mapOffx % bufWidth;
- int tempy = mapOffy % bufHeight;
- // 切割線右下角的寬與高
- int rightWidth = bufWidth - tempx;
- int rightHeight = bufHeight - tempy;
- // 畫左上
- drawRegion(g, carBuffer, tempx, tempy, rightWidth, rightHeight, 0, x, y);
- // 畫右上
- drawRegion(g, carBuffer, 0, tempy, scrWidth - rightWidth, rightHeight, 0, x + rightWidth, y);
- // 畫左下
- drawRegion(g, carBuffer, tempx, 0, rightWidth, scrHeight - rightHeight, 0, x, y + rightHeight);
- // 畫右下
- drawRegion(g, carBuffer, 0, 0, scrWidth - rightWidth, scrHeight
- - rightHeight, 0, x + rightWidth, y + rightHeight);
- }
- /**
- * 畫圖 *
- * @param g * 目標屏幕的畫筆
- * @param img * 原圖片
- * @param x_src * 原圖片X座標
- * @param y_src * 原圖片Y座標
- * @param width * 原圖片寬度
- * @param height * 原圖片高度
- * @param transform * 旋轉角度
- * @param x_dest * 目標屏幕的X座標
- * @param y_dest * 目標屏幕的Y座標
- * @param anchor * 畫筆的錨點
- */
- private void drawRegion(Canvas g, Bitmap img, int x_src, int y_src,
- int width, int height, int transform, int x_dest,
- int y_dest)
- {
- // 作寬度檢測
- if (width <= 0 || height <= 0)
- {
- return;
- }
- // 作超屏幕寬度檢測
- if (width > scrWidth)
- {
- width = scrWidth;
- // 作超屏幕高度檢測
- }
- if (height > scrHeight)
- {
- height = scrHeight;
- }
- m_View.drawClipImg(x_dest, y_dest, width, height, x_src, y_src, img, paint, g);
- }
當然,地圖的捲動和精靈的移動是分不開的,在本文中我們只闡述了遊戲的地圖繪製方法,關於精靈的繪製以及地圖隨精靈的位移而捲動,我們會在另一篇文章中做以介紹。
總結
卡馬克算法是在進行2D遊戲地圖捲動的算法中內存痕跡最小、效率適中的算法之一。其核心的思想就是把地圖捲動過程中移出屏幕(不需要在顯示的部分)所佔用的buffer區域,繪製上新的需要圖塊,在往屏幕上繪製的時候,通過四次繪製buffer把完整的地圖重現。
我們在實際的代碼編寫中按以下的方式進行。根據上面的基本思想,把地圖分爲四個塊(十字形的將buffer劃分爲四塊),用carx和cary來記錄十字分區的中心座標(相對於buffer的座標,我把這個點叫卡馬克分區點)。
當地圖向右移動的時候這時把卡馬克分區點的座標X方向加上一個tile的width,然後在從現在的卡馬克分區點的座標Y開始繪製提取出來的tileID對應的圖象,注意是從當前的卡馬克分區點的座標Y開始繪製,當超出carHeight時在從0開始繪製直到結束,這樣就完成了在水平方向上的更新。
還有就是在水平移動卡馬克分區點的時候是在buffer中循環的,也就是從0到carWidth的一個循環過程,Y方向上完全一致。最後是繪製過程,也就是將四個分區繪製出來,口訣就是左變右上變下,掌握好卡馬克算法對手遊開發很有幫助的。
注:本文參考了網上關於卡馬克算法的一些介紹並引用了其中的部分文字和圖片。
作者介紹
李建,樂成教育管理有限公司 高級講師。層就職於國內數家SP,CP公司,具有豐富的軟件、遊戲開發經驗。並從事多年教學工作,具有豐富的教學經驗。目前主要從事OPhone、J2ME開發和教學方面的工作。
(聲明:本網的新聞及文章版權均屬OPhone SDN網站所有,如需轉載請與我們編輯團隊聯繫。任何媒體、網站或個人未經本網書面協議授權,不得進行任何形式的轉載。已經取得本網協議授權的媒體、網站,在轉載使用時請註明稿件來源。)