はじめに
この記事は長野高専 Advent Calender 2022 のカレンダー2の15日目の記事です。みんなも書いていいんだよ
突然ですが、こんなコードを書いていませんか?
# include <stdio.h>
void main(int argc, char** argv)
{
int x = 0, y = 0;
while(1)
{
x++;
y++;
printf("x:%d, y:%d\n", x, y);
}
}
単純に1ループ毎にx
とy
をインクリメントしているだけの簡単なコードです。しかし、このコードにはある問題点があります。
それは、
FPSによってインクリメントされる速度が変わる
という点です。
FPSが60の場合、1秒後のxの値は60ですが、FPSが30の場合はxの値は30です。現在はただの変数の値が変わるだけですが、これがプレイヤーのx座標、y座標になった時を考えると深刻な問題になります。プレイヤーの移動速度がFPSによって変わってしまう状況は好ましくありません。また、基本的にFPSは完全に固定されることはありませんし、実行する環境の性能によって変わるので、対応が必要になります。
そこで、今回はFPSに依存しないようにするための手法の一つを紹介します。あくまで手法の一つです。
あと、なんかそういうFPSに関する設定とかあるのかな!?って思った人、ごめんなさい。そういうのは無いです。
方針
そもそもFPSとは何でしょう?再確認してみましょう。
フレームレート (Frame rate)は、動画において、単位時間あたりに処理させるフレームすなわち「コマ」の数(静止画像数)を示す、頻度の数値である[1]。通常、1秒あたりの数値で表し、FPS(英: frames per second=フレーム毎秒)という単位で表す。
フレームレート | Wikipedia
つまり、ここではFPSは1秒間に実行されるメインループの回数だと思ってください。
では、FPSに依存しないためにはどうしたらよいでしょうか?
どんな環境でも、どんな状況でも変わらない基準を使ったらよさそうです。
つまり、時間を使います。
具体的には前フレームからの経過時間を取得します。
言い方を変えると、前回のメインループから今現在のメインループまでの時間の差を取得します。
時間を取得しよう
C言語でミリ秒単位、マイクロ秒単位で時間を取得するためには、time.h
、sts/time.h
を使うことができます。
time.h
はミリ秒、sys/time.h
はマイクロ秒単位で現在時刻を取得できます。ただし、これらの二つのヘッダファイルは別物であることに注意してください。
今回は、マイクロ秒単位で現在時刻を取得できるsys/time.h
を使います。
sys/time.h
にはtimeval
構造体が定義されており、ここに秒とマイクロ秒が格納されています。
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* and microseconds */
};
整数秒はtv_sec
に、小数点以下のマイクロ秒はtv_usec
に格納されています。
現在時刻を取得する関数は、gettimeofday
関数です。
int gettimeofday(struct timeval *tv, struct timezone *tz);
引数にtimeval
構造体のポインタと、タイムゾーンを受け取ります。ただし、タイムゾーンは現在廃止予定となっており、POSIX規格ではNULL以外の値を指定した場合の挙動は未定義になっています。そのため、必ずNULLを指定します。
sys/time.h
を利用した現在時刻の取得を行うプログラムを示します。
#include <stdio.h>
#include <sys/time.h>
int main(void)
{
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
}
これで、nowTime
というtimeval
構造体の中に、gettimeofday
関数を呼び出した瞬間の秒とマイクロ秒が格納されました。
このgettimeofday
関数を毎ループ呼び出して、前回呼び出したgettimeofday
関数で取得した時間と比較をすればよさそうです。
前フレームからの経過時間を取得する
本題です。というより、自分が実装した方法の紹介です。
コード
前フレームからの経過時間を取得するライブラリを作りました。とりあえずそれのコードを示します。
#pragma once
#include <stdio.h>
#include <sys/time.h>
struct DeltaTime
{
double startTime; //計測開始した時点での秒数[秒]
double elapsedTime; //経過時間[秒]
double deltaTime; //前フレームからの経過時間[秒]
};
void initTime(void);
void updateTime(void);
double getElapsedTime(void);
double getDeltaTime(void);
#include "DeltaTime.h"
struct DeltaTime delta;
//DeltaTime構造体を初期化する関数
void initTime(void)
{
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
delta.startTime = nowTime.tv_sec + nowTime.tv_usec * 1.0E-6;
delta.elapsedTime = 0;
delta.deltaTime = 0;
}
//DeltaTime構造体を更新する関数
void updateTime(void)
{
static double oldTime = 0;
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
double now = nowTime.tv_sec + nowTime.tv_usec * 1.0E-6;
delta.elapsedTime = now - delta.startTime;
delta.deltaTime = now - oldTime;
oldTime = now;
}
//DeltaTime構造体の経過時間を取得する関数
double getElapsedTime(void)
{
return delta.elapsedTime;
}
//DeltaTime構造体の前フレームからの経過時間を取得する関数
double getDeltaTime(void)
{
return delta.deltaTime;
}
説明
まず、DeltaTime
構造体を作りました。ここにはプログラムが開始した瞬間の時刻、プログラムが開始してからの経過時間、前フレームからの経過時間を持っています。(プログラムが開始してからの経過時間は趣味で入れました)
struct DeltaTime
{
double startTime; //計測開始した時点での秒数[秒]
double elapsedTime; //経過時間[秒]
double deltaTime; //前フレームからの経過時間[秒]
};
DeltaTime.c
では、C++のクラスっぽいことをしています。
まず、DeltaTime.c
内でDeltaTime
構造体を宣言しています。
struct DeltaTime delta;
initTime
関数では、この宣言したDeltaTime
構造体の初期化を行っています。gettimeofday
関数により現在時刻を取得し、startTime
に格納しています。
//DeltaTime構造体を初期化する関数
void initTime(void)
{
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
delta.startTime = nowTime.tv_sec + nowTime.tv_usec * 1.0E-6;
delta.elapsedTime = 0;
delta.deltaTime = 0;
}
updateTime
関数では、その名の通りDeltaTime
構造体を更新しています。
gettimeofday
関数により、updateTime
関数が呼び出された時刻を取得しています。
前回updateTime
関数を呼び出した時間はstatic
修飾子を付けた変数oldTime
に格納しています。
//DeltaTime構造体を更新する関数
void updateTime(void)
{
static double oldTime = 0;
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
double now = nowTime.tv_sec + nowTime.tv_usec * 1.0E-6;
delta.elapsedTime = now - delta.startTime;
delta.deltaTime = now - oldTime;
oldTime = now;
}
使い方
最初にinitTime
関数を呼び出し、その後はループ毎にupdateTime
関数を呼び出した後にgetDeltaTime
関数を呼び出すことで、前フレームからの経過時間を取得できます。
#include <stdio.h>
#include "DeltaTime.h"
void main(int argc, char** argv)
{
double x = 0, y = 0;
double speed = 10.0;
initTime();
while(1)
{
updateTime();
x += speed * getDeltaTime();
y += speed * getDeltaTime();
}
}
コード全文
既に示していますが、サンプルを置いておきます。
サンプルコード
#pragma once
#include <stdio.h>
#include <sys/time.h>
struct DeltaTime
{
double startTime; //計測開始した時点での秒数[秒]
double elapsedTime; //経過時間[秒]
double deltaTime; //前フレームからの経過時間[秒]
};
void initTime(void);
void updateTime(void);
double getElapsedTime(void);
double getDeltaTime(void);
#include "DeltaTime.h"
struct DeltaTime delta;
//DeltaTime構造体を初期化する関数
void initTime(void)
{
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
delta.startTime = nowTime.tv_sec + nowTime.tv_usec * 1.0E-6;
delta.elapsedTime = 0;
delta.deltaTime = 0;
}
//DeltaTime構造体を更新する関数
void updateTime(void)
{
static double oldTime = 0;
struct timeval nowTime;
gettimeofday(&nowTime, NULL);
double now = nowTime.tv_sec + nowTime.tv_usec * 1.0E-6;
delta.elapsedTime = now - delta.startTime;
delta.deltaTime = now - oldTime;
oldTime = now;
}
//DeltaTime構造体の経過時間を取得する関数
double getElapsedTime(void)
{
return delta.elapsedTime;
}
//DeltaTime構造体の前フレームからの経過時間を取得する関数
double getDeltaTime(void)
{
return delta.deltaTime;
}
#include <stdio.h>
#include "DeltaTime.h"
void main(int argc, char** argv)
{
double x = 0, y = 0;
double speed = 10.0;
initTime();
while(1)
{
updateTime();
x += speed * getDeltaTime();
y += speed * getDeltaTime();
}
}
おわりに
私の環境では、某授業のOpenGLにてマウスをウィンドウ内で動かし続けている間だけFPSが2倍になるという事例が発生しました。
前フレームからの経過時間を取得するやり方は今後も色々使っていくと思うので、一度作っておくことをお勧めします。