1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

数行変更でできる60FPSを作る(フレームスキップ機能付き)

Last updated at Posted at 2024-06-28

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

フレームスキップとは?

処理落ちした場合、何もしなければ動きが遅くなります。
そこで、間に合わないときだけあえて描画をスキップするものです。

描画は最も処理が重いので、やらなければかなり処理時間を節約できます。

処理が間に合ったとき、描画はします。

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

曲線的に移動する

遅延Tがあったとした場合、遅れた分を取り戻そうとして計算します。
下図ではOpenSiv3D公式でおすすめしている方法、デルタタイムをかけてその分移動量を増やす方法です。等速直線移動の場合はうまくいきますが、曲線的な動きではフォローできません。
その結果座標が狂ってきます。さらに小数誤差も増えてしまいます。
image.png

フレームスキップ機能の場合、必ずT時間おきに座標計算とします。
T時間間に合わない場合、描画をしません。
間に合わない場合は描画をしません。間に合ったら描画をします。
以下の例は1フレーム描画が間に合わなかった例です。
image.png
つまり重い処理だと、1秒間に描画する枚数が少なくなります。
T時間ごとの経過で間に合わない限り描画をしないからです。
しかし、座標計算にはズレの発生は無くなります。(万能ではないがある程度カバー可能)

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

Copilotに質問してみたところ、すでに XNA3.1 からこの機能は実装されていたようです。
image.png

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

インクルードするファイル: これを保存します。

60FPSwithAutoFrameSkip.h

#pragma once
# include <Siv3D.hpp> //OpenSiv3D v0.6.11
using namespace std::chrono;

/*
このヘッダーファイルの機能

画素数を1280×720にして、家庭用ゲームと同じフレームレートにする(60)

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

下記ファイルにに1行追加して下さい

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


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


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

  例:
	void Main()
	{
		System60::SetDisplaySize(DisplayResolution::HD_1280x720 好きなサイズにして );  <<<<● 追加して
		・・・
		・・・
		・・・
		while(System60::Update())   <<<<● 60を追加して
		{
*/

/// @brief System60::SetDisplaySize(~ で設定して、System::Update()  >> System60::Update() に変更して下さい
class System60
{
private:
	/// @brief インスタンス生成しないでください
	System60() {}
public:
	/// 1:@brief main(){のすぐ下に  System60::SetDisplaySize(~ で設定して下さい
	const int tutorial_0 = 0;
	/// 2:System::Update()  を全て>> System60::Update() に変更して下さい
	const int tutorial_1 = 1;
	/// 3:tutorial_0,tutorial_1 ができればOKです
	const int tutorial_2 = 1;


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

		start = 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サイズとシーンサイズの画素数が違う場合、2つ引数を書く
	/// @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& duration)
	{
		System::Sleep(duration);
	}
	/// <summary>アプリケーションの終了</summary>
	static void Exit()
	{
		System::Exit();
	}

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

		auto end = high_resolution_clock::now();
		auto durtion = end - start;        // 要した時間を計算
		auto msec = duration_cast<std::chrono::nanoseconds>(durtion).count();
		return true;	// ここでは最適化無視しないと怖い
	}
};
/// <summary>アプリケーションの終了・・・ではバージョンアップで無くなったらしい</summary>
void Exit()
{
	System::Exit();
}

bool startSysytem = { System60::Initialize() };

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

60FPS の改造例

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

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

改造例:新規プロジェクト作成のを改造

Main.cpp

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

void Main()
{
    "↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓● 追加2 ●↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓";
	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 ● 変更1 ●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

分かりくいですか?ならばアップ
image.png

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

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

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?