¿Por qué algunos juegos antiguos se ejecutan demasiado rápido en hardware moderno?

64

Tengo algunos programas antiguos que saqué de una computadora Windows de principios de los 90 e intenté ejecutarlos en una computadora relativamente moderna. Curiosamente, corrieron a una velocidad increíblemente rápida: no, no el tipo de velocidad de 60 fotogramas por segundo, sino el tipo de oh-my-god-the-character-is-walking-at-the-speed-of-sound rápido. Presionaría una tecla de flecha y el sprite del personaje se deslizaría por la pantalla mucho más rápido de lo normal. La progresión del tiempo en el juego sucedía mucho más rápido de lo que debería. Incluso hay programas creados para ralentizar su CPU para que estos juegos sean realmente jugables.

He oído que esto está relacionado con el juego dependiendo de los ciclos de la CPU, o algo así. Mis preguntas son:

  • ¿Por qué los juegos más antiguos hacen esto y cómo se salieron con la suya?
  • ¿Cómo los juegos más nuevos no hacen esto y se ejecutan independientemente de la frecuencia de la CPU?
TreyK
fuente
Esto fue hace un tiempo y no recuerdo haber hecho ningún truco de compatibilidad, pero eso no viene al caso. Hay mucha información sobre cómo solucionar esto, pero no tanto sobre por qué exactamente funcionan de esa manera, que es lo que estoy preguntando.
TreyK
99
¿Recuerdas el botón turbo en las PC más antiguas? : D
Viktor Mellgren
1
Ah Me recuerda el retraso de 1 segundo en el ABC80 (PC sueco basado en z80 con Basic). FOR F IN 0 TO 1000; NEXT F;
Macke
1
Solo para aclarar, "algunos programas antiguos que saqué de una computadora con Windows de principios de los 90" son esos programas de DOS en una máquina con Windows, o programas de Windows donde ocurre este comportamiento? Estoy acostumbrado a verlo en DOS, pero no en Windows, IIRC.
Ese tipo brasileño
Ver también CPU o limitación de framerate en juegos antiguos
BlueRaja - Danny Pflughoeft

Respuestas:

52

Creo que asumieron que el reloj del sistema funcionaría a una velocidad específica, y vincularon sus temporizadores internos a esa velocidad de reloj. La mayoría de estos juegos probablemente se ejecutaban en DOS, y eran en modo real (con acceso completo y directo al hardware) y suponían que estaba ejecutando un sistema iirc de 4.77 MHz para PC y cualquier procesador estándar que ese modelo ejecutara para otros sistemas como el Amiga.

También tomaron atajos inteligentes basados ​​en esos supuestos, incluido el ahorro de un poco de recursos al no escribir bucles de temporización internos dentro del programa. También tomaron tanta potencia de procesador como pudieron, lo cual fue una idea decente en los días de chips lentos, a menudo pasivamente enfriados.

Inicialmente, una forma de sortear diferentes velocidades de procesador era el viejo y bueno botón Turbo (que ralentizaba su sistema). Las aplicaciones modernas están en modo protegido y el sistema operativo tiende a administrar recursos: no permitirían que una aplicación de DOS (que se ejecuta en NTVDM en un sistema de 32 bits de todos modos) use todo el procesador en muchos casos. En resumen, los sistemas operativos se han vuelto más inteligentes, al igual que las API.

Basada en esta guía en Oldskool PC donde la lógica y la memoria me fallaron, es una gran lectura, y probablemente profundiza en el "por qué".

Cosas como CPUkiller utilizan tantos recursos como sea posible para "ralentizar" su sistema, lo cual es ineficiente. Sería mejor usar DOSBox para administrar la velocidad del reloj que ve su aplicación.

Journeyman Geek
fuente
14
Algunos de estos juegos ni siquiera suponían nada, corrían tan rápido como podían, lo que era 'jugable' en esas CPU ;-)
Jan Doggen
2
Para información re. "¿Cómo los juegos más nuevos no hacen esto y se ejecutan independientemente de la frecuencia de la CPU?" intente buscar gamedev.stackexchange.com para algo así game loop. Básicamente hay 2 métodos. 1) Corre lo más rápido posible y escala las velocidades de movimiento, etc., según la rapidez con la que se ejecute el juego. 2) Si eres demasiado rápido, espera ( sleep()) hasta que estemos listos para el próximo 'tic'.
George Duckett
24

Como una adición a la respuesta de Journeyman Geek (porque mi edición fue rechazada) para las personas interesadas en la parte de codificación / perspectiva del desarrollador:

Desde la perspectiva de los programadores, para aquellos que estén interesados, los tiempos de DOS fueron momentos en los que cada tic de CPU era importante, por lo que los programadores mantuvieron el código lo más rápido posible.

Un escenario típico donde cualquier programa se ejecutará a la velocidad máxima de la CPU es este simple (pseudo C):

int main()
{
    while(true)
    {

    }
}

esto se ejecutará para siempre, ahora, vamos a convertir este fragmento de código en un juego pseudo-DOS:

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

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

a menos que las DrawGameOnScreenfunciones utilicen doble búfer / sincronización V (que era un poco costoso en los días en que se creaban los juegos de DOS), el juego se ejecutará a la velocidad máxima de la CPU. En un i7 móvil moderno, esto funcionaría entre 1,000,000 y 5,000,000 veces por segundo (dependiendo de la configuración de la computadora portátil y el uso actual de la CPU).

Esto significaría que si pudiera hacer que un juego de DOS funcione en mi CPU moderna en mis ventanas de 64 bits, podría obtener más de mil (1000) FPS, que es demasiado rápido para que cualquier humano lo juegue si el procesamiento de física "asume" que se ejecuta entre 50-60 fps.

Lo que los desarrolladores actuales (pueden) hacer es:

  1. Habilite V-Sync en el juego (* no disponible para aplicaciones con ventana ** [también conocido como disponible en aplicaciones de pantalla completa])
  2. Mida la diferencia horaria entre la última actualización y actualice la física de acuerdo con la diferencia horaria que efectivamente hace que el juego / programa se ejecute a la misma velocidad, independientemente de la tasa de FPS
  3. Limite la velocidad de fotogramas programáticamente

*** dependiendo de la configuración de la tarjeta gráfica / controlador / sistema operativo puede ser posible.

Para el punto 1 no hay ningún ejemplo que mostraré porque en realidad no es ninguna "programación". Solo está usando las características gráficas.

En cuanto a los puntos 2 y 3, mostraré los fragmentos de código y las explicaciones correspondientes:

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

Aquí puede ver la entrada del usuario y la física teniendo en cuenta la diferencia horaria, pero aún puede obtener más de 1000 FPS en la pantalla porque el ciclo se ejecuta lo más rápido posible. Debido a que el motor de física sabe cuánto tiempo pasó, no tiene que depender de "ninguna suposición" o "cierta velocidad de cuadros", por lo que el juego funcionará a la misma velocidad en cualquier CPU.

3:

Lo que los desarrolladores pueden hacer para limitar la velocidad de cuadros a, por ejemplo, 30 FPS en realidad no es nada más difícil, solo eche un vistazo:

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

Lo que sucede aquí es que el programa cuenta cuántos milisegundos han pasado, si se alcanza una cierta cantidad (33 ms), vuelve a dibujar la pantalla del juego, aplicando efectivamente una velocidad de fotogramas cercana a ~ 30.

Además, dependiendo del desarrollador, él / ella puede elegir limitar TODO el procesamiento a 30 fps con el código anterior ligeramente modificado para esto:

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

Hay algunos otros métodos, y algunos de ellos realmente los odio.

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

Sé que este es un método para limitar la velocidad de fotogramas, pero ¿qué sucede cuando el procesamiento de tu juego tarda 3 milisegundos o más? Y luego ejecutas el sueño ...

esto dará como resultado una velocidad de fotogramas más baja que la que solo sleep()debería estar causando.

Tomemos por ejemplo un tiempo de sueño de 16 ms. esto haría que el programa se ejecute a 60 hz. ahora el procesamiento de los datos, la entrada, el dibujo y todo lo demás lleva 5 milisegundos. Estamos a 21 milisegundos para un ciclo ahora, lo que resulta en un poco menos de 50 hz, mientras que fácilmente podría estar a 60 hz, pero debido al sueño es imposible.

Una solución sería hacer un sueño adaptativo en forma de medir el tiempo de procesamiento y deducir el tiempo de procesamiento del sueño deseado, lo que da como resultado la reparación de nuestro "error":

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);
        }
    }
}
Gizmo
fuente
16

Una causa principal es usar un bucle de retardo que se calibra cuando se inicia el programa. Cuentan cuántas veces se ejecuta un bucle en una cantidad de tiempo conocida y lo dividen para generar retrasos más pequeños. Esto se puede usar para implementar una función sleep () para acelerar la ejecución del juego. Los problemas surgen cuando este contador se maximiza debido a que los procesadores son mucho más rápidos en el ciclo que el pequeño retraso termina siendo demasiado pequeño. Además, los procesadores modernos cambian la velocidad en función de la carga, a veces incluso por núcleo, lo que hace que el retraso se reduzca aún más.

Para los juegos de PC realmente antiguos, simplemente corrían tan rápido como podían sin importar el ritmo del juego. Este fue más el caso en los días de IBM PC XT, sin embargo, donde existía un botón turbo que ralentizaba el sistema para que coincida con un procesador de 4.77 mhz por este motivo.

Los juegos y bibliotecas modernas como DirectX tienen acceso a temporizadores de alta precesión, por lo que no es necesario utilizar bucles de retardo basados ​​en código calibrado.

Brian
fuente
4

Al principio, todas las primeras PC funcionaban a la misma velocidad, por lo que no era necesario tener en cuenta la diferencia de velocidades.

Además, muchos juegos al principio tenían una carga de CPU bastante fija, por lo que era poco probable que algunos cuadros se ejecuten más rápido que otros.

Hoy en día, con sus niños y sus sofisticados tiradores de FPS, puede mirar al piso un segundo y al Gran Cañón al siguiente, la variación de carga ocurre con más frecuencia. :)

(Y, pocas consolas de hardware son lo suficientemente rápidas para ejecutar juegos a 60 fps constantemente. Esto se debe principalmente al hecho de que los desarrolladores de consolas optan por 30 Hz y hacen que los píxeles sean dos veces más brillantes ...)

Macke
fuente