次のゲームを作り始める
今メインでプレイしているゲームはDSのドラクエIXです。まだレベル35程度でストーリ中盤くらいでしょうか。パーティ編成がしっくりこなくて転職を繰り返してます。やり込むつもりはないですが、ストーリクリアするまでは続けるつもりです。
全然関係ない話でした。
次作るゲームは何か
前回SFMLを使うことに決めました。公式サイトにはチュートリアルが用意されているのでぽつぽつ読んでました。どうも次に作るゲームにどう使っていけばいいのかいまいちイメージできません。というかそもそも次に何を作るかも決まっていません。とりあえず何作るか決めて、必要になったら参考にするという進め方に変えようと思います。候補としては、
- 2Dシューティングゲーム
- マリオみたいなアクションゲーム
- RPG
なんかを作ってみたいです。2Dシューティングはゲームプログラミング入門の定番ではないかと思います。マリオみたいなアクションゲームはプラットフォームゲームというらしいです。あまり実際にそう呼ぶ人に会ったことないですが。RPGは初代ドラクエみたいなのイメージしてます。どれもファミコンとかスーパーファミコンくらいのプレイ感が得られるものを想定しています。もしかして発想が古いかもしれません。あくまで目的はC++プログラミングの練習ということで、ゲームデザインの練習が目的ではないので完成をイメージしやすくするため既存のゲームを真似て作ってみることします。
前回みたいに適当にとりあえず動くだけで終了、ではあまり面白くないので最低ラインを設けます。
- もし必要なら絵と音は自前で用意する。
- プレイ開始からゲーム終了まで一通り回るようにする。
- ひととおりプレイしてプレイした感が得られる程度のクオリティを目指す。
プログラミングが目的なので絵と音を自前で用意する意味はなく先に言ってたことと矛盾しているように思えるかもしれません。しかし、自分でやらないとあまり作った気になれず達成感を得られないので自前で用意することにします。とにかく一人でゲームを作るにはモチベーションを維持することが一番大事です。フォントは既存のものを使ってもそんなにモチベーションが下がりそうでもないのでフリーのものを使う予定です。とはいえ、ちょっとハードルを高くしすぎたかもしれません。絵を描くソフトウェアの使い方はあまり知らないので覚えないといけないし、録音機材とか何もない状態なのでそこから考えないといけないです。まあなんとかなるでしょう!
土台の部分を考える
何を作るかだいたい決まりましたので早速作り始めることにします。まずゲームの土台の部分について少し考えておこうと思います。
土台とは何を言おうとしているかと言うと、ウィンドウの初期化だとか、フレームレートの制御だとか、ゲームのロジックよりももっとずっと下にある部分のことの話をしようとしています。仮にゲームのプログラムを実行したとき次のような手続きが取られるかもしれません。
初期化する...
メインループに入る...
後片付けする...
さらにメインループに入ってみて、ここもある程度決まった手続きが取られているのではないかと考えてみます。例えば次のようになっているかもしれません。
ループ:
入力を処理する...
ゲームを更新する...
描画する...
こういった処理はゲームによって調整が必要ではあるでしょうが、だいたい似たような感じになるのではないかと考えてます。ただ、現実のゲームプログラムのソースコードを見たことがあるわけではないので確かなことは言えません。実際のものがこうなっているというのではなく、これくらい単純だったらいいのにという願望です。C++に書き直してみると、
void main_loop() {
while (1) {
process_input();
update();
render();
}
}
int main() {
initialize();
main_loop();
cleanup();
}
これもこのくらい単純だったらいいのにという願望です。ちょっと引っかかっているのは、この土台の部分をどの程度作り込むべきかということです。言い換えるとそれぞれの関数はどの程度の規模になって、どの程度複雑な処理を行うものになるかということです。
何か参考になるところはないかと思い、本棚から「ゲームコーディング・コンプリート」という本を引っ張り出してきてちょうど「ゲームの初期化と終了処理」という章と「メインループの制御」という章があったので読んでみました。しかし、この本はあまり入門向けとは言えるものではありませんでした。もう少し本格的なゲームを作るようになってから参考にしたほうが良さそうです。今の段階でこの本の水準まで持っていく必要ないでしょう。ちなみにこの本自体はものすごくいい本です。2010年に出た本で古いのですが新しい技術が知りたいのでなければ今でも十分読めます。WindowsのPCしかターゲットにしていなくてDirectXべったりのコードになってるのでうまく変換して読み進めないといけないです。そのくらいの作業が出来ないと読み進めるのも難しいくらいの難易度です。
ゲームエンジンを使った場合と比べて、一からから作り始めるとき最初に迷うのは技術的な部分よりもこの土台の部分の設計ではないではないかと思います。SFMLというライブラリを使うことにはしたものの、どのようなコードを書くかはまだほぼ自由にする余地が残されていて、何ならメインループを持たないような設計にすることもできるはずです。しかしそのような設計は今回採用しません。どのような設計が可能かもあまり検討することもしません。ただ単によく使われているだろうという理由で上のコードのようなもの(パターンと言っていいかもしれません)をベースにしていきます。方針としては、上のような単純なコードを基本として必要になったものをとにかく中に詰め込んでいく、というやり方で進めようと思います。必要と判断したものを詰め込んでいくので中には不足していたり余計だったり、不適切だったりすることもあるでしょうが、何も指針がないよりは作りやすいかと思うのでそのような方針で進めることにします。
ゲームの状態をどう表現するか、どのように管理するか
上のmain関数について考えてみます。
int main() {
initialize();
main_loop();
cleanup();
}
main関数がこの通りできるようにinitialize()とmain_loop()とcleanup()を作ろうとすると厄介なことがあります。initialize関数はゲーム全体のシステムを初期化するのでしょうが、その初期化した状態というのはどこで管理すればよいのでしょうか。ゲーム本体であるmain_loop関数ではおそらくそのゲームのシステムの状態にアクセスする必要があると想像できます。clenap関数が後片付けするのはそのシステムの状態を使うはずです。つまり、initialize関数はゲームのシステムを表現する何らかのオブジェクトを生成して、変数として保存して、後に続く関数に受け渡しができるようにしないといけないということです。関数をまたいで変数を受け渡しする方法は、関数の引数と戻り値を使うか、グローバル変数を使うかのどちらかが考えられます。
まず引数として使う方を考えてみます。
struct Game { ... };
Game *initialize();
void main_loop(Game *game);
void cleanup(Game *game);
int main() {
Game *game = initialize();
main_loop(game);
cleanup(game);
}
これだけ見るとそんなに悪くなさそうですがこれで終わりではないです。ゲーム内に存在する全てのものがGameにアクセスできる必要がある可能性があります。例えば、敵機が自機のところに向かって弾を打つために自機の位置を知らないといけないので、Gameに自機の位置を教えてくれるよう要求するかもしれません。ただ、ちょっとこれは飛躍し過ぎで、ゲームのシステムとゲームプレイの部分に一枚層(レイヤー)を挟んで直接ゲームプレイの状態をGameに管理させるのではなくすればいいでしょう。ともかく、コードの関数のあらゆるところにfoo(Game *game)という風にGameが引数に入り込んでくることになりかねません。メインループの方は次のような感じになってしまうでしょう。
void process_input(Game *game);
void update(Game *game);
void render(Game *game);
void main_loop(Game *game) {
process_input(game);
update(game);
render(game);
}
このコードにどういう印象を受けるでしょうか…実際のところCで書かれたライブラリの関数、APIではこのようなスタイルで書かれているものが多くあります。例えばCの標準ライブラリにあるf〜で始まるファイル入出力の関数なんかがそんな感じだし、LuaのAPIもそうだし、あともう一つ例として MySQLのAPIもそんな感じです。ここでgameのようなオブジェクトはハンドルと呼ばれることがあります(呼ばれないこともあります)。
あらゆる関数呼び出しでハンドルとなる変数を引数として渡すのは、もしCのf~やlua_〜やmysql_〜のようにライブラリのインターフェイスであるならならそんなに違和感ないように感じられます。ライブラリの機能を利用しているという意識が働くし、覚えやすいです。しかし、今作ろうとしているのはライブラリではありません。ゲームのアプリケーションです。アプリケーションのコードに含まれる関数ほぼすべての第一引数をgameとするなど冗長すぎます。そんなことをするよりかはまだグローバル変数にしたほうが良いでしょう。グローバル変数にすると次のようなコードが書けます。
struct Game { ... };
static Game *g_game;
void initialize() {
g_game = new Game;
// setup game...
// ...
}
void main_loop() {
// use g_game
}
void cleanup() {
// cleanup with g_game
// ...
delete g_game;
}
int main() {
initialize();
main_loop();
cleanup();
}
g_game変数にstaticキーワードをつけているところですが、グローバルのスコープ(あるいは名前空間のスコープ)、要は大体関数の外側でstaticをつけて宣言された変数は、内部リンケージ(interanl linkage)になり、その翻訳単位からしかアクセスできなくなります。どういうことが適当にいうと、このg_game変数がgame.cppに書かれているとすると、それ以外のcppファイルからはアクセスできなくなるということです。
例を挙げてみます。
a.cppというファイルがあるとします。
static int ok = 42;
int bad = 0xBAD;
int main() {}
もう一つb.cppというファイルがあるとします。
static int ok = 42;
int bad = 0xBAD;
この2つをコンパイルのみ行ってみます。リンクはしません。
g++ -c a.cpp b.cpp
ls -lt
合計 16
-rw-r--r-- 1 userx userx 984 2月 13 18:08 b.o
-rw-r--r-- 1 userx userx 1280 2月 13 18:08 a.o
-rw-r--r-- 1 userx userx 51 2月 13 18:05 a.cpp
-rw-r--r-- 1 userx userx 38 2月 13 18:02 b.cpp
コンパイルは通りました。出来たa.oとb.oをリンクしてみます。するとエラーになりました。
g++ a.o b.o
/usr/bin/ld: b.o:(.data+0x4): multiple definition of `bad'; a.o:(.data+0x4): first defined here
collect2: error: ld returned 1 exit status
エラーメッセージをよく読むと、b.oで、badが複数回定義されている、最初の定義はa.oにある、と書かれています。staticをつけたokの方はエラーになっていません。コードは省略しますが、badの宣言にstaticを付けるとリンクエラーは解消されます。逆に、a.cppとb.cppの両方共でokの宣言からstaticをはずすとokの方がリンクエラーになります。staticと大体反対の意味になるextermというのもあります。グローバル変数でstaticを省略した場合いくつかの例外がありますがデフォルトでexternになり、外部リンケージ(external linkage)を持つことになります。リンケージについては こことかここの解説が分かりやすいかもしれません。
グローバル変数を使えないかどうかを考えようとすると、まともなコードではグローバル変数は使ってはいけないし、そのような考えを持つことさえ恥じるべきだという偏執的な固定観念がひょっとして邪魔をするかもしれません。確かに何も考えずになんでもグローバル変数にしてしまうのは非常にまずいです。しかし、staticを使って内部リンケージの変数と宣言されたグローバル変数はうまく使えばそんなに悪いものではないのではないか、という考えを持っています。ただ、その手順は面倒で注意深く構成しないといけないので積極的に使いたいとはあまり思いません。
先のコードに戻ってg_gameに不用意にアクセスできないように少しファイルの構成を作り変えてみます。
game.h
void initialize();
void main_loop();
void cleanup();
game.cpp
struct Game { ... };
static Game *g_game;
void initialize() {
g_game = new Game;
// setup game...
// ...
}
void main_loop() {
// use g_game
}
void cleanup() {
// cleanup with g_game
// ...
delete g_game;
}
main.cpp
#include "game.h"
int main() {
initialize();
main_loop();
cleanup();
}
適当に書いただけなのでこれがうまくいくかどうか分かりません。ポイントはゲームの状態を保持するg_gameを外側からアクセスできないように保護して、外部に公開された関数をインターフェイスとしてg_gameの情報にアクセスするとという考え方です。言ってみればソースファイル単位でモジュールを構築することを目標としたものです。CもC++も直接モジュールをサポートする言語の機能はありません。しかしC++20ではmoduleが追加されるようです。
結局この方法を使うかどうかというと使わないです。もしC++ではなくCで書いていたなら採用していたかもしれません。
C++ではクラスを使うことが出来ます。クラスを使えばグローバル変数を使わずにゲームの状態を保持させたままでメンバ関数でその変数にアクセスすることが出来ます。関数の引数に冗長なgameという変数を渡す必要もありません。
game.hを次のように書きます。game.cppにはそのメンバ関数の処理を書きますが省略します。
class Game {
public:
Game();
void initialize();
void main_loop();
void cleanup();
private:
// メンバ変数にゲームの状態を保持させる...
};
main関数を書くmain.cppは次のようにします。
#include "game.h"
int main() {
Game game;
game.initialize();
game.main_loop();
game.cleanup();
}
長々と書いてきて割に出来上がったものは割とありきたりな特に目新しいものではありませんでした。暇な時よくこの部分のコードをどうすると良いのだろうかなどと考えることがあるのですが、良いアイデアが浮かびません。