2015年01月08日

プチコン3号 12日目 再び迷路生成プログラムについて

pc12-1.jpg

前回作成した「迷路探検ゲーム」のプログラムは、迷路生成には5日目に作成したプログラムを使っています。
5日目のプログラムで使用したアルゴリズムは、「バイナリーツリー法」と言います。
バイナリーツリー(二分木)とは、枝が二又に分かれる木のようなデータ構造です。
バイナリーツリー法で生成した迷路は、その経路がスタート地点を根とし、右または下方向だけに伸びていくバイナリーツリーになります。
下図を見れば、実際そうなっていることが解るでしょう。
pc12-2.jpg

さて、バイナリーツリー法の迷路は、欠点があります。
その話の前に、「良い迷路」とはどういうものかを考えてみましょう。
一言で言えば、「一見して正解が判りづらいもの」ということになると思います。

下図を見てください。
迷路のすべての通路に紐を置き、それらを各部屋の中央で結んでツリー構造を作ります。
そして、スタート地点とゴール地点をつかんで引っ張り、まっすぐに伸ばしたと考えます。
すると、図の左の迷路は図の右のように表せます。
pc12-3.jpg

良い迷路とは、このように正解を中央の一本道で表現したとき、
・正解の道が短すぎず、
・正解の道からの脇道が少なすぎず、
・脇道が正解に比べて短すぎない
というような迷路だと考えることができます。

例えば図(A)のような迷路は、脇道は多いが短すぎるため、すぐ袋小路に行き当たってしまいます。
また図(B)のような迷路は、脇道は複雑ですが肝心の正解が短いため、すぐゴールへ到達してしまう可能性があります。

ではバイナリーツリー法で作られた迷路はどうでしょうか?

このアルゴリズムでは、正解の道の長さは「迷路の幅+迷路の高さ」が最大です。
なぜならUターン、つまり連続して2回右へ曲がったり、連続して2回左へ曲がることができないからです。
これは「北か西、どちらかの壁を残す」というルールから来ています。
Uターンするような経路を作ろうとしたら、このルールを満たさない部屋が必要になります。

正解の道の長さ、脇道の長さ、共にあまり長くすることができないのはバイナリーツリー法の欠点です。
また、上で述べたとおりUターンするような経路が作れないことも問題です。


というわけで、今回は5日目の記事よりも良い迷路を生成するアルゴリズム「リカーシブ・バックトラッカー法」による迷路生成プログラムを作ってみました。

5日目の記事でも紹介しましたが、以下のリンクで様々な迷路生成アルゴリズムが解説されています。

"Algorithm" is Not a Four-Letter Word

「リカーシブ・バックトラッカー法」は、その中でも比較的、簡単に良い迷路を生成するアルゴリズムです。
これは、以下のようなルールで迷路を作ります。

(1)現在地の部屋は、元来た部屋へ向かう以外の方向はすべて壁があると考える。
(2)現在地のすぐ隣の部屋で、まだ訪れていない部屋を(ランダムに)選び、壁を壊してその部屋へ進む。
(3)もし周囲が全て訪れたことのある部屋ならば、来た道をひとつ引き返す。
(4)再び(1)から始める。

これを繰り返すと、最終的に全ての部屋を回り終えた後、出発点の部屋にまで戻ってきます。
それ以上は引き返せませんので、これで迷路が完成です。
ちなみに「リカーシブ」とは再帰的、すなわちある作業をより簡単な作業に分解すると、その中に再び同じ作業が含まれているような性質を指します。
また「バックトラック」とは、来た道をなぞって戻っていくようなイメージです。

今回は、この「リカーシブ・バックトラッカー法」による迷路生成プログラムを作ってみました。

基本的なデータ構造は5日目と同じです。
すなわち、部屋が縦横に規則正しく並んでいて、各部屋は「西」と「北」に壁があるものと考えます。
また、迷路の周囲を「入れない部屋」が取り囲んでいて、壁を壊してその部屋へ進むことはできないものとします。

上記を二次元配列を使って表現する点も同じです。
ただし、今回は「西の壁と北の壁の両方が無い部屋」もありえますので、部屋の壁の状態と、配列の数値との対応は

・北、西に壁:3
・北に壁:2
・西に壁:1
・壁無し:0
・未訪問:−1
・入れない:4

というように変更します。
迷路の初期状態は、すべての部屋が「−1」で、その周囲を「4」の部屋が取り囲んでいます。
pc12-4.jpg

「リカーシブ・バックトラック法」では、進めなくなったらどんどん後戻りしますので、元来た部屋をすべて記録しておく必要があります。
そのために「スタック」を使います。

スタック(Stack)とは積み重ねたものという意味ですが、その名の通り、データを追加するときは古いデータの上に新しいデータを積み重ねます。
そして、データを取り出すときは、新しいデータから取り出していきます。
積み重ねる操作を「Push」、取り出す操作を「Pop」と言います。
pc12-5.jpg

未訪問の部屋へ移動するときは、今いる部屋の座標をスタックへ「Push」します。
そして、後戻りするときは、戻るべき部屋の座標をスタックから「Pop」します。
SmileBASICでは、配列をスタックのように扱う命令「PUSH」「POP」が用意されていますので、今回はこれを使います。
なおSmileBASICの「PUSH」「POP」命令は、DIMで宣言した配列のサイズを超えた場合には配列のサイズを自動で大きくします。

(なお「リカーシブ」なプログラムの書き方としては、関数定義の中でその関数自身を呼び出すやり方が有名です。SmileBASICも、DEF文を使ってそのような関数定義が書けます。
ただ、残念なことに現在のバージョンのSmileBASICは再帰呼び出しできる回数に制限があり、それを超えるとエラーになってしまいます。具体的には、千数百回程度が限界のようです。
迷路の経路の最大の長さをこの回数が以下にしないとエラーになってしまいます。エラーにならない迷路のサイズは、目安としては50×50程度までです。
40×40の迷路でも1600部屋ありますから、脇道無しならエラーになる可能性があります。
そのため、今回は再帰呼び出しではなく、ループとスタックを使ってプログラムを実現しています。)

それではプログラムリストです。
pc12-6.png

M_W=199:M_H=119
DIM R[M_W+2,M_H+2]
FOR J=0 TO M_H+1
FOR I=0 TO M_W+1
IF I==0 || I==M_W+1 || J==0 || J==M_H+1 THEN
R[I,J]=4
ELSE
R[I,J]=-1
ENDIF
NEXT
NEXT
DIM XSTK[0],YSTK[0],DIR[4]
PUSH XSTK,0:PUSH YSTK,1
X=1:Y=1:R[X,Y]=3
ACLS:GFILL 1,1,399,239:GPSET 399,238,0
REPEAT
GLINE X*2,Y*2,XSTK[LEN(XSTK)-1]*2,YSTK[LEN(YSTK)-1]*2,0
N=0
IF R[X+1,Y] < 0 THEN DIR[N]=0:INC N
IF R[X,Y+1] < 0 THEN DIR[N]=1:INC N
IF R[X-1,Y] < 0 THEN DIR[N]=2:INC N
IF R[X,Y-1] < 0 THEN DIR[N]=3:INC N

IF N > 0 THEN
PUSH XSTK,X:PUSH YSTK,Y
ON DIR[RND(N)] GOTO @E,@S,@W,@N
@E:X=X+1:R[X,Y]=1:GOTO @DONE
@S:Y=Y+1:R[X,Y]=2:GOTO @DONE
@W:R[X,Y]=R[X,Y] AND 1
X=X-1:R[X,Y]=3:GOTO @DONE
@N:R[X,Y]=R[X,Y] AND 2
Y=Y-1:R[X,Y]=3:GOTO @DONE
@DONE
ELSE
X=POP(XSTK):Y=POP(YSTK)
ENDIF
UNTIL X==0
REPEAT UNTIL BUTTON(0):GCLS

1行目で、迷路のサイズを決めています。
今回は迷路の描画にはグラフィックスを使い、400ドット×240ドットの画面の2×2ドットで迷路の1部屋を表現することにしました。
それに合わせて、画面全体を迷路にする199×119という巨大迷路にしています。

最初の配列の初期化処理は、5日目のときとほぼ同じです。
ただし、先に説明したように、初期値は壁が「4」それ以外が「−1」となっています。

12行目で、座標を格納するスタックのための配列XSTK、YSTKと、進むことができる方向を記録するための配列DIRを宣言しています。
そして、スタックに座標(0,1)をプッシュしていますが、これは座標ではなくて「スタックが空である」ことの目印のために使います。
迷路の中の座標でX=0となるのは迷路の周囲の壁だけであり、迷路の中の座標でXがゼロになることはないからです。

スタート地点は(1,1)とし、その部屋を訪問済みにしています。壁は西・北とも「あり」の状態です。

そして、REPEAT〜UNTILのループで迷路の全ての部屋を訪問します。
ループから抜けられるのはX==0のときですが、この条件が満たされるのは13行目でスタックにプッシュした0がポップされたときだけです。

まず、現在の部屋の上下左右の部屋が訪問済みかどうかチェックし、未訪問ならその方向を配列「DIR」に追加します。
方向は、東・南・西・北をそれぞれ0,1,2,3で表します。

Nには、DIRに格納されている方向の個数が入ります。
(実はDIRもスタックにしても良かったのですが、スタックを簡単に空にする命令が見当たらなかったので止めました。)

そして、未訪問の部屋が1つ以上ある場合は、まず現在地をスタックにプッシュします。
次いで
DIR[RND(N)]
で、未訪問の方向を1つ、ランダムに選び、その方向に進みます。

進んだ先の部屋では、進行方向と整合するように北・西の壁の設定をします。
たとえば東の部屋へ進んだら、進んだ先の部屋に西の壁があるのはおかしいので、「北の壁のみあり」の状態にします。

西に進む処理で
R[X,Y]=R[X,Y] AND 1
という処理をしていますが、これは「現在いる部屋の西の壁を取り払う(北の壁は、あっても無くても現状維持)」ことを表します。
同様に北に進む処理では、現在いる部屋の北の壁を取り払います。

未訪問の部屋がひとつも無かった(N==0)場合は、スタックからX,Yをひとつ取り出し、そこへ移動します。
(つまり、来た道を1つ引き返す。)

スクリーンいっぱいの迷路ですが、けっこうあっという間に作成が終わります。
やはり、BASICとはいえ昔のマシンとは違いますね。
そもそも昔の8ビット機だったら、200×100の配列を使うプログラムなんて、ちょっと使うのがドキドキするようなメモリ食いのプログラムだったと思います。
posted by boochow at 01:42| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年01月06日

プチコン3号 11日目 当たり判定を追加して迷路探検ゲーム完成!

pc11-1.jpg

前回は迷路の中に敵を登場させて動かしました。
あとは、敵と自分の当たり判定を行い、さらに自分がゴールに到達したことを判定すれば、ゲームの完成です。

当たり判定は、プチコン3号のSmileBASICにスプライト同士の衝突判定をする命令が用意されていますので、これを使います。
ゴール判定のほうも、今回はゴールにスプライトで宝箱を置いて、そのスプライトとの衝突で判定することにしました。

今回、衝突(Collision)判定に使う命令は
SPCOL
SPHITSP

の二つです。

SPCOLは、スプライトの16×16ピクセルの領域のうち、当たりと判定すべき領域を矩形で指定します。
この指定をしなければ衝突判定は行われません。

SPHITSPは、2つのスプライトが重なっていればTRUEを返し、そうでなければFALSEを返します。
SmileBASICには、このほかスプライトが指定した矩形(Rectangle)領域に重なっているかどうかを判定するSPHITRC命令や、重なり状況(重なっていた時間、座標など)をより詳しく取得するSPHITINFO命令も用意されています(今回は使いません)。

当たり判定領域は、スプライトに実際に割り当てる絵に合わせて決めます。
今回は、敵およびプレイヤーの当たり判定領域としてキャラクタの中心部12×12ピクセルを指定しました。
また、ゴールである宝箱は16×16ピクセル全体を指定しました。
領域を狭くすれば当たり判定が甘く、広くすれば厳しくなります。
pc11-2.png


プレイヤーと敵が衝突した場合の処理ですが、とりあえず迷路内のランダムな座標へ飛ばしてしまうことにします。
プレイヤーがゴールの宝箱にタッチしたらゲーム終了です。

他に、BGMや効果音、アニメーションなども追加しましょう。
これらは8日目9日目で簡単に触れました。

以上の処理を前回のプログラムに追加します。
前回との差分を以下に掲載します。(82行目から)
pc11-3.jpg

FOR I=0 TO E_MAX
EX[I]=12+32*RND(M_W-1)
EY[I]=12+32*RND(M_H-1)
SPSET 1+I,1022

SPCOL 1+I,2,2,12,12,1
NEXT
SPCOL 0,2,2,12,12,1
SPANIM 0,"I",10,500,10,501,10,502,10,503,0

SPSET 100,269 'GOAL
SPCOL 100,0,0,16,16

BGMPLAY 14
@LOOP
SPOFS 100,M_W*32-X+172,M_H*32-Y+88
IF SPHITSP(0,100) THEN

BGMSTOP
BEEP 71:WAIT 80
ACLS
END
ENDIF
FOR I=0 TO E_MAX
IF SPHITSP(0,1+I) THEN
BEEP 4:WAIT 50
X=12+32*RND(M_W-1)
Y=12+32*RND(M_H-1)
ENDIF
IF ((EX[I]-12) MOD 32)==0 && ((EY[I]-12) MOD 32) ==0 THEN
REPEAT
D=RND(4)

82行目で、敵の座標の初期設定のあとに衝突判定領域の設定を追加しました。

同様に、プレイヤーのスプライトも衝突判定領域を設定します(84行目)。
さらにプレイヤーのみアニメーションも追加しました(85行目)。
9日目では移動方向に合わせて絵を変えていましたが、今回は手抜きして、移動方向に合わせた絵の変更は行わず、常に正面を向いた状態になっています。

86行目で、100番のスプライトを宝箱の絵に設定しています。
これがゴールになります。
その次の行で衝突判定領域を設定しています。
そしてBGMも追加しました。

そのあとは、ゲームのループの中に入ります。

まず、宝箱を表示します。
宝箱の位置は迷路の右下隅に固定しています。
ですが、このゲームは画面の中ではプレイヤーのキャラクタが固定されていて、地形のほうが動きますので、宝箱の表示位置を常時以下のように指定する必要があります。
SPOFS 100,M_W*32-X+172,M_H*32-Y+88

この座標の計算が分かりにくければ、前回の説明を参照してください。

次に、宝箱との衝突を
IF SPHITSP(0,100) THEN

で判定しています。
衝突していればゴールしたと見なし、BGMを止めて効果音を出し、ENDでプログラムを終了しています。
こういう風にプログラムをループのど真ん中でいきなり終わらせるのは行儀が悪いのですが、小さなプログラムですので大目に見ましょう。

敵との当たり判定は、同様にFORループの内側で
SPHITSP(0,1+I)
で敵1匹ずつ当たり判定をしています。
もし敵と衝突していれば、効果音を出してプレイヤーのキャラクタをランダムな座標に飛ばしています。


以上で、小さな「迷路探検」ゲームが完成しました。
今回はプログラムをMiiverseへアップロードしましたので、プログラムリストは載せません。
公開キーは「J23XVK3V」です。

このゲーム、実際に遊んでみると「迷路のできが今ひとつだ」と感じることと思います。
次回は、もう少し迷路らしい迷路の生成をやってみます。
posted by boochow at 01:17| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2015年01月04日

プチコン3号 10日目 迷路の中に敵を登場させてみる

pc10-1.jpg

今日は7日目の続きです。
迷路の中を歩くことができるようになったので、こんどは同時に敵も迷路の中に表示して動かしてみます。

敵の進行方向を決めるアルゴリズムはいろいろ考えられますが、今回はランダムに動かすことにします。
もちろん、壁を通り抜けて移動することはできません。
まずランダムに方向を決め、そちらへ移動できるかどうかを調べ、移動できなければ再度ランダムに方向を決めます。
効率は良くないですが、最悪でも来た方向へ戻ることはできるので、無限ループにはなりません。

さて、ランダムといっても、1ピクセルごとにランダムに動いたのでは足踏みに近い状態になってしまいます。
そこで迷路の1部屋ごとに、次にどの部屋へ向かうかをランダムに決めることにします。
具体的には、敵の座標が部屋の中央になったときだけ、方向転換ができることにします。

ここで「部屋の中央」の定義ですが、下の図を見てください。
pc10-2.jpg


今制作しているプログラムでは、迷路の1部屋は32×32ピクセルで、敵を表すスプライトは16×16ピクセルです。
また、部屋の上端8ピクセルと左端8ピクセルは、壁がある可能性がありますので空けておかなければなりません。
残り24×24ピクセルの中央に敵を現すスプライトを置くと、ちょうど通路の中央にあるように見えます。
つまり、部屋の左上隅から12ピクセルだけ右下に下がった座標が、部屋の中央ということになります。

ではプログラムの主要部分です。(全体は最後に掲載します。)
pc10-3.jpg

DIM DIR_X[4],DIR_Y[4]
@DIR:DATA 1,0,0,1,-1,0,0,-1 'E,S,W,N
RESTORE @DIR
FOR I=0 TO 3:READ DIR_X[I],DIR_Y[I]:NEXT

E_MAX=8
DIM EX[E_MAX+1],EY[E_MAX+1]
DIM EDX[E_MAX+1],EDY[E_MAX+1]
FOR I=0 TO E_MAX
EX[I]=12+32*RND(M_W-1)
EY[I]=12+32*RND(M_H-1)
SPSET 1+I,1022
NEXT

@LOOP
FOR I=0 TO E_MAX
IF ((EX[I]-12) MOD 32)==0 && ((EY[I]-12) MOD 32) ==0 THEN
REPEAT
D=RND(4)
TMP_X=EX[I]+DIR_X[D]*9
TMP_Y=EY[I]+DIR_Y[D]*9
UNTIL COUNT_WALL(MAP,TMP_X>>3,TMP_Y>>3)==0
EDX[I]=DIR_X[D]:EDY[I]=DIR_Y[D]
ENDIF
EX[I]=EX[I]+EDX[ I]:EY[I]=EY[I]+EDY[I]
SPOFS 1+I,EX[I]-X+192,EY[I]-Y+112
NEXT

まず、4つの方向(Direction)へ移動するときに、XとYをそれぞれどう変化させるかを表す配列DIR_XとDIR_Yを作っています。
4つの方向はそれぞれ右(1,0)下(0,1)左(-1,0)上(0,-1)となります。
今回は、この値をDATA文で書いておいてREADでDIR_XとDIR_Yに読み込んでいます。

次に、敵の座標と敵の移動のための加算値を配列で用意します。
EX・EYが敵の座標、EDX・EDYが加算値です。
敵の数はいくつでも良いのですが、ここでは9匹にしました。
E_MAX=8
となっていますが、配列の添え字は0から始まるので、それも含めて9になるように、それより1小さい値にしています。

FOR〜NEXTループで、EX・EYをランダムに設定しています。
ただし、座標値は先ほど述べたように部屋の中央に来るようにしています。
同時にSPSETでスプライトの絵を指定しています。
「1022」は幽霊のキャラクタ番号です。

@LOOP以降がメインループです。
FOR〜NEXTループで敵の座標を順に処理します。
IF文の条件式
((EX[I]-12) MOD 32)==0 && ((EY[I]-12) MOD 32) ==0

で、座標が部屋の中央かどうかを判定しています。
中央なら、DIR_X[0]・DIR_Y[0]〜DIR_X[3]・DIR_Y[3]のうちのいずれかの組をランダムに選びます。

そして、移動した場合の新しい座標(TMP_X,TMP_Y)を計算します。このとき
TMP_X=EX[I]+DIR_X[D]*9
のように「*9」がついていますが、これはマップの配列の1マスが8×8ピクセルに相当するので、(TMP_X,TMP_Y)がマップ上で隣のマス目になるようにするには、4より大きく12より小さいピクセル数分、移動させる必要があるからです。

最後にUNTIL文の条件式で、(TMP_X,TMP_Y)の方向に進むことができるかどうかを判断しています。
進むことができる場合はループを抜けますので、進行方向を表すDIR_X,DIR_Yの値をEDX,EDYに設定します。

IF文を抜けた後、EXとEYにそれぞれEDX,EDYを加算して敵の座標を更新します。
そして、更新した敵座標に応じて、敵のスプライトの座標も更新します。
スプライトの座標はBGに描かれた迷路の画像と合っていなければなりません。
スプライトの座標はスクリーン左上を原点とした座標で指定する必要があり、プログラムでは
EX[I]-X+192,EY[I]-Y+112

となっています。
これらの座標の関係は下図のようになっています。
pc10-4.jpg

太い赤い矢印で示しているのが求めたい座標です。
この座標は、(EX-X,EY-Y)と(192,112)の合成で表せることが分かります。

以上で、とりあえず敵をランダムにウロウロさせることができるようになりました。
最後にプログラム全体を掲載しておきますが、今回説明した以外に、一点変更があります。

COUNT_WALLSの中で使っている変数I,J,Nを「_I,_J,_N」にしました。
これは、メインルーチンと関数定義で同じ名前の変数を使っている場合に、関数定義の中の変数がメインルーチンの中の変数(グローバル変数)として扱われるという問題(プチコン3号のバグ?)があったためです。

プログラム自体も、だいぶ長くなってきましたので、少し構造を整理したほうが良さそうですね。
M_W=15:M_H=10
DIM R[M_W+2,M_H+2]
FOR J=0 TO M_H+1
FOR I=0 TO M_W+1
IF I==0 || I==M_W+1 || J==0 || J==M_H+1 THEN
R[I,J]=0
ELSE
R[I,J]=3
ENDIF
NEXT
NEXT
FOR J=1 TO M_H
FOR I=1 TO M_W
R_N=R[I,J-1]
R_W=R[I-1,J]
IF R_N==0 && R_W>0 THEN R[I,J]=1
IF R_N>0 && R_W==0 THEN R[I,J]=2
IF R_N>0 && R_W>0 THEN R[I,J]=1+RND(2)
NEXT
NEXT
ACLS
BGSCREEN 0,M_W*2+1,M_H*2+1
W1=292:W2=12580:W3=291:WE=257
FOR J=1 TO M_H
FOR I=1 TO M_W
IF R[I,J]==3 THEN C1=W3:C2=W1:C3=W2
IF R[I,J]==2 THEN C1=W2:C2=WE:C3=W2
IF R[I,J]==1 THEN C1=W1:C2=W1:C3=WE
BGPUT 0,I*2-2,J*2-2,C1
BGPUT 0,I*2-1,J*2-2,C2
BGPUT 0,I*2-2,J*2-1,C3
BGPUT 0,I*2-1,J*2-1,WE
NEXT
BGPUT 0,M_W*2,(J-1)*2,W2
BGPUT 0,M_W*2,J*2-1,W2
NEXT
BGFILL 0,0,M_H*2,M_W*2-1,M_H*2,292
BGPUT 0,M_W*2,M_H*2,256

DIM MAP[M_W*4+1,M_H*4+1]
FOR J=1 TO M_H
FOR I=1 TO M_W
X=I*4-4
Y=J*4-4
MAP[X,Y ]=1
IF (R[I,J] AND 1) > 0 THEN
MAP[X+1,Y]=1
MAP[X+2,Y]=1
MAP[X+3,Y]=1
ENDIF
IF (R[I,J] AND 2) > 0 THEN
MAP[X,Y+1]=1
MAP[X,Y+2]=1
MAP[X,Y+3]=1
ENDIF
NEXT
X=M_W*4
Y=J*4-4
MAP[X,Y]=1
MAP[X,Y+1]=1
MAP[X,Y+2]=1
MAP[X,Y+3]=1
NEXT
FOR I=0 TO M_W*4
MAP[I,M_H*4]=1
NEXT

SPSET 0,502
SPOFS 0,192,112,0
X=8:Y=8
DIM DIR_X[4],DIR_Y[4]
@DIR:DATA 1,0,0,1,-1,0,0,-1 'E,S,W,N
RESTORE @DIR
FOR I=0 TO 3:READ DIR_X[I],DIR_Y[I]:NEXT
E_MAX=8
DIM EX[E_MAX+1],EY[E_MAX+1]
DIM EDX[E_MAX+1],EDY[E_MAX+1]
FOR I=0 TO E_MAX
EX[I]=12+32*RND(M_W-1)
EY[I]=12+32*RND(M_H-1)
SPSET 1+I,1022
NEXT

@LOOP
FOR I=0 TO E_MAX
IF ((EX[I]-12) MOD 32)==0 && ((EY[I]-12) MOD 32) ==0 THEN
REPEAT
D=RND(4)
TMP_X=EX[I]+DIR_X[D]*9
TMP_Y=EY[I]+DIR_Y[D]*9
UNTIL COUNT_WALL(MAP,TMP_X>>3,TMP_Y>>3)==0
EDX[I]=DIR_X[D]:EDY[I]=DIR_Y[D]
ENDIF
EX[I]=EX[I]+EDX[ I]:EY[I]=EY[I]+EDY[I]
SPOFS 1+I,EX[I]-X+192,EY[I]-Y+112
NEXT
STICK OUT DX,DY
X_NEW=X+DX*3:Y_NEW=Y-DY*3
IF COUNT_WALL(MAP,X_NEW>>3,Y>>3)==0 THEN
X=X_NEW
ELSE
X=MAX(MIN(X_NEW,X OR 7),X-(X AND 7))
ENDIF
IF COUNT_WALL(MAP,X>>3,Y_NEW>>3)==0 THEN
Y=Y_NEW
ELSE
Y=MAX(MIN(Y_NEW,Y OR 7),Y-(Y AND 7))
ENDIF
BGOFS 0,X-192,Y-112,0
VSYNC 1
GOTO @LOOP

DEF COUNT_WALL(MAP,PX,PY)
_N=0
FOR _J=0 TO 2
FOR _I=0 TO 2
_N=_N+MAP[PX+_I,PY+_J]
NEXT
NEXT
RETURN _N
END
posted by boochow at 16:19| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2014年12月31日

プチコン3号 9日目 アニメーションとタッチパネルを使う

SmileBASICで特徴的なのが、スプライトのアニメーション機能です。
画像を切り替えてアニメーションするだけでなく、スケーリングや位置の変更も指定できます。

付属サンプルの8番目「技術デモ」を見ると、いろいろな機能を概観できます。
このサンプルのソースコードは以下のリンクに掲載されており、参考になります。

プチコン3号 - プログラムリスト

アニメーションの指定自体は、BGMと同様に、一度パラメータを設定するだけで、あとは何もしなくても勝手に進行していきます。
pc9-1.jpg

上のようにダイレクトモードで指定しただけで、アニメーションは勝手に動き続けます。
ここではSPANIM命令で、0番のスプライトのキャラ番号を
 2772(15フレーム)→2773(15フレーム)2774(15フレーム)2775(15フレーム)
と切り替えて、1秒で一周するようにしています。
最後のパラメータ「,0」は、このアニメーションを無限ループする指定です。


今回は、このアニメーション機能を使って、
「タッチパネルにタッチすると、その位置へキャラクタが歩いていく」
というプログラムを作ってみました。

ただ、このプログラム・・・今回は、うまく動作しない場合があります。
もしかするとプチコン3号のバグか、あるいはマニュアルに書かれていない仕様があるのかもしれません。
スマイルツール等を起動した後だと、画面に何も表示されないことがあります。
ACLSコマンドを実行しても変化が無いので、こういう場合はプチコン3号をいったん終了させてください。

プログラムは以下です。
pc9-2.jpg

XSCREEN 2,255,2
DISPLAY 1 'DISPLAY 0

GX=184:GY=104:GC=2764
C=GC+4:C_OLD=GC
SPSET 0,C
SPSCALE 0,2,2
@LOOP
SPOFS 0,GX,GY
IF C != C_OLD THEN
SPANIM 0,"I",15,C+1,15,C+2,15,C+3,15,C,0
C_OLD = C
ENDIF
IF MAINCNT MOD 12 ==0 THEN TOUCH OUT T,X,Y
IF T > 0 THEN
DX=SGN(X-GX):DY=SGN(Y-GY)
GX=GX+DX:GY=GY+DY
ON (DY>=0)*(DX+2) GOTO @N,@W,@S,@E
@N:C=GC+12:GOTO @DONE
@W:C=GC+8 :GOTO @DONE
@S:C=GC+4 :GOTO @DONE
@E:C=GC+0 :GOTO @DONE
@DONE
'C=(3-(DY>=0)*(DX+2))*4+GC
SPSTART 255 'SPSTART 0
ELSE
SPSTOP 255 'SPSTOP 0
ENDIF
VSYNC 1
GOTO @LOOP

まずタッチパネル側をディスプレイとして使用するため、XSCREENコマンドとDISPLAYコマンドを使います。
XSCREENのパラメータは、「モード,上画面で使うスプライト数,上画面で使うBG数」です。
スプライトは512枚、BGは4枚を、上画面と下画面で分け合って使います。
このプログラムはスプライトの半分の255枚を上画面に割り当てています。

DISPLAY命令は使用するディスプレイが上なら0、下なら1です。
ここで、コメントで
'DISPLAY 0
と入れましたが、実はここが0か1かで、この後のプログラムの動作が異なってしまいます。
表示されるディスプレイが異なるだけのはずなのですが・・・

3行目、GXとGYはスプライトを表示する座標、GCはアニメーションに使うキャラクタのうち最小のものです。
今回は、
・2764〜2767:右向き歩行
・2768〜2771:下向き歩行
・2772〜2775:左向き歩行
・2776〜2779:上向き歩行
の、4組16種のキャラクターを使います。

4行目、CとC_OLDは現在の向きに応じたキャラクタ番号(の最小のもの)です。
向きが変化したかどうかを判定するため、直前の状態をC_OLDに保持しています。

9行目からのIF文が、その判定処理です。
CがC_OLDと異なる場合、SPANIM命令を実行し、新しいアニメーションを設定します。

13行目からは、タッチパネルの入力処理です。
ここはループの中で毎回実行するのではなく、12回に1回(1秒間に5回)だけ実行しています。
こうすることで、キャラクタの向きが頻繁に変化しすぎて絵がちらつくのを少し防いでいます。

タッチパネルの読み出しは
TOUCH OUT T,X,Y

です。X,Yはもちろん座標です。
Tは、タッチ開始時点で0で、タッチが続いている限り1ずつ増加していきます。
これによって時間軸とX,Yという3次元の情報を取れます。

そのあとの処理は、ややトリッキーです。
やりたいことは
・タッチされたポイント(X,Y)とスプライトの現在位置(GX,GY)の大小関係を見て
・(GX+DX,GY+DY)が(X,Y)に近づくように(DX,DY)を決める(DX,DYはそれぞれ-1,0,1のいずれか)
・(DX,DY)が表す向きに応じて、キャラクタ番号Cを決める
という処理になります。

関数SGN(x)はxの正、負、0に応じて1、-1、0を返します。これを使って
DX=SGN(X-GX):DY=SGN(Y-GY)

として上記の1点目、2点目は解決です。

DX、DYはそれぞれ3状態あるので、組み合わせは9つの状態があります。
しかしキャラクタは今回は4方向しか用意できませんので、9つの状態と4方向の対応付けは下図の左のように決めました。
そして、4つの方向は図右のような番号に対応させています。
pc9-3.jpg

この対応付けを17行目の
(DY>=0)*(DX+2)

で行っています。
「(DY>=0)」という式は、DYが負のとき0、DYが0以上のとき1になります。
この式で、図右のような結果が得られることは計算してみると分かると思います。

ON〜GOTO文で、4つの方向に応じてジャンプしています。
ジャンプ先で、アニメーションに使用するキャラクタ番号をCに代入しています。
ラベルは東西南北(North,West,South,East)を表しています。

なお、今回はON〜GOTO文を使いましたが、23行目のコメントにあるように
C=(3-(DY>=0)*(DX+2))*4+GC

とすれば、このON〜GOTO文と同じ結果が1行で得られます。

このあと、「タッチしているときだけアニメーションさせる」ために、SPSTARTとSPSTOPを呼んでいます。
実はこの部分が、最初のほうで書いた「DISPLAYのパラメータが0か1かで動作が異なる部分」です。

アニメーションするスプライトは、5行目で指定したとおり0番なのですが、SPSTART/SPSTOPでは「255番」のスプライトを指定しています。
DISPLAYが0のときは、SPSTART 0で思ったとおりの動作をします。
DISPLAYが1のときは、どうも「SPSETで指定したスプライト管理番号+XSCREENで指定した上画面のスプライト数」が、SPSTART・SPSTOPでのスプライト管理番号になるようです。
ちょっと納得の行かない仕様ですので、もしかしたらプチコン3号のバグかもしれません。
posted by boochow at 18:33| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2014年12月30日

プチコン3号 8日目 BGMや効果音をつける

ゲームに効果音やBGMが付くと、それだけで意外と印象が変わります。
プチコン3号には、最初からゲーム用の効果音が134種、BGMが43曲ついています。
また、オリジナルの曲をMMLで記述してBGMとして使うこともできます。

まずはダイレクトモードで音を出してみました。
pc8-0.jpg

BEEPの使い方は以下の通りです。
BEEP 音の番号,音の高さ,音量,パンポット

音の番号は0〜133、音の高さは-32768〜32767、音量は0〜127、パンポットは0〜127です。
音の高さはセント(半音の1/100)単位で、負の値にすると元の音より低くなります。-1200で1オクターブ下がります。
パンポットは左端が0、中央が64、右端が127です。

BGMはBGMPLAYコマンドで流します。
パラメータはトラック番号(0〜7)、曲番号(0〜42)、音量(0〜127)です。
異なるトラック番号を指定することで、8つまでのBGMを同時に流すことができます。
あるいは、同じ曲を別のトラックで再生すれば、最大音量よりも大きな音で鳴らすこともできます。

EFCSET命令でエフェクトをかけることができます。
パラメータは0〜3で、大きいほど強くリバーブ(いわゆるエコー)がかかります。

BGMはプログラムの動作とは関係なく流れっぱなしになりますので、止めるにはBGMSTOPコマンドを使います。

全般的にプチコン3号の音関連の命令はよくできていて、音を鳴らすプログラミングに手をかける必要はあまり無さそうです。

今日は、「プチコン3号 3日目 絵をボタン操作で動かす」で作ったプログラムにBGMと効果音をつけてみました。
やったことは
・BGMを流す
・移動時に、移動スピードに合わせて音を出す
・ジャンプ時に音を出す
の3つです。

プログラムはこちらです。
スプライトをスライドパッドで操作する部分については、3日目に説明しましたので省略します。
7行目・8行目のMIN、MAXの使い方も、前回説明しましたのでそちらを見てください。
pc8-1.jpg

X=192:Y=112:Z=500
DZ=0
SPSET 0,310
BGMPLAY 26
@LOOP
STICK OUT DX,DY
X=MIN(MAX(X+DX*3,0),368)
Y=MIN(MAX(Y-DY*3,0),208)
IF WALK_CNT < 1 THEN
V=DX*DX+DY*DY
IF DZ == 0 && V > 0 THEN BEEP 2,-1500
V=FLOOR(V*7)
WALK_CNT = 20 / (1+V)
ELSE
DEC WALK_CNT
ENDIF
IF DZ==0 THEN
DZ=16 AND BUTTON(2)
IF DZ >0 THEN BEEP 8, 1500
ENDIF
Z=Z-DZ
IF Z<-200 THEN DZ=-DZ
IF Z>500 THEN DZ=0:Z=500
SPOFS 0,X,Y,Z
S=1.0+(500-Z)/500
SPSCALE 0,S,S
VSYNC 1
GOTO @LOOP

4行目でBGMを開始しています。
BGM関連の処理はこれだけです。

移動しているときに音を出す処理が9行目からのIF文です。
移動スピードに合わせて音を変える方法は、いろいろなものが考えられます。

たとえば
・移動している速さによって音の高さを変える
・移動している速さによって音の種類を変える
・移動している速さによって音のタイミングを変える
などです。

今回は、早く移動しているときは短い間隔で、ゆっくり移動しているときは長い間隔で音を出してみました。

変数WALK_CNTでBEEP文を実行する間隔を調整しています。
WALK_CNTは0になるとBEEPを実行します。
0でなければ、15行目のDEC文でWALK_CNTを1減らします。
(ちなみにDECは英語のdeclimentに由来します。)

BEEPを実行する前にまず、Vに移動速度に比例する正の数をセットしています。
DX、DYは-1.0〜1.0の値を取りますので、2乗して正の数にします。
そして、「ジャンプ中でなく(DZ==0)」「停止中でなければ(V>0)」BEEPを実行します。

そのあと、次のBEEPの実行までの間隔を、WALK_CNTに設定します。
設定する値は「20/(1+V)」となっています。Vの最小値は0ですので、設定する値の最大値は20です。
ループの一周は1/60秒ですので、もっとも間隔が長いときは毎秒3回BEEPが実行されることになります。

もし、速さに関係なく一定間隔でBEEPを実行するだけなら、このプログラムでWALK_CNTに代入する値を一定値にするか、さらに簡単にはシステム変数MAINCNTを使い、
IF MAINCNT MOD 16 == 0 THEN BEEP 2
とする方法もあります。

ジャンプしたときの効果音は、19行目で出しています。
ジャンプについては処理はこれだけです。
音量やパンポットの設定もしていませんが、たとえばXの値(0〜368)に応じてパンポットを設定しても良いかもしれませんね。
その場合はこのBEEP文は
BEEP 8, 1500, 127, X*127/368
というようになるでしょう。

プチコンのサウンドプログラミングはすごく楽ですね。

最後に注意点ですが、非常に短い間隔でBEEPを繰り返し呼ぶと、プチコン3号そのものが不安定になるようです。
作成中のプログラムが消えてしまう場合もあります。
たとえば以下の1行プログラムは実行するとプチコンが再起動してしまいました。
@1:BEEP 2:GOTO @1

ループの中でBEEPを呼ぶ場合には、ループの中に少なくともVSYNCは入れましょう。
posted by boochow at 10:31| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする
人気記事