コラム
2024/12/24
第94回 プログラミングについて『テトリスゲームを作ろう! その2』

前回はウインドウを開くところまでを作成しました。今回はテトリスゲームを作る上で必要なことと、決めておかなければならないことを考えてみます。
まずどうしても必要なことは、テトリスゲームはゲームのプレイ中はテトロミノが勝手に上から落ちてこなければなりませんので、その仕組みをどうするかです。何種類か方法が考えられますが、最初に考えたのが PeekMessage という関数を使う方法でした。
こ関数はメッセージキューから目的のメッセージを取得したり、取得後そのメッセージを削除できる関数で、
MSG msg;
while(1){
if(PeekMessage(&msg,m_hWnd,0,-1,PM_REMOVE)){
:
:
}
}
とする方法です。取得したメッセージが目的のものでなければ本来の処理をさせようと考えたのですが、どうもスマートな方法ではないようです。
次に、OnIdle を使用する方法です。OnIdle というのは CWinApp クラスのメンバーで、メッセージが何もないときに呼び出される関数です。この関数が呼び出されたときに新しくテトロミノを生成したり、表示中のテトロミノを落下させるようにしようという訳です。
BOOL TetrisApp::OnIdle(LONG lCount)
{
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
return TRUE;
}
このメンバー関数の詳細はVisual C++のヘルプを参照して下さい。
Windowsが OnIdle を呼び出したときに目的の動作を行なうように上の様にオーバーライドし、テトリスのウインドウに ID_IDLE コマンドのメッセージを送るようにしています。ID_IDLE は自分で適当な番号で定義しておきます。
PostMessage はメッセージを転送した後は、その処理がされたか否かにかかわらず呼び出し側に処理が戻ってきます。またこの関数の戻り値として TRUE を返すと再びメッセージキューが空になると呼び出されます。転送されるメッセージは m_pMainWnd 即ちTetris クラスのインスタンスに送られますので Tetris 側では、
private:
afx_msg void OnIdle();
と宣言しておき、メッセージマップに
ON_COMMAND(ID_IDLE,OnIdle)
を追加します。更に本体は、
afx_msg void Tetris::OnIdle()
{
:
:
}
として処理します。実際にこの方法が一番スマートに目的を達成できるようですので、この方法を採用しました。
テトリスゲームは0.5秒間隔ぐらいでテトロミノを落下させますが、上記の方法だけではものすごい勢いで落下してしまいます(それくらい頻繁にメッセージキューが
空になってしまいます)。ですので、任意のインターバルで処理をしなければなりません。
Tetris::OnIdle が呼び出される毎に時間を計測して任意のインターバルで処理をすればいいことになります。次のものは、その機能を入れた全リストです。
#include <afxwin.h>
#define BLOCK_SIZE 8
#define GRID_SIZE 10
#define GRID_SUMY 22
#define GRID_SUMX 10
#define TOP_MARGIN 3
//(1)
#define INITIAL_SPEED 500
#define DELTA_SPEED 20
//(2)
#define ID_IDLE 100
class TetrisApp : public CWinApp
{
//(3)
private:
virtual BOOL OnIdle(LONG lCount);
public:
virtual BOOL InitInstance();
};
class Tetris : public CFrameWnd
{
//(4)
private:
DWORD old_time;
DWORD speed;
afx_msg void OnIdle();
public:
Tetris();
//(5)
DECLARE_MESSAGE_MAP()
};
class TetrisApp TetrisApp;
BOOL TetrisApp::InitInstance()
{
m_pMainWnd = new class Tetris;
return TRUE;
}
//(6)
BOOL TetrisApp::OnIdle(LONG lCount)
{
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
return TRUE;
}
//(7)
BEGIN_MESSAGE_MAP(Tetris,CFrameWnd)
ON_COMMAND(ID_IDLE,OnIdle)
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);
//(8)
old_time=0;
speed =INITIAL_SPEED;
}
//(9)
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);
}
}
(1)でインターバル時間を定義しています。ゲーム開始時のインターバル時間としたいので定数名は INITIAL_SPEED としています。また DELTA_SPEED は10点毎にインターバル間隔を縮めたいので未だ使ってはいませんが定義しておきました。
#define INITIAL_SPEED 500
#define DELTA_SPEED 20
(2)ではアイドルメッセージの番号を定義しています。
#define ID_IDLE 100
(3)では TetrisApp クラスにプライベート属性でメンバー関数 OnIdle を宣言しているもので、
private:
virtual BOOL OnIdle(LONG lCount);
(6)でその本体を定義しています。
BOOL TetrisApp::OnIdle(LONG lCount)
{
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
return TRUE;
}
pMainWnd は CWinApp クラスのデータメンバーで、InitInstance で、
m_pMainWnd = new class Tetris;
として Tetris クラスのインスタンスへのポインタを保持していますので、
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
とすることで、Tetris クラスのインスタンスに ID_IDLE コマンドメッセージを転送することになります。
(4)は Tetris クラスにプライベート属性で、データメンバーとして old_time とspeed を定義しています。old_time は Tetris クラスのインスタンスが ID_IDLEメッセージによってテトロミノを作成したか、落下させたかの1つ前の時間を保持する変数で、speed はインターバル時間を保持するものです。
private:
DWORD old_time;
DWORD speed;
これらのデータメンバーは Tetris::Tetris コンストラクタ内で初期化(6)します。
old_time=0;
speed =INITIAL_SPEED;
また、メンバー関数 OnIdle もここで宣言しておきます。
(5)では、今回からのプログラムでメッセージを処理しますので、メッセージマップのハンドラの生成を宣言しておきます。
DECLARE_MESSAGE_MAP()
メッセージマップは(7)で本体を定義しています。
BEGIN_MESSAGE_MAP(Tetris,CFrameWnd)
ON_COMMAND(ID_IDLE,OnIdle)
END_MESSAGE_MAP()
(9)では Tetris クラスのメンバー関数 OnIdle を定義していて、動作のテストをするためにウインドウのタイトルを順次書き換えるように仮のコードにしています。
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);
}
}
GetTickCount 関数は Visual C++の古いバージョンでは CetCurrentTimeだったのですが、新しい関数が用意されたようなのでこれを使いました。
この OnIdle では呼び出されたときの時間と前の時間を比較してインターバル時間以上になったら、ウインドウのタートルバーにその回数を表示します。
今回はプログラムのリストについてはここまでです。次に決めておかなければならないことを考えましょう。
まず落下するテトロミノをどのようにプログラム内で表現しておくかです。
・7種類のテトロミノの組み合わせにそれぞれ番号を付けておき、番号と回転角度で表現する。
・4x2の配列と回転角度で表現する。
・4x4の配列で表現する。
どの方法でも実現できるはずですが、今回は3番目の4x4の2次元の配列を使った方法にし、できるだけプログラムを簡単にしてみようと考えてみました。
そこで、次の様に形状に番号を付けておき、
□□□□ □□□□ □□□□ □□□□
■■■■ ■■■□ ■■■□ ■■■□
□□□□ □■□□ □□■□ ■□□□
□□□□ □□□□ □□□□ □□□□
1 2 3 4
□□□□ □□□□ □□□□
■■□□ □■■□ ■■□□
□■■□ ■■□□ ■■□□
□□□□ □□□□ □□□□
5 6 7
これを回転させようという魂胆です。この図をよく見て気がつくのは回転されるときにどこを基準に回転させるかがむずかしそうです。この図からは、上から2番目、左から2番目の位置が中心位置として良さそうですが、単純にここを中心として左に回転させると、配列から飛び出してしまいます(下図)。
■
□■□□
□■□□
□■□□
□□□□
それならば、回転の中心位置も回転させてしまえばいいことになります。1を左に回転させた後の中心の位置が、上から3番目、左から2番目になるようにすれば、次の図のようになります。
□■□□
□■□□
□■□□
□■□□
ここまで考えて、だんだん面倒になってしまいました。回転の中心の位置が2次元の配列の中心の位置にないのが混乱の原因なのです。そこで、5x5の配列を考えてみました。
□□□□□ □□□□□ □□□□□ □□□□□
□□□□□ □□□□□ □□□□□ □□□□□
□■■■■ □■■■□ □■■■□ □■■■□
□□□□□ □□■□□ □□□■□ □■□□□
□□□□□ □□□□□ □□□□□ □□□□□
1 2 3 4
□□□□□ □□□□□ □□□□□
□□□□□ □□□□□ □□□□□
□■■□□ □□■■□ □■■□□
□□■■□ □■■□□ □■■□□
□□□□□ □□□□□ □□□□□
5 6 7
こうすれば回転の中心は5x5の配列の中心になり、はみ出してしまうこともありません。これは都合がいいということで、この方法を採用することにしました。
この辺でまた次回にしましょう。
まずどうしても必要なことは、テトリスゲームはゲームのプレイ中はテトロミノが勝手に上から落ちてこなければなりませんので、その仕組みをどうするかです。何種類か方法が考えられますが、最初に考えたのが PeekMessage という関数を使う方法でした。
こ関数はメッセージキューから目的のメッセージを取得したり、取得後そのメッセージを削除できる関数で、
MSG msg;
while(1){
if(PeekMessage(&msg,m_hWnd,0,-1,PM_REMOVE)){
:
:
}
}
とする方法です。取得したメッセージが目的のものでなければ本来の処理をさせようと考えたのですが、どうもスマートな方法ではないようです。
次に、OnIdle を使用する方法です。OnIdle というのは CWinApp クラスのメンバーで、メッセージが何もないときに呼び出される関数です。この関数が呼び出されたときに新しくテトロミノを生成したり、表示中のテトロミノを落下させるようにしようという訳です。
BOOL TetrisApp::OnIdle(LONG lCount)
{
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
return TRUE;
}
このメンバー関数の詳細はVisual C++のヘルプを参照して下さい。
Windowsが OnIdle を呼び出したときに目的の動作を行なうように上の様にオーバーライドし、テトリスのウインドウに ID_IDLE コマンドのメッセージを送るようにしています。ID_IDLE は自分で適当な番号で定義しておきます。
PostMessage はメッセージを転送した後は、その処理がされたか否かにかかわらず呼び出し側に処理が戻ってきます。またこの関数の戻り値として TRUE を返すと再びメッセージキューが空になると呼び出されます。転送されるメッセージは m_pMainWnd 即ちTetris クラスのインスタンスに送られますので Tetris 側では、
private:
afx_msg void OnIdle();
と宣言しておき、メッセージマップに
ON_COMMAND(ID_IDLE,OnIdle)
を追加します。更に本体は、
afx_msg void Tetris::OnIdle()
{
:
:
}
として処理します。実際にこの方法が一番スマートに目的を達成できるようですので、この方法を採用しました。
テトリスゲームは0.5秒間隔ぐらいでテトロミノを落下させますが、上記の方法だけではものすごい勢いで落下してしまいます(それくらい頻繁にメッセージキューが
空になってしまいます)。ですので、任意のインターバルで処理をしなければなりません。
Tetris::OnIdle が呼び出される毎に時間を計測して任意のインターバルで処理をすればいいことになります。次のものは、その機能を入れた全リストです。
#include <afxwin.h>
#define BLOCK_SIZE 8
#define GRID_SIZE 10
#define GRID_SUMY 22
#define GRID_SUMX 10
#define TOP_MARGIN 3
//(1)
#define INITIAL_SPEED 500
#define DELTA_SPEED 20
//(2)
#define ID_IDLE 100
class TetrisApp : public CWinApp
{
//(3)
private:
virtual BOOL OnIdle(LONG lCount);
public:
virtual BOOL InitInstance();
};
class Tetris : public CFrameWnd
{
//(4)
private:
DWORD old_time;
DWORD speed;
afx_msg void OnIdle();
public:
Tetris();
//(5)
DECLARE_MESSAGE_MAP()
};
class TetrisApp TetrisApp;
BOOL TetrisApp::InitInstance()
{
m_pMainWnd = new class Tetris;
return TRUE;
}
//(6)
BOOL TetrisApp::OnIdle(LONG lCount)
{
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
return TRUE;
}
//(7)
BEGIN_MESSAGE_MAP(Tetris,CFrameWnd)
ON_COMMAND(ID_IDLE,OnIdle)
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);
//(8)
old_time=0;
speed =INITIAL_SPEED;
}
//(9)
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);
}
}
(1)でインターバル時間を定義しています。ゲーム開始時のインターバル時間としたいので定数名は INITIAL_SPEED としています。また DELTA_SPEED は10点毎にインターバル間隔を縮めたいので未だ使ってはいませんが定義しておきました。
#define INITIAL_SPEED 500
#define DELTA_SPEED 20
(2)ではアイドルメッセージの番号を定義しています。
#define ID_IDLE 100
(3)では TetrisApp クラスにプライベート属性でメンバー関数 OnIdle を宣言しているもので、
private:
virtual BOOL OnIdle(LONG lCount);
(6)でその本体を定義しています。
BOOL TetrisApp::OnIdle(LONG lCount)
{
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
return TRUE;
}
pMainWnd は CWinApp クラスのデータメンバーで、InitInstance で、
m_pMainWnd = new class Tetris;
として Tetris クラスのインスタンスへのポインタを保持していますので、
m_pMainWnd->PostMessage(WM_COMMAND,ID_IDLE);
とすることで、Tetris クラスのインスタンスに ID_IDLE コマンドメッセージを転送することになります。
(4)は Tetris クラスにプライベート属性で、データメンバーとして old_time とspeed を定義しています。old_time は Tetris クラスのインスタンスが ID_IDLEメッセージによってテトロミノを作成したか、落下させたかの1つ前の時間を保持する変数で、speed はインターバル時間を保持するものです。
private:
DWORD old_time;
DWORD speed;
これらのデータメンバーは Tetris::Tetris コンストラクタ内で初期化(6)します。
old_time=0;
speed =INITIAL_SPEED;
また、メンバー関数 OnIdle もここで宣言しておきます。
(5)では、今回からのプログラムでメッセージを処理しますので、メッセージマップのハンドラの生成を宣言しておきます。
DECLARE_MESSAGE_MAP()
メッセージマップは(7)で本体を定義しています。
BEGIN_MESSAGE_MAP(Tetris,CFrameWnd)
ON_COMMAND(ID_IDLE,OnIdle)
END_MESSAGE_MAP()
(9)では Tetris クラスのメンバー関数 OnIdle を定義していて、動作のテストをするためにウインドウのタイトルを順次書き換えるように仮のコードにしています。
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);
}
}
GetTickCount 関数は Visual C++の古いバージョンでは CetCurrentTimeだったのですが、新しい関数が用意されたようなのでこれを使いました。
この OnIdle では呼び出されたときの時間と前の時間を比較してインターバル時間以上になったら、ウインドウのタートルバーにその回数を表示します。
今回はプログラムのリストについてはここまでです。次に決めておかなければならないことを考えましょう。
まず落下するテトロミノをどのようにプログラム内で表現しておくかです。
・7種類のテトロミノの組み合わせにそれぞれ番号を付けておき、番号と回転角度で表現する。
・4x2の配列と回転角度で表現する。
・4x4の配列で表現する。
どの方法でも実現できるはずですが、今回は3番目の4x4の2次元の配列を使った方法にし、できるだけプログラムを簡単にしてみようと考えてみました。
そこで、次の様に形状に番号を付けておき、
□□□□ □□□□ □□□□ □□□□
■■■■ ■■■□ ■■■□ ■■■□
□□□□ □■□□ □□■□ ■□□□
□□□□ □□□□ □□□□ □□□□
1 2 3 4
□□□□ □□□□ □□□□
■■□□ □■■□ ■■□□
□■■□ ■■□□ ■■□□
□□□□ □□□□ □□□□
5 6 7
これを回転させようという魂胆です。この図をよく見て気がつくのは回転されるときにどこを基準に回転させるかがむずかしそうです。この図からは、上から2番目、左から2番目の位置が中心位置として良さそうですが、単純にここを中心として左に回転させると、配列から飛び出してしまいます(下図)。
■
□■□□
□■□□
□■□□
□□□□
それならば、回転の中心位置も回転させてしまえばいいことになります。1を左に回転させた後の中心の位置が、上から3番目、左から2番目になるようにすれば、次の図のようになります。
□■□□
□■□□
□■□□
□■□□
ここまで考えて、だんだん面倒になってしまいました。回転の中心の位置が2次元の配列の中心の位置にないのが混乱の原因なのです。そこで、5x5の配列を考えてみました。
□□□□□ □□□□□ □□□□□ □□□□□
□□□□□ □□□□□ □□□□□ □□□□□
□■■■■ □■■■□ □■■■□ □■■■□
□□□□□ □□■□□ □□□■□ □■□□□
□□□□□ □□□□□ □□□□□ □□□□□
1 2 3 4
□□□□□ □□□□□ □□□□□
□□□□□ □□□□□ □□□□□
□■■□□ □□■■□ □■■□□
□□■■□ □■■□□ □■■□□
□□□□□ □□□□□ □□□□□
5 6 7
こうすれば回転の中心は5x5の配列の中心になり、はみ出してしまうこともありません。これは都合がいいということで、この方法を採用することにしました。
この辺でまた次回にしましょう。