现代开发人员可以做什么

现代开发人员可以做什么

我从一台 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 并使像素的亮度提高两倍......)

相关内容