はじめに
前回は、C++のGameクラスを導入して、ゲーム開始時・実行中・終了時のそれぞれの処理だけを書く場所を用意しました。
早くゲームのメインの部分を書き始めたい気持ちをぐっとこらえて、今回もC++の環境を整備したいと思います。というのも、次はシェーダを使った描画の解説に移りたいのですが、シェーダのソースコードを読み込んだり、コンパイル中などにエラーが起こった場合の処理など、文字列関係の処理がここから多くなってくるからです。文字列を操作するためのヘルパー関数をここで導入しておくと、後の実装がとてもスムーズに進められます。
また、シェーダのプログラムや画像ファイルなどを追加するために、Xcodeのプロジェクト内のグループについても今回整備しておきたいと思います。
1. 文字列サポートのためのC++ファイルの追加
まずは、文字列をサポートするためのC++ファイルを追加しましょう。Xcodeのプロジェクト・ナビゲータから「MyGLGame」フォルダを右クリックして、「New File…」を実行します。
次に表示されるテンプレートの選択画面では、「macOS」の「C++ File」を選択して、「Next」ボタンを押します。
ファイル名に「StringSupport」と入力し、ヘッダファイルを同時に作成するための「Also create a header file」のチェックボックスがチェックされていることを確認してから、「Next」ボタンを押します。次の画面で何も変更せずに「Create」ボタンを押すと、StringSupport.hppとStringSupport.cppの2つのファイルがプロジェクトに追加されます。
なお、ファイルが格納されている位置を表すファイルパスを取得するために、Objective-CでFoundationフレームワークにアクセスする必要がありますので、「.cpp」の拡張子を「.mm」に変更して、Objective-C++のファイルとしてコンパイルされるようにしておきます。
それではStringSupport.hppに、次のように関数とクラスの宣言を書きましょう。
-
printf()
関数の書式でC++文字列を作成するFormatString()
関数 - ファイル名からパス文字列に変換する
GetFilepath()
関数 - テキストファイルの内容をすべて文字列として読み込む
ReadTextFile()
関数 - ゲーム実行中に起きるエラーを表す
GameError
例外クラス
以上の4つをここで宣言します。
#ifndef StringSupport_hpp
#define StringSupport_hpp
#include <string>
#include <stdexcept>
/*! printf()関数の書式でC++文字列を作成します(1023文字以内)。 */
std::string FormatString(const char* format, ...);
/*! 指定された名前のファイルに対応したパス文字列を取得します。 */
std::string GetFilepath(const std::string& filename);
/*! 指定された名前のテキストファイルを文字列として読み込みます。 */
std::string ReadTextFile(const std::string& filename);
/*! ゲーム実行中に起きるエラーを表す例外クラス */
class GameError : public std::runtime_error
{
public:
template<typename... T>
GameError(const char *format, T... args)
: std::runtime_error(FormatString(format, args...)) {}
GameError(const std::string& message)
: std::runtime_error(message) {}
};
#endif /* StringSupport_hpp */
GameErrorクラスはヘッダファイルに実装を含めていますが、このような自前の例外クラスを用意することで、書式付きの可変長引数を使った例外作成ができるようになり、便利になります。
StringSupport.mmに、これらの関数の実装コードを書きます。
#include "StringSupport.hpp"
#include <fstream>
#include <stdexcept>
#import <Foundation/Foundation.h>
std::string FormatString(const char* format, ...)
{
static char buffer[1024];
va_list marker;
va_start(marker,format);
vsnprintf(buffer, 1024, format, marker);
va_end(marker);
return buffer;
}
std::string GetFilepath(const std::string& filename)
{
NSString *fileNameObj = [[NSString alloc] initWithCString:filename.c_str() encoding:NSUTF8StringEncoding];
NSString *body = [fileNameObj stringByDeletingPathExtension];
NSString *ext = [fileNameObj pathExtension];
NSString *path = [[NSBundle mainBundle] pathForResource:body ofType:ext];
if (!path) {
throw GameError("Cannot locate a file path: %s", filename.c_str());
}
return std::string([path cStringUsingEncoding:NSUTF8StringEncoding]);
}
std::string ReadTextFile(const std::string& filename)
{
std::string filepath = GetFilepath(filename);
std::ifstream ifs(filepath);
if (!ifs) {
throw GameError("Failed to open a file: %s", filepath.c_str());
}
ifs.seekg(0, std::ios::end);
int length = (int)ifs.tellg();
ifs.seekg(0, std::ios::beg);
char *buffer = new char[length + 1];
ifs.read(buffer, length);
buffer[length] = '\0';
std::string ret = buffer;
delete[] buffer;
return ret;
}
FormatString()
関数の実装では、可変数引数の基本的な処理を行っているだけです。1023文字(ナル終端文字を含めて1024文字)しか扱えない簡略な実装ですが、基本的な開発ではこれで問題は起きないでしょう。
GetFilepath()
関数の実装では、ファイル名として与えられたC++文字列をUTF-8として解釈してObjective-Cの文字列に変換し、拡張子なしのファイル名と拡張子に分離させた上で、NSBundleクラスのメソッドでリソースのファイルパスを取得しています。取得したファイルパスは、またUTF-8の文字エンコーディングでC++文字列に変換してリターンします。
ReadTextFile()
関数の実装では、指定されたファイル名をGetFilepath()
関数を使ってファイルパスに変換し、取得したファイルパスを使って、基本的なC++のファイル読み込みの方法であるifstreamクラスを使った方法で文字列を読み込んでいます。このように、ファイルパスさえ取得できてしまえば、C++のifstreamクラスも使えますし、C言語のfopen()
関数を使って読み込むこともできます。
2. Xcodeプロジェクトのグループの整理
プロジェクト・ナビゲータ上でGame.cppを右クリックして、出てくるメニューから「New Group without Folder」を実行します(**「without Folder」**を選択して、フォルダを作らないグループを用意していることに注意!)。
すると新しいグループができますので、名前を「GameLibrary」としておきましょう。
「New Group without Folder」を一度実行すると、次にグループを作る時にはフォルダを作らないグループを用意するのがデフォルト操作になって、メニューには「New Group with Folder」という項目が出て来るようになるので、注意が必要です。もう一度「Game.cpp」を選択して、右クリックメニューを出し、次は「New Group」を選択して、フォルダを作らないグループを「Resources」という名前で作成しましょう。
フォルダを持たない2つのグループが作成できました。
Game.hppとGame.cpp以外のMyGLGameフォルダ内のファイルを、すべてGameLibraryグループの中に移動します。以降の記事では、ゲームのメインの実装部分をGameクラスの中に書き、それ以外のOpenGLの処理をまとめたクラスを「GameLibrary」と呼んで、このGameLibraryグループの中に追加していきたいと思います。
これで以降の開発においては、Game.hppとGame.cppにC++のコードを書き、シェーダのソースコードやテクスチャの画像ファイルなどをResourcesグループに追加できるようになり、全体像がとても見渡しやすくなりました。
3. まとめ
今回は、前回に続いて、C++でOpenGLのプログラムを書いていくための環境を整備しました。次回は、経過時間を計測して、実行速度を表すFPSを計算する方法を解説します。
ここまでのプロジェクト:MyGLGame_step1-6.zip