技術文章> 手を動かしてさくさく理解する C/C++ コンソールアプリ入門 > ドットイートゲーム
ドットイートゲームは自機を操り、画面上のドットをすべて消す(食べる)アクションゲームだ。
オリジナルは、セガが1979年発表の「ヘッドオン」だが、翌年ナムコが発表した「パックマン」が世界的に大ヒットし、
(特にUSでは)バリエーションも多く作られている。
参照:ドットイートゲーム
本稿では、下図のような「ヘッドオン」と同じようなゲームソースの解説を行う。

本ゲームは動きが多く、単純に画面書き換えを行うと、画面がチラツキ見るに耐えなかったので、 「ダブルバッファ」を実装し、それを利用することにした。
「ダブルバッファ」はここで別途解説してるので、それを参照されたい。
本プログラムでは、車やマップの (x, y) 2次元座標を表すために Vec2 型を導入している。
typedef std::pair<int, int> Vec2; // (x, y) ベクトル
構造体を定義するのではなく、std::pair で簡易的に型定義を行っている。
2次元座標ベクトルは、通常演算子で比較、加算、減算ができると、コードが分かりやすく記述できて便利である。
演算子オーバーロードを用いて上記演算子を以下のように定義している。
bool operator==(const Vec2 &a, const Vec2 &b)
{
return a.first == b.first && a.second == b.second;
}
Vec2 operator+(const Vec2 &a, const Vec2 &b)
{
return Vec2(a.first + b.first, a.second + b.second);
}
Vec2 operator-(const Vec2 &a, const Vec2 &b)
{
return Vec2(a.first - b.first, a.second - b.second);
}
また、四隅での方向転換処理のために、左右に90度回転する関数も以下のように定義している。
// x' (cosθ - sinθ) x
// y' (sinθ + cosθ) y
Vec2 rot_right90(const Vec2 &v)
{
return Vec2(-v.second, v.first);
}
Vec2 rot_left90(const Vec2 &v)
{
return Vec2(v.second, -v.first);
}
演習問題:
自機、敵機を表す構造体を以下のように定義している。
struct Car
{
public:
Car(int x = 0, int y = 0, int dx = 0, int dy = 0, int lane = 0)
: m_pos(x, y), m_v(dx, dy)
, m_lane(lane)
, m_laneChanged(false)
{}
public:
Vec2 m_pos; // 位置
Vec2 m_v; // 速度
int m_lane; // 一番外側が0
bool m_laneChanged; // レーンチェンジした直後かどうか
};
各データを指定したコンストラクタとコピーコンストラクタを定義。
データメンバには、車の位置、速度ベクトル、レーン番号、レーンチェンジしたかどうかのフラグを用意した。
位置に速度を足し込んだものが次の位置となる。
レーン番号は、車が今どのレーンにいるかを示すもの。一番外側が0で、一番内側が4だ。
敵機は自機のレーン番号との差異を見て、レーン変更を行う。自機・敵機の位置座標からレーンを毎回計算してもいいのだが、
コードが複雑になるので、情報として保持しておくことにした。
自機は最大2レーン移動することができるが、敵機は1レーンしか移動できない仕様とした。
それを実現するために、レーン移動済みフラグを導入し、これが true であればレーン移動処理を行わないこととした。
#define CONS_WD 80 #define CONS_HT 25 #define MAP_WD (CONS_WD/2) #define MAP_HT (CONS_HT-1) #define SCORE_X 11*2 #define SCORE_Y 11 #define LOOP_INTERVAL 10
intg_score;// スコア Car g_car; // 自機 intg_nEnemy;// 敵機数 std::vector<Car> g_enemy; // 敵機 char g_map[MAP_HT][MAP_WD];// マップ DblBuffer g_db; // ちらつき防止表示用ダブルバッファ
const char *mapData[] = {
"/--------------------------------------#",
"| . . . . . . . . . . . . . . . . . . |",
"|./---------------- ----------------#.|",
"| | . . . . . . . . . . . . . . . . | |",
"|.|./-------------- --------------#.|.|",
"| | | . . . . . . . . . . . . . . | | |",
"|.|.|./------------ ------------#.|.|.|",
"| | | | . . . . . . . . . . . . | | | |",
"|.|.|.|./---------- ----------#.|.|.|.|",
"| | | | | . . . . . . . . . . | | | | |",
"|.|.|.|.|./------------------#.|.|.|.|.|",
"| | | |",
"| | | |",
"|.|.|.|.|.L------------------J.|.|.|.|.|",
"| | | | | . . . . . . . . . . | | | | |",
"|.|.|.|.L---------- ----------J.|.|.|.|",
"| | | | . . . . . . . . . . . . | | | |",
"|.|.|.L------------ ------------J.|.|.|",
"| | | . . . . . . . . . . . . . . | | |",
"|.|.L-------------- --------------J.|.|",
"| | . . . . . . . . . . . . . . . . | |",
"|.L---------------- ----------------J.|",
"| . . . . . . . . . . . . . . . . . . |",
"L--------------------------------------J",
0,
};
void init_map()
{
for (int y = 0; mapData[y] != 0; ++y) {
for (int x = 0; x < MAP_WD; ++x) {
g_map[y][x] = mapData[y][x];
}
}
}
上記はマップデータ初期値と、それを g_map[][] に設定する関数。
マップの各場所の値は char 1文字で表している。このデータを適当に変えれば、違うマップにすることも可能だ。
void add_enemy()
{
g_enemy.clear();
g_enemy.push_back(Car(MAP_WD / 2 + 1, 1, 1, 0));
if( g_nEnemy == 1 ) return;
g_enemy.push_back(Car(MAP_WD / 2 + 1, MAP_HT - 2, -1, 0));
if( g_nEnemy == 2 ) return;
g_enemy.push_back(Car(9, MAP_HT / 2 + 1, -1, 0, /*lane:*/4));
}
上記は敵機を生成し、動的配列 g_enemy に追加する関数。
敵機数 g_nEnemy を参照し、その個数だけ生成・追加している。
敵機の位置は、上部、下部、左部とし、速度ベクトルは走る方向に正しく設定している。
void init()
{
g_car = Car(MAP_WD / 2 - 1, 1, -1, 0);
add_enemy();
init_map();
}
上記は1ゲームごとの初期化関数。
自機・敵機を生成し、マップを初期化している。
int draw_map()
{
int nDot = 0;
for (int y = 0; y < MAP_HT; ++y) {
g_db.setCursorPos(0, y);
const char *ptr = g_map[y];
for (int x = 0; x < MAP_WD; ++x) {
switch (*ptr++) {
case '-':
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.write("━");
break;
case '|':
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.write("┃");
break;
case '/':
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.write("┏");
break;
case '#':
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.write("┓");
break;
case 'L':
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.write("┗");
break;
case 'J':
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.write("┛");
break;
case '.':
g_db.setColor(DblBuffer::YELLOW, DblBuffer::BLACK);
g_db.write("・");
++nDot;
break;
default:
g_db.setColor(DblBuffer::BLACK, DblBuffer::BLACK);
g_db.write(" ");
break;
}
}
}
return nDot;
}
void draw_car()
{
g_db.setCursorPos(g_car.m_x*2, g_car.m_y);
g_db.setColor(DblBuffer::GREEN, DblBuffer::BLACK);
g_db.write("@");
}
void draw_enemy()
{
g_db.setColor(DblBuffer::RED, DblBuffer::BLACK);
for (int i = 0; i < (int)g_enemy.size(); ++i) {
g_db.setCursorPos(g_enemy[i].m_x*2, g_enemy[i].m_y);
g_db.write("◆");
}
}
std::string to_string(int v)
{
if( !v ) return std::string("0");
std::string str;
while( v ) {
str = std::string(1, '0' + v % 10) + str;
v /= 10;
}
return str;
}
void draw_score()
{
g_db.setColor(DblBuffer::GRAY, DblBuffer::BLACK);
g_db.setCursorPos(SCORE_X, SCORE_Y);
g_db.write("SCORE:");
std::string str = to_string(g_score);
while( str.size() < 6 )
str = "0" + str;
g_db.setCursorPos(SCORE_X, SCORE_Y + 1);
g_db.write(str);
}
演習問題:
int main()
{
g_nEnemy = 1; // 敵機数:1
init();
draw_map();
draw_car();
draw_enemy();
draw_score();
g_db.swap(); // 表示バッファ切り替え
getchar();
return 0;
}
自機・敵機移動処理の前に、移動先に移動可能かどうかをチェックする関数を定義する。
// 指定位置に壁が無いかどうかをチェック
bool can_move_to(int x, int y)
{
return g_map[y][x] == ' ' || g_map[y][x] == '.';
}
下記は、車を移動する関数。
void move_car(Car &car)
{
int x = car.m_x + car.m_dx;
int y = car.m_y + car.m_dy;
if( !can_move_to(x, y) ) {
if( car.m_dy == 0 ) { // 水平方向に移動している場合
if( can_move_to(car.m_x, car.m_y + 1) )
car.m_dy = 1;
else
car.m_dy = -1;
car.m_dx = 0;
} else { // 垂直方向に移動している場合
if( can_move_to(car.m_x + 1, car.m_y) )
car.m_dx = 1;
else
car.m_dx = -1;
car.m_dy = 0;
}
x = car.m_x + car.m_dx;
y = car.m_y + car.m_dy;
}
car.m_x = x;
car.m_y = y;
}
下記の関数は、敵機を自機のレーン方向にレーンチェンジする関数。
// 敵機を自機の方にレーン変更
// 一番外側が レーン0,一番内側が レーン4
void change_lane(Car &car)
{
if( car.m_laneChanged || // 2回連続レーンチェンジ不可
car.m_lane == g_car.m_lane || // 同一レーンの場合
!can_move_to(car.m_x + car.m_dx, car.m_y + car.m_dy) ) // 行き止まりの場合
{
car.m_laneChanged = false;
return;
}
if( car.m_lane > g_car.m_lane ) { // 外側のレーンに移動
if( car.m_dy == 0 ) { // 水平方向に移動している場合
if( car.m_y < MAP_HT/2 ) {
if( can_move_to(car.m_x, car.m_y-1) ) {
car.m_y -= 2;
--car.m_lane;
car.m_laneChanged = true;
}
} else {
if( can_move_to(car.m_x, car.m_y+1) ) {
car.m_y += 2;
--car.m_lane;
car.m_laneChanged = true;
}
}
} else {
}
} else { // 内側のレーンに移動
if( car.m_dy == 0 ) { // 水平方向に移動している場合
if( car.m_y < MAP_HT/2 ) {
if( can_move_to(car.m_x, car.m_y+1) ) {
car.m_y += 2;
++car.m_lane;
car.m_laneChanged = true;
}
} else {
if( can_move_to(car.m_x, car.m_y-1) ) {
car.m_y -= 2;
++car.m_lane;
car.m_laneChanged = true;
}
}
} else {
}
}
assert(car.m_lane >= 0 && car.m_lane < 5);
}
m_laneChanged フラグを参照し、レーンチェンジ直後であればレーンチェンジを行わないようにしている。
自機のレーンと同じ場合、行き止まり
水平方向に移動しているときだけレーンチェンジ可能にしている。
for (int i = 0; i < (int)g_enemy.size(); ++i) {
move_car(g_enemy[i]);
change_lane(g_enemy[i]);
}
bool game()
{
g_score = 0;
g_nEnemy = 1;
init();
int key = 0; // 押下されたキー
int keyDown = 0; // 押下状態のキー
bool update = true; // 再描画フラグ
int iv = 10; // 処理インターバル
for (int cnt=1;;++cnt) {
if( update ) {
int nDot = draw_map();
draw_enemy();
draw_car();
draw_score();
g_db.swap();
if( !nDot ) { // ドット全消去
mciSendString(TEXT("play one23.mp3"), NULL, 0, NULL);
Sleep(1000);
g_score += g_nEnemy * 1000;
++g_nEnemy;
init();
continue;
}
if( check_crash() ) {
mciSendString(TEXT("play s-burst01.mp3"), NULL, 0, NULL);
return false;
}
}
Sleep(10);
update = false;
if( !keyDown ) { // キー押下を受け付けていない場合
if( isKeyPressed(VK_LEFT) ) {
key = keyDown = VK_LEFT;
} else if( isKeyPressed(VK_RIGHT) ) {
key = keyDown = VK_RIGHT;
} else if( isKeyPressed(VK_UP) ) {
key = keyDown = VK_UP;
} else if( isKeyPressed(VK_DOWN) ) {
key = keyDown = VK_DOWN;
}
} else {
if( !isKeyPressed(keyDown) )// 押されたキーが離された
keyDown = 0;
}
if( cnt % 10 == 0 ) {
for (int i = 0; i < (int)g_enemy.size(); ++i) {
move_car(g_enemy[i]);
change_lane(g_enemy[i]);
}
update = true;
}
accel_decel(key, iv);
if( cnt % iv == 0 ) {
move_car(g_car, key);
eat_dot();
update = true;
iv = 10;
}
}
}