Por que alguns jogos antigos rodam rápido demais no hardware moderno?


64

Eu tenho alguns programas antigos. Eu peguei um computador Windows do início dos anos 90 e tentei executá-los em um computador relativamente moderno. Curiosamente, eles correram a uma velocidade incrivelmente rápida - não, os 60 quadros por segundo são rápidos, mas sim o tipo oh-meu-deus-o-personagem-está-andando-na-velocidade-do-som velozes. Eu pressionava uma tecla de seta e o sprite do personagem percorria a tela muito mais rápido que o normal. A progressão do tempo no jogo estava acontecendo muito mais rápido do que deveria. Existem até programas feitos para desacelerar sua CPU, para que esses jogos sejam realmente jogáveis.

Ouvi dizer que isso está relacionado ao jogo, dependendo dos ciclos da CPU, ou algo assim. Minhas perguntas são:

  • Por que jogos mais antigos fazem isso e como eles se safaram?
  • Como os jogos mais recentes não fazem isso e são executados independentemente da frequência da CPU?

Isso foi há um tempo atrás e não me lembro de fazer nenhum truque de compatibilidade, mas isso não vem ao caso. Existem muitas informações sobre como corrigir isso, mas não muito sobre por que exatamente elas funcionam dessa maneira, e é isso que estou perguntando.
precisa saber é o seguinte

9
Lembre-se do botão turbo em PCs mais antigos? : D
Viktor Mellgren

1
Ah Faz-me lembrar o atraso de 1 segundo no ABC80 (PC sueco baseado em z80 com Basic). FOR F IN 0 TO 1000; NEXT F;
12263 Macke

1
Apenas para esclarecer, "alguns programas antigos que eu executei em um computador Windows do início dos anos 90" são aqueles programas DOS em uma máquina Windows ou programas Windows em que esse comportamento acontece? Estou acostumado a vê-lo no DOS, mas não no Windows, IIRC.
Aquele cara brasileiro

Respostas:


52

Eu acredito que eles assumiram que o relógio do sistema funcionaria a uma taxa específica e amarraram seus cronômetros internos a essa taxa de relógio. A maioria desses jogos provavelmente rodava no DOS e era de modo real (com acesso completo e direto ao hardware) e supunha que você estivesse executando um sistema iirc 4,77 MHz para PCs e qualquer processador padrão desse modelo executado para outros sistemas como o Amiga.

Eles também adotaram atalhos inteligentes com base nessas suposições, incluindo a economia de um pouquinho de recursos ao não escrever loops de tempo internos dentro do programa. Eles também consumiram o máximo de energia possível do processador - o que era uma idéia decente nos dias de chips lentos, geralmente resfriados passivamente!

Inicialmente, uma maneira de contornar a velocidade diferente do processador era o bom e velho botão Turbo (que tornava o sistema mais lento). Os aplicativos modernos estão no modo protegido e o sistema operacional tende a gerenciar recursos - eles não permitiriam que um aplicativo DOS (que esteja executando o NTVDM em um sistema de 32 bits de qualquer maneira) use todo o processador em muitos casos. Em resumo, os sistemas operacionais tornaram-se mais inteligentes, assim como as APIs.

Muito baseado neste guia no Oldskool PC, onde a lógica e a memória falharam - é uma ótima leitura, e provavelmente vai mais fundo no "porquê".

Coisas como CPUkiller usam o máximo de recursos possível para "desacelerar" o sistema, o que é ineficiente. Seria melhor usar o DOSBox para gerenciar a velocidade do relógio que seu aplicativo vê.


14
Alguns desses jogos nem sequer supor qualquer coisa, eles correram tão rápido quanto podiam, que era 'jogável' sobre aqueles do ;-) CPU
Jan Doggen

2
Para informações re. "Como os jogos mais recentes não fazem isso e funcionam independentemente da frequência da CPU?" tente pesquisar gamedev.stackexchange.com por algo parecido game loop. Existem basicamente 2 métodos. 1) Corra o mais rápido possível e aumente a velocidade de movimento, etc, com base na rapidez com que o jogo corre. 2) Se você for rápido demais, aguarde ( sleep()) até estarmos prontos para o próximo 'tick'.
George Duckett

24

Como complemento à resposta do Journeyman Geek (porque minha edição foi rejeitada) para as pessoas interessadas na parte de codificação / perspectiva do desenvolvedor:

Do ponto de vista dos programadores, para os interessados, os tempos do DOS eram momentos em que cada escala de CPU era importante; portanto, os programadores mantinham o código o mais rápido possível.

Um cenário típico em que qualquer programa será executado na velocidade máxima da CPU é simples (pseudo C):

int main()
{
    while(true)
    {

    }
}

isso vai durar para sempre, agora, vamos transformar esse trecho de código em um jogo pseudo-DOS:

int main()
{
    bool GameRunning = true;
    while(GameRunning)
    {
        ProcessUserMouseAndKeyboardInput();
        ProcessGamePhysics();
        DrawGameOnScreen();

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

a menos que as DrawGameOnScreenfunções usem buffer duplo / sincronização V (que era meio cara nos dias em que os jogos do DOS eram criados), o jogo será executado na velocidade máxima da CPU. Nos dias atuais, o i7 móvel seria executado em torno de 1.000.000 a 5.000.000 de vezes por segundo (dependendo da configuração do laptop e do uso atual da CPU).

Isso significaria que, se eu pudesse fazer com que qualquer jogo DOS funcionasse na minha CPU moderna em minhas janelas de 64 bits, poderia obter mais de mil (1000!) FPS, o que é muito rápido para qualquer ser humano jogar se o processamento físico "assumir" que ele é executado entre 50-60 fps.

O que os desenvolvedores do dia atual (podem) fazem é:

  1. Ativar V-Sync no jogo (* não disponível para aplicativos em janela ** [também disponível apenas em aplicativos em tela cheia])
  2. Meça a diferença de tempo entre a última atualização e atualize a física de acordo com a diferença de tempo que efetivamente faz o jogo / programa rodar na mesma velocidade, independentemente da taxa de FPS
  3. Limitar a taxa de quadros programaticamente

*** dependendo da configuração da placa gráfica / driver / sistema operacional, pode ser possível.

Para o ponto 1, não há exemplo que mostrarei porque não há realmente nenhuma "programação". É apenas usando os recursos gráficos.

Como nos pontos 2 e 3, mostrarei os trechos de código e explicações correspondentes:

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;
        }
    }
}

Aqui você pode ver a entrada do usuário e a física levar em consideração a diferença de horário, mas ainda assim você pode obter mais de 1000 FPS na tela porque o loop está sendo executado o mais rápido possível. Como o mecanismo de física sabe quanto tempo passou, ele não precisa depender de "nenhuma suposição" ou "uma certa taxa de quadros" para que o jogo funcione na mesma velocidade em qualquer CPU.

3:

O que os desenvolvedores podem fazer para limitar a taxa de quadros para, por exemplo, 30 FPS não é nada difícil, basta dar uma olhada:

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;
        }
    }
}

O que acontece aqui é que o programa conta quantos milissegundos se passou, se uma certa quantia for atingida (33 ms) e redesenha a tela do jogo, aplicando efetivamente uma taxa de quadros próxima de ~ 30.

Além disso, dependendo do desenvolvedor, ele / ela pode optar por limitar TODO o processamento a 30 qps com o código acima ligeiramente modificado para isso:

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;
            }
        }
    }
}

Existem alguns outros métodos, e alguns deles eu realmente odeio.

Por exemplo, usando sleep(<amount of milliseconds>).

Sei que esse é um método para limitar a taxa de quadros, mas o que acontece quando o processamento do jogo leva 3 milissegundos ou mais? E então você executa o sono ...

isso resultará em uma taxa de quadros menor do que aquela que somente sleep()deveria estar causando.

Vamos, por exemplo, dormir 16 ms. isso faria o programa rodar a 60 hz. agora o processamento dos dados, entrada, desenho e todo o material leva 5 milissegundos. agora estamos em 21 milissegundos para um loop, o que resulta em pouco menos de 50 hz, enquanto você ainda pode facilmente estar a 60 hz, mas por causa do sono é impossível.

Uma solução seria fazer um sono adaptativo na forma de medir o tempo de processamento e deduzir o tempo de processamento do sono desejado, resultando na correção de nosso "bug":

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

Uma causa principal é o uso de um loop de atraso que é calibrado quando o programa é iniciado. Eles contam quantas vezes um loop é executado em um período conhecido e o dividem para gerar atrasos menores. Isso pode ser usado para implementar uma função sleep () para acelerar a execução do jogo. Os problemas surgem quando esse contador é maximizado devido aos processadores serem muito mais rápidos no loop que o pequeno atraso acaba sendo muito pequeno. Além disso, os processadores modernos alteram a velocidade com base na carga, às vezes até por núcleo, o que diminui ainda mais o atraso.

Para jogos para PC realmente antigos, eles rodavam o mais rápido possível, sem levar em conta o ritmo do jogo. Esse foi mais o caso nos dias do IBM PC XT, no entanto, onde existia um botão turbo que atrasava o sistema para corresponder a um processador de 4,77 mhz por esse motivo.

Jogos modernos e bibliotecas como o DirectX têm acesso a cronômetros de alta precessão, portanto, não é necessário usar loops de atraso com base em código calibrado.


4

Todos os primeiros PCs rodavam na mesma velocidade no começo, portanto não havia necessidade de explicar a diferença de velocidades.

Além disso, muitos jogos no começo tinham uma carga de CPU bastante fixa, por isso era improvável que alguns quadros rodassem mais rápido que outros.

Hoje em dia, com seus filhos e seus atiradores sofisticados de FPS, você pode olhar para o chão um segundo e, no Grand Canyon, o próximo, a variação de carga acontece com mais frequência. :)

(E poucos consoles de hardware são rápidos o suficiente para rodar jogos a 60 fps constantemente. Isso se deve principalmente ao fato de os desenvolvedores de console optarem por 30 Hz e tornar os pixels duas vezes mais brilhantes ...)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.