はじめに
前回は、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を次のように編集します。
#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では、次のように各関数の実装を書きます。
#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をインクルードします。
#import "MyGLView.h"
#import <OpenGL/OpenGL.h>
#import <OpenGL/gl3.h>
#include "Game.hpp"
#include "Time.hpp"
ゲーム起動直後に呼び出されるprepareOpenGL
メソッドの先頭で、Time構造体のStart()
関数を呼び出すコードを書きます。これで実行開始時の時間が初期化されます。
- (void)prepareOpenGL
{
[super prepareOpenGL];
Time::Start();
/* 以下、省略 */
}
毎フレームごとに呼び出されるrender
メソッドの最後に、経過時間を更新するためのUpdate()
関数を呼び出すコードを書きます。なお、計測したFPS値をウィンドウのタイトルに表示するためのsetTitle:
メソッドの呼び出しのコードも、その直後に書いておきます。
- (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ファイルを読み込むコードを書きます。
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "Time.hpp"
...
そしてvalue
変数の値を変更しているGame::Render()
関数の実装を、次のように、Time::deltaTime
変数を使って値を変更するように書き換えます。このように書くことで、秒速0.25で値が変化するようになります。この書き方は、Unityでコードを書く場合と同様です。
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
変数を使って、正確な時間に基づいたアニメーションが記述できるようになりました。
次回は、キーボードの操作をサポートして、キーボード入力に基づいて値を変更できるようにしていきましょう。