初探Nintendo DS程式開發與設計(二)

nds-dev-bitmap自從年代久遠的「初探Nintendo DS程式開發與設計(一)」問世以後,經過了漫長的兩個月等待,新的續篇終於不負眾望地(?)火熱出爐囉!這篇文章將進入 Nintendo DS 的內心世界,深入探索它在樸實外表下所隱藏的強大能力。然後,同樣地以一個簡單的範例介紹基礎的背景圖件顯示功能。

首先要瞭解的是 DS 的硬體架構。

DS 是一個擁有雙中央處理器 (CPU) 的系統,兩個處理器分別是 ARM 9 以及 ARM 7;ARM 9 是一個 32-bits 架構 66 MHhz 速度的處理器,而 ARM 7 是一個 32-bits 架構 33 MHhz 速度的處理器。DS 擁有 4 MB 的主記憶體容量,總計 656 KB 的顯示記憶體容量,以及數個大小不等的快取記憶體;詳細的內容配置,請參照這張 DS 的記憶體架構圖。除了核心架構以外,當然還有音效硬體、麥克風、觸控螢幕、主機按鍵、GBA 卡匣槽,以及 Wi-Fi 網路的支援能力。

不同於 PC 平台上動輒成千上百的記憶體容量,整個 DS 的主記憶體只有 4 MB 的大小,而相當重要的顯示記憶體更是只有 656 KB 的容量;因此,在 DS 上對於任何資源的配置行為,都需要精打細算地評估考量。ARM 9 與 ARM 7 都能夠自由存取主記憶體,而主記憶體通常會用來存放 ARM 9 所使用的遊戲執行檔,以及與遊戲相關的實體檔案。至於 ARM 7 的執行檔,雖然也能夠放在主記憶體中,但是為了效能上的考量,devkitPro 預設的行為會將執行檔放在快取記憶體 IWRAM 之中。另外,DS 的觸控螢幕輸入功能,是交由 ARM 7 所處理的;在 libnds 的預設執行檔中,ARM 7 會在每次主迴圈更新時,讀取觸控螢幕的相關資料,並且將這些資料存入 ARM 9 也能夠讀取的地方。

正如 DS 擁有上下雙螢幕的顯示介面,所以 DS 中也存在著 2 個繪圖核心引擎,能夠分別操作上螢幕與下螢幕的繪圖工作。這兩個繪圖核心引擎,一般分別稱為 Main Engine 以及 Sub Engine。在預設的情況下,Main Engine 是用來操作上螢幕繪圖,而 Sub Engine 則是用來操作下螢幕的繪圖,不過也可以在程式中使用 lcdSwap() 函式,交換上下螢幕所使用的繪圖引擎。而 Main Engine 與 Sub Engine 的不同點包括:

  • Main Engine 擁有 8 種繪圖模式 (Graphics Modes),而 Sub Engine 擁有 6 種繪圖模式;Main Engine 額外的 2 種繪圖模式,能夠用來繪製大型的 Bitmap。
  • Main Engine 可以使用其中一個背景圖層做為 3D 的繪圖模式。
  • Main Engine 可以直接使用記憶體中指定的數值繪製畫面;也就是前篇文章中所使用的 Frame Buffer 模式。

在 DS 的小小世界裡,它總共只認得四種格式的繪圖資料:BitmapTiled BackgroundSprite 以及 3D。Main Engine 以及 Sub Engine 都各能夠使用至多 4 個背景圖件 (Background) 與 128 個 2D 圖件 (Sprite)。而背景圖件有兩種顯示模式,其一是使用單張的 Bitmap 圖檔做為背景顯示;另外一種方法,則是使用拼貼 (Tiled) 的方式,將數個小區塊的圖片組合成為一張完整的背景圖片。在本文的範例程式中,將實作最簡單的 Bitmap 顯示模式。

為了使 Main Engine 與 Sub Engine 順利進行繪圖工作,在程式初始化時,必須先分別指定這兩個引擎所使用的背景類型 (Background Type),共有下列 6 種模式:

  • Framebuffer:直接使用記憶體中的數值。
  • Text:使用拼貼的方式繪製背景圖件。
  • Rotation:與 Text 相同,另外可對背景圖件進行旋轉與縮放操作。
  • Extended Rotation:如同 Framebuffer 模式,可直接使用記憶體中的數值,另外可對背景圖件進行旋轉與縮放操作。
  • 3D:使用類似 OpenGL 的 API 繪製 3D 圖形。
  • Large Bitmap:能夠繪製 512 x 1024 或 1024 x 512 大小,8-bits 模式的巨大 Bitmap 圖片。

關於繪圖模式與背景類型的詳細列表,請參照 Graphics Modes 以及 Background Types 中的表格說明。

舉個例子說明,當在程式中使用 videoSetMode(MODE_5_2D) 時,就是指定了 Main Engine 將使用第 5 種繪圖模式;而在這個繪圖模式之下的 4 層背景,BG0(第 0 層背景)能夠使用 Text 或 3D 背景,BG1 使用 Text 背景,而 BG2 與 BG3 都只能使用 Extended Rotation 背景類型。在一般的使用狀況中,未必會需要同時開啟 4 層背景圖件,因此在程式中,需要程式設計者自行指定要啟動的圖層,例如 videoSetMode(MODE_5_2D | DISPLAY_BG3_ACTIVE) 就是指定 Main Engine 使用第 5 種繪圖模式,並且開啟 BG3 背景圖層。

Video RAM(顯示記憶體,常縮寫為 VRAM),也就是用來處理圖像顯示用的記憶體,其中可以存放 3D 物件的貼圖、2D 的 Sprite 物件,以及各種與繪圖程序相關的資料。DS 中共有 A、B、C、D 至 I,總共 9 條 VRAM Bank,程式設計者必須將它們映射 (Mapping) 至適當的主記憶體位址中,使繪圖引擎能夠取得所需的圖像資料。

這 9 條 VRAM Bank 各有不同的映射用途,例如在程式中使用 videoSetBankA(VRAM_A_MAIN_BG_0x6000000) 的意義,就是把 Bank A 映射至 0x6000000 的記憶體位址上,同時指定這個位址中的內容,是要拿來做為顯示 Main Engine 的背景圖件之用;接著,只要再把圖片資料放進這個位址中,就能夠順利地顯示圖片內容了。搞懂記憶體映射的問題,會是一開始進入 NDS 程式設計時遇到的最大難題。詳細的映射範圍與用途,請參照 VRAM 中的表格內容。

Console 程式設計與 PC 程式設計最大的不同點,在於 Console 需要面對的是更低階以及更底層的記憶體操作程序。而在進入 0 與 1 的微觀操作世界之前,還有一項需要特別提出來解釋的部分:DS 的硬體架構中,沒有浮點數運算器 (Floating Point Unit,簡稱 FPU) 的存在。因此,如果在程式碼中直接使用 float 資料型別的操作,會大幅地影響程式系統的效能。

為了達到浮點數運算的功能,在 DS 程式設計中有一個特殊的 Fixed Point Number 系統,能夠使用 16-bits 或 32-bits 的整數型別來表示浮點數的資料型別。例如在一個 32-bits 的 Integer 中,可以將 32 個 bits 視為 1、15、16 三個部分;最前頭的 1 bit 用來表示正數或負數,接著的 15 個 bit 表示數字的整數部分,而最末端的 16 bits 則當作數字的小數點部分。另外在轉換矩陣 (Transformation Matrix) 的數值設定中,會需要使用 16-bits,視為 0、8、8 三個部分的 Integer;例如 myVariable = 1 << 8,就是將 myVariable 變數的值設為 1 的動作。

與 DS 相關的硬體架構以及準備知識已經介紹得差不多了,接下來終於能夠正式進入程式設計的部分。這次,我們將拋棄親愛的 Visual Studio、甩掉親切的 PAlib,像個男子漢堂堂正正地面對 devkitPro 與 libnds!PAlib 本身相當地易學易用,但是也因為它的包裝與易用性,會使程式設計者忽略了許多基礎的知識以及重要的細節。為了能夠更深入地學習 NDS 的程式設計,務必從最基礎的 libnds 開始著手,才能夠得到更深入的收穫。

在四月下旬時,已經釋出了最新的 devkitPro Updater 1.4.6,如果之前有按照前篇文章的流程安裝過 PAlib,建議先執行 Uninstall 程序移除原有的 devkitPro 以後(PAlib 也會一併被刪除),再使用最新的 Updater 下載安裝新版 devkitPro。而建置 NDS 程式用的 IDE,可以選擇使用 devkitPro 內附的 Programmer’s Notepad。Programmer’s Notepad 所使用的專案附檔名為 *.pnproj,直接點擊就能夠開啟專案,然後按下 Alt+1 就能夠開始建置專案,而 Alt+2 則是清除專案輸出檔。

OK!總算能夠開始撰寫程式了!在 NDS 的程式設計領域中,沒有便利的 API 可以呼叫,沒有妥善包裝的物件可以使用,很多時候程式設計者都需要對硬體的記憶體位址,直接進行 bit 層級的操作。例如:

// 這是什麼鬼?
((uint16*) 0x6800000)[1] = 31;
((uint16*) 0x6800000)[2] = ((31 << 5) | (31 << 10));
((uint16*) 0x6800000)[3] = ((1) | (1<< 5) | (1<< 10));

安西教練說過:「能掌握記憶體位址的人,就能掌握 DS 程式!」但是對於程式設計者來說,很難有多餘的腦容量能夠去記憶這些莫名的記憶體位址與奇怪的數字組合。所以為了便利地操作這些記憶體位址,libnds 已經預先定義了非常多的 Preprocessor Macro 與 Global Variables。在 devkitPro 的安裝目錄下,打開 \include\nds\memory.h 以及 \include\nds\arm9\video.h 檔案,就可以看到許多與記憶體位址相關的定義。

因此重新改寫上述的程式碼,可以轉換成:

// 這樣就清楚多了!
VRAM_A[1] = RGB15(31, 0, 0);
VRAM_A[2] = RGB15(0, 31, 31);
VRAM_A[3] = RGB15(1, 1, 1);

為了使 NDS 能夠讀取圖片資料,在啟動專案編譯程序時,devkitPro 會依照 Makefile 檔案中的指令,將專案目錄中的圖片格式檔案,編譯轉換成特殊的格式,然後產生出一個與檔名相對應的表頭檔 (Header File)。例如一張名稱為 image.png 的圖片會產生出 image.h 表頭檔,而表頭檔的內容如下所示:

#ifndef __IMAGE__
#define __IMAGE__

#define imageBitmapLen 49152
extern const unsigned int imageBitmap[12288];

#define imagePalLen 512
extern const unsigned short imagePal[256];

#endif // __IMAGE__

檔案中定義了 imageBitmap 以及 imagePal 兩個變數,由 imageBitmap 可以存取到 image.png 的圖片資料,而 imagePal 則是 image.png 所使用的調色盤 (Palette) 資料。然後程式設計者就可以利用這兩個變數,將資料複製至合適的記憶體位址以顯示圖片。

為了正確地顯示出 Bitmap 格式的背景圖件,需要按照以下幾個步驟設定:

  1. 設定繪圖模式,並且啟動背景圖層。
  2. 將 VRAM 映射至適當的記憶體位址中。
  3. 設定背景圖層所使用的圖片格式。
  4. 設定背景圖層的轉換矩陣數值。
  5. 將圖片的資料複製至 VRAM 映射的記憶體位址中。

依序完成以上五項步驟之後,即可於 NDS 中順利地顯示出圖片:

#include < nds.h >
#include "image01.h"

void main()
{
    irqInit();
    irqSet(IRQ_VBLANK, 0);

    // 1. 設定繪圖模式,並且啟動背景圖層。
    videoSetMode(MODE_5_2D | DISPLAY_BG3_ACTIVE);

    // 2. 將 VRAM 映射至適當的記憶體位址中。
    vramSetBankA(VRAM_A_MAIN_BG_0x06000000);

    // 3. 設定背景圖層所使用的圖片格式。
    BG3_CR = BG_BMP8_256x256;

    // 4. 設定背景圖層的轉換矩陣數值。
    // 此處是將 2x2 的轉換矩陣設定為單位矩陣
    BG3_XDX = 1 << 8;
    BG3_XDY = 0;
    BG3_YDX = 0;
    BG3_YDY = 1 << 8;

    // 5. 將圖片的資料複製至 VRAM 映射的記憶體位址中。
    dmaCopy(image01Bitmap, BG_GFX, image01BitmapLen);
    dmaCopy(image01Pal, BG_PALETTE, image01PalLen);

    // 完成圖片載入及顯示程序!

    while (1)
    {
        swiWaitForVBlank();
    }
}

範例程式中,我在上下螢幕各顯示了一張圖片,因此除了 Main Engine 以外,需要仿照上述的步驟設定 Sub Engine 的相關參數。在程式中,只要輕輕地點擊觸控螢幕,上下螢幕的圖片就會互換位置,並且發出一個簡短的音效聲音。如果讀者們恰巧擁有 NDS 能夠使用的謎之備份卡,只要把檔案中內附的 NDSDev_Bitmap.nds 拷貝進去,就可以在真正的 NDS 主機上看到程式的執行成果囉。

更多更深入的 NDS 程式設計內容,咱們下次見!(謎之聲:下次是何時啊?= =+)

程式碼範例下載:NDSDev_Bitmap.zip (下載次數: 1010 )

參考資料:


One thought on “初探Nintendo DS程式開發與設計(二)”

  1. 我記得以前作的DS project用的fixed point decimal是19-bit integer 12-bit fraction,
    不知道是不是我記錯了, 還是規格來自SDK, 硬體完全不管?

    半路:
    你有使用過官方的 SDK 開發 DS 遊戲嗎?真好好好好。 O_O

    就我所知,DS 系統中有數種 fixed point 的表示法。你說的那種應該是 f32 = 1.19.12 fixed point number (used for matrices) 的格式;而我目前使用的 0.8.8 格式,是專門用在 Extended Rotation 背景類型的 Scaling 用途中。關於這一點,我想無論是使用 SDK 或者直接操作 bit/byte,行為模式應該都會相同才對。

Leave a Reply