数行変更でできる６０FPSを作る（フレームスキップ機能付き）


以前、必要性に感じて、OpenSiv3Dので作りましたが、あれ以降クラス化して誰か作るのかな？とか考えていたんですが、出てこなかったので、公開することにしました。
フレームごとに経過時間を単純に掛け算している方法だと問題が起きてしまいます。
短いコードですが意外に地味で面倒くさかったです。

## フレームスキップとは？
処理落ちした場合、何もしなければ動きが遅くなります。
そこで、間に合わないときだけあえて描画をスキップするものです。
##### 描画は最も処理が重いので、やらなければかなり処理時間を節約できます。
処理が間に合ったとき、描画はします。

分かりにくいので次に例を示します。

### 曲線的に移動する

遅延Tがあったとした場合、遅れた分を取り戻そうとして計算します。
下図ではOpenSiv3D公式でおすすめしている方法、デルタタイムをかけてその分移動量を増やす方法です。等速直線移動の場合はうまくいきますが、曲線的な動きではフォローできません。
その結果座標が狂ってきます。さらに小数誤差も増えてしまいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76087/9659a503-6c63-acc8-7f69-68f574668aa1.png)

フレームスキップ機能の場合、必ずT時間おきに座標計算とします。
T時間間に合わない場合、描画をしません。
間に合わない場合は描画をしません。間に合ったら描画をします。
以下の例は１フレーム描画が間に合わなかった例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76087/7aadadcc-80fe-a9fc-8b71-a5b94c209e25.png)
つまり重い処理だと、１秒間に描画する枚数が少なくなります。
T時間ごとの経過で間に合わない限り描画をしないからです。
しかし、座標計算にはズレの発生は無くなります。（万能ではないがある程度カバー可能）



ちなみにすでにオワコン化したXNAライブラリでは標準装備でフレームスキップ機能は実装していました。
公式サイトでも全く説明されていなかったので、知ったときには驚きました。
``※何も意識せずともデフォルトで60FPS設定されている``
と同時にプロジェクト起動直後、フレームレートが粗くなる理由も理解しました。

Copilotに質問してみたところ、すでに XNA 2.0 からこの機能は実装されていたようです。(２００７年１２月)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76087/dca5a45e-69f8-4005-9d55-c1b6e7e37382.png)

XNA 3.1 は 3.0 のマイナーチェンジバージョンなので恐らく 3.0 もフレームスキップ機能は付いていると思われる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76087/9fc9123f-c48f-ec05-5a8d-c6e8d4c85050.png)


この点、Unityではどう対処しているかというと、デフォルトだと物理演算上（RigitBody）では、必ず 1/50 秒ごとに 座標計算しているようです。


ソースコードは結構前に作ったので内容を失念していますが、とりあえず、多く実行しているところ今のところ動作しています。

### プロジェクトに導入するファイル

インクルードするファイル：　今のプロジェクトの <B>Main.cpp</B> と同じ場所にこれを保存します。
```C++:60FPSwithAutoFrameSkip.h
#pragma once

#pragma once
# include <Siv3D.hpp> //OpenSiv3D v0.6.16 動作チェック済み
namespace chrono = std::chrono;

#pragma region このファイルの使い方(設定方法)
/*
このヘッダーファイルの機能

画素数を１２８０×７２０にして、家庭用ゲームと同じフレームレートにする（６０）

使い方：
このファイルをプロジェクトに入れ、

下記ファイルにに１行追加して下さい ＜● は3箇所あるので注意＞

Main.cpp の3箇所を変更する方法

----- stdafx.h -----
# pragma once
//# define NO_S3D_USING
# include <Siv3D.hpp>
#include"60FPSwithAutoFrameSkip.h" // ● <<<<<<<< １行追加

そうしたら、
System::Update();  を System60::Update();
に変更して下さい


Main関数に２点を追加と変更して下さい

  例：
	void Main()
	{
											　　//好きな画面サイズにして
		System60::SetDisplaySize(DisplayResolution::HD_1280x720);  <<<<● 追加して
		・・・
		・・・
		・・・
		while(System60::Update())   <<<<● 60を追加して
		{
*/
/// 0:
/// @brief このファイル 60FPSwithAutoFrameSkip.h をMain.cpp と同じ場所にコピーしてください
/// @brief そうしたら、Main.cpp か stdafx.h どちらかの
/// @brief #include <Siv3D.hpp> の下の行に
/// @brief #include"60FPSwithAutoFrameSkip.h" を追加してください 
constexpr int s60_tutorial_0 = 0;
/// 1:
/// @brief main(){のすぐ下に
/// @brief System60::SetDisplaySize(DisplayResolution::～) で設定して下さい
constexpr int s60_tutorial_1 = 1;
/// 2:
/// @brief System::Update()  を全て>>
/// @brief System60::Update() に変更して下さい
constexpr int s60_tutorial_2 = 2;
/// 3:
/// @brief tutorial_0～2 ができればOKです
constexpr int s60_tutorial_3 = 3;

#pragma endregion

/*
※ # include <Siv3D.hpp> の下の行に
#include"60FPSwithAutoFrameSkip.h" が入っており、
さらにそのファイルがMain.cppと同じ場所にある時、

～.cpp ファイル内で s60_ と打ち込むと説明メニューが出ます。

*/

/// @brief System60::SetDisplaySize(～ で設定して、System::Update()  >> System60::Update() に変更して下さい
class System60
{
private:
	/// @brief インスタンス生成できません
	System60() = default;
public:

	inline static chrono::high_resolution_clock::time_point start; // 計測スタート時刻を保存 //XXXX

	static const int FPS = 60; // 1秒間に1画面を書き換える回数
	/// @brief フルスクリーンの画素数
	inline static Size displayResolution = DisplayResolution::HD_1280x720;
	inline static bool IsFullScreen = false;

	/// @brief 画面サイズを設定する
	/// @param displayResolution DisplayResolution::～～ で設定
	/// @param IsFullScreen trueでフルスクリーン、 リリース時以外は基本はウィンドウで開発したほうがいいです
	inline static void SetDisplaySize(Size _displayResolution, bool _IsFullScreen = false)
	{
		System60::displayResolution = _displayResolution;
		System60::IsFullScreen = _IsFullScreen;
		DisplaySet();
	}

#pragma optimize("", off)
	/// @brief ６０FPS　で System::Update() を行う 
	/// @return フレームスキップする場合、描画しない
	static bool Update()
	{
		_ASSERT_EXPR(IsDisplaySet, L"\n\n  while(System60::Update())  の前に \n\n System60::SetDisplaySize(～ )\n\nで設定して下さい ");

		if (displayResolution.x == 0)
		{
			displayResolution = DisplayResolution::HD_1280x720;
			DisplaySet();	// 本当はMain()に入る直前に行いたいのだが仕方なく
		}

		auto elapset = [&]()
			{
				auto end = chrono::high_resolution_clock::now();
				auto durtion = end - start;        // 要した時間を計算
				auto nanosecound = chrono::duration_cast<std::chrono::nanoseconds>(durtion).count();
				return nanosecound;
			};
		/// @brief フレームスキップ確認、１フレーム時間を超えたら描画せずに次のループへ
		/// @return true
		if (elapset() > 1000 * 1000 * 1000 / (long long)FPS)
		{
			start = chrono::high_resolution_clock::now();      // 計測スタート時刻を保存
			return true;
		}
		long long sleepTime = elapset() / (long long)(1000000.0-100.0) / (long long)2.0; // -100.0 は微調整
		System::Sleep((int32)sleepTime);
		while (elapset() < 1000 * 1000 * 1000 / FPS); //16
		// Sleep精度が不安なのでしかたなく待機時間の半分を空ループにした。動作は安定している

		start = chrono::high_resolution_clock::now();      // 計測スタート時刻を保存

		return System::Update();
	}

private:
	inline static bool IsDisplaySet = false;
	/// @brief 画面サイズ変更
	/// @param displayResolution 画素数指定 
	/// @return 画面変更成功
	static bool DisplaySet()
	{
		IsDisplaySet = true;
		Scene::SetResizeMode(ResizeMode::Keep);
		bool ok = Window::Resize(displayResolution);
		Scene::Resize(displayResolution);
		if (IsFullScreen)
		{
			Window::SetFullscreen(true);
		}
		return ok;
	}
	/// @brief 画面の画素数を設定する
	/// 基本的には、引数は一つだけにするが、
	/// Windowサイズとシーンサイズの画素数が違う場合、２つ引数を書く
	/// @param windowsSize ウィンドウの実画素数 ex  DisplayResolution::HD_1280x720
	/// @param sceneSize ウィンドウ内の仮想の画素数  ここは書かなくても可能
	/// @return ウィンドウサイズの変更に成功したら true
	static bool DisplaySet(Size windowsSize, Size sceneSize = { 0,0 })
	{
		displayResolution = windowsSize;
		bool ok = Window::Resize(windowsSize);
		if (sceneSize.x != 0)
		{
			Scene::Resize(windowsSize);
		}
		return ok;
	}
public:
	/// @brief  指定したミリ秒待つ
	/// @param millisecond ミリ秒
	static void Sleep(int32 millisecond)
	{
		System::Sleep(millisecond);
	}
	/// @brief 指定した秒数待つ
	/// @param duration 秒数（少数可能）
	static void Sleep(Duration const& duration)
	{
		System::Sleep(duration);
	}
	/// <summary>アプリケーションの終了</summary>
	static void Exit()
	{
		System::Exit();
	}

	//if (System::MessageBoxOKCancel(mes)
	//
	static MessageBoxResult MessageBoxOKCancel(const String& mes)
	{
		return System::MessageBoxOKCancel(mes);
	}
	/// @brief 画面表示前の初期化  諸事情でpublicですが、使わないで下さい。動作がおかしくなります。
	/// @return 
	static bool Initialize()
	{
		start = chrono::high_resolution_clock::now();      // 計測スタート時刻を保存
		//displayResolution = { 0,0 };

		//後で使うかもしれないので保存  // NOLINT(cpp:S1135)
		//auto end = chrono::high_resolution_clock::now();
		//auto durtion = end - start;        // 要した時間を計算
		//auto nsec = duration_cast<std::chrono::nanoseconds>(durtion).count();  未使用

		return true;	// ここでは最適化無視しないと怖い
	}
};
const bool bootOnlySysytem_0 = { System60::Initialize() };


```
このプログラムを作るに当たり厄介だったのが、実は精度が細かいと思われた std::chrono が単位が細かいだけで、<B>あまり信用できないらしい</B>という説明が書いてあるサイトを見つけたことです。
そのサイトでは、時間を計測していろいろ試して、結果的にタイマーのハードを直接確認する方法がベストという結論を出していました。仕事ぽい事をやって事実検証してますね。
で、PCのハードのタイマーを直接調べる方法を後から確認しようとしましたが、肝心のそのサイトを失念してしまいました。
どうすればいいか、仕方なく、最速の空ループと半分のstd::chronoのループとを合わせた方法を取りました。
苦肉の策です。
とりあえず、この 60FPSの処理は、いろんなPCで多く実行して確認しています。
このファイルをインクルードしてちょっと変更すればフレームスキップ機能付き６０FPS の出来上がりです。


# ６０FPS の改造例
インクルードファイルをプロジェクトに追加
( もしくはOpenSiv3Dのライブラリに突っ込めばその方がその後は楽かも )

２行追加、#include のと Main()のすぐ下
１行変更、System::  を System60:: に変更
をすればできます。

改造例：新規プロジェクト作成のを改造
```C++:Main.cpp

# include <Siv3D.hpp> // Siv3D v0.6.14
    "↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓● 追加１ ●↓　↓　↓　↓　↓　↓　↓　↓";
#include "60FPSwithAutoFrameSkip.h"//●追加１（または stdafx.hに追加）

void Main()
{
    "↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓● 追加２ ●↓　↓　↓　↓　↓　↓　↓　↓";
	System60::SetDisplaySize(DisplayResolution::SVGA_800x600);//●追加

	// 背景の色を設定する | Set the background color
	Scene::SetBackground(ColorF{ 0.6, 0.8, 0.7 });

	// 画像ファイルからテクスチャを作成する | Create a texture from an image file
	const Texture texture{ U"example/windmill.png" };

	// 絵文字からテクスチャを作成する | Create a texture from an emoji
	const Texture emoji{ U"🦖"_emoji };

	// 太文字のフォントを作成する | Create a bold font with MSDF method
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };

	// テキストに含まれる絵文字のためのフォントを作成し、font に追加する | Create a font for emojis in text and add it to font as a fallback
	const Font emojiFont{ 48, Typeface::ColorEmoji };
	font.addFallback(emojiFont);

	// ボタンを押した回数 | Number of button presses
	int32 count = 0;

	// チェックボックスの状態 | Checkbox state
	bool checked = false;

	// プレイヤーの移動スピード | Player's movement speed
	double speed = 200.0;

	// プレイヤーの X 座標 | Player's X position
	double playerPosX = 400;

	// プレイヤーが右を向いているか | Whether player is facing right
	bool isPlayerFacingRight = true;
 
    "            60 ● 変更１ ●System60にする    以上";
	while (System60::Update())//●変更
	{
		// テクスチャを描く | Draw the texture
		texture.draw(20, 20);

		// テキストを描く | Draw text
		font(U"Hello, Siv3D!🎮").draw(64, Vec2{ 20, 340 }, ColorF{ 0.2, 0.4, 0.8 });

		// 指定した範囲内にテキストを描く | Draw text within a specified area
		font(U"Siv3D (シブスリーディー) は、ゲームやアプリを楽しく簡単な C++ コードで開発できるフレームワークです。")
			.draw(18, Rect{ 20, 430, 480, 200 }, Palette::Black);

		// 長方形を描く | Draw a rectangle
		Rect{ 540, 20, 80, 80 }.draw();

		// 角丸長方形を描く | Draw a rounded rectangle
		RoundRect{ 680, 20, 80, 200, 20 }.draw(ColorF{ 0.0, 0.4, 0.6 });

		// 円を描く | Draw a circle
		Circle{ 580, 180, 40 }.draw(Palette::Seagreen);

		// 矢印を描く | Draw an arrow
		Line{ 540, 330, 760, 260 }.drawArrow(8, SizeF{ 20, 20 }, ColorF{ 0.4 });

		// 半透明の円を描く | Draw a semi-transparent circle
		Circle{ Cursor::Pos(), 40 }.draw(ColorF{ 1.0, 0.0, 0.0, 0.5 });

		// ボタン | Button
		if (SimpleGUI::Button(U"count: {}"_fmt(count), Vec2{ 520, 370 }, 120, (checked == false)))
		{
			// カウントを増やす | Increase the count
			++count;
		}

		// チェックボックス | Checkbox
		SimpleGUI::CheckBox(checked, U"Lock \U000F033E", Vec2{ 660, 370 }, 120);

		// スライダー | Slider
		SimpleGUI::Slider(U"speed: {:.1f}"_fmt(speed), speed, 100, 400, Vec2{ 520, 420 }, 140, 120);

		// 左キーが押されていたら | If left key is pressed
		if (KeyLeft.pressed())
		{
			// プレイヤーが左に移動する | Player moves left
			playerPosX = Max((playerPosX - speed * Scene::DeltaTime()), 60.0);
			isPlayerFacingRight = false;
		}

		// 右キーが押されていたら | If right key is pressed
		if (KeyRight.pressed())
		{
			// プレイヤーが右に移動する | Player moves right
			playerPosX = Min((playerPosX + speed * Scene::DeltaTime()), 740.0);
			isPlayerFacingRight = true;
		}

		// プレイヤーを描く | Draw the player
		emoji.scaled(0.75).mirrored(isPlayerFacingRight).drawAt(playerPosX, 540);
	}
}

//
// - Debug ビルド: プログラムの最適化を減らす代わりに、エラーやクラッシュ時に詳細な情報を得られます。
//
// - Release ビルド: 最大限の最適化でビルドします。
//
// - [デバッグ] メニュー → [デバッグの開始] でプログラムを実行すると、[出力] ウィンドウに詳細なログが表示され、エラーの原因を探せます。
//
// - Visual Studio を更新した直後は、プログラムのリビルド（[ビルド]メニュー → [ソリューションのリビルド]）が必要な場合があります。
//



```

実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76087/bd842c52-0f74-180c-1b96-0da5319aa79b.png)

分かりくいですか？ならばアップ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76087/aeb1c45d-5e4a-3c72-b00f-4121dbdc4d9a.png)

成功しています。
このPC、普段は２４０FPSくらい出ています。

これを実装すればどのPCでも６０FPSになります。