为什么一些旧游戏在现代硬件上运行速度过快?

Tre*_*eyK 65 cpu-speed

我有一些旧程序,我从 90 年代早期的 Windows 计算机上拉下来,并试图在相对现代的计算机上运行它们。有趣的是,他们以极快的速度奔跑——不,不是每秒 60 帧的快,而是天哪,角色正在以声音的速度行走快速地。我会按一个箭头键,角色的精灵会比正常情况更快地在屏幕上滑动。游戏中的时间进程比它应该发生的要快得多。甚至还有一些程序可以降低 CPU 的速度,从而使这些游戏真正可以玩。

我听说这与游戏有关,取决于 CPU 周期,或类似的东西。我的问题是:

  • 为什么老游戏会这样做,他们是如何逃脱惩罚的?
  • 较新的游戏如何这样做并独立于 CPU 频率运行?

Jou*_*eek 54

我相信他们假设系统时钟将以特定速率运行,并将其内部计时器绑定到该时钟速率。这些游戏中的大多数可能在 DOS 上运行,并且是实模式(具有完整的、直接的硬件访问),并假设您运行的是用于 PC的iirc 4.77 MHz 系统以及该模型为其他系统(如 Amiga)运行的任何标准处理器。

他们还基于这些假设采取了巧妙的捷径,包括通过不在程序中编写内部时序循环来节省一点资源。它们还尽可能多地占用了处理器能力——这在速度缓慢、通常被动冷却的芯片时代是一个不错的主意!

最初解决不同处理器速度的一种方法是使用旧的Turbo 按钮(它会减慢您的系统速度)。现代应用程序处于保护模式,操作系统倾向于管理资源——在许多情况下,它们不允许DOS 应用程序(无论如何都在 32 位系统上的 NTVDM 中运行)用尽所有处理器。简而言之,操作系统变得更加智能,API 也是如此。

大量基于Oldskool PC 上的本指南,其中逻辑和内存让我失望——这是一本很好的读物,可能更深入地探讨了“为什么”。

CPUkiller这样的东西会尽可能多地消耗资源来“减慢”系统的速度,这是低效的。您最好使用DOSBox来管理您的应用程序看到的时钟速度。

  • 其中一些游戏甚至没有假设任何东西,它们尽可能快地运行,这在那些 CPU 上是“可玩的”;-) (14认同)
  • 有关信息重新。“较新的游戏如何不这样做并独立于 CPU 频率运行?” 尝试在 gamedev.stackexchange.com 中搜索类似“游戏循环”的内容。基本上有2种方法。1) 跑得尽可能快,并根据游戏运行的速度调整移动速度等。2) 如果您太快,请等待 (`sleep()`),直到我们为下一个“滴答”做好准备。 (2认同)

Giz*_*zmo 26

作为对 Journeyman Geek 的回答(因为我的编辑被拒绝)的补充,供对编码部分/开发人员观点感兴趣的人使用:

从程序员的角度来看,对于那些感兴趣的人来说,DOS 时代是每个 CPU 滴答都很重要的时期,因此程序员尽可能快地保存代码。

任何程序都以最大 CPU 速度运行的典型场景是这个简单的伪 C:

int main()
{
    while(true)
    {

    }
}
Run Code Online (Sandbox Code Playgroud)

这将永远运行。

现在,让我们把这个代码片段变成一个伪 DOS 游戏:

int main()
{
    bool GameRunning = true;

    while(GameRunning)
    {
        ProcessUserMouseAndKeyboardInput();
        ProcessGamePhysics();
        DrawGameOnScreen();

        // close game
        if(Pressed(KEY_ESCAPE))
        {
            GameRunning = false;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

除非该DrawGameOnScreen功能使用双缓冲/垂直同步(这在制作 DOS 游戏的年代有点昂贵),否则游戏将以最大 CPU 速度运行。在现代移动 i7 上,这将以每秒 1,000,000 到 5,000,000 次的速度运行(取决于笔记本电脑的配置和当前的 CPU 使用率)。

这意味着,如果我可以在我的 64 位 Windows 的现代 CPU 上运行任何 DOS 游戏,我可以获得超过一千(1000!)FPS——这对于任何人来说都太快了——如果物理处理“假设”它在 50 到 60 FPS 之间运行。

现代开发人员(可以)做什么

  • 在游戏中启用垂直同步(不适用于窗口应用程序* - 即,仅适用于全屏应用程序)
  • 测量自上次更新以来的时间并相应地调整物理处理,这有效地使游戏/程序无论CPU速度如何都以相同的速度运行
  • 以编程方式限制帧率

*取决于图形卡/驱动器/ OS结构,可以是可能的。

对于第一个选项,我不会展示任何示例,因为它并不是真正的“编程”——它只是使用图形功能。

至于另外两个选项,我会展示相应的代码片段和解释。

测量自上次更新以来的时间

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里你可以看到用户输入和物理考虑了时间差,但你仍然可以在屏幕上获得 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这里发生的是程序计算经过的毫秒数,当达到一定数量(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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

其他选择

还有其他一些方法,其中一些我真的很讨厌。例如,使用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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Bri*_*ian 16

一个主要原因是使用了在程序启动时校准的延迟循环。他们计算循环在已知时间内执行的次数,并将其划分以产生更小的延迟。然后可以使用它来实现 sleep() 函数来调整游戏的执行速度。由于处理器在循环中速度太快以至于小延迟最终变得太小,因此当这个计数器达到最大值时就会出现问题。此外,现代处理器根据负载改变速度,有时甚至是基于每个核心,这使得延迟更大。

对于非常老的 PC 游戏,他们只是尽可能快地运行,而不考虑游戏的节奏。在 IBM PC XT 时代更是如此,但是由于这个原因,存在一个涡轮按钮会降低系统速度以匹配 4.77mhz 处理器。

现代游戏和像 DirectX 这样的库可以访问高速进动计时器,因此不需要使用基于校准代码的延迟循环。


Mac*_*cke 5

所有第一台 PC 一开始都以相同的速度运行,因此无需考虑速度差异。

此外,许多游戏一开始就有相当固定的 CPU 负载,因此某些帧不太可能比其他帧运行得更快。

如今,随着您的孩子和精美的 FPS 射击游戏,您可以前一秒看地面,下一秒就看大峡谷,负载变化更频繁地发生。:)

(而且,很少有硬件控制台能够足够快地以 60 fps 持续运行游戏。这主要是因为控制台开发人员选择 30 Hz 并使像素闪亮两倍......)