niubo_
2009-03-12 08:02:56
摘自:[url]http://blog.monkeypotion.net/gameprog/note/nds-dev-sprite[/url]
作者:猴子灵药
[float=left][attach]1003[/attach][/float]
延续前篇文章中所介绍的 Bitmap 背景图件显示功能后,本篇开始进入 2D 图件 (Sprite) 的二维世界。所谓的 Sprite,原意是指「调皮捣蛋的小精灵」,但是在电脑绘图与游戏程式的领域中,Sprite 一词则被用来当作二维绘图物件的代名词:
[quote]Sprite is the term used to describe a 2D graphics with a number of related functions. [/quote]
[indent]从前,在那个只有 X 轴与 Y 轴的平面世界中,Sprite 物件就是一切事物的根基,能够用来表示角色人物、怪物敌人以及武器装备等许多不同的游戏物件。Sprite 可以只是一张单纯的静态图片,也可以由数张图片组合成为动画的形式。简单来讲,Sprite 不过是一张可以移动与旋转的 2D 贴图罢了。[/indent]
以 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 位于 0×07000000 的记忆体位址上;而 Sub Engine 所能够使用的 Sprite Attributes Memory 则位于 0×07000400 位址。在这一块用途限定的记忆体位址中,能够使程式设计者设定各种与 Sprite 物件相关的属性,包括 Sprite 的位置、大小、形状、色彩模式、显示优先权、特殊效果等等。
而在 Sprite 的功能限制上,Main Engine 以及 Sub Engine 个别能够:
[quote][list]
[*]拥有 128 个 Sprite 物件。
[*]拥有 32 个仿射转置 (Affine Transformation) 矩阵。
[*]可使用 16 色以及 256 色两种色彩模式。
[*]可使用 1024 个图块 (Tile)。
[/list][/quote]
[indent]所谓的仿射转置矩阵,是用来转换座标空间用的一种数学方法,只要按照矩阵公式调整其中的数值,就能够使二维空间中的 Sprite 物件呈现出旋转 (Rotation) 与缩放 (Scale) 的效果。但是在 128 个 Sprite 物件中,并不是全部都能拥有独佔的仿射转置矩阵;最多能够产生出 32 个不同的仿射转置矩阵,然后由所有的 Sprite 物件共享。在色彩模式的部分,Sprite 物件能够使用 16 色 (4-bits) 与 256 色 (8-bits) 两种模式;这两种色彩模式都会使用到调色盘 (Palette) 的颜色配置方法,以达到节省记忆体空间的目的。而关于图块的定义与功用,将保留到下一篇文章中详述。[/indent]
以 Main Engine 的操作为例,如果要使用 Sprite 的功能,首先要启动 NDS 的 Sprite 绘图模式,并且设定绘图记忆体的映射位置:
[code=c] // 启动 Sprite 绘图模式,并且使用 1D Tile 模式
videoSetMode(MODE_5_2D | DISPLAY_SPR_ACTIVE | DISPLAY_SPR_1D);
// 将 Bank E 设定给 Sprite 功能使用
vramSetBankE(VRAM_E_MAIN_SPRITE);[/code]
在 libnds 的程式码中,用来操作 Sprite 物件行为与属性的主要结构为 tOAM;所谓的 OAM,也就是 Object Attribute Memory 的缩写名称。所以我们可以在程式中,配置一个由 libnds 所定义的 tOAM 物件,以便于后续操作使用:
[code=c]// 主要的 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];[/code]
而在开始使用 Sprite 物件之前,最好先将 tOAM 结构的内容重新设定为初始化的状态,包含 spriteBuffer 与 matrixBuffer 在内:
[code=c]// 重设 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;
}[/code]
其中的 SPRITE_COUNT 为 libnds 内定预设值 128,而 MATRIX_COUNT 则为 32。在 spriteBuffer 中总共存在三大分类的属性,所以必须将这些属性值全部关闭重设;另外,matrixBuffer 则是如前篇文章所提的作法,将其重设为单位矩阵。在更改了 Sprite 的属性值之后,接着需要使用 dmaCopy() 函式将 tOAM 中的 spriteBuffer 资料复制至指定的记忆体位址中:
[code=c]DC_FlushAll();
dmaCopy(pkOAM->spriteBuffer, OAM, SPRITE_COUNT * sizeof(SpriteEntry));[/code]
这里的 OAM,也就是由 libnds 所定义的 Sprite Attributes Memory 位址 0×07000000。以上这些资讯,全部都定义在 libnds 目录下的 include/nds/arm9/sprite.h 档案中,只要有安装 devkitPro 就能够随时查阅检视。
接下来,为了让 Sprite 物件能够自由地在二维的平面世界中转来转去,在此要先定义一项全新的几何学规则:
一个圆的角度为 512 度。
请暂时捨弃我们所熟知的「一个圆为 360 度」的基础数学知识。为什么要使用这样奇怪的自订规则系统呢?因为 512 是 2 的幂次方,会比较便于在电脑系统中使用移位计算。一个圆是 512 度,半个圆则是 256 度,因此角度与径度相互转换的公式如下所示:
[code=c]#define PI (3.1415926)
// 径度转换成角度
inline int Rad2Deg(float fRadius)
{
return (int)(fRadius * (256 / PI));
}
// 角度转换成径度
inline float Deg2Rad(int fDegree)
{
return (fDegree * (PI / 256));
}[/code]
定义了全新的 512 度系统以及径度角度转换的函式之后,就可以开始尽情地转动 Sprite 物件了:
[code=c]// 取得仿射矩阵
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;[/code]
上述程式码主要是利用在 include/nds/arm9/trig_lut.h 中预先定义好的 SIN 与 COS 查值表 (Lookup Table) ,取出对应于 512 角度系统的 sin 函数值与 cos 函数值,然后填入仿射转置矩阵之中,即可使 Sprite 达到旋转的效果。这个部分的程式码,运用了数学中的线性代数理论,如果有兴趣深入瞭解仿射转置矩阵的使用原理,可以参考「The Affine Transformation Matrix」这篇文章的内容。
搞定了复杂的旋转程序之后,接着要移动 Sprite 物件就容易许多了。在 libnds 预先定义好的 SpriteEntry 结构中,已经提供了 posX 与 posY 成员变数供程式设计者直接使用:
[code=c]// 将 pkSpriteEntry 移动到 (100, 50) 的位置
pkSpriteEntry->posX = 100;
pkSpriteEntry->posY = 50;[/code]
而在范例程式中,为了使飞机物件能够朝着机头面向的角度移动,必须利用三角函数的公式,分别计算出 X 轴向与 Y 轴向的位移量:
[indent]X 轴位移量 = 动量 * sin(角度)
Y 轴位移量 = 动量 * cos(角度) [/indent]
首先利用 Deg2Rad() 函式,将 512 角度系统的数值转换成为径度,然后才能够将径度值传入 sin() 与 cos() 函式中得出相对应的位移量:
[code=c]// 将角度转换为径度
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));[/code]
至此,就完成了 Sprite 物件的初始化、更新、旋转与位移程序!
如前篇文章所提到的内容,NDS 架构中的 Sprite 物件功能,也是藉由记忆体位址的 bits 操作以控制各种相关的属性值。而在 libnds 的include/nds/arm9/sprite.h 程式码之中,可以看到作者巧妙地利用了结构的 union 语法,简化了繁复的记忆体位元操作,使程式设计者能够在同一块记忆体区域中,以结构成员变数的方式更轻易地存取各种属性值。
最后,我将这些与 Sprite 相关的功能,包装成为一个 SpriteManager 类别;不过程式码没有经过太多测试,敬请小心服用:
[code=c]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;
};[/code]
程式码范例下载:[attach]1004[/attach]
操作方式:按住方向键的「上」可移动飞机,「左」与「右」可旋转飞机的方向。
参考资料:[url=http://patater.com/manual]Introduction to Nintendo DS Programming[/url]:[url=http://patater.com/files/projects/manual/manual.html#id2740389]Chapter 6[/url]
niubo_
2009-03-12 08:07:43
一点说明:
这个示例程序中对于控制精灵的部分的操作都是自己动手写的代码,对于了解精灵的工作原理很有参考价值。实际上新版的devkitpro中已经有一些用来控制精灵的数据类型和函数,可以参考附带的sprint相关示例。