ゲームプログラミング
出典: フリー教科書『ウィキブックス(Wikibooks)』
目次 |
[編集] 概観
[編集] ゲームとは
ゲームと一言でいっても、その言葉を使う人によってその意味は様々ですが、ここでは特にコンピュータを利用したゲーム(コンピュータゲーム)について述べます。コンピュータを利用したゲームといっても、ここでいうゲームという言葉の意味はそれほど限定されたわけではありません。なぜなら、コンピュータは文字、映像、音楽を含めた様々な情報を扱うことができ、一般的にコンピュータを利用しないで行うことができる事柄も、コンピュータを利用した上で、行うことができる場合があるからです。
例えば、トランプを利用して行うゲームはいくつか有名なものが知られています。例としてソリティア、大貧民(大富豪とも呼ばれる)、ポーカー、ブラックジャックなどがあげられます。これらのうちのいくつか(おそらく大半が)実際にコンピュータ上のゲームとして作られています。例えば、ブラックジャック、ソリティアは、gnome-gamesや各種Windowsのアクセサリに含まれています。
もちろんゲームと名の付くあらゆる事柄がコンピュータ上で再現できるかといえば、それは今のところSFの領域に含まれる事柄です。例えば、スポーツの試合をゲーム化することは頻繁に行われていますが、それらはあくまで選手の行動を動作として簡略化し、それを利用することでゲーム化したものです。選手の挙動の全てをゲーム内で実行させるには、現在使われているよりもはるかに高速な計算機が必要です。最も選手の挙動の全てを再現することはゲームを作る上で重要ではないし、特に必要でもない事には注意が必要です。
結局、コンピュータ上で扱えるゲームがどんなものであるかは、その言葉を使う人によっても、また技術の発展具合によってもある程度ばらつきがあることがわかります。ここでは家庭用ゲーム機か、家庭用のコンピュータで扱える範囲の事柄をここでいうゲームとし、それらについて扱います。
[編集] ゲームプログラミングとは
ゲームだけでなくコンピュータ上で行う活動については、いろいろな場面でプログラミングが必要になります。プログラミングは、コンピュータの動作を定める活動のことで、コンピュータに自分が行わせたい活動をさせるために必要になります。
コンピュータに行わせたい活動といっても、文書作成をコンピュータに行わせたいために、エディタを利用することは、通常プログラミングとは呼びません。ただし、エディタを利用しているといっても、例えばエディタのメニューを利用することはコンピュータに指示を出していることに他ならないため、このような呼び名の違いはある程度歴史的なものと考えることもできそうです。ここでは一応プログラミング言語を利用する活動をプログラミングと呼びます。ここでいうプログラミングとは、何らかのパーサを利用して、コンピュータに指示を出す機構の事を指します。プログラミングやパーサについては、高等学校情報Bなどを参照してください。
ゲームを書くために利用される言語は多岐にわたっています。歴史的にはC言語や、特に計算機のスピードが重要になる場面ではアセンブラを利用してプログラミングを行うことが普通に行われていました。現在では計算機がある程度速くなったことや、ゲームプログラムの開発を複数人で行うことでテクニカルなプログラミングが避けられるようになったことにより、ゲームプログラミングは普通のプログラミングに近いスキルとなっています。しかし、特にアクションゲームなどのリアルタイムでの画面書き換えが必要なゲームで、プログラムのスピードが重視されることは変わっていません。また、コンピュータの性能があがるにつれ、それらの性能を全て引き出すように表現手段が変化してきたため(3D、ポリゴンなどを参照)、状況によっては複雑なプログラミングが必要になることもあります。
[編集] コンピュータゲームの種類
コンピュータゲームはいくつかの種類に分けられます。ここでは代表的なものについてまとめます。残念ながら、ゲームの分類の仕方には正式なものがないため、ここでの分類はある程度筆者の主観によっています(落ちものゲームはパズルゲームの一部とするか、など)。
- パズルゲーム
- パズルは一般的なパズルと同じ意味の言葉で、何らかの目的が提示され、ゲームのルールに従ってその目的を達成することを目指します。
- 代表的な例はマインスイーパ、倉庫番などがあげられます。全体的に素早い操作よりも思考力が問われる内容が多く、低速なコンピュータでも動作するものが多いと言えます。そのため、高度なプログラミング技術よりもゲームを考案するアイディアが重要になり、古いゲームが長く利用され続ける分野でもあります。ただし、有名なゲームでは既に誰かが作成していることが普通であるため、現在のゲームメーカが扱うことはあまりありません。
- 落ちものゲーム
- 画面上方から何かが落下し、それを操作して何らかの目的を果たすゲームです(広義にはパズルゲームに含まれます)。
- 代表的なものに、テトリス(メーカー複数)、ぷよぷよ(セガ)、ドクターマリオ(任天堂)などがあります。
- これらはそれぞれルールが異なっており、プログラムの手法もある程度異なってきます。ただし、(ブロックなどの)落下の部分だけは共通です(ちなみに落下してくるのは順に、ブロック、ぷよ、カプセルです)。
- シューティングゲーム
- ある程度自由に動かせるプレイヤーを操作し、敵を倒すことを目的としたゲームです。
- プレイヤーは多くの場合飛行することができ、移動はプレイヤーがそのように飛んでいるものと解釈されます。敵を倒すために多くの場合プレイヤーは前方に飛ぶ弾を打つことができます。一方、敵がプレイヤーにあたった場合にはプレイヤーはダメージを受けることが普通です。ただし、バリアのような設定がある場合、もしくは敵の弾を受けたときにダメージがあり、敵との衝突によってはダメージを受けないという設定を置く場合もあります。コンピュータ上の表現として2Dを用いたものと3Dを用いたものがあり、これらはコンピュータの計算量にかなり違いが出るため、時期によってどちらが採用されるかが(ある程度)決まります。
- 2Dの代表的な例としてグラディウス、ツインビー(コナミ)、3Dの例としてエースコンバット(ナムコ)、スターフォックス64(任天堂)などがあります。
- アクションゲーム
- こちらもプレイヤーをある程度自由に動かし、敵を倒していきます。シューティングゲームとの違いは、プレイヤーは多くの場合人型や動物などの生物の形をした何かであり、また重力の影響である程度以上の上方には移動できないことが多いことがあげられます(落下の概念があることも多いです)。ただし、プレイヤー自身が空を飛ぶことができる場合があるので、必ずというわけではありません。また、ロールプレイングゲームのようにプレイヤーが成長する要素を加えたものも多く、その場合にはアクションRPGなどと呼ばれることもあります。アクションゲームにも2Dと3Dの表現があり、3Dの表現ではコンピュータの性能に対する要求が高い場合が多く、通常の家庭用コンピュータでは動作しない場合があります(Windows Vista動作PCのように高機能なコンピュータの場合には別です)。
- 代表的な例として、2Dでは、スーパーマリオブラザーズ(任天堂)、魔界村(カプコン)、忍者龍剣伝(テクモ)、3Dではスーパーマリオ64(任天堂)、デビルメイクライ(カプコン)などがあります。
- ロールプレイングゲーム
- ロールプレイングゲームは多くの場合RPGと省略され、非常に多くのゲームが発売されている分野です。プレイヤーはゲーム中のキャラクターを操作し、ゲームの物語を体験します。多くの場合途中には敵との戦いがあり、その中ではRPGの他の部分と異なった操作が使われます。多くの場合プレイヤーがキャラクターが行う行動を選択し、それをプレイヤーが操作するキャラクターが実行するという仕方で戦闘が進みます。しかし、アクションゲームのようにキャラクターを操作する場合もあるため、一概にいうことはできません。
- 代表的な例として、ドラゴンクエスト、ファイナルファンタジー(スクウェア・エニックス)、ポケットモンスター(任天堂)などがあります。
- スポーツゲーム
- 何らかのスポーツの動作を簡略化してプレイヤーが操作できる形にし、それによってそのスポーツを実践させます。
- 代表的な例として、みんなのゴルフ(ソニー・コンピュータエンタテインメント)、ウイニングイレブン、実況パワフルプロ野球(コナミ)などがあります。
- ボードゲーム
- 何らかのボードゲームをゲーム化したものですが、既に知られているボードゲームを単にゲーム化した場合もありますが、ゲーム独自のボードゲームである場合もあります。
- 代表的な例として、いただきストリート(スクウェア・エニックス)、桃太郎電鉄(ハドソン)、マリオパーティ(任天堂)などがあります。
- その他
- その他にも多数の様々なオリジナルのジャンルがあります。
- 完全に既存のジャンルと異なる独自のもの、複数のジャンルの組み合わせ(複合)などがあります。ただし、ゲームによっては既存のジャンルに分類されることもあります。
[編集] 実際のゲームプログラミング
どのようなゲームを作るときでも、ゲームプログラミングでは、
- プログラミング言語を扱うことができる。
- ゲームが提示する内容を表示することができる。
- プレイヤーからの入力を扱うことができる。
などの技術が必要になります。
プログラミング言語とプレイヤーからの入力については歴史的にもあまり変化がありません。プログラミング言語ではC言語、C++などが用いられます。ただし、携帯電話向けのゲームではJavaが利用されますが、これは携帯電話を提供する各社がJavaをアプリケーションの言語として選んだことによります(iアプリ、EZアプリ、S!アプリなどを参照)。
プレイヤーからの入力にはコンピュータでは通常キーボードかマウスを利用します。他にジョイスティックやゲームパッドが利用される場合もあります。家庭用ゲーム機ではコントローラが利用されることが多いのですが、ニンテンドーDSではタッチパネル、Wiiでは複数の入力機器が提供されることが発表されています。幸いにもそれらをプログラムから扱う手法はそれほど入力機器ごとにそれほど大きく変化することはありません(プログラムから周辺機器を扱う方法については、デバイスドライバ、高等学校情報Bなどを参照)。
残念なことに画面を表示することのうちで3Dの表現は割合難しく、ある程度の数学を理解することが必要になります。2Dに関してはプログラムの面ではさほど難しい部分はありません。しかし、特に最近のゲームでは非常に多くの画像データが必要となるため、それらを用意することは困難です。最も画像データの作成については、プログラムによって解決できることではないのでここでは扱いません。
[編集] プログラミング言語
ゲームプログラミングに限らず、プログラミングを行うには、プログラミング言語を習得することが必要になります。プログラミング言語は計算機に対する指示をパーサを利用して出すことを目的とした計算機プログラムで、それに関する研究は計算機科学の一分野をなしています。本来計算機言語を利用する前には、計算機の仕組みについて学んでおくことが本道です。しかし、幸いにも計算機そのものについて詳しく知らなくとも、プログラミングを行うことはある程度可能です。
現在よく利用されているプログラミング言語として、C言語、C++、Javaがあげられます。これらの言語の詳細については、対応する項を参照してください。ここでは主にC言語とC++を紹介します。
[編集] 入力機器からの入力
[編集] プラットフォームごとの入力の差異
入力機器は、利用者からの入力を計算機に与えるものを指します。代表的なものには、キーボードとマウスがあげられます。これらの機器に入力を行うと、計算機のCPUに割り込み(ハードウェア割り込み)を行い、計算機はこれらの機器から何らかの入力が行われたことを知ります。実際には機器から送られてくる信号は入力の種類によって異なるため、入力機器と計算機に共通の意味づけを持った信号を使うことで、計算機は実際に利用者によって行われた入力を知ることができます。
キーボードやマウスからの入力はPC/AT互換機と呼ばれる種類のコンピュータでは統一されており、計算機はその仕様に従って信号を解釈します。これらの入力の仕様についてはOADG[1]の文献を参照してください。
最近のコンピュータではこのような入力機器からの入力はOSによって扱われます。これは入力機器からの信号を直接扱うよりも、人間にとってわかりやすい形にしてから入力を提示した方が、利用者にとって便利であることによります。
残念なことに、OSが入力機器からの入力を利用者に提示する方法は、OSごとに異なっています。例えば、UNIX系のOSでは入力機器からの信号を利用するために、デバイスファイルを利用します(UNIX/Linux入門も参照)。一方、Windowsでは、デバイスファイルは存在せず、別の方法を使います。
ゲームの開発ではこれらの非互換性が厄介な問題になります。これは、各OSごとに入力を扱う方法を変えなくてはいけないからです。実際にはこの問題はいくつかの方法で回避されます。1つの方法としてプラットフォームによらない入力を提供するJavaのような言語を利用することができます。もしくは、プラットフォームごとの入力の差異を吸収するようなライブラリを利用することです。C言語を利用する場合にはこのようなライブラリはいくつか知られています。代表的な例ではGTK+、Qt、SDLがあげられます。これらはどれもWindows, Mac OS X, Linuxなどをサポートしており、これらに共通の入力を与えます。複数のプラットフォームで動作するようなプログラムを書きたいときにはこのようなライブラリを利用することを考えるとよいでしょう。
[編集] 入力の例
[編集] C言語の関数を利用した例
非常に簡単な入力は、C言語のgetchar関数などを利用して処理することができます。例えば、次の例は入力した文字をそのまま表示します。
- 注意
- このプログラムは2.6系列のLinux上で実行できることを確認していますが、他のプラットフォームでは異なった振舞いをするかもしれません。
#include <stdio.h> int main(){ int c; c = getchar(); printf ("あなたは%cと入力しました。\n", c); return 0; }
このプログラムの実行結果は次の様になります。これはgetcharの時点で'g'と入力した場合です。
$./a.out g あなたはgと入力しました。
この方法は簡単ですがいくつか欠点があります。まず、getchar関数が実行された時点でこのプログラムは一旦停止して利用者が入力を行うのを待ちます。この性質は、例えばアクションゲームでは利用者の入力を待つためにゲームの動きが止まってしまうことに対応しており、厄介な問題です。また、上の出力からはわからないのですが、上の例ではgを入力した後にEnterキーを押して入力が終了したことを計算機に知らせています。これは例えばキャラクターを動かす時に常にEnterキーで確定する必要があることに対応しています。通常のゲームではこのような事は起こらないので、このことも問題です。
UNIX系のシステムでは(Linuxはこれに含まれる)これらを回避するための方法としてtcsetattr関数を使った方法が知られています。この方法を説明することもできるのですが、この方法には1つ厄介な問題があります。tcsetattr関数でカノニカルモードを取り止め、c_cc配列のMINとTIMEを0に設定します。この時上の2つの問題は解決され、Enterキーを押すことなく入力が受け付けられる上、プログラムが利用者の入力を待つこともありません。しかし、上の方法では、ShiftキーやCtrlキーなどの特殊なキーが押されたことを読み取ることができません。また、矢印キーを押したときの結果はおそらくシステムごとに異なった結果を与えます。結局この方法では数字キーと英字キー以外の入力についてはうまく扱うことができません。一方計算機上のゲームでは矢印キーを利用する例も多いので、これは問題です(例えば、SuperTux、gnibblesなど)。
実際には上であげたゲームはそれぞれSDL、GTK+というライブラリを使っているので、どのように入力を得ているのかを見るのは困難です。Linuxの場合について結果を述べると、上のライブラリはLinux上では、どちらもX Window Systemを利用して入力を得ています。Linux上のXはShiftキーなどの入力を扱うために、プログラム中でioctlコールを利用してデバイスファイル(ここではttyファイル)の性質を変更しています。実際にはLinux上でのtcsetattr関数もioctlコールの1つとして実装されているため、このioctl命令を使うことでtcsetattr関数を使った場合よりも多くの事ができます[2][3]。
これらのライブラリはそれぞれのプラットフォーム上で全てのキーを利用する手立てを提供しています。大抵のゲームではこれらのライブラリのいずれかを利用しているので、ここではそれらを用いて入力を扱うことにします。個々のライブラリの入力の扱いかたについては対応するドキュメントを参照してください。
[編集] 2Dの画面出力
画面出力の場合にも入力機器の場合と同じで、これらを操作する方法はOSごとに異なっています。先ほどあげたGTK+, Qt, SDLなどのライブラリはクロスプラットフォームの画面出力を提供しているため、これらを利用することで全てのプラットフォームで動くプログラムを作ることができます。
[編集] 具体的な例:ブロックくずし
ここまでで2次元の描画と入力機器の扱いかたについて述べてきました。ここではこれらを使った具体的な例として、ブロックくずしをあげます。
ブロック崩しは有名なゲームですが、一応内容について説明します。このゲームでは、プレイヤーはバットと呼ばれる板状のものを操作します。バットは画面下方を左右に移動することができます。プレイヤーは画面上方に向けてバットからボールを発射します。ボールは画面の端で跳ね返り、やがて画面下方に戻ってきます。この時ボールが画面下方に向けてバットよりも下に行かないように、プレイヤーはバットでボールを打ち返します。画面上方にはボールを当てることで壊れるブロックが複数配置されており、プレイヤーは破壊できるブロックを全て破壊することを目的とします。
[編集] 図
実際にこのことをプログラムとして書こうとするとおおよそ1,000行程度のCプログラムになるようです。ここでは既に完成している例として、gnome-breakoutを利用し、2Dゲームのプログラミングとして代表的な部分について解説していきます。ここでは特に、gnome-breakout-0.5.3を利用しました。ここからの解説ではこれらのコードを参照することが多いので、ゲームのソースを入手しておくとよいでしょう。一般的なソースの読み方については、OSS開発ツールなどを参照してください。
前提として、gnome-breakoutではゲームのライブラリとしてGNOMEを利用しています。GTK+はGnomeのGUIツールキットなので多くの描画はGTK+の関数を利用してなされます。GTK+の関数についてはGnomeのサイトかOSS開発ツール GUIツールキットを参照してください。
[編集] メインループ
このゲームはボールやバットの操作を行うため、定期的に画面の書き換えを行います。画面の書き換えは繰り返し実行されますが、この繰り返される部分をメインループと呼ぶことがあります。メインループは一部のパズルゲームを除くあらゆるゲームで用いられる技術です。基本的にはメインループは、
while(state == RUNNING){ ...; }
のように無限ループを使って書かれます。RUNNINGは大抵の場合ゲームの状況を表すenum(列挙子)で、
typedef enum { RUNNING, STOPPED } state;
などと定義されます。今回のゲームに関しては、メインループはsrc/game.c内のiterate_gameで定義されています。ただし、ここで使っているソースは見やすいように編集してあります。
while (game->state == STATE_RUNNING) { // 中略 iterate_bat(game); iterate_balls(game); iterate_powerups(game); iterate_blocks(game); // 中略 gui_update_game(game); // 中略 gettimeofday(&end_tv, &tz); diff_t = ((start_tv.tv_sec - end_tv.tv_sec) * USEC_PER_SEC) + (start_tv.tv_usec - end_tv.tv_usec) + USEC_PER_FRAME; if (diff_t > 0) usleep(diff_t); }
STATE_RUNNINGは、src/breakout.hで定義されたenumです。この例はいくつかの重要な技法を含んでいるので、ここで紹介します。
[編集] 物体ごとの処理
上の例ではwhileループ(メインループ)内でiterate_xxxで書かれるいくつかの関数が実行されていますが、この関数名は自己を説明しています。これらの関数はそれぞれbatやballなど対応するデータの移動や衝突判定などを行います。実はiterate_blocksではブロックが破壊される際のアニメーションを行っているのですが、ここでは詳しく述べません。いずれにせよメインループ内で実行される関数はゲーム内で対応するキャラクターなどを持っており、それらに操作をするための関数である場合がほとんどです。オブジェクト指向を使ってプログラムを書くなら、上の例は、
bat->iterate(game); balls->iterate(game); ...
などとなるでしょう。この場合おそらくbat, ballなどのクラスはiterateをvirtual(C++の時)なメソッドとして持つクラスを継承します。
[編集] 画面の書き換え
上の例は、gui_update_gameという関数が続いています。この関数はいくつかの処理をしますが、ここで注目する距離は、関数の最後の処理で
// 中略 gnome_canvas_update_now(gui->canvas); return; }
の部分です。gnome_canvas_update_nowはgnomeが提供する関数で、ゲームの描画を行う領域全体を書き換えます。上のiterate_xxx内でボールの移動などの処理は既に実行されているのですが、ここで変更したのはあくまでボールの位置を表す数値であり、それだけで画面の表示は変更されません。そのため、画面の変更をこの部分で一律に行っていることを示しています。
実際には例えば、iterate_ballの中で、
old_pos = ball->pos; move_ball(ball); update_rect(old_pos, ball_width, ball_height); update_rect(ball->pos, ball_width, ball_height);
のように画面の書き換えを行うこともできます。ここで、update_rectは、
update_rect(pos, width, height);
で表され、posで表される場所から、 (width, height) で表される長方形で囲われる領域の描画を行う関数としました。実際には画面の描画は計算機に取って手間のかかる作業で、扱う領域が広いほど画面の再描画には多くの時間がかかります。そのため、上の例では2回の書き換えを行う必要があることを考えても、上の書き換えの方が高速で行われるでしょう。ただし、扱う画面が小さいなら書き換えは十分速く行われると期待されるので、どのように書き換えを行うかは状況によります。
[編集] fpsの管理
fpsはframe per secondの略で、ゲームの時間制御の要となる概念です。計算機だけでなく、テレビもそうですが、これらの機器は1秒間に複数回の画面の書き換えを行います。例えば、日本のテレビではおおよそ30回/秒の書き換えを行います。ただし、テレビの書き換えはインターレース方式で行われるので、実質的に60回/秒の書き換えを行うことになります。詳しくはテレビを参照してください。ここで、画面の書き換えは例えば1/60秒で行われるとすると、これは非常に短い時間に見えます。しかし、例えば現在の計算機のクロック周波数が1GHZ程度とすると、計算機が1度の計算を行うのにかかる時間は、
秒です。これは画面の書き換えを行うのに必要な時間よりはるかに速いため、何も工夫をしなければ、計算機は画面の書き換えより速く物体の動作などの処理をしてしまいます。これは例えば、描画が行われないままボールが多くの距離を移動するなどの事につながるため、何らかの対策を講ずる必要があります。
ここで用いる方法は、メインループ1回分の処理を終えた後、次の画面の書き換えが行われるまで、プログラムの実行を止めておくという方法です。この方法には、プログラムの実行を一時的に止めるusleep関数を使います。
usleep(int microseconds);
usleep関数はmicrosecondsで表されるマイクロ秒だけ、プログラムの実行を止めます。実際にどの程度の間実行を止めるかは、プログラムが処理にかかった時間と、その結果を画面に送るのにかかった時間の和がどの程度だったかによります。上の例ではdiff_tが実際に実行を止める時間に対応します。gettimeofdayは、現在の時刻をマイクロ秒単位で取得する関数です。最後のgettimeofdayではend_tvの名の通り、処理が終わった時刻を取得しています。実際には上の例でiterate_xxxが行われる前に、gettimeofdayでstart_tvを取得している場面があり、これら2つを用いて処理にかかった時刻を得ることができます。画面の書き換えを1秒に実際に行う回数をfpsと呼びます。ここでは書かれていないのですが、このゲームは50fpsで動いているので、各々の書き換えの間に、20ミリ秒=20000マイクロ秒が経過します。処理にかかった時間をnマイクロ秒とすると、
diff_t = 20000 - n
で実行を止める時間が計算されます。上の例では何故か -n + 20000となっていますが意味は同じです。
ここからはballやbatなどそれぞれの処理を見ていきます。batの処理では、キーボードやマウスからの入力を受け取る方法について説明します。
[編集] バットの処理
このゲームではキーボードかマウスのどちらかを使ってバットの操作をします。ここではまず、キーボードやマウスの入力を読み取る方法について述べます。
バットの動作の処理はおおよそ次のようになっています。
if (key_right_is_pressed){ if (bat_x + bat_width + bat_move < GAME_WIDTH) bat_x += bat_move; } else if (key_left_is_pressed){ if (bat_x - bat_move > 0) bat_x -= bat_move; }
ここで、それぞれの変数は
bat_x: バットのx座標 (0 < bat_x < GAME_WIDTH) GAME_WIDTH: 扱うウィンドウの幅 bat_move: 1度の処理でバットが動く距離
を表します。ここで、key_right_is_pressedとkey_left_is_pressedはキーの状態を表す変数です。ここでキーボードを扱う関数はこれらの変数の値を操作する必要があります。これらの変数はsrc/game.c内のkey_right(left)_pressedとkey_right(left)_releasedの4つで変更されています。それぞれの関数はおおよそ
void key_right_pressed(Game *game){ right_ispressed = TRUE; }
のように対応する変数の中身を書き換えています。
- 注意
- 実際のコードでは、上でいうbat_moveの値に正負の値を取らせてバットの移動の処理を簡略化しています。また、右キーと左キーの両方が押された場合の処理も行っています。ここでは、簡単のためispressedの変数だけを扱います。
- 注意
- C++と違い、C言語ではboolean型が存在しないため、TRUEやFALSEは普通のC言語プログラムでは扱われません。ここでのTRUEは、GLibで与えられているマクロであり、値は
- FALSE
- (0)
- TRUE
- (!FALSE)
- です。
上で紹介した4つの関数は、GTK+の関数にコールバック関数として渡されます。ただし、GTK+はkey_press_eventとkey_release_eventの2つの操作を与え、押されたキーごとに操作を与えるわけではありません。そのため、2つのイベントに対して1つずつのコールバックを与え、コールバック関数内でどのキーが押されたかを判断する必要があります。実際にコールバックとして扱われる関数は,src/gui-callbacks.c内のcb_keyupとcb_keydownです。これらはほとんど同じ処理を行うので、cb_keydownだけを紹介します。
gint cb_keydown (GtkWidget *widget, GdkEventKey *event, gpointer data) { // 中略 if(event->keyval == game->flags->left_key) { key_left_pressed(game); } else if(event->keyval == game->flags->right_key) { key_right_pressed(game); } // 中略
まずGTK+はキーボードからの入力をイベントとして扱います。イベントは機器からの入力などを一般化したもので、その多くはGTK+が依存しているライブラリから提供されます。例えばlinux上ではキーボードからの入力に関するイベントは対応するX Window Systemの処理から与えられ、Mac OS X上ではquartzの処理から与えられます。これらのイベントの詳細はGTK+の立場から見ることはできないので、詳細を見るには、対応するライブラリの説明を参照してください(Xプログラミングも参照)。GTK+からはあるウィジェットにフォーカスがある時に、何らかのキーが押されたときには、そのウィジェットに対して"key_press_event"が与えられます。このとき、このイベントに対応するコールバック関数には、イベントが発行されたウィジェットに加えて、GdkEventKey型の引数が与えられます。この型は押されたキーの種類などの情報を含んでいる構造体で、この引数を使って押されたキーを判別することができます。
上のcb_keydownも"key_press_event"のコールバックとして与えられる関数で、その引数は順に
gint cb_keydown (GtkWidet *, GdkEventKey *, void *)
となっています。ここで最初のGtkWidget *と、GdkEventKey *は上で述べたイベントが与えられたウィジェットと押されたキーの詳細を与える情報に対応します。最後のvoid * = gpointerは他に与えたい引数がある時に使うことができる変数ですが、ここではGame型のgameという引数を与えています。これはkey_left(right)_pressedの引数で、バットが動く方向などを記録しておく変数です。
ここまででキーが押されたときに実行させる関数を定義しました。次に実際にこの関数をキーが押された時の関数として登録する処理を見ます。この処理はsrc/gui.cのgui_init内で行われ、対応する部分は
// 中略 g_signal_connect(GTK_OBJECT (gui->app), "key_press_event", GTK_SIGNAL_FUNC (cb_keydown), gui); g_signal_connect(GTK_OBJECT (gui->app), "key_release_event", GTK_SIGNAL_FUNC (cb_keyup), gui); // 中略
です。g_signal_connectはGTK+で"イベント"に対するコールバックを登録する一般的な関数です。この関数の引数は、
g_signal_connect(GtkWidget *widget, char *event_name, GCallback cb_func, gpointer data);
で与えられ、それぞれイベントを扱わせたいウィジェット、ウィジェットが持つイベントのうちで対応するイベントを選ぶための文字列、コールバック関数、 コールバック関数に与えるデータとなっています。
- 注意
- 最初の引数は実際にはGtkWidget *である必要は無く、GObjectを継承したクラスのポインタならなんでも可能です。
ここで、GTK+のkey_press_eventとkey_release_eventが現れるタイミングについて説明します。key_press_eventとkey_release_eventはそれぞれキーが押され始めた瞬間と、キーが放された瞬間に与えられます。あるキーを押しつづけた場合key_press_eventが現れ続けるのではないため、キーが押しつづけられているのを知るためには、先ほど述べたkey_is_pressedなどの変数の値を設定しておき、key_release_eventが現れたときにこの値を書き換える必要があります。
[編集] ボールの動作
ボールはバットと違い、プレイヤーによる操作を受けないので、その意味ではボールの動作は単純です。しかし、このゲームではボールとバット、ボールとブロック等の間の衝突の判定を全てiterate_balls内で行っているため、iterate_ballsは割合複雑です。
iterate_ballsは、ball.cで定義されています。この処理はおおよそ
void iterate_balls(){ move_ball(); ball_block_collision(); ball_bat_collision(); ball_wall_collision() }
となっています。ここではこれらの処理を順に見て行きます。一般に、物体を動作させその後に他の物体との衝突判定を行うという一連の手順はシューティングゲームやアクションゲームでは必ず現れる手順です。
[編集] ボールの移動
このゲームではボールの特性を表す構造体として、Ball (src/breakout.h) が定義されており、その内容は、およそ
typedef struct { double x1, y1, x2, y2, speed, direction } Ball;
で与えられます。
- 注意
- 実際にはこれ以外にもいくつかの要素が定義されていますが、ここではこれらだけを使います。
x1, y1, x2, y2はそれぞれボールが含まれる長方形の左下と右上の座標を指します。これはボールに限らず、物体の絵を保存する際には一般的に、長方形の領域が用いられることによります。speedとdirectionはそれぞれボールが動く速度とその方向を表します。x方向の速度をv_x, y方向の速度をv_yと表すなら、それぞれ
v_x = speed * cos(direction); v_y = speed * sin(direction);
が成り立ちます。ここまでの結果を用いるとmove_ballの動作はほぼあきらかで、その操作はおよそ
void move_ball(Ball *ball){ double v_x, v_y; v_x = speed * cos(direction); v_y = speed * sin(direction); ball->x1 += v_x; ball->x2 += v_x; ball->y1 += v_x; ball->y2 += v_x; }
となります。
[編集] 一般的な衝突判定
ほとんどの場合物体と物体の衝突はそれぞれの物体を囲む長方形が重なることを用いて検出されます。このとき、物体を囲む長方形はできる限り物体の大きさと等しい大きさにしておくことが重要です。ここでは、ともにx1,y1,x2,y2を持つaとbという2つの物体があるとき、それらの衝突を判定する方法についてまとめます。実際に、このゲームでもボールとバットの衝突判定に、この手順が用いられています。
- 注意
- ボールとブロックの衝突判定はブロックが動かないことを利用した簡単化された手順を用いています。
まずx座標とy座標は互いに関係なく動かせるので、x座標だけについて考えます。この場合問題は2つの2次不等式a.x1 < x < a.x2,b.x1 < x < b.x2で表される領域xが存在するかどうかを確かめる問題に帰着します。2次不等式については高等学校数学Iを参照してください。
まず、a、bの各値が並ぶ順番が何通りあるかを計算します。a、bはそれぞれ2つのx座標を持っていますが、aの中での大小関係、bの中での大小関係は定まっており、常にx1 < x2です。そのため、例えばababのような並びを書けば、その順序は自動的にa.x1 < b.x1 < a.x2 < b.x2と定まります。このことを考えれば、a、bの値は単にa, a, b, bの4つの文字を並びかえる場合の数と等しいことが分かります。場合の数については高等学校数学Aを参照してください。
実際に取り得る値を書き並べると、
aabb, abab, abba, baab, baba, bbaa
の6つがあげられ、全部で6つであることがわかります。この結果は計算法としては、互いに区別できない要素を含んだ順列の計算に対応しており、その場合には
に対応します。詳しくは高等学校数学Aを参照してください。
実際にはこれらの並びのうち4つが衝突が起こっている場合に対応します。具体的には4つの図が対応します。これらを一律に検出するには、例えば次のような試験が用いられます。
(b.x1 < a.x1 && a.x1< b.x2)||(b.x1 < a.x2 && a.x2 < b.x2)||(a.x1 < b.x1 && b.x2 < a.x2)
src/collision.cのcheck_collisionでは、これと同じ手順が書かれています。
上の操作をx座標とy座標について行えば、衝突の判定は完了します。
[編集] ボールと壁、ブロックの衝突判定
ボールや他の物体(ここではブロック)との衝突が確認されたら、次にボールがどちらの方向からぶつかったかを確認します。この確認にはsrc/collision.cのfind_hit_side関数が使われます。この関数では、まずボールの速度と方向を使って、ボールが移動する前の位置まで戻します。ここでボールの位置とブロックの位置を調べることで、ボールがどちらの方向からぶつかってきたかを知ることができます。
実際に物体が衝突したときどちらの方向へ反射するかはsrc/collision.cのrecalculate_ball_trajectoryで判定されます。仮にボールの速度をvxとvyで記録させるなら、右、左からの衝突では
vx *= -1;
上、下からの衝突では
vy *= -1;
で結果が得られることに注意が必要です。今回は速度と方向で記録しているため、各方向毎の場合わけが必要です。
ブロック以外でゲームで利用するウィンドウの端にある壁と衝突するときにも上の関数は利用されます。壁との衝突判定は一般的な衝突判定より単純で
- ball.x<0 ではボールが左の壁に衝突した
- ball.x>GAME_WIDTH では、ボールが右の壁に衝突した
などとなります。
- 注意
- 実際のコード中では、実際には壁と壁の継ぎ目に衝突した場合の判定も行っています。
[編集] ボールとバットの衝突判定
ボールとバットの衝突は、src/collision.c内のball_bat_collisionで扱われます。この場合、ボールが反射する方向はバットとの位置関係で変化するようになっており、(おそらく)上級者はこれを用いてボールを狙った方向に送ることができます。ここでは詳しくは扱いません。
[編集] 最後に
ここまでで簡単なブロック崩しゲームに必要な手法について述べました。実際にはこのゲームではパワーアップの概念やレベル毎のブロックの配置の違いなど説明していないいくつかの要素があります。それらも調べてみるとよいでしょう。
[編集] テトリス:gnometris
テトリスは代表的な落ち物ゲームで、非常に有名なゲームです。いくつかのフリーな実装があるのですが、ここではgnome-gamesの実装であるgnometrisを利用します。
[編集] アクション:xjump
xjumpはROYALPANDA氏によって書かれた簡単なアクションゲームです。xjumpはXlibを用いて書かれており、Windows上でコンパイルするのはやや難しいのですが、同じ種類のゲームでsdljumpというゲームもあるので、ビルドが困難と感じたらそちらを用いるとよいでしょう。
ここではアクションゲームを作るうえで基本的になる重力やジャンプの扱いについて解説します。
[編集] 3Dプログラミング
3DのプログラミングにはDirectXやOpenGLなどのAPIが利用されます。詳しくはOpenGLなどを参照してください。
[編集] カメラの配置
3Dゲームを作るときにはカメラの動作を決める必要があります。2Dの表現ではキャラクタを上から見る、斜め上から見るなどの表現がなされますが、それらはどれも場面をある1方向から見る表現でした。一方、3Dで場面を作るときには、それらはあらゆる方向から見ることができるので、見る人の視点によって描画される内容を変更する必要があります。
簡単な表現では、カメラの位置はプレイヤーが動かすキャラクタの場所に固定します。このとき、プレイヤーキャラクタが画面内に映らなくなる表現とカメラをプレイヤーキャラクタの後方に配置して、プレイヤーキャラクタを画面内に含める表現があります。前者の表現はFPS(First-Person shooting: 1人称シューティング)でよく用いられます。
基本的に3Dでの描画を行うには、それぞれの物体に対して座標を与え、それを適切な角度から2Dへの射影を行うことでなされていました。このとき、3Dの描画にOpenGLをつかうとすると、各物体の座標の与え方にいくつかの制限が加わります。例えば座標の原点は常にカメラの位置に固定される、画面上方向は常にy座標とする、などです。
幸いにもgluLookAt関数を利用することで、この制限を乗り越えることができます。gluLookAt関数は、カメラの位置、カメラが向いている方向、上方向の3つの3次元ベクトルを用いて指定する関数で、カメラの位置を変更するのと同じ効果をもたらす行列を作成し、現在選択されている行列にかけます。通常の状態ではカメラの位置(0,0,0)、カメラが向いている方向(0,0,-1),上方向(0,1,0)となり、このときかけ算される行列は単位行列に等しくなります。 gluLookAtの行列は次で与えられます。
まず、
で表される行列で、座標の方向を調節します。ただし、
,
,
,
, 
を用います。更に、カメラの位置を動かすために、
glTranslated(-eyex, -eyey, -eyez);
を実行します。
ここでは、上の変換のうちで座標の方向を変更する行列について説明します。この行列の導出にはいくつかの前提が必要となります。まず、変換後の座標系ではカメラは-z方向を向いています。またこのとき、画面上方向は必ずy方向になります。これらはOpenGLの座標の取り方から来る制限です。ここではもう1つの制限として、gluLookAtで得られる行列が直交行列であることを仮定します。直交行列の各行と列は、お互いに直交する単位ベクトルとなり、直交行列の逆行列は元の行列の転置行列になります。詳しくは線型代数学を参照してください。
行列を求める方針として、
を
に変換し、
を
に変換するような行列を探します。このような行列をPとおくと、仮定からPは直交行列なので、
より、
が成り立ちます。最後の式は具体的に計算できて、
- (p21,p22,p23) = (u1,u2,u3)
が得られます。同様にして、
- (p31,p32,p33) = ( − f1, − f2, − f3)
が得られ、求めたい行列のうち6つの要素が得られたことになります。Pが直交行列という仮定を用いると、(p11,p12,p13)は、
に直交する必要があります。このようなベクトルは
に比例する必要があります。ここでは右手系の座標系を保つために、
と取ります。これで元の変換行列が得られたことになります。
プレイヤーが動かすキャラクタの位置と向いている方向を表す構造体をcameraとし、その値を次のようにおくと(cはcamera型の変数で、この変数がプレイヤーキャラクタの位置と方向を保持しているものとします)、
typedef struct { double x,y,z,dx,dy,dz; } camera; camera c;
カメラの位置をプレイヤーキャラクタの位置に変更する方法は次のようになります。
void set_camera(){ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluLookAt(c.x,c.y,c.z,c.x+c.dx,c.y+c.dy,c.z+c.dz,0,0,1); }
ただし、ここでは最後の引数(0,0,1)で、上方向がz方向であると定義しました。
上の変換では、カメラの位置を変更したのですが、これだけだと変更された後の位置を中心として、長さ2で表される立方体の中の物体しか描画されません。これはもともと投影行列が単位行列だったときには、原点を中心として長さ2の立方体内の物体しか描画されないことと対応しています。より遠方の物体を描画するには、glOrtho, glFrustumの両関数を利用することができます。一般的なゲームでは"遠方のものは小さく見える"といった表現がなされるので、ここではglFrustumを用います。glFrustumの引数は状況によりますが、x,y方向に関係する引数を大きく取ると視界が広くなり、z方向の座標を大きく取ると遠くのものまで見えるようになります。ここでは
glFrustum(left,right,bottom,top,near,far);
をgluLookAtの1つ前にいれます。
実際にこの関数を使ってプレイヤーキャラクタが動く様子を書くことができます。処理の様子は、
int main(){ init_gl(); init_camera(); while(1){ set_camera(); draw_scene(); swap_buffers(); check_fps(); } return 0; }
のようになります。set_camera以外の関数は説明していないので、以降で説明します。
- init_glは、OpenGLの初期化を行う関数です。この関数は使っている環境によりますが、glutInit (GLUT) , SDL_SetVideoMode (SDL) などが対応する関数です。
- init_cameraは、プレイヤーキャラクタの位置を設定する関数です。cをグローバル変数としておけば、camera型の変数を、引数として渡す必要は無くなります。
- set_cameraは先程説明した関数です。ここではカメラの位置は定数なのですが、後にカメラの位置を動かす場合も扱うので、ループ内に入れました。
- draw_sceneは画面に現れる物体を描画する部分です。ここではいくつかの三角錐を配置しました。
- swap_buffersは、glが描画した内容を画面に表示する関数です。glutSwapBuffers (GLUT) , SDL_SwapBuffers (SDL) などが用いられます。
- check_fpsは、処理にかかった時間を計算し、画面の書き換えのタイミングと次の処理のタイミングが合うようにする関数です。usleep(環境依存), SDL_Delay (SDL) などが用いられます。
次の図は、カメラの位置を動かしながら描画を行ったものです。元々遠くに見えていたものが近づくにつれて大きくなる様子がわかります。
具体的には画面中に見える白い物体はそれぞれ三角錐で、中心の座標は(0,0,0),(3,1,0),(3,4,3),(5,-2,1)となっています。更にカメラの位置は(10,0,1)であり、カメラが向いている方向はいずれの場合も-x方向です。
ここで、init_cameraとdraw_sceneの内容を紹介します。init_cameraは、カメラの位置と方向を定める構造体cに、初期値を与える関数です。ここでは次のようにしています。
static void init_camera(){ c.x = 10; c.y = 0; c.z = 1; c.dx = -1; c.dy = 0; c.dz = 0; }
単にカメラの座標を(10,0,1)とし、方向を-x方向に定めているだけです。ここでz座標が0でないのは、FPSでカメラの位置がキャラクタの顔の辺りにおかれることを意識したものです。c.zを0とすると地面を這うような表現になります。
draw_sceneは、次のような関数です。
static void draw_scene(){ glClear(GL_COLOR_BUFFER_BIT); watch_from_camera(); draw_cone(0,0,0); draw_cone(3,1,0); draw_cone(3,4,3); draw_cone(5,-2,1); }
glClearは、画面の表示をクリアする関数です。watch_from_cameraとdraw_coneはそれぞれ次のように与えています。
static void watch_from(double x, double y, double z, double dx, double dy, double dz){ glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(-1,1,-1,1,0.9,50); gluLookAt(x,y,z, x + dx,y+dy,z+dz, 0,0,1); } static void watch_from_camera (){ watch_from(c.x, c.y, c.z, c.dx, c.dy, c.dz); }
ここで、watch_fromが視点変換を行う関数の本体であり、watch_from_cameraは、引数を与えることを目的とした関数です。ここでは
- カメラがただ1つであり
- その名前はcであり
- それはグローバル変数である
という仮定をおいています。更に、draw_coneは、
#define A (glVertex3f(x,y,z)); #define B (glVertex3f(x+0.5,y,z)); #define C (glVertex3f(x,y+0.5,z)); #define D (glVertex3f(x,y,z+0.5)); static void draw_cone(double x, double y, double z){ glBegin(GL_TRIANGLES); A B C; A B D; B C D; A C D; glEnd(); } #undef A #undef B #undef C #undef D
としています。頂点と面を指定して多角形を書くときには、頂点と面のそれぞれをGLfloat型と、GLint型の2次元配列を使う方が普通です(例えばRed Book3章)。ここでは簡単のためマクロを使いました。
ここまででカメラの位置を指定する方法を解説しました。ここからはカメラの位置を機器の入力を受け付けて変更する方法について述べます。
実際には入力を受けてカメラの位置をどのように動かすかは、例をアクションゲームに限っても、個々のゲームによって変化します。例えば、
- 常にプレイヤーキャラクタの後ろにつく[1]
- プレイヤーキャラクタの位置とカメラの位置を別に用意し、それぞれを別に動かせるようにする[2]
- プレイヤーキャラクタの後ろにつくが、キャラクタが反転したときや移動したときの動作に自由度を残す(即座にキャラクタの後ろに回るわけでないので、キャラクタの正面が見えることがある)[3]
- 場面毎にカメラの位置を固定する[4]
ここでは簡単のため、1番目の選択肢である"プレイヤーキャラクタの後ろにつく"を採用します。これは、カメラの位置とキャラクタの位置が同一であるか、簡単な変換で導出できる関係にあるためです。
既に2Dのゲームプログラミングを通して、何度か入力機器の取扱いについて見て来ました。ここでは入力機器を扱う方法としてSDLを使います。SDLは入力機器の初期化と、2D,3Dの描画を扱うための簡単なライブラリで、多くの(主に個人製作の)ゲームで利用されています。
SDLでは機器の入力はイベントとして扱われます。イベントを供給する元はシステムによって様々ですが、X(Linux他)や、DirectInput (Windows) が用いられます。SDLがこれらのシステムからイベントを得ている方法を見るには、SDL-x.x.x/src/video以下の各ディレクトリを見ることが必要です。ここでは詳細を追求せずに、SDLの関数を使うことにします。
実際にイベントの監視を行うには、メインループの中で、
while(SDL_PollEvent(&e)) process_event(&e);
などとします。ここで、eはSDL_Event型の変数で、main関数内で定義します。実際にイベントを扱うのはprocess_event関数で行います。SDLが扱うイベントは、キーボード、マウス、ジョイスティックなどの入力機器からの要請の他に、スクリーンからのexposeイベントやresizeイベントがあります。これらは使っていたウィンドウが他のウィンドウで隠された時や、扱うウィンドウの大きさを変更したときに供給されるイベントです。これらの様々なイベントを扱うため、SDL_Event構造体には、
Uint8 type;
という要素が含まれています。これは各々のイベントの種類を表す要素で、process_event内ではこの値に従って処理を分ける必要があります。具体的には
static void process_event(SDL_Event *e){ switch(e->type){ case(SDL_KEYDOWN): cb_keydown((SDL_KeyboardEvent *)e); break; case(SDL_KEYUP): cb_keyup((SDL_KeyboardEvent *)e); break; default: break; } }
cb_keydown(up)の関数では実際に押された(離された)キーを取得します。このときにはSDL_KeyboardEvent内の、
e->keysym.sym
要素を利用します。詳しくは、SDLのインストール先から、SDL/SDL_keyboard.hや、SDL/SDL_keysym.hなどを参照してください。cb_keydown(up)内での具体的な処理は対応するpressed関係の変数を書き換えることです。この処理は2Dの時の例と同じなので省略します。
ここまででキーが押されているかどうかを知ることができるようになりました。ここからは具体的なカメラの移動を見ていきます。ここではset_camera関数内で、カメラの移動を行います。カメラの位置と方向はc内に記録されているので、この関数ではpressed関係の変数の値を見て、カメラの方向を変更する処理が必要になります。
実際に行う処理は、カメラの位置を変更する処理と、カメラの方向を変更する処理に分かれます。ここではカメラの位置を変更する処理を先に扱います。
FPSでは、カメラとキャラクタの移動は、次のように行われます。
- "前"を選ぶとキャラクタはカメラが向いている方向に進む
- "後ろ"を選ぶとキャラクタはカメラが向いている方向と反対に進む
ここでは"前"を表すキーとして上キーを使い、"後ろ"を表すキーとして下キーを用います。具体的なset_cameraは次のようになります。
static void set_camera(){ if (up_is_pressed()){ c.x += a*c.dx; c.y += a*c.dy; } else if (down_is_pressed()){ c.x -= a*c.dx; c.y -= a*c.dy; } else if () // 回転のための処理 }
ここで、aはキャラクタの移動速度を調整するための定数です。実際には人間が地面を蹴って移動するとき人間が得るのは力積なので、キャラクタの移動では位置ではなくキャラクタの速度を変更するべきです。ここでは簡単のため移動にかかる時間は無視できるものとしました。力積については高等学校理科 物理Iを参照してください。
次に、キャラクタの方向を変える処理について説明します。ここではカメラの方向を3次元ベクトルで保存しているのですが、説明の都合上、方向を&thetaと&phiを使って表します。ここで、&thetaは、z軸方向とカメラの方向がなす角、&phiは、x軸方向と、カメラの方向ベクトルをxy平面に射影したベクトルがなす角とします。
通常キャラクタの方向を変更するときには&phiだけを変更します。ただし、キャラクタの視点に立って辺りを見回す表現(主観視点と呼ばれる)では、&thetaも含めて変更する必要があります。ここでは&phiだけを変更します。具体的には左回転をするときには、
phi += b;
右回転では
phi -= b;
とします。ここで、bはキャラクタの回転速度を表す定数です。
キャラクタの方向を表す3次元ベクトルと&theta, &phiは次の三角関数で結ばれています。
- nx = sin(θ)cos(φ),ny = sin(θ)sin(φ),nz = cos(θ)
逆の変換は
となります。これらの値を使って3次元ベクトルと方向を表す角の変換を行うことができます。
1度の回転の角度が十分小さいときには、
static void turn_left(){ c.dx -= m*c.dy; c.dy += m*c.dx; normalize(c); }
で左回転を、
static void turn_right(){ c.dx += m*c.dy; c.dy -= m*c.dx; normalize(); }
で右回転を表すこともできます。ここで、mは定数であり、normalize関数は、cの方向ベクトルを正規化する関数としました。これは、回転の角度が小さいとき、回転による方向ベクトルの変更を元のベクトルに直交するベクトルで近似できることを利用した変換です。方向角を使わないで回転を表現したい場合に利用するとよいでしょう。
これらの関数を用いると、set_camera内のif - else if文に、
} else if (left_is_pressed()){ turn_left(); } else if (right_is_pressed()){ turn_right(); }
を付け加えることになります。
[編集] 具体的な例:sdl_3d_jump
ここまでで3Dの座標を設定し、定義された物体をプレイヤーキャラクタの視点から観測する方法についてまとめました。例えば3Dのアクションゲームではこれらの手法に加えて重力を与えて、キャラクタが下向きに落下するようにしたり、キャラクタをジャンプさせるという作業が必要になります。しかし、これらの手法は基本的に2Dの場合と変わらないので、ここでは詳しく述べません。
実際には、上で述べたカメラの動かし方と、xjumpなどの2Dアクションの手法を用いて、3Dにおけるxjumpの対応物を作ることができます。ここでは、実際にそれを作成した場合について解説します。
xjumpの対応物を作るために最低限必要な要素は次のようになります。
- 床を作る
- キャラクタが空中にいるとき、キャラクタが落下するようにする
- 床の上にキャラクタがいるときには、キャラクタは落下しない
他に、本来のxjumpでは
- ジャンプに成功した枚数を表示する
- キャラクタの状態によって、キャラクタの画像が変化する
などいくつかの要素があります。ここでは簡単のため、
- 枚数は表示しない
- キャラクタは三角錐で代用
などとして進めます。
- 注意
- DirectXの場合と違い、OpenGLには3Dのデータを読み込むための関数は存在しません(DirectXにはXファイルと呼ばれるファイル形式がある)。後に、外部ライブラリを用いて3Dデータを読み込む手法について説明します。
ここからは先に述べた3つの要素について解説します。xjumpでは、ジャンプによって飛び移るための床は、乱数を用いて生成していました。ここでは簡単のため床の位置は定数の2次元配列で与えることにします。更に、床の大きさを固定することにすると、各々の床を得るために必要な情報は、床のうちの任意の1点です。ここではx座標とy座標が最も小さい部分とします。これらの情報は、例えば、
double planes [N_PLANES][3] = {{0,0,2},{4,0,4}};
のように与えることができます。
次に、キャラクタが落下する処理については、カメラの位置を動かす処理で、
c.z -= vz; vz -= g;
などとします。gはここでは下向きの加速度を表す定数です。これらの処理は2Dの場合と同じなので詳しく述べません。
最後に、キャラクタが床の上にいるかを判定するためには、次のような関数を使います。
static int on_the_ground(){ int i = 0; while (i < N_PLANE){ if ( (PL_X < planes[i][0] + 9.5) && (PL_X > planes [i][0] +0.5) &&(PL_Y < planes[i][1]+6.5) && (PL_Y > planes [i][1]-0.5) &&(PL_Z < planes [i][2]+1.0)&&(PL_Z > planes[i][2]-0.5) ) { return 1; } ++i; } ... }
if文で行っていることはキャラクタの位置(PL_X,PL_Y,PL_Zで与えられる)が各々の床の範囲内にあるかどうかを確かめる作業です。3Dの時の違いは、x座標とy座標の2つについて判定が必要になった点だけです。また、実際にキャラクタが床にいるときに、キャラクタのv_zを0にするなどの処理も行っていますが、2Dの時と変わらないので省略します。
これらの手順で作ったプログラムを仮に3d_xjumpと呼びます。実際に筆者がGLの初期化にSDLを利用してこれを作成してみたところ、300行程度で収まりました。ただし、ここではカメラに回転を行わせず、常にx座標正の方向からキャラクタを見る視点にしました。ここでスクリーンショットをいくつか載せます。

- 手前に見える三角錐を操って空中のボードに飛び移っていきます。上の方に多くのボードが見えています。

- 最初の1段に飛び乗った所です。画面中央に次に飛び移れそうなボードが見えています。

- ある程度進んでから撮ったショットです。画面奥に見える2枚のボードの間はかなりの距離があり、飛び移るのが難しい難所となっています。

- 先程見えていた2枚のボードの間に来た場面です。右端に見えているのがこれから飛び移るボードですが、距離を正確に読むのが難しいポイントです。

- コースの全景が見えるポイントから撮ったショットです。実際には画面中央上寄りに見えるボードから飛び降りながら撮りました。画面中央奥手に見える2枚のボードが先程の難所で、画面中最も左下に見えるボードが1枚目のボードです。
ここまででキャラクタやカメラの位置を動かす方法について説明しました。ここからは、より複雑な物体を表示する方法について述べます。ただし、複雑な物体を扱うときでもカメラやキャラクタの位置を動かす方法はこれまでと同様です。
[編集] 物体の表示
ここでは複雑な物体を表示する方法について説明します。既に、OpenGLで頂点を用いた物体の描画について説明しました。また、カメラ配置の例では、三角錐を作成しました。実際にはより複雑な物体を表示することが求められます。例えば後に取り上げるSuperTuxKartでは、カートに乗ったペンギンなどが画面に表示されます。また、それらのカートが走るコースも、表示する必要があります。
これらの描画を行うには、3Dのデータを作成するためのソフトウェアが必要となります。このソフトウェアはモデラと呼ばれ、3Dのゲーム開発をする上で重要なソフトウェアです。モデラ自体はゲームだけでなく、3Dのモデルを作成するときには常に利用されます。
フリーで使用できるモデラとしてはblenderが有名です。このソフトは、Windows, Mac OS, Linuxなど各種のプラットフォーム上で利用できるソフトウェアで、先程のSuperTuxKartでは実際に使用されているようです。ソフトの使い方はblenderなどを参照してください。
モデラを使って作成されたデータは、3Dデータとして保存されます。これは頂点の位置や、面を塗る色などを指定しており、これを読み取ることでモデラで作ったデータを画面上に表示することができます。
残念ながらこれらのデータには、標準化されたものがなく、データ形式はモデラごとに変化します。各種の形式を読み取るためにはそれらに対応するライブラリを使うのが簡単です。SuperTuxKartでは、PLIBを使っています。PLIBは、Windows, Mac OS, Linuxなどで動作する3Dプログラミングのためのライブラリです。SuperTuxKartではデータの受渡しに.acファイルを使っていますが、このファイル形式はblenderとPLIBの双方でサポートされています。
実際にはPLIBを利用して、カメラの配置など3Dプログラミングに必要な作業を行うこともできます。しかし、ここではPLIBの機能には深入りせず、与えられた3Dモデルのファイルを表示するプログラムを作成するに留めます。このプログラムは、PLIBのサンプルであるplib_example( )の、src/ssg/tux/tux_example.cxxとほぼ同じ内容です。
- 注意
- ここではOpenGLを用いるためにGLUTを利用しますが、PLIB自身もOpenGLの設定を行う機能を持っている様です。
#include <plib/ssg.h> #include <GL/glut.h> ssgRoot *root = NULL; static void display(){ glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); ssgCullAndDraw(root); glutSwapBuffers(); } int main(int argc, char **argv){ glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH); glutCreateWindow("aaa"); glEnable(GL_DEPTH_TEST); data_init(); camera_init(); glutDisplayFunc(display); glutMainLoop(); }
ここまでが、PLIBとGLUTを用いるときの基本的な描画を行う部分です。今までと違うのは、物体の描画にssgCullAndDraw関数を用いていることです。PLIBは3DモデルのデータをssgRootクラスを用いて管理します。ssgCullAndDraw関数は3Dモデルのデータを実際に描画する関数です。ここでは描画する3Dモデルに対するポインタをrootとしました。実際にデータをrootに与える部分はdata_init関数としており、次のように与えられます。
static void data_init(){ root = new ssgRoot; ssgEntity *snowman = ssgLoadAC("snowman.ac"); root->addKid(snowman); }
ただし、ここでは"snowman.ac"というACファイルが同じディレクトリ内にあるものとしました。ここまでで物体の描画は行われますが、物体が実際に見えていることを知るために、カメラの位置を動かす必要があります。その作業はcamera_init関数で行います。
static void camera_init(){ sgCoord campos; sgSetCoord(&campos, 0, -10,0,0,1,0); ssgSetCamera(&campos); }
ここではカメラの位置を(0,-10,0)とし、カメラの向きを(0,1,0)としました。何も設定しないとファイルから読み出された物体の位置は(0,0,0)となるので、これで設定は完了です。
ここで作成したacファイルは球と円柱だけを用いたものです。より複雑な物体を作るにはblenderなどを参照してください。
[編集] 具体的な例
[編集] glTron
[編集] Neverball
[編集] SuperTuxKart
http://supertuxkart.berlios.de/

![\begin{pmatrix}
s[0]&s[1]&s[2]&0\\
u[0]&u[1]&u[2]&0\\
-f[0]&-f[1]&-f[2]&0\\
0&0&0&1
\end{pmatrix}](http://upload.wikimedia.org/math/5/c/5/5c552b7d430acc3d863a20c1070be5bd.png)









