プチコン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は入れましょう。

プチコン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

プチコン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を使って書き換えてみました。

これで迷路をスクロールさせることができました。
キャラクターを迷路の中で移動させる場合は、壁があるかどうか判定を追加する必要があります。
それはまた次回に作りたいと思います。