C言語でできることのご紹介として、簡単なスネークゲームを作りました。
C言語は基本的に組み込みソフトを開発するための言語という紹介をしてきましたが、内容をきちんと理解していれば思い通りのゲームを作ることも可能です。
今回作成したゲームは、C言語の本を1冊勉強していれば理解ができる内容になっているので、理解の確認としてソースコードをぜひ読んでみてください!
実際にゲームなどを作ることができれば、成長を感じてモチベーションのアップになるよ!
スネークゲームとは
画面の中でヘビを動かして、餌を食べていき成長していくゲームです。
壁、もしくは自分の体に当たるとゲームオーバーです。
餌ってまさか…
C#を使ったリッチな環境で作ったスネークゲームもあります。
C言語、C#それぞれのメリット・デメリットがイメージできると思うので、是非ご覧になってください。
環境
Windows11
Visual Studio 2019
で作成しました。
(2023/3/11追記)
Visual Studio 2022で動作させる場合、壁の「■」とヘビの「□」が半角として扱われて画面が崩れてしまうようです。
壁を「壁」、ヘビを「蛇」に書き換えると画面が崩れることなく遊ぶことができます。
(任意の全角文字に置き換えてください。)
ソースコード
早速ですが、以下がソースコード全文です。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <conio.h>
/************マクロの定義 ************/
// フィールドの横幅と縦幅
#define WIDTH 15
#define HEIGHT 10
// スネークの頭の初期位置
#define STARTX (WIDTH/2)
#define STARTY (HEIGHT/2)
// スネークの最大長
#define MAXLENGTH ((WIDTH - 2) * (HEIGHT- 2))
// スネークの最大長の1/3の長さ
#define LENGTH_LEV1 (MAXLENGTH / 3)
// スネークの最大長の2/3の長さ
#define LENGTH_LEV2 (LENGTH_LEV1 * 2)
// キー未入力時のスネーク進行速度(TIME_○lev[msec] * LOOPNUM)
#define TIME_LEV1 50
#define TIME_LEV2 30
#define TIME_LEV3 10
#define LOOPNUM 10
/************ マクロの定義 ************/
/************ 定数定義 ************/
// キー入力をenumの定数するための定義
enum direction {
left,
up,
right,
down,
non
};
/************ 定数定義 ************/
/************ 構造体の型定義 ************/
// スネークの元となる構造体
typedef struct snakePosition
{
int x, y;
enum direction nextDirection;
}snake;
// 餌の元となる構造体
typedef struct snakeFood
{
int x, y;
}food;
/************ 構造体の型定義 ************/
/************ グローバル変数 ************/
// フィールドを2次元配列で定義
int field[HEIGHT][WIDTH];
// スネークを定義
snake snake1[MAXLENGTH];
// 餌を定義
food food1;
// スネークの初期の長さを定義
int snakeLength = 2;
// 餌を食べたことを検出するためのフラグ
int eatFlag = 0;
/************ グローバル変数宣言 ************/
/************ プロトタイプ宣言 ************/
int init();
int viewField();
int countSec();
enum direction getKey();
int shiftSnake();
int moveSnake(enum direction key);
int makeFood();
int judge();
/************ プロトタイプ宣言 ************/
int main()
{
enum direction key;
int loopCount = 0;
init(); // 初期化処理(壁、スネーク、餌を作る)
viewField(); // 描画処理
while(1) {
countSec(); // 時間をカウントする
key = getKey(); // キー入力を受け付ける
if (loopCount == LOOPNUM || key != non) { // 時間経過かキー入力があれば更新する
moveSnake(key);
if (judge() < 0) break;
viewField();
loopCount = 0;
}
loopCount += 1;
};
return 0;
}
int init()
{
// フィールドの初期化
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
if (i == 0 || i == (HEIGHT - 1) || j == 0 || j == (WIDTH - 1)) field[i][j] = 1;
else field[i][j] = 0;
}
}
// スネークの初期の長さ
snakeLength = 2;
// スネークの初期化
memset(snake1, 0, sizeof(snake1));
snake1[0].x = STARTX;
snake1[0].y = STARTY;
snake1[0].nextDirection = left;
snake1[1].x = STARTX + 1;
snake1[1].y = STARTY;
// 餌を作る
makeFood();
return 0;
}
int viewField()
{
// フィールドの初期化
system("cls");
// 壁をフィールドに反映
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
if (i == 0 || i == (HEIGHT - 1) || j == 0 || j == (WIDTH - 1)) field[i][j] = 1;
else field[i][j] = 0;
}
}
// スネークをフィールドに反映
for (int i = 0; i < snakeLength; i++) {
field[(HEIGHT - 1) - snake1[i].y][snake1[i].x] = 2;
}
// 餌をフィールドに反映
field[(HEIGHT - 1) - food1.y][food1.x] = 3;
// フィールドを描画
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
if (field[i][j] == 1) printf("■");
else if (field[i][j] == 2) printf("□");
else if (field[i][j] == 3) printf("餌");
else printf(" ");
if (j == (WIDTH - 1)) printf("\n");
}
}
return 0;
}
int countSec()
{
// 時間(mSec)をカウントする
double startTime = 0.0;
double endTime = 0.0;
double totalTime = 0.0;
int level = 0;
// レベルをチェック
if (snakeLength < LENGTH_LEV1) level = TIME_LEV1;
else if (snakeLength >= LENGTH_LEV1 && snakeLength < LENGTH_LEV2) level = TIME_LEV2;
else level = TIME_LEV3;
startTime = clock();
while (1) {
endTime = clock();
totalTime = endTime - startTime;
if (totalTime > TIME_LEV1) break;
}
return 0;
}
enum direction getKey() {
int key = 0;
if (_kbhit()) {
key = _getch();
}
if (key == 0x4B) /* ←キー */
return left; /* 左に移動 */
else if (key == 0x48) /* ↑キー */
return up; /* 上に移動 */
else if (key == 0x4D) /* →キー */
return right; /* 右に移動 */
else if (key == 0x50) /* ↓キー */
return down; /* 下に移動 */
else
return non;
return non;
}
// 動く処理
int shiftSnake() {
if (snakeLength == 1) return -1;
for (int i = 1; i < snakeLength; i++) {
// 餌を食べて重なった部分はずらさない
if (snake1[snakeLength - i].x == snake1[snakeLength - (i + 1)].x
&& snake1[snakeLength - i].y == snake1[snakeLength - (i + 1)].y) {
continue;
}
snake1[snakeLength - i].x = snake1[snakeLength - (i + 1)].x;
snake1[snakeLength - i].y = snake1[snakeLength - (i + 1)].y;
}
return 0;
}
int moveSnake(enum direction key) {
switch (key) {
case left:
//首がある方向のキー入力だった場合は受け付けない
if (snake1[0].x == (snake1[1].x + 1)) {
shiftSnake(); // 首から下を前にシフトする
snake1[0].x += 1; // 頭を動かす
break;
}
shiftSnake();
snake1[0].x -= 1;
snake1[0].nextDirection = left;
break;
case up:
if (snake1[0].y == (snake1[1].y - 1)) {
shiftSnake();
snake1[0].y -= 1;
break;
}
shiftSnake();
snake1[0].y += 1;
snake1[0].nextDirection = up;
break;
case right:
if (snake1[0].x == (snake1[1].x - 1)) {
shiftSnake();
snake1[0].x -= 1;
break;
}
shiftSnake();
snake1[0].x += 1;
snake1[0].nextDirection = right;
break;
case down:
if (snake1[0].y == (snake1[1].y + 1)) {
shiftSnake();
snake1[0].y += 1;
break;
}
shiftSnake();
snake1[0].y -= 1;
snake1[0].nextDirection = down;
break;
case non:
// キー入力がなかった場合は頭の向いている方向へ動かす
switch (snake1[0].nextDirection) {
case left:
shiftSnake();
snake1[0].x -= 1; // 頭を左に動かす
snake1[0].nextDirection = left; // 頭が次に動く方向を左に設定
break;
case up:
shiftSnake();
snake1[0].y += 1;
snake1[0].nextDirection = up;
break;
case right:
shiftSnake();
snake1[0].x += 1;
snake1[0].nextDirection = right;
break;
case down:
shiftSnake();
snake1[0].y -= 1;
snake1[0].nextDirection = down;
break;
}
break;
}
return 0;
}
int judge()
{
// 壁に当たったらアウト
if (snake1[0].x == 0 || snake1[0].x == (WIDTH - 1) || snake1[0].y == 0 || snake1[0].y == (HEIGHT - 1)) {
return -1;
}
// 頭が体に当たったらアウト
if (snakeLength < 5) ;
else if (snakeLength >= 5){
for (int i = 5; i < snakeLength; i++) {
if (snake1[0].x == snake1[i].x && snake1[0].y == snake1[i].y)
return -1;
}
}
// 餌を食べた
if (snake1[0].x == food1.x && snake1[0].y == food1.y) {
// 餌を移動する
makeFood();
// スネークを延ばす
snakeLength += 1;
snake1[snakeLength - 1].x = snake1[snakeLength - 2].x;
snake1[snakeLength - 1].y = snake1[snakeLength - 2].y;
return 0;
}
return 0;
}
int makeFood()
{
int x, y;
int flag;
// 餌の初期化
food1.x = 0;
food1.y = 0;
while (1) {
flag = 0;
srand((unsigned int)time(NULL)); // 現在時刻の情報で初期化
x = 1 + (rand() % (WIDTH - 2));
y = 1 + (rand() % (HEIGHT - 2));
for (int i = 0; i < snakeLength; i++) {
if (snake1[i].x == x && snake1[i].y == y) {
flag = 1; // スネークと重なったらフラグを立てる(失敗)
}
}
if (flag == 0) {
break;
}
}
food1.x = x;
food1.y = y;
return 0;
}
解説
マクロ
/************マクロの定義 ************/
// フィールドの横幅と縦幅
#define WIDTH 10
#define HEIGHT 10
// スネークの頭の初期位置
#define STARTX (WIDTH/2)
#define STARTY (HEIGHT/2)
// スネークの最大長
#define MAXLENGTH ((WIDTH - 2) * (HEIGHT- 2))
// スネークの最大長の1/3の長さ
#define LENGTH_LEV1 (MAXLENGTH / 3)
// スネークの最大長の2/3の長さ
#define LENGTH_LEV2 (LENGTH_LEV1 * 2)
// キー未入力時のスネーク進行速度(TIME_LEV○[msec] * LOOPNUM)
#define TIME_LEV1 50
#define TIME_LEV2 30
#define TIME_LEV3 10
#define LOOPNUM 10
/************ マクロの定義 ************/
マクロで、ゲームで使う定数値を定義しています。
ここで定義したマクロをソースコードの中で使えば、ここの数値を変更するだけで全てのマクロ使用箇所の数値を書き換えることができます。
とりあえずフィールドは、縦10×横10の合計100マスで定義して、その他に必要な定数は計算で定義していきます。
ヘビの最大長(MAXLENGTH)はフィールドの合計から壁を除いた数を計算で求めています。
ヘビは放っておいたら勝手に進みますが、レベル1の段階では(50×10)=500[ms]で1マス進みます。
構造体
/************ 構造体の型定義 ************/
// スネークの元となる構造体
typedef struct snakePosition
{
int x, y;
enum direction nextDirection;
}snake;
// 餌の元となる構造体
typedef struct snakeFood
{
int x, y;
}food;
/************ 構造体の型定義 ************/
ヘビのと餌の元となる構造体を宣言します。
ヘビは、現在位置を入れるxとyに加えて、頭だけ次にどの方向へ動くかの情報が必要なので、nextDirectionという変数を持たせます。
餌は位置の情報だけあればいいので、x、yのみです。
グローバル変数
/************ グローバル変数 ************/
// フィールドを2次元配列で定義
int field[HEIGHT][WIDTH];
// スネークを定義
snake snake1[MAXLENGTH];
// 餌を定義
food food1;
// スネークの初期の長さを定義
int snakeLength = 2;
// 餌を食べたことを検出するためのフラグ
int eatFlag = 0;
/************ グローバル変数宣言 ************/
複数の関数をまたいで必要になる変数は、グローバル変数で宣言します。
グローバル変数が多すぎると管理が大変になるので最小限におさめます。
まずは、フィールドを二次元配列で宣言します。縦と横はマクロで定義した10×10です。
ヘビは先ほどの構造体を配列で宣言することで、ヘビを構成するマス1つ1つが位置情報を持ちます。
(次に進む方向は頭のマスだけが持てばいいですが、体のマスにも持たせておきます。)
main関数
int main()
{
enum direction key;
int loopCount = 0;
init(); // 初期化処理(壁、スネーク、餌を作る)
viewField(); // 描画処理
while(1) {
countSec(); // 時間をカウントする
key = getKey(); // キー入力を受け付ける
if (loopCount == LOOPNUM || key != non) { // 時間経過かキー入力があれば更新する
moveSnake(key);
if (judge() < 0) break;
viewField();
loopCount = 0;
}
loopCount += 1;
};
return 0;
}
初期化などの処理を済ませた後、while分でゲームを進めるために必要な処理を延々と繰り返します。
1セットの処理は、以下の処理で構成します。
- 時間をマクロで決めた秒数だけカウントする
- キー入力を受け付ける
- キー入力があった場合か、時間が経過した場合
- ヘビを動かす
- 壁や餌に当たったか判定する
- 画面を描画する
- ループカウントを進める
何もキーを入力しなければループカウントが進んでいき、10回カウントをしたところでヘビを進めます。
countSec()はマクロで定義した50[ms]間待機して、ループカウントはマクロで10回と定義しているので、だいたい500[ms]で1マス進みます。
init() 初期化処理
int init()
{
// フィールドの初期化
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
if (i == 0 || i == (HEIGHT - 1) || j == 0 || j == (WIDTH - 1)) field[i][j] = 1;
else field[i][j] = 0;
}
}
// スネークの初期の長さ
snakeLength = 2;
// スネークの初期化
memset(snake1, 0, sizeof(snake1));
snake1[0].x = STARTX;
snake1[0].y = STARTY;
snake1[0].nextDirection = left;
snake1[1].x = STARTX + 1;
snake1[1].y = STARTY;
// 餌を作る
makeFood();
return 0;
}
フィールドは、配列のインデックス0と9のところだけ1で、残りは0を格納します。
配列は10で宣言した場合インデックスは0~9なので、マクロをインデックスとして使う場合は1を引かなくてはいけません。
ヘビは、構造体の配列のうちインデックス0が頭を表します。
初期の長さは2マスとします。
viewField() 描画処理
int viewField()
{
// フィールドの初期化
system("cls");
// 壁をフィールドに反映
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
if (i == 0 || i == (HEIGHT - 1) || j == 0 || j == (WIDTH - 1)) field[i][j] = 1;
else field[i][j] = 0;
}
}
// スネークをフィールドに反映
for (int i = 0; i < snakeLength; i++) {
field[(HEIGHT - 1) - snake1[i].y][snake1[i].x] = 2;
}
// 餌をフィールドに反映
field[(HEIGHT - 1) - food1.y][food1.x] = 3;
// フィールドを描画
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
if (field[i][j] == 1) printf("■");
else if (field[i][j] == 2) printf("□");
else if (field[i][j] == 3) printf("餌");
else printf(" ");
if (j == (WIDTH - 1)) printf("\n");
}
}
return 0;
}
画面を描画する処理をまとめています。
今回は、空白、壁、ヘビ、餌の4種類を描画する必要がありますが、それぞれに数字を割り当てることで判別していきます。
空白:0
壁:1
ヘビ:2
餌:3
まず、system(“cls”);でコンソールの表示をリセットします。
この処理をしないと、フィールドを更新する度にどんどん下へ描画されていきます。
壁をフィールドに反映する処理では、フィールド配列の端っこは壁になるので1,それ以外は空白なので0を入れていきます。
ヘビをフィールドに反映する処理では、ヘビの構造体のx座標とy座標にあたるところに2をいれます。
ヘビのx座標とy座標を、そのままフィールドの二次元配列のインデックスに使用するとy座標が反転してしまうので、インデックスに入れるときに高さから引く処理を入れています。
field[(HEIGHT – 1) – snake1[i].y][snake1[i].x] = 2;
ちなみにフィールドの配列は以下のようになっています。
[0][0] [0][1] [0][2] [0][3]・・・
[1][0] [1][1] [1][2] [1][3]・・・
[2][0] [2][1] [2][2] [2][3]・・・
[3][0] [3][1] [3][2] [3][3]・・・
1つめの括弧がy座標(逆)、2つ目の括弧がx座標を表していますね。
餌をフィールドに反映する処理では、ヘビと同様の方法で3を格納します。
最後に、フィールド配列に入っている数値をもとに、それぞれの記号を描画していきます。
countSec() 時間をカウント
int countSec()
{
// 時間(mSec)をカウントする
double startTime = 0.0;
double endTime = 0.0;
double totalTime = 0.0;
int level = 0;
// レベルをチェック
if (snakeLength < LENGTH_LEV1) level = TIME_LEV1;
else if (snakeLength >= LENGTH_LEV1 && snakeLength < LENGTH_LEV2) level = TIME_LEV2;
else level = TIME_LEV3;
startTime = clock();
while (1) {
endTime = clock();
totalTime = endTime - startTime;
if (totalTime > TIME_LEV1) break;
}
return 0;
}
決められた時間をカウントする処理です。
clock()はプログラムを実行してから経過した時間を返してくれます。
countSec()関数を呼んだ時間がstartTime、countSec()関数が呼ばれてから経過した時間がendTime、その差分がtotalTimeです。
totalTimeがマクロで定義した時間になるまでwhile文を延々とループします。
ゲームに緊張感を出すため、ヘビが長くなるにつれてカウントする時間を減らしています。
getKey() キー入力を受け付け
enum direction getKey() {
int key = 0;
if (_kbhit()) {
key = _getch();
}
if (key == 0x4B) /* ←キー */
return left; /* 左に移動 */
else if (key == 0x48) /* ↑キー */
return up; /* 上に移動 */
else if (key == 0x4D) /* →キー */
return right; /* 右に移動 */
else if (key == 0x50) /* ↓キー */
return down; /* 下に移動 */
else
return non;
return non;
}
_kbhit()はキーが入力された場合に0以外の数字を返すので、キーが入力されるとif文の中に入ります。
if文の中で_getch()を使うと、入力したキーを変数keyに格納します。
カーソルキーにはそれぞれに以下の数字が割り振られているため、これを利用して何が入力されたかを判別します。
←キー:75(10進数) = 4B(16進数)
↑キー:72(10進数) = 48(16進数)
→キー:77(10進数) = 4D(16進数)
下キー:79(10進数) = 50(16進数)
プログラムの中で、数字の前に0xをつけると16進数として処理されるようになります。
shiftSnake() moveSnake() ヘビが動く処理
int shiftSnake() {
if (snakeLength == 1) return -1;
for (int i = 1; i < snakeLength; i++) {
// 餌を食べて重なった部分はずらさない
if (snake1[snakeLength - i].x == snake1[snakeLength - (i + 1)].x
&& snake1[snakeLength - i].y == snake1[snakeLength - (i + 1)].y) {
continue;
}
snake1[snakeLength - i].x = snake1[snakeLength - (i + 1)].x;
snake1[snakeLength - i].y = snake1[snakeLength - (i + 1)].y;
}
return 0;
}
int moveSnake(enum direction key) {
switch (key) {
case left:
//首がある方向のキー入力だった場合は受け付けない
if (snake1[0].x == (snake1[1].x + 1)) {
shiftSnake(); // 首から下を前にシフトする
snake1[0].x += 1; // 頭を動かす
break;
}
shiftSnake();
snake1[0].x -= 1;
snake1[0].nextDirection = left;
break;
case up:
if (snake1[0].y == (snake1[1].y - 1)) {
shiftSnake();
snake1[0].y -= 1;
break;
}
shiftSnake();
snake1[0].y += 1;
snake1[0].nextDirection = up;
break;
case right:
if (snake1[0].x == (snake1[1].x - 1)) {
shiftSnake();
snake1[0].x -= 1;
break;
}
shiftSnake();
snake1[0].x += 1;
snake1[0].nextDirection = right;
break;
case down:
if (snake1[0].y == (snake1[1].y + 1)) {
shiftSnake();
snake1[0].y += 1;
break;
}
shiftSnake();
snake1[0].y -= 1;
snake1[0].nextDirection = down;
break;
case non:
// キー入力がなかった場合は頭の向いている方向へ動かす
switch (snake1[0].nextDirection) {
case left:
shiftSnake();
snake1[0].x -= 1; // 頭を左に動かす
snake1[0].nextDirection = left; // 頭が次に動く方向を左に設定
break;
case up:
shiftSnake();
snake1[0].y += 1;
snake1[0].nextDirection = up;
break;
case right:
shiftSnake();
snake1[0].x += 1;
snake1[0].nextDirection = right;
break;
case down:
shiftSnake();
snake1[0].y -= 1;
snake1[0].nextDirection = down;
break;
}
break;
}
return 0;
}
入力されたキーを元に、ヘビを動かします。
まず、ヘビを構成するマスをshiftSnake()関数で一つずつ前にずらします。
これは、配列の後ろから頭まで順々にシフトしていくことで実現できます。
最後に頭を新しいマスの座標に書き換えることで、実質ヘビが前に進んだことになります。
ただし、自分の首がある方向へキーが入力された場合、そちらには進めないので強制的に元々向いていた方向へ進めます。
judge() 判定処理
int judge()
{
// 壁に当たったらアウト
if (snake1[0].x == 0 || snake1[0].x == (WIDTH - 1) || snake1[0].y == 0 || snake1[0].y == (HEIGHT - 1)) {
return -1;
}
// 頭が体に当たったらアウト
if (snakeLength < 5) ;
else if (snakeLength >= 5){
for (int i = 5; i < snakeLength; i++) {
if (snake1[0].x == snake1[i].x && snake1[0].y == snake1[i].y)
return -1;
}
}
// 餌を食べた
if (snake1[0].x == food1.x && snake1[0].y == food1.y) {
// 餌を移動する
makeFood();
// スネークを延ばす
snakeLength += 1;
snake1[snakeLength - 1].x = snake1[snakeLength - 2].x;
snake1[snakeLength - 1].y = snake1[snakeLength - 2].y;
return 0;
}
return 0;
}
ヘビを進めた後に、壁か自分の体、もしくは餌に当たったかの判定をします。
壁に当たったらmain関数にー1を返し、main関数はループ処理を抜けてプログラムが停止します。
餌を食べたら、ヘビの構造体配列を一つ増やします。
一番しっぽ側のマスをコピーして一つ増やして重ねておき、shiftSnake()関数で重なっているところはシフトしないことで、ヘビを増やすことにしました。
makeFood() 餌を作る
int makeFood()
{
int x, y;
int flag;
// 餌の初期化
food1.x = 0;
food1.y = 0;
while (1) {
flag = 0;
srand((unsigned int)time(NULL)); // 現在時刻の情報で初期化
x = 1 + (rand() % (WIDTH - 2));
y = 1 + (rand() % (HEIGHT - 2));
for (int i = 0; i < snakeLength; i++) {
if (snake1[i].x == x && snake1[i].y == y) {
flag = 1; // スネークと重なったらフラグを立てる(失敗)
}
}
if (flag == 0) {
break;
}
}
food1.x = x;
food1.y = y;
return 0;
}
rand()関数でランダムなx座標、y座標をつくり、餌の座標とします。
rand()関数だけで使ってしまうと、何度つかっても同じ数値が作られてしまうので、srand()関数とtime()関数で初期化をする必要があります。
また、餌の作られる座標が、ヘビが既にいる座標と重なってしまった場合気持ちが悪いので、もう一度座標をランダムに作るところからやり直します。
この処理は、ヘビが長くなると餌の座標と重なりやすくなってしまい、餌の生成に時間がかかることが欠点です。
(解決方法をコメントにて求む!)
以上でゲームは完成です。
まとめ
C言語で作れるスネークゲームをご紹介しました。
プログラミングは実際に手を動かして書いてみることで疑問がどんどん湧いてきて、それを本やネットで調べることでさらに理解が深まります。
自分が楽しめる方法でアウトプットしてみることで、モチベーションを保ちながら勉強をがんばりましょう!
コメント