目前,我们已经掌握了如何使用DirectX绘制四边形,纹理映射技术,以及正交摄像机的内容。对于2D游戏的开发,这些内容基本上已经足够了。2D游戏的本质就是图像游戏,2D游戏中的动画其实就是一系列连续动作的图像,称之为序列帧动画。这也是动画片制作的原理。在动画中,每一张图像就是一帧,为了让人们的眼睛感觉出这是一个连续的动作,需要在1秒钟之内至少要变化24帧,才能达到自然过渡的流畅效果。在游戏开发中,手机端至少要达到30帧,电脑端至少要达到60帧,在VR中至少要达到90帧。我们以电脑游戏为例,1秒钟内我们至少要进行60次的画面渲染,我们称之为每秒传输帧数(FPS)。我们在使用DirectX构建Direct3D设备指针对象的时候,有一个参数设置为:D3DParameters.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT; 它代表的意思就是说我们指定游戏的FPS约等于电脑屏幕刷新频率。我们可以通过查看自己电脑的“监视器”来看屏幕刷新频率,如下图:

一般情况下,电脑的屏幕刷新频率都是60赫兹。在Unity中提供了一个Application.targetFrameRate变量来设置这个帧数。但是,这个并不是绝对的。也就是说,在1秒钟之内,并不是绝对的渲染画面60次。这个要根据每次绘制画面的工作量来决定,如果场景非常的复杂,并且还有很多的实时光影效果,那么这个渲染的时间就可能会变长。如果这个场景持续很长时间,那么我们实际的FPS值可能会低于60。这种情况下,会影响我们的动画效果。举一个简单的例子,角色移动速度是每秒2米,也就是说在1秒钟之内,该角色必须完成移动2米的动画。但是,如果由于渲染时间过长,导致FPS值下降,那么它的实际可能就不会完成2米的动画效果。这种情况下,我们就需要让这个动画乘以前后两帧的时间间隔。也就是使用时间来干预控制动画的实际效果。这里的关键点就是前后两帧之间的时间间隔,在DirectX中,我们可以手动计算,在Unity中提供了Time.deltaTime。

我们使用VS2019创建一个新项目“D3D_06_Animation”,这个案例就用来演示2D动画效果,本质就是连续渲染不同的贴图而已。为此,我们提前准备了一系列的图片,如下:

本案例代码主要复制“D3D_05_Texture”旧项目。首先,我们还是声明全局变量,如下代码:

// 引入头文件
#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游戏开发"    // 窗口类名// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 动画播放当前纹理贴图索引(纹理数组下标)
int index = 0;// 动画纹理贴图总数量(纹理数组大小)
const int total = 22;// 定义FVF灵活顶点格式结构体
struct D3D_DATA_VERTEX { FLOAT x, y, z, u, v; };// 定义包含纹理的顶点类型
#define D3D_FVF_VERTEX (D3DFVF_XYZ | D3DFVF_TEX1)// 顶点缓冲区对象
LPDIRECT3DVERTEXBUFFER9 D3DVertexBuffer = NULL;// 纹理对象数组
LPDIRECT3DTEXTURE9 D3DTexture[total];

请注意,本案例需要加载的头文件以及库文件!!!我们定义了一个纹理数组,共计22个纹理对象,也就是对应我们上面的22张图片。我们需要连续渲染纹理数组里面的图片,因此,我们声明了一个全局数组下标变量。接下来就是我们的initScene函数,主要就是构建四边形顶点数组数据,以及加载22个纹理对象。代码如下:

// 初始化四边形顶点数组数据
D3D_DATA_VERTEX vertexArray[] =
{// 三角形V0V1V2,左下角顺时针{ -10.0f, -10.0f, 0.0f, 0.0f, 1.0f },    // 左下角UV坐标(0,1){ -10.0f,  10.0f, 0.0f, 0.0f, 0.0f },    // 左上角UV坐标(0,0){  10.0f,  10.0f, 0.0f, 1.0f, 0.0f },    // 右上角UV坐标(1,0)// 三角形V0V2V3,左下角顺时针{ -10.0f, -10.0f, 0.0f, 0.0f, 1.0f },  // 左下角UV坐标(0,1){  10.0f,  10.0f, 0.0f, 1.0f, 0.0f },    // 右上角UV坐标(1,0){  10.0f, -10.0f, 0.0f, 1.0f, 1.0f },    // 右下角UV坐标(1,1)
};// 创建顶点缓存对象
D3DDevice->CreateVertexBuffer(sizeof(vertexArray), 0, D3D_FVF_VERTEX, D3DPOOL_DEFAULT, &D3DVertexBuffer, NULL);// 填充顶点缓存对象
void* ptr;
D3DVertexBuffer->Lock(0, sizeof(vertexArray), (void**)&ptr, 0);
memcpy(ptr, vertexArray, sizeof(vertexArray));
D3DVertexBuffer->Unlock();// 创建纹理对象,纹理图片统一放在"asset"目录下
for (int i = 0; i < total; i++) {// 拼接纹理贴图文件名称wchar_t file1[10], file2[20];swprintf_s(file1, 10, L"asset/%d", (i + 1));wcscpy_s(file2, 20, file1);wcscat_s(file2, 20, L".jpg");D3DXCreateTextureFromFile(D3DDevice, file2, &D3DTexture[i]);
}// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);// 初始化投影变换
initProjection();// 初始化光照
initLight();

上面的代码几乎没有改动,主要是纹理图片的加载,我们使用一个循环来创建纹理对象。关于如何拼接图片目录和名称,这里不在叙述。值得注意的,这部分操作需要iostream头文件支持。投影变换和光照都是之前“D3D_05_Texture”旧项目中的代码,不需要改动。接下来就是renderScene函数,代码如下:

// 设置纹理
D3DDevice->SetTexture(0, D3DTexture[index]);// 纹理数组索引累加
index++;
if (index >= total) { index = 0; }// 绘制四边形
D3DDevice->SetStreamSource(0, D3DVertexBuffer, 0, sizeof(D3D_DATA_VERTEX));
D3DDevice->SetFVF(D3D_FVF_VERTEX);
D3DDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 2);

上面的代码重点就是渲染的纹理对象是D3DTexture数组中指定下标的元素(纹理对象)。这个下标是一个全局变量,每次渲染一个数组元素后,下标变量累加,如果超出数组长度,就从数组第一个元素(0)继续开始。这样,每次绘制图形的时候,加载的纹理就不一样了。运行之后,就能看到动画效果。

我们发现,动画展示的速度非常快。为了计算到底有多快,我们可以手动计算一下FPS数值是多少。我们增加GetFPS函数,用来计算FPS数值,并通过字体对象打印到窗体上面。首先就是声明GetFPS函数和字体对象:

static float  fps = 0;              // FPS值
static int    frameCount = 0;      // 累积帧数
static float  currentTime = 0.0f;  // 当前时间
static float  lastTime = 0.0f;     // 持续时间// 每调用一次Get_FPS()函数,帧数自增1
frameCount++;// 获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间
currentTime = timeGetTime() * 0.001f;// 如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零
if (currentTime - lastTime > 1.0f)
{// 计算这1秒钟的FPS值fps = (float)frameCount / (currentTime - lastTime);// 将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间lastTime = currentTime;// 将本次帧数frameCount值清零frameCount = 0;
}return fps;

这个函数的算法比较简单,首先注意的是,函数中定义了静态变量,也就是说,即便函数调用结束,该变量的数值依然存在。这样在每次绘制(renderScene函数)调用GetFPS函数的时候,静态变量的值是一直存在的。每次调用GetFPS函数,我们就累加frameCount,同时记录总的渲染时间是否过去了1秒钟。如果超过一秒,就使用总的渲染次数frameCount除以这个时间段(1秒钟左右)。这样,我们就能够计算出FPS数值了。当然,计算完之后,我们会将静态变量frameCount清零,继续计算下一个1秒钟时间段内的FPS数值。需要注意的是,该方法中timeGetTime()函数用户系统时间,该函数需要头文件time.h和库文件winmm.lib的支持。为了能够把FPS数值打印到窗体上,我们还需要创建字体对应,也就是在initScene函数中完成:

// 创建一个字体对象
D3DXCreateFont(D3DDevice, 24, 0, 1, 1, false, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微软雅黑", &D3DFont);

字体对象完成后,就能够在renderScene函数中调用GetFPS函数,打印FPS数值了:

// 定义一个窗口矩形
RECT formatRect;
GetClientRect(hwnd, &formatRect);// 绘制FPS值,同时输出数组下标变量值
float charCount = GetFPS();
std::string mystring = std::string("FPS:") + std::to_string(charCount) + " (" + std::to_string(index) + ")";
int mystringSize = (int)(mystring.length() + 1);
wchar_t* mywstring = new wchar_t[mystringSize];
MultiByteToWideChar(CP_ACP, 0, mystring.c_str(), -1, mywstring, mystringSize);
D3DFont->DrawText(0, mywstring, -1, &formatRect, DT_LEFT, D3DCOLOR_XRGB(255, 0, 0));

上面的代码我们使用了string来显示FPS字符串,同时将string转化成wchar_t字符串,用于D3DFont->DrawText函数的输出。String需要头文件string的支持。运行效果如下:

我们可以看到,我们计算出来的FPS数值基本上就是60,也就是我们电脑屏幕的刷新率。对于我们本案例的动画,60的FPS数值确实有点高了,动画播放太快了。那么,我们如何手动去控制这个FPS数值呢?因为,我们间接调用renderScene函数的位置是在入口wWinMain函数的消息循环过程,我们在这个无限循环中,每次都是间接调用renderScene函数的。其实,我们可以按照GetFPS函数的思路,来计算两次循环之间的时间差,如果符合我们给定的条件,就去调用renderScene函数。例如,我们想要30的FPS数值,也就是1秒钟渲染30次,那么前后渲染两次的时间差就约等于0.5秒。本案例中,我们使用另一种方式来解决动画播放快慢的问题。我们新增加两个全局变量,如下:

// 动画播放总时间,也就是2秒钟绘制完一套纹理贴图
const int second = 2000;// 动画帧间隔时间,播放总时间 ÷ 动画纹理贴图总数量
const int interval = 90;

我们声明一个动画播放一遍的总时间,例如2秒钟。那么每一帧的渲染时间差应该大约是0.09秒左右,也就是90微秒。接下来,我们只需要改动纹理数组的下标变量值即可。让这个下标变量值得变动符合90微秒的限制,代码如下:

// 渲染运行时间差大于等于 interval 才会更新纹理,否则维持原纹理
int currentSecond = timeGetTime();
static int lastSecond = currentSecond;
if (currentSecond - lastSecond >= interval) {index++;lastSecond = currentSecond;if (index >= total) { index = 0; }
}
D3DDevice->SetTexture(0, D3DTexture[index]);

之前的代码可以注释或删除掉,然后使用上面的代码。它的原理也是一样的,就是计算前后两次渲染的时间差,如果大于我们规定的90微秒,就变动纹理数组下标变量。运行代码,我们就可以明显的看到,动画运行的速度下降了。使用这种方式的好处在于,我们可以手动控制动画的播放速度。例如,有两个角色移动速度为5和10,那么在相同时间内,两者的移动距离是不一样的,而且播放的帧数也不一样。否则就会产出不协调的事情发生,两角色迈着相同的步伐,却移动了不同的距离。这里的代码仍然有一个缺陷,也就是上面我们提到时间差对动画的影响。因为上述代码能够运行成功的前提是,我们的FPS数值是60的情况,也就是说快的动画,可以减慢。假如,我们电脑配置很低,实际运行后发现,FPS数值可能非常的低,导致我们在2秒钟之内,不能完成22张贴图的渲染(我们代码是累加的),这样的效果肯定是不行的。如果,我们要求不管实际FPS值是多少,我们都需要在2秒钟之内完成22张贴图的渲染,显然不容易实现。那么,我们就可以根据时间差来快进数组索引变量值,也就是说时间点到哪里,就渲染对应的纹理对象。如果因为本次渲染时间过长,导致下次纹理对象无法渲染的话,就直接渲染下下张纹理图片。总之,保证2秒钟之后,一定是渲染最后一张纹理图片。这就是上面我们提到的,让动画乘以前后两帧的时间间隔,来实现一致的动画效果(计算机硬件导致FPS值偏低的情况)。在Unity中,所有的动画基本上都是按照这个思路来完成的。

帧动画虽然简单,但是需要美工人员绘制大量的图像。在2D动画中,还有另外一种形式的动画,称之为骨骼动画。它的原理就类似于“皮影戏”。这种技术的本质就是将角色的二维图像拆分成多个部分,然后再拼接成一个完整的图像,各个部分通过矩阵变换完成不同的动画帧。其实3D游戏的骨骼动画,也是这个原理。这种2D形式的动画,不需要绘制大量的图像,只需要存储每个帧动画对应的矩阵数据即可。当然,这种动画需要第三方的支持库才能使用的,比如Spine,DragonBones等等,当然Unity也支持2D的骨骼动画。注意,2D骨骼动画虽然能够减少贴图的文件数量,以及动画制作工作量。但是,某些复杂的旋转动画,2D骨骼动画可能就无法完美的实现了。

在2D游戏开发中,贴图是最基本的,称之为精灵(Sprite)。DirectX中专门提供了一个LPD3DXSPRITE对象,用于精灵的支持。这个对象,我们可以理解它就是一个四边形纹理。我们使用VS2019创建一个新项目“D3D_06_Sprite”用来演示精灵的使用方法。首先,还是我们全局变量的声明:

// Direct3D设备指针对象
LPDIRECT3DDEVICE9 D3DDevice = NULL;// 鼠标位置
int mx = 0, my = 0;// 精灵对像
LPD3DXSPRITE D3DSprite = NULL;// 精灵纹理
LPDIRECT3DTEXTURE9 D3DTexture = NULL;

接下来就是我们的initScene函数,代码如下:

// 初始化精灵对像
D3DXCreateSprite(D3DDevice, &D3DSprite);// 创建纹理对象
D3DXCreateTextureFromFile(D3DDevice, L"sunwukong.bmp", &D3DTexture);// 线性纹理
D3DDevice->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
D3DDevice->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);

可以看出,这个精灵对象要比构建四边形简单多了。需要注意的是,精灵对象的使用不需要投影和光照。这个就有点类似于D3DFVF_XYZRHW 顶点类型。接下来就是我们的renderScene函数,代码如下:

// 精灵透明渲染
D3DSprite->Begin(D3DXSPRITE_ALPHABLEND);// 2D坐标转换矩阵
D3DXMATRIX mat;
D3DXVECTOR2 scale = D3DXVECTOR2(0.5f, 0.5f);
D3DXVECTOR2 pos = D3DXVECTOR2(0.0f, 0.0f);
D3DXMatrixTransformation2D(&mat,        // 指向 D3DXMATRIX 结构的变换矩阵NULL,       // 缩放中心向量0.0f,      // 缩放旋转系数&scale,        // 缩放向量,缩小一半(0.5f)NULL,      // 旋转向量0.0f,        // 旋转角度,单位是弧度&pos);      // 平移向量,屏幕的(100,100)坐标处
D3DSprite->SetTransform(&mat);// 矩形区域
RECT rect;
rect.left = 0;
rect.top = 0;
rect.right = 512;
rect.bottom = 512;// 精灵旋转中心点
D3DXVECTOR3 center = D3DXVECTOR3(0, 0, 0);// 精灵位置
D3DXVECTOR3 position = D3DXVECTOR3(0.0f, 0.0f, 0.0f);// 渲染精灵
D3DSprite->Draw(D3DTexture,      // 精灵所用到的纹理&rect,           // 纹理图像指定区域,可以为NULL&position,        // 纹理旋转中心点&position,        // 精灵在屏幕上的渲染位置0xffffffff);  // 纹理颜色调整// 结束渲染
D3DSprite->End();

同代码,我们可以看到,精灵类的使用,还是比较复杂的。这里我们不在详细介绍,代码注释中已经说的差不多了。我们使用了透明纹理,这一点精灵的用法还是很简单的。我们可以运行代码,看看效果:

在2D游戏的实际开发中,很多人还是不怎么使用上面的精灵类,他们仍然坚持使用四边形纹理,只不过就此进行了封装,方便使用。

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

workspace.zip

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

第六章 DirectX 2D游戏和帧动画(上)相关推荐

  1. 游戏设计的艺术:一本透镜的书——第十六章 故事和游戏结构能用间接控制巧妙地联合起来

    这是一本游戏设计方面的好书 转自天之虹的博客:http://blog.sina.com.cn/jackiechueng 感谢天之虹的无私奉献 Word版可到本人的资源中下载 第十六章 故事和游戏结构能 ...

  2. 【DirectX 2D游戏编程基础】DirectX精灵的创建

    首先,说明一下,我的博客里的代码均为完整代码,只要环境搭建没有问题,复制代码即可运行 工程文件下载地址:http://download.csdn.net/detail/shangdi712/90520 ...

  3. 2d游戏中角色动画解决方案

    刚刚在cocos creator论坛中,看到有水友在update更新spriteFrame来做角色动画,其实是可以使用 cc.Animation 来做角色动画,,这是我们游戏的实现方式,给大家参考下. ...

  4. 第六章 ES高级搜索—聚集查询(上)

    聚集查询(Aggregation)提供了针对多条文档的统计运算功能,它不是针对文 档本身内容的检索,而是要将它们聚合到一起运算某些方面的特征值. 聚集查询与SQL语言中的聚集函数非常像,聚集函数在El ...

  5. 【笔记整理】通信原理第六章复习——数字带通传输系统(上)(二进制数字调制)

    数字的带通传输系统 数字信号的传输方式分为基带传输和带通传输.实际中的大多数信道因具有带通特性而不能直接传送基带信号,这是因为数字基带信号往往具有丰富的低频分量.为了使数字信号在带通信道中传输,必须用 ...

  6. 计量经济学-第六章自回归——科克伦检验结果和书上不一致(SIGMASQ)?

    最近很多同学学习计量经济学自回归时,遇见了一个小问题,就是使用EVIEWS进行科克伦检验和教材中的结果不一样,这是因为自EVIEWS8之后,默认的方法发生了改变. EVIEWS8之后 在命令行输ls ...

  7. 使用directX 7结合C#进行2D游戏编程

    使用directX 7结合C#进行2D游戏编程 前言 对于C#的开发人员来讲,GDI+ 是一个拥有丰富的绘图API指令.传统.高效的程序集.但不幸的是,你要想用她来开发一个复杂而又平滑的动画的时候,我 ...

  8. python scratch unity_Unity3D研究院之2D游戏开发制作原理(二十一)

    经过了4个月不懈的努力,我和图灵教育合作的这本3D游戏开发书预计下个月就要出版了.这里MOMO先打一下广告,图灵的出版社编辑成员都非常给力,尤其是编辑小花为这本书付出了很大的努力,还有杨海玲老师,不然 ...

  9. 2D游戏比3D游戏哪个更好做?游戏行业什么职业最吃香?

    通常情况下,同样档次2D游戏,开发成本和难度低于3D游戏. 对比两组共六个游戏,分别说明目前业界在2D游戏和3D游戏不同做法. 2D游戏(征途) 2D游戏(龙之皇冠) 2D游戏(奥利和迷雾森林) 3D ...

最新文章

  1. python下什么-什么是Python?最全的python百科
  2. 简明代码介绍类激活图CAM, GradCAM, GradCAM++
  3. nvcc fatal : Unsupported gpu architecture 'compute_11'
  4. 不愧是阿里大佬,mysql存储过程写法案例
  5. 原生的html组件,如何创建HTML5与原生UI组件混合的移动应用程序
  6. 11月25日发!余承东官宣华为Mate新成员:最强悍高端平板?
  7. Bailian3756 多边形内角和【数学计算】
  8. ShowAPI识别验证码
  9. Android手游lua脚本的加密与解密
  10. 数字化时代,安全沙箱技术促进企业网络安全生态安全运转
  11. 怎么注册购买163VIP邮箱
  12. 零基础学习UI设计,有哪些软件推荐
  13. 自己动手,解决微信投票提示“投票失败”问题
  14. c语言方程没有解,【C语言】一元二次方程的解
  15. 初识QT之QTWidget窗口
  16. HLG 火影忍者之~静音
  17. Mathematica画图的问题
  18. 2022危险化学品经营单位主要负责人考试题库及答案
  19. LWIP (chapter 2.01) pbuf数据包缓存
  20. STM32F4学习笔记1

热门文章

  1. cat <<EOF与cat <<-EOF的区别
  2. php反调试,反调试原理
  3. Virus_LPK专杀
  4. 如何在Windows7下配置ASP服务器IIS
  5. 大数据Hive函数高阶
  6. 攻防世界-MISC-练习区-12(功夫再高也怕菜刀)
  7. 新的一年如何给自己制定一个年度计划
  8. OTO模式 传统产业掘金互联网时代的利器
  9. 硬件工程师遇到的问题总结
  10. 走进WebKit--开篇