第16章 構造体 ゲーム作成

第16章 構造体
■ ゲーム作成
16.3 関数と構造体
■ 乱数の発生
■ 制御コード
■ 列挙型
■ system関数
今日のポイント
構造体、タグ名、メンバーとそれらの
使い方を思い出す!
 構造体のデータを関数間でやりとり
する場合の仮引数・実引数の使い方
 関数値を構造体で戻す方法

16.3 関数と構造体
構造体、タグ名、メンバーとは何だったか?
プログラム例 16.3.2 から
struct point {
double x;
double y;
};
タグ名
メンバーの宣言
x 座標
y 座標
「2つのdouble型から成る構造体に
point という名前のブランドタグをつ
けよう」
という感じ
void center_point(struct point p1, struct point p2);
中点を求めて出力する関数 center_point のプロトタイプ宣言
p1, p2 は point というタグがついた構造体変数
16.3 関数と構造体
プログラム例 16.3.2
構造体データの引渡し
#include <stdio.h>
struct point {
double x; double y;
};
void center_point(struct point p1, struct point p2);
int main(void)
{
struct point p1,
p1.x = 3.8; p1.y
p2.x = 8.4; p2.y
center_point(p1,
return 0;
}
p2;
= 5.6;
= 18.2;
p2);
構造体の実引数を関数に引き渡す
16.3 関数と構造体
プログラム例 16.3.2
構造体データの引渡し
仮引数が構造体である関数の定義
void center_point(struct point p1, struct point p2)
{
double xm, ym;
メンバーを用いて計算
xm = (p1.x + p2.x) / 2.;
ym = (p1.y + p2.y) / 2.;
printf("(%.2f, %.2f) と (%.2f, %.2f) の"
"中点の座標は (%.2f, %.2f)\n",
p1.x, p1.y, p2.x, p2.y, xm, ym);
}
出力はメンバーで
16.3 関数と構造体
関数から結果を受け取る場合 → ポインタで
プログラム例16.3.3から
タグ名
struct grade {
氏 名
科目1の点数
char *name;
科目2の点数
int subject1;
科目3の点数
int subject2;
平均点
int subject3;
double average;
メンバーの宣言
};
void average_of_scores(struct grade *h_p);
平均点計算用の関数 average_of_scores のプロトタイプ宣言
h_p は grade というタグがついた構造体の先頭アドレスを指すポインタ変数
16.3 関数と構造体
プログラム例 16.3.3 改
3科目の平均点を出すプログラム
平均点計算用の関数のプロトタイプ宣言
h_p は grade というタグがついた
構造体の先頭を指すポインタ変数
#include <stdio.h>
struct grade {
char *name; int subject1; int subject2;
int subject3; double average;
};
void average_of_scores(struct grade *h_p);
ここでは Smith という構造体変数の
int main(void)
アドレス(=ポインタ)が実引数
{
struct grade Smith = {"John Smith", 90, 80, 35, 0};
average_of_scores(&Smith);
printf("name: %s\n", Smith.name);
printf("科目1: %d 科目2: %d 科目3: %d\n",
Smith.subject1, Smith.subject2, Smith.subject3);
printf("平均: %f\n", Smith.average);
return 0;
}
16.3 関数と構造体
プログラム例 16.3.3 改
3科目の平均点を出すプログラム
平均点計算用の関数の定義
h_p は grade というタグがついた
構造体の先頭を指すポインタ変数
void average_of_scores(struct grade *h_p)
{
h_p -> average = (h_p -> subject1
+ h_p -> subject2
+ h_p -> subject3) / 3.;
}
ポインタの場合
選択演算子 "->" でメンバーを指定する
*h_p.average ⇔ h_p -> average
16.3 関数と構造体
プログラム例 16.3.2 改
関数から結果を構造体で
受け取るもう1つの方法
#include <stdio.h>
struct point {double x; double y;};
struct point center_point(struct point p1, struct point p2);
戻り値が構造体であることを宣言
int main(void)
{
結果を受け取る構造体 p3 を用意
struct point p1, p2, p3;
p1.x = 3.8; p1.y = 5.6;
p2.x = 8.4; p2.y = 18.2;
結果の構造体を関数から戻り値で受け取る
p3 = center_point(p1, p2);
printf("(%.2f, %.2f) と (%.2f, %.2f) の"
"中点の座標は (%.2f, %.2f)\n",
p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
return 0;
}
16.3 関数と構造体
プログラム例 16.3.2 改
関数から結果を構造体で
受け取るもう1つの方法
戻り値が構造体である関数の定義
struct point center_point(struct point p1, struct
point p2)
{
結果を入れる構造体を用意
struct point p;
p.x = (p1.x + p2.x) / 2.;
p.y = (p1.y + p2.y) / 2.;
return p;
}
メンバーを用いて計算
結果を構造体で返す
■ 乱数の発生
p.183

int rand(void)

疑似乱数の発生
 範囲:0~RAND_MAX(=32767)
 <stdlib.h>内で宣言
void srand(unsigned n)

疑似乱数のシード(種)の指定・変更
 <stdlib.h>内で宣言
time_t time(time_t *timer)





経過時間を秒単位で表した数値
引数は NULL (空ポインタ)でよい
<time.h>内で宣言
サイコロ・プログラム(dice.c)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
int i;
/* 実行するたびに違う値が得られるように、
* 現在の時刻値を使って乱数ジェネレータを初期化 */
srand((unsigned)time(NULL));
/* サイコロを10回振る */
for (i = 0; i < 10; i++)
printf("%d ", (rand() % 6) + 1); // 6の剰余系+1
printf("\n");
return 0;
}
■ 制御コード

教科書 p.180, 表A.2: 制御コード






\a ベル・警告音の出力
\r 行頭に戻る(キャリッジ・リターン)
\f そのままの位置で行送り(ライン・フィード)
\n 改行(\r\f)
\t 水平タブ
使用例


putchar('\a');
printf("現在 %d 回目\r\a", i);
サイコロ・プログラム2(dice2.c)
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main(void)
{
int i, n, m;
/* 現在の時刻値を使って乱数ジェネレータを初期化 */
srand((unsigned)time(NULL));
printf(" ---\n");
n = rand() % 6; m = n + 8 + rand() % 10;
for (i = n; i < m; i++)
n=0~5
printf("\r| %d |\a", i % 6 + 1);
m = n+8 ~ n+17
printf("\n ---\n");
1回の alert に約200 ms かかる
return 0;
→ このサイコロは1.6 ~3.4秒間転がる
}
■ 列挙型



名前付き定数をゼロからの整数に対応させる
enum タグ {列挙子リスト};
例:
enum DAYS {
SUNDAY, MONDAY, ..., SATURDAY
};
実体→
0,
1,
...,
6
■ system関数



OS(コマンドプロンプト)のコマンドをプログラム
の中から実行できる。
system("cls") で画面クリア
#include <stdlib.h> が必要
地底探検ゲーム(undergnd.c)
講義資料のWebページからダウンロード
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<time.h>
乱数発生に必要
乱数初期化に必要
/* 地面を格納する構造体の宣言 */
struct ground {
int block[10];
// 地下情報を格納する配列の宣言
char blockStr[21]; // 地下ブロックパターン
};
g[0].block = {1, 0, 0, 1, 0, 0, 0, 1, 0, 1}
g[0].blockstr ■__■___■_■
2バイト文字なので倍のサイズが必要 + \0 → 21バイト
地底探検ゲーム(undergnd.c)
/* 地面を作成する関数 */
void makeGround(struct ground *p)
{
3バイトの文字列2つの配列
int i;
char *wall[] = { " ", "■" };
"■"
/* 左の壁 */
strcpy(p->blockStr, wall[1]);
4で割り切れたら偽→1,
p->block[0] = 1;
割り切れなければ真→0
/* 中央のブロックパターン */
for (i = 1; i <= 8; i++) {
p->block[i] = rand() % 4 ? 0 : 1; // 確率1/4でブロック発生
strcat(p->blockStr, wall[p->block[i]]);
}
/* 右の壁 */
対応する位置にブロックを置く
strcat(p->blockStr, wall[1]);
p->block[9] = 1;
"■"
}
地底探検ゲーム(undergnd.c)
現在地記憶用変数
int main(void)
ギブアップチェック用フラグ
{
int i, score = 0, x = 4, key = 1;
struct ground g[4]; // 地面の構造体配列(4層分)
/* 最初の地面の作成 */
srand(time(NULL) % 100); // 乱数シード:現在時刻の100の剰余
for (i = 0; i < 4; i++) makeGround(g + i);
構造体配列の
/* メインループ */
現在地マーク
while (key) {
ポインタで引渡し
の埋め込み
/* 画面表示 */
strncpy(g[2].blockStr + x * 2, "☆", 2);
for (i = 0; i < 4; i++)
4層分の地面を描画
printf("%s\n", g[i].blockStr);
strncpy(g[2].blockStr + x * 2, " ", 2);
次回の印刷時に前回の☆印が残らないよう
に2バイト分の空白で事前に消しておく
地底探検ゲーム(undergnd.c)
/* キー入力 */
printf("距離 %d m: ", score);
printf("4←→6 ↓=2 ギブアップ=0: ");
左に移動
scanf("%d", &key);
/* 移動 */
右に移動
if (key == 4 && g[2].block[x - 1] == 0) x--;
else if (key == 6 && g[2].block[x + 1] == 0) x++;
else if (key == 2 && g[3].block[x] == 0) {
下に移動
for (i = 0; i < 3; i++) g[i] = g[i + 1];
makeGround(g + 3);
1段下からコピー
score++;
最下層の新規作成
}
}
/* 終了処理 */
printf("スコア: %d メートル!\n", score);
return 0;
}
マイン・スイーパー(mineswpr.c)
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<string.h>
<time.h>
#define TRUE 1
#define BSIZE 5
// ゲームボードのサイズ
#define MINENUM 5 // 地雷の数
// 地雷を表現する構造体
typedef struct {
int x, y;
} Mine;
整数値の座標で表現
Mine 型として定義
enum MSTAT { MISS, NEAR, HIT };
static char map[BSIZE][BSIZE];
地雷の探査結果用の列挙型
MSTATの定義
地雷マップ用の配列
staticにしておくと初期化される
マイン・スイーパー(mineswpr.c)
// 地雷を追加する関数
void Mine_set(Mine *p)
{
do {
p->x = rand() % BSIZE;
すでに配置されている地雷と
p->y = rand() % BSIZE;
重複しないようチェック
} while (map[p->y][p->x]);
map[p->y][p->x] = TRUE;
地雷マップに登録
}
// 地雷に近いかどうかをチェックする関数
enum MSTAT Mine_check(int cx, int cy, Mine *p)
{
int distX, distY;
地雷を踏んだらHITを出力
if (p->x == cx && p->y == cy) return HIT;
distX = p->x - cx;
地雷の隣ならNEARを出力
distY = p->y - cy;
if (-1 <= distX && distX <= 1 &&
-1 <= distY && distY <= 1) return NEAR;
return MISS;
}
それ以外ならMISSを出力
マイン・スイーパー(mineswpr.c)
// ゲームボード用データ構造
ボードの状況用配列
int status[BSIZE][BSIZE];
地雷用配列を確保
Mine m[MINENUM];
enum BSTAT { ALREADY, OPEN, BOMB };
ボードの状況記述用の
列挙型BSTATの定義
// ゲームボードの初期化関数
void Board_init(void)
{
int x, y, i;
// 状態の初期化
for (y = 0; y < BSIZE; y++)
for (x = 0; x < BSIZE; x++)
status[y][x] = -1;
未探査状態は -1
// 地雷セット
for (i = 0; i < MINENUM; i++) Mine_set(&(m[i]));
}
地雷の数だけ呼び出す
ポインタを渡す
マイン・スイーパー(mineswpr.c)
// 指定された座標を開く関数
enum BSTAT Board_open(int x, int y)
{
int i, nearCount = 0;
enum MSTAT ms;
地雷の探査結果用
if (status[y][x] >= 0) return ALREADY;
探査済みなら 0 以上
for (i = 0; i < MINENUM; i++) {
→ALREADY を出力
ms = Mine_check(x, y, &m[i]);
if (ms == HIT) return BOMB;
未探査ならチェック
if (ms == NEAR) nearCount++;
結果がHITならBOMBを出力
}
status[y][x] = nearCount;
結果がNEARならカウンタを1つ
return OPEN;
増して次の地雷をチェック
}
NEARの数をボードに記載
全部の地雷についてチェックして
NEARばかりならOPENを出力
マイン・スイーパー(mineswpr.c)
// ゲームボードを画面に表示する関数
void Board_show(void)
{
int i, x, y;
ボードの x 座標 A~E の表示
printf(" ");
for (i = 0; i < BSIZE; i++) putchar('A' + i);
printf("\n");
for (y = 0; y < BSIZE; y++) {
printf("%d ", y + 1);
y 座標の数字表示
for (x = 0; x < BSIZE; x++) {
未探査なら # を表示
if (status[y][x] < 0) printf("#");
else printf("%1d",status[y][x]);
探査済みなら
}
NEARの数を表示
printf("\n");
}
}
マイン・スイーパー(mineswpr.c)
int main(void)
{
int i, x, y, openCount = 0;
char charX, charY, key[100];
enum BSTAT bs;
ゲームボードの状況報告用
// 乱数の初期化
srand(time(NULL) % 100);
Board_init();
ゲームボードの初期化
// メインループ
while (TRUE) {
ゲームボードの表示
Board_show();
printf("座標を入力してください(例 A1): ");
fgets(key, 100, stdin);
キーボードから1行入力
if (strlen(key) < 2) continue;
2文字未満ならやりなおし
// 入力されたX座標をチェックする
charX = key[0];
if (charX < 'A' || charX > 'A' + BSIZE-1) continue;
x = charX - 'A';
ボードの範囲外ならやりなおし
マイン・スイーパー(mineswpr.c)
ボードの範囲外ならやりなおし
// 入力されたY座標をチェックする
charY = key[1];
if (charY < '1' || charY > '1' + BSIZE-1) continue;
y = charY - '1';
地雷を踏んだら強制終了
// 判 定
if ((bs = Board_open(x, y)) == BOMB) {
printf("\a\a\a\a地雷を踏んでしまいました!\n");
break;
}
探査済みならやりなおし
if (bs == ALREADY) continue;
if (++openCount == BSIZE*BSIZE - MINENUM) {
printf("\aクリアしました!\n");
探査済み数=全座標数
Board_show();
-地雷数 ならクリア
break;
最終結果を表示
}
}
マイン・スイーパー(mineswpr.c)
printf("\n");
for (i=0; i<MINENUM; i++)
printf(" %c%d",'A'+m[i].x,1+m[i].y);
return 0;
}
正解(地雷の座標)の表示
status[][]
map[][]
Mine m[]
m[0]={4,1}
m[1]={1,0}
m[2]={3,4}
m[3]={0,4}
m[4]={0,2}
1
2
3
4
5
A
0
0
1
0
1
B
1
0
0
0
0
C
0
0
0
0
0
D
0
0
0
0
1
E
0
1
0
0
0
A B C D E
1 -1-1-1-1-1
2 -1-1-1-1-1
3 -1-1-1-1-1
4 -1-1-1-1-1
5 -1-1-1-1-1
スキルアップタイム1



地底探検ゲーム(undergnd.c)をコンパイルし、
動作を確認せよ
srand(…)の行をコメントアウトすると、どうなる
か、確認せよ
ファイル入出力関数の利用により、「ハイスコア
の表示・保存機能」を付け足せ(保存ファイル名
はhighscr.txt)
スキルアップタイム1のヒント
int highscore=0;
FILE *fp;
...
if ((fp = fopen("highscr.txt", "r")) != NULL) {
fscanf(fp, "%d", &highscore);
fclose(fp);
}
if (score > highscore) {
printf("ハイスコアです!¥n");
fp = fopen("highscr.txt", "w");
fprintf(fp, "%d¥n", score);
fclose(fp);
} else printf("ハイスコア: %d メートル¥n", highscore);
スキルアップタイム2




マイン・スイーパー(mineswpr.c)をコンパイル
し、動作を確認せよ
srand(…)の行をコメントアウトすると、どうなる
か、確認せよ
ゲームボードを表示するたびに、実行ウィンドウ
の上部から書き直すように改良せよ
ゲームボードのサイズや地雷の数を変えてみよ