C言語でシューティングゲームを作る【コンソールゲーム】
目次
C言語でシューティングゲームを作る【コンソールゲーム】
はい。最近の記事では暑いしか言ってない気がしますが、今日も言わせてください。
暑いです。
ちょっと異常な気象ですね。
それで今回はC言語でシューティングゲームを作りたいと思います。
コンソールゲームでリアルタイムではなく入力ごとにゲームが更新される仕組みのやつです。
そのソースコードを解説していきます。
動作風景
今回のゲームは動作させると以下のようになります。
ソースコード全文
ソースコードの全文は以下になります。
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include <math.h> enum { ENEMIES_SIZE = 4, BULLETS_SIZE = 10, CANVAS_WIDTH = 60, CANVAS_HEIGHT = 30, CANVAS_SIZE = CANVAS_WIDTH * CANVAS_HEIGHT, PLAYER, ENEMY, BULLET, }; typedef struct { int x, y; } Vector2i; typedef struct { Vector2i pos; } Player; typedef struct { Vector2i pos; bool is_active; } Enemy; typedef struct { Vector2i pos; bool is_active; } Bullet; Player player; Enemy enemies[ENEMIES_SIZE]; Bullet bullets[BULLETS_SIZE]; int canvas[CANVAS_SIZE]; void init(void) { player.pos.x = CANVAS_WIDTH / 2; player.pos.y = CANVAS_HEIGHT - 1; int i = player.pos.y * CANVAS_WIDTH + player.pos.x; canvas[i] = PLAYER; } void draw(void) { system("clear"); // Windows: system("cls"); for (int y = 0; y < CANVAS_HEIGHT; y++) { for (int x = 0; x < CANVAS_WIDTH; x++) { int i = y * CANVAS_WIDTH + x; int c = canvas[i]; switch (c) { case PLAYER: putchar('P'); break; case ENEMY: putchar('E'); break; case BULLET: putchar('@'); break; default: putchar('.'); break; } } putchar('\n'); } } void fire(void) { for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (!bullet->is_active) { bullet->is_active = true; bullet->pos.x = player.pos.x; bullet->pos.y = player.pos.y - 1; break; } } } void move_left(void) { if (player.pos.x > 0) { player.pos.x--; } } void move_right(void) { if (player.pos.x < CANVAS_WIDTH) { player.pos.x++; } } bool update_by_input(void) { printf("space to fire. a to move left, d to move right.\n"); printf("command > "); char cmd[128]; if (!fgets(cmd, sizeof cmd, stdin)) { return false; } int c = cmd[0]; if (c == ' ') { fire(); } else if (c == 'a') { move_left(); } else if (c == 'd') { move_right(); } return true; } void random_spawn_enemy(void) { if (rand() % 3 == 0) { for (int i = 0; i < ENEMIES_SIZE; i++) { Enemy *enemy = &enemies[i]; if (!enemy->is_active) { enemy->is_active = true; enemy->pos.x = rand() % CANVAS_WIDTH; enemy->pos.y = 0; break; } } } } void update_enemies(void) { for (int i = 0; i < ENEMIES_SIZE; i++) { Enemy *enemy = &enemies[i]; if (enemy->is_active) { if (rand() % 2 == 0) { enemy->pos.y++; if (enemy->pos.y >= CANVAS_HEIGHT) { enemy->is_active = false; } } } } } void update_bullets(void) { for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (bullet->is_active) { bullet->pos.y--; if (bullet->pos.y < 0) { bullet->is_active = false; } } } } void check_collision(void) { for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (bullet->is_active) { for (int j = 0; j < ENEMIES_SIZE; j++) { Enemy *enemy = &enemies[j]; if (enemy->is_active) { if (bullet->pos.x == enemy->pos.x && abs(bullet->pos.y - enemy->pos.y) <= 1) { bullet->is_active = enemy->is_active = false; break; } } } } } } void update_canvas(void) { memset(canvas, 0, sizeof canvas); int i = player.pos.y * CANVAS_WIDTH + player.pos.x; canvas[i] = PLAYER; for (int i = 0; i < ENEMIES_SIZE; i++) { Enemy *enemy = &enemies[i]; if (enemy->is_active) { int j = enemy->pos.y * CANVAS_WIDTH + enemy->pos.x; canvas[j] = ENEMY; } } for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (bullet->is_active) { int j = bullet->pos.y * CANVAS_WIDTH + bullet->pos.x; canvas[j] = BULLET; } } } bool update(void) { if (!update_by_input()) { return false; } random_spawn_enemy(); check_collision(); update_bullets(); update_enemies(); update_canvas(); return true; } int main(void) { init(); for (;;) { draw(); if (!update()) { break; } } return 0; }
定数
定数は以下になります。
enum { ENEMIES_SIZE = 4, BULLETS_SIZE = 10, CANVAS_WIDTH = 60, CANVAS_HEIGHT = 30, CANVAS_SIZE = CANVAS_WIDTH * CANVAS_HEIGHT, PLAYER, ENEMY, BULLET, };
ENEMIES_SIZE
とBULLETS_SIZE
は配列の要素数です。
今回はint
の配列にプレイヤーや敵を配置します。
この1次元配列をキャンバスと呼称します。
キャンバスの横幅がCANVAS_WIDTH
で高さがCANVAS_HEIGHT
です。
キャンバスの配列の要素数がCANVAS_SIZE
です。
PLAYER
, ENEMY
, BULLET
はキャンバス上のオブジェクトを表す定数です。
構造体
typedef struct { int x, y; } Vector2i; typedef struct { Vector2i pos; } Player; typedef struct { Vector2i pos; bool is_active; } Enemy; typedef struct { Vector2i pos; bool is_active; } Bullet;
キャンバス上に配置されるオブジェクトは座標で管理されます。
その座標を表現する構造体がVector2i
です。
これはx座標とy座標を表現します。
キャンバスは1次元配列で座標は2次元になってます。
ですので2次元の座標を1次元の配列のインデックスにするには座標変換が必要になります。
Player
はプレイヤー、Enemy
は敵、Bullet
は弾丸です。
Enemy
とBullet
のis_active
はオブジェクトがアクティブな時に真になります。
アクティブなオブジェクトは更新されて描画されるようになります。
非アクティブなオブジェクトは更新もされないし描画もされません。
グローバル変数
グローバル変数は以下になります。
Player player; Enemy enemies[ENEMIES_SIZE]; Bullet bullets[BULLETS_SIZE]; int canvas[CANVAS_SIZE];
今回はシングルスレッドでゲームを作るのでオブジェクトはすべてグローバル変数にしています。
player
はプレイヤー、enemies
は敵のオブジェクト、bullets
は弾丸、canvas
はキャンバスです。
main関数
main関数は以下になります。
int main(void) { init(); for (;;) { draw(); if (!update()) { break; } } return 0; }
init()
で初期化後に無限ループでdraw()
とupdate()
を実行します。
update()
がfalse
を返して来たら無限ループから脱出します。
draw関数
draw関数は以下です。
void draw(void) { system("clear"); // Windows: system("cls"); for (int y = 0; y < CANVAS_HEIGHT; y++) { for (int x = 0; x < CANVAS_WIDTH; x++) { int i = y * CANVAS_WIDTH + x; int c = canvas[i]; switch (c) { case PLAYER: putchar('P'); break; case ENEMY: putchar('E'); break; case BULLET: putchar('@'); break; default: putchar('.'); break; } } putchar('\n'); } }
draw関数ではキャンバスを描画します。
キャンバスは1次元配列ですが、for文は2次元で回します。
そしてy * CANVAS_WIDTH + x
で2次元のカウント変数を1次元の配列の添え字に変換します。
キャンバスの要素をswitch文で分岐して描画を行います。
x座標の要素が出力できたらputchar('\n')
で改行を入れます。
改行を入れないとちゃんとゲーム画面が描画されません。
update関数
update関数は以下です。
bool update(void) { if (!update_by_input()) { return false; } random_spawn_enemy(); check_collision(); update_bullets(); update_enemies(); update_canvas(); return true; }
update_by_input()
は入力に応じてゲームを更新します。
random_spawn_enemy()
はランダムに敵キャラをスポーンします。
check_collision()
は敵キャラと弾丸の衝突判定です。
update_bullets()
は弾丸の更新。
update_enemies()
は敵キャラの更新。
update_canvas()
はオブジェクトをキャンバスに反映します。
update関数はbool
を返します。
false
を返したら無限ループが終了します。
update_by_input関数
update_by_input関数は以下です。
bool update_by_input(void) { printf("space to fire. a to move left, d to move right.\n"); printf("command > "); char cmd[128]; if (!fgets(cmd, sizeof cmd, stdin)) { return false; } int c = cmd[0]; if (c == ' ') { fire(); } else if (c == 'a') { move_left(); } else if (c == 'd') { move_right(); } return true; }
この関数はプロンプトを出し、ユーザーから一行入力されるのを待ちます。
cmd
には入力された文字列が保存されます。
cmd[0]
が半角スペースならfire()
を呼び出します。
英字のa
だったらmove_left()
, 英字のb
だったらmove_right()
を呼び出します。
fgets()
がNULLポインタを返したらこの関数はfalse
を返し、無事に終了したらtrue
を返します。
random_spawn_enemy関数
void random_spawn_enemy(void) { if (rand() % 3 == 0) { for (int i = 0; i < ENEMIES_SIZE; i++) { Enemy *enemy = &enemies[i]; if (!enemy->is_active) { enemy->is_active = true; enemy->pos.x = rand() % CANVAS_WIDTH; enemy->pos.y = 0; break; } } } }
この関数はランダムに敵をスポーン(配置)します。
rand() % 3 == 0
は乱数を3で割ってその余りが0だったら真になります。
enemies
をfor文で回し、enemy->is_active
が偽なら敵のアクティブ状態と座標を初期化します。
敵キャラは上から登場しますのでy座標は常に0になります。
x座標は0からCANVAS_WIDTH
の範囲でランダムにセットされます。
check_collision関数
void check_collision(void) { for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (bullet->is_active) { for (int j = 0; j < ENEMIES_SIZE; j++) { Enemy *enemy = &enemies[j]; if (enemy->is_active) { if (bullet->pos.x == enemy->pos.x && abs(bullet->pos.y - enemy->pos.y) <= 1) { bullet->is_active = enemy->is_active = false; break; } } } } } }
この関数では敵キャラと弾丸の衝突判定をします。
敵と弾丸が衝突していたら両方を非アクティブにします。
まずfor文でbullets
を回し、弾丸がアクティブならenemies
を回します。
そして弾丸と敵の座標を比較します。
if (bullet->pos.x == enemy->pos.x && abs(bullet->pos.y - enemy->pos.y) <= 1) { bullet->is_active = enemy->is_active = false; break; }
比較式はオブジェクトのx座標とy座標を比較します。
abs(bullet->pos.y - enemy->pos.y) <= 1
という式ですが、これはy座標の差分の絶対値が1以下かどうか調べています。
仮にbullet->pos.y == enemy->pos.y
という式にすると、不具合が起こります。
これは実際に変更して試してみてほしいです。弾丸と敵キャラが素通りしてしまうバグが出ます。
update_bullets関数
void update_bullets(void) { for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (bullet->is_active) { bullet->pos.y--; if (bullet->pos.y < 0) { bullet->is_active = false; } } } }
この関数は弾丸を更新します。
弾丸がアクティブならy座標をキャンバスの上方向に移動させます。
弾丸のy座標が0より下になったら弾丸を非アクティブにします。
update_enemies関数
void update_enemies(void) { for (int i = 0; i < ENEMIES_SIZE; i++) { Enemy *enemy = &enemies[i]; if (enemy->is_active) { if (rand() % 2 == 0) { enemy->pos.y++; if (enemy->pos.y >= CANVAS_HEIGHT) { enemy->is_active = false; } } } } }
この関数は敵を更新します。
敵はランダムに下移動してきます。上から下に向かって移動してくるわけですね。
キャンバスの外に出たら非アクティブになります。
敵がアクティブならrand() % 2 == 0
の式に応じてy座標を下方向に移動させます。
update_canvas関数
void update_canvas(void) { memset(canvas, 0, sizeof canvas); int i = player.pos.y * CANVAS_WIDTH + player.pos.x; canvas[i] = PLAYER; for (int i = 0; i < ENEMIES_SIZE; i++) { Enemy *enemy = &enemies[i]; if (enemy->is_active) { int j = enemy->pos.y * CANVAS_WIDTH + enemy->pos.x; canvas[j] = ENEMY; } } for (int i = 0; i < BULLETS_SIZE; i++) { Bullet *bullet = &bullets[i]; if (bullet->is_active) { int j = bullet->pos.y * CANVAS_WIDTH + bullet->pos.x; canvas[j] = BULLET; } } }
この関数はオブジェクトをキャンバスに反映させます。
キャンバスのプレイヤーの座標にPLAYER
を、敵の座標にENEMY
を、弾丸の座標にBULLET
を代入します。
おわりに
今回はC言語でシューティングゲームを作ってみました。
何か参考になれば幸いです。
(^ _ ^) | 弾丸をシュート! |
(・ v ・) | 敵を撃破! |