パックマン的ゲームの作成(チュートリアル) - C言語とelで様々なゲームを作ろう
目次
- C言語とelで様々なゲームを作ろう
- Visual C++ .NET での設定
- テンプレートファイルの解説
- シューティングゲームの作成(チュートリアル)
- パックマン的ゲームの作成(チュートリアル)
- ブロック崩しの作成
- 15パズルの作成
- 横スクロールジャンピングゲームの作成
- オセロの作成
- 神経衰弱の作成
- 7ならべの作成
- テトリスの作成
- ぷよぷよの作成
プロジェクトファイル
今回の講座のソースを全て含んだプロジェクトファイル(Visual C++ .NET)を以下に置いておきます。ソースを入力するのが面倒な人は使ってください。pacman.zip
MIDIのBGMはTAM Music Factory様の素材を使用しています。
自機が動く
まず、自機が上下左右に動くだけのプログラムを作成します。自機キャラクタの作成
ペイントソフトなどで自機画像を 32 ×32 ピクセル、24 ビットカラーの BMP 形式で作り、「jiki.bmp」という名前でプロジェクトフォルダ下の Debug フォルダに保存してください。↑自機の例
プログラムの入力
「pacman」というプロジェクトを作成して、「pacman.cpp」という新規ファイルを作り template.cpp の内容をコピーし、以下のプログラムの太字部分を追加・変更してください。
///////////////////////////////////////////////////////////////////////////////
// パックマン的なゲーム
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "ddraw.lib")
#pragma comment(lib, "dsound.lib")
#include "el.h"
#define MAIN_SCREEN 1
void MainScreen(void);
DDOBJ jiki;
///////////////////////////////////////////////////////////////////////////////
// メイン関数
int elMain("パックマン的なゲーム");
{
elWindow(640, 480, FALSE);
elLoop()
{
elSetScreen(MAIN_SCREEN, MainScreen());
}
elExitMain();
}
///////////////////////////////////////////////////////////////////////////////
// ウインドウ生成関数
void elCreate(void)
{
elDraw::Screen(640, 480);
jiki = elDraw::LoadObject("jiki.bmp");
elCallScreen(MAIN_SCREEN);
}
///////////////////////////////////////////////////////////////////////////////
// キーボード関数
void elKeyboard(void)
{
case VK_ESCAPE:
{
elDraw::Exit();
break;
}
elExitKeyboard();
}
///////////////////////////////////////////////////////////////////////////////
// イベント関数
long elEvent(void)
{
elExitEvent();
}
///////////////////////////////////////////////////////////////////////////////
// メイン画面
void MainScreen(void)
{
static int mx = 1, my = 1;
elDraw::Clear();
switch (PushKey) {
case VK_LEFT: mx--; break;
case VK_RIGHT: mx++; break;
case VK_UP: my--; break;
case VK_DOWN: my++; break;
}
elDraw::Layer(mx * 32, my * 32, jiki, 0, 0, 32, 32);
SHOW(0, 0, "ESCキーで終了");
elDraw::Refresh();
Sleep(80);
}
ビルド・実行
メニューの「ビルド」→「ビルド」を選び、ソースをビルドします。エラーがなければ、メニューの「ビルド」→「実行」を選び、プログラムを実行します。自機が画面下に表示され、カーソルキーで上下左右に移動することができれば正常です。解説
DDOBJ jiki;
DDOBJ 型は el において画像を格納する型です。
jiki = elDraw::LoadObject("jiki.bmp");
上で定義した変数 jiki に、画像を読み込みます。
static int mx = 1, my = 1;
自機の最初の座標を (1, 1) にしています。今回はのゲームでは 1 ピクセル毎ではなく、32 ピクセル毎に自機が動き、この座標を 32 倍したものになります。つまり最初の座標は (32, 32) です。
switch (PushKey) {
PushKey は直前に押されたキーが入る変数です。ここでは押されたキーによって分岐しています。
case VK_LEFT: mx--; break;
カーソルキーの左が押されると、PushKey に VK_LEFT が入ります。ここでは x 座標をひとつ減らしてしています
case VK_RIGHT: mx++; break;
カーソルキーの右が押されると、PushKey に VK_RIGHT が入ります。ここでは x 座標をひとつ増やしています
case VK_UP: my--; break;
カーソルキーの上が押されると、PushKey に VK_UP が入ります。ここでは y 座標をひとつ減らしてしています
case VK_DOWN: my++; break;
カーソルキーの下が押されると、PushKey に VK_DOWN が入ります。ここでは y 座標をひとつ増やしています
elDraw::Layer(mx * 32, my * 32, jiki, 0, 0, 32, 32);
elDraw::Layer 関数は画面にスプライトを表示する関数です。定義は以下の通り。
void elDraw::Layer(int Px, int Py, DDOBJ ObjDD, int X1, int Y1, int X2, int Y2);
画像 ObjDD の左上座標(X1, Y1) - 右下座標(X2, Y2)の領域を(Px, Py)に表示します。
Sleep(80);
32ピクセルずつ自機が動くのでは速すぎるので少し時間待ちしています。Sleepは引数で指定した時間(ミリ秒)、待ちます。今回は 80 ミリ秒待つことになります。マップの表示
次に迷路マップを表示するプログラムを作成します。マップを構成するパーツを今回は「チップ」と呼びます。チップの種類は「通路」「壁」「エサ」となります。チップの作成
横 96 × 縦 32 ピクセルの画像を作り、左から 32 ピクセルずつ通路・壁・エサの画像を描き「chip.bmp」という名前でプロジェクトフォルダ下の Debug フォルダに保存してください。↑チップの例
プログラムの入力
pacman.cpp の以下の太字部分を追加してください。「・・・」はソースが省略されている部分です。配列 map の数字の意味は、0が通路、1が壁、2がエサとなります。適当に配置してください。
・・・
void MainScreen(void);
DDOBJ jiki, chip;
int map[15][20] = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,2,2,2,2,2,2,2,1,0,0,0,0,0,1},
{1,0,0,0,0,0,2,2,2,2,2,2,2,1,0,0,0,0,0,1},
{1,0,0,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,0,1},
{1,2,2,0,1,0,2,2,2,1,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,1,0,2,2,2,1,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,0,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,0,0,1,1,1,1,1,1,1,1,0,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,0,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,1,2,2,2,0,1,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,1,2,2,2,0,1,0,0,2,2,1},
{1,0,0,0,1,1,0,0,0,1,1,1,0,0,1,1,0,2,2,1},
{1,0,0,0,0,1,2,2,2,2,2,2,2,0,0,0,0,0,0,1},
{1,0,0,0,0,1,2,2,2,2,2,2,2,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
};
・・・
jiki = elDraw::LoadObject("jiki.bmp");
chip = elDraw::LoadObject("chip.bmp");
elCallScreen(MAIN_SCREEN);
・・・
switch (PushKey) {
case VK_LEFT: mx--; break;
case VK_RIGHT: mx++; break;
case VK_UP: my--; break;
case VK_DOWN: my++; break;
}
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 20; x++) {
elDraw::Layer(x * 32, y * 32, chip, map[y][x] * 32, 0, map[y][x] * 32 + 32, 32);
}
}
elDraw::Layer(mx * 32, my * 32, jiki, 0, 0, 32, 32);
・・・
実行
ビルドして実行します。マップが画面上に表示されれば正常です。解説
int map[15][20] = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,2,2,2,2,2,2,2,1,0,0,0,0,0,1},
{1,0,0,0,0,0,2,2,2,2,2,2,2,1,0,0,0,0,0,1},
{1,0,0,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,0,1},
{1,2,2,0,1,0,2,2,2,1,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,1,0,2,2,2,1,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,1,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,0,0,1,1,1,1,1,1,1,1,0,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,1,2,2,2,0,0,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,1,2,2,2,0,1,0,0,2,2,1},
{1,2,2,0,0,0,2,2,2,1,2,2,2,0,1,0,0,2,2,1},
{1,0,0,0,1,1,0,0,0,1,1,1,0,0,1,1,0,2,2,1},
{1,0,0,0,0,1,2,2,2,2,2,2,2,0,0,0,0,0,0,1},
{1,0,0,0,0,1,2,2,2,2,2,2,2,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
};
マップ画像を2次元配列で作成します。要素の値として、0が通路、1が壁、2がエサという意味を持たせています。
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 20; x++) {
elDraw::Layer(x * 32, y * 32, chip, map[y][x] * 32, 0, map[y][x] * 32 + 32, 32);
}
}
マップを実際に描画している部分です。縦方向に 15 回、横方向に 20 回ループしています。map[y][x] は現在の位置の要素の値を調べ、それに応じて chip.bmp の中から範囲を決めて描画しています。map[y][x] が 0 の場合は (0, 0) - (32, 32) つまり通路を描画します。
map[y][x] が 1 の場合は (32, 0) - (64, 32) つまり壁を描画します。
map[y][x] が 2 の場合は (64, 0) - (96, 32) つまりエサを描画します。
壁との当たり判定
次に自機が壁をすり抜けないように壁との当たり判定を行います。プログラムの入力
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
static int mx = 1, my = 1;
elDraw::Clear();
int kx = mx, ky = my;
switch (PushKey) {
case VK_LEFT: kx--; break;
case VK_RIGHT: kx++; break;
case VK_UP: ky--; break;
case VK_DOWN: ky++; break;
}
if (map[ky][kx] != 1) {
mx = kx; my = ky;
}
・・・
実行
ビルドして実行します。自機が壁で止まるようになれば正常です。解説
int kx = mx, ky = my;
自機が移動した後に壁があった場合、元の位置に戻さなければならないため、なんらかの方法で元の座標を保存しておく必要があります。今回は仮の座標 (kx, ky) を作り、それに対して移動の操作を行うことによって、元の座標に戻れるようにしています。
if (map[ky][kx] != 1) {
mx = kx; my = ky;
}
仮の座標で移動した先が壁(1)でないとき、初めて本当の座標 (mx, my) に仮の座標を代入します。エサを食べる&スコアを追加
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
static int mx = 1, my = 1;
static int score = 0;
elDraw::Clear();
int kx = mx, ky = my;
switch (PushKey) {
case VK_LEFT: kx--; break;
case VK_RIGHT: kx++; break;
case VK_UP: ky--; break;
case VK_DOWN: ky++; break;
}
if (map[ky][kx] != 1) {
mx = kx; my = ky;
}
if (map[my][mx] == 2) {
map[my][mx] = 0;
score += 10;
}
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 20; x++) {
elDraw::Layer(x * 32, y * 32, chip, map[y][x] * 32, 0, map[y][x] * 32 + 32, 32);
}
}
elDraw::Layer(mx * 32, my * 32, jiki, 0, 0, 32, 32);
SHOW2(0, 0, "SCORE:%d", score);
elDraw::Refresh();
Sleep(80);
}
解説
if (map[my][mx] == 2) {
map[my][mx] = 0;
score += 10;
}
自機が移動した先がエサ(2)だった場合は通路(0)にすることによって「エサを食べる」ということを表しています。さらに変数 score に得点として 10 点を加算しています。エサを食べたときの音を出す
エサを食べたときの音をwaveファイルで用意し「eat.wav」という名前にしてプロジェクトフォルダ下の Debug フォルダに保存してください。フリー素材を使うと楽です。プログラムの入力
pacman.cpp の以下の太字黒字部分を追加・変更してください。
・・・
DDOBJ jiki, chip;
DSOBJ eat;
・・・
chip = elDraw::LoadObject("chip.bmp");
eat = elSound::LoadObject("eat.wav");
・・・
if (map[my][mx] == 2) {
map[my][mx] = 0;
score += 10;
elSound::Play(eat);
}
・・・
解説
DSOBJ eat;
el で効果音(waveファイル)を扱うときは DSOBJ 型の変数を使います。
eat = elSound::LoadObject("eat.wav");
音声を変数に読み込んでいます。これによってメモリ上に音声データが読み込まれますので、効果音を素早く再生することが可能です。
elSound::Play(eat);
読み込んだ音声をエサを食べたタイミングで再生しています。BGMを流す
BGMをMIDIファイルで用意し「bgm.mid」という名前にしてプロジェクトフォルダ下の Debug フォルダに保存してください。フリー素材を使うと楽です。プログラムの入力
pacman.cpp の以下の太字黒字部分を追加・変更してください。
・・・
eat = elSound::LoadObject("eat.wav");
elMusic::Play("bgm.mid");
elCallScreen(MAIN_SCREEN);
・・・
解説
elMusic::Play("bgm.mid");
この関数で引数て指定したMIDIファイルを演奏します。敵を追加
次に敵を追加します。最初は敵が自機に向かってまっしぐらに向かってくるようにします。敵キャラクターの作成
横 32 × 縦 32 ピクセルの敵画像を作り、「teki.bmp」という名前でプロジェクトフォルダ下の Debug フォルダに保存してください。↑敵の例
プログラムの入力
pacman.cpp の以下の太字黒字部分を追加・変更してください。
・・・
void MainScreen(void);
DDOBJ jiki, chip, teki;
DSOBJ eat;
・・・
chip = elDraw::LoadObject("chip.bmp");
teki = elDraw::LoadObject("teki.bmp");
eat = elSound::LoadObject("eat.wav");
・・・
score += 10;
elSound::Play(eat);
}
kx = ex; ky = ey;
if (kx > mx) kx--;
if (kx < mx) kx++;
if (ky > my) ky--;
if (ky < my) ky++;
if (map[ky][kx] != 1) {
ex = kx; ey = ky;
}
・・・
elDraw::Layer(mx * 32, my * 32, jiki, 0, 0, 32, 32);
elDraw::Layer(ex * 32, ey * 32, teki, 0, 0, 32, 32);
SHOW2(0, 0, "SCORE:%d", score);
・・・
解説
kx = ex; ky = ey;
自機と同じく、壁に当たったときに元の座標に戻れるように仮の座標に代入しています。
if (kx > mx) kx--;
敵のX座標が自機のX座標よりも大きければ敵のX座標を減らすことによって自機に向かって来るようにします。
if (kx < mx) kx++;
敵のX座標が自機のX座標よりも小さければ敵のY座標を増やすことによって自機に向かって来るようにします。
if (ky > my) ky--;
if (ky < my) ky++;
Y方向についても同じ処理を行います。これによって敵が自機にまっしぐらに向かってくるようになります。敵の移動にバリエーションを出す
上記の敵の動きでは壁に当たると全く動けなくなる・すぐに自機に追いついてしまう、などの問題があります。そこで少しランダム性を取り入れ、敵の動きを自然な感じにします。プログラムの入力
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
kx = ex; ky = ey;
if (rand() % 3 == 0) {
if (kx > mx) kx--;
if (kx < mx) kx++;
if (ky > my) ky--;
if (ky < my) ky++;
} else {
kx += rand() % 3 - 1;
ky += rand() % 3 - 1;
}
if (map[ky][kx] != 1) {
ex = kx; ey = ky;
}
・・・
解説
if (rand() % 3 == 0) {
この if 文の条件が真の場合、自機を追いかけるようになり、偽の場合、ランダムに動くようになります。つまり、1/3 の確率で自機を追いかけ、2/3 の確率でランダムに動きます。
kx += rand() % 3 - 1;
ky += rand() % 3 - 1;
X, Y座標とも -1, 0, 1 のいずれかの値を加算し、ランダムに動かしている部分です。ゲームクリア処理
エサを全部食べたときにゲームクリアにする処理を記述します。プログラムの入力
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
DSOBJ eat;
int esaNum = 0;
int map[15][20] = {
・・・
elDraw::Screen(640, 480);
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 20; x++) {
if (map[y][x] == 2) esaNum++;
}
}
jiki = elDraw::LoadObject("jiki.bmp");
・・・
score += 10;
esaNum--;
elSound::Play(eat);
・・・
Sleep(80);
if (esaNum <= 0) {
MESG("ゲームクリア!");
elDraw::Exit();
}
}
解説
int esaNum = 0;
エサの数を保持する変数です。2次元配列 map の中からエサの数をカウントするので、最初は 0 にしておきます。
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 20; x++) {
if (map[y][x] == 2) esaNum++;
}
}
エサの数をカウントしている部分です。map の要素を順に見て行き、エサ(2)だったら esaNum をインクリメントします。
esaNum--;
エサを食べたとき、カウントをひとつ減らします。
if (esaNum <= 0) {
MESG("ゲームクリア!");
elDraw::Exit();
}
カウントが 0、つまり全てのエサを食べたときにゲームクリアとします。MESG は el の関数でポップアップウインドウを出します。elDraw::Exit 関数によりプログラムを終了させます。ゲームオーバー処理
自機と敵が当たったときにゲームオーバーになる処理を記述します。プログラムの入力
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
if (esaNum <= 0) {
MESG("ゲームクリア!");
elDraw::Exit();
}
if (mx == ex && my == ey) {
MESG("ゲームオーバー");
elDraw::Exit();
}
}
解説
if (mx == ex && my == ey) {
MESG("ゲームオーバー");
elDraw::Exit();
}
自機と敵の座標が一致したときにゲームオーバーとします。移動方向によってキャラクタを変える
自機がどちらの方向へ移動していても同じキャラクタでは動いていることが分かり難いので、移動方向によってキャラクタを変える処理を行います。方向付き自機キャラクタの作成
jiki.bmp を変更して、サイズを横 128 × 縦 32 ピクセルとし、左から順に 32 ピクセルずつ右向き・下向き・左向き・上向きのキャラクタを描きます。↑自機の例
プログラムの入力
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
static int mx = 1, my = 1;
static int md = 0;
static int ex = 18, ey = 13;
・・・
switch (PushKey) {
case VK_LEFT: kx--; md = 2; break;
case VK_RIGHT: kx++; md = 0; break;
case VK_UP: ky--; md = 3; break;
case VK_DOWN: ky++; md = 1; break;
}
・・・
elDraw::Layer(mx * 32, my * 32, jiki, md * 32, 0, md * 32 + 32, 32);
・・・
解説
static int md = 0;
自機の方向を表す変数です。0が右、1が下、2が左、3が上となります。
case VK_LEFT: kx--; md = 2; break;
case VK_RIGHT: kx++; md = 0; break;
case VK_UP: ky--; md = 3; break;
case VK_DOWN: ky++; md = 1; break;
自機がそれぞれの方向へ移動したとき、変数 md に適切な値を入れるようにしています。
elDraw::Layer(mx * 32, my * 32, jiki, md * 32, 0, md * 32 + 32, 32);
自機の画像から適切な方向のキャラクタを切り出して描画しています。md が 0 つまり右向きのときは画像の (0, 0) - (32, 32)
md が 1 つまり下向きのときは画像の (32, 0) - (64, 32)
md が 2 つまり左向きのときは画像の (64, 0) - (96, 32)
md が 3 つまり上向きのときは画像の (96, 0) - (128, 32)
がそれぞれ描画されます。
自機にアニメーションを付ける
自機にエサを食べるアニメーションを付けます。自機キャラクタの作成
jiki.bmp を変更して、サイズを横 128 × 縦 64 ピクセルとし、下半分にアニメーションをしたキャラクタを描きます。↑自機の例
プログラムの入力
pacman.cpp の以下の太字部分を追加・変更してください。
・・・
static int md = 0;
static int ma = 0;
static int ex = 18, ey = 13;
・・・
esaNum--;
elSound::Play(eat);
}
ma = 1 - ma;
・・・
elDraw::Layer(mx * 32, my * 32, jiki, md * 32, ma * 32, md * 32 + 32, ma * 32 + 32);
・・・
解説
static int ma = 0;
自機のアニメーションを表す変数です。0のとき上半分の画像、1のとき下半分の画像が描画されます。
ma = 1 - ma;
今回は2パターンのアニメーションなので、ma に 0 と 1 が交互に入るようにします。
elDraw::Layer(mx * 32, my * 32, jiki, md * 32, ma * 32, md * 32 + 32, ma * 32 + 32);
ma が 0 のとき上半分のキャラクタ、1 のとき下半分のキャラクタを描画するようにします。