C++でゲームを作ってみようー基礎編(3/5)

では、続きを作っていきましょう
前回こんな感じの仕様書と決めたこと、プログラムでしたね

仕様書
  • ジャンル :パズルゲーム(たぶんね)
  • どういう風:荷物を所定の位置に置く、ただし簡単に行かせはしない
  • 必要なもの:プレイヤー、荷物、ゴール・・・
  • どんな場面があるか:タイトル、ゲーム画面、エンディング
各場面で動くもの
  • タイトル  :(選択肢)
  • ゲーム画面 :プレイヤー、荷物、ゴール
  • エンディング:なし?
どういう風に動くか
  • 選択肢  :キーボードのWで上にSで下に移動、Zで決定
  • プレイヤー:キーボードのWASDで上下左右に動く、壁には行けない
  • 荷物   :プレイヤーが押したら押した方向に壁がない時に動く
  • ゴール  :ゲーム開始時に決定されてそれ以降動くことはない
各場面はどうやったら変わるか
  • タイトル→ゲーム画面  :タイトルではじめるを選ぶ
  • タイトル→ゲーム終了  :タイトルで終わるを選ぶ
  • ゲーム画面→タイトル  :プレイヤーが諦めたとき
  • ゲーム画面→エンディング:クリアした時
  • エンディング→タイトル :Z押したら
プログラム
enum 場面制御{
TITLE,
GAME,
ENDING,
GAMEEND,
};

void main(){
	bool ゲーム終了する = false
	場面制御 今の場面 = TITLE

	while(!ゲーム終了する){
		入力受付
		switch(今の場面){
		case TITLE:
			タイトル
			if(タイトルではじめるが選ばれた){
				今の場面 = GAME;
			}
			if(タイトルでおわるが選ばれた){
				今の場面 = GAMEEND;
			}
		break;
		
		case GAME:
			ゲーム画面
			if(クリアした){
				今の場面 = ENDING;
			}
			if(プレイヤーが諦めた){
				今の場面 = TITLE;
			}
		break;
		
		case ENDING:
			エンディング
			if(Z押された){
				今の場面 = TITLE;
			}
		break;
		
		case GAMEEND:
			ゲーム終了する = true;
		break;
		}
	}
}

入力受付

では早速、入力受付から書いていきましょう

まずは、キーボードの読み取りで有名なscanfを使う想定で・・・(c++なんだからcin使えよってのはなしで)
入力受付のとこを次のように変えてみますか
(ここからはプログラムの必要なところだけを抜粋します)

while(!ゲーム終了する){
	char 入力 = '';
	scanf(入力で読み取る)


こんな風にすれば入力に応じた動作ができそうです
・・・それにしても、scanf使ってる時点でリアルタイム性もなきに等しいですね
一々入力が終わったらEnter押すのもめんどくさい
ということでkbhitっていうのとgetchを使います
なお、kbhitを使うにはconio.hを追加でincludeする必要があります

#include <conio.h>

while(!ゲーム終了する){
	char 入力 = '';
	if(kbhit())入力 = getch();

これでキーが押されたときにのみ入力待機状態に入るようになりました、
まぁつまるとこEnterを押す必要もなくなりリアルタイム処理になりました

副作用として物凄い高速でキー入力が受け付けられます
このままキーを押しっぱなしにすると物凄い勢いでプレイヤーが移動してしまう可能性が・・・面白そう
キーを1回押すだけで1移動単位だけ移動をさせたいですね
どうしましょうか


押してないか、押してる最中なのか、そもそもどんだけの時間キーを押してるのか・・・
各キーの押してる時間を管理する変数でも作りますか

押してない = 0
押してる = 1以上(押してる時間を表す)

のような

int 各キー押してる時間[キーの数]

を作って、更新は

各キー押してる時間[押してるキー]++;

でいけそうですね
長くなりました、プログラムに入れてみましょうか

void main(){

int 各キー押してる時間[キーの数];
...
if(kbhit()){
	char 入力 = getch();
	各キー押してる時間[入力]++;
	
	for(int i=0;i < キーの数;i++){
		if(入力 != i)各キー押してる時間[i] = 0;
	}
}

for文は押してないキーの時間を0にするものです
あ、ちなみに、このままじゃできないです
入力文字はそのまま配列番号には使えないですしね
上手いこと考えましょう

・・・そういえば各キー押してる時間を取得する関数作ってないな
まぁ確認したいキーの時間が0だったら押してない、1(のみ、2以上は含まない)だったら押したっていう判定返すだけだし、すぐに作れる作れる

これで、キーからの入力受け付けは大丈夫そうですかね

タイトルの処理

では次に、タイトルの処理をしていきましょうか
mainの中身が長くなるのを避けたいのでタイトルを制御する関数作っておきましょう

void Title();

でいいですかね

タイトルでは入力に応じて選択肢を動かしたいわけですね
で、やりたいことを文章にしてみると

void Title(){
	if(Wが押された)選択肢を上にする
	if(Sが押された)選択肢を下にする
	if(Zが押された)選択肢を決定する
	選択肢に応じて場面を変える
}

こんなんでしょうかね

・・・あれあれ、場面ってどうやって変えたらいいんでしょう
関数化したせいでmainの変数を変えられません

Title(場面制御*)みたいに場面管理をしてる変数を渡すべきでしょうか・・・
それとも 場面制御 Title()みたいに戻り値を作るべきでしょうか・・・
他にも方法があるかもしれないですね・・・

・・・・・・mainからしたら、大切な今の場面を渡すのは怖いですね
Titleからしたら、今の場面はここ!って渡されても、うん自分じゃんって感じるかも
つまり、Titleでは今がどの場面かっていう情報は必要ないわけです
っていうことで場面制御 Title()みたいに戻り値を返す方針で行きます

場面制御 Title(){
	場面制御 tmp = TITLE
	if(Wが押された)選択肢を上にする
	if(Sが押された)選択肢を下にする
	if(Zが押された){
		はじめるが選ばれてるなら	tmp = GAME
		おわるが選ばれてるなら tmp = GAMEEND
	}
	return tmp
}

こんな感じですかね
かなり雑ですけど、まぁなんとなくで書いていきましょう
選択肢はenumでもいいし、int型でもいいし、適当に変数作っておきましょう

・・・・・・そういえば、今まで一切画面に描画してないですね
そろそろ描画しましょうか
まぁタイトルの文字と選択肢ぐらい置いておきましょうか

場面制御 Title(){
	場面制御 tmp = TITLE
	if(Wが押された)選択肢を上にする
	if(Sが押された)選択肢を下にする
	if(Zが押された){
		はじめるが選ばれたなら	tmp = GAME
		おわるが選ばれたなら tmp = GAMEEND
	}
	printf(タイトル文字,選択肢)
	return tmp
}

いやー雑ですね、CUIなんで描画関数はprintfとかです、coutでもいいんじゃないですかね
ちなみに、このまま描画すると物凄い勢いでタイトルとかが何回も表示されます
リアルタイム処理してますので・・・
まぁ、うまいこと画面に合わせて改行を入れるか、リアルタイム処理をやめましょう

とりあえず、タイトルの制御はこんな感じでプログラムをまとめてみますか

enum 場面制御{
TITLE,
GAME,
ENDING,
GAMEEND,
};

場面制御 Title(){
	場面制御 tmp = TITLE
	if(Wが押された)選択肢を上にする
	if(Sが押された)選択肢を下にする
	if(Zが押された){
		はじめるが選ばれたなら	tmp = GAME
		おわるが選ばれたなら tmp = GAMEEND
	}
	printf(タイトル文字,選択肢)
	return tmp
}

void main(){
	bool ゲーム終了する = false
	場面制御 今の場面 = TITLE
	int 各キー押してる時間[キーの数];
	
	while(!ゲーム終了する){
		if(kbhit()){
			char 入力 = getch();
			各キー押してる時間[入力]++;
			
			for(int i=0;i < キーの数;i++){
				if(入力 != i)各キー押してる時間[i] = 0;
			}
		}
		switch(今の場面){
		case TITLE:
			今の場面 = Title();
		break;
		
		case GAME:
			ゲーム画面
			if(クリアした){
				今の場面 = ENDING;
			}
			if(プレイヤーが諦めた){
				今の場面 = TITLE;
			}
		break;
		
		case ENDING:
			エンディング
			if(Z押された){
				今の場面 = TITLE;
			}
		break;
		
		case GAMEEND:
			ゲーム終了する = true;
		break;
		}
	}
}

勘のいいあなた、他の場面も同様にして
場面制御 Game()とか作っていけばいいんじゃないかって思いましたか?
はい、その通りです
思わなかったあなた、とりあえず次のプログラムでswitchの見た目がすっきりした感じを味わっておきましょう

enum 場面制御{
TITLE,
GAME,
ENDING,
GAMEEND,
};

場面制御 Title(){
	場面制御 tmp = TITLE
	if(Wが押された)選択肢を上にする
	if(Sが押された)選択肢を下にする
	if(Zが押された){
		はじめるが選ばれたなら	tmp = GAME
		おわるが選ばれたなら tmp = GAMEEND
	}
	printf(タイトル文字,選択肢)
	return tmp
}

場面制御 Game();
場面制御 Ending();
void main(){
	bool ゲーム終了する = false
	場面制御 今の場面 = TITLE
	int 各キー押してる時間[キーの数];
	
	while(!ゲーム終了する){
		if(kbhit()){
			char 入力 = getch();
			各キー押してる時間[入力]++;
			
			for(int i=0;i < キーの数;i++){
				if(入力 != i)各キー押してる時間[i] = 0;
			}
		}
		switch(今の場面){
		case TITLE:
			今の場面 = Title();
		break;
		
		case GAME:
			今の場面 = Game();
		break;
		
		case ENDING:
			今の場面 = Ending();
		break;
		
		case GAMEEND:
			ゲーム終了する = true;
		break;
		}
	}
}

なんかもうちょっとすっきりいけそうですねぇ
実は更にできますが、でもこれは応用に取っておきます
クラスの継承とか、ポリモーフィズムがわかればいけるんですが・・・ムズカシイヨ

さて、きちんとプログラムを書いて、一旦動作確認をしてみましょう
Game()とかの中身は適当でいいですよ、return TITLE;っていう一行でも書いておけばいいのでは
コンパイルしてコマンドプロンプトなりターミナルなりで実行してみましょう
なんかエラーが出たら修正しましょう、文字化けしたら対処方法を調べましょう

ちなみに自分はこんな感じで表示してます、リアルタイム処理は外してます
f:id:Nsan:20170806203921p:plain

' * 'があるのが現在選択している場所です

ここで、1回キーボードを押さないと文字が出てこない、
おわるを選択した後にもう一度キーを入力しないと終わらない、
っていうことが発生するかもしれないですね、どうすればいいかはお考え下さい
めっちゃくちゃ頭悪いなーって思っちゃう方法でもいいんですよ
一旦書いたらあとでいい方法が思い浮かぶかもしれません

実はこの時点で
入力
更新
描画
っていうゲームの基本3大要素は終わりました
・・・ほんとですよ?
どのゲームでも入力をとってそれを元に内部値を更新して画面に描画しています

とりあえずここまで
次はGame()を作っていきましょうか