《Windows游戏编程大师技巧》学习笔记(一)

《Windows游戏编程大师技巧》学习笔记(一)

一般的游戏循环结构

一个视频游戏基本上是一个连续的循环,它完成逻辑动作并以每秒30帧或更高的刷新率在屏幕上绘制图像,与动画和电影的放映原理相似,简化的游戏循环结构如下图所示:
简化的游戏循环结构示意图
对上图每一部分流程进行说明:

  1. 初始化:游戏程序执行初始化操作,如内存分配、资源检查与加载等;
  2. 进入游戏循环:代码进入到游戏主循环体内部,各种操作开始运行,直到用户退出主循环;
  3. 获取玩家输入:缓存/处理玩家通过鼠标、键盘或手柄等设备的输入信息,以备后续游戏逻辑使用;
  4. 执行游戏逻辑部分:执行诸如人工智能、物理系统等游戏逻辑的更新,更新后的数据用于渲染下一帧图像;
  5. 渲染下一帧图像:根据前述的输入和逻辑处理,游戏的下一帧动画已经具备渲染条件;渲染中的图像常被放置于不可见的缓冲区,因此玩家不会看到这一帧画面被逐步渲染的过程;渲染完成后的图像会从缓冲区中迅速拷贝至显示设备中(双缓冲原理);
  6. 同步显示:随着时间推移,渲染每一帧游戏所需要的数据量和运算量可能会有较大变化,对计算机设备的负荷也有所不同,在不加限制的情况下就会导致游戏画面刷新率(帧率)时高时低;所以必须通过定时功能来维持帧率稳定;
  7. 循环:重复执行从“进入游戏循环”开始到现在的步骤;
  8. 退出:到达这一步表示游戏将关闭并返回操作系统,执行的操作与“初始化”阶段相反:对必要的数据进行持久化,释放内存等相关资源。

游戏循环与FSM

在大多数情况下,游戏循环是一个含有大量状态的FSM(Finite State Machine,有限状态自动机),如下图所示:
游戏循环状态转换图.png
简化的代码如下:

#define GAME_INIT       0   // 初始化状态
#define GAME_MENU       1   // 菜单状态
#define GAME_STARTING   2   // 准备开始运行状态
#define GAME_RUNNING    4   // 正在运行状态
#define GAME_EXIT       5   // 游戏准备退出

// 设置游戏的初始状态为初始化
int game_state = GAME_INIT;

int main()
{
    // 开始执行游戏主循环
    while (game_state != GAME_EXIT)
    {
        switch (game_state)
        {
        case GAME_INIT:
            Init();                 // 初始化游戏资源
            game_state = GAME_MENU; // 跳转至菜单状态
            break;
        case GAME_MENU:
            // 预先定义并声明 int Menu(); 函数,
            // 用来处理反馈玩家在菜单界面的选择,
            // 返回的 game_state 可能是 GAME_STARTING,也可能是 GAME_EXIT 等
            game_state = Menu();
            break;
        case GAME_STARTING:
            // 预先定义并声明 void Setup_For_Run(); 函数
            // 预先初始化游戏运行中所需要的资源
            Setup_For_Run();
            game_state = GAME_RUNNING; // 跳转至运行状态
            break;
        // GAME_RUNNING 条件下执行前述的游戏主循环部分
        case GAME_RUNNING:
            Clear();        // 情况上一帧的绘制内容
            Get_Input();    // 获取玩家输入
            Do_Logic();     // 处理人工智能、物理系统等逻辑
                            // 可能在此阶段改变游戏状态,修改 game_state 的值
            Render_Frame(); // 渲染并显示当前帧内容
            Wait();         // 适当延时,保持帧率稳定
            break;
        case GAME_EXIT:
            Release_And_Cleanup(); // 释放游戏资源
            break;
        default:
            break;
        }
    }

    return 0;
}

除去上述内容,我们还可以声明变量存储游戏中的异常信息,并在main 函数结束时将错误码返回给操作系统。

常规游戏编程技巧

视频游戏是超高性能的计算机程序,所以必须对其运行时间和内存要求严格把控,避免在游戏主循环内部调用太多高层API,否则可能出现性能问题;下面是作者特别指出的几个编程技巧:

  1. 不要怕使用全局变量:虽然这可能会扰乱全局命名空间,但是由于参数需要压栈和出栈,对于某些内部逻辑简单的函数,调用它所需要的时间可能会远大于执行它所需要的时间,而全局变量可以避免这个问题,如下例:
// 使用参数对所需数据进行传递
void DrawPoint(int x, int y, int color)
{
    // 在屏幕上绘制一个像素点
    video_buffer[x + y * MEMORY_PITCH] = color;
}

// 使用全局变量对所需数据进行传递
int gx, gy, gcolor;
void DrawPoint_G()
{
    video_buffer[gx + gy * MEMORY_PITCH] = gcolor;
}
  1. 使用内联函数:与上一条相似,同样也是为避免参数传递带来的性能损耗,inline可以完全摆脱函数调用,但是带来的弊端是可能使编译后的程序体积变大;注意,在使用inline时遵循上一条规则同样合适,尤其是在函数调用时只有一两个参数改变了值的情况——其余旧值可以无须重新加载便可使用;
  2. 尽量使用32位变量而不是8位变量或16位变量:重点在于字节对齐,提高内存寻址效率,如下例:
// 坏的示例:2 * sizeof(short) + sizeof(char) = 5 bytes
struct CPoint
{
    short x, y;
    unsigned char c;
};

// 好的示例:3 * sizeof(int) = 12 bytes
struct CPoiny
{
    int x, y;
    int c;
};

除去上述方法,还可使用编译器指示符,如 #pragma pack(n) 来设定结构体、联合以及类成员变量以n字节方式对齐;当然,这样带来的弊端就是会因为填充而浪费过多内存,但是在大多数情况下,较之速度的提高,这点代价是值得的;
4. 注释代码:为了确保隔夜的代码依然能够被自己读懂;
5. 简化指令:当用类似RISC(精简指令集计算机)来编程时,尽可能简化代码,这些计算机的处理器特别喜欢简单的指令,而不是复杂的指令,这样对编译器同样友好,如下例:

// 坏的示例
if ((x += (2 * buffer[index++])) > 10) { /* do work */ };

// 好的示例
x += (2 * buffer[index]);
index++;
if (x > 10) { /* do work */ };

按照此方式编写代码原因有二:首先,它可以运行调试程序在代码各部分之间放置断点;其次,在处理器支持的情况下运行更多的执行单元并行处理更多代码;
6. 使用二进制移位运算:对于乘数或除数是2的幂的运算,使用左移或右移有利于提高计算效率;
7. 设计高效的算法:使用清楚、高效的算法,而不是暴力穷举;
8. 不要在编程过程中优化代码:在绝大多数情况下这只是浪费时间,建议将代码优化的工作放到模块完成或者整个程序完成后再进行;除此之外,性能优化不能只凭借经验和感觉,必须要以性能测试(Profiling)结果为依据;
9. 注意代码格式:各层次代码要错落有致,对阅读和理清逻辑关系大有用处,不要写的杂乱无章缩进混乱;
10. 不要为简单对象定义太复杂的数据结构:确保你的数据结构正好适合你真正要解决的问题,链表很好用,但是无需在设计固定大小的元素容器时抛弃数组,静态分配内存会更有好处;
11. 对C++谨慎:标准日新月异,高级特性越来越多,但是与上一条相同,我们只需要用合适的设计恰到好处地解决问题,无需设计过多class,并把任何东西都重载,简单直观的代码是最好的,也最容易调试;
12. 勇敢地断舍离:如果已经意识到了自己的编程思路是错误的,那就及时调转车头绕路而行,在工作中发现问题,重新评估总是比硬着头皮走下去更节约时间和精力;
13. 及时备份你的工作:Git是个好东西,尤其是项目逐渐复杂起来的时候,快速定位在稳定版本之后的修改对错误排查也大有用处;
14. 在开发前先规划好项目:文件名、目录名,以及变量的命名规范等,对图形和声音资源分开存放,策划的工作要先于程序进行;
15. 使用合适且专业的软件:虽然用Windows画板完成所有的游戏素材让人敬佩,但是使用PS或许会节省大量时间精力,以及,获得更好的画面效果。