第六章 DirectX 2D游戏和帧动画(下)

本文档介绍了如何使用DirectX开发一个简单的2D游戏场景,以超级马里奥为例。通过封装四边形纹理对象,实现地图背景和角色精灵的绘制。重点在于,随着马里奥行走,正交摄像机随之移动。文章详细展示了地图类和精灵类的实现,包括构造函数、渲染方法和析构函数,并提供了用户输入处理,使精灵能根据键盘方向移动并更新动画。
摘要由CSDN通过智能技术生成

2D游戏的实际开发中,很多人还是不怎么使用上面的精灵类,他们仍然坚持使用四边形纹理,只不过就此进行了封装,方便使用。接下来,我们新建一个项目“D3D_06_Mario”。在这个案例中,我们将实现一个简单的超级马里奥的游戏场景,这是一个2D场景。本案例的重点在于,随着马里奥角色的行走,正交摄像机也跟随移动。我们先看看本案例用到的素材:

首先,我们需要封装两个四边形纹理对象,一个是地图背景,一个是角色精灵。两个类的区别在于背景只有一个贴图,而角色精灵需要贴图序列完成动画。为了能够对头文件的统一引入,我们先创建“main.h”文件,代码如下:

#pragma once
#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <iostream>
#include <time.h>
#include <string>

// 引入依赖的库文件
#pragma comment(lib,"d3d9.lib")
#pragma comment(lib,"d3dx9.lib")
#pragma comment(lib,"winmm.lib")

#define WINDOW_LEFT		200				// 窗口位置
#define WINDOW_TOP		100				// 窗口位置
#define WINDOW_WIDTH	800				// 窗口宽度
#define WINDOW_HEIGHT	600				// 窗口高度
#define WINDOW_TITLE	L"D3D游戏开发"	// 窗口标题
#define CLASS_NAME		L"D3D游戏开发"	// 窗口类名

在以后的大部分项目中,我们基本上都会按照这种思路来合理的规划我们的代码文件结构。这种思路就是对公共的内容依文件类的形式进行封装,然后在其他地方引入使用。下面我们封装地图背景,创建“map.h”和“map.cpp”两个文件,其中“map.h”代码如下:

#include "main.h"

// 定义FVF灵活顶点格式结构体
struct D3D_MAP_DATA_VERTEX { FLOAT x, y, z, u, v; };

// 定义包含纹理的顶点类型
#define D3D_MAP_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)

// 地图背景类
class Map {

public:

	// 四边形位置和尺寸
	float xPos, yPos, lenght;

	// 顶点缓冲区对象
	LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer;
	
	// 纹理对象
	LPDIRECT3DTEXTURE9 D3DTexture;

	// Direct3D设备指针对象
	LPDIRECT3DDEVICE9 D3DDevice = NULL;

public:

	// 构造函数
	Map(LPDIRECT3DDEVICE9 device, float x, float y, float len, const wchar_t* file);

	// 渲染函数
	void render();

	// 析构函数
	~Map();

};

然后“map.cpp”代码如下:

#include "map.h"

// 地图背景类:构造函数
Map::Map(LPDIRECT3DDEVICE9 device, float x, float y, float len, const wchar_t* file) {

	// 类属性赋值
	D3DDevice = device;
	xPos = x, yPos = y, lenght = len;
	
	// 四边形顶点数组数据,V0顶点是x/y位置
	D3D_MAP_DATA_VERTEX vertexArray[] =
	{
		{ xPos,		        yPos,			0.0f, 0.0f, 1.0f },
		{ xPos,		        yPos + lenght,	0.0f, 0.0f, 0.0f },
		{ xPos + lenght,	yPos + lenght,	0.0f, 1.0f, 0.0f },
		{ xPos,		        yPos,			0.0f, 0.0f, 1.0f },
		{ xPos + lenght,	yPos + lenght,	0.0f, 1.0f, 0.0f },
		{ xPos + lenght,	yPos,			0.0f, 1.0f, 1.0f },
	};
	
	// 创建顶点缓冲区对象
	D3DDevice->CreateVertexBuffer(sizeof(vertexArray), 0, D3D_MAP_FVF_VERTEX, D3DPOOL_DEFAULT, &D3DVertexBuffer, NULL);
	
	// 填充顶点缓冲区对象
	void* ptr;
	D3DVertexBuffer->Lock(0, sizeof(vertexArray), (void**)&ptr, 0);
	memcpy(ptr, vertexArray, sizeof(vertexArray));
	D3DVertexBuffer->Unlock();

	// 创建纹理对象
	D3DXCreateTextureFromFile(D3DDevice, file, &D3DTexture);
};

// 地图背景类:渲染函数
void Map::render() {

	D3DDevice->SetTexture(0, D3DTexture);
	D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_MAP_DATA_VERTEX));
	D3DDevice->SetFVF(D3D_MAP_FVF_VERTEX);
	D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
};

// 地图背景类:析构函数
Map::~Map() {

	D3DTexture->Release();
	D3DTexture = NULL;
	D3DVertexBuffer->Release();
	D3DVertexBuffer = NULL;
};

这些代码内容基本上就是绘制一个四边形纹理。紧接着是我们精灵类的封装,创建“sprite.h”和“sprite.cpp”两个文件,“sprite.h”代码如下:

#include "main.h"

// 定义FVF灵活顶点格式结构体
struct D3D_SPRITE_DATA_VERTEX { FLOAT x, y, z, u, v; };

// 定义包含纹理的顶点类型
#define D3D_SPRITE_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)

// 自定义精灵类
class Sprite {

public:

	// 精灵位置和尺寸
	float x, y, len;

	// 精灵动画索引
	int animation;
	
	// 精灵移动速度
	float moveSpace;
	
	// 顶点缓冲区对象
	LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer = NULL;

	// 纹理对象数组
	LPDIRECT3DTEXTURE9 D3DTexture[6];

	// Direct3D设备指针对象
	LPDIRECT3DDEVICE9 D3DDevice = NULL;

public:

	// 构造方法
	Sprite(LPDIRECT3DDEVICE9 device, float _x, float _y, float _len);

	// 渲染方法
	void render();

	// 析构方法
	~Sprite();

};

在构建精灵类的时候,我们使用纹理数组以及下标变量animation,以及精灵移动速度moveSpace变量。然后就是“sprite.cpp”的代码实现:

#include "sprite.h"

// 自定义精灵类:构造方法
Sprite::Sprite(LPDIRECT3DDEVICE9 device, float _x, float _y, float _len) {

	// 类属性赋值
	D3DDevice = device;
	x = _x, y = _y, len = _len;
	animation = 1, moveSpace = 1.0f;
	
	// 精灵四边形顶点数组
	D3D_SPRITE_DATA_VERTEX vertexArray[] =
	{
		{ x,		y,		    0.0f, 0.0f, 1.0f },
		{ x,		y + len,	0.0f, 0.0f, 0.0f },
		{ x + len,	y + len,	0.0f, 1.0f, 0.0f },
		{ x,		y,		    0.0f, 0.0f, 1.0f },
		{ x + len,	y + len,	0.0f, 1.0f, 0.0f },
		{ x + len,	y,		    0.0f, 1.0f, 1.0f },
	};
	
	// 创建顶点缓冲区对象
	D3DDevice->CreateVertexBuffer(sizeof(vertexArray), 0, D3D_SPRITE_FVF_VERTEX, D3DPOOL_DEFAULT, &D3DVertexBuffer, NULL);

	// 访问顶点缓存
	void* ptr;
	D3DVertexBuffer->Lock(0, sizeof(vertexArray), (void**)&ptr, 0);
	memcpy(ptr, vertexArray, sizeof(vertexArray));
	D3DVertexBuffer->Unlock();


	// 创建纹理对象
	for (int i = 0; i < 6; i++) {
		// 拼接纹理贴图文件名称
		wchar_t file1[10], file2[20];
		swprintf_s(file1, 10, L"asset/M%d", (i + 1));
		wcscpy_s(file2, 20, file1);
		wcscat_s(file2, 20, L".bmp");
		D3DXCreateTextureFromFile(D3DDevice, file2, &D3DTexture[i]);
	}
};

// 自定义精灵类:渲染方法
void Sprite::render() {

	// 开启Alpha融合
	D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
	// 设置混合因子
	D3DDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
	D3DDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
	// 设置融合运算方式(相加)
	D3DDevice->SetRenderState(D3DRS_BLENDOP, D3DBLENDOP_ADD);
	// 设置纹理
	D3DDevice->SetTexture(0, D3DTexture[animation]);
	// 绘制四边形
	D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_SPRITE_DATA_VERTEX));
	D3DDevice->SetFVF(D3D_SPRITE_FVF_VERTEX);
	D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);
	// 关闭Alpha混合
	D3DDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);
};

// 自定义精灵类:析构方法
Sprite::~Sprite() {

	for (int i = 0; i < 6; i++) {
		D3DTexture[i]->Release();
		D3DTexture[i] = NULL;
	}
	D3DVertexBuffer->Release();
	D3DVertexBuffer = NULL;
};

精灵类的实现本质也是四边形纹理,在渲染的时候,我们启用了透明纹理。两个类都封装完毕之后,我们就可以在主源文件main.cpp使用了,首先是全局变量的声明:

// 引入头文件
#include "main.h";
#include "map.h"
#include "sprite.h"

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;

// 鼠标位置
int mx = 0, my = 0;

// 地图背景对象数组
Map* map[3];

// 精灵对象
Sprite* sprite;

我们声明了3个地图背景对象,其实也可以使用一个大的地图。因为本案例使用的地图文件很小,因此直接加载它是没有问题的。但是对于大的地图,可以将地图切分成几块,分别加载。接下来就是我们的initScene函数,代码如下:

// 初始化地图背景对象数组
map[0] = new Map(D3DDevice, 0,  -5.3f, 40, L"asset/bg1.jpg");
map[1] = new Map(D3DDevice, 40, -5.3f, 40, L"asset/bg2.jpg");
map[2] = new Map(D3DDevice, 80, -5.3f, 40, L"asset/bg3.jpg");

// 初始化精灵对象
sprite = new Sprite(D3DDevice, 0.0f, 0.0f, 5);

// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

// 初始化投影变换
initProjection();

// 初始化光照
initLight();

这里,我们要着重说一下地图的加载,这个和投影变换有很大的关系。2D游戏都是在X/Y的二维坐标系中完成的。在这个二维坐标系中,所有的物体加载都需要X/Y坐标值。为了方便我们的计算,我们一般使用X>0Y>0的坐标系来完成游戏世界的加载。本案例中,我们让精灵的开始点位于X=0Y=0的坐标系原点,而地图也根据精灵的位置来调整。这样做的好处就是,精灵的很多移动,攻击等坐标值操作,只考虑正数,不考虑负数了。那么,既然场景地图和精灵的位置都发生了改变,为了让玩家能够重新调整视角,我们同步调整正交摄像机的位置和观察点,让游戏内容展示在窗体的中央位置。initProjection函数如下:

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
D3DXVECTOR3 viewEye(40.0f, 15.0f, -10.0f);
D3DXVECTOR3 viewLookAt(40.0f, 15.0f, 0.0f);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

// 正交投影变换
D3DXMATRIX matProject;
D3DXMatrixOrthoLH(&matProject, WINDOW_WIDTH / 10, WINDOW_HEIGHT / 10, 0, 100);
D3DDevice->SetTransform(D3DTS_PROJECTION, &matProject);

// 视口变换
D3DVIEWPORT9 viewport = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, 1 };
D3DDevice->SetViewport(&viewport);

以上代码主要是摄像机的位置和观察点的设置,然后我们采用正交投影。initLight函数就不在叙述了,我们依然使用全局光照和默认材质。下面来介绍renderScene函数,代码如下:

// 世界矩阵
D3DXMATRIX bgMatrix;
D3DXMatrixTranslation(&bgMatrix, 0, 0, 0);
D3DDevice->SetTransform(D3DTS_WORLD, &bgMatrix);

// 绘制地图背景
map[0]->render();
map[1]->render();
map[2]->render();

// 世界矩阵
D3DXMATRIX worldMatrix;
D3DXMatrixTranslation(&worldMatrix, sprite->x, sprite->y, 0);
D3DDevice->SetTransform(D3DTS_WORLD, &worldMatrix);

// 绘制精灵
sprite->render();

上述代码,也非常的简练,主要是绘制地图和精灵。注意,精灵的绘制,使用了精灵的坐标值。因为我们接下来要根据用户的键盘输入来控制精灵的移动,也就是改变起X值。先运行代码,看看目前的效果:

我们可以看到,地图和精灵的位置都是正确的。接下来,我们来根据用户的输入来控制精灵的移动和动画。说白了,就是修改精灵的纹理数组下标变量和位置X值。但是,最重要的问题就是,正交摄像机要跟随精灵一起移动。因此,我们需要声明正交摄像机位置和观察点变量,代码如下:

// 摄像机位置,观察点
float eyeX = 0.0f, eyeY = 0.0f, eyeZ = 0.0f;
float lookAtX = 0.0f, lookAtY = 0.0f, lookAtZ = 0.0f;

然后在initProjection函数中,使用这个全局变量,代码如下:

// 设置取景变换矩阵
D3DXMATRIX viewMatrix;
eyeX = lookAtX = 40.0f;
eyeY = lookAtY = 15.0f;
eyeZ = -10.0f;
lookAtZ = 0.0f;
D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ);
D3DXVECTOR3 viewLookAt(lookAtX, lookAtY, lookAtZ);
D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewLookAt, &viewUp);
D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);

现在就是我们要完成用户交互功能了,我们直接给出update函数代码:

// 只处理键盘按下事件
if (type != 1) return;

// 默认不移动摄像机
bool input = false;

switch (wParam) {

	case VK_LEFT:

		// 精灵向左移动
		if (sprite->x > 0) {

			// 纹理数组下标变动
			if (sprite->animation < 3) {
				// 转向
				sprite->animation = 4;
			} else {
				// 前进
				sprite->animation++;
				if (sprite->animation > 5) sprite->animation = 3;
			}

			// 位置变动
			sprite->x -= sprite->moveSpace;
		}

		// 摄像机跟随向左移动
		if (sprite->x < 80 && eyeX > 40) {

			eyeX -= sprite->moveSpace;
			lookAtX -= sprite->moveSpace;
			input = true;
		}
		break;

	case VK_RIGHT:

		// 精灵向右移动
		if (sprite->x < 115) {

			// 纹理数组下标变动
			sprite->animation++;
			if (sprite->animation > 2) sprite->animation = 0;

			// 位置变动
			sprite->x += sprite->moveSpace;
		}

		// 摄像机跟随向右移动
		if (sprite->x > 10 && eyeX < 80) {
			eyeX += sprite->moveSpace;
			lookAtX += sprite->moveSpace;
			input = true;
		}
		break;
}

// 更新取景变换
if (input) {
	D3DXMATRIX viewMatrix;
	D3DXVECTOR3 viewEye(eyeX, eyeY, eyeZ);
	D3DXVECTOR3 viewAt(lookAtX, lookAtY, lookAtZ);
	D3DXVECTOR3 viewUp(0.0f, 1.0f, 0.0f);
	D3DXMatrixLookAtLH(&viewMatrix, &viewEye, &viewAt, &viewUp);
	D3DDevice->SetTransform(D3DTS_VIEW, &viewMatrix);
}

上面的代码比较混乱,混乱的原因有两点。第一,需要调整animation的数值,以达到动画播放正确。第二,就是摄像机的跟随在地图最两边的时候,要做调整。最后一点要说明的就是,关于上面一些固定数值的问题。我们之前讲过,图形(精灵)的虚拟单位和窗体屏幕的像素单位的转化是由摄像机来决定的。因此,当摄像机保持在一个特定位置的时候,我们就可以让图形(精灵)的虚拟单位=像素单位。这样做的好处,就是我们可以根据贴图的像素尺寸来做参考,用于图形(精灵)的各种数值操作。而且,我们之前将地图和精灵的位置都放到了XY坐标系的第一象限(X>0&&Y>0)的区域,这样更加方便我们各种计算了。关于马里奥的游戏,我们就先做到这里,下一章我们继续完善。

  本课程的所有代码案例下载地址:

workspace.zip

备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咆哮的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值