Por que alguns jogos antigos são executados muito rapidamente no hardware moderno?

62

Eu tenho alguns programas antigos que eu tirei de um computador Windows dos anos 90 e tentei executá-los em um computador relativamente moderno. Curiosamente, eles correram a uma velocidade rápida - não, não o tipo de rápido de 60 quadros por segundo, e sim o tipo do tipo "oh-meu-deus-o-personagem-está-andando-na-velocidade-do-som" velozes. Eu apertava uma tecla de seta e o sprite do personagem passava pela 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é mesmo programas feitos para diminuir a velocidade da 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 parecido. Minhas perguntas são:

  • Por que jogos mais antigos fazem isso e como eles conseguiram se safar?
  • Como os jogos mais recentes não fazem isso e são executados independentemente da frequência da CPU?
por TreyK 12.08.2013 / 03:20

4 respostas

51

Eu acredito que eles assumiram que o clock do sistema rodaria em uma taxa específica, e empataram em seus timers internos para essa taxa de clock. A maioria desses jogos provavelmente era executada no DOS e era modo real (com acesso completo e direto ao hardware) e supunha que você estivesse executando um sistema iirc de 4.77 MHz para PCs e qualquer processador padrão que o modelo rodasse para outros sistemas como o Amiga.

Eles também usaram atalhos inteligentes baseados nessas suposições, incluindo salvar um pouquinho de recursos, não escrevendo loops internos de temporização dentro do programa. Eles também pegaram o máximo de poder de processamento que conseguiram - o que foi uma ideia decente nos dias de chips lentos, muitas vezes passivamente resfriados!

Inicialmente, uma maneira de contornar a velocidade do processador era o bom e velho botão Turbo (que reduziu a velocidade do seu sistema). Aplicações modernas estão em modo protegido e o sistema operacional tende a gerenciar recursos - eles não permitiriam um aplicativo DOS (que é executado no NTVDM em um sistema de 32 bits) para usar todo o processador em muitos casos. Em suma, os sistemas operacionais ficaram mais inteligentes, assim como as APIs.

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

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

    
por 12.08.2013 / 03:47
23

Como complemento à resposta de Journeyman Geek (porque minha edição foi rejeitada) para as pessoas que estão interessadas na perspectiva de parte / desenvolvedor de código:

Da perspectiva dos programadores, para aqueles que estão interessados, os tempos do DOS eram tempos em que cada tick do CPU era importante para que os programadores mantivessem 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 será executado para sempre, agora, vamos transformar esse trecho de código em um pseudo-jogo DOS:

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

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

a menos que as funções DrawGameOnScreen usem buffer duplo / V-sync (que era um pouco caro nos dias em que os jogos DOS eram feitos), o jogo rodará na velocidade máxima da CPU. Em um dia moderno, o i7 móvel rodaria 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 conseguisse qualquer jogo DOS trabalhando em meu CPU moderno em meu 64bit windows eu poderia obter mais de mil (1000!) FPS que é muito rápido para qualquer ser humano tocar se o processamento físico "assumir "corre entre 50-60 fps.

O que os desenvolvedores atuais podem fazer:

  1. Habilite o V-Sync no jogo (* não disponível para aplicativos em janelas ** [também disponível em aplicativos de tela inteira])
  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 de forma programática

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

Para o ponto 1 não há exemplo que mostrarei porque não é realmente uma "programação". Está apenas usando os recursos gráficos.

Quanto aos pontos 2 e 3, mostrarei os snippets 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 tempo, mas você ainda pode obter mais de 1000 FPS na tela porque o loop está rodando 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 mais 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 passaram, se uma certa quantia for atingida (33 ms), então ele redesenha a tela do jogo, efetivamente aplicando 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 fps 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>) .

Eu sei que este é um método para limitar a taxa de quadros, mas o que acontece quando o processamento do seu 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, tirar um tempo de sono de 16 ms. isso faria o programa rodar a 60 hz. agora o processamento dos dados, entrada, desenho e todo o material leva 5 milissegundos. estamos em 21 milisegundos para um loop agora que resulta em pouco menos de 50 hz, enquanto você poderia facilmente estar em 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 do 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);
        }
    }
}
    
por 12.08.2013 / 23:46
16

Uma causa principal é usar um loop de atraso que é calibrado quando o programa é iniciado. Eles contam quantas vezes um loop é executado em um período de tempo conhecido e o divide para gerar atrasos menores. Isso pode ser usado para implementar uma função sleep () para acompanhar a execução do jogo. Os problemas surgem quando esse contador é excedido devido a 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é em uma base por núcleo, o que torna o atraso ainda maior.

Para jogos de PC realmente antigos, eles correram o mais rápido que podiam, sem levar em conta o ritmo do jogo. Este 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 este motivo.

Os jogos e bibliotecas modernos, como o DirectX, têm acesso a timers de alta precessão, portanto, não é necessário usar loops de atraso baseados em código calibrado.

    
por 12.08.2013 / 04:04
4

Todos os primeiros PCs rodaram na mesma velocidade no começo, então não houve necessidade de explicar a diferença de velocidade.

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

Hoje, com seus filhos e seus atiradores de FPS, você pode olhar para o chão um segundo e, no grand canyon, 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 qps constantemente. Isso se deve principalmente ao fato de que os desenvolvedores de console optam por 30 Hz e deixam os pixels duas vezes mais brilhantes ...)

    
por 12.08.2013 / 10:22

Tags