ゲームエンジンを使わずにC++とOpenGLでゲームを作った話
先日、iOS用ゲームアプリ「センメツコースター」をリリースしました。このゲームの開発にはゲームエンジンは使っておらず、C++とOpenGLとOpenALなどで実装されています。最近はゲームエンジンを使うことが当たり前になっており、ゲームエンジンを使わないゲームの作り方があまり知られていない気がしたので「センメツコースター」を例にして、どうやってC++とOpenGLでゲームを作るのかを(大雑把に)紹介したいと思います。記事中で取り上げたツールやライブラリへのリンクは最後にまとめて掲載してます。
作業環境
作業はすべてMac上で行いました*1が、基本的にクロスプラットフォームなツールやライブラリしか使っていないのでWindowsでも同じように開発できると思います。また、「センメツコースター」はiOS用アプリですが開発自体はMac上で行っていたので、PCゲーム開発の話として読んで下さい。iOS対応の話はまた別の機会に…。
最低限必要なもの
まず、そもそもゲームを作るとはどういうことなのか。ゲームは何で出来ているのか。大きく分けると以下の要素になります。
- ソフトウェアの生成
- 絵の表示
- 音の再生
- 入力の受付
- ファイルの読み書き
- メインループ
ソフトウェアの生成とファイル読み書き以外、すべて素のC++では実現できません。外部のライブラリに頼る必要があります。逆にいうと、これらさえ用意できればあとはC++でコードを書くだけでゲームを作ることができます(理論的には)。
ソフトウェアの生成 : C++コンパイラ
ゲームとは何なのか。実態はソフトウェアです。アプリケーション、実行ファイルとも言います。ソフトウェアを生成する手段はいろいろありますが、「センメツコースター」ではC++コンパイラを使っています。つまりC++コードをコンパイルしてソフトウェアを生成しているわけです。
C++コンパイラはいろいろな種類がありますが、今回はClangを使いました。WindowsだとVC++が一番手軽かもしれません。コンパイラは直接使うことも出来ますが、一般的には統合開発環境(IDE)経由で使います。C++の統合開発環境もいろいろありますが、私はQtCreatorを好んで使っています。QtCreatorはクロスプラットフォームなのでWindowsでも使うことができます。他の統合開発環境はMacだとXcode、WindowsだとVisualStudioが有名です。
絵の表示 : OpenGL + GLFW
次に必要なのは絵の表示です。素のC++では絵の表示を行うことができないので外部ライブラリに頼ることになります。C++を使ったゲーム開発で一般的なのはOpenGLとDirectXです。記事のタイトルにもあるように今回はOpenGLを選びました。OpenGLの魅力は何と言っても動作環境の多さです。MacとWindowsはもちろんiOSやAndroidでも動作します。ただしOpenGLと言っても1枚岩ではなく、複数のバージョンとOpenGLESというコンパクト版があります。OpenGLESは組み込み用のOpenGLなので、Mac,WindowsはOpenGLのみに対応、iOS,AndroidはOpenGLESのみに対応しています。ですが、OpenGLESはOpenGLのコンパクト版なので、OpenGLESを使っておけばそのままOpenGL環境でも動作します(バージョンは合わせる必要があります)。「センメツコースター」ではOpenGLES2.0を選びました。
ところで絵の表示には、絵を表示する場所も用意する必要があります。絵を表示する場所とは、PCゲームの場合はウインドウのことです。また、ウインドウを用意した上でOpenGLを利用できるように関連付けを行う必要もあります。これらをまとめてやってくれるのがGLFWというライブラリです。GLFWを使うことで、簡単にOpenGLの描画先となるウインドウを用意することができます。さらにGLFWは入力の受付やメインループを作成するための機能も持っています。OpenGLでゲームを作るためにうってつけのライブラリです。
音の再生 : OpenAL + ALURE
効果音やBGMの再生もゲームには必須でしょう。音の再生も素のC++では行うことができないので外部のライブラリが必要になります。「センメツコースター」ではOpenALを選択しました。OpenALは名前からわかるようにOpenGLの音声版のような存在です。OpenALはOpenGLと同じように多くの環境で動作します。OpenGLとは違いコンパクト版(組み込み版)や大きなバージョン違いはありません。
OpenALは音の再生を行うことができるのですが、wavやoggのような音声ファイルを読み込むことはできません。そこで利用するのがALUREというライブラリです。ALUREはOpenALのユーティリティライブラリです。音声ファイルを読み込んでOpenALへ渡してくれます。
入力の受付(マウス、キーボード、ゲームパッドなど) : GLFW
ゲームにはユーザーからの入力を受け付ける処理も必要です。これも素のC++ではできません*2。これには「絵の表示」のところでも出たGLFWが利用できます。
ファイルの読み書き : C言語標準関数
ゲームにセーブ機能をつける場合、ファイルの読み書き機能が必要になってきます。セーブ機能をつけなくても、テクスチャファイルなどの読み込みで必要です*3。これはC言語の標準関数、fopenとfwriteで対応可能です。fopenとfwriteはC言語の標準関数なので基本的にはどの環境でも動きます*4。
メインループ : GLFW
最後に必要なのは定期的にゲームの更新処理と描画処理を実行することです。これをメインループと呼びます。メインループは正しく一定の周期で回す必要があるのですが、それを行うにはモニタの垂直同期待ちを行うか高精度タイマー(時間計測)が必要です。どちらも素のC++では実現できないことですが、GLFWにはどちらも用意されています。
現実的に必要になってくるもの
ここまででゲームを作るために必要最低限のものが揃いました。しかし、このままゲームを作り始めようとしてもいろいろ足りないことに気がつくでしょう。そんな足りないものの中でも特に必要になってくるであろうものを紹介します。これらは自分で実装してもいいし、他の人が作ったライブラリを使ってもいいでしょう。
OpenGL、OpenALラッパー
描画を行うために必要なOpenGLですが、OpenGLのAPIは使いづらい上に低レベルなので直接使うのではなく扱いやすくしたラッパークラス、ラッパー関数経由で使うことになると思います。
OpenALも同様です。
数学系クラス、関数
- ベクトル
- 行列
- 矩形
- 当たり判定その他の処理
ベクトルは座標データとして使いますし、当たり判定などでも使います。行列はOpenGLで頂点データを移動回転拡縮をさせるのに必要です。当たり判定はアクションゲームでは特に必要になってくる処理です。
これら数学系クラス、関数は自分で実装するよりすでに存在するライブラリを利用した方がいいかもしれません。なぜならその方が正確かつ高速な可能性が高いからです。「センメツコースター」では全部自分で実装しましたが…。
画像ファイルローダー
画像ファイルローダーとは画像ファイルを読み込んで解析しOpenGL用テクスチャデータとして変換する機能のことです。「センメツコースター」ではTGAファイルのローダーのみ作りました。TGAは簡単なフォーマットなので自作しましたが、pngなどの複雑なフォーマットの場合はライブラリを利用したほうがいいでしょう。
フォント描画システム
フォント描画システムは必須ではありませんが用意しておかないと、テキストを描画する箇所でいちいち画像データを用意する必要が出てきます。フォント描画システムの実装は、まともにやろうとするとかなり大変です。一番楽に必要最低限の実装をするとしたら、文字を0-9A-Zの範囲のみで固定文字幅のフォントテクスチャをペイントソフトなどで用意することでしょうか。固定文字幅でないテキスト描画や、日本語にも対応しようとするとかなり実装難易度が上がります。もしかしたらゲームエンジンを使わなかったことを一番後悔する場面がフォント描画システムを作るときかもしれません。テキストを描画したいだけなのになんでこんなに苦労しているんだろうと。
「センメツコースター」では、フォントテクスチャ生成ツールを作成し、そのツールから生成されたテクスチャデータと文字情報テーブル(jsonファイル)からテキストを描画するシステムを実装しました。
タスクシステム的なもの
ゲームは画面内にいろいろな物が表示されます。そしてよく動きます。これを実現するための古典的な方法がタスクシステムです。別にタスクシステムである必要はないのですが、個々のオブジェクトが毎フレーム更新処理を呼び出される仕組みはゲームと相性がいいのでまず必要になってくると思います。
参考: 【C++ ゲームプログラミング】STLで実装する最小のタスクシステム - Flat Leon Works
さらに実装、導入したもの
「最低限必要なもの」と「現実的に必要になってくるもの」を紹介してきました。ここでは「センメツコースター」でさらに実装、導入したもの紹介します。
UTF8対応文字列クラス
C++には文字列クラスとして標準でstd::stringがありますがUTF8に対応しておらず*5、日本語を含む文字列の場合1文字1文字を正確に扱うことができません。これが問題になるのは、例えばフォント描画システムで日本語を描画する場合などです。またstd::stringは機能も少ないといった不満もあったので、独自に文字列クラスを作成しました。
GUIシステム
GUIシステムとは、ボタンやウインドウなどのGUIを実装するための仕組みです。具体的には以下のような機能群です。
- Widget(Form)のような共通の基底クラス
- Widgetの親子関係(位置や表示状態の連動、マウスイベントなどの伝搬制御)
- マウスやタッチイベント発生時のコールバック
- ボタンなどの汎用的機能の提供
このようなGUIシステムを用意することでゲーム中のUIの実装が楽になります。またデバッグ機能やツールを作る場合にも利用できます。
Jsonの導入
Jsonとは汎用データフォーマットです。Jsonを使うことで、構造化されたデータをファイルに書き出し/ファイルから読み出しすることができるようになります。ゲームでは、設定ファイルやセーブデータのフォーマットとして利用できます。Json以外の選択肢としてはxml、ini、バイナリなどがありますが、Jsonが一番扱いやすいと思います。Jsonをバイナリ化し高速化したMessagePackというものもあります。
Jsonを利用するためにはJsonフォーマットの読み取りと書き出し処理が必要ですが、「センメツコースター」ではPicoJsonというライブラリを利用しました。
Luaの導入
Luaは軽量スクリプト言語です。単体で使うのではなくプログラムに組み込んで使うことに特化しています。Luaは柔軟なのでC++では大量にコーディングする必要があることを数行で書くことができるようになります。「センメツコースター」ではLuaをゲーム全体の制御と敵生成処理ルーチン、各種イベント、チュートリアルの実装に使っています。特にゲーム全体の制御にLuaを使うことの効果は抜群で、今はLuaを使わずにゲームを作ることは考えられないくらいです(個人の感想です)。
各種データ作成ツールのファイルローダー
データ作成ツールというのは、例えばドット絵エディタやマップエディタのことです。「センメツコースター」ではドット絵エディタとしてAseprite、ドット絵のアニメーションデータエディタとしてDarkFunctionEditor、マップエディタとしてTiledMapEditorを利用し、それぞれのファイルローダーを実装しました。*6
まとめ
以上が「センメツコースター」を作るためにやったことです。もちろん実際にはもっとたくさんのことをやったのですが、C++とOpenGLでゲームを作るという意味ではだいたいカバーできていると思います。ただ、さらっと書いてますがいろいろな場面で苦労は多いです。例えばOpenGLで板ポリを出すだけでも相当つまづくポイントが多いです。ですが、この記事で紹介しているようなことを実装できた時点でもう自分だけのゲームエンジンが出来ているようなものです。あとは自分の好きなようにゲームエンジンを強化してくだけです。楽しい!でも、ゲームエンジンばかり作り込んでしまいゲームが完成しないということには注意しましょう。おすすめなのは実際にゲームを作りながら必要になった機能だけゲームエンジンに追加していくことです。
リンク集
- QtCreator : https://www.qt.io/ide/
- C++標準関数クラスリファレンス : http://en.cppreference.com/w/
- GLFW : http://www.glfw.org/
- OpenGLES 2.0ドキュメント : https://www.khronos.org/registry/OpenGL-Refpages/es2.0/
- OpenALリファレンス日本語訳 : http://www.memorize-being.net/releases/oal11spec-ja/
- ALURE : http://kcat.strangesoft.net/alure.html
- PicoJson : https://github.com/kazuho/picojson
- Lua : http://www.lua.org/home.html
- Luaリファレンス日本語訳 : http://milkpot.sakura.ne.jp/lua/lua52_manual_ja.html