技術文章> 手を動かしてさくさく理解する 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; } } }