コラム
2024/12/24
前回の終わりにテトロミノの表現方法を決めましたので、どんどんプログラムを作っていきましょう。
まず、Tetris クラスに落下するテトロミノと格子の状態の配列や必要なデータメンバーを追加します。
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();
public:
Tetris();
DECLARE_MESSAGE_MAP()
};
追加したデータメンバーの内訳は次の通りです。
playing 現在プレイ中か否かを示すデータメンバー。
-1 − ゲームが起動した直後。
0 − プレイしていない。
1 − プレイ中。
point 得点。
grid_matrix[GRID_SUMY][GRID_SUMX]
格子の状態。
0 − 何もない。
1 − ブロックがある。
2 − 完成した行。
block_matrix[5][5]
テトロミノ。
0 − 何もない。
1 − ブロックがある。
block_flag
0 − テトロミノ(ブロック)は作成されていない。
1 − テトロミノ(ブロック)が落下中。
block_ypos、block_xpos;
テトロミノ(ブロック)の中心の位置。
このデータメンバーの追加に伴って、Tetris::Tetris コンストラクタに
playing = -1;
を追加し、起動直後という設定にしておきます。
次に、ゲームが起動されたときに突然テトロミノが落下してくるというのも気持ちの準備が出来ていないときにはびっくりするので、何らかの入力待ちにした方がいいと思います。これを行なうのがいつの時点がいいのかが問題なのですが、私の場合は OnPaintで行なう様にしています。OnPaint は CWnd クラスのメンバー関数で、ウインドウの一部または全体を描画する必要があるときに呼び出されるものです。
Tetris クラスに afx_msg void OnPaint(); の宣言を追加し、メッセージマップにON_WM_PAINT() を追加します。そして Tetris::OnPaint() 本体は、
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);
}
}
}
としましょう。Tetris::Tetris コンストラクタで playing データメンバーを -1 にしておきましたので、このメンバー関数が呼び出されるときにも -1 のままになっていま
す。ですので、-1 のときに『開始しますか?』というメッセージボックスを表示し、入力待ちにします。メッセージボックスは Tetris の基本クラスの CWnd のメンバー関数で、実行してみればどんなものかすぐに分かると思います。
Yesのときには ID_PLAY コマンドメッセージを転送し(このメッセージ番号は 101と定義しておきます)、Noのときには終了メッセージを転送します。またこの入力待ちは起動時の一回限りとしますので、playing は 0 にしておきます。
とりあえず、ここまでのプログラムの全体です。
#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
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();
void Redraw();
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_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;
static int count=0;
char tmp[21];
cur_time=GetTickCount();
if(cur_time-old_time>=speed){
old_time=cur_time;
sprintf(tmp,"%d",++count);
SetWindowText(tmp);
}
}
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);
}
}
}
このプログラムを実行すると、起動直後に『開始しますか?』と聞いてきて、Yesとすると、タイトルに順次数値が表示されます。
次は ID_PLAY メッセージを処理しましょう。このメッセージを処理するメンバー関数を afx_msg void OnStartPlay() とし、Tetris クラスに追加し、メッセージマップにON_COMMAND(ID_PLAY,OnStartPlay) を追加します。そして Tetris::OnStartPlay() 本体は、
afx_msg void Tetris::OnStartPlay()
{
int y,x;
//(1)
for(y=0;y<GRID_SUMY;y++){
for(x=0;x<GRID_SUMX;x++)grid_matrix[y][x]=0;
}
//(2)
block_flag=0;
point =0;
playing =1;
speed =INITIAL_SPEED;
//(3)
Redraw();
}
とします。
(1)では格子の配列を初期化しています。全部 0 にしているので格子上には何もない状態です。
(2)ではデータメンバーを初期化しています。
block_flag=0; //ブロックは生成されていない。
point =0; //得点は 0。
playing =1; //プレイ中。
speed =INITIAL_SPEED; //インターバル間隔を初期化。
(3)は全体の再描画です。このメンバー関数はまだ宣言されていないので、Tetris クラスに void Redraw() を追加しておきます。Redraw() は格子と落下中にテトロミノをデータの内容と同じにするようにウインドウ全体を描画し直す関数です。
次に Redraw() は、
void Tetris::Redraw()
{
int yary[GRID_SUMX*GRID_SUMY],xary[GRID_SUMX*GRID_SUMY],lary;
int y,x;
int loop,color;
//(1)
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);
}
//(2)
WritePoint();
if(block_flag)WriteBlock(1);
}
上のプログラムで、WriteBlockTask()、WritePoint()、WriteBlock() はまだ宣言もされていませんので、次のように Tetris クラスに宣言しておきます。
void WriteBlock(int wflag);
void WriteBlockTask(COLORREF color,int *yary,int *xary,int lary);
void WritePoint();
この3つのメンバー関数は、
void WriteBlock(int wflag);
落下中のテトロミノを描画する。
void WriteBlockTask(COLORREF color,int *yary,int *xary,int lary);
格子やテトロミノの要素である正方形を指定された位置に指定された色で描画する。
void WritePoint();
得点を描画する。
ということにしておきます。
(1)では格子の状態を描画します。このループ
for(loop=0;loop<=2;loop++){
:
:
}
は、最初は grid_matrix 配列の要素の値が 0 のもの(何もない)を、2回目のときには、値が 1 のもの(積み上げられているブロック)、3回目には値が 2のもの(1列が完成している)の格子の位置(座標値)を xary、yary 配列に代入し、WriteBlockTask() 関数を使って描画します。
if(loop==0)color=BACKGROUND;
else if(loop==1)color=FIXED ;
else color=COMPLETE ;
では描画する色を設定しているのですが、これもまだ定義されていませんので、
#define FIXED RGB(127, 0, 0) //暗い赤
#define BACKGROUND RGB(255,255,255) //白
#define COMPLETE RGB(255, 0, 0) //赤
と定義しておきます。色は好みですので適当に変えて下さい。
(2)は現在の得点を WritePoint() を使って描画し、テトロミノ(ブロック)が生成されているのであれば WrteBlock() を使ってそれも描画します。
まだ途中なのですが、また次回にしましょう。
第95回 プログラミングについて『テトリスゲームを作ろう! その3』

前回の終わりにテトロミノの表現方法を決めましたので、どんどんプログラムを作っていきましょう。
まず、Tetris クラスに落下するテトロミノと格子の状態の配列や必要なデータメンバーを追加します。
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();
public:
Tetris();
DECLARE_MESSAGE_MAP()
};
追加したデータメンバーの内訳は次の通りです。
playing 現在プレイ中か否かを示すデータメンバー。
-1 − ゲームが起動した直後。
0 − プレイしていない。
1 − プレイ中。
point 得点。
grid_matrix[GRID_SUMY][GRID_SUMX]
格子の状態。
0 − 何もない。
1 − ブロックがある。
2 − 完成した行。
block_matrix[5][5]
テトロミノ。
0 − 何もない。
1 − ブロックがある。
block_flag
0 − テトロミノ(ブロック)は作成されていない。
1 − テトロミノ(ブロック)が落下中。
block_ypos、block_xpos;
テトロミノ(ブロック)の中心の位置。
このデータメンバーの追加に伴って、Tetris::Tetris コンストラクタに
playing = -1;
を追加し、起動直後という設定にしておきます。
次に、ゲームが起動されたときに突然テトロミノが落下してくるというのも気持ちの準備が出来ていないときにはびっくりするので、何らかの入力待ちにした方がいいと思います。これを行なうのがいつの時点がいいのかが問題なのですが、私の場合は OnPaintで行なう様にしています。OnPaint は CWnd クラスのメンバー関数で、ウインドウの一部または全体を描画する必要があるときに呼び出されるものです。
Tetris クラスに afx_msg void OnPaint(); の宣言を追加し、メッセージマップにON_WM_PAINT() を追加します。そして Tetris::OnPaint() 本体は、
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);
}
}
}
としましょう。Tetris::Tetris コンストラクタで playing データメンバーを -1 にしておきましたので、このメンバー関数が呼び出されるときにも -1 のままになっていま
す。ですので、-1 のときに『開始しますか?』というメッセージボックスを表示し、入力待ちにします。メッセージボックスは Tetris の基本クラスの CWnd のメンバー関数で、実行してみればどんなものかすぐに分かると思います。
Yesのときには ID_PLAY コマンドメッセージを転送し(このメッセージ番号は 101と定義しておきます)、Noのときには終了メッセージを転送します。またこの入力待ちは起動時の一回限りとしますので、playing は 0 にしておきます。
とりあえず、ここまでのプログラムの全体です。
#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
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();
void Redraw();
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_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;
static int count=0;
char tmp[21];
cur_time=GetTickCount();
if(cur_time-old_time>=speed){
old_time=cur_time;
sprintf(tmp,"%d",++count);
SetWindowText(tmp);
}
}
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);
}
}
}
このプログラムを実行すると、起動直後に『開始しますか?』と聞いてきて、Yesとすると、タイトルに順次数値が表示されます。
次は ID_PLAY メッセージを処理しましょう。このメッセージを処理するメンバー関数を afx_msg void OnStartPlay() とし、Tetris クラスに追加し、メッセージマップにON_COMMAND(ID_PLAY,OnStartPlay) を追加します。そして Tetris::OnStartPlay() 本体は、
afx_msg void Tetris::OnStartPlay()
{
int y,x;
//(1)
for(y=0;y<GRID_SUMY;y++){
for(x=0;x<GRID_SUMX;x++)grid_matrix[y][x]=0;
}
//(2)
block_flag=0;
point =0;
playing =1;
speed =INITIAL_SPEED;
//(3)
Redraw();
}
とします。
(1)では格子の配列を初期化しています。全部 0 にしているので格子上には何もない状態です。
(2)ではデータメンバーを初期化しています。
block_flag=0; //ブロックは生成されていない。
point =0; //得点は 0。
playing =1; //プレイ中。
speed =INITIAL_SPEED; //インターバル間隔を初期化。
(3)は全体の再描画です。このメンバー関数はまだ宣言されていないので、Tetris クラスに void Redraw() を追加しておきます。Redraw() は格子と落下中にテトロミノをデータの内容と同じにするようにウインドウ全体を描画し直す関数です。
次に Redraw() は、
void Tetris::Redraw()
{
int yary[GRID_SUMX*GRID_SUMY],xary[GRID_SUMX*GRID_SUMY],lary;
int y,x;
int loop,color;
//(1)
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);
}
//(2)
WritePoint();
if(block_flag)WriteBlock(1);
}
上のプログラムで、WriteBlockTask()、WritePoint()、WriteBlock() はまだ宣言もされていませんので、次のように Tetris クラスに宣言しておきます。
void WriteBlock(int wflag);
void WriteBlockTask(COLORREF color,int *yary,int *xary,int lary);
void WritePoint();
この3つのメンバー関数は、
void WriteBlock(int wflag);
落下中のテトロミノを描画する。
void WriteBlockTask(COLORREF color,int *yary,int *xary,int lary);
格子やテトロミノの要素である正方形を指定された位置に指定された色で描画する。
void WritePoint();
得点を描画する。
ということにしておきます。
(1)では格子の状態を描画します。このループ
for(loop=0;loop<=2;loop++){
:
:
}
は、最初は grid_matrix 配列の要素の値が 0 のもの(何もない)を、2回目のときには、値が 1 のもの(積み上げられているブロック)、3回目には値が 2のもの(1列が完成している)の格子の位置(座標値)を xary、yary 配列に代入し、WriteBlockTask() 関数を使って描画します。
if(loop==0)color=BACKGROUND;
else if(loop==1)color=FIXED ;
else color=COMPLETE ;
では描画する色を設定しているのですが、これもまだ定義されていませんので、
#define FIXED RGB(127, 0, 0) //暗い赤
#define BACKGROUND RGB(255,255,255) //白
#define COMPLETE RGB(255, 0, 0) //赤
と定義しておきます。色は好みですので適当に変えて下さい。
(2)は現在の得点を WritePoint() を使って描画し、テトロミノ(ブロック)が生成されているのであれば WrteBlock() を使ってそれも描画します。
まだ途中なのですが、また次回にしましょう。