vol.6 関数(ポインタ、構造体使用)、モードによる場面転換(列挙型)

目次

はじめに

今回からついに関数についてやっていきたいと思います。
といっても普通の数値に関する関数は皆さん実装できると思うので、今回はポインタ絡みの関数についてやっていけたらなと思います。
ポインタ絡みということで、構造体に出てきたアロー演算子についても関数で使っていきたいですね。
また、関数毎にゲームシーンを分ける場合があるので、直接関数に関係はありませんが、少し新しいことを導入したいと思います。

関数でのポインタ使用

まず基本的にポインタ絡みの関数を実装する際に、必ずしも関数が値を返してほしいわけではないということを念頭に置きましょう。そういった場合には関数をvoid型で宣言します。
void型で宣言した関数は戻り値を持たないので、関数内で動作をひとしきりした後、何も返さずに関数を終了してくれます。
ところが、何も返さない関数など何に使えるのかと思う人はいると思います。
void型関数は主に2種類の用途があって
①単純に動作をまとめておきたい場合
②複数の値を返したい場合

があります。return ●; では、戻り値が1つしか設定できないので、通常の関数の考え方では実装できないということです。
今回は②の場合について考えていきたいと思います。
複数の値を返してほしい場合、「ポインタ」を使うことになります。
ポインタを介して複数の値を変更することで実質②を適えるというわけです。この時に、returnの箇所は不要となるので、void型関数で宣言することになります。

さて、早速サンプルプログラムを見ていましょう。

例:足し算した結果と引き算した結果を同時に返すプログラム

#include<stdio.h>

void add_sub(int a, int b, int *p, int *q)
{
	*p=a+b;
	*q=a-b;
	//実際return していなくでも*p,*qを介して値を複数返しているような状況になる
}

int main(void)
{
	int a=2, b=3;
	int p, q; //p:足し算の結果 q:引き算の結果
	add_sub(a,b,&p,&q);
	printf("%d %d\n",p,q);

	return 0;
}

関数での構造体使用

関数の引数に構造体を使うことも可能です。実際にこれを使えば構造体で定義したプレイヤーの体力などを変動させることができます。実際に例を見てみましょう。

例:敵にxポイントのダメージを与えるプログラム

#include<stdio.h>

typedef struct{

    int HP;

}player_t; //プレイヤータイプの略


void damage(int x, player_t *enemy) //型宣言の player_tに注意
{
	(enemy->HP) -=x; //enemyのHPを「enemy->HP」で表している。それをxだけ減らす
}

int main(void)
{
	player_t enemy={100};
	printf("減少前:%d\n",enemy.HP); //出力結果:減少前:100

	int x=30;
	damage(x,&enemy);
	printf("減少後:%d\n",enemy.HP); //出力結果:減少後:70

	return 0;
}

このようにアロー演算子を使うことによってポインタと同等の動作をすることができます。

ちなみに、前回の戦闘シーンでこうげきとかいふくの動作を関数にするとこのようになります。

void atack(PERSON player1, PERSON *player2)
{
	int db=rand_bonus(player1.ATK /10); //ダメージ増減分 ダメージボーナスの略
	PERSON *p2;
	p2 = player2;
	if ((p2->def_flag) == 1){ //防御判定があれば
		(p2->HP) -= (player1.ATK + db) / 2; //ダメージ半減
		(p2->def_flag) = 0; //防御判定リセット
	}
	else (p2->HP) -= player1.ATK + db;
	if ((p2->HP) < 0) (p2->HP) = 0; //HPは非負整数なので、最小値は0
	printf("%sの残りHP %d\n", p2->name, p2->HP);
}

void heal(PERSON *player)
{
	int heal_base = 30; //基本回復量
	PERSON *p;
	p = player;
	if ((p->MP) < 4)
		printf("しかしMPが足りない!\n");
	
	else{
		(p->MP) -= 4; //MPを減らす
		(p->HP) += heal_base + rand_bonus(3); //体力回復
		printf("%sの体力は%dに回復した!\n", p->name, p->HP);
		printf("残りMPは%d\n", p->MP);
	}
}

モードの管理

ここからは関数自体から少し離れて、場面転換に着目したいと思います。
場面転換についてはvol.3でやりましたが、ゲームでは複数の場面転換が存在することが当たり前です。それらを区別するために「モード番号」という考え方をしてみましょう。
例えば、外フィールド画面、町フィールド画面、戦闘画面、メニュー画面の4種類がシーンがあるとしましょう。これらのシーン毎にモード番号を割り振ります。
今回はそれぞれ0,1,2,3番というモード番号を割り振ることにします。
シーン変遷を簡単なプログラムで実装してみましょう。

#include<stdio.h>
#include<conio.h>

void field(void);
void m_field(void);
void battle_field(void);
void menu_field(void);

int main(void)
{
	int mode=0; //外フィールドにいる

	switch (mode)
	{
	case 0:
		field();
		break;
	case 1:
		m_field();
		break;
	case 2:
		battle_field();
		break;
	case 3:
		menu_field();
		break;
	}

	_getch(); //待機

	mode = 3; //メニュー画面を開いた
	
	switch (mode)
	{
	case 0:
		field();
		break;
	case 1:
		m_field();
		break;
	case 2:
		battle_field();
		break;
	case 3:
		menu_field();
		break;
	}

	_getch();
	return 0;
}

void field(void)
{
	printf("ここは外フィールド\n");
}

void m_field(void)
{
	printf("ここは町フィールド\n");
}
void battle_field(void)
{
	printf("ここは戦闘画面\n");
}
void menu_field(void)
{
	printf("ここはメニュー画面\n");
}

このようにすればmodeの値によってシーンを変えることができます。

ところが、シーンの数が増えてくると、例えば

mode = 157;

としたときに一体何のシーンなのか把握しきれません。
そんなときに活躍するのが、今回新しく登場する「列挙型(enum)」と呼ばれるものです。
使い方としては構造体と似ているので早速構文を紹介します。

宣言

typedef enum{
	モード名0,
	モード名1,
	モード名2,
・・・
}列挙体名;

今回で言えば、

typedef enum{
	FIELD_TYPE_FIELD,
	FIELD_TYPE_M_FIELD,
	FIELD_TYPE_BATTLE,
	FIELD_TYPE_MENU
}mode_t;

とすれば、新しく「mode_t」型の列挙体を作成することになります。
この時、自動的に上から順に0,1,2,・・・と番号を振ってくれます。
CELL_TYPE_FIELDは0のことですし、FIELD_TYPE_BATTLEは2のことを指します。

これを使って先ほどのプログラムを書き換えるとこのようになります。

#include<stdio.h>
#include<conio.h>

void field(void);
void m_field(void);
void battle_field(void);
void menu_field(void);

typedef enum{
	FIELD_TYPE_FIELD,
	FIELD_TYPE_M_FIELD,
	FIELD_TYPE_BATTLE,
	FIELD_TYPE_MENU
}mode_t;

int main(void)
{
	mode_t mode=FIELD_TYPE_FIELD; //外フィールドにいる

	switch (mode)
	{
	case FIELD_TYPE_FIELD:
		field();
		break;
	case FIELD_TYPE_M_FIELD:
		m_field();
		break;
	case FIELD_TYPE_BATTLE:
		battle_field();
		break;
	case FIELD_TYPE_MENU:
		menu_field();
		break;
	}

	_getch(); //待機

	mode = FIELD_TYPE_MENU; //メニュー画面を開いた
	
	switch (mode)
	{
	case FIELD_TYPE_FIELD:
		field();
		break;
	case FIELD_TYPE_M_FIELD:
		m_field();
		break;
	case FIELD_TYPE_BATTLE:
		battle_field();
		break;
	case FIELD_TYPE_MENU:
		menu_field();
		break;
	}

	_getch();
	return 0;
}

void field(void)
{
	printf("ここは外フィールド\n");
}

void m_field(void)
{
	printf("ここは町フィールド\n");
}
void battle_field(void)
{
	printf("ここは戦闘画面\n");
}
void menu_field(void)
{
	printf("ここはメニュー画面\n");
}

このようにすれば、見ただけでどのシーンについて記述しているかわかりやすくなりますね。

※ちなみに、列挙体はわざわざ「mode_t」型を作成せずとも使うことができます。
例えば

enum{
	FIELD_TYPE_FIELD,
	FIELD_TYPE_M_FIELD,
	FIELD_TYPE_BATTLE,
	FIELD_TYPE_MENU
};

とだけ書けば純粋に上から0,1,2,3と数を割り当ててくれます。
同じプログラム内ならどこでも FIELD_TYPE_BATTLE は2という数字として取り扱ってくれるので、こっちの方が直感的で分かりやすいかもしれません。
これは

#define FIELD_TYPE_FIELD 0
#define FIELD_TYPE_M_FIELD 1
#define FIELD_TYPE_BATTLE 2
#define FIELD_TYPE_NMENU 3

とほとんど同様のことです。(見た目上は、ですけども)
列挙体を使うかどうかはお好みでどうぞー

おわりに

今までやってきた内容を関数で実装し、分岐も分かりやすく書く方法を伝授してきました。
ここまでくるとずいぶんたくさんの事をC言語で実装できるようになってるのではないかなと思います。
まだ今(9月6日現在)は手を付けてる人は少ないと思いますが、もし質問等あれば遠慮なくしてくださいね。
次回は何をしましょう。考えておきます。
では、お疲れ様でした。