Почему некоторые старые игры работают слишком быстро на современном оборудовании?

64

У меня есть несколько старых программ, которые я снял с Windows-компьютера начала 90-х и попытался запустить их на относительно современном компьютере. Интересно, что они бегали с невероятной скоростью - нет, не с частотой 60 кадров в секунду, а с типом "о, боже мой!" быстрый. Я нажимал клавишу со стрелкой, и спрайт персонажа проносился по экрану намного быстрее, чем обычно. Временная прогрессия в игре происходила намного быстрее, чем следовало. Есть даже программы, разработанные для замедления вашего процессора, так что в эти игры можно играть.

Я слышал, что это связано с игрой в зависимости от циклов процессора или что-то в этом роде. Мои вопросы:

  • Почему старые игры делают это, и как им это сходит с рук?
  • Как новые игры не делают этого и работают независимо от частоты процессора?
TreyK
источник
Это было некоторое время назад, и я не помню, чтобы я делал какие-то хитрости в совместимости, но это не относится к делу. Существует много информации о том, как это исправить, но не столько о том, почему именно они работают именно так, о чем я и спрашиваю.
TreyK
9
Помните кнопку турбо на старых компьютерах? : D
Виктор Меллгрен
1
Ах. Вспоминает 1-секундную задержку на ABC80 (шведский компьютер на базе z80 с Basic). FOR F IN 0 TO 1000; NEXT F;
Мак
1
Просто чтобы уточнить: «несколько старых программ, которые я выполнил на компьютере Windows начала 90-х», - это те программы DOS на компьютере с Windows или программы Windows, где такое поведение происходит? Я привык видеть это в DOS, но не в Windows, IIRC.
Этот бразильский парень
См. Также ограничение ЦП или частоты кадров в старых играх
BlueRaja - Дэнни Пфлугхофт,

Ответы:

52

Я полагаю, что они предполагали, что системные часы будут работать с определенной частотой, и привязали свои внутренние таймеры к этой частоте. Большинство из этих игр, вероятно, работали в DOS и работали в реальном режиме (с полным, прямым доступом к оборудованию) и предполагали, что вы используете систему iirc 4,77 МГц для ПК и любой другой стандартный процессор этой модели для других систем, таких как Amiga.

Они также использовали умные комбинации клавиш, основанные на этих предположениях, в том числе экономили крошечные ресурсы, не записывая внутренние циклы синхронизации внутри программы. Они также потребляли столько процессорной мощности, сколько могли - что было достойной идеей в дни медленных, часто пассивно охлажденных чипов!

Первоначально одним из способов обойти разную скорость процессора была старая добрая кнопка Turbo (которая замедляла работу вашей системы). Современные приложения находятся в защищенном режиме, а ОС стремится управлять ресурсами - они не позволяют приложению DOS (которое в любом случае работает в NTVDM на 32-разрядной системе) использовать весь процессор во многих случаях. Короче говоря, операционные системы стали умнее, как и API.

Основано на этом руководстве на ПК Oldskool, где логика и память подвели меня - это отличное чтение и, вероятно, более подробно объясняет «почему».

Такие вещи, как CPUkiller, используют как можно больше ресурсов, чтобы «замедлить» работу вашей системы, что неэффективно. Вам лучше использовать DOSBox для управления тактовой частотой, которую видит ваше приложение.

Подмастерье Компьютерщик
источник
14
Некоторые из этих игр даже не предполагали ничего, они работали так быстро, как могли, что было «играбельно» на этих процессорах ;-)
Jan Doggen
2
Для информации. «Как новые игры не делают этого и работают независимо от частоты процессора?» попробуйте поискать на gamedev.stackexchange.com что-то подобное game loop. Есть в основном 2 метода. 1) Бегите как можно быстрее, масштабируйте скорость движения и т.д. в зависимости от скорости игры. 2) Если вы слишком быстры, подождите ( sleep()), пока мы не будем готовы к следующему «тику».
Джордж Дакетт
24

Как дополнение к ответу Journeyman Geek (потому что мое редактирование было отклонено) для людей, которые интересуются частью кода / разработчиком:

С точки зрения программистов, для тех, кто интересуется, времена DOS были временем, когда каждый такт процессора был важен, поэтому программисты сохраняли код как можно быстрее.

Типичный сценарий, в котором любая программа будет работать с максимальной скоростью ЦП, выглядит следующим образом (псевдо-C):

int main()
{
    while(true)
    {

    }
}

это будет работать вечно, теперь давайте превратим этот фрагмент кода в псевдо-DOS-игру:

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

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

если DrawGameOnScreenфункции не используют двойную буферизацию / V-синхронизацию (что было довольно дорого в те времена, когда создавались игры для DOS), игра будет работать с максимальной скоростью процессора. На современном мобильном i7 это будет работать примерно от 1 000 000 до 5 000 000 раз в секунду (в зависимости от конфигурации ноутбука и текущего использования процессора).

Это означало бы, что если бы я мог заставить любую DOS-игру работать на моем современном процессоре в моих 64-битных окнах, я мог бы получить более тысячи (1000!) FPS, что слишком быстро для любого человека, чтобы играть, если физическая обработка «предполагает», что он работает между 50-60 кадров в секунду.

Разработчики текущего дня (могут):

  1. Включить V-Sync в игре (* недоступно для оконных приложений ** [также доступно только в полноэкранных приложениях])
  2. Измерьте разницу во времени между последним обновлением и обновите физику в соответствии с разницей во времени, которая эффективно заставляет игру / программу работать с одинаковой скоростью независимо от скорости FPS.
  3. Ограничить частоту кадров программно

*** это может быть возможно в зависимости от конфигурации видеокарты / драйвера / ОС .

Для пункта 1 нет примера, который я покажу, потому что на самом деле это не «программирование». Это просто использование графических функций.

Что касается пунктов 2 и 3, я покажу соответствующие фрагменты кода и пояснения:

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

Здесь вы можете видеть, как пользовательский ввод и физика учитывают разницу во времени, но вы все равно можете получить более 1000 кадров в секунду на экране, потому что цикл работает максимально быстро. Поскольку физический движок знает, сколько времени прошло, он не должен зависеть от «никаких предположений» или «определенной частоты кадров», поэтому игра будет работать с одинаковой скоростью на любом процессоре.

3:

Что разработчики могут сделать, чтобы ограничить частоту кадров, например, до 30 кадров в секунду, на самом деле ничего сложнее, просто взгляните:

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

Здесь происходит то, что программа считает, сколько миллисекунд прошло, если достигнут определенный объем (33 мс), то она перерисовывает игровой экран, эффективно применяя частоту кадров около ~ 30.

Кроме того, в зависимости от разработчика он / она может ограничить ВСЕ обработки до 30 кадров в секунду с немного измененным кодом выше:

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

Есть несколько других методов, и некоторые из них я действительно ненавижу.

Например, используя sleep(<amount of milliseconds>).

Я знаю, что это один из способов ограничения частоты кадров, но что происходит, когда обработка вашей игры занимает 3 миллисекунды или больше? И тогда вы выполняете сон ...

это приведет к более низкой частоте кадров, чем тот, который только sleep()должен вызывать.

Давайте, например, возьмем время сна 16 мс. это заставит программу работать на частоте 60 Гц. Теперь обработка данных, ввод, рисование и все остальное занимает 5 миллисекунд. Сейчас мы находимся на 21 миллисекунде для одной петли, что приводит к чуть менее 50 Гц, в то время как вы можете легко быть на 60 Гц, но из-за сна это невозможно.

Одним из решений было бы создание адаптивного сна в форме измерения времени обработки и вычитания времени обработки из требуемого сна, что привело к исправлению нашей «ошибки»:

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
источник
16

Одной из основных причин является использование контура задержки, который калибруется при запуске программы. Они подсчитывают, сколько раз цикл выполняется за известное время, и делят его для генерации меньших задержек. Затем его можно использовать для реализации функции sleep () для ускорения выполнения игры. Проблемы возникают, когда этот счетчик становится максимальным из-за того, что процессоры в цикле работают намного быстрее, поэтому небольшая задержка оказывается слишком маленькой. Кроме того, современные процессоры меняют скорость в зависимости от нагрузки, иногда даже в расчете на ядро, что делает задержку еще больше.

Для действительно старых компьютерных игр они просто бежали так быстро, как только могли, не взирая на ход игры. Это было больше в те дни, когда у IBM PC XT была турборежим, который по этой причине замедлял систему до 4,77 МГц.

Современные игры и библиотеки, такие как DirectX, имеют доступ к таймерам высокой прецессии, поэтому нет необходимости использовать калиброванные циклы задержки на основе кода.

Брайан
источник
4

Вначале все первые ПК работали с одинаковой скоростью, поэтому не было необходимости учитывать разницу в скорости.

Кроме того, многие игры в начале имели довольно фиксированную загрузку процессора, поэтому маловероятно, что некоторые кадры будут работать быстрее, чем другие.

В настоящее время, с вашими детьми и вашими модными FPS-стрелками, вы можете смотреть на пол одну секунду, а в Гранд-Каньон на следующем, изменение нагрузки происходит чаще. :)

(И лишь несколько аппаратных консолей достаточно быстры, чтобы постоянно запускать игры со скоростью 60 кадров в секунду. Это в основном связано с тем, что разработчики консолей выбирают 30 Гц и делают пиксели в два раза ярче ...)

Маке
источник