C言語でFPSに依存しないアニメーションをする

# はじめに
この記事は[長野高専 Advent Calender 2022](https://qiita.com/advent-calendar/2022/nnct "長野高専 Advent Calender 2022") の**カレンダー2**の15日目の記事です。みんなも書いていいんだよ

突然ですが、こんなコードを書いていませんか？
```C
# 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に依存しないようにするための手法の一つを紹介します。<font color=#bbbbbb>あくまで手法の一つです。</font>

あと、なんかそういうFPSに関する設定とかあるのかな！？って思った人、ごめんなさい。そういうのは無いです。

# 方針
そもそもFPSとは何でしょう？再確認してみましょう。
> フレームレート (Frame rate)は、動画において、単位時間あたりに処理させるフレームすなわち「コマ」の数（静止画像数）を示す、頻度の数値である[1]。通常、1秒あたりの数値で表し、FPS（英: frames per second＝フレーム毎秒）という単位で表す。
[フレームレート | Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AC%E3%83%BC%E3%83%88)

つまり、ここでは**FPSは1秒間に実行されるメインループの回数**だと思ってください。
では、FPSに依存しないためにはどうしたらよいでしょうか？
どんな環境でも、どんな状況でも変わらない基準を使ったらよさそうです。
つまり、**時間**を使います。
具体的には**前フレームからの経過時間**を取得します。
言い方を変えると、前回のメインループから今現在のメインループまでの時間の差を取得します。


# 時間を取得しよう
C言語でミリ秒単位、マイクロ秒単位で時間を取得するためには、`time.h`、`sts/time.h`を使うことができます。
`time.h`はミリ秒、`sys/time.h`はマイクロ秒単位で現在時刻を取得できます。ただし、これらの二つのヘッダファイルは別物であることに注意してください。
今回は、マイクロ秒単位で現在時刻を取得できる`sys/time.h`を使います。
`sys/time.h`には`timeval`構造体が定義されており、ここに秒とマイクロ秒が格納されています。
```C
struct timeval {
	time_t		tv_sec;		/* seconds */
	suseconds_t	tv_usec;	/* and microseconds */
};
```
整数秒は`tv_sec`に、小数点以下のマイクロ秒は`tv_usec`に格納されています。
現在時刻を取得する関数は、`gettimeofday`関数です。
```C
int gettimeofday(struct timeval *tv, struct timezone *tz);
```
引数に`timeval`構造体のポインタと、タイムゾーンを受け取ります。ただし、タイムゾーンは現在廃止予定となっており、POSIX規格ではNULL以外の値を指定した場合の挙動は未定義になっています。そのため、必ずNULLを指定します。
`sys/time.h`を利用した現在時刻の取得を行うプログラムを示します。
```C
#include <stdio.h>
#include <sys/time.h>

int main(void)
{
    struct timeval nowTime;
    gettimeofday(&nowTime, NULL);
}
```

これで、`nowTime`という`timeval`構造体の中に、`gettimeofday`関数を呼び出した瞬間の秒とマイクロ秒が格納されました。
この`gettimeofday`関数を毎ループ呼び出して、前回呼び出した`gettimeofday`関数で取得した時間と比較をすればよさそうです。

# 前フレームからの経過時間を取得する
本題です。というより、自分が実装した方法の紹介です。
## コード
前フレームからの経過時間を取得するライブラリを作りました。とりあえずそれのコードを示します。

```C:DeltaTime.h
#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);
```

```C:DeltaTime.c
#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`構造体を作りました。ここにはプログラムが開始した瞬間の時刻、プログラムが開始してからの経過時間、前フレームからの経過時間を持っています。(プログラムが開始してからの経過時間は趣味で入れました)
```C:DeltaTime構造体
struct DeltaTime
{
    double startTime;           //計測開始した時点での秒数[秒]
    double elapsedTime;         //経過時間[秒]
    double deltaTime;           //前フレームからの経過時間[秒]
};
```
`DeltaTime.c`では、C++のクラスっぽいことをしています。
まず、`DeltaTime.c`内で`DeltaTime`構造体を宣言しています。
```C
struct DeltaTime delta;
```
`initTime`関数では、この宣言した`DeltaTime`構造体の初期化を行っています。`gettimeofday`関数により現在時刻を取得し、`startTime`に格納しています。

```C:initTime関数
//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`に格納しています。
```C:updateTime関数
//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`関数を呼び出すことで、前フレームからの経過時間を取得できます。
```C:example.c
#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();
    }
}
```

## コード全文
既に示していますが、サンプルを置いておきます。
<details><summary>サンプルコード</summary>

```C:DeltaTime.h
#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);
```

```C:DeltaTime.c
#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;
}
```
```C:example.c
#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();
    }
}
```
</details>


# おわりに
私の環境では、某授業のOpenGLにてマウスをウィンドウ内で動かし続けている間だけFPSが2倍になるという事例が発生しました。
前フレームからの経過時間を取得するやり方は今後も色々使っていくと思うので、一度作っておくことをお勧めします。

# 参考文献
[時間情報の取得方法と扱い方 |  青色工房](https://www.mm2d.net/main/prog/c/time-04.html)