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


64

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 de 60 images par seconde, mais plutôt avec le genre de personnage oh-mon-dieu-le-personnage-marche-à-la-vitesse du son vite. J'appuyais sur une flèche et le sprite du personnage glissait beaucoup plus rapidement que d'habitude sur l'écran. 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 jeux plus récents ne le font-ils pas et ne fonctionnent-ils pas indépendamment de la fréquence du processeur?

C'était il y a longtemps et je ne me souviens pas d'avoir fait de ruse de compatibilité, mais ce n'est pas la question. Il y a beaucoup d'informations disponibles sur la façon de résoudre ce problème, mais pas autant sur la raison pour laquelle elles fonctionnent exactement de la sorte, c'est ce que je demande.
TreyK

9
Rappelez-vous le bouton turbo sur les anciens PC? : D
Viktor Mellgren

1
Ah Me rappelle le délai de 1 seconde sur l’ABC80 (PC suédois basé sur z80 avec Basic). FOR F IN 0 TO 1000; NEXT F;
Macke

1
Juste pour clarifier, "quelques vieux programmes que j'ai extraits d'un ordinateur Windows du début des années 90" sont-ils des programmes DOS sur une machine Windows ou des programmes Windows dans lesquels ce problème se produit? J'ai l'habitude de le voir sous DOS, mais pas sous Windows, IIRC.
Ce gars brésilien

Réponses:


52

Je crois qu'ils ont supposé que l'horloge du système fonctionnerait à un taux spécifique et que leurs minuteries internes seraient liées à cette fréquence. 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 utilisiez un système iirc 4.77 MHz pour les 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 ralentissait votre système) était initialement un moyen de contourner la vitesse du processeur . Les applications modernes sont en mode protégé et le système d'exploitation a tendance à gérer les ressources - elles ne permettraient pas à une application DOS (qui s'exécute sous NTVDM sur un système 32 bits de toute façon) d'utiliser tous les processeurs 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 logique et la mémoire me manquaient - 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.


14
Certains de ces jeux ne supposaient même rien, ils tournaient aussi vite qu'ils le pouvaient, ce qui était "jouable" sur ces CPU ;-)
Jan Doggen

2
Pour information re. "Comment les jeux plus récents ne font-ils pas cela et ne fonctionnent-ils pas indépendamment de la fréquence du processeur?" essayez de rechercher gamedev.stackexchange.com pour quelque chose comme game loop. Il existe essentiellement 2 méthodes. 1) Courez aussi vite que possible et ajustez les vitesses de déplacement, etc. en fonction de la rapidité du jeu. 2) Si vous êtes trop rapide, attendez ( sleep()) jusqu'à ce que nous soyons prêts pour la prochaine "tick".
George Duckett

24

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 le point de vue du codage / 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 gardaient donc le code le plus rapidement 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 DrawGameOnScreenfonctions n'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 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 avoir 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 ** [alias uniquement disponible dans les applications 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 aussi rapide 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 n'importe quel processeur.

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 qui sleep()devrait être la seule cause.

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 créer un sommeil adaptatif en mesurant le temps de traitement et en déduisant le temps de traitement du sommeil souhaité, 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);
        }
    }
}

16

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 la 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 rythme le jeu. C'était plus le cas à l'époque d'IBM PC XT, mais il existait un bouton turbo qui ralentissait le système afin 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é.


4

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 dans 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 ...)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.