初探Nintendo DS程式開發與設計(四):做個拼貼藝術大師

由之前一系列的三篇文章中,已經一步步地瞭解了如何在 NDS 平台上使用 Framebuffer 背景模式、Bitmap 背景模式,以及 Sprite 物件功能,接著本篇將再次深入與遊戲製作息息相關的背景顯示功能,介紹在真實的遊戲程式應用中,更加實用的圖塊背景 (Tiled Background) 模式

前情提要

由於硬體架構上的先天限制,在 NDS 平台中,沒有如同個人電腦平台上的全彩 (32 bits) 或高彩 (24 bits) 顯示模式可以使用;在這個小小的掌上世界中,所有的繪圖物件只能夠使用 16 bits、8 bits 或 4 bits 的色彩顯示模式。而如前篇文章的內容所提,Sprite 物件能夠使用 16 色 (4 bits) 與 256 色 (8 bits) 兩種模式,對於背景圖件來說亦然如此。在 Sprite 的功能中,可以選擇使用 OBJCOLOR_16 或 OBJCOLOR_256 這兩項色彩模式,而在背景圖件中,也有相對應於 16 色與 256 色模式的 BG_COLOR_16 與 BG_COLOR_256 定義。

瞭解了色彩模式的種類之後,就可以開始進入圖塊 (Tile) 的世界了。很久很久以前,在那個電腦記憶體仍然十分珍貴而稀少的年代裡,多數的家用遊樂器主機,都只配備著非常有限的記憶體容量。而對於掌上型主機來說,記憶體的限制則更為顯著。因此,我們無法使用多張高解析度的點陣圖 (Bitmap) 做為遊戲中的背景圖片,所以在 NDS 上存在著一種特別的繪圖顯示模式,能夠以一組相同大小的圖塊,拼貼組合成為完整的遊戲背景圖片。因此,遊戲開發者就可以利用類似磁磚拼貼的方式,由有限數量的小型圖塊,組合出變化多端的大型圖片。

在 NDS 平台上,Tile 是指一個 8×8 大小,共計由 64 個像素點所組成的圖塊。

tileset以一張 32×32 大小的點陣圖為例,當程式設計者將原先存放於卡匣中的點陣圖片載入至 NDS 的記憶體中時,在硬體系統的內部會將圖片以 8×8 的大小,依照橫列的順序依序讀取進來。因此,如圖所示,一張 32×32 大小的點陣圖片,就會被分為由 1 至 16 共計 16 個圖塊。

圖塊背景模式的顯示原理,其實非常類似於圖片的調色盤 (Palette) 顯示模式原理。首先將相等大小的圖塊載入繪圖記憶體中,然後在背景的顯示區域中,再指定一個索引值 (Index) 至指定的圖塊編號;如此一來,NDS 的繪圖引擎,就能夠依據索引值找出相對應的圖塊資料以做為繪圖顯示之用。而這些索引值資料,則經常被稱為 Map 或者 Tilemap。

請仔細地端詳這張精美的 2D Background Memory 圖片,其中的 Graphics Base(也可稱為 Char Base)就是存放圖塊資料的地方,而 Map Base 則是存放背景圖件索引值的地方。Map Base 的每一塊 Base 大小為 0x800h (2 KByte);而 Graphics Base 的每一塊 Base 則佔據著 0x4000h (16 KByte) 的記憶體空間。但是請注意,在圖片上顯示出的 Graphics Base 與 Map Base,只是邏輯觀念上的分野,實際上兩者皆位於相同的記憶體位址 0x06000000 之中,只是依據程式設計者所做的設定,而使 NDS 系統對記憶體的內容物做出適當的解譯。

為了使 CPU 存取記憶體資料的速度加快,在大多數的電腦硬體架構中,都存在著 Memory Alignment 的規則與限制,當然 NDS 平台也不例外。因此在圖塊背景模式中,程式設計者不能夠隨性地將資料填入任意的記憶體位址中,而是必須將圖塊資料與索引資料對齊 (align) 硬體架構中的邏輯邊界。但是這些在 Base 與 Base 之間的分界,完全只是邏輯性的分野,並沒有任何內建的機制能夠確保這些記憶體位址上的資料不會相互重疊,因此程式設計者必須負責將資料寫入適當的記憶體位址,並且確保它們不會彼此覆寫

至此,我們知道為了顯示圖塊背景,需要設定兩種資料:

  • 圖塊資料 (Graphics):由點陣圖片分解而成 8×8 大小的像素點資料。
  • 索引資料 (Map):用來索引至圖塊資料的數值。

而為了能夠迅速存取 Map Base 與 Graphics Base 的記憶體位址,所以在 libnds 中已經為 Main Engine 預先定義了方便使用的 #define 值:

#define BG_MAP_RAM(base)        (((base)*0x800) + 0x06000000)
#define BG_TILE_RAM(base)        (((base)*0x4000) + 0x06000000)

舉例來說,如果要將索引資料寫入第 0 個 Base,並且將圖塊資料寫入第 1 個 Base 中,可以使用以下的方法:

// Get Map Base 0
u16* piMapMemory = (u16*)BG_MAP_RAM(0);
// Copy data to Map Base 0
dmaCopy(mapData, piMapMemory, mapDataLen);

// Get Graphics Base 1
u16* piTileMemory = (u16*)BG_TILE_RAM(1);
// Copy data to Graphics Base 1
dmaCopy(backgroundTiles, piTileMemory, backgroundTilesLen);

接著,再於背景圖件的設定中,指定所使用的 Map Base 與 Tile Base 編號,就能夠順利地顯示背景圖片了:

// 背景圖層0:使用 32x32 個 Map,256 色模式,以及 Map Base 0 與 Tile Base 1 的資料
BG0_CR = BG_32x32 | BG_COLOR_256 | BG_MAP_BASE(0) | BG_TILE_BASE(1);

在 NDS 的程式設計領域中,有一個非常重要的觀念,就是不論是 Bitmap 資料、Tile 資料、Map 資料或者 3D 資料,同樣都存在於相同的記憶體區段之中,NDS 本身並不知道如何詮釋這些記憶體中存在的資料,因此程式設計者需要藉由 videoSetMode() 函式以及其他相關的設定,才能夠使 NDS 對記憶體中的資料做出正確的解譯。

還記得在前篇文章中,有提到 Sprite 系統最多可擁有 1024 個不同的 Tile 數嗎?因為圖塊的 Map 有 8 bits 與 16 bits 兩種形式,在 8 bits 模式下,最多只能索引至 2 的 8 次方,也就是 256 個不同的 Tile;而在 16 bits 模式下,只有其中的 10 個 bits 會拿來做為索引值的用途,最多只能索引至 1024 個不同的 Tile,所以這也就成為了 Sprite 所能使用的圖塊上限值。另外,16 bits 的 Map 還能夠提供調色盤以及圖塊鏡射 (Flip) 的功能。

在本文的範例程式中,將會載入 4 張 32×32 大小的圖片,用來組合出數種不同的背景圖片樣式。由前述的圖塊定義可知,32×32 大小的圖片會被分解成為 16 個圖塊,其中第一張圖片的圖塊索引編號為 0、1、2 … 至 15 ,第二張圖片的編號則從 16 至 31,其他則以此類推:

  • 圖片編號 0:由圖塊 0、1、2 …… 至 15。
  • 圖片編號 1:由圖塊 16、17、18 …… 至 31。
  • 圖片編號 2:由圖塊 32、33、34 …… 至 47。
  • 圖片編號 3:由圖塊 48、49、50 …… 至 63。

接著,可以利用字元陣列的方式,定義出背景圖片的樣式:

static char g_acMap1[] = 
{
	2,2,2,0,1,1,0,0,
	2,2,2,0,1,1,0,0,
	2,2,2,0,0,0,0,0,
	0,0,0,0,0,3,3,3,
	0,1,1,0,0,3,3,3,
	0,1,1,0,0,3,3,3,
};

要注意的是,這個陣列中的編號並不是圖塊編號,而是用來表示螢幕上所需顯示的圖片編號;在遊戲的背景中,填入 0 的位置顯示第一張圖片,1 顯示第二張圖片,2 顯示第三張圖片,3 則顯示第四張圖片。再來就是利用前述的 libnds 定義,將點陣圖片的資料複製至 BG_TILE_RAM(1) 的位址上,並且將 g_acMap1 的資料與 BG_MAP_RAM(0) 傳入 BuildBG() 函式中進行處理:

// Load graphics data
dmaCopy(backgroundTiles, (u16*)BG_TILE_RAM(1), backgroundTilesLen);
dmaCopy(backgroundPal, BG_PALETTE, backgroundPalLen);

// Build up map data
BuildBG((u16*)BG_MAP_RAM(0), g_acMap1);

NDS 的螢幕尺寸為 256×192 個像素點,以 32×32 大小的圖片來說,在單一螢幕上總共能夠容納 8×6 張圖片,也就是每一列有 8 張圖片,每一行則有 6 張圖片。所以,需要在 BuildBG() 函式中,對每一列與每一行的圖片進行設定:

void BuildBG(u16* piMemory, char* pcMap)
{
	for (int y = 0; y < TILESET_HEIGHT; y++) {  // TILESET_HEIGHT = 6
		for (int x = 0; x < TILESET_WIDTH; x++) {  // TILESET_WIDTH = 8		
			char cTileIndex = *pcMap++;
			BuildTileset(piMemory, x, y, cTileIndex);
		}
	}
}

而在 BuildTileset() 函式中,則需要在先前傳入的 BG_MAP_RAM(0) 位址上,設定圖塊的索引資料:

void BuildTileset(u16* piMemory, int iX, int iY, char cTileIndex)
{
	char cValue = cTileIndex * TILES_PER_TILESET;
	int iTileX = iX * TILES_NUM_WIDTH;
	int iTileY = iY * TILES_NUM_HEIGHT;

	piMemory[iTileX + iTileY * BG_WIDTH] = cValue;
	piMemory[iTileX + iTileY * BG_WIDTH + 1] = cValue + 1;
	piMemory[iTileX + iTileY * BG_WIDTH + 2] = cValue + 2;
	piMemory[iTileX + iTileY * BG_WIDTH + 3] = cValue + 3;
    
	piMemory[iTileX + (iTileY + 1) * BG_WIDTH] = cValue + 4;
	piMemory[iTileX + (iTileY + 1) * BG_WIDTH + 1] = cValue + 5;
	piMemory[iTileX + (iTileY + 1) * BG_WIDTH + 2] = cValue + 6;
	piMemory[iTileX + (iTileY + 1) * BG_WIDTH + 3] = cValue + 7;
    
	piMemory[iTileX + (iTileY + 2) * BG_WIDTH] = cValue + 8;
	piMemory[iTileX + (iTileY + 2) * BG_WIDTH + 1] = cValue + 9;
	piMemory[iTileX + (iTileY + 2) * BG_WIDTH + 2] = cValue + 10;
	piMemory[iTileX + (iTileY + 2) * BG_WIDTH + 3] = cValue + 11;
    
	piMemory[iTileX + (iTileY + 3) * BG_WIDTH] = cValue + 12;
	piMemory[iTileX + (iTileY + 3) * BG_WIDTH + 1] = cValue + 13;
	piMemory[iTileX + (iTileY + 3) * BG_WIDTH + 2] = cValue + 14;
	piMemory[iTileX + (iTileY + 3) * BG_WIDTH + 3] = cValue + 15;
}

nds-dev-tiled-background在上述的程式碼中,先將原來傳入 BuildBG() 函式的圖片編號,轉換成相對應的圖塊編號 cValue,接著再直接存取 piMemory,也就是 BG_MAP_RAM(0) 位址,以每一列 4 個圖塊為一組,一次完成 16 個圖塊的索引值設定。

大功告成!只需要按照上述的步驟一步步執行,就能夠完成 NDS 的程式設計領域中,非常實用也非常具有彈性的圖塊背景模式功能。

接續之前三篇文章所介紹的 Framebuffer、Bitmap 與 Sprite,一路走來已經認識了 NDS 平台的輸入功能、聲音功能、Sprite 圖件,以及圖塊背景顯示模式。光是利用這些知識,已經非常足夠寫出幾個簡單的小遊戲了。有興趣的讀者,不妨試著利用這些概念開始動手寫個簡單的小型遊戲吧!如果能夠利用所學的知識,製造出完整的遊戲成品,不止可以獲得滿滿的成就感,同時也是一種非常棒的正向學習回饋噢! :D

程式碼範例下載:NDSDev_TiledBackground.zip (下載次數: 1338 )
操作方式:程式中共有 3 種不同樣式的 Tilemap,按下「A」鍵或「B」鍵即可切換。

參考資料:NDS/Tutorials Day 4: 2D Tile Graphics

2 Replies to “初探Nintendo DS程式開發與設計(四):做個拼貼藝術大師”

  1. NDS 的螢幕尺寸為 256×192 個像素點,以 32×32 大小的圖片來說,在單一螢幕上總共能夠容納 8×6 張圖片,也就是每一列有 8 張圖片,每一行則有 6 張圖片。所以,需要在 BuildBG() 函式中,對每一列與每一行的圖片進行設定:

    这里应该是说每一行有 8 张图片,每一列有 6 张图片吧~~~

Leave a Reply

Leave a Reply