古之学者必有师,对于技术的提升,只靠自己的摸索虽然能得到深刻的经验,但往往没有较高的效率。笔者这些天学习了BV1eM4m1S74K"提瓦特幸存者"的C++开发,也是实现了该类型游戏的开发。

今天,就通过经验总结,亲手结束这一小段学习过程!

游戏的基本框架
所有游戏的底层框架都是通过一个主循环来刷新画布,每个循环中都实现读取操作、处理数据、绘制画面这三个步骤。
cpp
int main() {
while (running) {
//动态延时(记录本次循环开始的时间)
DWORD start_time = GetTickCount(); //32位无符号整数,长度不随编译器变化
//读取操作区域
while (peekmessage(&msg)) {}
//处理数据区域
cleardevice();
//渲染画面区域
FlushBatchDraw();
//记录循环结束时间
DWORD end_time = GetTickCount();
DWORD delta_time = end_time - start_time;
//1秒144次刷新,即帧率为144,一个循环就是一帧
if (delta_time < 1000 / 144) {
Sleep(1000 / 144 - delta_time);
}
}
EndBatchDraw();
return 0;
}
在这些操作执行完后,加上动态延时从而实现游戏开发中"帧"的概念。这也是游戏引擎设计的基本框架,是不是很有Update()函数的感觉?

关于渲染缓冲区:
为了隐藏渲染或其他处理的过程,BeginBatchDraw函数会让后续的渲染操作在缓冲区中进行,像一块闲置的画布,当调用FlushBatchDraw或EndBatchDraw,即所有操作绘制结束时,直接代替当前画布,达到无缝衔接的效果。
一些代码细节:
视频的创作者在许多地方都体现了C++的编码细节,比如使用static静态变量只在第一次调用时创建(后续调用会跳过)复用同一内存;使用TCHAR这一Windows兼容类型适应非英文环境;同样使用宽字符串适应非英文环境......
头文件依赖问题
在这次C++开发的过程中,我总是被所谓"出现未定义的变量名"给搞到破防。这是头文件重复包含导致的。

一个项目各个区域的执行顺序是全局变量->静态全局变量->函数声明->静态函数声明->类定义->静态类成员定义->命名空间变量->函数定义......头文件一般用来包含变量与函数的声明、类的定义等重要信息。
头文件的循环包含引发编译错误的原因:
编译器处理 #include 时,会把对应头文件内容嵌入包含位置。若头文件循环包含,其可能会陷入无限递归尝试展开头文件的情形。即便使用包含守卫( #ifndef 三件套 )或 #pragma once 规避重复展开,由于头文件解析时需要对方类型完成自身声明或定义,循环依赖会导致部分必要的声明或定义无法在依赖解析阶段正确处理。
类似A.h在第一行包含了B.h,同时B.h的第一行包含了A.h, B类使用的A类的定义,但是在头文件依赖解析A.h时,先展开B.h,但B.h遍历到A.h会因为包含守卫而跳过,这下B类里A就没有了定义......
这里有三个编程习惯可以尽量避免头文件重复包含的问题:
1、使用前向声明,在使用A的头文件中声明声明一下A类。不需要访问具体成员时需要。
2、使用包含守卫,即 #ifndef、#define、#endif 连招。
3、使用 #pragma once,可替代包含守卫,使头文件在一个编译单元中只包含一次。
但这些方法只能尽量避免我们遇到的问题,最重要的还是在一开始就规划好项目的结构。保持头文件声明、源文件定义的好习惯,在必要时进行重构,让文件包含的脉络清晰,一目了然。

动画与MCI工具
游戏中使用的动画分为骨骼动画(关键帧动画)与序列帧动画。序列帧动画就是让图片素材以若干个帧为单位进行交替,从而达到动画播放的效果。
这里使用自定义图集类来批量载入名称有规律的图片素材。
cpp
Atlas::Atlas(LPCTSTR path, int num) {
TCHAR path_file[256];
for (size_t i = 0; i < num; ++i) {
_stprintf_s(path_file, path, i);
IMAGE* frame = new IMAGE();
loadimage(frame, path_file);
frame_list.push_back(frame);
}
}
这种方法依次用从零开始的自然数代替图集中的数字部分,实现图片载入。

MCI工具(媒体控制接口)能够让我们以字符串的形式对windows系统发出指令,控制音乐的播放。
但在我的测试中,mp3文件在播放时,会明显影响游戏的帧率。经过测试与查询,这与MP3格式文件的特性有关:编解码边播放。而MCI解码的消息可能会打断Sleep,让主循环提前醒来,导致帧率变高。

所以在加载音频文件时,更推荐使用WAV格式:1、MCI 加载 MP3 资源时,底层会创建额外的线程或窗口,并且会向主消息队列发送消息(比如 MM_MCINOTIFY),EasyX 的 peekmessage 也会处理这些消息。2、WAV 文件是无压缩格式,处理简单,不会影响主线程;而 MP3 需要解码,可能会影响主线程的消息分发和定时精度。3、某些 Windows 环境下,MCI 加载MP3会让Sleep变得不准确,主循环实际刷新频率变高。
小结
虽然本篇图文列出的点很少,但是这次学习经历切切实实加深了游戏开发的理解。我想这些框架性的东西也可能成为游戏引擎开发的一个开端,而通过C++而不是依赖引擎的开发,更能深入底层逻辑,让日后对代码的优化的方向更清晰。

如有补充纠正欢迎留言。