初探Nintendo DS程式開發與設計(三):二維世界小精靈

延續前篇文章中所介紹的 Bitmap 背景圖件顯示功能後,本篇開始進入 2D 圖件 (Sprite) 的二維世界。所謂的 Sprite,原意是指「調皮搗蛋的小精靈」,但是在電腦繪圖與遊戲程式的領域中,Sprite 一詞則被用來當作二維繪圖物件的代名詞:

Sprite is the term used to describe a 2D graphics with a number of related functions.

nds-dev-sprite從前,在那個只有 X 軸與 Y 軸的平面世界中,Sprite 物件就是一切事物的根基,能夠用來表示角色人物、怪物敵人以及武器裝備等許多不同的遊戲物件。Sprite 可以只是一張單純的靜態圖片,也可以由數張圖片組合成為動畫的形式。簡單來講,Sprite 不過是一張可以移動與旋轉的 2D 貼圖罷了。

以 DirectX 為例,在 D3D 中已經包含了 Sprite 的物件功能,能夠使用 LPD3DXSPRITE 與 D3DXSprite() 輕易地創建出 Sprite 物件。即使目前許多平台上的遊戲,都已經趨向全 3D 型態的呈現方式,但是如果能夠應用得宜的話,傳統的 Sprite 物件繪圖模式仍然有很多的可能性存在。例如著名的線上遊戲《仙境傳說》,就是以 2D Sprite 人物,搭配 3D 型態場景的絕佳實例。

如果之前有接觸過 GBA 程式開發與設計知識的讀者,應該能夠很容易發現 NDS 平台與 GBA 平台的硬體架構與核心功能,其實擁有許多的相同之處。任天堂公司保留了原本設計良好的 GBA 架構,然後更進一步延伸,加入了 3D 繪圖、觸控操作、雙處理器引擎等等進階的硬體架構與功能。但是在 2D 繪圖的處理層面,仍然沿襲 GBA 平台的各種設定以及使用方法。而 Sprite 的許多定義與操作行為,也都是由 GBA 時代流傳下來的概念,因此在 NDS 平台上,Sprite 同樣佔了舉足輕重的關鍵地位。

正如 2D Memory Layout 的圖片所示,在 NDS 的記憶體配置架構中,有一個部分是專門為 Sprite 物件功能所設置的區域:Main Engine 所能夠使用的 Sprite Attributes Memory 位於 0x07000000 的記憶體位址上;而 Sub Engine 所能夠使用的 Sprite Attributes Memory 則位於 0x07000400 位址。在這一塊用途限定的記憶體位址中,能夠使程式設計者設定各種與 Sprite 物件相關的屬性,包括 Sprite 的位置、大小、形狀、色彩模式、顯示優先權、特殊效果等等。

而在 Sprite 的功能限制上,Main Engine 以及 Sub Engine 個別能夠:

  • 擁有 128 個 Sprite 物件。
  • 擁有 32 個仿射轉置 (Affine Transformation) 矩陣。
  • 可使用 16 色以及 256 色兩種色彩模式。
  • 可使用 1024 個圖塊 (Tile)。

所謂的仿射轉置矩陣,是用來轉換座標空間用的一種數學方法,只要按照矩陣公式調整其中的數值,就能夠使二維空間中的 Sprite 物件呈現出旋轉 (Rotation)縮放 (Scale) 的效果。但是在 128 個 Sprite 物件中,並不是全部都能擁有獨佔的仿射轉置矩陣;最多能夠產生出 32 個不同的仿射轉置矩陣,然後由所有的 Sprite 物件共享。在色彩模式的部分,Sprite 物件能夠使用 16 色 (4-bits) 與 256 色 (8-bits) 兩種模式;這兩種色彩模式都會使用到調色盤 (Palette) 的顏色配置方法,以達到節省記憶體空間的目的。而關於圖塊的定義與功用,將保留到下一篇文章中詳述。

以 Main Engine 的操作為例,如果要使用 Sprite 的功能,首先要啟動 NDS 的 Sprite 繪圖模式,並且設定繪圖記憶體的映射位置:

// 啟動 Sprite 繪圖模式,並且使用 1D Tile 模式
videoSetMode(MODE_5_2D | DISPLAY_SPR_ACTIVE | DISPLAY_SPR_1D);	

// 將 Bank E 設定給 Sprite 功能使用
vramSetBankE(VRAM_E_MAIN_SPRITE);

在 libnds 的程式碼中,用來操作 Sprite 物件行為與屬性的主要結構為 tOAM;所謂的 OAM,也就是 Object Attribute Memory 的縮寫名稱。所以我們可以在程式中,配置一個由 libnds 所定義的 tOAM 物件,以便於後續操作使用:

// 主要的 OAM 結構
tOAM* pkOAM = new tOAM();

// 總共有 128 個 SpriteEntry 物件可使用
SpriteEntry* pkSprite0 = pkOAM->spriteBuffer[0];
SpriteEntry* pkSprite1 = pkOAM->spriteBuffer[1];
// ...
SpriteEntry* pkSprite127 = pkOAM->spriteBuffer[127];

// 總共有 32 個 SpriteRotation 物件可使用
SpriteRotation* pkSpriteRot0 = pkOAM->matrixBuffer[0];
SpriteRotation* pkSpriteRot1 = pkOAM->matrixBuffer[1];
// ...
SpriteRotation* pkSpriteRot31 = pkOAM->matrixBuffer[31];

而在開始使用 Sprite 物件之前,最好先將 tOAM 結構的內容重新設定為初始化的狀態,包含 spriteBuffer 與 matrixBuffer 在內:

// 重設 Sprite Buffer 的內容
for (int i = 0; i < SPRITE_COUNT; i++)
{
    pkOAM->spriteBuffer[i].attribute[0] = ATTR0_DISABLED;
    pkOAM->spriteBuffer[i].attribute[1] = 0;
    pkOAM->spriteBuffer[i].attribute[2] = 0;
}

// 重設 Matrix Buffer 的內容
for (int i = 0; i < MATRIX_COUNT; i++)
{
    pkOAM->matrixBuffer[i].hdx = 1 << 8;
    pkOAM->matrixBuffer[i].hdy = 0;
    pkOAM->matrixBuffer[i].vdx = 0;
    pkOAM->matrixBuffer[i].vdy = 1 << 8;
}

其中的 SPRITE_COUNT 為 libnds 內定預設值 128,而 MATRIX_COUNT 則為 32。在 spriteBuffer 中總共存在三大分類的屬性,所以必須將這些屬性值全部關閉重設;另外,matrixBuffer 則是如前篇文章所提的作法,將其重設為單位矩陣。在更改了 Sprite 的屬性值之後,接著需要使用 dmaCopy() 函式將 tOAM 中的 spriteBuffer 資料複製至指定的記憶體位址中:

DC_FlushAll();
dmaCopy(pkOAM->spriteBuffer, OAM, SPRITE_COUNT * sizeof(SpriteEntry));

這裡的 OAM,也就是由 libnds 所定義的 Sprite Attributes Memory 位址 0x07000000。以上這些資訊,全部都定義在 libnds 目錄下的 include/nds/arm9/sprite.h 檔案中,只要有安裝 devkitPro 就能夠隨時查閱檢視。

接下來,為了讓 Sprite 物件能夠自由地在二維的平面世界中轉來轉去,在此要先定義一項全新的幾何學規則:

一個圓的角度為 512 度。

請暫時捨棄我們所熟知的「一個圓為 360 度」的基礎數學知識。為什麼要使用這樣奇怪的自訂規則系統呢?因為 512 是 2 的冪次方,會比較便於在電腦系統中使用移位計算。一個圓是 512 度,半個圓則是 256 度,因此角度與徑度相互轉換的公式如下所示:

#define PI (3.1415926)

// 徑度轉換成角度
inline int Rad2Deg(float fRadius)
{
	return (int)(fRadius * (256 / PI));
}

// 角度轉換成徑度
inline float Deg2Rad(int fDegree)
{
	return (fDegree * (PI / 256));
}

定義了全新的 512 度系統以及徑度角度轉換的函式之後,就可以開始盡情地轉動 Sprite 物件了:

// 取得仿射矩陣
SpriteRotation* pkSpriteRot0 = pkOAM->matrixBuffer[0];

// 取得 sin 值與 cos 值
s16 s = SIN[fAngle & SPRITE_ANGLE_MASK] >> 4;
s16 c = COS[fAngle & SPRITE_ANGLE_MASK] >> 4;

// 設定矩陣數值
pkSpriteRot0->hdx = c;
pkSpriteRot0->hdy = s;
pkSpriteRot0->vdx = -s;
pkSpriteRot0->vdy = c;

上述程式碼主要是利用在 include/nds/arm9/trig_lut.h 中預先定義好的 SIN 與 COS 查值表 (Lookup Table) ,取出對應於 512 角度系統的 sin 函數值與 cos 函數值,然後填入仿射轉置矩陣之中,即可使 Sprite 達到旋轉的效果。這個部分的程式碼,運用了數學中的線性代數理論,如果有興趣深入瞭解仿射轉置矩陣的使用原理,可以參考「The Affine Transformation Matrix」這篇文章的內容。

搞定了複雜的旋轉程序之後,接著要移動 Sprite 物件就容易許多了。在 libnds 預先定義好的 SpriteEntry 結構中,已經提供了 posX 與 posY 成員變數供程式設計者直接使用:

// 將 pkSpriteEntry 移動到 (100, 50) 的位置
pkSpriteEntry->posX = 100;
pkSpriteEntry->posY = 50;

而在範例程式中,為了使飛機物件能夠朝著機頭面向的角度移動,必須利用三角函數的公式,分別計算出 X 軸向與 Y 軸向的位移量:

X 軸位移量 = 動量 * sin(角度)
Y 軸位移量 = 動量 * cos(角度)

首先利用 Deg2Rad() 函式,將 512 角度系統的數值轉換成為徑度,然後才能夠將徑度值傳入 sin() 與 cos() 函式中得出相對應的位移量:

// 將角度轉換為徑度
float fRadius = Deg2Rad(fAngle);

// 將 Sprite 的位置分別加上 X 軸與 Y 軸的位移分量
static const int THRUST_FACTOR = 2;
pkSpriteEntry->posX += (int)(THRUST_FACTOR * sin(fRadius));
pkSpriteEntry->posY += (int)(-THRUST_FACTOR * cos(fRadius));

至此,就完成了 Sprite 物件的初始化、更新、旋轉與位移程序!

如前篇文章所提到的內容,NDS 架構中的 Sprite 物件功能,也是藉由記憶體位址的 bits 操作以控制各種相關的屬性值。而在 libnds 的include/nds/arm9/sprite.h 程式碼之中,可以看到作者巧妙地利用了結構的 union 語法,簡化了繁複的記憶體位元操作,使程式設計者能夠在同一塊記憶體區域中,以結構成員變數的方式更輕易地存取各種屬性值。

最後,我將這些與 Sprite 相關的功能,包裝成為一個 SpriteManager 類別;不過程式碼沒有經過太多測試,敬請小心服用:

class SpriteManager
{
public:
    SpriteManager();
    ~SpriteManager();

    void Update();
    SpriteInfo* CreateSprite(SpriteStructure& rkSpriteStructure);
    void TranslateSprite(SpriteInfo* pkInfo, u16 iPosX, u16 iPosY);
    void TranslateOffsetSprite(SpriteInfo* pkInfo, u16 iPosX, u16 iPosY);
    void RotateSprite(SpriteInfo* pkInfo, u16 iAngle);
    tOAM* GetOAM() const { return m_pkOAM; }

private:
    void InitializeOAM();

private:
    tOAM* m_pkOAM;
    SpriteInfo m_kSpriteInfo[SPRITE_COUNT];
    int m_iSpriteIndex;    
    int m_iTileIndex;
};

程式碼範例下載:NDSDev_Sprite.zip (下載次數: 934 )
操作方式:按住方向鍵的「上」可移動飛機,「左」與「右」可旋轉飛機的方向。

參考資料:Introduction to Nintendo DS ProgrammingChapter 6

2 thoughts on “初探Nintendo DS程式開發與設計(三):二維世界小精靈”

Leave a Reply

Leave a Reply