web-dev-qa-db-fra.com

Pourquoi certains vieux jeux fonctionnent-ils beaucoup trop rapidement sur du matériel moderne?

J'ai installé quelques anciens programmes sur un ordinateur Windows du début des années 90 et essayé de les exécuter sur un ordinateur relativement moderne. Chose intéressante, ils ont fonctionné à une vitesse fulgurante - non, pas avec la vitesse rapide à 60 images par seconde, mais plutôt avec le genre vite. J'appuyais sur une touche fléchée et le Sprite du personnage zippait sur l'écran beaucoup plus rapidement que la normale. La progression temporelle dans le jeu se passait beaucoup plus rapidement que prévu. Il existe même des programmes conçus pour ralentir votre processeur afin que ces jeux soient réellement jouables.

J'ai entendu dire que cela est lié au jeu en fonction des cycles du processeur, ou quelque chose comme ça. Mes questions sont:

  • Pourquoi les jeux plus anciens font-ils cela et comment s'en sont-ils sortis?
  • Comment les nouveaux jeux ne le font-ils pas et s’ils fonctionnent indépendamment de la fréquence du processeur?
62
TreyK

Je crois qu'ils ont supposé que l'horloge du système fonctionnerait à un taux spécifique, et lié dans leurs minuteries internes à cette fréquence d'horloge. La plupart de ces jeux fonctionnaient probablement sous DOS et étaient en mode réel (avec un accès matériel complet et direct) et supposaient que vous exécutiez uniirc4.77 MHz pour PC et quel que soit le processeur standard utilisé par ce modèle pour d’autres systèmes comme Amiga.

Ils ont également utilisé des raccourcis astucieux basés sur ces hypothèses, notamment une économie de ressources en évitant l'écriture de boucles de synchronisation internes dans le programme. Ils ont également utilisé autant de puissance de processeur que possible - ce qui était une idée décente à l'époque des puces lentes, souvent refroidies passivement!

Le bon vieux bouton turbo (qui a ralenti votre système) a été initialement un moyen de contourner les vitesses de traitement différentes. Les applications modernes sont en mode protégé et le système d'exploitation tend à gérer les ressources - ce n'est pas le casallowune application DOS (qui s'exécute de toute façon dans NTVDM sur un système 32 bits) d'utiliser tout le processeur dans de nombreux cas. En bref, les systèmes d’exploitation sont devenus plus intelligents, tout comme les API.

Fortement inspiré de ce guide sur Oldskool PC où la mémoire et la mémoire ont échoué - c’est une excellente lecture, qui approfondit probablement le "pourquoi".

Des choses comme CPUkiller utilisent autant de ressources que possible pour "ralentir" votre système, ce qui est inefficace. Vous feriez mieux d'utiliser DOSBox pour gérer la vitesse d'horloge de votre application.

51
Journeyman Geek

En complément de la réponse de Journeyman Geek (parce que mon édition a été rejetée) pour les personnes intéressées par la perspective partie codeur/développeur:

Du point de vue des programmeurs, pour ceux qui sont intéressés, les temps de DOS étaient des moments où chaque tick du processeur était important, les programmeurs ont donc gardé le code aussi rapidement que possible.

Un scénario typique dans lequel n'importe quel programme s'exécutera à la vitesse maximale de la CPU est simple (pseudo C):

int main()
{
    while(true)
    {

    }
}

cela fonctionnera pour toujours, transformons cet extrait de code en un pseudo-jeu-DOS:

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

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

à moins que les fonctions DrawGameOnScreen utilisent la double mise en tampon/synchronisation V (ce qui était assez cher à l'époque des jeux DOS), le jeu tournera à la vitesse maximale du processeur. Sur un mobile i7 moderne, cette vitesse serait d'environ 1 000 000 à 5 000 000 fois par seconde (en fonction de la configuration de l'ordinateur portable et de l'utilisation actuelle du processeur).

Cela signifierait que si je pouvais faire jouer n'importe quel jeu DOS sur mon processeur moderne dans mes fenêtres 64 bits, je pourrais obtenir plus de mille (1 000!) FPS, ce qui est trop rapide pour qu'un humain puisse jouer si le traitement physique "suppose" qu'il s'exécute entre 50-60 fps.

Ce que les développeurs du jour actuel (peuvent) font est:

  1. Activer V-Sync dans le jeu (* non disponible pour les applications fenêtrées ** [disponible uniquement dans les applications en plein écran])
  2. Mesurer la différence de temps entre la dernière mise à jour et mettre à jour la physique en fonction de la différence de temps qui permet effectivement au jeu/programme de fonctionner à la même vitesse quel que soit le taux de FPS
  3. Limiter le framerate par programmation

*** En fonction de la configuration de la carte graphique/du pilote/du système d'exploitation, cela peut être possible .

Pour le point 1, je ne montrerai aucun exemple car ce n’est pas vraiment une "programmation". C'est juste en utilisant les fonctionnalités graphiques.

En ce qui concerne les points 2 et 3, je montrerai les extraits de code et les explications correspondants:

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

Ici, vous pouvez voir que les entrées utilisateur et la physique tiennent compte de la différence de temps, mais vous pouvez toujours obtenir plus de 1000 FPS à l'écran car la boucle est exécutée aussi rapidement que possible. Parce que le moteur physique sait combien de temps a passé, il ne doit pas dépendre "d'aucune hypothèse" ou "d'un certain nombre d'images par seconde", de sorte que le jeu fonctionnera à la même vitesse sur tous les processeurs.

3:

Ce que les développeurs peuvent faire pour limiter le nombre d’images par exemple à 30 images par seconde n’est en réalité rien de plus difficile, jetez-y un coup d’œil:

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

Ce qui se passe ici, c’est que le programme compte le nombre de millisecondes écoulées. Si une certaine quantité est atteinte (33 ms), il redessine l’écran de jeu, en appliquant effectivement une cadence proche de ~ 30.

En outre, en fonction du développeur, il/elle peut choisir de limiter le traitement TOUT à 30 ips avec le code ci-dessus légèrement modifié:

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

Il existe quelques autres méthodes, et certaines que je déteste vraiment.

Par exemple, en utilisant sleep(<amount of milliseconds>).

Je sais que cette méthode permet de limiter le nombre d'images par seconde, mais que se passe-t-il lorsque le traitement de votre jeu prend 3 millisecondes ou plus? Et puis vous exécutez le sommeil ...

cela se traduira par une fréquence d'images inférieure à celle que seul sleep() devrait être à l'origine.

Prenons par exemple un temps de sommeil de 16 ms. cela ferait fonctionner le programme à 60 hz. maintenant, le traitement des données, la saisie, le dessin et tout le reste prend 5 millisecondes. nous sommes à 21 millisecondes pour une boucle, ce qui donne un peu moins de 50 hz, alors que vous pourriez facilement être à 60 hz, mais à cause du sommeil, c’est impossible.

Une solution serait de faire un sommeil adaptatif en mesurant le temps de traitement et en déduisant le temps de traitement du sommeil voulu, ce qui résoudrait notre "bogue". ":

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

L'une des principales causes est l'utilisation d'une boucle à retard calibrée au démarrage du programme. Ils comptent le nombre de fois qu'une boucle est exécutée dans un laps de temps connu et le divisent pour générer des retards plus courts. Ceci peut ensuite être utilisé pour implémenter une fonction sleep () afin de stimuler l'exécution du jeu. Les problèmes surviennent lorsque ce compteur atteint son maximum, car les processeurs sont tellement plus rapides sur la boucle que le petit retard finit par être beaucoup trop petit. De plus, les processeurs modernes changent de vitesse en fonction de la charge, parfois même par cœur, ce qui réduit encore plus le délai.

Pour de très vieux jeux sur PC, ils ont juste couru aussi vite qu'ils le pouvaient, sans se soucier d'essayer de les rythmer. C’était plus le cas dans les _ XT jours PC IBM cependant, où il existait un bouton turbo qui ralentissait le système pour qu’il corresponde à un processeur de 4,77 MHz pour cette raison.

Les jeux modernes et les bibliothèques telles que DirectX ont accès à des temporisateurs à précession élevée, vous n'avez donc pas besoin d'utiliser des boucles de retard basées sur du code calibré.

16
Brian

Tous les premiers PC fonctionnaient à la même vitesse au début, il n'était donc pas nécessaire de prendre en compte la différence de vitesse.

En outre, de nombreux jeux au début avaient une charge de processeur assez fixe, il était donc peu probable que certaines images s'exécutent plus rapidement que d'autres.

De nos jours, avec vos enfants et vos tireurs FPS sophistiqués, vous pouvez regarder le sol une seconde et le grand canyon la suivante, la variation de charge se produit plus souvent. :)

(Et, peu de consoles de matériel sont assez rapides pour faire fonctionner des jeux à 60 ips en permanence. Cela est principalement dû au fait que les développeurs de consoles optent pour 30 Hz et rendent les pixels deux fois plus brillants ...)

4
Macke