HSP3でのデザインパターン

概要

 ソフトウェア開発にはデザインパターン(設計パターン)というものがあるそうで、 過去のノウハウを蓄積して名前をつけて再利用しやすいようにしたものだそうです。

 HSPにもそういうのがあればプログラミング効率も向上するんじゃないかなと思い、 私の経験に基づくプログラムのパターンを集約してみました。 他の人のやり方も見てみたいんですが難しいかな。○○をするサンプルとかだと沢山あるんですけどね。

 厳密には「デザインパターン」の定義には当てはまらないと思いますがそこはご容赦を。 適当です。

項目一覧

 書き上げてみたら項目がたくさんになってしまいました。 1つ1つ探すのも面倒だと思うので目次つけましたので下記の一覧からどうぞ。 あ、上から順に見ていってもいいですよ。

ハローワールド

「Hello, world!」です。
HSPの場合最もシンプルな構造はこれに尽きますね。

mes "Hello, world!"

解説

解説の必要もないくらいです。
上から順番に書かれている順に実行される。
最後の行まで来たら実行が一時中断される。(stop命令と同じ動作になる。)
「直線的に上から下に流れる」というデザインです。分岐もジャンプもなにもないシンプルなもの。

サブルーチン

 今まで「ルーチンワーク」や「ルーチン~」と言ってきたのに最近(これを書いている今は2016年)になって急に「ルーティーン」と 英語の正しい発音に近い読み方を耳にする機会が増えましたね。 今更カッコつけたくなったのか、今まで知らなかった人がroutineという単語を知るようになったのかわかりませんが、未だに違和感と抵抗感があります。

 ということで、サブルーチン(subroutine)です。subroutineの日本語訳がサブルーチンなので、サブルーティンと発音するときっと「なぜそこだけ英語?」という顔をされます。気をつけたいですね。
さておき、特定の処理を何度も使う場合、サブルーチンは欠かせません。 少し込み入ったものを作ろうとすると必ず使用するパターンです。

;-----------------------------------------------------------
;
;	メインルーチン
;
;-----------------------------------------------------------

;	サブルーチンの呼び出し
gosub *label_1
gosub *label_2
on 1 gosub *label_1, *label_2

stop
// メインルーチンここまで



;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------
*label_1
	//	ここにサブルーチンの処理を書く
	mes "サブルーチン 1"
	return

*label_2
	//	ここにサブルーチンの処理を書く
	mes "サブルーチン 2"
	return

解説

gosubでジャンプして、returnでgosub命令の次の行に戻る。簡単な動作です。

 押さえるべきポイントもあまりありませんが、あえて書くと次のようになります。
 メインルーチンとサブルーチンの記述位置が重要で、サブルーチンを先に書いてしまうとサブルーチンが実行されてしまいます。 またメインルーチンの最後をstopなどで止めないと、メインルーチン実行後にサブルーチンが実行されてしまいます。 gosubでのジャンプ以外でサブルーチンが実行されないように記述することが重要です。 メインルーチンと明確に切り分けて記述する事も重要です。

ボタン

 ボタンオブジェクトなどからサブルーチンにジャンプするケースです。
 前述したサブルーチンを使ったものとあまり変わりませんが、ユーザーのアクションをきっかけにサブルーチンに飛ぶパターンです。 button命令に限らず、何らかのユーザーアクションをきっかけにサブルーチンを実行するケースはよくありますね。

;-----------------------------------------------------------
;
;	メインルーチン
;
;-----------------------------------------------------------
button gosub "ボタン 1", *label_1
button gosub "ボタン 2", *label_2
stop

;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------
*label_1
	//	ここにサブルーチンの処理を書く
	mes "サブルーチン 1"
	return

*label_2
	//	ここにサブルーチンの処理を書く
	mes "サブルーチン 2"
	return

解説

 stop命令の行でユーザーアクションを待機します。 ボタンが押されるとラベルにジャンプして、returnでまたstop命令の行に戻ります。 gosub命令同様に、メインルーチンとサブルーチンの記述位置が重要です。

 stop命令の代わりにawait命令を使用した場合、await命令の行でユーザーアクションを待機します。 この場合gosub先からreturnで戻ると、awaitの残った待機時間を無視してawait命令の次の行に戻って実行を再開します。 これ意外と盲点だったりするので注意が必要です。

 メインルーチンと明確に切り分けて記述する事も重要です。

イベント駆動

 HSPが用意しているボタン以外のユーザーアクションでサブルーチンにジャンプするパターンです。 ツールを作る場合によく使います。

 ゲームの場合はループが常に動いているのでループ内で確認すればいいのですが、ツールだとそういうわけにも行かないのでイベントを取得してサブルーチンにジャンプします。

 無限ループでループ待機しておいてstick命令とかで取得すればいいじゃないか?と思うかもしれませんが、 イベント駆動型はループ待機に比べると動作が圧倒的に軽く、CPU負荷が小さくなります。また入力の取りこぼしも発生しません。 例えば、ループ待機の場合は1回のループ中に2回以上クリックしても1回しかクリックを検出できませんが、 イベント駆動型なら確実に全てのクリックを検出できます。

;-----------------------------------------------------------
;
;	初期化
;
;-----------------------------------------------------------
#define WM_COMMAND     $111


;-----------------------------------------------------------
;
;	ウィンドウ作成 1個目
;
;-----------------------------------------------------------
screen 0
mes "クリック検出"
mes "キー入力検出"
mes "アプリ終了検出"
a = "メッセージ検出"
input a, 200 : inputId = stat

;-----------------------------
;	割り込みの設定
;-----------------------------
onclick gosub *label_click
onkey   gosub *label_key
onexit  goto  *label_exit
oncmd   gosub *l_On_window_WM_COMMAND, WM_COMMAND


;-----------------------------------------------------------
;
;	ウィンドウ作成 2個目
;
;-----------------------------------------------------------
screen 1
b = "メッセージ検出"
input b, 200 : inputId_b = stat
oncmd   gosub *l_On_window_WM_COMMAND_b, WM_COMMAND

;	メインのウィンドウをアクティブにする
gsel 0, 1
stop


;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------

;-----------------------------
;	イベント:クリック
;-----------------------------
;マウスクリック時に動作
;
*label_click
	;iparam マウスボタンID
	;	0 マウスの左ボタン
	;	3 マウスの右ボタン
	;	6 マウスの中ボタン(ホイールボタン)
	;
	;lparam マウスポインタのクライアント座標位置
	;	下位ワードにX座標
	;	上位ワードにY座標
	;
	;wparam 押し下げられているボタン番号
	;	 1 マウスの左ボタン
	;	 2 マウスの右ボタン
	;	 4 +Shiftキー
	;	 8 +Ctrlキー
	;	16 マウスの中ボタン(ホイールボタン)
	;
	
	;	マウス座標を取得
	x = lparam & 0xffff
	y = lparam>>16
	mes "MOUSE = " + iparam + ", " + wparam + ", X = " + x + ", Y=" + y
	return

	
;-----------------------------
;	イベント:キー
;-----------------------------
;キー入力時に動作
;
*label_key
	;iparam 押し下げられているキーコード(文字コード)
	;	キーコードは、getkey命令と同じ数値
	;	文字入力関連以外は 0
	;
	;lparam いくつかのキー押し関連情報
	;	30ビット目 そのキーが直前に押されている(1)か、離されている(0)
	;
	;wparam 押し下げられているキーコード(仮想キーコード)
	;	キーコードは、getkey命令と同じ数値
	;
	
	if iparam!0 {
		s = strf("[%c]", iparam)
	} else {
		;iparam = 0 の場合
		s = "[]"
	}
	mes "KEY = " + wparam + " " + s + (lparam>>30&1)

	return

;-----------------------------
;	イベント:終了
;-----------------------------
;終了時に動作
;
*label_exit
	;iparam 終了した要因
	;	0 ユーザーの意思による通常の終了
	;	1 システムによる終了 (Windowsのシャットダウン時)
	;
	;lparam <未使用>
	;
	;wparam 終了の割り込み処理を行ったウィンドウID値
	;

	dialog "終了します。\n終了した要因 " + iparam + "\nウィンドウID " + wparam
	;	必ずendを呼んで終了させる必要がある
	end

;-----------------------------
;	イベント:メニューアイテムを選択
;-----------------------------
;メニューアイテムを選択した時に動作
;
*l_On_window_WM_COMMAND
	wID         =  wParam & 0xFFFF			;wParam パラメータの下位ワードの値(メニューアイテム、コントロール、アクセラレーターの ID)
	wNotifyCode = (wParam >> 16) & 0xFFFF	;wParam パラメータの上位ワードの値(通知コード)
	hwndControl =  lParam					;lParam パラメータの値(ハンドル)

	;	スクリーン 0 の入力ボックスのハンドルを取得
	wid = ginfo_sel
	gsel 0
	hnd = objinfo (inputId, 2)
	
	;	変更が検出された場合の動作
	if hwndControl = hnd {
		mes "入力ボックスが変更された。"
	}

	gsel wid
	return


;-----------------------------
;	イベント:メニューアイテムを選択
;-----------------------------
;2つ目のウィンドウのメニューアイテムを選択した時に動作
;
*l_On_window_WM_COMMAND_b
	wID         =  wParam & 0xFFFF			;wParam パラメータの下位ワードの値(メニューアイテム、コントロール、アクセラレーターの ID)
	wNotifyCode = (wParam >> 16) & 0xFFFF	;wParam パラメータの上位ワードの値(通知コード)
	hwndControl =  lParam					;lParam パラメータの値(ハンドル)

	;	スクリーン 0 の入力ボックスのハンドルを取得
	wid = ginfo_sel
	gsel 1
	hnd = objinfo (inputId, 2)
	
	;	変更が検出された場合の動作
	if hwndControl = hnd {
		mes "The input box has been changed."
	}

	gsel wid
	return

解説

 割り込み実行指定系の命令は、ウィンドウ上に配置する見えないオブジェクトというつもりで配置して使用します。 onclick、onkey、onexitはどのウィンドウに配置しても同じなので複数配置しても、最後の1個しか動作しません。 これに対しoncmdは同じメッセージIDでもウィンドウごとにそれぞれ配置できます。 また同じウィンドウ内でもメッセージIDが異なれば何個でも置くことが出来ます。

 ウィンドウ作成中に動作してしまわないよう、ウィンドウを作る最後に記述するのが無難だと思います。 引数 0 で、イベント割り込み実行の一時的なOFFをするのもいいかもしれませんね。

エラー処理

 エラーというイベントが発生した場合の処理なので、これもイベント駆動の分類と言えます。 それでも分けて書くのには理由があります。

 onerror命令は、エラーを無視してプログラムを継続実行させ続けるための命令ではありません。 エラーが発生した場合に、これまでの作業データを保存してから終了するなどの「安全な終了」をするためのものです。

;
;	エラー処理
;

;-----------------------------------------------------------
;
;	ウィンドウ作成
;
;-----------------------------------------------------------
button goto "エラー検出", *label_make_err

;-----------------------------
;	割り込みの設定
;-----------------------------
onerror goto  *label_err
stop


;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------

;-----------------------------
;	エラーを作成
;-----------------------------
*label_make_err
	;	#Error 19 「0で除算しました」
	i = 1/0
	stop


;-----------------------------
;	イベント:エラー
;-----------------------------
;エラー発生時に動作
;
*label_err
	;wparam エラー番号
	;lparam エラー発生行番号
	;iparam 0(なし)
	sdim msg, 128
	if wparam =  1 : msg = "システムエラーが発生しました"
	if wparam =  2 : msg = "文法が間違っています"
	if wparam =  3 : msg = "パラメータの値が異常です"
	if wparam =  4 : msg = "計算式でエラーが発生しました"
	if wparam =  5 : msg = "パラメータの省略はできません"
	if wparam =  6 : msg = "パラメータの型が違います"
	if wparam =  7 : msg = "配列の要素が無効です"
	if wparam =  8 : msg = "有効なラベルが指定されていません"
	if wparam =  9 : msg = "サブルーチンやループのネストが深すぎます"
	if wparam = 10 : msg = "サブルーチン外のreturnは無効です"
	if wparam = 11 : msg = "repeat外でのloopは無効です"
	if wparam = 12 : msg = "ファイルが見つからないか無効な名前です"
	if wparam = 13 : msg = "画像ファイルがありません"
	if wparam = 14 : msg = "外部ファイル呼び出し中のエラーです"
	if wparam = 15 : msg = "計算式でカッコの記述が違います"
	if wparam = 16 : msg = "パラメータの数が多すぎます"
	if wparam = 17 : msg = "文字列式で扱える文字数を越えました"
	if wparam = 18 : msg = "代入できない変数名を指定しています"
	if wparam = 19 : msg = "0で除算しました"
	if wparam = 20 : msg = "バッファオーバーフローが発生しました"
	if wparam = 21 : msg = "サポートされない機能を選択しました"
	if wparam = 22 : msg = "計算式のカッコが深すぎます"
	if wparam = 23 : msg = "変数名が指定されていません"
	if wparam = 24 : msg = "整数以外が指定されています"
	if wparam = 25 : msg = "配列の要素書式が間違っています"
	if wparam = 26 : msg = "メモリの確保ができませんでした"
	if wparam = 27 : msg = "タイプの初期化に失敗しました"
	if wparam = 28 : msg = "関数に引数が設定されていません"
	if wparam = 29 : msg = "スタック領域のオーバーフローです"
	if wparam = 30 : msg = "無効な名前がパラメーターに指定されています"
	if wparam = 31 : msg = "異なる型を持つ配列変数に代入しました"
	if wparam = 32 : msg = "関数のパラメーター記述が不正です"
	if wparam = 33 : msg = "オブジェクト数が多すぎます"
	if wparam = 34 : msg = "配列・関数として使用できない型です"
	if wparam = 35 : msg = "モジュール変数が指定されていません"
	if wparam = 36 : msg = "モジュール変数の指定が無効です"
	if wparam = 37 : msg = "変数型の変換に失敗しました"
	if wparam = 38 : msg = "外部DLLの呼び出しに失敗しました"
	if wparam = 39 : msg = "外部オブジェクトの呼び出しに失敗しました"
	if wparam = 40 : msg = "関数の戻り値が設定されていません。"
	if wparam = 41 : msg = "関数を命令として記述しています。"
	if msg = "" : msg = "未知のエラーが発生しました。"
	
	;	エラーメッセージを自作して表示
	dialog "#Error " + wparam + " in line " + lparam + "\n-->" + msg, 1, "Error(onerrorエラー処理)"

	;	エラーが出た場合は、再開せずに終了
	end

解説

 onerrorはgotoで使用して、ジャンプ先のサブルーチンはendで終わる。 retrunなどで復帰しないようにしてください。エラーを放置して復帰すると何があるかわかりません。 ユーザーも何があるかわからない状況で動き続ける事は望んでいません。確実な動作、信頼できる結果を求めています。 エラーが出たらこれらが保証できないのできちんと終了して信頼されるプログラムにしましょう。

 そもそもプログラムは、予想できるエラーは回避するよう組み上げていくべきです。 0除算エラー(Error 19)なら、割り算をする前にゼロでは無いことを確認するようスクリプトを記述するべきです。 画像ファイルがみつからない(Error 13)なら、ファイルを開く前にファイルの有無を確認するべきです。

ゲームでよく使うループ

 HSPではツール作る人よりもゲームを作る人のほうが多いんじゃないでしょうか。 まずはシンプルで最小構成のパターンです。 よく使うので、私は「かんたん入力」に登録してすぐ呼び出せるようにしています。


// ループに入る前の処理を記述

;-----------------------------------------------------------
;
;	メインループ
;
;-----------------------------------------------------------
*main
	redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	// ここに1ループで行う処理を記述
		
	goto *main

解説

 ゲームの場合は動いたときにだけ画面を書き換えるのではなく、常に画面全体を1秒間に30回(60回の場合もある)の一定ペースで画面を書き換え続けます。TVや映画と同じですね。

 ループが1回まわることを1フレームと数えます。 画面を書き換える速さをフレームレートと言い、1秒間に30フレーム書き換えが発生した場合、この速さは30fps(Frame Per Second=フレーム/秒)と表します。 TV放送だと30fps、映画やアニメだと24fpsです。ゲームでも30fpsあれば十分ですが、映像の滑らかさを求めて60fpsや120fpsのものもあります。当然負荷が高いので必要に応じて選択するべきでしょう。

 蛇足ですが、TVなどのアニメーションでは3コマ打ちや2コマ打ちと言うものがありそれぞれ8fps、12fpsなのですが動きのスピード感やコストなどの理由でよく使われるそうです。 むしろフルアニメーションと言われるヌルヌル動く24fpsの方は、金がかかりすぎるのであまりやらないみたいです。場面によってはヌルヌル過ぎて気持ち悪いというのもあるようです。 こういうこともあるのでゲームを作る際、無駄に高fpsで制作する必要はありません。特に必要がなければ30fpsもあれば十分です。

ゲームでのキー入力

 ゲームでは必ずユーザーの入力ができないと話になりません。出来なきゃただのアニメーション動画になってしまいます。 そのためにキーの取得と取得した値の検出が必要になります。

 ゲームコントローラーを使った入力は「mod_joystick」のjstick命令を使ってください。

dim key
dim mc

;-----------------------------------------------------------
;
;	メインループ
;
;-----------------------------------------------------------
*main
	redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	;---------------------
	;	入力値を取得
	;---------------------
	stick  key, 0x07FF
	getkey mc, 4


	;---------------------
	;	入力値に応じた動作を出力
	;---------------------
	
	;	キー入力情報を判定
	if (key &    1)!0 : mes "カーソルキー左(←)"
	if (key &    2)!0 : mes "カーソルキー上(↑)"
	if (key &    4)!0 : mes "カーソルキー右(→)"
	if (key &    8)!0 : mes "カーソルキー下(↓)"
	if (key &   16)!0 : mes "スペースキー"
	if (key &   32)!0 : mes "Enterキー"
	if (key &   64)!0 : mes "Ctrlキー"
	if (key &  128)!0 : mes "ESCキー"
	if (key &  256)!0 : mes "マウスの左ボタン"
	if (key &  512)!0 : mes "マウスの右ボタン"
	if (key & 1024)!0 : mes "TABキー"
	if mc = 1 : mes "3ボタンマウスのまん中のボタン"

	goto *main

解説

 stickやjstick命令で取得した値には複数のキー入力の情報が入っています。 それぞれのキーの状態はビット演算で取り出して調べる必要があります。 ビット演算はわかると簡単なんですが、プログラム初心者が多いHSPでは躓きやすいポイントになっています。 論理演算との区別が曖昧なまま使うからです。 大抵のサンプルは「!0」が省略されているからというのもあるかもしれませんね。 基本的にHSPでは論理演算はありません。AND(&)やOR(|)などはビット演算です。

 getkeyは1個ずつしか検出出来ず、簡単なので説明は省略します。

ゲームでのキー入力2

 上述の取得方法だけでは十分ではない場合があります。 もう少し多彩な入力値の検出方法のパターンを見てみましょう。 具体的には次のような場合の検出方法です。

  • 前ループのキーの状態
  • 今のキーの状態
  • 押下(トリガータイプとして取得)
  • 押上(トリガータイプとして取得)
  • 継続して押下
  • 継続して押していない

 またgetkeyでしか取得できないキーをゲームに使いたい場合、stick命令で取得した値と 異なる処理を行うとスクリプトが複雑になってしまいます。 同じ記述ができるようにしたほうがスクリプトがスッキリするのでバグも出にくくすることが出来ます。

dim key
dim mc

;-----------------------------------------------------------
;
;	メインループ
;
;-----------------------------------------------------------
*main
	redraw 1 : await 160 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	;---------------------
	;	入力値を取得
	;---------------------
	key0 = key1
	stick  key1, 0x07FF
	getkey mc, 4

	;	キー入力の状態を取り出しやすいように変換
	;キー入力値を合成して以下の状態を取得できるように変換
	; key0 : 前ループのキーの状態
	; key1 : 今のキーの状態
	; key2 : 押下(トリガータイプとして取得)
	; key3 : 押上
	; key4 : 継続押下
	; key5 : 継続して押していない
	key1 |= mc<<11
	key2  = key0 ^ 0xFFFFFFFF & key1
	key3  = key0 & (key1 ^ 0xFFFFFFFF)
	key4  = key0 &  key1
	key5  = key0 |  key1 ^ 0xFFFFFFFF

	;---------------------
	;	入力値に応じた動作を出力
	;---------------------
	key = key1
	;	キー入力情報を判定
	if (key & 0x0001)!0 : mes "カーソルキー左(←)"
	if (key & 0x0002)!0 : mes "カーソルキー上(↑)"
	if (key & 0x0004)!0 : mes "カーソルキー右(→)"
	if (key & 0x0008)!0 : mes "カーソルキー下(↓)"
	if (key & 0x0010)!0 : mes "スペースキー"
	if (key & 0x0020)!0 : mes "Enterキー"
	if (key & 0x0040)!0 : mes "Ctrlキー"
	if (key & 0x0080)!0 : mes "ESCキー"
	if (key & 0x0100)!0 : mes "マウスの左ボタン"
	if (key & 0x0200)!0 : mes "マウスの右ボタン"
	if (key & 0x0400)!0 : mes "TABキー"
	if (key & 0x0800)!0 : mes "3ボタンマウスのまん中のボタン"


	goto *main

解説

 まず「key1 |= mc<<11」として、空いている領域にgetkeyで取得した情報を書き込みます。 以降はstick命令で取得した場合と同じ要領で情報を取り出すことが出来るようになります。 さらにgetkeyで取得した別のキーの情報を書き込む場合は、 「key1 |= mc<<12」「key1 |= mc<<13」というように書き込む場所をずらします。書き込みできるのは11~31までです。

 key2~key5のビット演算はここでは解説しません。キー取得した直後にこう書いておけば後が楽、ぐらいに考えておくといいと思います。

ゲームの描画

 ゲームなら入力に応じた絵を出さないといけません。 簡単なパターンを見てみましょう。

;-----------------------------------------------------------
;
;	初期化
;
;-----------------------------------------------------------
dim key

;	初期表示位置
px = ginfo_winx / 2
py = ginfo_winy / 2

;	移動量
d = 2


;-----------------------------------------------------------
;
;	メインループ
;
;-----------------------------------------------------------
*main
	redraw 1 : await 16 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	;---------------------
	;	入力値を取得
	;---------------------
	stick  key, 0x07FF

	;---------------------
	;	入力値に応じた動作
	;---------------------
	;移動
	if (key & 1)!0 : px -= d
	if (key & 2)!0 : py -= d
	if (key & 4)!0 : px += d
	if (key & 8)!0 : py += d

	;---------------------
	;	描画
	;---------------------
	pos px, py
	mes "テスト"


	goto *main

解説

ループ内の何処に書いても描画はするのですが、処理の順番を次のようにしたほうが複雑にならずにすむと思います。

  1. 入力値を取得
  2. 入力値に応じた動作を計算。座標などの情報を算出。
  3. 描画だけをする。

状態取得→計算→描画。
座標計算など事前にできる計算は先に済ませてしまい、描画のときはなるべく描画だけに集中できるようにした方がスッキリします。

モジュール呼び出し

モジュールやdllを使う場合、ファイルをincludeする必要がありますね。


;-----------------------------------------------------------
;
;	別ファイルを結合
;
;-----------------------------------------------------------
#include "hspmath.as"
//	ここから下にモジュールを使った命令を記述できる

;-----------------------------------------------------------
;
;	メインルーチン
;
;-----------------------------------------------------------
mes pow(2,3)

解説

 モジュールやdllをincludeする場合は、基本的にメインになるスクリプトの一番最初に記述します。 スクリプトの途中に呼び出すなど変則的な呼び出し方は混乱を招くのでお勧めしません。 はじめにまとめて読み込んでしまいましょう。

モジュールの直接記述

 モジュールは別ファイルに記述してincludeしたほうがスクリプトが見やすくなるのですが、 あまり汎用性のないちょっとしたものならメインスクリプトの中に書いてしまったほうが手っ取り早いこともあります。


;-----------------------------------------------------------
;
;	モジュール
;
;-----------------------------------------------------------
#module
;-----------------------------------------------------------
;
;	命令の一行説明文
;
;[ Infomation ]
;	test p1
;	int hikisu=0~(0): 引数の説明
;	stat : 返り値の説明
;
;[ comment ]
; 命令の詳細な説明。
;
;-----------------------------------------------------------
#deffunc test int hikisu
	mes "モジュールの引数:" + hikisu
	res = hikisu * -1
	return res

#global
//	ここから下にモジュールを使った命令を記述できる



;-----------------------------------------------------------
;
;	メインルーチン
;
;-----------------------------------------------------------
test 123
mes stat

解説

 includeする時と同様にメインスクリプトの最上部に記述します。 きちんとコメントを入れてメインプログラムと見分けがつくように注意が必要です。 しかし、あまりいい方法とはいえません。

 作った関数はその使い方もしっかりメモしておきます。 自分が作ったものでも時間が経つとすぐにはどんなものだったかを思い出せないものです。

モジュール

 命令を自作する際にモジュールを使っていると思います。便利ですよね。

 モジュールはサブルーチンと異なり変数名などの重複を気にしなくていいので、 呼び出し元のスクリプトと切り離してプログラムすることが出来ます。 上手に作れば別のソフトを作る際にも再利用が可能なので、作業効率も品質も向上します。なるべく丁寧に作りたいところです。

 またモジュール内で値を保持できるという特徴も持っています。 これも上手に使うと便利な機能です。


;
;[ Infomation ]
; Name      : モジュール正式名称
; SubName   : 簡易名称やサブタイトルなど
; Version   : バージョン番号
; copyright : 作者名
;
;[ Update history ]
; yyyy/mm/dd : ver  : comment
; 更新日     : バージョン : 更新内容
;
;[ Comment ]
; ここにはモジュールの説明などのコメントを書きます。
; コメントはHDLに対応した「ドキュメンテーションコメント」を使用する方法もあります。
;


// 「__MODULE_NAME__」はモジュール独自のユニークな名称を指定します。
// これで同じモジュールの2重読み込みが防止できます。
#ifndef __MODULE_NAME__
#define global __MODULE_NAME__

#module
;===================================================================================================

;-----------------------------------------------------------
;
;	定数の定義
;
;-----------------------------------------------------------
#const global KAZU 100+50
#enum  global KAZU_A = 0
#enum  global KAZU_B
#enum  global KAZU_C


;-----------------------------------------------------------
;
;	1 line comment
;
;[ Infomation ]
;	command p1, p2
;	int p1=0~(0): hogehoge
;	var p2(100)  : hogehoge
;	refstr : hogehoge
;		0 : hogehoge
;		1 : hogehoge
;
;[ comment ]
;
;-----------------------------------------------------------
#deffunc command_ int p1, var p2
	return

#define global command(%1=0, %2=100) command_ %1, %2



;-----------------------------------------------------------
;
;	1 line comment
;
;[ Infomation ]
;	val = function( p1, p2 )
;	int p1=0~(0): hogehoge
;	var p2(100)  : hogehoge
;	val : hogehoge
;		0 : hogehoge
;		1 : hogehoge
;
;[ comment ]
;
;-----------------------------------------------------------
#defcfunc function_ int p1, var p2
	return

#define global ctype function(%1=0, %2=100)	function_(%1, %2)


;===================================================================================================
#global

#endif	;__MODULE_NAME__



;-------------------------------------------------------------------------------
;
;	仮実行スクリプト(デバッグ作業用)
;
;-------------------------------------------------------------------------------

;#####################################################################
;ここにはデバッグ作業用のスクリプトを記述します。
;ここを有効にするとこのファイル単独での実行が可能になります。
;
;0	:リリースモード 本体側から#includeで連結して動作させる場合です。
;1	:デバッグモード このファイル単品で動作確認が出来ます。
#if 0
	;	ここにデバッグ用コードを記述する

	// ここにこのモジュールを使用したサンプルを記述します。
	// 開発中のテストプログラムを書くのにも使えます。

#endif
;#####################################################################

解説

 使い捨てのつもりで作らないというのが一番重要なポイントかもしれません。 と言っても私も1回限りのモジュールファイルを毎回1つは作ってしまうんですが…。

 それでも過去に作ったモジュールを何度も使いまわすので、使いまわせるように作る事が重要になります。 具体的には、使い方やコメントをしっかり残す。バージョン管理をしっかり行う。が必要になってきます。 HDLが読み込めるドキュメンテーションコメントを書くようにするのもいいでしょう。 ドキュメンテーションコメントを用いない場合は、hsファイルを作ってHDL(ヘルプ)で調べることが出来るようにしておきましょう。
 配布の予定がなくても配布するつもりで作りましょう。 そうれば何年前に作ったモジュールでもすぐに使うことが出来ます。 逆にそうしておかないと先月作ったモジュールすら簡単には使えなくなってしまいます。

 モジュールファイルを使っていると、間違えて同じモジュールファイルを何度もincludeしてしまいエラーを発生させてしまうことがあります。 いちいちスクリプトを見直すのは面倒なので、ここではマクロを使って回避しています。 他のモジュールと重複しないユニークな名前を使うようにしてください。

 モジュール作成中は実行テストを頻繁に行います。 またモジュールを使ったプログラムを開発中にバグが見つかった場合、その修正にもテストコードを走らせる必要があります。 そのような場合、例のように「仮実行スクリプト」の領域を用意しておくとテストプログラムを探す手間もなく、手軽にデバッグをすることが出来ます。

コメント

 コメントを軽視すると間違いなく後悔する目にあいます。 自己流でもいいので一定のルールを持ってコメントを入れるようにしたいですね。


;
;[ Infomation ]
; Name      : ソフトウェア正式名称
; SubName   : 簡易名称やサブタイトルなど
; Version   : バージョン番号
; copyright : 作者名
;
;[ Update history ]
; yyyy/mm/dd : ver  : comment
; 更新日     : バージョン : 更新内容
;
;[ Comment ]
; プログラムの説明などのコメント
;

;-----------------------------------------------------------
;
;	モジュール
;
;-----------------------------------------------------------
#module
;-----------------------------------------------------------
;
;	命令の一行説明文
;
;[ Infomation ]
;	command p1, p2
;	int p1=0~(0): 引数の説明
;	var p2(100)  : hogehoge
;	stat : 返り値の説明
;		0 : hogehoge
;		1 : hogehoge
;
;[ comment ]
; 命令の詳細な説明。
;
;-----------------------------------------------------------
#deffunc command
	return

#global


;-----------------------------------------------------------
;
;	セパレータ
;
;-----------------------------------------------------------

;-----------------------------
;	セパレータ
;-----------------------------

	;---------------------
	;	セパレータ
	;---------------------

解説

 ここでは使用頻度が高くパターン化しているものを書きました。 仕様を記述するテンプレートとセパレータです。 コメントはこれだけに限らずバンバン入れておくと、後で助かることがよくあります。なるべく書きましょう。

 またコメントではスクリプトの動きの説明ではなく、動作目的を書くようにしましょう。 動きはスクリプト見ればわかりますからね。 動きの説明だけを書いていると自分が書いたスクリプトに対して 「そんなことは見れば分かる。お前は何がしたいんだ?」とつぶやく羽目になります。

ゲームでキーボード入力を取得

 ゲームでユーザーからの入力値を取得する場合、1フレームに1回しかチェックしないため 1フレームに1回以上の高速入力が行われると値の取得漏れが置きます。 タイピングゲームや格闘ゲームなどの素早い入力が必要なゲームでは致命的な問題につながります。


#include "kernel32.as"
#include "winmm.as"

#module
;-----------------------------------------------------------
;
;	一定の時間で待つ
;
;[ Infomation ]
;	asleep waittime
;	int waittime = 0~(0): 待ち時間(1ms単位)
;	stat : Sleep関数で実際に待機させた時間。[ms]
;
;[ comment ]
; プログラムの実行を一定時間だけ中断します。
; 
; await命令と同様に、前回asleepした時間からの待ち時間を指定します。
; これにより、描画速度の違いなどから時間が早く過ぎることを防止することができます。
; リアルタイムで更新される画面などの速度を一定に保つ時に使用します。
; 
; await 0 の代わりにはなりません。長い時間ループが起こる可能性がある場所には
; waitかawait命令も入れるようにしてください。
; 
; await命令と異なりon~系の命令によるジャンプを受けつけません。on~系の命令を使う際は、
;  asleep 16
;  await 0
; などのようにawait命令を続けて記述してください。
; 
; awati命令で待機中にon~系でジャンプすると待機が解除されてしまいます。
; このためイベントが発生するとfps値が上昇する現象が起こります。
; これに対してasleep命令ではon~系でのジャンプでも待機が解除されることはありません。
; イベント発生によるfps値上昇を防ぐことができます。
;
;-----------------------------------------------------------
#deffunc asleep int waittime
	// 実際に待機する時間を算出
	timeGetTime
	tm = stat
	wt  = waittime - (tm - tm0)
	if wt<0 : wt = 0
	
	// 待機
	Sleep wt
	
	timeGetTime
	tm0 = stat
	return wt

#global


;-----------------------------------------------------------
;
;	ループ前の準備
;
;-----------------------------------------------------------
// ループに入る前の処理を記述

;	キー入力のバッファ領域
dim keyInput, 10	;最大10個
keyInputNum = 0		;入力された数、キューの数

;	キー入力受付
onkey gosub *label_key


;-----------------------------------------------------------
;
;	メインループ
;
;-----------------------------------------------------------
*main
	redraw 1
	asleep 300	//	動作確認しやすくするため大きめの待機時間にしています。
	await 0 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	;---------------------
	;	入力されたキーを取り出す(デキュー)
	;---------------------
	;ここではFIFOを使う例を示す。
	mes "キューの数:" + keyInputNum
	repeat keyInputNum
		mes strf("[%c]", keyInput(cnt))
	loop
	;キューをリセット
	keyInputNum = 0



	goto *main


;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------

;-----------------------------
;	イベント:キー
;-----------------------------
;キー入力時に動作
;必ずawait行からこのルーチンへジャンプしてきます。
;
*label_key
	if (iparam!0) {
		;	キー入力結果をエンキュー
		;keyInputに入力されたキーを保存する。
		if keyInputNum<10 {
			keyInput(keyInputNum) = iparam
			keyInputNum++
		}
	}

	return

解説

 このパターンは、ゲームのループ中にイベント駆動型の方法で入力を取得する方法です。 通常はループのタイミング調整にawait命令を使うため、イベントを取得して戻るとawaitの待機を無視してしまい 意図していた一定のフレームレートを維持できなくなってしまいます。 そこで、ここでは待機命令を自作してこの問題を回避しています。

 1回のループ中に複数回の入力が行われた場合、処理する必要があるキューとしてバッファに格納します。 格納されたキューは入れた順に取り出して処理するようにしています。これをFIFO(First In, First Out)と言います。

ステートマシン

 ツールやゲームなどでは、さまざまな状態があります。 特定の編集モードがONの状態や、キャラクターのアクションの状態、ゲーム中とゲームOP中など。 これら様々な状態の推移を管理する必要があります。

 まだあまり使い込んでいないので洗練されていませんが、一例としてパターンを記載します。先ずはツールの場合から。


;-----------------------------------------------------------
;
;	初期化
;
;-----------------------------------------------------------
dim state
state = 0


;-----------------------------------------------------------
;
;	ウィンドウ
;
;-----------------------------------------------------------

;	状態変化
button gosub "0に推移", *label_0
button gosub "1に推移", *label_1
button gosub "1に推移(1)", *label_1_1
button gosub "2に推移", *label_2

stop

;-----------------------------------------------------------
;
;	メインルーチン
;
;-----------------------------------------------------------
*main
	
	;---------------------
	;	状態に応じた処理分岐
	;---------------------
	switch state
	case 0
		;---------------------
		;	状態 0
		;---------------------
		mes "状態 0"
		swbreak
		
	case 1
		;---------------------
		;	状態 1
		;---------------------
		mes "状態 1"
		swbreak
		
	case 2
		;---------------------
		;	状態 2
		;---------------------
		mes "状態 2"
	
		;	次の状態
		state = 3
		swbreak
	
	case 3
		;---------------------
		;	状態 3
		;---------------------
		mes "状態 3"
		swbreak
		
	default
		;---------------------
		;	状態 デフォルト
		;---------------------
		mes "状態 その他"
		swbreak
	swend

	return
// メインルーチンここまで



;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------
*label_0
	;	状態変更
	state = 0
	gosub *main
	return

*label_1
*label_1_1
	;	状態変更
	if state=0 {
		;	状態1は状態0からしか推移しないという条件をもたせた例
		state = 1
	}
	gosub *main
	return

*label_2
	;	状態変更
	state = 2
	gosub *main
	return

解説

 状態変化のきっかけがあったら状態を変更してメインルーチンを1回実行します。 必ず同じメインルーチンを中心に動作するように記述しています。

 状態の変化は術の状態を自由に行き来できてしまうと都合がわるい場合があります。例として条件をつけたものも書いてみました。 「状態 2」は1回実行後、「状態 3」に自動的に推移します。 「状態 1」は「状態 0」からしか推移しません。

 これはあまり洗練されたスクリプトではないので、参考にならないかもしれませんね。

ステートマシン ゲーム

 ツールやゲームなどでは、さまざまな状態があります。 特定の編集モードがONの状態や、キャラクターのアクションの状態、ゲーム中とゲームOP中など。 これら様々な状態の推移を管理する必要があります。

 まだあまり使い込んでいないので洗練されていませんが、一例としてパターンを記載します。ゲームでのステートマシン実装例です。


;-----------------------------------------------------------
;
;	初期化
;
;-----------------------------------------------------------
dim state
state = 0

;-----------------------------------------------------------
;
;	ウィンドウ
;
;-----------------------------------------------------------
;	状態変化
button gosub "0に推移", *label_0
button gosub "1に推移", *label_1
button gosub "1に推移(1)", *label_1_1
button gosub "2に推移", *label_2


;-----------------------------------------------------------
;
;	メインループ
;
;-----------------------------------------------------------
*main
	redraw 1 : await 500 : redraw 0 : color 255, 255, 255 : boxf : color : pos 0,0

	;---------------------
	;	状態に応じた処理分岐
	;---------------------
	pos 200,0
	switch state
	case 0
		;---------------------
		;	状態 0
		;---------------------
		mes "状態 0"
		swbreak
		
	case 1
		;---------------------
		;	状態 1
		;---------------------
		mes "状態 1"
		swbreak
		
	case 2
		;---------------------
		;	状態 2
		;---------------------
		mes "状態 2"
	
		;	次の状態
		state = 3
		swbreak
	
	case 3
		;---------------------
		;	状態 3
		;---------------------
		mes "状態 3"
		swbreak
		
	default
		;---------------------
		;	状態 デフォルト
		;---------------------
		mes "状態 その他"
		swbreak
	swend

	goto *main





;-----------------------------------------------------------
;
;	サブルーチン
;
;-----------------------------------------------------------
*label_0
	;	状態変更
	state = 0
	return

*label_1
*label_1_1
	;	状態変更
	if state=0 {
		;	状態1は状態0からしか推移しないという条件をもたせた例
		state = 1
	}
	return

*label_2
	;	状態変更
	state = 2
	return

解説

 状態変化のきっかけがあったら状態を変更してメインルーチンに戻ります。 必ず1個のメインループを中心に動作するようにしています。 各状態での動作はサブルーチン化するとスクリプトが見やすくなると思います。

 これはあまり洗練されたスクリプトではないので、参考にならないかもしれませんね。 もっとシンプルに管理、記述したいのですが…何とかならないかな。

ライセンス

いずれもNYSLです。
好きに使って下さい。

関連記事

  1. ファイルを分けて開発する 動機 HSPで作る時って普通は1個のファイルでガリガリ書いて...
  2. oncmd命令に関するお話 概要 HSPにはWindowメッセージを取得して割り込み実行...
  3. スキップしない待機 概要 on~系の命令はstop, wait, await命令...
  4. font命令の速度 概要 HSPのFONT命令はその昔「重いのでメインループ内で...
  5. 再帰とローカル変数 調査環境   HSP3での再帰の実装について真面目に調べてみ...
  6. HSP3でのビットシフト 算術シフトって初めて聞きました  HSP3のビットシフトにつ...
  7. テキストデータの読み込み もくじ 新情報を加えたら量が増えてきたので目次を作りました。...