在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>0和Y>0的坐标系来完成游戏世界的加载。本案例中,我们让精灵的开始点位于X=0和Y=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)的区域,这样更加方便我们各种计算了。关于马里奥的游戏,我们就先做到这里,下一章我们继续完善。
本课程的所有代码案例下载地址:
备注:这是我们游戏开发系列教程的第二个课程,这个课程主要使用C++语言和DirectX来讲解游戏开发中的一些基础理论知识。学习目标主要依理解理论知识为主,附带的C++代码能够看懂且运行成功即可,不要求我们使用DirectX来开发游戏。课程中如果有一些错误的地方,请大家留言指正,感激不尽!