【C言語プログラミング12】マリオのピクロスを作ってみる
このゲームご存じですか?確か最初はゲームボーイのソフトだったと思うのですが、数字をヒントにブロックを削って行って絵が完成したらゲームクリアと言った内容のゲームです。今回はこれを自作してみる。
ゲームの詳細説明
ピクロスのゲーム画面は以下の通りです。
真ん中に10×10のブロックが表示されていて、左と上に数字がならんでいます。この数字は削るブロックが縦横にいくつ存在するのかを示しています。例えば上の真ん中に10と書いてありますね。つまり、この縦列は全部削る対象だと言うことになります。4 1とか1 3のように数字が二つ以上並んでいるのは、4つ連続して削らないといけないブロックと、1つだけ削らないといけないブロックがありますよという意味です。例(■■■■☐☐■☐☐☐)
ピクロスを設計する
さて、このゲームをどうやって作るか。今回もコンソールを使うのでリッチなアニメーションは当然無理です。なのでピクロスの本質的なゲーム性のみを楽しめるものにします。最低限必要な機能としては以下5つとします。
1)ブロックを表示する
2)削るブロックを選択し、削ることができる
3)削らないことが確定したブロックに印をつけられる
4)間違ったブロックを削ってしまったらアラームを鳴らす
5)最後まで削り終わったら正解の絵を表示する
ブロックを表示する
今回のブロックは10×10とします。ブロックは100個ですが、これにヒントになる数字も併せて表示する必要があります。先ずは、ブロックと数字のセットをエクセルシートで考えてみます。
これは、解答となる絵です。メガネですね。本家のピクロスからパクって来ました。これは解答ですので、ゲーム開始時は以下のように表示する必要があります。白紙のブロックと数字だけが表示された状態です。
先ずは、この初期画面を表示するプログラムを作成してみます。
#include <stdio.h>
#define NUM_OF_BLOCK_X 10
#define NUM_OF_BLOCK_Y 10
#define NUM_OF_HINT 8
#define W 1 /* 白ブロック */
/* ゲームブロック */
char GameBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W , '0',' ',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '2','2',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '4','4',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','4','1',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '3','3',' ',' ','\n' },
{ '7','1','1','1','1','1','1','1','1','7','\n' },
{ ' ','1','1','5',' ',' ','5','1','1',' ','\n' },
{ ' ','1','1',' ',' ',' ',' ','1','1',' ','\n' },
};
void main(void)
{
/*--------------------------*/
/* ゲームブロックを表示する */
/*--------------------------*/
printf("%c", ' ');
for (int y = 0; y < 13; y++) {
for (int x = 0; x < 15; x++) {
if (GameBlocks[y][x] == 0) {
break;
}
else if (GameBlocks[y][x] == W) { // 白ブロックを表示(削られる前)
printf("■");
}
else {
printf("%c%c", GameBlocks[y][x], ' '); // ヒントを表示
}
}
}
}
これを実行すると以下のようになります。先頭でGameBlocksというchar型の二次元配列を宣言して、初期値を入れています。この初期値はエクセルシートの図2で検討した内容がそのまま入っています。言い換えれば、図2をC言語で表現したということです。図2がC言語化されれば、表示するのは簡単ですね。forループを使えば、左上から右下に向かってprintfするだけです。いつぞやのルフィと同じです。
今選択されているブロックを表現する
ブロックを削るにせよ、印をつけるにせよ、今選択中のブロックが何処なのかを表現する必要があります。今回はそれを”*”マークで表します。これを今後カーソルと呼びます。最初のカーソル位置は左上としましょうか。カーソルはゲームプレイ中は上下左右に移動することになるので、カーソルの現在位置を管理する必要があります。その管理用変数を作って、実際にカーソルを表示してみます。ブロックを表示しようとしている位置がカーソルの位置と一致したら、そこはブロックではなく”*”を表示する条件を追加しました。
void main(void)
{
int cursorX = 0;
int cursorY = 0;
/*--------------------------*/
/* ゲームブロックを表示する */
/*--------------------------*/
printf("%c", ' ');
for (int y = 0; y < 13; y++) {
for (int x = 0; x < 15; x++) {
if (GameBlocks[y][x] == 0) {
break;
}
else if (y == cursorY && x == cursorX) {
printf("*");
}
else if (GameBlocks[y][x] == W) { // 白ブロックを表示(削られる前)
printf("■");
}
else {
printf("%c%c", GameBlocks[y][x], ' '); // ヒントを表示
}
}
}
}
実行結果は次の通りです。左上は”*”になっていますね。
カーソルを動かせるようにする
ここまで来たら、今度はカーソルを動かせるようにしましょうか。PCでゲームをする人は慣れていると思いますが、十字キーの代わりにアルファベットキーを使ってカーソルを動かすことにします。
w:上 s:下 a:左 d:右 とします。また、削らないブロックへのマーキングはM、削る時はEnterキーとします。
では、カーソルを動かす処理を作りましょう。W、S、A、Dの何れかのキー入力を受け取らないといけませんね。今回は_getch関数を使うことにします。これは単純に1文字だけを受け取る関数です。#include <conio.h>で使えるようになります。wsadのキーボード入力を受けたら、カーソルが移動したように見せないといけません。これを表現するには、一旦画面をクリアして、カーソルが上下左右に一つ移動した画面を再描画します。コンソール画面をクリアするには、system("cls");を使います。これは#include <stdlib.h>で使えるようになります。
先程作ったブロック表示処理とカーソル移動処理を組み合わせると以下のフローチャートになります。最初に画面を一旦クリアして、ブロック表示をします。その後キーボードからの入力文字をチェックし、wsadの何れかであれば、現在のカーソル位置を更新し、先頭の処理に戻します。先頭に戻すと言うのはループさせると言うことです。
最初に作ったブロック表示処理とカーソル移動処理を組み合わせると以下のコードになります。
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define NUM_OF_BLOCK_X 10
#define NUM_OF_BLOCK_Y 10
#define NUM_OF_HINT 8
#define W 1 /* 白ブロック */
/* ゲームブロック */
char GameBlocks[NUM_OF_BLOCK_Y + NUM_OF_HINT][NUM_OF_BLOCK_X + NUM_OF_HINT] = {
{ W , W , W , W , W , W , W , W , W , W , '0',' ',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '2','2',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '4','4',' ',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','4','1',' ','\n' },
{ W , W , W , W , W , W , W , W , W , W , '1','1','1','1','\n' },
{ W , W , W , W , W , W , W , W , W , W , '3','3',' ',' ','\n' },
{ '7','1','1','1','1','1','1','1','1','7','\n' },
{ ' ','1','1','5',' ',' ','5','1','1',' ','\n' },
{ ' ','1','1',' ',' ',' ',' ','1','1',' ','\n' },
};
void main(void)
{
int cursorX = 0;
int cursorY = 0;
while (1) {
system("cls");
/*--------------------------*/
/* ゲームブロックを表示する */
/*--------------------------*/
printf("%c", ' ');
for (int y = 0; y < 13; y++) {
for (int x = 0; x < 15; x++) {
if (GameBlocks[y][x] == 0) {
break;
}
else if (y == cursorY && x == cursorX) {
printf("*");
}
else if (GameBlocks[y][x] == W) { // 白ブロックを表示(削られる前)
printf("■");
}
else {
printf("%c%c", GameBlocks[y][x], ' '); // ヒントを表示
}
}
}
/*----------------------------------*/
/* キーボードからの入力を監視する */
/*----------------------------------*/
char key = _getch();
switch (key) {
case 'w': // ↑
if (cursorY > 0) {
cursorY--;
}
break;
case 's': // ↓
if (cursorY < NUM_OF_BLOCK_Y - 1) {
cursorY++;
}
break;
case 'a': // ←
if (cursorX > 0) {
cursorX--;
}
break;
case 'd': // →
if (cursorX < NUM_OF_BLOCK_X - 1) {
cursorX++;
}
break;
default:
break;
}
}
}
switch-case文
swicth-case文は今回初めて登場する文法で、これも条件分岐制御になります。if文と同じです。switch-caseの基本文法は以下の通り。
switch (変数) {
case 値:
// 処理
break;
default:
break;
}
switchの()内には変数が入ります。()内に指定した変数の値が、caseの後ろに記述した値に合致すれば、その下の行に記述した処理を実行します。処理の最後はbreak;で終了します。ピクロスのコードでは以下の様になっていますね。
switch (key) {
case 'w':
if (cursorY > 0) {
cursorY--;
}
break;
・・・
・・
・
default:
break;
}
keyが'w'だったらcursorYをデクリメントしています。つまり、カーソルY軸の現在位置を上方向に移動させるということです。if (cursorY > 0)という条件を入れているのは、0がY軸の一番上なので、これ以上上方向に移動させないためです。最後のdefault:はどの条件にも合致しなかった場合に実行する処理を記述します。今回はwsad以外のキーボード以外の入力があった場合の処理を書きます。何もすることがないので、即break;としています。
カーソル動作テスト
プログラムが動作するかテストしてみました。右上、右下、左下、真ん中にカーソルが移動するかどうかテストした結果です。
【右上】
【右下】
【左下】
【真ん中】
大丈夫そうですね。上下左右にカーソルが移動しています。ここまで来ればゲームの7割は完成したようなもんです。残りの仕事は以下の通り。
1)削らないところに印を付ける
2)削ったところが正解・不正解の判定
3)答えの絵を出す
今回はここまで、お疲れ様でした。