[タイトル][SDL][C言語実験室][Kanon専用DNMLビューアKDVXA][BM][ブロック崩し2000][マインスイーパー][ドリラー][ソロモンの鍵アナザー][CDプレイヤJoplay][Depth][Baloon Tripper][SDLバカ一代サポート][掲示板][リンク][日記][自己紹介]

 

Linuxでゲームを作ってみる?

せっかくマニュアルも日本語化されたことだし、この際Linuxでゲームを作ってみませんか?Simple DirectMedia Layerなら難しいことを勉強しなくても作れる上に、さまざまなプラットホームへ適用できて一石二鳥。と言うわけで、つらつらとゲームを作る手助けになるような事でも書いてみようかなと思ってみたり。使用言語はCです。なおここに書いてあることはSDLのドキュメントやサンプルソースを読めばすぐにわかるようなことですので、英語を読むのが好きな人やプログラムのイロハを知ってる人が見てもあまり意味ないかもしれません。また、技術的に未熟なため、とんでもないことしているかもしれません。そのときはぜひお知らせください。

ブロック崩し2000

なんともヘボヘボなゲーム名ですね。とりあえずスクリーンショットふたつ。

このようなチープなゲームを題材にSDLの関数の使用例を示していきたいと思います。

Windows版実行形式ファイル

8月3日版(bccのMakefile付き)

Linux版は少しお待ちください。

0.SDLインストール

ここに書いてあります。SDLのほかにCコンパイラが必要ですが、Linuxなら多分ディフォルトでついています。

1.SDLのイニシャライズとBMPの読みこみ及び表示(2000/2/25)

ブロック崩しを題材に解説していきたいと思います。いまさらなぜブロック崩し?という感じですが、まあ、私が作りたくなったからです。

まずSDLを使う準備をします。

int initVideo(void){
  if(SDL_Init(SDL_INIT_VIDEO)<0){
    fprintf(stderr,"couldn't initialize SDL:%s\n",SDL_GetError());
    return(-1);
  }
  atexit(SDL_Quit);
  screen=SDL_SetVideoMode(640,480,16,SDL_SWSURFACE);
  if(screen==NULL){
    fprintf(stderr,"couldn't set 640x480x16 video mode: %s\n",SDL_GetError());
    return(-1);
    }
   return(0);
}

screenはグローバルに宣言したSDLサーフェス変数へのポインタです。SDLではグラフィックをサーフェスで管理します。これは画面表示用に使います。screenを初期化したあと、ビデオモードを指定します。ここでは640x480の16bppですね。失敗したらその旨を伝えて終了します。

次にビットマップを読みこんでみましょう。SDLにはそういう関数が用意してあるのでとても楽です。もちろん自前でBMPを読みこむプログラムを書いてもいいです。今回はSDLの機能を使ってみます。次の関数はセットアップ用で、背景となるファイルを読み、SDLのサーフェスにしています。

int gameSetup(void){
  /* set back ground graphic */
  bg=SDL_LoadBMP(bgfile);
  if(bg==NULL)return(-1);
  return(0);
}

bgやbgfileもグローバル変数にしてます。まあ、こんな事してるとそのうち痛い目に会うでしょうが(笑)、今は気にせずにプログラムします。bgはサーフェスへのポインタで、使用後には解放しなければなりません。

void gameExit(void){
  SDL_FreeSurface(bg);
}

screenはSDL_Quit関数が後処理してくれるので解放しなくていいです。では、スクリーンにビットマップをコピーして表示させてみましょう。

void gameMain(void){
  SDL_BlitSurface(bg,NULL,screen,NULL);
  SDL_UpdateRect(screen,0,0,0,0);
  SDL_Delay(5000);
}

SDL_BlitSurfaceはサーフェスを転送する関数です。bgをscreenへコピーしています。引数にNULLを指定することによってサーフェス全体をコピーします。ここでは640x480で背景を作ってあると仮定しています。ホントはきっちりサイズを指定するべきですね。SDL_UpdateRectをしないと、変更が画面に反映されません。後ろの0,0,0,0は更新するエリアを示していますが、こう指定することで全体を更新表示します。SDL_Delayは指定した時間だけ待つ関数です。ここでは5000ms待ちます。

main(int argc,char *argv[]){
  if(initVideo())exit(-1);
  if(gameSetup())exit(-1);
  gameMain();
  gameExit();
}

というわけで、背景を5秒表示して終了するプログラムの完成です。Makefileもインクルードパスやライブラリーパス指定の参考になると思います。
ここまでのソースファイル

2.簡単な数字表示と等速ループ(2000/3/1)

それでは数字を表示させてみましょう。数字表示用に構造体を作ります。

typedef struct{
  SDL_Surface *bmp[10];
  int skip;
}CNumber;

skipで数字と数字の間隔をドット単位で指定できるようにしておきます。そして変数をやはりグローバルで用意します。

CNumber Number;

次にセットアップ時にグラフィックを読みこみます。

int gameSetup(void){
   int i;
   SDL_Surface *p;
   Uint32 trans;
(略)
   /* set number graphic and other property*/
  memset(&Number,0,sizeof(CNumber));
   for(i=0;i<10;i++){
     Number.bmp[i]=SDL_LoadBMP(numberfile[i]);
     p=Number.bmp[i];
     if(p==NULL){
       gameExit();
       return(-1);
     }
     //set transparent color
     trans=SDL_MapRGB(p->format,0xff,0,0xff);
     SDL_SetColorKey(p,SDL_SRCCOLORKEY,trans);
     }
   Number.skip=24;

Uint32は符号無し32ビット整数で、SDLから提供されています。transは透明色を表しています。ここでは紫(0xFF,0,0xFF)にしています。今回は紫を透明にして数字グラフィックを作ったためです。まず、SDL_MapRGB関数で色を作ります。その後でSDL_SetColorKeyで透明色を指定してサーフェスに透明色を追加しています。これ以後、このサーフェスの紫色は別のサーフェスにコピーされなくなります。さて、10個読み終えたら、後処理も追加しなければなりません。

void gameExit(void){
  int i;
(略)
   for(i=0;i<10;i++) if(Number.bmp[i]!=NULL)SDL_FreeSurface(Number.bmp[i]);

そして表示関数を作ります。

void drawNumber(int x,int y,int order,int num){
   int letter;
   SDL_Rect dest;
   dest.x=x+Number.skip*(order-1);
   dest.y=y;
   do{
     letter=num%10;
     dest.w=Number.bmp[letter]->w;
     dest.h=Number.bmp[letter]->h;
     SDL_BlitSurface(Number.bmp[letter],NULL,screen,&dest);
     num/=10;
     order--;
     dest.x-=Number.skip;
   }while(order>0);
}

今回は数字グラフィックのサイズでスクリーンに書きこまないといけないのでサイズや座標を指定します。SDL_Rectは4つのメンバ(x,y,w,h)を持っています。wはグラフィックの幅、hはグラフィックの高さです。ついでにSDL_Surfaceもwやhをメンバに持っていることが上のソースからわかります。関数の引数は左から順に、表示X座標、Y座標、桁、表示する数、と、なっています。

さて、次にゲームで必ず必要になる等速ループを作ってみましょう。これまた非常に簡単です。インクルードファイルにSDL_timer.hを追加したら、

void loopWait(void){
  Uint32 nowtime,lefttime;
   static Uint32 lasttime=0;
   const Uint32 interval=1000/FPS;
   nowtime=SDL_GetTicks();
   lefttime=lasttime+interval-nowtime;
   if(lasttime+interval>nowtime){
     SDL_Delay(lefttime);
     lasttime=nowtime+lefttime;
  }else lasttime=nowtime;
}

こんなかんじの関数をループ内のラストにでも置けば等速ループの完成です。SDL_GetTicksは現在の時間をms単位で得る関数です。FPSは

#define FPS 30

のように定義しています。一秒間のフレーム数です。大きくすればするほど滑らかにグラフィックが表示されるはずです。正確に30FPSを刻むわけではないのですが、今回はこれでもいいでしょう。

void gameMain(void){
  int step=0;
  do{
     SDL_BlitSurface(bg,NULL,screen,NULL);
     drawNumber(400,200,6,step);
     SDL_UpdateRect(screen,0,0,0,0);
     loopWait();
   }while(step++<FPS*10);
}

というわけで、10秒間フレームをカウントして表示するプログラムの完成です。

ここまでのソースファイル

3.イベント処理と自板の移動(2000/3/7)

実はイベント処理を説明してしまうと8割方はSDLの機能を解説したことになるかもしれません。グラフィックの取り扱いとキーボードやマウスなどの処理のほかに必要なものと言えば後はCD演奏や効果音の発生くらいでしょうか?今回はマウスやキーボードの入力のチェックと自分の移動を実装してみます。

まずその前にグラフィックを読まなければなりませんが、このままだと新しいグラフィックを追加するたびに初期化コードを書かなければならないみたいですね。このままにしておくのはマヌケなので一括で読みこみ、処理をしていくように変更します。まずそのために構造体を作ります。

typedef struct{
  SDL_Surface **image;
  char *filename;
}CImage;

そして

CImage Images[]={
  {&bg,"blockbg.bmp"},
  {&(Number.bmp[0]),"num32-0.bmp"};
(略)
  {&block,"block_r.bmp"},
};
const int nImages=14;

としておいて、グローバル変数をアタッチしておきます。

int imageLoad(void){
  int i,result=0;
  CImage *p;
  const Uint8 r=0xff,g=0,b=0xff;//set violet to transparent color
  Uint32 trans;
  for(i=0;i<nImages;++i){
    p=&(Images[i]);
    *(p->image)=SDL_LoadBMP(p->filename);
    if(*(p->image)==NULL)result=-1;
     //set transparent color
    trans=SDL_MapRGB((*(p->image))->format,r,g,b);
    SDL_SetColorKey(*(p->image),SDL_SRCCOLORKEY,trans);
  }
  if(result)imageFree();
  return(result);
}

グラフィック読みこみを統一して行うようにしました。もちろん後で一括してグラフィックを解放する必要があります。

void imageFree(void){
  int i;
  CImage *p;
  for(i=0;i<nImages;++i){
    p=&(Images[i]);
    if(NULL!=*(p->image)) SDL_FreeSurface(*(p->image));
  }
}

さて表示する準備が整ったので次はイベント処理に移ります。SDLはキーボードやマウスなどの入力信号やウインドウに送られてくるメッセージを受け取るとイベントが発生します。それをキューに蓄えておいて、後でそれを順次処理していくことになります。

さて、今回追加した変数等を挙げておきますと、

typedef struct{
SDL_Surface *bmp;
Sint32 x,y;
Sint32 vx,vy;
Sint32 ax,ay;
}CMyBoard;

自分の座標のデータです。meという変数で定義しています。y座標は今のところ使っていません。

typedef struct{
int done;
int abort;
}CGameState;

ゲームの状態を制御するための構造体です。gameStateという変数で定義しています。

では実際のイベント処理を見ていきましょう。


void readKeys(void){
  SDLKey *key;
  while ( SDL_PollEvent(&event) > 0 ) {
    switch (event.type) {
      case SDL_KEYDOWN: {
        key=&(event.key.keysym.sym);
        if(*key==27){
          gameState.abort=1;
          break;
        }
      }
      break;
      case SDL_MOUSEMOTION:{
        me.vx=event.motion.xrel*UNIT_SPEED;
      }
      break;
      case SDL_QUIT: {
        gameState.abort=1;
      }
      break;
    }
  }
}

eventはSDL_Event型の変数です。SDLからイベントを受け取ることが出来ます。ここではグローバル変数で定義してあります。さてまずSDL_PollEventでイベントを読みます。そして受け取ったイベント毎に処理をしていきます。

SDL_Quitはウインドウの閉じるボタンを押したときに送られてくるメッセージです。ここでアプリケーションを終了する処理を書かないと、閉じるボタンを押しても終了できなくなります。

SDL_MOUSEMOTIONはマウスが動いたときに発生するイベントです。詳しくはマニュアルを見ていただくことにしてここではマウスのX座標の相対移動値を参照しています。xrelがそれにあたります。ここではそれを自板の移動速度にしています。

SDL_KEYDOWNはその名の通りキーを押し下げられたときに発生するイベントです。keysym.symで押されたキーをチェックします。キーとキーの番号の対応については、SDLサンプルプログラムtestkeysを実行するとキーのリストを生成してくれるのでそれを参考にすると良いでしょう。それで作成した、キーマップをkeymap.txtとして添付しておきます。さて、ここで実際に何をしているのかと言うと、エスケープキーが押されたときに、プログラムを終了扱いにしているだけです。

さて、次は移動の処理と表示です。全体の流れとしては次のような感じで考えています。

void gameMain(void){
  do{
    //erase background by drawing bg
    SDL_BlitSurface(bg,NULL,screen,NULL);
    //read keys
    readKeys();
    //move objects
    moveObj();
    //draw sprites
    drawSprites();
    //update
    SDL_UpdateRect(screen,0,0,0,0);
    //wait periodic time
    loopWait();
  }while(!gameState.done && !gameState.abort);
}

まず自板の動きを作ってみます。

void moveMe(void){
  //my board moving
  //move
  me.x+=me.vx;
  if(me.x<0){
    me.x=0;
    me.vx=-me.vx;
  }
  if(me.x>(320-48)<<16){
    me.x=(320-48)<<16;
    me.vx=-me.vx;
  }
  //set accelaration
  if(me.vx>0)me.ax=-UNIT_SPEED;else
  if(me.vx<0)me.ax=UNIT_SPEED;else
  me.ax=0;
  //change velocity
  me.vx+=me.ax;
}

特に説明は必要ないかと思います。一応言っておきますと、座標データは16ビット底上げした値を使っています。もちろん少数を整数で取り扱いたいためです。さて、つぎに表示部分を作ります。

void drawSprites(void){
  SDL_Rect where={0,0,0,0};
  static int i=0;
  drawNumber(400,200,6,i);
  i++;if(i>999999)i=0;
  where.x=(me.x>>16)+36;
  where.y=me.y>>16;
  where.w=myboard->w;
  where.h=myboard->h;
  SDL_BlitSurface(myboard,NULL,screen,&where);
}

フレームカウントおよび、自板を表示しています。

今回、なぜかマウスの初期相対移動値が0にならなかったので初期化時にイベントをすべて吐き出しています。

//extingush pre inputted event
do ; while ( SDL_PollEvent(&event) > 0 );

あと、いくつか変更点を挙げますと、まずビデオ初期化時にフルスクリーン指定しています。

screen=SDL_SetVideoMode(640,480,16,SDL_SWSURFACE|SDL_FULLSCREEN);

また、マウスカーソルを消しています。

//erase mouse cursor
SDL_ShowCursor(0);

というわけで、マウスで自板を操作するところまで実装しました。起動後、フルスクリーン化するのでアプリケーション終了ボタンを押すことが出来ません。ESCで終了してください。

ここまでのソースファイル

4.ハードウエアの機能を使ってみる(2000/3/11)

前回マウスモーションの相対値を参照する所がありましたが、Xのフルスクリーンではちゃんとした値が得られないことがあります。これはSDLのバグです。現時点で最新のSDL1.0.8では直っているようです。

さて、今回はボールの動きを追加します。それに伴い、ハードウエアの機能を使って高速な描画を行ってみます。

その前にブロックの配置を指定しておきます。適当なテキストファイルに適当なフォーマットで記述することにします。

int blockread(char *filename){
  FILE *fp;
  unsigned char buf[1000],*p;
  unsigned char *ignore="#\n\r";
  int done,x,y,i;
  CBlock *blk;
  fp=fopen(filename,"rb");
  if(fp==NULL){
(略)

この辺はCの教科書を読むと、もっとマシなルーチンを作ることができるでしょう。SDLの説明を主眼に置いているのでとりあえずとばします。

高速描画のために、読みこんだグラフィックのフォーマットを揃えます。なぜかフォーマットを揃えないと透明色指定がうまく行かないようです。

//convert surface
converted=SDL_ConvertSurface(*(p->image),screen->format,0);
if(NULL!=converted){
  SDL_FreeSurface(*(p->image));
  *(p->image)=converted;
}else{
  result=-1;
}

SDL_ConvertSurfaceは指定したグラフィックを指定したフォーマットに変更し、そのグラフィックの実体へのポインタを返します。ここではスクリーンのフォーマットに変更し、それまでのグラフィックをデリートして新しくコンバートしたサーフェスをアタッチしています。

次にスクリーンを初期化するときにハードウエアサーフェスを使うようにします。また、ダブルバッファを使うように指定します。こうしなければ、ひどいちらつきが画面に出てしまうのです。

flags=SDL_HWSURFACE|SDL_FULLSCREEN|SDL_DOUBLEBUF;
screen=SDL_SetVideoMode(640,480,16,flags);

ダブルバッファによる表示とは、スクリーンを2枚用意し、描画と表示を使い分け、ちらつきをなくすことを指します。つまり、スクリーンにBlitしても表示用スクリーンにはBlitされず、裏画面にBlitされます。すべてのグラフィックをBlitし終わったら、スクリーンをフリップします。こうすることで、スクリーンの表と裏を瞬時に変えて、描画時のちらつきを無くします。ダブルバッファによる高速フリップ処理はフルスクリーン時にしか使用できません。

スクリーンのアップデートは必要無くなります。その代わりにダブルバッファのフリッピングを実行します。

//update
SDL_Flip(screen);

さてブロック崩しの中身のほうですが、少しゲームのサイズを変更しています。

後はボールの反射と表示を作ります。

void moveBalls(void){
  CBall *p;
  CBlock *q;
  int i,j;
  Sint32 relx,rely;
  Sint32 oldx,oldy;
  Sint32 oldrelx,oldrely;
  Sint32 refx,refy;
  for(i=0;i<BALL_MAX;++i){
    p=&(balls[i]);
    if(!p->state)continue;
(以下略)

特に難しいことはしていないので詳しくはソースを参照してください。また、反射のプログラムは手抜きしてますので各自改良してください。

また整数のsinおよびcosを作っています。ボールを板で跳ね返したときの角度をこれで決めます。

void makeAngle(void){
  int i;
  double pi=3.141592653;
  for(i=0;i<ANGLE_DIVS;++i){
    DigitAngle.sin[i]=(Sint32)(sin(2*pi*i/ANGLE_DIVS)*65536);
    DigitAngle.cos[i]=(Sint32)(cos(2*pi*i/ANGLE_DIVS)*65536);
  }
}

また、今回は1つのボールをテスト的にプレイエリアに入れています。

main(int argc,char *argv[]){
(略)
  balls[0].state=1;
  balls[0].x=100<<16;
  balls[0].y=200<<16;
  balls[0].vx=65536*2;
  balls[0].vy=65536*2;
(略)

というわけで、板を操作しボールを跳ね返すことが出来るようになりました。

#今回、シフト演算子が算術演算子よりも優先順位が低いってはじめて知った(爆)

ここまでのソースファイル

5.音楽と効果音(2000/3/18)

新しくなるたびに原型が崩れていくというとんでもないプログラムもそろそろ終わりです。今回はCDによる音楽再生とSDLから提供されているライブラリーを使った効果音の発生を実装してみます。

CD演奏に関しては、専用の関数群が用意されています。今回はCD関連のプログラムをcdmusic.cにまとめてみます。

int cd_init(int drive){
  CD_OK=1;
  NOW_PLAY=0;
  playtrack=0;
  /* Initialize SDL first */
  if ( SDL_Init(SDL_INIT_CDROM) < 0 ) {
    fprintf(stderr, "Couldn't initialize SDL: %s\n",SDL_GetError());
    CD_OK=0;
  }
  cdrom = SDL_CDOpen(drive);
  if ( cdrom == NULL ) {
    fprintf(stderr, "Couldn't open default CD-ROM drive: %s\n",SDL_GetError());
    //exit(2);
    CD_OK=0;
  }
  status = SDL_CDStatus(cdrom);
}

CDの初期化です。ビデオの時と同じように指定します。SDL_CDOpenは扱いたいCDROMドライブを指定してCDROMを開きます。たとえばCDROMとCDRとDVDROMを持っていて、この順番で登録されていれば、0番がCDROM、1番がCDR…と、なります。変数cdromはSDL_CD型のポインタです。もうこの型などの記述はおなじみですね。もちろん後で解放します。

int cd_close(void){
  if(CD_OK){
    SDL_CDClose(cdrom);
  }
}

ここで注意しておきたいのはクローズする前に曲を停止しておかないと、曲が鳴ったままSDLがCDを解放してしまう点です。正確には、CDをクローズする前に、演奏停止しなければなりませんが、演奏中止する前に一時停止しなければなりません。

int cd_play(int track){
  if(!CD_OK)return(-1);
  if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ){
    SDL_CDPlayTracks(cdrom, track, 0, 1, 0);
    playtrack=track;
    NOW_PLAY=1;
  }
}

CDの再生です。一番はじめの曲の番号は0番です。ここではtrackという変数で指定していますね。ほかに一時停止や演奏中止があります。似たような関数なので説明を割愛します。

さて次は効果音です。SDLで用意されている関数でインプリメントしても良いのですが、結構面倒なので用意されているライブラリーを使ってみます。そのためにまずSDLのバージョンを1.1.1以降にします。次にSDL_Mixerをインストールします。このSDL_MixerライブラリはWAVのほかにMIDIやMP3、外部サウンドプログラムをSDLで簡単に使うためのライブラリーです。

残念ながら私の今現在の環境ではライブラリーを構築できないのでオブジェクトリンクによって、このライブラリを使うことにします。

SDL_Mixer.hをインクルードします。効果音はMix_Chunkという型のポインタを使用します。

typedef struct{
  Mix_Chunk **wave;
  char *filename;
}CSound;

まず初期化します。

int initAudio(void){
  if (Mix_OpenAudio(audio_rate, audio_format, audio_channels, 512) < 0) {
    fprintf(stderr, "Couldn't open audio: %s\n", SDL_GetError());
    return(-1);
  }
  audio_open=1;
  return(0);
}

audio_rateは再生レートで22050などの値を指定します。audio_formatは符号有無、8ビットあるいは16ビットの指定です。エンディアン(バイトオーダー)も指定できます。audio_channnelsはチャンネルの数で2だとステレオになります。最後の512はバッファの大きさで、このサイズが大きいほど一度にサウンドカードにデータを転送するかわりにレスポンスが落ちます。512はSDLが最低としているほど低いサイズですが、この程度にしておかないとレスポンスが悪いのです。

int waveLoad(void){
  int i;
  CSound *p;
  for(i=0;i<nSound;++i){
    p=&(sound[i]);
    *(p->wave)=Mix_LoadWAV(p->filename);
    if(*(p->wave)==NULL){
      fprintf(stderr, "Couldn't load %s: %s\n",
      wavefile, SDL_GetError());
    return(-1);
    }
  }
  return(0);
}

Mix_LoadWAVでファイルを指定してWAVファイルを読みこみます。ここで初期化時に指定されたフォーマットへコンバートされます。この辺が便利なところです。SDLの関数からWAVを読んだ場合は自前で変換しなければなりません。

読んだサウンドを鳴らす方法は

Mix_PlayChannel(-1, wave, 0);

こうなります。ここで第一引数は鳴らすチャンネルで、-1を指定すると空いているチャンネルを使って効果音を再生します。第2引数はMix_Chunkへのポインタ、最後の引数は鳴らす回数です。ここに指定した数+1回、サウンドを繰り返して鳴らします。-1を指定することで約65000回鳴らします。WAVフォーマットの音楽を鳴らすときにちょうど良いと思います。

ブロック崩し関連のソースは変更が多すぎて書ききれません。じっくり読むか、わからなければ私に聞いてください。

ここまでのソースファイル

 

以上で終わりにします。かなり当初から変更されましたし、かなり自虐的なゲームになってしまいました…。オマケにソースファイルもかなり暗号入ってるかもしれません(笑)。

 

 

ご意見、ご感想、ご要望はこちらまで

Copyright (c) adas,2000.