为什么有些旧游戏在现代硬件上运行得太快?


62

我有一些旧的程序,我从90年代早期的Windows计算机中取出并试图在相对现代的计算机上运行它们。有趣的是,它们以极快的速度运行 - 不,不是每秒60帧的速度,而是哦,我的上帝那个角色是走在速度的声音类型快速。我会按一个箭头键,角色的精灵会在屏幕上拉得比正常情况快得多。游戏中的时间进展发生得比应有的快得多。甚至有程序制作 减慢CPU速度 这些游戏实际上是可玩的。

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

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

这是一段时间了,我不记得做任何兼容性的诡计,但这是不重要的。有很多关于如何解决这个问题的信息,但不是很多 为什么 他们正是这样运行的,这就是我所要求的。
TreyK

9
还记得旧电脑上的涡轮按钮吗? :d
Viktor Mellgren

1
啊。让我记住ABC80上的1秒延迟(基于瑞典z80的PC与Basic)。 FOR F IN 0 TO 1000; NEXT F;
Macke

1
只是为了澄清一下,“我从90年代早期的Windows计算机中删除了一些旧程序”是Windows机器上的DOS程序,还是发生这种行为的Windows程序?我习惯在DOS上看到它,但不是Windows,IIRC。
That Brazilian Guy

Answers:


51

我相信他们假设系统时钟将以特定速率运行,并将其内部定时器与该时钟速率相关联。大多数这些游戏可能都是在DOS上运行的 实模式 (完整,直接的硬件访问)并假设您正在运行 IIRC 用于PC的4.77 MHz系统以及该型号为Amiga等其他系统运行的标准处理器。

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

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

严重依赖 这个指南在Oldskool PC上 逻辑和记忆失败的地方 - 这是一个很好的阅读,可能更深入到“为什么”。

好像的东西 CPUkiller 使用尽可能多的资源来“减慢”你的系统,这是低效的。你最好还是用 DOSBox中 管理应用程序看到的时钟速度。


14
其中一些游戏甚至没有任何假设,它们的运行速度尽可能快,这对那些CPU来说是“可玩的”;-)
Jan Doggen

2
有关信息。 “较新的游戏如何做不到这一点并独立于CPU频率运行?”尝试搜索gamedev.stackexchange.com之类的东西 game loop。基本上有两种方法。 1)根据游戏运行的速度,尽可能快地运行并缩放移动速度等。 2)如果你太快等了( sleep() )直到我们为下一个'滴答'做好准备。
George Duckett

23

对于对编码部分/开发人员的观点感兴趣的人来说,作为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 函数使用双缓冲/ V-sync(在DOS游戏制作的时候有点贵),游戏将以最高CPU速度运行。 在现代的移动i7上,每秒运行大约1,000,000到5,000,000次(取决于笔记本电脑的配置和当前的CPU使用情况)。

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

今天开发人员(可以)做的是:

  1. 在游戏中启用V-Sync(*不适用于窗口应用程序** [仅限全屏应用程序中提供的a.k.a.])
  2. 测量上次更新之间的时间差,并根据时间差更新物理,有效地使游戏/程序以相同的速度运行,而不管FPS速率如何
  3. 以编程方式限制帧速率

***取决于显卡/驱动程序/操作系统配置 可以 有可能。

对于第1点,我没有示例,因为它不是真正的“编程”。它只是使用图形功能。

至于第2点和第3点,我将显示相应的代码片段和解释:

2:

int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;
    while(GameRunning)
    {
        TimeDifference = GetCurrentTime()-LastTick;
        LastTick = GetCurrentTime();

        //process movement based on how many time passed and which keys are 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上以相同的速度工作。

3:

开发人员可以做的将帧速率限制为30 FPS实际上并不困难,只需看看:

int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;

    double FPS_WE_WANT = 30;
    //how many milliseconds need to pass before we need to draw again so we get the framerate we want?
    double TimeToPassBeforeNextDraw = 1000.0/FPS_WE_WANT;
    //For the geek programmers: note, this is pseudo code so I don't care for variable types and return types..
    double LastDraw = GetCurrentTime();

    while(GameRunning)
    {
        TimeDifference = GetCurrentTime()-LastTick;
        LastTick = GetCurrentTime();

        //process movement based on how many time passed and which keys are pressed
        ProcessUserMouseAndKeyboardInput(TimeDifference);

        //pass the time difference to the physics engine so it can calculate anything time-based
        ProcessGamePhysics(TimeDifference);

        //if certain amount of milliseconds pass...
        if(LastTick-LastDraw >= TimeToPassBeforeNextDraw)
        {
            //draw our game
            DrawGameOnScreen();

            //and save when we last drawn the game
            LastDraw = LastTick;
        }

        //close game if escape is pressed
        if(Pressed(KEY_ESCAPE))
        {
            GameRunning = false;
        }
    }
}

这里发生的是程序计算已经过了多少毫秒,如果达到一定量(33毫秒),那么它重绘游戏屏幕,有效地应用接近~30的帧速率。

此外,根据开发人员的不同,他/她可能会选择将所有处理限制为30 fps,并将上述代码略微修改为:

int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;

    double FPS_WE_WANT = 30;
    //how many miliseconds need to pass before we need to draw again so we get the framerate we want?
    double TimeToPassBeforeNextDraw = 1000.0/FPS_WE_WANT;
    //For the geek programmers: note, this is pseudo code so I don't care for variable types and return types..
    double LastDraw = GetCurrentTime();

    while(GameRunning)
    {

        LastTick = GetCurrentTime();
        TimeDifference = LastTick-LastDraw;

        //if certain amount of miliseconds pass...
        if(TimeDifference >= TimeToPassBeforeNextDraw)
        {
            //process movement based on how many time passed and which keys are 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 drawn the game
            LastDraw = LastTick;

            //close game if escape is pressed
            if(Pressed(KEY_ESCAPE))
            {
                GameRunning = false;
            }
        }
    }
}

还有一些其他的方法,其中一些我真的很讨厌。

例如,使用 sleep(<amount of milliseconds>)

我知道这是限制帧速率的一种方法,但是当游戏处理需要3毫秒或更长时间会发生什么?然后你执行睡眠......

这将导致比仅有的帧速率更低的帧率 sleep() 应该是造成的。

例如,我们假设睡眠时间为16毫秒。这将使程序运行在60赫兹。现在处理数据,输入,绘图和所有东西需要5毫秒。现在我们在一个循环中的时间为21毫秒,这导致略低于50赫兹,而你可能很容易仍然在60赫兹,但由于睡眠,这是不可能的。

一个解决方案是制作一个 适应性睡眠 以测量处理时间和从想要的睡眠中减去处理时间的形式,从而修复我们的“错误”:

int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;
    long long NeededSleep;

    while(GameRunning)
    {
        TimeDifference = GetCurrentTime()-LastTick;
        LastTick = GetCurrentTime();

        //process movement based on how many time passed and which keys are 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);
        }
    }
}

16

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

对于真正老旧的PC游戏,他们只是尽可能快地跑,而不考虑尝试调整游戏。在IBM PC XT时代更是如此,但是由于这个原因,存在一个涡轮按钮,使系统速度变慢,以匹配4.77mhz处理器。

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


4

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

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

如今,对于你的孩子和你喜欢的FPS射击游戏,你可以一秒钟看地板,然后在下一个大峡谷,负载变化更频繁地发生。 :)

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.