はじめに
前回までで、Xcodeのプロジェクトを作成して、OpenGLの基本的な設定を行い、ウィンドウのリサイズ方法も調整することができました。
今回は、ゲームに欠かすことのできない、画面とデータを更新するためのゲーム更新ループを作成する方法を解説します。
ゲーム更新ループの速度は、FPS (Frame Per Second)、すなわち、1秒間に何回画面(フレーム)を更新するかという数値で表されます。PC用ゲームの基本FPSは60fpsです。これは、旧来のテレビやPCモニタの垂直同期周波数が60Hzに設定されていることが多かったことに起因します。
現在は120Hzや144Hzのモニタも存在しますので、「60」という数値にそれほど意味はありませんし、今回使用するディスプレイ・リンクを使った方法でも、60という数値を設定する箇所はありません。ただし50fpsを下回るようになると、人間の目には滑らかさが減っていくのが感じられるようになりますので、「滑らかさが十分に保たれている基準の数値」として「60fps」以上出ているか、それをかなり下回っているかといったことを判断すると良いでしょう。(FPSを計算する方法は、また別の記事で解説します。)
SEGAのSPECIAL COLUMN『SEGAAGES2500VF2』メイキンマニアックスなどを参照すると、ゲームセンター用のゲームは57.5fpsで作られていて、これをそのまま60fpsの環境に移植すると、格闘ゲームのコマンド入力の操作感にかなり影響を及ぼすというようなことが書かれています。ゲームの種類によっては、FPSの数値はかなり重要な場合があります。
1. ディスプレイ・リンクを作成して開始する
現在のmacOS上でゲーム更新用のループを作成するためには、画面の垂直同期周波数を考慮しながら一定間隔でコールバック関数を呼び出してくれる、ディスプレイ・リンクと呼ばれる機構を利用します。これはCore Videoと呼ばれるビデオ処理用フレームワークの機能の一部となっており、自分でスレッドを作成して実行するよりも効率的な描画処理が行われることが期待できます。
MyGLView.mを編集して、「@implementation MyGLView
」と書かれている直後に波括弧を追加して、インスタンス変数を3つ宣言します。OpenGLのコンテキストを表す変数と、ディスプレイリンクを操作するためのCVDisplayLinkRef型のハンドラと、ゲームが更新されていることを確認するためのテスト用のfloat変数です。
またその変数宣言の直後に、DisplayLinkCallback()
という名前の関数を追加します。この関数は、ディスプレイ・リンクによって一定間隔で呼び出されます。
DisplayLinkCallback()
関数の最後の引数にOpenGLビューのポインタがvoidポインタとして渡されてきますので、MyGLViewクラスのポインタとして扱うためにキャストして使います。キャストする際には、ARC (Automatic Reference Counting)機構に対してポインタの解放等について考慮しなくても良いように、「__bridge」というキーワードを添えてキャストします。コールバック関数は、ゲームの基本の60fpsで1秒間に60回、1分間で3600回も呼び出されるものですので、コールバック関数内の処理を覆う「@autoreleasepool
」キーワードを忘れずに書いて、毎フレームの処理ごとに発生する使用メモリを細かく解放していくことも重要です。
こうして取得したMyGLViewクラスのインスタンスに対して、後述するrender
メソッドを呼び出すことで、ゲーム実行中に一定間隔で繰り返しこのrender
メソッドが呼び出されるようになります。
@implementation MyGLView {
NSOpenGLContext *glContext;
CVDisplayLinkRef displayLink;
float value;
}
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink,
const CVTimeStamp* now,
const CVTimeStamp* outputTime,
CVOptionFlags flagsIn,
CVOptionFlags *flagsOut,
void *displayLinkContext)
{
@autoreleasepool {
MyGLView *glView = (__bridge MyGLView *)displayLinkContext;
[glView render];
return kCVReturnSuccess;
}
}
次に、MyGLViewクラスのprepareOpenGL
メソッドの実装の最後に、テスト用変数の初期化のためのコードと、ディスプレイ・リンクを作成して実行開始するコードを追加しましょう。
なお、以前はglContext
変数をprepareOpenGL
メソッドのローカル変数として用意していましたが、今回はMyGLViewクラスのインスタンス変数としてglContext
変数を用意しましたので、ローカル変数としてのglContext
変数の宣言を削除しておくのを忘れないようにしてください。
- (void)prepareOpenGL
{
[super prepareOpenGL];
glContext = [self openGLContext];
glClearColor(1.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
[glContext flushBuffer];
value = 0.0f;
// 垂直同期を使用するためには以下2行のコメントアウトを外す
//GLint swapInt = 1;
//[glContext setValues:&swapInt forParameter:NSOpenGLCPSwapInterval];
CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, &DisplayLinkCallback, (__bridge void*)(self));
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(displayLink, cglContext, cglPixelFormat);
CVDisplayLinkStart(displayLink);
}
OpenGLのコンテキストは、メインスレッド上でしか取得してはいけないため、prepareOpenGLメソッドで取得して保存しておかないと、後々ディスプレイ・リンクからのコールバックがあった時点で取得しようとするとエラーが出る環境があります。
ディスプレイ・リンクを作成するためには、CVDisplayLinkCreateWithActiveCGDisplays()
という関数を呼び出します。そしてCVDisplayLinkSetOutputCallback()
関数で、関数のポインタを渡してコールバック関数とその引数(MyGLViewクラスのインスタンス)を指定します(ここでもARCのために「__bridge」キーワードを指定してvoidポインタにキャストします)。
ディスプレイ・リンクを開始させるためには、CocoaのOpenGLビューのさらに下のレイヤで動作しているCore OpenGL (CGL)と呼ばれる機構にアクセスする必要があります。そのため、事前に取得してあったOpenGLのコンテキストからさらにCGLのコンテキストを取得し、またCGL用のピクセルフォーマット情報も取得して、それらの情報を渡して、CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext()
関数を呼び出すことによって、ディスプレイ・リンクの設定が完了します。あとはCVDisplayLinkStart()
関数を呼び出すことによって、ディスプレイ・リンクの実行が開始されます。
なお、prepareOpenGL
メソッドに追加したコード中、コメントアウトしてある2行のコメントアウトを外すことで、厳密に垂直同期信号に同期したタイミングでOpenGLの更新が行われるようになります。ただし、垂直同期信号に同期させるためには適宜待ち時間が発生することになり、そこで重めの描画処理を行った場合などには、「同期のための待ち時間」+「重い描画処理時間」がフレーム落ちの原因を作ってしまい、却ってゲームの滑らかさを奪ってしまうことにもつながります。また前述のように、PC環境によって垂直同期信号の周波数がかなり変わる時代になりましたので、厳密に垂直同期信号に更新タイミングを合わせるよりも、待ち時間をなくしてできるだけ高速なループを繰り返す方が安定したゲーム実行につながるのではないかと思います(Apple主催のWWDCでも、繰り返しこのように説明されてきました。2014年以降はグラフィック描画のベース環境がMetalに移行しましたので、めっきりこのようなお話も出なくなりましたが。。。)。
2. ゲーム更新用メソッドを実装する
それでは最後に、ゲームの更新を行うrender
メソッドを、MyGLView.mの「@end
」の行の直前に追加しましょう。なお、render
メソッドの中で利用するPingPong()
関数もその上に追加します。
float PingPong(float t)
{
t -= floorf(t / 2.0f) * 2.0f;
return 1.0f - fabsf(t - 1.0f);
}
- (void)render
{
[glContext lock];
[glContext makeCurrentContext];
glClearColor(1.0f - PingPong(value), PingPong(value), 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
value += 0.01f;
[glContext flushBuffer];
[glContext unlock];
}
@end
ディスプレイ・リンクによって呼び出されたコールバックの中でOpenGLの処理を行うためには、まずOpenGLのコンテキストに対してlock
メソッドを使ってロックします。OpenGLのコンテキストがロックできて、現在のこのメソッドだけがOpenGLの描画を行う権利を得たことが確定した時点で、makeCurrentContext
メソッドを呼び出して、OpenGLの描画コマンドを実行していきます。
ここでは、value
変数が0.01fずつ加算され続けるようにして、そのvalue
変数を元に、0.0f → 1.0f → 0.0f → 1.0f → ...と変化するような値をPingPong()
関数を使って計算しています。RGBのRの値とGの値が連続的に変化して、画面の色がピンク色から紫色を経て水色に変化します。なお、だいたいの環境ではディスプレイ・リンクは60fpsで実行されるようですので、約1.67秒(=(1.0/0.01)/60)で1.0fずつ変化する計算です。経過時間に基づいて厳密に変化量を計算する方法については、また別の記事で解説します。
描画コマンドを発行し終えたら、OpenGLのコンテキストに対してflushBuffer
メソッドを送って、描画を実行した上で、ダブルバッファリングの表画面と裏画面を切り替えます。そしてロックしておいたOpenGLのコンテキストを、unlock
メソッドでアンロックします。
3. おわりに
今回は、ディスプレイ・リンクを作ってゲーム更新用ループを作成する方法について解説しました。画面の色が動的に変化するようになって、ほんの少しですが、ゲームらしさの片鱗が見えてきたかと思います。OpenGLを利用するためにマスターしなければいけない事項は多いですので、まだまだ入り口を少し抜けた程度のことですが (^^;)、着実に一歩ずつ理解していきましょう。
開始したディスプレイ・リンクは、正式にはアプリ終了前に停止して削除しなければいけませんが、ここのタイミング調整も少しテクニックが必要ですので、次の記事で解説したいと思います。macOSは優秀なOSですので、今のようなテスト段階であれば、多少の後処理は省略してもそれなりにちゃんとクリーンアップ処理を内部的にやってくれると信じましょう ;-)。
また、現在どれくらいのFPSで処理が行われているのかを計算する方法についても、記事を改めて解説したいと思います。
ここまでのプロジェクト:MyGLGame_step1-3.zip