web-dev-qa-db-ja.com

最近のハードウェアで古いゲームの実行が速すぎるのはなぜですか?

90年代前半のWindowsコンピューターを引っ張って、比較的最近のコンピューターで実行しようとした古いプログラムがいくつかあります。興味深いことに、彼らは非常に速い速度で走っていました-いいえ、毎秒60フレームではなく、ああ、私のキャラクターのキャラクターは、音のスピードで歩いています。速い。矢印キーを押すと、キャラクターのスプライトが通常よりもはるかに速く画面をジップします。ゲームの時間進行は、予想よりもはるかに速く発生していました。これらのゲームを実際にプレイできるように、 CPUをスローダウン するプログラムも作成されています。

これはCPUサイクルに依存するゲームなどに関係していると聞いています。私の質問は:

  • なぜ古いゲームはこれを行うのですか、そしてどのようにして彼らはそれを回避しましたか?
  • 新しいゲームはどのようにしてこれを実行せず、CPU周波数とは独立して実行しますか?
64
TreyK

私は、システムクロックが特定のレートで動作すると想定し、内部タイマーをそのクロックレートに関連付けていると考えています。これらのゲームのほとんどはおそらくDOS上で実行され、 リアルモード (完全な直接ハードウェアアクセス)であり、iirc 4.77 MHzシステムをPCおよびその他の標準で実行していると想定しています。このモデルのプロセッサは、Amigaなどの他のシステムで実行されました。

また、プログラム内に内部タイミングループを記述しないことにより、リソースのごく一部を節約するなど、これらの前提に基づいて巧妙なショートカットを採用しました。また、可能な限り多くのプロセッサ能力を消費しました。これは、低速で、しばしば受動的に冷却されるチップの時代には、まともなアイデアでした!

当初、異なるプロセッサ速度を回避する1つの方法は、古き良き ターボボタン (システムの速度を低下させる)でした。最新のアプリケーションはプロテクトモードであり、OSはリソースを管理する傾向があります。つまり、すべてのプロセッサを使い切ることはできませんallow DOSアプリケーション(32ビットシステムのNTVDMで実行されています)。多くの場合。つまり、APIと同様に、OSも賢くなっています。

重く基づいている Oldskool PCのこのガイド ロジックとメモリが私を失敗させた場所-これは素晴らしい読み物であり、おそらく「理由」をより深く掘り下げています。

CPUkiller のようなものは、システムを「スローダウン」するために、可能な限り多くのリソースを使用しますが、これは非効率的です。 DOSBox を使用して、アプリケーションが認識するクロック速度を管理することをお勧めします。

52
Journeyman Geek

コーディングパート/開発者の観点に関心のある人のためのJourneyman Geekの回答(私の編集が拒否されたため)への追加として:

プログラマーの観点から見ると、興味がある人にとって、DOSの時代はすべてのCPUティックが重要であるときだったので、プログラマーはコードを可能な限り高速に保ちました。

プログラムが最大CPU速度で実行される典型的なシナリオは、次の単純な疑似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-syncを使用しない限り(これは、DOSゲームが作成された当時は高額でした)、ゲームは最大CPU速度で実行されます。現代のモバイルi7では、これは1秒あたり約1,000,000〜5,000,000回で実行されます(ラップトップの構成と現在のCPU使用率によって異なります)。

これは、64ビットWindowsで最新のCPUでDOSゲームを動作させることができれば、1000(1000!)を超えるFPSが得られる可能性があることを意味します。 50〜60 FPSで実行されることを「想定」します。

現代の開発者ができること

  • ゲームでV-Syncを有効にする(ウィンドウアプリケーション*では使用できません。つまり、フルスクリーンアプリでのみ使用できます)
  • 最後の更新からの時間を測定し、それに応じて物理処理を調整します。これにより、CPU速度に関係なく、ゲーム/プログラムを同じ速度で効率的に実行できます。
  • プログラムでフレームレートを制限する

*グラフィックスカード/ドライバー/ OS構成によっては、mayが可能です。

最初のオプションについては、実際には「プログラミング」ではないため、例を示しません。グラフィック機能を使用するだけです。

他の2つのオプションについては、対応するコードスニペットと説明を表示します。

最終更新からの時間を測定する

_int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;

    while(GameRunning)
    {
        TimeDifference = GetCurrentTime() - LastTick;
        LastTick = GetCurrentTime();

        // process movements based on time passed and keys 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以上のFPSを得ることができます。物理エンジンは経過時間を認識しているため、「仮定なし」または「特定の周波数」に依存する必要がないため、ゲームはどのCPUでも同じフレームレートで動作します。

プログラムでフレームレートを制限する

たとえば、フレームレートを30 FPSに制限するために開発者ができることは、これ以上難しくありません。

_int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;

    double DESIRED_FPS = 30;

    // how many milliseconds need to pass before the next draw so we get the framerate we want
    double TimeToPassBeforeNextDraw = 1000.0/DESIRED_FPS;

    // note to geek programmers: this is pseudo code, so I don't care about variable types and return types
    double LastDraw = GetCurrentTime();

    while(GameRunning)
    {
        TimeDifference = GetCurrentTime() - LastTick;
        LastTick = GetCurrentTime();

        // process movements based on time passed and keys pressed
        ProcessUserMouseAndKeyboardInput(TimeDifference);

        // pass the time difference to the physics engine, so it can calculate anything time-based
        ProcessGamePhysics(TimeDifference);

        // if certain number of milliseconds pass...
        if(LastTick-LastDraw >= TimeToPassBeforeNextDraw)
        {
            // draw our game
            DrawGameOnScreen();

            // and save when we last drew the game
            LastDraw = LastTick;
        }

        // close game if escape is pressed
        if(Pressed(KEY_ESCAPE))
        {
            GameRunning = false;
        }
    }
}
_

ここで何が起こるかというと、プログラムは渡されたミリ秒をカウントし、一定の量(33 ms)に達すると、ゲーム画面を再描画し、実質的に30 FPSに近いフレームレートを適用します。

また、開発者は、上記のコードをわずかに変更することでall処理を30 FPSに制限することを選択できます。

_int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;

    double DESIRED_FPS = 30;

    // how many milliseconds need to pass before the next draw so we get the framerate we want
    double TimeToPassBeforeNextDraw = 1000.0/DESIRED_FPS;

    // note to geek programmers: this is pseudo code, so I don't care about variable types and return types
    double LastDraw = GetCurrentTime();

    while(GameRunning)
    {
        LastTick = GetCurrentTime();
        TimeDifference = LastTick - LastDraw;

        // if certain number of milliseconds pass...
        if(TimeDifference >= TimeToPassBeforeNextDraw)
        {
            // process movements based on time passed and keys 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 drew the game
            LastDraw = LastTick;

            // close game if escape is pressed
            if(Pressed(KEY_ESCAPE))
            {
                GameRunning = false;
            }
        }
    }
}
_

その他の選択肢

他にもいくつかの方法があり、そのうちのいくつかは本当に嫌いです。たとえば、sleep(NumberOfMilliseconds)を使用します。

これがフレームレートを制限する方法の1つであることはわかっていますが、ゲームの処理に3ミリ秒以上かかり、その後スリープを実行するとどうなりますか?これにより、sleep()のみが原因となっているフレームレートよりも低いフレームレートが得られます。

たとえば、スリープ時間を16ミリ秒としましょう。これにより、プログラムは60 Hzで実行されます。データ、入力、描画などすべての処理に5ミリ秒かかるとしましょう。これにより、1ループで21ミリ秒になり、50 Hzをわずかに下回りますが、60 Hzのままにすることは簡単ですが、スリープがハードコードされているため、不可能です。

1つの解決策は、処理時間を測定し、目的のスリープから処理時間を差し引く形で「適応型スリープ」を作成し、「バグ」を修正することです。

_int main()
{
    bool GameRunning = true;
    long long LastTick = GetCurrentTime();
    long long TimeDifference;
    long long NeededSleep;

    while(GameRunning)
    {
        TimeDifference = GetCurrentTime() - LastTick;
        LastTick = GetCurrentTime();

        // process movements based on time passed and keys 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);
        }
    }
}
_
24
Gizmo

主な原因の1つは、プログラムの起動時に調整される遅延ループの使用です。既知の時間内にループが実行される回数をカウントし、分割して小さな遅延を生成します。その後、これを使用してsleep()関数を実装し、ゲームの実行のペースを調整できます。このカウンタが最大になると、ループ上のプロセッサが非常に高速になり、小さな遅延が非常に小さくなり、問題が発生します。さらに、最新のプロセッサーは負荷に基づいて速度を変更し、コアごとに変更することもあるので、遅延がさらに解消されます。

本当に古いPCゲームの場合は、ゲームのペースを調整しようとすることを考慮せずに、できるだけ速く実行しました。これは、IBM PC XT日ではより当てはまりますが、ターボボタンが存在していて、この理由でシステムが4.77MHzプロセッサに一致するのを遅らせました。

DirectXのような最新のゲームやライブラリは高精度のタイマーにアクセスできるため、調整済みコードベースの遅延ループを使用する必要はありません。

16
Brian

最初のすべてのPCは最初は同じ速度で動作していたため、速度の違いを考慮する必要はありませんでした。

また、最初の多くのゲームにはかなり固定されたCPU負荷があったため、一部のフレームが他のフレームよりも速く実行されることはほとんどありませんでした。

今日では、子供たちと派手なFPSシューティングゲームを使用して、床を1秒間見ることができ、グランドキャニオンでは次の段階で、負荷変動がより頻繁に発生します。 :)

(そして、いくつかのハードウェアコンソールはゲームを60 fpsで絶えず実行するのに十分な速度です。これは主に、コンソール開発者が30 Hzを選択し、ピクセルを2倍の光沢にすることによるものです...)

4
Macke