前準備がもうちょっと必要です。もうしばらくつまらない準備にお付き合いください。
龍神録2プログラミングの館では「フレームによる計算」をして弾の計算などを行います。
ほとんどのモニタは60Hz以上なので、1秒間に60回画面を更新することを前提にした造りにします。
速さが1フレームに10ピクセル進む弾があったとしたら、
60Hzのモニタであれば1秒間で600ピクセル進みます。
しかしFPS制御をしなければ120Hzのモニタでは1200ピクセル進んでしまいます。
このようなことが無いように、60FPS以上のレートで画面を更新しないようにします。
クラスとしては重要な役目を果たすのですが、正直中身について特に良く知らなくてもいいので、
この章では本クラスの中身を詳しく紹介する気がありません。
このFpsクラスは私自身10年位前から使いまわしていて、特に変える必要もないのでずっとこのままです。
このサイトを見ている人も自分のプロジェクトにコピペしたらもう二度と見ることの無いクラスですので、
別に中身を理解しなくてもいいというつもりで見て下さい。
Fps.h
#pragma once #include <list> class Fps { public: Fps(); void wait(); void draw() const; private: std::list<int> _list; float _fps; unsigned _counter; void updateAverage(); void regist(); unsigned getWaitTime() const; };
Fps.cpp
#include <DxLib.h> #include "Fps.h" #include "CalcUtils.h" const static int LIST_LEN_MAX = 120; //最大120フレームで待機処理を計算する(2以上にする) const static int FPS = 60; //FPS const static int UPINTVL = 60; //60フレームに一度更新する Fps::Fps():_counter(0),_fps(0){} void Fps::wait() { _counter++; Sleep(getWaitTime()); //待つべき時間を取得して待つ regist(); if (_counter == UPINTVL) { //更新タイミングに1回平均値を更新する updateAverage(); _counter = 0; } } void Fps::draw() const { if (_fps == 0) { return; } DrawFormatString(0, 0, GetColor(255, 255, 255), "%04.1ffps", _fps); } void Fps::regist() { _list.push_back( GetNowCount() ); //現在の時刻を記憶 if( _list.size() > LIST_LEN_MAX ){ //器から漏れたらポップ _list.pop_front(); } } unsigned Fps::getWaitTime() const { int len = (int)_list.size(); if( len == 0 ){ return 0; } int shouldTookTime = (int)(1000/60.f*(len)); //計算上かかるべき時間 int actuallyTookTime = GetNowCount() - _list.front(); //実際にかかった時間 int waitTime = shouldTookTime - actuallyTookTime; //計算上かかるべき時間 - 実際にかかった時間 はすなわち待つべき時間 waitTime = waitTime > 0 ? waitTime : 0; return (unsigned)(waitTime); } void Fps::updateAverage() { int len = (int)_list.size(); if (len < LIST_LEN_MAX) { //まだ平均を計算するレベルにまでたまっていなければ計算しない return; } int tookTime = _list.back() - _list.front();//LIST_LEN_MAXフレームにかかった時間 float average = (float)tookTime / (len - 1);//平均を取る if (average == 0) {//0割り防止 return; } _fps = CalcUtils::roundPoint(1000 / average, 2);//小数点第2位で四捨五入する }
120フレーム分の時刻をリストに保持し、そこから平均を算出します。
ポイントは、待機する時間を1つ前のフレームから計算しないこと。
GetNowCountやSleepの精度はミリ秒なので、16.66666...[ms]待機しなければならない処理には精度が合わないため
長い目で見て待機処理を実施しています。詳しいことは旧龍神録の館に譲ります。
FpsクラスのインスタンスはLooperクラスが持ちます。
Looper.h
#pragma once #include <stack> #include <memory> #include "AbstractScene.h" #include "IOnSceneChangedListener.h" #include "Fps.h" class Looper final : public IOnSceneChangedListener { public: Looper(); ~Looper() = default; bool loop(); void onSceneChanged(const eScene scene, const Parameter& parameter, const bool stackClear) override; private: std::stack<std::shared_ptr<AbstractScene>> _sceneStack; //シーンのスタック Fps _fps; };
Looper.cpp
#include "Looper.h" #include "TitleScene.h" #include "Error.h" #include "GameScene.h" #include "Macro.h" using namespace std; Looper::Looper() { Parameter parameter; _sceneStack.push(make_shared<TitleScene>(this, parameter)); //タイトル画面シーンを作ってpush } /*! @brief スタックのトップのシーンの処理をする */ bool Looper::loop() { _sceneStack.top()->update(); //スタックのトップのシーンを更新 _sceneStack.top()->draw(); //スタックのトップのシーンを描画 _fps.draw(); _fps.wait(); return true; } /*! @brief シーン変更(各シーンからコールバックされる) @param scene 変更するシーンのenum @param parameter 前のシーンから引き継ぐパラメータ @param stackClear 現在のシーンのスタックをクリアするか */ void Looper::onSceneChanged(const eScene scene, const Parameter& parameter, const bool stackClear) { if (stackClear) {//スタッククリアなら while (!_sceneStack.empty()) {//スタックを全部ポップする(スタックを空にする) _sceneStack.pop(); } } switch (scene) { case Title: _sceneStack.push(make_shared<TitleScene>(this, parameter)); break; case Game: _sceneStack.push(make_shared<GameScene>(this, parameter)); break; default: ERR("あるはずのないシーンが呼ばれました"); break; } }
Looper::loop()内の_fps.wait()でうまいこと60FPSになるように待機してくれるようになります。
実行結果