プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第96回 プログラミングについて『テトリスゲームを作ろう! その4』
コラム
2025/01/31

第96回 プログラミングについて『テトリスゲームを作ろう! その4』

アポロレポート
今回も前回の続きです。前回宣言したまま本体を作成していなかったメンバー関数3つを定義しましょう。

 最初は指定されたブロックを指定された色で描画する関数 WriteBlockTask() です。

void Tetris::WriteBlockTask(COLORREF color,int *yary,int *xary,int lary)
{
                          //(1)
 CClientDC dc(this);
 CPen  pen (PS_SOLID,0,color),*old_pen ;
 CBrush brush(      color),*old_brush;
 int i;
 int xpos,ypos;
                          //(2)
 old_pen =dc.SelectObject(&pen );
 old_brush=dc.SelectObject(&brush);
                          //(3)
 for(i=0;i<lary;i++){
  ypos=yary[i]*GRID_SIZE+GRID_SIZE*TOP_MARGIN+GRID_SIZE/2;
  xpos=xary[i]*GRID_SIZE           +GRID_SIZE/2;

  dc.Rectangle(
   xpos-BLOCK_SIZE/2 ,ypos-BLOCK_SIZE/2 ,
   xpos+BLOCK_SIZE/2+1,ypos+BLOCK_SIZE/2+1);
 }
                          //(4)
 dc.SelectObject(old_pen );
 dc.SelectObject(old_brush);
                          //(5)
  pen.DeleteObject();
 brush.DeleteObject();
}

(1)ではこのメンバー関数で使用する変数を定義しているのですが、

   CClientDC dc(this);

   はクライアント領域を描画するデバイスコンテキストを定義しています。これは単純な変数ではなく、CClientDC クラスのオブジェクトとして生成します。引数の this は Tetris クラスのインスタンスへのポインタです。

    このデバイスコンテキストは生成時に this で示される CWnd クラスまたはそれから派生したクラスのオブジェクトから GetDC() 関数でそのデバイスコンテキストへのポインタを取得し、消滅時には ReleaseDC() 関数を呼び出して使用していたデバイスコンテキストを解放する機能を持ったものです。それ以外は CDC クラスと同じ機能を持っています。

    Windows上のデバイスコンテキストは最大で5つ同時に使え、描画するときにウインドウの持っているデバイスコンテキストへのポインタを取得し、使用後は解放しておかないと他のウインドウやアプリケーションが描画できなくなりますので、気を付けていかなければなりません。

    ですので、CClientDC ではなく CDC クラスを使用するのであれば、

   CDC *dc;

   dc=GetDC();

      :
      :

   ReleaseDC(dc);

   というように必ず解放するようにします。
   
   CPen  pen (PS_SOLID,0,color),*old_pen ;
   CBrush brush(      color),*old_brush;

   は描画するときに使用するペンとブラシを定義しています。
   *old_pen と *old_brush は SelectObject() 関数でペンやブラシを選択したときのそれまでのペンやブラシへのポインタを保持しておくために定義しておきます。

(2)では、描画する前にペンとブラシを設定しています。CClientDC の基本クラスのCDC クラスのメンバー関数の SelectObject() を使用し、生成したペンとブラシを選択させます。このときこの関数はそれまでのペンやブラシへのポインタを返しますので、そのポインタを保持しておかなければなりません。(4)で再びペンやブラシを元のものに戻すためです。

(3)では配列 xary、yary 内の格子上での座標値を実際のウインドウのクライアント領域の座標上に直して正方形を描画しています。ここで1つ注意しておくことは、dc.Rectangle() で第3と第4引数に 1 を加算していることです。この関数は指定された位置を対角とする矩形を描画しますが、

   dc.Rectangle(x1,y1,x2,y2);

   としたときに、

   x2-x1、y2-y1 のサイズのものを描画しますので、それぞれ 1 だけ小さい大きさになってしまうからです。

(5)は(4)でペンとブラシを元に戻した後、使用したペンとブラシのオブジェクトを削除するためのものです。

 次は、落下するテトロミノを描画するメンバー関数です。

 void Tetris::WriteBlock(int wflag)
 {
  int yary[25],xary[25];
  int lary;
  int y,x;
  COLORREF color;

  lary=0;
  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(!block_matrix[y][x])continue;

    yary[lary]=y+block_ypos-2;
    xary[lary]=x+block_xpos-2;
    lary++;
   }
  }

  if(wflag)color=FLOATING;
  else   color=BACKGROUND;

  WriteBlockTask(color,yary,xary,lary);
 }

このメンバー関数は特に説明する部分はないのですが、

  yary[lary]=y+block_ypos-2;
  xary[lary]=x+block_xpos-2;

の部分で 2 を引いているのは、前々回に5x5の2次元の配列をテトロミノのデータ構造とし、その中心位置が配列の中心と考えることにしていましたので、現在の位置と配列の中心位置を一致させるために行なっています。
 描画に必要な座標値を配列に代入したら、この関数の引数 wflag が 0 のときには消去、1 のときには描画とするため、色にはそれぞれ BACKGROUND と FLOATING を設定して WriteBlockTask() を使って描画します。FLOATING は #define FLOATING RGB(0,0,0)と定義しておきます。

 3つ目は、WritePoint() です。

 void Tetris::WritePoint()
 {
  CClientDC dc(this);
  char text[21];

  if(point==0)strcpy (text,"       ");
  else    sprintf(text,"Point=%d",point);

  dc.TextOut(0,0,text,strlen(text));
 }

このメンバー関数は特に説明の必要はないでしょう。じっくりと見ていただければ理解できると思います。

 ここまでで、コンパイルとリンクが可能になりますが、まだ見た目には何にも変わらないので全文の紹介は後回しにしましょう。

前回まではウインドウのタイトルに数字を表示していましたが、今度はこのメンバー関数を次のように本来の機能にしていきましょう。

 afx_msg void Tetris::OnIdle()
 {
  DWORD cur_time;

  if(!playing)return;

  cur_time=GetTickCount();
  if(cur_time-old_time>=speed){
   old_time=cur_time;

   if(block_flag==0)MakeBlock();
//  else       DownBlock();
  }
 }

特に難しいところはありません。インターバル間隔になったとき、テトロミノが生成されていなければ生成し、生成されていれば1つ落下させるということを行なうようにします。とりあえずブロックを生成する部分を作成しますので、DownBlock() のところはコメントにしておきます。また MakeBlock() は Tetris クラスに void MakeBlock() として追加しておきます。

 Tetris::MakeBlock() メンバー関数を作りましょう。ちょっと長いのですが我慢して下さい。

 void Tetris::MakeBlock()
 {
  int type;
  int y,x;
                           //(1)
  srand(GetTickCount());

  type=rand()%7+1;
                           //(2)
  for(y=0;y<5;y++){
   for(x=0;x<5;x++)block_matrix[y][x]=0;
  }
                           //(3)
  if(type==1){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[2][4]=1;
  }
  else if(type==2){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][2]=1;
  }
  else if(type==3){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][3]=1;
  }
  else if(type==4){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][1]=1;
  }
  else if(type==5){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[3][2]=1;
   block_matrix[3][3]=1;
  }
  else if(type==6){
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][1]=1;
   block_matrix[3][2]=1;
  }
  else if(type==7){
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][2]=1;
   block_matrix[3][3]=1;
  }
                           //(4)
  block_xpos=GRID_SUMX/2;
  block_ypos=2;
  block_flag=1;
                           //(5)
  WriteBlock(1);
  Delay(0.5);
  WriteBlock(0);
  block_flag=0;
}

(1)はランダムにテトロミノの形状番号を決定しています。srand() という関数は、乱数を発生する rand() 関数の初期化をするものです。rand() 関数は初期値が同じ場合には順次ランダムな値を発生させますが、発生する値の順番が同じになってしまいますので、srand() を現在の時間をパラメータにして初期化させています。

  srand(GetTickCount());

  type=rand()%7+1;

rand()%7+1 では発生した乱数を 7 で割った余りに 1 を加えていますが、こうすることで 1~7 までの値が式の結果になります。

(2)ではテトロミノ用の2次元の配列を初期化しています。これは説明の必要はないでしょう。

  for(y=0;y<5;y++){
   for(x=0;x<5;x++)block_matrix[y][x]=0;
  }

(3)はランダムに決定したテトロミノの形状番号通りにテトロミノ用の配列を設定しているものです。このプログラムでは単純に IF文 で番号によって内容を設定していますが、7種類の形状を予めテーブル(配列)に定義しておいて一意に設定することも可能ですので試してみて下さい。

(4)は落下するテトロミノの中心位置と、テトロミノが生成されたというフラグをオンにしています。

(5)はテトロミノがうまく生成できるかを実験するために、生成したテトロミノを表示し、0.5秒待ってから消去します。その後、テトロミノが生成されたというフラグをオフにします。Delay というメンバー関数は定義されていないので、void Delay(double delta_time) と Tetris クラスに追加しておきます。

Tetris::Delay メンバー関数は次の通りです。

 void Tetris::Delay(double delta_time)
 {
  DWORD to,dt;

  dt=(int)(delta_time*1000.0);

  to=GetTickCount();

  while(1){
   if(GetTickCount()-to>dt)break;
  }
 }

このメンバー関数も難しくはないと思いますので、じっくりと眺めて下さい。

 ここでこれまでのプログラムの全リストを紹介します。

 #include <afxwin.h>

 #define BLOCK_SIZE  8
 #define GRID_SIZE 10
 #define GRID_SUMY 22
 #define GRID_SUMX 10
 #define TOP_MARGIN  3

 #define INITIAL_SPEED 500
 #define  DELTA_SPEED 20

 #define ID_IDLE 100
 #define ID_PLAY 101

 #define FLOATING  RGB( 0, 0, 0)
 #define FIXED    RGB(127, 0, 0)
 #define BACKGROUND RGB(255,255,255)
 #define COMPLETE  RGB(255, 0, 0)

 class TetrisApp : public CWinApp
 {
  private:
  virtual BOOL OnIdle(LONG lCount);

  public:
   virtual BOOL InitInstance();
 };

 class Tetris : public CFrameWnd
 {
  private:
   int playing;
   int point ;

   DWORD old_time;
   DWORD speed;

   int  grid_matrix[GRID_SUMY][GRID_SUMX];
   int  block_matrix[5][5];
   int  block_flag,block_ypos,block_xpos;

   afx_msg void OnIdle();
   afx_msg void OnPaint();
   afx_msg void OnStartPlay();
       void Redraw();
       void WriteBlock(int wflag);
       void WriteBlockTask(COLORREF color,int *yary,int *xary,int lary);
       void WritePoint();
       void MakeBlock();
       void RotationBlock(int block[5][5]);
       void Delay(double delta_time);

  public:
   Tetris();

  DECLARE_MESSAGE_MAP()
 };

 class TetrisApp TetrisApp;

 BOOL TetrisApp::InitInstance()
 {
  m_pMainWnd = new class Tetris;

  return TRUE;
 }

 BOOL TetrisApp::OnIdle(LONG lCount)
 {
  m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);

  return TRUE;
 }

 BEGIN_MESSAGE_MAP(Tetris,CFrameWnd)
  ON_COMMAND(ID_IDLE,OnIdle)
  ON_COMMAND(ID_PLAY,OnStartPlay)
  ON_WM_PAINT()
 END_MESSAGE_MAP()

 Tetris::Tetris()
 {
  RECT rect;
  int xsize,ysize;

  Create(
   NULL,"",
   WS_BORDER | WS_CAPTION | WS_OVERLAPPED | WS_MINIMIZEBOX | WS_SYSMENU,
   rectDefault);

  GetWindowRect(&rect);
  xsize=rect.right -rect.left;
  ysize=rect.bottom-rect.top ;

  GetClientRect(&rect);
  xsize-=(rect.right -rect.left);
  ysize-=(rect.bottom-rect.top );

  xsize+=GRID_SIZE* GRID_SUMX;
  ysize+=GRID_SIZE*(GRID_SUMY+TOP_MARGIN);

  SystemParametersInfo(SPI_GETWORKAREA,0,&rect,0);

  MoveWindow(
   (rect.left+rect.right )/2-xsize/2,
   (rect.top +rect.bottom)/2-ysize/2,
   xsize,ysize);

  ShowWindow(SW_SHOW);

  playing = -1;
  old_time= 0;
  speed  =INITIAL_SPEED;
 }

 afx_msg void Tetris::OnIdle()
 {
  DWORD cur_time;

  if(!playing)return;

  cur_time=GetTickCount();
  if(cur_time-old_time>=speed){
   old_time=cur_time;

   if(block_flag==0)MakeBlock();
 // else       DownBlock();
  }
 }

 afx_msg void Tetris::OnPaint()
 {
  CPaintDC dc(this);

  if(playing== -1){
   playing=0;

   if(MessageBox("開始しますか?","開始",MB_YESNO)==IDNO){
     PostQuitMessage(0);
   }
   else{
    PostMessage(WM_COMMAND,ID_PLAY);
   }
  }
 }

 afx_msg void Tetris::OnStartPlay()
 {
  int y,x;

  for(y=0;y<GRID_SUMY;y++){
   for(x=0;x<GRID_SUMX;x++)grid_matrix[y][x]=0;
  }

  block_flag=0;
  point   =0;
  playing  =1;
  speed   =INITIAL_SPEED;

  Redraw();
 }

 void Tetris::Redraw()
 {
  int yary[GRID_SUMX*GRID_SUMY],xary[GRID_SUMX*GRID_SUMY],lary;
  int y,x;
  int loop,color;

  for(loop=0;loop<=2;loop++){
   lary=0;
   for(y=0;y<GRID_SUMY;y++){
    for(x=0;x<GRID_SUMX;x++){
     if(grid_matrix[y][x]==loop){
      yary[lary]=y;
      xary[lary]=x;
      lary++;
     }
    }
   }

      if(loop==0)color=BACKGROUND;
   else if(loop==1)color=FIXED   ;
   else      color=COMPLETE ;
   WriteBlockTask(color,yary,xary,lary);
  }

  WritePoint();

  if(block_flag)WriteBlock(1);
 }

 void Tetris::WriteBlockTask(COLORREF color,int *yary,int *xary,int lary)
 {
  CClientDC dc(this);
  CPen  pen (PS_SOLID,0,color),*old_pen ;
  CBrush brush(      color),*old_brush;
  int i;
  int xpos,ypos;

  old_pen =dc.SelectObject(&pen );
  old_brush=dc.SelectObject(&brush);

  for(i=0;i<lary;i++){
   ypos=yary[i]*GRID_SIZE+GRID_SIZE*TOP_MARGIN+GRID_SIZE/2;
   xpos=xary[i]*GRID_SIZE           +GRID_SIZE/2;

   dc.Rectangle(
    xpos-BLOCK_SIZE/2 ,ypos-BLOCK_SIZE/2 ,
    xpos+BLOCK_SIZE/2+1,ypos+BLOCK_SIZE/2+1);
  }

  dc.SelectObject(old_pen );
  dc.SelectObject(old_brush);

   pen.DeleteObject();
  brush.DeleteObject();
 }

 void Tetris::WriteBlock(int wflag)
 {
  int yary[25],xary[25];
  int lary;
  int y,x;
  COLORREF color;

  lary=0;
  for(y=0;y<5;y++){
   for(x=0;x<5;x++){
    if(!block_matrix[y][x])continue;

    yary[lary]=y+block_ypos-2;
    xary[lary]=x+block_xpos-2;
    lary++;
   }
  }

  if(wflag)color=FLOATING;
  else   color=BACKGROUND;

  WriteBlockTask(color,yary,xary,lary);
 }

 void Tetris::WritePoint()
 {
  CClientDC dc(this);
  char text[21];

  if(point==0)strcpy (text,"       ");
  else    sprintf(text,"Point=%d",point);

  dc.TextOut(0,0,text,strlen(text));
 }

 void Tetris::MakeBlock()
 {
  int type;
  int y,x;

  srand(GetTickCount());

  type=rand()%7+1;

  for(y=0;y<5;y++){
   for(x=0;x<5;x++)block_matrix[y][x]=0;
  }

  if(type==1){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[2][4]=1;
  }
  else if(type==2){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][2]=1;
  }
  else if(type==3){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][3]=1;
  }
  else if(type==4){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][1]=1;
  }
  else if(type==5){
   block_matrix[2][1]=1;
   block_matrix[2][2]=1;
   block_matrix[3][2]=1;
   block_matrix[3][3]=1;
  }
  else if(type==6){
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][1]=1;
   block_matrix[3][2]=1;
  }
  else if(type==7){
   block_matrix[2][2]=1;
   block_matrix[2][3]=1;
   block_matrix[3][2]=1;
   block_matrix[3][3]=1;
  }

  block_xpos=GRID_SUMX/2;
  block_ypos=2;
  block_flag=1;

  WriteBlock(1);
  Delay(0.5);
  WriteBlock(0);
  block_flag=0;
 }

 void Tetris::RotationBlock(int block[5][5])
 {
  int tmp[5][5];
  int loop;
  int y,x;

  for(loop=0;loop<=1;loop++){
   for(y=0;y<5;y++){
    for(x=0;x<5;x++){
     if(loop==0) tmp[y ][x]=block[y][x];
     else    block[4-x][y]= tmp[y][x];
    }
   }
  }
 }

 void Tetris::Delay(double delta_time)
 {
  DWORD to,dt;

  dt=(int)(delta_time*1000.0);

  to=GetTickCount();

  while(1){
   if(GetTickCount()-to>dt)break;
  }
 }

 このプログラムを実行してみて下さい。順次テトロミノが表示されるはずです。ただしテトロミノは落下しません。

 次回はなんとかテトリスを完成させましょう。

そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?