Xcode
OpenGL
macos
Objective-C++

macOSでOpenGLプログラミング(1-7. 経過時間とFPSの測定)

macOSでOpenGLプログラミングの目次に戻る

はじめに

前回は、C++での文字列処理のクラスを追加し、ファイルを整理するためのグループ分けを行いました。

今回は、ゲーム開始時からの経過時間を測定し、経過時間に応じたゲーム実行のコードを書けるようにしましょう。また実行速度を表すFPSを計算する方法についても解説します。

1. Timeクラスの追加

時間を管理するTime構造体を定義するためのファイルを追加します。StringSupport.mmを右クリックして、出てくるメニューから「New File…」を選択します。

 

テンプレート画面では、「macOS」の「C++ File」を選択して「Next」ボタンを押します。

 

ファイル名に「Time」と入力し、「Also create a header file」がチェックされていることを確認して、「Next」ボタンを押します。次の画面で何も変更せずに「Create」ボタンを押すと、Time.hppとTime.cppの2つのファイルができます。

 

Objective-C++を利用しますので、「Time.cpp」ファイルの拡張子を「.mm」に変更しておきます。

 

Time.hppを次のように編集します。

Time.hpp
#ifndef Time_hpp
#define Time_hpp

struct Time
{
    static unsigned frameCount; //!< フレーム数
    static float    time;       //!< ゲーム開始時からの経過時間(秒)
    static float    deltaTime;  //!< 直前のフレームからの経過時間(秒)
    static float    fps;        //!< FPS

    static void Start();
    static void Update();
};

#endif /* Time_hpp */

時間を管理するためのTime構造体をこのように定義し、4個のstatic変数を用意し、ゲーム開始時に初期化のために呼ばれるStart()関数と、毎フレーム呼ばれるUpdate()関数を用意します。

Time.mmでは、次のように各関数の実装を書きます。

Time.mm
#include "Time.hpp"

#import <Foundation/Foundation.h>

static NSTimeInterval   startTime;
static NSTimeInterval   oldTime;
static NSTimeInterval   oldFPSTime;

unsigned Time::frameCount   = 0;
float Time::time            = 0.0f;
float Time::deltaTime       = 0.0f;
float Time::fps             = 0.0f;


void Time::Start()
{
    startTime = [NSDate timeIntervalSinceReferenceDate];
    oldTime = startTime;
    oldFPSTime = 0.0f;
}

void Time::Update()
{
    NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
    time = now - startTime;
    deltaTime = now - oldTime;
    oldTime = now;

    frameCount++;
    if (frameCount % 60 == 0) {
        fps = 60.0f / (time - oldFPSTime);
        oldFPSTime = time;
    }
}

現在時刻を秒単位で取得するために、NSDateクラスのtimeIntervalSinceReferenceDateメソッドを呼び出します。このメソッドは、世界標準時の2001年1月1日 00:00:00からの経過時間を秒単位でリターンします。

こうして取得した現在時間を、Start()関数がゲーム開始時に呼ばれるタイミングでstartTime変数に格納しておき、その後、毎フレームUpdate()関数が呼ばれるタイミングで現在時刻からstartTimeを引くことで、ゲーム開始時から現在までの秒数がtime変数に格納されます。

直前のフレームで取得した現在時刻をoldTime変数に格納し、それを現在時刻から引くことで、直前のフレームからの経過時間がdeltaTime変数に格納されます。

Update()関数が呼ばれるたびにframeCount変数をインクリメントし、60フレームごとに、前回FPSを計算したときからの経過時間でフレーム数を割ることで、FPSを計算して、その値をfps変数に格納することができます。

なお、Time.mmの先頭で定義しているstaticな変数(Time.mmでのみ利用可能な変数)を、Time構造体のstatic変数として定義していないのは、これらの変数の型がNSTimeIntervalというObjective-Cの型だからです。これをヘッダファイルに書いてしまうと、Time.hppを読み込んでTime構造体を使用するすべてのファイルの拡張子を「.mm」にしてObjective-C++でコンパイルしなければいけなくなります。しかしこれらの変数をTime.mmでのみ宣言しておけば、Time.hppは純粋なC++互換のファイルとなりますので、「.cpp」ファイルから読み込んでそのまま使用できます。

2. Time構造体の更新用関数を呼び出す

MyGLView.mmファイルを編集して、Time構造体の2つの更新用関数を呼び出すコードを書きます。

まずファイルの先頭でTime.hppをインクルードします。

MyGLView.mm(ヘッダファイルのインクルード部)
#import "MyGLView.h"

#import <OpenGL/OpenGL.h>
#import <OpenGL/gl3.h>

#include "Game.hpp"
#include "Time.hpp"

ゲーム起動直後に呼び出されるprepareOpenGLメソッドの先頭で、Time構造体のStart()関数を呼び出すコードを書きます。これで実行開始時の時間が初期化されます。

MyGLView.mm(prepareOpenGLメソッド)
- (void)prepareOpenGL
{
    [super prepareOpenGL];

    Time::Start();

    /* 以下、省略 */
}

毎フレームごとに呼び出されるrenderメソッドの最後に、経過時間を更新するためのUpdate()関数を呼び出すコードを書きます。なお、計測したFPS値をウィンドウのタイトルに表示するためのsetTitle:メソッドの呼び出しのコードも、その直後に書いておきます。

MyGLView.mm(renderメソッド)
- (void)render
{
    /* 先頭部分は省略 */

    // 経過時間の更新
    Time::Update();
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self.window setTitle:[NSString stringWithFormat:@"Game (%.2f fps)", Time::fps]];
    }];
}

ウィンドウのタイトルを設定するようなGUI操作のコードは、基本的にメインスレッド上で実行する必要があります。そこでNSOperationQueueクラスのメソッドを使って、setTitle:メソッドの呼び出しのコードを、メインスレッド上で実行されるブロック内に書いておきます。

ちなみに、printf()関数やNSLog()関数を使った標準出力への書き出しやデバッグログの書き出しは、意外と重い処理ですので、計測したFPS情報をこれらの関数を使って表示してはいけません。それだけでFPSの数値がどんどん落ちてしまいます。これはゲームのデバッグにおいても同じ事が言えます。

以上でTime構造体の各static変数に、計測した時間が格納されるようになります。

3. 計測した時間を利用するコードを書く

こうして計測した前フレームからの経過時間を表すdeltaTime変数を使用することで、Gameクラスの変数の値を変更する時に、秒単位で変更するコードが書けるようになります。

まず、Time構造体が利用できるように、 Game.hppの先頭にTime.hppファイルを読み込むコードを書きます。

Game.hpp(先頭部分)
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "Time.hpp"
...

そしてvalue変数の値を変更しているGame::Render()関数の実装を、次のように、Time::deltaTime変数を使って値を変更するように書き換えます。このように書くことで、秒速0.25で値が変化するようになります。この書き方は、Unityでコードを書く場合と同様です。

Game.cpp(Render()関数)
void Game::Render()
{
    glClearColor(1.0f - PingPong(value), PingPong(value), 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 変更前
    //value += 0.01f;

    // 変更後
    value += 0.25f * Time::deltaTime;
}

これを実行すると、次のように、タイトルバーにFPS情報が表示され、色が秒速0.25で変化するようになったことが分かります。つまり、value変数の値が4秒間できっちり1.0ずつ変化するようになっています。

 

Time::deltaTime変数を使わずに書いていた時には、「『およそ』60分の1秒間に1回0.01ずつ値を変化させる」ということしか書き表せていなかったので、正確な時間に基づいた表現ができていませんでした。

ここまでのプロジェクト:MyGLGame_step1-7.zip

4. まとめ

今回は、Time構造体を追加し、経過時間とFPSを計測できるようにしました。また前フレームからの経過時間を表すdeltaTime変数を使って、正確な時間に基づいたアニメーションが記述できるようになりました。

次回は、キーボードの操作をサポートして、キーボード入力に基づいて値を変更できるようにしていきましょう。


次の記事:macOSでOpenGLプログラミング(1-8. キーボード操作をサポートする)