Home -> HSP講座 -> 初級編 No.11

// 数当てゲーム - 構造化バージョン

#packopt name "数当てゲーム"
#packopt hide 1

// 定数(= const)を宣言
#const IDW_Main		0		// メイン画面のウィンドウID
#const MAX_ANSWER	100		// 答えの最大
#const ANS_LIMIT	6		// 回答できる限界

*main
	gosub *initialize		// 一番最初のサブルーチン
	gosub *SetVariable		// 変数を用意
	gosub *SettingWindow		// スクリーンを構築
	gosub *CreateQuestion		// 問題を作る
	
	gsel IDW_Main, 1
	stop
	
//######## サブルーチン群 ########
*initialize
	randomize		// 一回だけ使えばいい
	gsel 0, -1
	return
	
// 変数を用意
*SetVariable
	dim question		// 問題の答え
	dim answer		// プレイヤーの回答
	dim nAnsCount		// 回答した回数
	
	dim ID_Edit		// input のオブジェクトID	
	return
	
// スクリーンを構築する
*SettingWindow
	
	screen IDW_Main, 480, 320, 2		// メイン画面
	title "数当てゲーム ver.1.0"		// タイトルバーには、ゲーム名を表示させる
	
	objsize 80, 25				// ボタンの大きさを設定
	pos  20,  50 : mes "1 〜 "+ MAX_ANSWER +"までの私を当ててみよ!"
	pos  30,  80 : input answer, 120, 24		// 回答
	pos 160,  80 : button gosub "答える", *OnAnswer	// 答え合わせ
	
	pos  50, 140					// カレントポジションを移動
	return
	
// 画面をリセット
*ResetWindow
	redraw 2
	color 255, 255, 255 : boxf : color
	sysfont 0		// 最初のフォント
	pos  20,  50 : mes "1 〜 "+ MAX_ANSWER +"までの私を当ててみよ!"	// もう一度書く(消されたから)
	pos  50, 140
	redraw 1
	return
	
// 問題を作る
*CreateQuestion
	// 0〜MAX_ANSWER-1 なので、+1 して 1〜MAX_ANSWER にする
	question = rnd(MAX_ANSWER) + 1
	return
	
// 判定処理 ( button で飛んでくる )
*OnAnswer
	// ※判定する順番は大事です。
	
	nAnsCount ++		// 答えた回数を数える
	
	if ( answer == question ) {
		// 正解
		gosub *Clear			// 正解したときのラベルに飛ぶ
		
	} else : if ( (1 <= answer && answer <= MAX_ANSWER) == 0 ) {
		mes "1 〜 "+ MAX_ANSWER +" で選んでね。"
		
	} else : if ( answer * 2 < question ) {		// 問題は、回答の二倍以上
		mes "小さすぎない?"
		
	} else : if ( answer / 2 > question ) {		// 問題は、回答の半分以下
		mes "大きすぎない?"
		
	} else : if ( answer < question ) {		// 問題は、回答より大きい
		mes "もっと大きいよ"
		
	} else : if ( answer > question ) {		// 問題は、回答より小さい
		mes "もっと小さいよ"
	}
	
	// ここに来るということは、正解じゃない
	if ( nAnsCount >= ANS_LIMIT ) {		// 限界まで答えていたら
		gosub *TimeUp			// 時間切れのラベル
	}
	return
	
// 正解した
*Clear
	color 0, 0, 255
	font msgothic, 50, 1
	mes "正解!お見事!!"
	
	dialog "正解です!\nあなたは "+ nAnsCount +"回で正解しました", 0, "おめでとう"
	
	// 画面をリセット
	gosub *ResetWindow
	
	// 問題変更
	nAnsCount = 0
	gosub *CreateQuestion
	return
	
// 時間切れ
*TimeUp
	color 255, 0, 0
	font msgothic, 60, 1
	mes "タイムアップ!!"
	
	dialog "時間切れです。答えは "+ question +"でした。", 0, "残念"
	
	// 画面をリセット
	gosub *ResetWindow
	
	// 問題変更
	nAnsCount = 0
	gosub *CreateQuestion
	
	return

数当てゲームを改良しよう

こんにちは、上大です。
↑にあるのはこの前作成した数当てゲームを、構造化したものです。

さて、今回は、数当てゲームのバージョンアップです。
あのままじゃどう考えてもシンプルすぎます!
というわけで、タイトル画面を実装しようと思います。


タイトル画面とは?

タイトル画面というのは、ゲームが起動されたときに表示される画面のことです。
いきなりゲームが開始するのではちょっと、おもしろみが欠けますね?

主に、タイトル画面には、以下の3つがあれば良いでしょう。

べた塗りというのは、一色で塗りつぶされたものです。boxfで塗りつぶしたのがべた塗りになります。
選択する項目には、「ゲーム開始」や「ルール説明」、「終了」などがあります。(ルール説明はなくてもいいです)

ちなみに、今回の実装ではべた塗りの背景を使います。
僕は、画像を用意するのが得意ではないので。(>*<;)


項目選択を実装する

とりあえず、「項目選択」から始めましょう。
難しいのはこれの実装だけです。

選択させるには、主に3つのやり方があります。
それぞれの長所と短所を書き出してみます。

項目選択の手法
ボタン選択 領域クリック カーソルで選択
外見 オブジェクト なんでもあり (キー入力)
長所
(メリット)
プログラムが簡単。
変数が不要。
見た目が自由。
ループが不要。
選択する雰囲気が出る。
短所
(デメリット)
見た目が悪い
(シンプルすぎる)
プログラムが難しい。
変数をたくさん使う。
プログラムが長くなる。
キー入力を監視する必要がある。
項目の配置を、カーソルの動き方がわかるよう考慮する必要がある。

とりあえずサンプルをそれぞれ用意しました。
[ボタン領域クリックカーソル]

さて、今回は「カーソル選択」の方法を使います。
というわけで、stick命令の知識が必要です

// stick を使う

#const IDW_Main 0	// 今回は絶対いらない!

*main
	gosub *SettingWindow
	goto  *mlp
	
*mlp
	redraw 2
	
	color 255, 255, 255 : boxf : color
	
	// 変数 keys にキー押し下げ状況が格納される
	stick keys, -1		// -1 は、すべて非トリガータイプになります。
	pos 20, 20
	mes "keys = "+ keys
	
	redraw 1
	
	await 17
	goto *mlp
	
*SettingWindow
	screen IDW_Main, 240, 180
	title "stick test"
	return

こんなに簡単なスクリプトなら、構造化する必要がありません。
*mlp ラベルの部分だけで良かったのに……

それは置いておいて、↑のスクリプト実行中に、キーをいくつか押してみてください。
気付いたかもしれませんが、複数同時に押すと、数が加算されます。

stick命令を実行すると、p1 (↑では keys ) の変数に状況を代入します。
押し下げ状態を感知できるキーの数は少なめですが、一度にいくつもチェックできるので便利です。
しかし、さっきのように加算されてしまうと、

	if ( keys == 1024 ) :	// [Tab]キーが押されたなら……

のように相等演算子 == でやると、失敗します。
さて、どうしましょう。

stickのワンキー・ヘルプを見てください。
キーごとに、値が2倍に増えていっているのに気がつきましたか? (知らなきゃ普通気付きません)
これを利用します。
2倍に増えていっているということは、2のN乗であることが分かります。
値が2のN乗かを調べて、違ったら、値より小さい数の中で、一番大きい2のN乗になる数を探します。
その探した数のキーは押されています。
元の値からこのキーの値を引いて、また元に戻ります。
これを元の値が0になるまで続けていくと、押されたキーの組み合わせが分かります。
ちょっと難しいのですが、↓のサンプルでやってみています。

// stick から組み合わせを取得

#const IDW_Main 0	// 今回も絶対いらない!

*main
	gosub *SettingWindow
	goto  *mlp
	
*mlp
	redraw 2
	
	color 255, 255, 255 : boxf : color
	
	stick keys, -1		// 変数 keys にキー押し下げ状況が格納される
	pos 20, 20
	mes "keys = "+ keys
	
	repeat
		if ( keys == 0 ) { break }
		
		// keys が2のN乗かどうかを調べる
		bBinPow = 0			// 2のN乗であるかどうかを表す数
		npow2_n = 1			// 2のN乗の数
		repeat 10, 1
			if ( keys == npow2_n ) { bBinPow = 1 : break }	// なる
			npow2_n *= 2
		loop
		
		if ( bBinPow != 0 ) {
			mes "Down : "+ keys
			break
		}
		
		// keys より小さい中で最大の、2のN乗の数を求める
		npow2_n = 1
		repeat 10, 1
			// 2倍して keys を超えるなら、ここでやめる
			if ( (npow2_n * 2) > keys ) { break }
			
			npow2_n *= 2		// 2倍する
		loop
		
		// keys からその値を引く
		mes "Down : "+ npow2_n		// 押されていたことを表す
		keys -= npow2_n
		
		// 始めに戻る
	loop
	
	redraw 1
	
	await 17
	goto *mlp
	
*SettingWindow
	screen IDW_Main, 240, 180
	title "stick test"
	return

さて出来ました! 完璧です!
完璧ですが、…………面倒です!!!!
こんな面倒な命令があるわけがありません。
当然、もっと簡単な方法があるはずです。

簡単な方法は、あるにはあります。これを使うと、一発で押されているか分かります。
しかし、説明していると非常にめんどうです。しかも非常にわかりにくいので、説明は省かせてもらいます。
狂う可能性99%の計画では、中級編のどこかで説明しますが、今はstick用として使ってください。
ビット演算子and(&)です。

使い方だけ説明します。
stickのp1の変数に代入された値と、押されてるか調べたいキーの値を、& して、if文の条件にします。
押されていたら、条件が成立します。

// stick の値を & で判定

#const IDW_Main 0	// 今回も絶対いらない!

*main
	gosub *SettingWindow
	goto  *mlp
	
*mlp
	redraw 2
	
	color 255, 255, 255 : boxf : color
	
	stick keys, -1		// 変数 keys にキー押し下げ状況が格納される
	pos 20, 20
	mes "keys = "+ keys
	
	npow2_n = 1
	
	repeat 11
		if ( keys & npow2_n ) {	// 押されていたら
			mes npow2_n
		}
		npow2_n *= 2
	loop
	
	redraw 1
	
	await 17
	goto *mlp
	
*SettingWindow
	screen IDW_Main, 240, 180
	title "stick test"
	return

僕の考えた方法でやるより、びっくりするほど早いですね。
これがまさに、機械の力です。

補足みたいですが、stickの p2 に「非トリガータイプキー指定」というものがあります。
トリガータイプなら、押された瞬間しか反応しません。
非トリガーにすると、押しっぱなしに対応できます。

さて、stick命令の説明は完了です。(足りなければ掲示板へどうぞ)

stick さえ使えれば、ほかに難しいことはありません。
やることはさっきと同じです。


タイトルを描画する

font で、でっかい文字にしたらOK!
以上。


背景を描画する

本当は、画像や絵を背景にした方が良いのですが、
面倒なので boxf 命令を使います。


タイトル画面を実装する

ここから、本格的にタイトル画面を実装します。
とはいっても難しいのは「項目選択」ぐらいで、
これを一人で組むのは難しいのじゃないかなーと…………。

という訳で、今回はいきなり完全版をお届けします。(おい)

// 数当てゲーム - タイトル画面つき

#packopt name "数当てゲーム"
#packopt hide 1

// 定数(= const)を宣言
#const IDW_Main	0		// メイン画面のウィンドウID
#const MAX_ANSWER	100		// 答えの最大
#const ANS_LIMIT	6		// 回答できる限界

#const WIN_SIZEX	480		// ウィンドウのサイズX (横幅)
#const WIN_SIZEY	320		// ウィンドウのサイズY (高さ)

*main
	gosub *initialize		// 一番最初のサブルーチン
	gosub *SetVariable		// 変数を用意
	gosub *SettingWindow		// スクリーンを構築
	gosub *ChangeScreenTitle	// タイトル画面にする
	gosub *CreateQuestion		// 問題を作る
	
	gsel IDW_Main, 1
	goto *mainlp			// main loop に突入
	
*mainlp
	redraw 2
	
	// カーソル移動を読む
	stick kDown
	
	if ( kDown & 2 ) {			// ↑ボタン
		gosub *CursorUp
		gosub *ChangeScreenTitle	// 書き直す (再描画する)
		
	} else : if ( kDown & 8 ) {		// ↓ボタン
		gosub *CursorDown
		gosub *ChangeScreenTitle	// 書き直す (再描画する)
		
	} else : if ( kDown & 32 ) {	// [Enter] 決定
		// subroutine jump !
		switch iCursor
		case 0
			gosub *ChangeScreenToGame	// ゲーム画面にする
			repeat					// ゲーム中は停止する
				if ( bGameEnd != 0 ) {	// 偽じゃなくなったら
					bGameEnd = 0		// 戻しておく
					break			// 終了する
				}
				wait 10
			loop
			gosub *ChangeBackColor	// ついでに背景色を変更しておく
			gosub *ChangeScreenTitle	// タイトル画面にする
			swbreak
		case 1
			gosub *ChangeBackColor	// 変更
			gosub *ChangeScreenTitle	// 再描画して有効にする
			swbreak
		case 2 : goto *exit : swbreak
		swend
	}
	
	redraw 1
	await 51
	goto *mainlp
	
*CursorUp
	iCursor --
	if ( iCursor < 0 ) { iCursor = 2 }
	return
	
*CursorDown
	iCursor ++
	iCursor \= 3
	return
	
//######## サブルーチン群 ########
*exit
	end
	
*initialize
	randomize		// 一回だけ使えばいい
	gsel 0, -1
	return
	
// 変数を用意
*SetVariable
	dim question			// 問題の答え
	dim answer			// プレイヤーの回答
	dim nAnsCount			// 回答した回数
	dim bGameEnd			// ゲームが終了したとき真になる
	
	dim ID_Edit			// input のオブジェクトID	
	
	dim iCursor			// カーソル
	dim kDown			// キー押し下げ状況
	
	gosub *ChangeBackColor	// タイトル画面の背景色
	
	return
	
// スクリーンを構築する
*SettingWindow
	screen IDW_Main, WIN_SIZEX, WIN_SIZEY, 2	// メイン画面
	title "数当てゲーム ver.1.1"			// バージョンアップ!
	
	return
	
// 画面を完全にリセットする
*ResetScreen
	color 255, 255, 255 : boxf : color
	clrobj			// オブジェクトをすべて削除
	pos 0, 0		// カレントポジションも忘れずに!
	return
	
// タイトル画面にする
*ChangeScreenTitle
	redraw 2
	
	gosub *ResetScreen		// リセット
	
	// 背景色で塗りつぶし
	color titleclr_r, titleclr_g, titleclr_b : boxf : color
	
	// でっかいタイトルを書く
	font msgothic, 80			// 巨大
	pos 10, 20 : mes "数当てゲーム"
	
	// 選択項目を設置
	sysfont 0			// 通常のフォント
	
	if ( iCursor == 0 ) { pos 30, 120 : mes "→" : color 255 } else { color }
		pos 50, 120 : mes "ゲームを始める"
		
	if ( iCursor == 1 ) { pos 30, 140 : mes "→" : color 255 } else { color }
		pos 50, 140 : mes "背景色を変更する"
		
	if ( iCursor == 2 ) { pos 30, 160 : mes "→" : color 255 } else { color }
		pos 50, 160 : mes "終了する"
	
	
	redraw
	return
	
// ゲーム用の画面にする
*ChangeScreenToGame
	redraw 2
	
	gosub *ResetScreen		// リセット
	color titleclr_r, titleclr_g, titleclr_b : boxf : color
	sysfont 0		// 最初のフォント
	objsize 80, 25						// ボタンの大きさを設定
	pos  20,  50 : mes "1 〜 "+ MAX_ANSWER +"までの私を当ててみよ!"
	pos  30,  80 : input answer, 120, 24		// 回答
	pos 160,  80 : button gosub "答える", *OnAnswer	// 答え合わせ
	
	pos  50, 140					// カレントポジションを移動
	
	redraw
	return
	
// タイトル画面の背景色を変更する
*ChangeBackColor
	titleclr_r = (rnd(2) + 1) * 127 + 1		// 128 or 255
	titleclr_g = (rnd(2) + 1) * 127 + 1		// 明るめの色になるはず
	titleclr_b = (rnd(2) + 1) * 127 + 1
	return
	
// 問題を作る
*CreateQuestion
	// 0〜MAX_ANSWER-1 なので、+1 して 1〜MAX_ANSWER にする
	question = rnd(MAX_ANSWER) + 1
	return
	
// 判定処理 ( button で飛んでくる )
*OnAnswer
	// * 判定する順番は大事です。
	
	nAnsCount ++		// 答えた回数を数える
	
	if ( answer == question ) {
		// 正解
		gosub *Clear				// 正解したときのラベルに飛ぶ
		
	} else : if ( (1 <= answer && answer <= MAX_ANSWER) == 0 ) {
		mes "1 〜 "+ MAX_ANSWER +" で選んでね。"
		
	} else : if ( answer * 2 < question ) {	// 問題は、回答の二倍以上
		mes "小さすぎない?"
		
	} else : if ( answer / 2 > question ) {	// 問題は、回答の半分以下
		mes "大きすぎない?"
		
	} else : if ( answer < question ) {		// 問題は、回答より大きい
		mes "もっと大きいよ"
		
	} else : if ( answer > question ) {		// 問題は、回答より小さい
		mes "もっと小さいよ"
	}
	
	// ここに来るということは、正解じゃない
	if ( nAnsCount >= ANS_LIMIT ) {		// 限界まで答えていたら
		gosub *TimeUp				// 時間切れのラベル
	}
	return
	
// 正解した
*Clear
	color 0, 0, 255
	font msgothic, 50, 1
	mes "正解!お見事!!"
	
	dialog "正解です!\nあなたは "+ nAnsCount +"回で正解しました", 0, "おめでとう"
	
	// 画面をリセット
	gosub *ChangeScreenToGame
	
	// 問題変更
	nAnsCount = 0
	gosub *CreateQuestion
	
	bGameEnd = 1	// ゲームが終了したことを表す
	return
	
// 時間切れ
*TimeUp
	color 255, 0, 0
	font msgothic, 60, 1
	mes "タイムアップ!!"
	
	dialog "時間切れです。答えは "+ question +"でした。", 0, "残念"
	
	// 画面をリセット
	gosub *ChangeScreenToGame
	
	// 問題変更
	nAnsCount = 0
	gosub *CreateQuestion
	
	bGameEnd = 1
	return

さて、解説は不要でしょう。
もうすでにめちゃめちゃ長いですし、これ以上長くなってほしくないので、解説は省略、自分で読んでください。

※わからなかったら、ここの掲示板で質問してもかまいません。

ふぅ、これで以上です。また次回。


by 上大

第十章へ   第十二章へ