数々の3Dゲームで利用されるゲームエンジン
ゲーム開発の世界では、他のプログラミング分野に比べて、ごく初期の段階からプラットフォームを正しく特定しておくことが特に重要です。Windows、Linux、およびOS Xをすべてサポートしたいとお考えですか?
実際、ゲームプラットフォームの草分けとして、1996年にOpenGL向けに開発された革命的なゲーム「
Quake」を引き合いに出す人も多いでしょう。しかし、Quake並みのゲームプレイ水準に達するためには、第一級のオーディオサポート、ネットワーク接続、ユーザー入力デバイスサポート、リアルタイム管理機能なども必要になります。
この2つの課題、つまりクロスプラットフォームサポートとゲームの面白さの両方を追及するために役立つソリューションが
Simple DirectMedia Layer(SDL)です。これは、オーディオ、キーボード、マウス、ジョイスティック、OpenGL、および2Dビデオフレームバッファに対する低レベルのアクセスを可能にする、クロスプラットフォームのマルチメディアライブラリです。
SDLは、Linux、Windows、各種のMacOS、WinCE、Dreamcastなど、およそ考え得るほぼすべてのプラットフォームをサポートしています。SDLはGNU LGPL v2で配布されているため、提供されるDLLにリンクしさえすれば、商用プログラムでも自由に利用できます。SDLはMPEGプレイヤーやハードウェアエミュレータのほか、数多くの人気ゲームでも利用されています(有名なLinux用の「
Civilization: Call To Power」もその1つです)。
SDLはCで書かれていますが、C++でもネイティブに動作し、他の言語にも対応しています(Ada、Eiffel、Java、Lua、ML、Perl、PHP、Pike、Python、Rubyなど)。
SDLの活躍範囲は幅広く、私の大好きなオープンソースのフライトシミュレータ「
GL-117」でも、SDLがエンジンとして使われています。現在、SDLのホームページには、SDLエンジンを利用しているゲームが568本も登録されています(うち450本はWin32実行可能ファイルをビルドできます)。
簡単なサンプル:エイリアン来襲!
ゲームエンジンのことを知るには、サンプルコードを見るのが一番です。今回は、Sam Lantingaが1980年代の「
スペースインベーダー」を模して作成したゲームを取り上げたいと思います。その名もシンプルに「
aliens(エイリアン)」です。
なんと、このゲームのソースコードは全体で560行しかありません。すべてのコードを紹介したいのはやまやまですが、少々長くなりすぎるので、最も重要な部分だけに(それこそレーザービーム級の密度で!)焦点を当て、それ以外の部分は割愛します。本稿のコードリストではaliens.cでの行番号を併記しますので、Visual Studioで実際にコードを見るときの目安にしてください。
最初に説明しておくと、このゲームでは、全体の動作を実現するために
SDL_mixerと
SDL_imageという別々の2つのSDLプロジェクトのコードを利用しています。
SDL_mixerは、SDLアプリケーション用の、プラットフォーム非依存のサウンドミキシングライブラリプラグインです。このプラグインを利用すると、ミキシングアルゴリズムを書かなくても、音楽と一緒に複数のサンプルを再生できます。たとえば、砲撃音をBGMにシームレスにミックスすることができます。また、さまざまなファイル形式のサンプルや音楽の読み込み、および再生を容易に行うことができます。
SDL_imageは、SDLアプリケーション用のプラットフォーム非依存のグラフィック読み込みプラグインです。これにより、BMP、PNM(PPM/PGM/PBM)、XPM、LBM、PCX、GIF、JPEG、PNG、TGA、およびTIFFファイル形式をアプリケーションに読み込むことができます。
以降で紹介するコードを実際に試してみたい場合は、この2つのパッケージをダウンロードしておいてください。
main()関数
当然ながら、実際のアクションは次に示すmain()関数から始まります。
524行目では、
SDL_Init()を使用してオーディオとビデオのサブシステムを初期化します。他に使用できるオプションとしては、ジョイスティック、タイマー、CD-ROMアクセスを有効にするものや、イベントマネージャを独立したスレッドとして実行するためのものがあります。
531行目では、Mix_OpenAudio()を呼び出すことでSDL_Mixerマネージャの使用を開始します。この例では、古い低速のCPUを考慮して低品質のオーディオ(11Khz、8ビット符号なしサンプル、モノラル、512バイトバッファ)を使用しています。
次に、
SDL_SetVideoMode()を呼び出すことで、デバイスに合った適切なbpsを使用して640×480ディスプレイ用にビデオを開きます。このとき、システムメモリバッファを使用し、全画面表示(ウィンドウを画面全体に表示するモード)にするよう指定しています。この他に使用できるオプションとしては、ダブルバッファリング(アニメーションを滑らかにします)、OpenGLコンテキスト、サイズ変更可能ウィンドウ、非同期再描画などがあります。
521 main(int argc, char *argv[])
522 {
523 /* Initialize the SDL library */
524 if ( SDL_Init(SDL_INIT_AUDIO|SDL_INIT_VIDEO) < 0 ) {
525 fprintf(stderr, "Couldn't initialize SDL: %s¥n",
SDL_GetError());
526 exit(2);
527 }
528 atexit(SDL_Quit);
529
530 /* Open the audio device */
531 if ( Mix_OpenAudio(11025, AUDIO_U8, 1, 512) < 0 ) {
532 fprintf(stderr,
533 "Warning: Couldn't set 11025 Hz 8-bit audio¥n-
Reason: %s¥n",
534 SDL_GetError());
535 }
536
537 /* Open the display device */
538 screen = SDL_SetVideoMode(640, 480, 0,
SDL_SWSURFACE|SDL_FULLSCREEN);
539 if ( screen == NULL ) {
540 fprintf(stderr, "Couldn't set 640x480 video mode: %s¥n",
541 SDL_GetError());
542 exit(2);
543 }
544
545 /* Initialize the random number generator */
546 srand(time(NULL));
547
548 /* Load the music and artwork */
549 if ( LoadData() ) {
550 /* Run the game */
551 RunGame();
552
553 /* Free the music and artwork */
554 FreeData();
555 }
556
557 /* Quit */
558 Mix_CloseAudio();
559 exit(0);
560 }
RunGame()関数
メインのロジックループは、もちろんすべてRunGame()内にあります。この関数は約200行から成るので、全体を一度に見るには長すぎます。そこで、SDLの機能を説明しながら、いくつかの重要なセクションを取り上げることにします。コードを省略した部分は、コメント中の省略記号(...)で示します。
301 void RunGame(void)
302 {
303 int i, j;
304 SDL_Event event;
305 Uint8 *keys;
306
307 /* Paint the background */
308 numupdates = 0;
309 for ( i=0; i<screen->w; i += background->w ) {
310 SDL_Rect dst;
311
312 dst.x = i;
313 dst.y = 0;
314 dst.w = background->w;
315 dst.h = background->h;
316 SDL_BlitSurface(background, NULL, screen, &dst);
317 }
318 SDL_UpdateRect(screen, 0, 0, 0, 0);
最初のセクション(301〜318行目)では、
SDL_BlitSurface()を使用して背景色のスライスをスクリーンバッファに順次blit転送し、
SDL_UpdateRect()を使用して再描画をセットアップしているだけです。
319
320 /* Initialize the objects */
321 // . . .
333 CreateAlien();
334 DrawObject(&aliens[0]);
335 UpdateScreen();
336
337 while ( player.alive ) {
338 /* Wait for the next frame */
339 WaitFrame();
340
341 /* Poll input queue, run keyboard loop */
342 while ( SDL_PollEvent(&event) ) {
343 if ( event.type == SDL_QUIT )
344 return;
345 }
346 keys = SDL_GetKeyState(NULL);
347
348 /* Erase everything from the screen */
349 // . . .
366 /* Decrement the lifetime of the explosions */
367 // . . .
373 /* Create new aliens */
374 if ( (rand()%ALIEN_ODDS) == 0 ) {
375 CreateAlien();
376 }
377
378 /* Create new shots */
379 if ( ! reloading ) {
380 if ( keys[SDLK_SPACE] == SDL_PRESSED ) {
381 for ( i=0; i<MAX_SHOTS; ++i ) {
382 if ( ! shots[i].alive ) {
383 break;
384 }
385 }
386 if ( i != MAX_SHOTS ) {
387 shots[i].x = player.x +
388 (player.image->w-shots[i].image->w)/2;
389 shots[i].y = player.y -
390 shots[i].image->h;
391 shots[i].alive = 1;
392 Mix_PlayChannel(SHOT_WAV,
393 sounds[SHOT_WAV], 0);
394 }
395 }
396 }
これは、RunGame()ループ内の入力管理を行う部分です。
一見シンプルに見えるSDL_PollEvent()は、イベントに関する情報を含んだ
SDL_Eventオブジェクトを返します。ここでSDL_QUITという特殊な値を使用して、ESCキーが押されているかどうかを確認します。
これは純粋にキーボード操作だけでプレイするゲームなので、
SDL_GetKeyState()を使用すれば必要な情報をすべて入手できます。PCのキーボードは複数のキーを同時に押せるよう設計されているので、結果は配列として返されます。この配列ではSDLK_SPACEのようなキーボードコード値がインデックスとして使われます(SDLK_SPACEはスペースバーを表します)。
397 reloading = (keys[SDLK_SPACE] == SDL_PRESSED);
398
399 /* Move the player */
400 //. . .
415 /* Move the aliens and the shots */
416 // . . .
443
444 /* Detect collisions */
445 for ( j=0; j<MAX_SHOTS; ++j ) {
446 for ( i=0; i<MAX_ALIENS; ++i ) {
447 if ( shots[j].alive && aliens[i].alive &&
448 Collide(&shots[j], &aliens[i]) ) {
449 aliens[i].alive = 0;
450 explosions[i].x = aliens[i].x;
451 explosions[i].y = aliens[i].y;
452 explosions[i].alive = EXPLODE_TIME;
453 Mix_PlayChannel(EXPLODE_WAV,
454 sounds[EXPLODE_WAV], 0);
455 shots[j].alive = 0;
456 break;
457 }
458 }
459 }
ここではMix_PlayChannel()に注目してください。これを使用することで、適切な状況で砲撃音を追加します。
このゲームでは3つのチャンネルを同時に使用し、音楽、砲撃音、爆発音をそれぞれ独立したチャンネルで扱います。なぜなら、そのときどきの状況によって、最大3つのサウンドを同時に再生する必要があるからです。独立したチャンネルを用意しておかないと、あるサウンドから別のサウンドへの切り替えをひっきりなしに行うことになり、サウンドトラックがぶつ切れになってしまいます。
話が前後しますが、これらのサウンドは次のような呼び出しで読み込んでおきます。
127 sounds[MUSIC_WAV] = Mix_LoadWAV(DATAFILE("music.wav"));
それでは、コードの続きに戻りましょう。
474 // . . .
475 /* Draw the aliens, shots, player, and explosions */
476 for ( i=0; i<MAX_ALIENS; ++i ) {
477 if ( aliens[i].alive ) {
478 DrawObject(&aliens[i]);
479 }
480 }
481 // . . .
494 UpdateScreen();
495
496 /* Loop the music */
498 if ( ! Mix_PlayingMusic() ) {
499 Mix_PlayMusic(music, 0);
500 }
506
507 /* Check for keyboard abort */
508 if ( keys[SDLK_ESCAPE] == SDL_PRESSED ) {
509 player.alive = 0;
510 }
511 }
512
513 /* Wait for the player to finish exploding */
514 while ( Mix_Playing(EXPLODE_WAV) ) {
515 WaitFrame();
516 }
517 Mix_HaltChannel(-1);
518 return;
519 }
520
本稿では、すべての処理について細かく説明したわけではありません。たとえば特に触れませんでしたが、UpdateScreen()は、表示するビットマップのリストを反復処理して画面へのblit転送を行う関数です。これらのコードの詳細については、ソースコードを見てください。
まとめ
500行あまりのコードで実装された昔懐かしい2Dシューティングゲームに音楽とリアルタイムプレイを組み合わせただけですが、SDLの素晴らしさはわかっていただけたのではないでしょうか。もちろん、この同じコードから、WindowsやLinuxをはじめ、さまざまなプラットフォームで実行可能なアプリケーションをコンパイルできます。本稿では、SDLの表面を少し触ってみただけにすぎません。興味のある方は、ぜひ実際に使ってみてください。