Dlaczego niektóre stare gry działają zbyt szybko na nowoczesnym sprzęcie?

64

Mam kilka starych programów, ściągnąłem komputer z systemem Windows z wczesnych lat 90. i próbowałem uruchomić je na stosunkowo nowoczesnym komputerze. Co ciekawe, biegali z niesamowitą szybkością - nie, nie z szybkością 60 klatek na sekundę, raczej, och, mój-boże-bohater-idzie-z-prędkością-dźwięku szybki. Naciskałem klawisz strzałki, a duszek postaci przesuwa się po ekranie znacznie szybciej niż zwykle. Postępy w grze przebiegały znacznie szybciej niż powinny. Istnieją nawet programy spowalniające procesor, dzięki czemu gry te są rzeczywiście dostępne.

Słyszałem, że jest to związane z grą w zależności od cykli procesora lub czegoś podobnego. Moje pytania to:

  • Dlaczego starsze gry to robią i jak sobie z tym poradziły?
  • Jak nowsze gry tego nie robią i działają niezależnie od częstotliwości procesora?
TreyK
źródło
To było dawno temu i nie przypominam sobie robienia żadnych sztuczek kompatybilności, ale to nie ma sensu. Istnieje wiele informacji na temat tego, jak to naprawić, ale nie tak bardzo, dlaczego dokładnie działają w ten sposób, o co pytam.
TreyK,
9
Pamiętasz przycisk turbo na starszych komputerach? : D
Viktor Mellgren,
1
Ach Pamiętam 1 sekundowe opóźnienie na ABC80 (szwedzki komputer oparty na z80 z Basic). FOR F IN 0 TO 1000; NEXT F;
Macke,
1
Żeby wyjaśnić, „kilka starych programów, które ściągnąłem z komputera z systemem Windows z wczesnych lat 90.”, to te programy DOS na komputerze z systemem Windows, czy programy Windows, w których takie zachowanie się dzieje? Jestem przyzwyczajony do oglądania go w systemie DOS, ale nie w systemie Windows, IIRC.
Ten Brazylijczyk
Zobacz także ograniczenie procesora lub
liczby

Odpowiedzi:

52

Uważam, że założyli, że zegar systemowy będzie działał z określoną częstotliwością, i powiązali wewnętrzne zegary z tą częstotliwością. Większość z tych gier prawdopodobnie działała w systemie DOS i działała w trybie rzeczywistym (z pełnym, bezpośrednim dostępem do sprzętu) i zakładała, że ​​korzystasz z systemu iirc 4,77 MHz dla komputerów PC i jakiegokolwiek standardowego procesora, który model ten działał dla innych systemów, takich jak Amiga.

Przyjęli również sprytne skróty oparte na tych założeniach, w tym zaoszczędzili trochę zasobów, nie pisząc wewnętrznych pętli czasowych w programie. Zużyli również tyle mocy procesora, ile mogli - co było przyzwoitym pomysłem w czasach wolnych, często pasywnie chłodzonych układów!

Początkowo jednym ze sposobów na obejście różnych prędkości procesora był dobry stary przycisk Turbo (który spowolnił twój system). Nowoczesne aplikacje są w trybie chronionym, a system operacyjny ma tendencję do zarządzania zasobami - nie pozwala aplikacji DOS (która i tak działa w systemie NTVDM w systemie 32-bitowym) w wielu przypadkach zużywać cały procesor. Krótko mówiąc, systemy operacyjne stały się inteligentniejsze, podobnie jak interfejsy API.

Opierając się mocno na tym przewodniku na PC Oldskool, gdzie zawiodła mnie logika i pamięć - to świetna lektura i zapewne bardziej zagłębia się w „dlaczego”.

Rzeczy takie jak CPUkiller zużywają jak najwięcej zasobów, aby „spowolnić” system, co jest nieefektywne. Lepiej byłoby użyć DOSBoxa do zarządzania zegarem, który widzi twoja aplikacja.

Journeyman Geek
źródło
14
Niektóre z tych gier nawet niczego nie zakładały, działały tak szybko, jak tylko mogły, co można było „zagrać” na tych procesorach ;-)
Jan Doggen,
2
W celu uzyskania informacji „Jak nowsze gry tego nie robią i działają niezależnie od częstotliwości procesora?” spróbuj przeszukać gamedev.stackexchange.com w poszukiwaniu czegoś podobnego game loop. Istnieją w zasadzie 2 metody. 1) Biegnij tak szybko, jak to możliwe i skaluj prędkości ruchu itp. W oparciu o szybkość gry. 2) Jeśli jesteś zbyt szybki, poczekaj ( sleep()), aż będziemy gotowi na kolejne „tik”.
George Duckett,
24

Jako dodatek do odpowiedzi Journeyman Geek (ponieważ moja edycja została odrzucona) dla osób, które są zainteresowane częścią kodowania / perspektywą programisty:

Z punktu widzenia programistów, dla tych, którzy są zainteresowani, czasy DOS były czasami, w których każdy takt procesora był ważny, więc programiści zachowali kod tak szybko, jak to możliwe.

Typowy scenariusz, w którym dowolny program będzie działał z maksymalną szybkością procesora, jest taki prosty (pseudo C):

int main()
{
    while(true)
    {

    }
}

to będzie działać wiecznie, teraz zmieńmy ten fragment kodu w pseudo-grę DOS:

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

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

chyba że DrawGameOnScreenfunkcje używają podwójnego buforowania / synchronizacji V (co było dość drogie w czasach, gdy powstawały gry DOS), gra będzie działać z maksymalną szybkością procesora. Na współczesnym telefonie komórkowym i7 działałoby to z prędkością od 1 000 000 do 5 000 000 razy na sekundę (w zależności od konfiguracji laptopa i bieżącego użycia procesora).

Oznaczałoby to, że gdybym mógł uruchomić dowolną grę DOS działającą na moim nowoczesnym procesorze w 64-bitowych oknach, mógłbym uzyskać więcej niż tysiąc (1000!) Klatek na sekundę, co jest zbyt szybkie dla każdego człowieka, jeśli proces fizyki „zakłada”, że działa pomiędzy 50-60 fps.

Programiści (mogą) na dzień dzisiejszy to:

  1. Włącz V-Sync w grze (* niedostępne dla aplikacji okienkowych ** [inaczej dostępne tylko w aplikacjach pełnoekranowych])
  2. Zmierz różnicę czasu między ostatnią aktualizacją i zaktualizuj fizykę zgodnie z różnicą czasu, która skutecznie sprawia, że ​​gra / program działa z tą samą prędkością, niezależnie od prędkości FPS
  3. Programowo ograniczaj liczbę klatek na sekundę

*** w zależności od konfiguracji karty graficznej / sterownika / systemu operacyjnego może być to możliwe.

Dla punktu 1 nie pokażę żadnego przykładu, ponieważ tak naprawdę nie jest to „programowanie”. Po prostu korzysta z funkcji graficznych.

Jeśli chodzi o pkt 2 i 3, pokażę odpowiednie fragmenty kodu i objaśnienia:

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

Tutaj możesz zobaczyć, jak dane wejściowe użytkownika i fizyka uwzględniają różnicę czasu, ale wciąż możesz uzyskać ponad 1000 FPS na ekranie, ponieważ pętla działa tak szybko, jak to możliwe. Ponieważ silnik fizyki wie, ile czasu minęło, nie musi on zależeć od „żadnych założeń” lub „pewnej liczby klatek na sekundę”, więc gra będzie działać z tą samą prędkością na dowolnym procesorze.

3:

Co programiści mogą zrobić, aby ograniczyć liczbę klatek na sekundę, na przykład do 30 klatek na sekundę, jest w rzeczywistości niczym trudniejszym, wystarczy spojrzeć:

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

To, co się tutaj dzieje, polega na tym, że program liczy, ile milisekund minęło, jeśli określona ilość zostanie osiągnięta (33 ms), wówczas przerysowuje ekran gry, skutecznie stosując częstotliwość klatek bliską ~ 30.

Ponadto, w zależności od programisty, może on ograniczyć przetwarzanie WSZYSTKIE do 30 fps z powyższym kodem nieco zmodyfikowanym do tego:

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

Jest kilka innych metod, a niektórych z nich naprawdę nienawidzę.

Na przykład za pomocą sleep(<amount of milliseconds>).

Wiem, że jest to jedna z metod ograniczania liczby klatek na sekundę, ale co się dzieje, gdy przetwarzanie gry trwa 3 milisekundy lub dłużej? A potem wykonujesz sen ...

spowoduje to niższą liczbę klatek na sekundę niż ta, która sleep()powinna tylko powodować.

Weźmy na przykład czas snu 16 ms. spowoduje to, że program będzie działał przy 60 Hz. teraz przetwarzanie danych, danych wejściowych, rysowania i wszystkich innych zajmuje 5 milisekund. mamy teraz 21 milisekund na jedną pętlę, co powoduje nieco mniej niż 50 Hz, podczas gdy możesz łatwo być przy 60 Hz, ale z powodu snu jest to niemożliwe.

Jednym z rozwiązań byłoby wykonanie adaptacyjnego snu w formie pomiaru czasu przetwarzania i odliczenia czasu przetwarzania od pożądanego snu, co skutkuje naprawieniem naszego „błędu”:

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
źródło
16

Jedną z głównych przyczyn jest użycie pętli opóźniającej, która jest kalibrowana podczas uruchamiania programu. Liczą, ile razy pętla wykonuje się w znanym czasie i dzielą ją, aby wygenerować mniejsze opóźnienia. Można to następnie wykorzystać do zaimplementowania funkcji sleep () w celu przyspieszenia wykonania gry. Problemy pojawiają się, gdy ten licznik jest maksymalny, ponieważ procesory są tak szybsze w pętli, że małe opóźnienie jest zbyt małe. Ponadto nowoczesne procesory zmieniają prędkość w zależności od obciążenia, czasem nawet w przeliczeniu na rdzeń, co sprawia, że ​​opóźnienie jest jeszcze większe.

W przypadku naprawdę starych gier komputerowych działały tak szybko, jak to możliwe, bez względu na próbę nadania tempa grze. Tak było raczej w dniach IBM PC XT, jednak istniał przycisk turbo, który spowolnił system do dopasowania procesora 4,77 MHz z tego powodu.

Nowoczesne gry i biblioteki, takie jak DirectX, mają dostęp do liczników czasu wysokiej precesji, więc nie trzeba używać skalibrowanych pętli opóźnień opartych na kodzie.

Brian
źródło
4

Wszystkie pierwsze komputery działały z tą samą prędkością na początku, więc nie trzeba było uwzględniać różnicy prędkości.

Ponadto wiele gier na początku miało dość stałe obciążenie procesora, więc nie było prawdopodobne, aby niektóre ramki działały szybciej niż inne.

W dzisiejszych czasach, gdy masz dzieci i swoich fantazyjnych strzelanek FPS, możesz spojrzeć na podłogę w jednej sekundzie, a w wielkim kanionie w następnej kolejności zmiany obciążenia zdarzają się częściej. :)

(I niewiele konsol sprzętowych jest wystarczająco szybkich, aby stale uruchamiać gry przy 60 fps. Wynika to głównie z faktu, że twórcy konsol wybierają 30 Hz i sprawiają, że piksele są dwukrotnie błyszczące ...)

Macke
źródło