【DxLib】60Hz以上のディスプレイでも使えるFPS固定方法

# はじめに

垂直同期を切っている場合，120Hzなどの高いリフレッシュレートのディスプレイでは明らかに1秒間に60回以上メインループが回ってしまいます．
そのため，`Sleep`関数などを用いてディレイをかける必要があります．「ゲームプログラミングの館」や「龍神録プログラミングの館」ではこれの実装方法が紹介されています．

https://dixq.net/g/03_14.html

https://dixq.net/rp2/07.html

しかし，実際にこのプログラムを用いた場合に画面が頻繁にカクつくことがあります．このようになってしまう原因は，上述のプログラムには時間が短かった時にディレイする処理しかなく，時間をかけすぎた時に描画をスキップする処理（いわゆるコマ落ち）が存在しないためです．（以下の掲示板でも同じ内容のことが指摘されています）

https://dixq.net/forum/viewtopic.php?t=20224

この記事では，普段私が個人的に使用している，描画のスキップの処理を追加したプログラムを紹介したいと思います．

# ソースコード

~~~cpp:fps_controller.h
#pragma once

#include <list>

class FpsController final {
  public:
    FpsController() = delete;
    explicit FpsController(int target_fps);

    //! @brief 処理が早すぎる場合，FPSを一定にするために待つ．
    void Wait();

    //! @brief 60Hz以上のモニター使用時に処理が詰まって画面がちらつかないように，
    //! 描画処理をスキップするかどうかを判定する．
    //! @return 処理が詰まって描画を飛ばしたいときに
    //! true を返す．その後フラグを false にする．
    bool SkipDrawScene();

  private:
    //! @brief 現在の時刻を記録する関数．
    //! @param[in] now_time 現在の時刻(ミリ秒)
    void RegisterTime(int now_time);

    //! @brief どれだけ待てばよいか返す関数．
    //! @param[out] wait_time 待つべき時間 (ミリ秒)．
    //! @return コマ落ちしている場合は false．
    bool CheckNeedSkipDrawScreen(int* wait_time) const;

    //! @brief 目標のFPSが正しいかどうかを判定する関数．
    //! @return 負の値，または60より大きい値であれば false．
    bool TargetFpsIsValid() const;

    const int kTargetFpsValue;

    //! 1フレーム当たりにかかる時間(ミリ秒)
    const int kOneFrameTime;

    //! リストに2秒分のフレームごとにかかった時間を記録するため，
    //! リストの最大サイズを決める．
    const int kListMax;

    //! 1フレームごとにかかった時間を記録するリスト．
    std::list<int> time_list_;

    //! コマ落ちを実装するためのフラグ．
    //! trueであれば 1フレーム描画を飛ばし，その後フラグを折る
    bool need_skip_draw_screen_;
};
~~~

~~~cpp:fps_controller.cpp
#include "fps_controller.h"

#include <cmath>
#include <string>

#include <Dxlib.h>

FpsController::FpsController(const int target_fps) 
    : kTargetFpsValue(target_fps)
    , kOneFrameTime(static_cast<int>(1000.0 / target_fps))
    , kListMax(target_fps * 2)
    , need_skip_draw_screen_(false) {}

void FpsController::Wait() {
    if (!TargetFpsIsValid()) { return; }

    // 待つべき時間を取得して待つ．
    int wait_time = 0;

    if (CheckNeedSkipDrawScreen(&wait_time)) {
        WaitTimer(wait_time);   // 取得した時間分待つ．
        // Sleep(wait_time);    // windows API版．

        RegisterTime(GetNowCount());  // 現在の時刻を記録する．
    } else {
        // 時間オーバーしているので，コマ落ちの処理をする．
        // このフレームは理想的な処理をしたものとして，記録する．
        RegisterTime(time_list_.back() + kOneFrameTime);

        need_skip_draw_screen_ = true;  // 描画を飛ばすフラグを立てる．
    }
}


bool FpsController::SkipDrawScene() {
    if (!TargetFpsIsValid()) { return false; }

    // スキップフラグが立っているならば，そのフラグを折り，シーンをスキップする．
    if (need_skip_draw_screen_) {
        need_skip_draw_screen_ = false;
        return true;
    }

    return false;
}

void FpsController::RegisterTime(const int now_time) {
    time_list_.push_back(now_time);  // 現在の時刻を記憶．

    if (time_list_.size() > kListMax) {
        // 器から漏れたら削除する．
        time_list_.pop_front();
    }
}

bool FpsController::CheckNeedSkipDrawScreen(int* time) const {
    // 時刻を初期化．
    (*time) = 0;

    // 時刻リストが空なら，Wait時間は0秒．
    if (time_list_.empty()) {
        (*time) = 0;
        return true;
    }

    // 実際にかかった時間を求める．
    int actually_took_time = GetNowCount() - time_list_.back();

    // 計算上かかるべき時間 - 実際にかかった時間　はすなわち待つべき時間．
    int wait_time = kOneFrameTime - actually_took_time;

    if (wait_time >= 0) {
        // 待ち時間が正の値であるとき，
        // (つまり，かかるべき時間より実際にかかった時間が小さい時)はそのまま値を返す．
        (*time) = wait_time;
        return true;
    } else {
        // 待ち時間が負の値であるとき．
        if (static_cast<int>(abs(wait_time)) < kOneFrameTime) {
            // １フレーム以上遅れていないならば，処理を行う．
            return false;
        }
    }

    // どれにも引っかからなかった場合０を返す．
    (*time) = 0;
    return true;
}

bool FpsController::TargetFpsIsValid() const {
    // マイナスの値は許容しない．
    if (kTargetFpsValue <= 0) { return false; }

    // １秒間に１フレーム以上は許容しない．
    if (kTargetFpsValue > 60) { return false; }

    return true;
}

~~~

# 使用方法

以下のようなイメージで使用してください．

~~~cpp:ゲームのメインループの例
#include <DxLib.h>

#include "fps_controller.h"

FpsController fps_controller;

bool Loop() {
    // 描画にかかわらないような処理．
    // Update();

    // コマ落ちする場合は描画処理をスキップする．
    if (!fps_controller.SkipDrawScene())
    {
        // 裏画面に描画した絵を消す．
        if (ClearDrawScreen() < 0) { return false; }

        // 描画処理．
        // Draw();

        // スクリーンに裏画面に描画した内容を移す．
        if (ScreenFlip() < 0) { return false; }
    }

    // FPS を一定に保つために待つ．
    fps_controller.Wait();

    return true;
}

int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int) {
    // DX ライブラリ初期化処理．
    if (DxLib_Init() < 0) { return 0; }

    while (ProcessMessage() >= 0) {
        if (!Loop()) { break; }
    }

    DxLib_End();
    return 0;
}
~~~