今年(2016年)のHSPプログラムコンテストも入賞作品が決まり、ひと段落着いたところでHSPTV部門での個人的なテクニックを書きたいと思います。
実際の内容は、投稿したHSPTVプログラムのソースとその説明です。
なお、本記事はHot Soup Processor Advent Calendar 2016の9日目の記事です。
HSPTV部門とは?
HSPプログラムコンテストのうち、HSPTVブラウザ上で動作するプログラムを投稿する部門です。
投稿には制限事項があり、この制限範囲内に収まるように工夫する必要があるわけです。
- start.axのファイルサイズを6000バイト以下にすること。
- 指定以外の素材(画像、音声など)を用意しない。
- OBAQ.DLL以外の拡張プラグインを使わない。など……
また、hsptv.asを用いてランキング機能を利用することができます(リンク先は2011年の情報です)。
ランキングに含まれるコメント情報を、本来のコメントとしてではなくデータ領域として利用することで、疑似対戦型ゲームやチャットアプリを実現できます。
ソースファイル
今年のHSPプログラムコンテストに投稿したプログラムを貼ります。
「ショートストーリー」(ソース)
start.ax内に無理やり詰め込んだRPG。
会話を半角カナで打ち込んだり(例:カイワ ヲ ハンカクカナ デ ウチコンダリ)、フィールドのデータを自動生成したりしてます。「雑貨屋競争」(ソース)
ランキング機能を利用した経営シミュレーション(?)。
「いいね★」によってお店を評価しています。
その他お店の編集・公開機能など。
ソースファイルを見ながら、start.axの削減術の実例を挙げていきます。
データを自動生成する
膨大な量のデータは、start.ax内に持たずに自動生成することで大幅にサイズを削減できます。
たとえば、アルゴリズムで迷路を自動生成したり……
しかし、自動生成するプログラムそのもののサイズを考える必要もあります。
ショートストーリーのマップは、データとして持たずに自動生成しています。
地上フィールドは画像素材「bg04.jpg」を用いて生成しています。
/*
0…黒(壁)初期化時
1…
2…
3…
4…
5…
ど6…海
う7…海浜,洞窟の地面
屈8…石,洞窟の壁
9…草原
A…道
B…林
*/
//(中略)
celload dir_exe+"\\hsptv\\bg04.jpg",3 //※dir_tvマクロを用いると、パス指定を短くできる
//(中略)
gsel 3
repeat 96,8
cnt2 = cnt
repeat 96,8
pget cnt*3+20,cnt2*3+125
map(cnt,cnt2) = 0x0b - ( (ginfo_r+ginfo_g+ginfo_b)/19)\6
loop
loop
数値型配列mapに、フィールドマップが格納されます。
生成アルゴリズムは試行錯誤で見つけたものです。
フィールドに合わせて、「真ん中の島に魔王がいる」というストーリーにしました。
生成元画像と並べてみると、それなりに雰囲気が似ています。
地下ダンジョンは乱数を用いて生成しています。
迷路生成アルゴリズムを使うと面白そうでしたが、ショートストーリーではかなり適当に、サイズが小さくなるプログラムを組んでいます(ちゃんとした迷路になってない)。
地下ダンジョンに町を作ったことを考えると、これでよかったのかなと思っています。
「住人は地下に逃げている」という後付けストーリーのもと、地下ダンジョンと町を併用しています。
複数行の文字列は1つのmesで
複数行の文字列を表示するとき、
pos 480,0
mes "雑貨屋競争"
mes ""
mes "人気ランキング"
と1行ごとに定義するのではなく、改行文字'\n'を使って、
pos 480,0
mes "雑貨屋競争\n\n人気ランキング"
と1命令、1文字列にまとめます。
また、なるべくpos命令で位置調整せず、スペースや改行によって合わせます。
この例だと、”人気ランキング”の位置合わせに改行を使ってます。
ただし、スペースも改行も1つ当たり1バイト喰うので、カレントポジションからあまりにも離れた位置にはposを使っています。
ショートストーリーのモンスターはAAで表現しています。
スクリプト上では分かりにくい記述ですが、しょうがないですね。
monster_char = " ____\n≦ ・/|/| ̄≡\n  ̄ ̄ └ ̄└ ̄"/*バッタ、ドクカマキリ*/
文字列の配列を1つにまとめる
これは、すべての文字列配列に適用できるわけではありませんが、それぞれの文字列長さがだいたい等しいときに有用だと思います。
例えば、ショートストーリーのアイテム名は、
object_name = "--------","話ス調ベル","宿屋 ","疲レドリンク","カギ ","リピート薬 ","モンスの印 "
と配列で定義せず、
object_name = "--------話ス調ベル宿屋 疲レドリンクカギ リピート薬 モンスの印 "
と固定長の文字列として1つの変数にまとめています。
配列定義で、データを区切る,(カンマ)は、結構容量を喰うようです。
しかし、文字列を固定長にすると、どうしても長いアイテム名に合わせる必要があるため、無駄なスペースによるパディングが生じます。
また、それぞれの文字列を抽出するには、strmidを使って
strmid(object_name,i*8,8)
とする必要があり、単純な配列参照よりサイズを喰います。
この方法が使える文字列データは、限られてきますね。
数値データを文字列に落とし込む
これは、個人的におすすめな方法です。
6ビット(0-63)の範囲内に収まる数値型配列データを、文字列データに落とし込みます。
例えば、モンスターの配色を決めるデータを定義・参照するには
monster_color = "-(DyE'D!?"//データ定義
//(中略)
color64 read_str_data(monster_color,monster_no)//データ参照
//(中略)
#deffunc color64 int c
c_sub = c - 0x20
color (c_sub>>4)*85,((c_sub>>2)&3)*85,(c_sub&3)*85
return
#defcfunc read_str_data var read_str_data_array,int read_str_data_ofset//※関数名はそのままstart.axに埋め込まれるので、短いほうがいい
read_str_data_sub = peek(read_str_data_array,read_str_data_ofset)
if read_str_data_sub>='。'{
return read_str_data_sub-'。'+0x41
}else{
return read_str_data_sub-'0'
}
としています。煩雑なソース例になってしまいましたが、次の流れでデータを扱っています。
(1) 数値データに文字コード'0'を加算して、1文字で表現する。
(2) peek命令で文字列から1文字を抽出し、文字コード'0'で減算して復号化する。
'0'から始まる64個(6ビット相当)の文字なかに、スクリプト上で表現できない文字はないようです。よって、
peek(monster_color,monster_no)-'0'
でも参照可能ですが、-'0'がネックになるので専用のread_str_data関数を定義しています。
なお、read_str_data関数は"ほぼ"7ビットのデータに対応しています(あてにはならない)。
というのは、Shift-JISの半角カナの領域を無理やり利用すれば、ほぼ7ビット相分の文字を用意できます。
いくつか文字が足りないので厳密には7ビットではないうえ、HSPTVランキングサーバーに対しては使えません。
サーバーのコメント領域に半角カナは使えないようです。
read_str_data関数は6ビットの範囲内なら安定して使えると思いますが、7ビットのデータは、次のようなマップデータぐらいにしか使っていません。
フィールド上のどこに人がいるか、何を話すか、などの情報を詰め込んでいます。
map_data = "F=タ11000=テ31;:08j328W3=`32?83<_262809T270009M18000A:190009L4:400Ha53894Ha145b0Fe33F?4Y81;300X@33N84Qm262b0dX34DA0ェD1>000ョセ4:600`ョ35;<0hェ1A000gk36FS70000000゚"/*フィールド*/
hsptvディレクトリの素材にアクセスするとき
複数の素材を利用するとき、デフォルトのカレントディレクトリのまま
#define SE_IINE 2
mmload dir_tv+"se_tyuiin.wav",SE_IINE
#define SE_PUT 3
mmload dir_tv+"se_foot.wav",SE_PUT
とするよりも、カレントディレクトリを移動したほうが容量を削減できます。
#define SE_IINE 2
chdir dir_tv
mmload "se_tyuiin.wav",SE_IINE
#define SE_PUT 3
mmload "se_foot.wav",SE_PUT
なお、マクロdir_tvは、hspdef.asによってdirinfo(5)と定義されているようです。
新規命令・関数の名前は短くする
バイナリエディタStirlingによってstart.axを覗くと、#deffunc・#defcfuncによって定義した命令名・関数名はそのまま埋め込まれていることが分かります。
ショートストーリーでは冗長な名前をつけてますが、これを受けて雑貨屋競争では1文字の命令・関数名を定義するようにしました。
#defcfunc c int c_index,int c_ofset //visit,iine専用
//(省略)
#deffunc u int u_index,int u_ofset,int u_option //visit,iine専用
//(省略)
#defcfunc r int r_index,int r_peekofset //comment専用
//(省略)
#deffunc w int w_data //send_comment専用
//(省略)
#deffunc s array s_array //バブルソート専用
//(省略)
cel*命令群で画像素材を描画する
知っている人には当たり前なことですが、同じサイズの画像素材(アイコンなど)はcel*命令群で描画します。
ショートストーリーの開発初期は、celputの存在を知らず、gcopyで描画していました。
もちろん、celputのほうが容量は小さくなりました。
おわりに
start.axのサイズをギリギリまで削減して大規模なゲームを作るのも楽しいですが、ゲーム性・ゲームバランスが重要です。
私の投稿プログラムは、ショートストーリーも雑貨屋競争も不親切なところが多いです。
HSPTVプログラムなりに、小さくまとめることも大切ですね。