我从一台 90 年代早期的 Windows 电脑上下载了一些旧程序,并尝试在一台相对现代的电脑上运行它们。有趣的是,它们运行速度快得惊人 - 不,不是每秒 60 帧的那种快,而是那种“天哪,角色以音速行走”的那种快。我按下箭头键,角色的精灵就会比平时快得多地在屏幕上移动。游戏中的时间进展比它应该的要快得多。甚至有程序专门设计来降低 CPU 速度以便这些游戏真正可玩。
我听说这与游戏依赖于 CPU 周期或类似因素有关。我的问题是:
- 为什么老游戏会这样做?他们又是如何做到这一点的?
- 新游戏如何不是这样做并且独立于 CPU 频率运行?
答案1
我相信他们假设系统时钟会以特定的速率运行,并将内部计时器与该时钟速率绑定在一起。这些游戏中的大多数可能都在 DOS 上运行,并且实模式(具有完整、直接的硬件访问)并假设你正在运行我记得个人电脑采用 4.77 MHz 系统,而其他系统(如 Amiga)则采用该型号的标准处理器。
他们还根据这些假设采取了巧妙的捷径,包括通过不在程序中编写内部计时循环来节省一点点资源。他们还尽可能地占用处理器能力——这在速度缓慢、通常采用被动冷却的芯片时代是一个不错的想法!
最初解决处理器速度差异的一种方法是使用传统的涡轮按钮(这会降低你的系统速度)。现代应用程序处于保护模式,操作系统倾向于管理资源——它们不会允许在许多情况下,DOS 应用程序(无论如何,它在 32 位系统上的 NTVDM 中运行)会耗尽所有处理器。简而言之,操作系统已经变得更智能,API 也是如此。
主要基于本指南在 Oldskool PC 上当我的逻辑和记忆力都无法满足我的需求时,这本书非常值得一读,并且可能更深入地探讨“为什么”。
类似的东西CPU杀手消耗尽可能多的资源来“减慢”你的系统速度,这是低效的。你最好使用DOS盒管理应用程序看到的时钟速度。
答案2
作为对 Journeyman Geek 的回答的补充(因为我的编辑被拒绝了),对于那些对编码部分/开发人员观点感兴趣的人来说:
从程序员的角度来看,对于那些感兴趣的人来说,DOS 时代是每个 CPU 滴答都很重要的时候,因此程序员会尽可能快地编写代码。
任何程序都以最大 CPU 速度运行的典型场景是这个简单的伪 C:
int main()
{
while(true)
{
}
}
这将永远持续下去。
现在,让我们将这段代码片段变成一个伪 DOS 游戏:
int main()
{
bool GameRunning = true;
while(GameRunning)
{
ProcessUserMouseAndKeyboardInput();
ProcessGamePhysics();
DrawGameOnScreen();
// close game
if(Pressed(KEY_ESCAPE))
{
GameRunning = false;
}
}
}
除非该DrawGameOnScreen
功能使用双缓冲/垂直同步(在制作 DOS 游戏的年代,这有点昂贵),否则游戏将以最大 CPU 速度运行。在现代移动 i7 上,这将以每秒约 1,000,000 到 5,000,000 次的速度运行(取决于笔记本电脑的配置和当前的 CPU 使用率)。
这意味着,如果我可以在 64 位 Windows 中在我的现代 CPU 上运行任何 DOS 游戏,我就可以获得超过一千(1000!)FPS - 这对于任何人类来说都太快了 - 如果物理处理“假设”它以 50 到 60 FPS 之间运行。
现代开发人员可以做什么
- 在游戏中启用 V-Sync(不适用于窗口应用程序* - 即仅适用于全屏应用程序)
- 测量自上次更新以来的时间并相应地调整物理处理,这实际上使得游戏/程序以相同的速度运行,而不管 CPU 速度如何
- 通过编程限制帧速率
* 取决于显卡/驱动程序/操作系统配置,它可能是可能的。
对于第一个选项,我不会展示任何示例,因为它不是真正的“编程”——它只是使用图形功能。
至于另外两个选项,我将展示相应的代码片段和解释。
测量自上次更新以来的时间
int main()
{
bool GameRunning = true;
long long LastTick = GetCurrentTime();
long long TimeDifference;
while(GameRunning)
{
TimeDifference = GetCurrentTime() - LastTick;
LastTick = GetCurrentTime();
// process movements based on time passed and keys pressed
ProcessUserMouseAndKeyboardInput(TimeDifference);
// pass the time difference to the physics engine, so it can calculate anything time-based
ProcessGamePhysics(TimeDifference);
DrawGameOnScreen();
// close game if escape is pressed
if(Pressed(KEY_ESCAPE))
{
GameRunning = false;
}
}
}
在这里,您可以看到用户输入和物理考虑了时间差,但您仍然可以在屏幕上获得 1000+ FPS,因为循环以尽可能快的速度运行。因为物理引擎知道经过了多少时间,所以它不必依赖于“无假设”或“特定频率”,因此游戏将在任何 CPU 上以相同的帧速率运行。
通过编程限制帧速率
开发人员可以将帧速率限制为例如 30 FPS,这并不困难 - 只需看一下:
int main()
{
bool GameRunning = true;
long long LastTick = GetCurrentTime();
long long TimeDifference;
double DESIRED_FPS = 30;
// how many milliseconds need to pass before the next draw so we get the framerate we want
double TimeToPassBeforeNextDraw = 1000.0/DESIRED_FPS;
// note to geek programmers: this is pseudo code, so I don't care about variable types and return types
double LastDraw = GetCurrentTime();
while(GameRunning)
{
TimeDifference = GetCurrentTime() - LastTick;
LastTick = GetCurrentTime();
// process movements based on time passed and keys pressed
ProcessUserMouseAndKeyboardInput(TimeDifference);
// pass the time difference to the physics engine, so it can calculate anything time-based
ProcessGamePhysics(TimeDifference);
// if certain number of milliseconds pass...
if(LastTick-LastDraw >= TimeToPassBeforeNextDraw)
{
// draw our game
DrawGameOnScreen();
// and save when we last drew the game
LastDraw = LastTick;
}
// close game if escape is pressed
if(Pressed(KEY_ESCAPE))
{
GameRunning = false;
}
}
}
这里发生的情况是,程序计算经过的毫秒数,当达到一定量(33 毫秒)时,它会重新绘制游戏屏幕,有效地应用接近 30 FPS 的帧速率。
此外,开发人员可能会选择限制全部稍微修改一下上面的代码,就可以将处理速度提高到 30 FPS:
int main()
{
bool GameRunning = true;
long long LastTick = GetCurrentTime();
long long TimeDifference;
double DESIRED_FPS = 30;
// how many milliseconds need to pass before the next draw so we get the framerate we want
double TimeToPassBeforeNextDraw = 1000.0/DESIRED_FPS;
// note to geek programmers: this is pseudo code, so I don't care about variable types and return types
double LastDraw = GetCurrentTime();
while(GameRunning)
{
LastTick = GetCurrentTime();
TimeDifference = LastTick - LastDraw;
// if certain number of milliseconds pass...
if(TimeDifference >= TimeToPassBeforeNextDraw)
{
// process movements based on time passed and keys pressed
ProcessUserMouseAndKeyboardInput(TimeDifference);
// pass the time difference to the physics engine, so it can calculate anything time-based
ProcessGamePhysics(TimeDifference);
// draw our game
DrawGameOnScreen();
// and save when we last drew the game
LastDraw = LastTick;
// close game if escape is pressed
if(Pressed(KEY_ESCAPE))
{
GameRunning = false;
}
}
}
}
其他选择
还有其他几种方法,其中一些我真的很讨厌。例如,使用sleep(NumberOfMilliseconds)
。
我知道这是限制帧速率的一种方法,但是当您的游戏处理需要 3 毫秒或更长时间,然后您执行睡眠时会发生什么?这将导致帧速率低于应有的帧速率sleep()
。
例如,假设睡眠时间为 16 毫秒。这将使程序以 60 Hz 运行。现在假设数据、输入、绘图和所有内容的处理需要 5 毫秒。这使我们一个循环需要 21 毫秒,结果略低于 50 Hz,虽然您仍然可以很容易地保持 60 Hz,但由于硬编码睡眠,这是不可能的。
一个解决方案是进行“自适应睡眠”,以测量处理时间并从所需睡眠中扣除处理时间的形式,从而修复我们的“错误”:
int main()
{
bool GameRunning = true;
long long LastTick = GetCurrentTime();
long long TimeDifference;
long long NeededSleep;
while(GameRunning)
{
TimeDifference = GetCurrentTime() - LastTick;
LastTick = GetCurrentTime();
// process movements based on time passed and keys pressed
ProcessUserMouseAndKeyboardInput(TimeDifference);
// pass the time difference to the physics engine, so it can calculate anything time-based
ProcessGamePhysics(TimeDifference);
// draw our game
DrawGameOnScreen();
// close game if escape is pressed
if(Pressed(KEY_ESCAPE))
{
GameRunning = false;
}
NeededSleep = 33 - (GetCurrentTime() - LastTick);
if(NeededSleep > 0)
{
Sleep(NeededSleep);
}
}
}
答案3
一个主要原因是使用在程序启动时校准的延迟循环。它们计算循环在已知时间内执行的次数,并将其除以生成较小的延迟。然后可以使用它来实现 sleep() 函数来调整游戏的执行速度。当由于处理器在循环上的速度太快而导致计数器达到最大值时,问题就出现了,因为小延迟最终变得太小了。此外,现代处理器会根据负载改变速度,有时甚至会根据每个核心改变速度,这会使延迟更加严重。
对于非常老的 PC 游戏,它们只会尽可能快地运行,而不会考虑游戏的节奏。然而,在 IBM PC XT 时代,情况更是如此,因为存在一个加速按钮,可以降低系统速度以匹配 4.77mhz 处理器。
现代游戏和库(例如 DirectX)可以访问高进动计时器,因此不需要使用基于校准代码的延迟循环。
答案4
所有第一台 PC 一开始都以相同的速度运行,因此无需考虑速度差异。
此外,许多游戏一开始的 CPU 负载都是相当固定的,因此某些帧不太可能比其他帧运行得更快。
如今,有了孩子和喜欢的 FPS 射击游戏,你前一秒还在看地面,后一秒就看到大峡谷,负载变化的情况更加频繁地发生。:)
(并且,很少有硬件控制台的速度足够快,可以持续以 60 fps 的速度运行游戏。这主要是因为控制台开发人员选择了 30 Hz 并使像素的亮度提高两倍......)