4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

長野高専Advent Calendar 2022

Day 15

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

Last updated at Posted at 2022-12-14

はじめに

この記事は長野高専 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ループ毎にxyをインクリメントしているだけの簡単なコードです。しかし、このコードにはある問題点があります。
それは、
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.hsts/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関数で取得した時間と比較をすればよさそうです。

前フレームからの経過時間を取得する

本題です。というより、自分が実装した方法の紹介です。

コード

前フレームからの経過時間を取得するライブラリを作りました。とりあえずそれのコードを示します。

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);
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構造体を作りました。ここにはプログラムが開始した瞬間の時刻、プログラムが開始してからの経過時間、前フレームからの経過時間を持っています。(プログラムが開始してからの経過時間は趣味で入れました)

DeltaTime構造体
struct DeltaTime
{
    double startTime;           //計測開始した時点での秒数[秒]
    double elapsedTime;         //経過時間[秒]
    double deltaTime;           //前フレームからの経過時間[秒]
};

DeltaTime.cでは、C++のクラスっぽいことをしています。
まず、DeltaTime.c内でDeltaTime構造体を宣言しています。

struct DeltaTime delta;

initTime関数では、この宣言したDeltaTime構造体の初期化を行っています。gettimeofday関数により現在時刻を取得し、startTimeに格納しています。

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に格納しています。

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関数を呼び出すことで、前フレームからの経過時間を取得できます。

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();
    }
}

コード全文

既に示していますが、サンプルを置いておきます。

サンプルコード
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);
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;
}
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();
    }
}

おわりに

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

参考文献

時間情報の取得方法と扱い方 | 青色工房

4
0
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
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?