プチコン3号 5日目 迷路を作ってみる

pc5-1.jpg

前回、背景グラフィックスを描いてスクロールさせてみましたが、今度はこれを応用して迷路を抜けるゲームを何回かかけて作ってみたいと思います。
そこで、まずは迷路を自動生成するプログラムを作ってみました。

迷路の生成アルゴリズムは、いろいろあります。
今回は、そのなかでももっとも単純なものを使いました。

まず部屋が縦横に規則正しく並んでいて、各部屋は「西」と「北」に壁があるものと考えます。
図では、青が西の壁、赤が北の壁です。

pc5-2.jpg

次に、各部屋について、西か北、どちらかの壁をランダムに壊します。
ただし、隣に部屋が無い壁は壊すことができません。
すると、たとえば下図のようになります。
これで迷路の出来上がりです。

pc5-3.jpg

この方法で作る迷路は欠点があります。
一番上の行は、北の壁が壊せないので常に西の壁が壊されます。
一番左の列は、西の壁が壊せないので常に北の壁が壊されます。
各部屋の北か西の壁を必ず壊さなければならないので、東や南にしか出口のない部屋が作れません。

こういった欠点のない様々な迷路の生成アルゴリズムが考案されています。
興味のある方は、下記のページをごらんください。
英語ですが、動作するサンプルが豊富で楽しめます。

"Algorithm" is Not a Four-Letter Word

さて、上記のアルゴリズムをBASICで書いてみました。
表示部分も含めて何とか1画面に納まりました。

pc5-2.jpg
M_W=49:M_H=24
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 OR I==M_W+1 OR J==0 OR 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 AND R_W>0 THEN R[I,J]=1
IF R_N>0 AND R_W==0 THEN R[I,J]=2
IF R_N>0 AND R_W>0 THEN R[I,J]=1+RND(2)
NEXT
NEXT
FOR J=1 TO M_H
FOR I=1 TO M_W
IF R[I,J]==3 THEN PRINT "┌";
IF R[I,J]==2 THEN PRINT "─";
IF R[I,J]==1 THEN PRINT "│";
NEXT
PRINT "│"
NEXT
PRINT "─"*M_W

まず、ずっと飛ばして最後のFOR~NEXTループを見てください。
これは迷路の画面表示部分です。
壁の有無について、以下のように数字を割り当てました。
・北、西に壁:3
・北に壁:2
・西に壁:1
FOR~NEXTループでは、この数字に従って罫線を1文字出力しています。
なお、罫線は記号入力モードで入力できます。記号入力モードにするには、EDITボタンの上のハートマークのキーを押します。

部屋の状態は、上記の3つの数に加えて
・部屋がない:0
という数値も割り当てます。
数値0の部屋を、迷路を取り囲むように配置します。

すると、初期状態(全部の部屋の北・西の壁がある状態)は下図のようになります。(迷路のサイズは5×5にしています。)

pc5-4.jpg

それでは、プログラムの最初から説明していきます。
先頭部分、M_HとM_Wが迷路(Maze)の大きさです。
M_Hが縦の大きさ(Height)、M_Wが横の大きさ(Width)を表します。
今回は、プチコンの画面サイズが50文字×30行なので、それより小さくしています。
次に、迷路の部屋を現す配列を宣言しています。
サイズが「M_W+2,M_H+2」となっているのは、迷路の周囲を取り囲む「0」の部屋の分を含めているためです。

続いて、配列を初期状態にします。配列と迷路の部屋との対応は下図のようになっています。

pc5-5.jpg

一番上の行(J==0)と一番下の行(J==M_H+1)、一番左の列(I==0)と一番右の列(I==M_H+1)は0、それ以外は3にします。

FOR J=0 TO M_H+1
FOR I=0 TO M_W+1
IF I==0 OR I==M_W+1 OR J==0 OR J==M_H+1 THEN
R[I,J]=0
ELSE
R[I,J]=3
ENDIF
NEXT
NEXT

次に、「0」の部屋以外の各部屋について、北の壁と西の壁のどちらかを消します。
実際には、配列の要素の値を1または2にしていきます。

R_N=R[I,J-1]
R_W=R[I-1,J]
IF R_N==0 AND R_W>0 THEN R[I,J]=1
IF R_N>0 AND R_W==0 THEN R[I,J]=2
IF R_N>0 AND R_W>0 THEN R[I,J]=1+RND(2)

R_Nは部屋の北隣の部屋の状態、R_Wは部屋の西隣の部屋の状態です。

北隣が0で西隣が0以外なら、西の壁を壊します。(北の壁がある状態=1)
西隣が0で北隣が0以外なら、北の壁を壊します。(西の壁がある状態=2)
両方とも0以外の場合は、状態1と状態2のどちらかをランダムに選びます。
RND(2)は0以上2未満の整数をランダムに返しますので、1+RND(2)で1か2のどちらかの値になります。

以上で迷路ができました。
表示部分は最初に解説しましたので、プログラムはこれで終わりです。

次は、キャラクターを操作して迷路を抜けるプログラムを作りたいと思います。

プチコン3号 4日目 背景をスクロールさせる

昨日はスプライトを動かしてみましたが、今日はRPGでよくあるような、主人公を中央に固定して背景をスクロールさせるプログラムを作ってみました。

プチコン3号には背景用グラフィックス(以下BG)のための命令が数多く備わっています。
スプライトとBGがあれば、2Dのゲームの画像表示そのもので悩むことはほぼ無いのではないかと思います。

今回はBGを以下のように使います。

pc4-1.png

まず、スクリーンの3倍の大きさのBGを用意します。
そして、スプライトはスクリーン中央に固定し、スクリーン全体をBGの上で移動させます。
すると、結果的にスプライトがBGの上を移動しているように見えることになります。

ただし、画面の端からはみ出さないように気をつけなければなりません。
移動範囲はスクリーンの2倍、Xが0から799まで、Yが0から480までとなります。
それ以上動かすと、BGの外側の何もない部分が見えてしまいます。

pc4-2.png

実際に作ってみたプログラムは以下の通りです。
基本的には、前回と同じプログラムで、スプライトを動かす代わりにBGを動かしているだけです。
他に、最初にBGに絵を描いている処理が増えています。

pc4-0.jpg

X=400:Y=240:Z=500
DZ=0
SPSET 0,506
SPOFS 0,184,104,Z
BGSCREEN 0,75,45
BGFILL 0,0,0,74,44,684
BGFILL 0,11,6,63,38,574
BGPUT 0,37,22,246
@LOOP
STICK OUT DX,DY
X=X+DX*8
IF X>800 THEN X=800
IF X<0 THEN X=0 Y=Y-DY*8 IF Y>480 THEN Y=480
IF Y<0 THEN Y=0 IF DZ==0 THEN DZ=16 AND BUTTON(2) Z=Z-DZ IF Z<-200 THEN DZ=-DZ IF Z>500 THEN DZ=0:Z=500
BGOFS 0,X,Y,500
S=1.0+(500-Z)/500
SPSCALE 0,S,S
VSYNC 1
GOTO @LOOP

まず最後から5行目の「BGOFS」命令ですが、これはSPOFS同様、指定した番号のBGのX,Y,Zの値を指定します。
XとYは、スクリーンの原点(左上)が、BG上でどの座標にあるかを表します。

Xが大きくなると、スクリーンはBGの上を右へ移動していきます。
そのとき3DSの画面上では、BGの画像が右から左へとスクロールしているように見えることになります。

BGSCREEN 0,75,45

はBGの大きさを指定しています。
プチコン3号はBGを4枚持っており、最初に番号でBGを指定します。今回は0番です。
大きさは、16ピクセル単位での指定となります。横75だと、75*16=1200ピクセルとなります。

BGFILL 0,0,0,74,44,684

は、BGの中の四角形の領域を、指定した画像で塗りつぶします。
領域の座標は、やはり16ピクセル単位での指定となります。

画像と番号との対応は、スプライトと同じくSmileツールの中で確認できます。
今回は、0番のBGの全体を684番の画像で塗りつぶしています。
684番の画像は、下の画面で言うと四角のコンクリートのような画像です。

BGFILL 0,11,6,63,38,574

も同じ命令ですが、画像は574番で、これは草地の画像です。
これでスプライトが移動できる範囲を塗りつぶしています。
その次の

BGPUT 0,37,22,246

は画像を1つ(16×16ピクセル)だけ描く命令です。
BGの中央に、246番の「家」の画像を描いています。

pc4-3.png

プチコン3号 3日目 絵をボタン操作で動かす

昨日はスプライトを画面に表示して、座標の値を変化させて位置を動かしました。
今日はその続きで、Nintendo 3DSの操作パッドでスプライトを操作してみました。
キャラクターをスライドパッドで移動させ、Aボタンでジャンプさせます。

プチコン3号では、スライドパッドの入力は

STICK OUT DX,DY

です。
DXとDYには、実際にはスライドパッドの値を代入したい変数の名前を書きます。
値はそれぞれ、スティックが中央のとき0、左端・下端のとき-1.0、右端・上端のとき1.0です。
Y軸の座標系が画面とは逆になっています。

ボタンの読み取りはもう少し複雑です。
まず、読み取りモードが複数あり、使うモードをパラメータで指定します。
また、複数のボタンの状態を一度に読み取れるように、読み取り結果はビットパターンで返されます。
使い方は以下のようになります。

B=BUTTON(M)

Mはモードを表す整数、Bは読み取り結果を代入するための変数の名前を書きます。

読み取りモードは、

 0 押され続けている状態
 1 ボタンが押された瞬間だけビットが1になる(リピート機能付き)
 2 ボタンが押された瞬間だけビットが1になる(リピート機能なし)
 3 ボタンが放された瞬間だけビットが1になる

の4つが用意されています。
ゲームでの使い方を考えると、たとえば
・押している間だけ移動する → モード0
・押すと弾丸発射、押しっぱなしで連射 → モード1
・押すと弾丸発射、連射なし → モード2
・画面に描かれたボタンをクリックする場合 → モード3
というようになるでしょう。

返り値のビットパターンは、桁の小さいほう(右)から、
 上下左右ABXYLR
のようになっています。まあまあ覚えやすいですね。
ボタンと、それが押されているときの値の対応は下図のようになっています。

pc3-2.png

上のビットパターンを覚えていれば、値は計算で出せます。
今回はAボタンを使うので、右から5番目のビットですから
 2^(5-1) = 2^4 = 16
となります。小学生には、チョット難しいでしょうか・・・。

複数のボタンが押されている場合、値は足し算になります。
たとえば、上ボタンとAボタンが押されていると、16+1=17が返り値になります。

返り値の中に、ある値が含まれているかどうかはビット演算子「AND」で求めます。
たとえば、Bという変数に16という値が含まれているかどうかは、

B AND 16

の演算結果が0か0でないかで判別できます。含まれていないなら結果は0です。

2進数で書くと、16は10000、1は1、17は10001になります。
ANDという演算は、2つの2進数で両方とも1になっているところだけを抜き出します。
10001 AND 10000 = 10000です。
2進数を使うほうが分かりやすいと感じたら、プログラムも2進数で書くと良いかもしれません。
プチコン3号では、2進数は”&B”を頭につけます。
たとえば「B AND 16」は「B AND &B10000」と書いても同じです。

さて、今日つくったプログラムです。
長さは昨日とほとんど変わりません。

pc3-1.jpg

X=192:Y=112:Z=500
DZ=0
SPSET 0,310
@LOOP
STICK OUT DX,DY
X=X+DX*5
IF X>368 THEN X=368
IF X<0 THEN X=0
Y=Y-DY*5
IF Y>208 THEN Y=208
IF Y<0 THEN Y=0 IF DZ==0 THEN DZ=16 AND BUTTON(2)
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

STICK OUTで読み取ったスライドパッドの値は、絶対値が最大1.0と小さいので、5倍して速度の値として使っています。
画面の端から飛び出さないように、Xの最大値と最小値をチェックしています。
MAX、MINといった命令を使って書くやり方もあります。
Yの値もほとんど処理は同じですが、Y軸は画面とスライドパッドが逆方向なので、Y+DYではなくY-DYになっています。

Zですが、常にZ=Z-DZを実行していますので、Zの値をDZだけ減算しつづけています。
ボタンを押さないとき、DZは0で、Zは変化しません。
Aボタンが押されたら、DZに正の値を入れます。
するとZの値がどんどん小さくなっていきますので、最小値(ここでは-200にしています)に達したら、今度はDZを負の値にします。
今度はZがどんどん大きくなっていき、最大値(500)に達したらDZを0にします。

以下の部分は、プログラミング初心者には分かりにくいかもしれません。

IF DZ==0 THEN DZ=16 AND BUTTON(2)

DZに「16 AND BUTTON(2)」を代入しています。
BUTTON(2)には、全てのボタンの状態がすべて入っています。
その中から、Aボタンの状態だけを取り出すために、ビット演算「AND」を使います。
「16 AND BUTTON(2)」の演算結果は、Aボタンが押されたら16、押されていなかったら0になります。
その16という数値を、そのままDZの値として流用しています。

やろうとしていることから考えると、本当は

IF DZ==0 THEN
IF (16 AND BUTTON(2)) >0 THEN DZ=16
ENDIF

と書くべきところですが、IF文ひとつで済ませようと、横着しているわけです。