人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

正確なFPS

正確なFPS(Frame Per Second)を計算して画面に表示します。正確なFPSの実現方法は、『Killer Game Programming in Java』を参考にしました。

fps.jar

ゲームループの構造

ゲームループは下のような構造になっています。詳しくは、アクティブレンダリング(2006/5/7)を参照。

    /**
     * ゲームループ
     */
    public void run() {
        while (true) {
            gameUpdate();  // ゲーム状態を更新(ex: ボールの移動)
            gameRender();  // バッファにレンダリング(ダブルバッファリング)
            paintScreen(); // バッファを画面に描画(repaint()を自分でする!)

            // 休止
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

1フレームというのはゲームループ1回分のことです。つまり、1フレームで行う必要のある処理は、

  • ゲーム状態の更新(gameUpdate)
  • バッファにレンダリング(gameRender)
  • バッファ内容を画面に描画(paintScreen)
  • 休止(sleep)

です。ここで休止が必要なのは1つのプログラムがCPUを独占してしまうのを防ぐためです。ゲームループが休止している間に他のプログラムが実行されます(ゲームしながらインターネットできるのはこのためです)。

たとえば、FPSを50とします。これは1秒間に50フレームという意味です。つまり、1秒間に50回ゲームループがまわります。上の4つの処理を1秒間に50回繰り返すってことですね。1秒間に50フレームということは1フレームあたり1/50=0.02秒=20ミリ秒使うことができます。つまり、上の4つの処理の実行に20ミリ秒間だけ使えます。

時間の単位

1s(秒)=1000ms(ミリ秒)=1000000μs(マイクロ秒)=1000000000ns(ナノ秒)

Javaでは時間の測定によくSystem.currentTimeMillis()が使われます。このメソッドは、経過時間をミリ秒単位で返しますが精度に問題があります。

JDK1.5からはSystem.nanoTime()というメソッドが追加されています。このメソッドは、経過時間をナノ秒単位で返す非常に高精度なタイマーです。今回のFPSの計算でもnanoTime()を使っています。そのため、JDK1.5以降のバージョンのJavaをインストールする必要がありますので注意してください。

正確なFPS

正確なFPSを実現するにはゲームループで少し工夫が必要です。ここでは、

  • 正確な休止時間を計算する
  • 休止できないときでも定期的に他のスレッドに制御を移す
  • 休止にもちいるThread.sleep()の誤差を吸収する

という3つの工夫をとっています。

    public void run() {
        long beforeTime, afterTime, timeDiff, sleepTime;
        long overSleepTime = 0L;
        int noDelays = 0;

        beforeTime = System.nanoTime();
        prevCalcTime = beforeTime;

        running = true;
        while (running) {
            gameUpdate();
            gameRender();
            paintScreen();

            afterTime = System.nanoTime();
            timeDiff = afterTime - beforeTime;
            // 前回のフレームの休止時間誤差も引いておく
            sleepTime = (PERIOD - timeDiff) - overSleepTime;

            if (sleepTime > 0) {
                // 休止時間がとれる場合
                try {
                    Thread.sleep(sleepTime / 1000000L); // nano->ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // sleep()の誤差
                overSleepTime =
                    (System.nanoTime() - afterTime) - sleepTime;
            } else {
                // 状態更新・レンダリングで時間を使い切ってしまい
                // 休止時間がとれない場合
                overSleepTime = 0L;
                // 休止なしが16回以上続いたら
                if (++noDelays >= 16) {
                    Thread.yield(); // 他のスレッドを強制実行
                    noDelays = 0;
                }
            }

            beforeTime = System.nanoTime();

            // FPSを計算
            calcFPS();
        }

        System.exit(0);
    }

上のコードを図示するとこんな感じです。

f:id:aidiary:20090828220015g:plain

FPSの計算

FPSの計算はcalcFPS()で行います。このメソッドは、1秒間のフレーム数(frameCount)を経過時間(大体1秒)で割ってFPS を計算しています。時間計測の単位がナノ秒なのでFPSを計算するときに秒に変換しています。actualFPSに格納されたFPS値はパネル上に描画されます。clacFPS()はゲームループ内で呼び出しています。

    /**
     * FPSの計算
     * 
     */
    private void calcFPS() {
        frameCount++;
        calcInterval += PERIOD;

        // 1秒おきにFPSを再計算する
        if (calcInterval >= MAX_STATS_INTERVAL) {
            long timeNow = System.nanoTime();
            // 実際の経過時間を測定
            long realElapsedTime = timeNow - prevCalcTime; // 単位: ns

            // FPSを計算
            // realElapsedTimeの単位はnsなのでsに変換する
            actualFPS = ((double) frameCount / realElapsedTime) * 1000000000L;
            System.out.println(df.format(actualFPS));

            frameCount = 0L;
            calcInterval = 0L;
            prevCalcTime = timeNow;
        }
    }