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) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2014年12月29日

プチコン3号 7日目 ドラクエ風に迷路の中を歩き回る

pc7-1.jpg

前回、BGに迷路を表示させることができましたので、今度はプレイヤーのキャラクタを中央に置いて迷路の中を歩き回れるようにします。
ドラゴンクエストで、ダンジョンの中を歩き回るときのような感じです。

前回、迷路を表示してスクロールすることはできていますので、今回と前回の表示の違いは、画面中央にプレイヤーキャラがあるかどうかだけです。
しかし実際には、
・迷路の中で実際にプレイヤーキャラを動かしてみて
・壁にぶつかった場合は壁の手前までの移動に修正し
・プレイヤーキャラの新しい位置に応じて迷路をスクロール
という処理が必要になります。

壁に衝突したかどうかの判定のために、まず迷路のマップを表すデータを作ります。
BG表示のときは、迷路の各部屋を2×2の絵で表現しましたが、マップデータでは壁と通路をより明確に分離するために、迷路の1部屋を4×4のマス目で表すことにします。

マップデータのイメージは下図のような感じです。
太線が迷路の1部屋、細線がマップの1マスです。
迷路の壁は1マス幅、通路は3マス幅になっています。
pc7-2.jpg

画面上では迷路の1部屋をBG4枚=32×32ピクセルで表示していますので、マップの1マスは8×8ピクセル相当になります。
このマップ自体はプログラムの内部で使うだけなので、キャラ番号などを気にする必要はなく、壁と通路が区別できれば十分です。

まずは、このマップを作成するプログラムです。
(今回はプログラムが長いので、テキストは最後に掲載します)
pc7-3.jpg

マップにも配列変数を使います。配列のサイズは迷路のサイズ(M_W、M_H)の4倍に右端の1列と最下行の1行を追加したものになります。
配列の各要素には、壁がある場所には1を代入します。壁が無い場所は初期値(0)です。

横に4つ、または縦に4つ、値を代入する必要があるので、行数だけは多いですが、ループを回して、迷路の各部屋を表す配列R[]を参照しながら、マップの配列MAP[]に代入するだけの、簡単な処理です。
なお、部屋の状態をチェックするのに、==ではなくANDを使っています。状態3のときは状態1の処理と状態2の処理を両方行いたいのでこのようにしています。

配列の処理完了後のイメージは下の図のようになります。これは5×5の迷路です。
マップは1マスを1つの数字でプリントしています。
右側の迷路に対応する配列の値は、左側のようになります。
壁が1、通路が0になっていますね。
pc7-4.jpg


次に、プレイヤーのキャラクタが、指定された場所へ移動できるかどうかを、このマップを使って判定します。
ただ、マップは8×8ピクセル単位ですが、キャラクタは1ピクセル単位でスムーズに動かしたいですね。
そのための処理はちょっと複雑です。

下の図の左の絵を見てください。
プレイヤーのキャラクタのサイズは16×16ピクセルですので、大きさとしてはマップの2×2マス相当です。
しかし、キャラクターはピクセル単位で移動しますので、実際には3×3マスの範囲にまたがって存在しています。
pc7-5.jpg

ここで、プレイヤーが図の右の絵の点線の位置へ移動しようとしているとします。
右上の絵では、キャラクタが壁にぶつかっていますので、この位置に移動することはできません。
右下はキャラクタが通路の中へ移動していますので、この位置へは移動可能です。

この絵をよく見ると、指定された位置へ無条件に移動して良いかどうかの判定は、プレイヤーのキャラクタを含む3×3マスの中に、壁が含まれているかどうかで判定できることが分かります。

では、3×3マスの中に壁が含まれていた場合は、「プレイヤーは動けない」のでしょうか?
確かに、指定された位置へ移動したらキャラクタが壁の中にめり込んでしまいます。

しかし実際には、プレイヤーは進みたい位置ではなく、「進みたい方向」を指示しているのだと考えられます。
であれば、可能な範囲でキャラクタを動かしてあげたほうが良いでしょう。

下の図を見てください。
左下の絵が、先ほどの「移動することができない状態」の絵です。
pc7-6.jpg

しかし、よく見ると、移動の横方向の成分だけを取り出せば(図右下)、移動することはできます。
また、縦方向の成分についても、壁の手前までは移動することができます(図左上)。
つまり、「指定した位置には移動することができない」場合でも、実は図右上に示した位置までは移動することができるのです。
このような処理をしてやると、ゲームの操作性がかなり向上します。

次に、キャラクタの位置に応じて迷路をスクロールする処理ですが、座標同士の関係を下の図で説明します。
pc7-8.jpg

迷路が描かれたBGは、1つの部屋あたり32×32ピクセルの1枚の大きな画像です。
そしてプレイヤーのキャラクタは、プレイヤーにとっては、この大きな画像の原点から見て座標(X,Y)に乗っています。

一方、表示としては、プレイヤーのキャラクタはスクリーンの中央に固定されています。
スクリーンのサイズは400ピクセル×240ピクセルですので、プレイヤーのキャラクタはスクリーンの原点から見て常に(200,120)の位置にあります。
そして、スクリーンの原点(赤丸)とBGの原点(青丸)の位置関係はBGOFS命令で指定します。

この関係を式で表すと、
BGOFS=(X,Y)-(200,120)
が常に成り立っています。

以上の処理をまとめた、プレイヤーのキャラクタ移動のプログラムは下図のようになります。
pc7-7.jpg

まず、88行目のDEF文を見てください。
ここでは、関数COUNT_WALLを定義しています。
この関数はマップの中の指定した3×3の領域の中に、壁がいくつあるかを数え、その数を返します。

関数のパラメータは、マップを表す配列と、3×3の領域の左上の座標です。
マップは壁のある場所が1、無い場所は0ですので、3×3の領域の中の壁の個数がN_Wに入り、その値が関数の呼び出し側に返されます。

ではプログラムの先頭から見ていきます。
SPSET 0,502
SPOFS 0,192,108,0
X=8:Y=8
@LOOP

最初の2行でスクリーンの中央に502番のキャラクタをスプライトで表示しています。
スプライトの基準点がスプライト自身の左上になるので、画面の中央にスプライトを置くための座標は(200,120)ではなくそこから8ピクセルだけ左上に移動した(192,112)にしています。

次のXとYは、プレイヤーのキャラクタのBG上での初期座標です。左端、上端は壁がありますので、(8,8)が迷路の左上隅の通路になります。

ループの中は、まずスライドパッドの値を読み取って、移動先の位置を(X_NEW,Y_NEW)に代入します。
今回は、パッドの読み取り値を3倍していますので、最大で一度に3ピクセルだけ縦横に移動します。
STICK OUT DX,DY
X_NEW=X+DX*3:Y_NEW=Y-DY*3

次に、COUNT_WALLを使って、移動先の壁の有無を調べます。
COUNT_WALLのパラメータは、マップを表す配列とマップ上での座標ですので、X_NEWとY_NEWはマップ上の座標に変換してから渡します。
この変換に使っている演算「>>3」は「3ビット右へシフト」ですが、ここでは「÷8」とほぼ同じと思ってください。
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

先に説明した「可能な範囲でキャラクタを動かしてあげる」処理をするため、X方向の移動処理とY方向の移動処理を別々に行っています。
そのため、COUNT_WALLを含む処理を2回連続で行っています。
1回目は「(可能な範囲で)X軸方向に移動」し、2回目は「(可能な範囲で)Y軸方向に移動」させています。

ここで「可能な範囲で」という部分ですが、「可能な範囲」とは、「マップ上での位置(マス)が変わらない範囲」つまり「8ピクセル単位のマス目を超えない範囲」です。
この計算をしているのが
X=MAX(MIN(X_NEW,X OR 7),X-(X AND 7))

という部分です。
X OR 7とかX-(X AND 7)とか謎の式が並んでいるように見えるかもしれませんが、これらの値は下図の様な関係になっています。
pc7-9.jpg

Xがマップのあるマス目の中にあるとき、
・そのマス目の中にある一番小さいXの値: X-(X AND 7)
・そのマス目の中にある一番大きいXの値: X OR 7
です。
この範囲での位置移動なら、マップ上での位置は変化しないので、壁との衝突は起こりません。(現在すでにその場所にいるのですから!)

ある値X_NEWが与えられたとき、その値が必ず「X-(X AND 7)以上X OR 7以下」の範囲に入るように変更したいのですが、それには
X_NEW と X OR 7 の値のうち小さいほうを取り
その値と X-(X AND 7)の値のうち大きいほうをX_NEWとする

とすれば、X_NEWはX OR 7より大きくならず、またX-(X AND 7)より小さくならないことが保証できます。
上のプログラムは、この処理を1行で行っているだけです。

そして最後に、X,YをBGOFSのパラメータに変換して終了です。
この座標の変換については先ほど説明しました。

今回は以上ですが、ちょっと長かったですね・・・。
あらためて全部説明しようとすると、結構いろいろと解説しなければならないことがあるものですね。

最後に今回のプログラムのコード全体を掲載しておきます。
迷路のサイズは49×24だと広すぎる感じでしたので15×10にしています。
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
@LOOP
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_W=0
FOR J=0 TO 2
FOR I=0 TO 2
N_W=N_W+MAP[PX+I,PY+J]
NEXT
NEXT
RETURN N_W
END
posted by boochow at 11:11| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2014年12月28日

プチコン3号 6日目 迷路を背景グラフィックスにしてスクロールさせる

前回、迷路を生成しましたので、今度は迷路をBGに表示させて、4日目のようにスライドパッドでスクロールさせてみたいと思います。
こんな感じの表示で、スライドパッドで迷路をスクロールさせます。
いずれは画面中央にキャラクターを置いて、迷路の中を歩けるようにしたいと思いますが、今日はまだスクロールさせるだけです。
pc6-1.jpg

前回はPRINT文で罫線を使って迷路を表示しましたが、今回はBGPUTでBGに描画します。
使用する絵は、「291,292,256,257」の組を選びました。
291が状態3の部屋、292が状態1の部屋、256は右下隅の表示欠けの埋め、257は空白です。
他にもSmileツールで探せば、迷路の表示に使える絵がいろいろと用意されています。
pc6-2.jpg
pc5-4.jpg

状態2の部屋に使う絵は、用意されてません。
これは状態1の部屋に使う292番のキャラクタを、270度回転して使います。
270度回転するには、キャラクタ番号に12288を足します。
12288+292=12580が、状態2の部屋の画像になります。

BGキャラクタの回転に関しては、プチコン3号のマニュアルにも現時点ではあまり詳しく書かれていなかったので、ここでちょっと補足しておきます。
キャラ番号は16ビットで指定しますが、上位4ビットが図のように回転・横反転・縦反転の指定に使われます。
簡単には、
・回転: 回転の状態(0〜3)に4096を掛けた数
・横反転: 16384
・縦反転: 32768
をキャラ番号に加算すれば、回転・反転したキャラクタを表示できます。

ちなみにキャラ番号を16進数3桁で表現すれば、頭に1桁の16進数を付加するだけなので、足し算を計算しなくて済みます。
たとえば、292番は16進数で&H124ですので、270度回転したキャラクタは&H3124になります。
このへんの話になると、プログラミング初心者の方には難しいかもしれませんね・・・
pc6-3.jpg


さて、プログラムに戻ります。
BGもスプライトも16×16ピクセルですので、迷路の各部屋をBG1枚に割り当てると、壁の厚みがあるために窮屈な感じになりそうです。
そこで、各部屋をBG4枚で表現することにしました。
具体的には、下図のような具合です。
太い青線が1部屋分、細い青線がBG1枚です。
たとえば左上の部屋を表現するには、以下のように4回BGに書き込みます。
(0,0)→ 291
(1,0)→ 292
(0,1)→ 12580
(1,1)→ 257
pc6-4.jpg

また、一番右の列には125800、一番下の部屋の下部には292番のキャラクタを描きます。
これだけだと、右下の壁が少し欠けてしまいますので、そこに256番のキャラクタを描きます。

スクロール操作は、BGの大きさが違うことを除けば、基本的には4日目の記事のままです。

以上をプログラムにしたものが以下になります。
画面は、迷路の部分(1行〜20行)は前回と同じですので省略しました。
pc6-5.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 || 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

X=0:Y=0:XMIN=-200:YMIN=-120
XMAX=16*(M_W*2)-200:YMAX=16*(M_H*2)-120
@LOOP
STICK OUT DX ,DY
X=MAX(XMIN,X+DX*8)
X=MIN(X,XMAX)
Y=MAX(YMIN,Y-DY*8)
Y=MIN(Y,YMAX)
BGOFS 0,X,Y,500
VSYNC 1
GOTO @LOOP

今回の追加部分はACLS以降の部分です。
BGのサイズは、迷路一部屋あたり2×2のBGを使いますので、迷路のサイズ(M_W、M_H)の2倍、それに右端の1列と最下行の1行を追加したものになります。
W1、W2、W3、WEは、3種類の壁および壁が無い状態のキャラ番号です。

C1〜C3は、1つの部屋に対するBGのキャラ番号を代入します。
C1が左上、C2が右上、C3が左下です。右下は常に壁無しです。
各部屋の状態R[I,J]に応じて、C1〜C3にW1〜WEを代入します。
そのあと、BGPUTでBGにキャラクタを書き込んでいます。

39行目以降は、スライドパッドでスクロールするためのコードです。
画面の中央のポイント(200,120)が迷路からはみ出さないようにXとYの最大値・最小値を決めています。
また、端までスクロールしたときの判定を以前はIF文で行っていたところを、MINとMAXを使って書き換えてみました。

これで迷路をスクロールさせることができました。
キャラクターを迷路の中で移動させる場合は、壁があるかどうか判定を追加する必要があります。
それはまた次回に作りたいと思います。
posted by boochow at 13:13| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする

2014年12月27日

プチコン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のどちらかの値になります。

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

次は、キャラクターを操作して迷路を抜けるプログラムを作りたいと思います。
posted by boochow at 13:16| Comment(0) | プチコン講座 | このブログの読者になる | 更新情報をチェックする
人気記事