【プチコン講座】プレイヤーを表示して操作してみよう:ドット移動編
こんにちは。継続の錬金術士なおキーヌです。
ブログ毎日更新は133日目になります。
前回「【プチコン講座】マップエディタで作ったマップを読み込んで表示しよう」でマップを描いてそれをプログラムで表示するところまでやりました。
マップを表示しただけでは面白くないので今回は早速プレイヤーキャラクターを表示して操作してマップを歩けるようにしてみましょう。
プレイヤーをマップ上で操作するには大きく分けてグリッド移動とドット移動の2種類があります。
今回は簡単なドット移動の作り方をご紹介します。
ポケモンで例えると、ドット絵時代の初代~BW2までのポケモンと3DになったXY~ピカブイまでのポケモンでわけられますね。
初代からBW2までのポケモンの主人公は基本的に1回十字キーを押すと1歩歩いて隣のマスまで進みます。
一方3Dになったポケモンの主人公は自由にグリグリと3D空間を歩き回れると思います。
今回作るのは3D空間ではなく2D空間をグリグリと歩き回れるようになるドット移動です。
それではプチコンでRPG作り第5回目を始めましょう。
スプライトを表示してみよう
マップチップ1つもスプライトと同じですが、プチコンではBGとSPで分けられているのでスプライト=動かせるモノと認識しておきましょう。
それでは早速画面にスプライトを表示してみましょう!
前回のマップを表示したソースコードにそのまま追記してOKです。
# 変数定義
VAR SP_PLAYER = 0 # PLAYERスプライトNo.
DIM SP_ANIM_NO[10] # スプライトの初期定義Noを格納する配列
# 配列初期値設定
SP_ANIM_NO[0] = 500 # PLAYERの初期定義Noを代入
# スプライト表示
SPSET SP_PLAYER, SP_ANIM_NO[SP_PLAYER]
打ち終わったらRUNで実行してみましょう。
画面左上にキャラクターが表示されたでしょうか?
しかしこのままでは木にめり込んでいますね。
位置をずらすためにSPOFSという命令を与えてみましょう。
# スプライト表示
SPSET SP_PLAYER, SP_ANIM_NO[SP_PLAYER]
SPOFS SP_PLAYER, 1, 0
さて、どうでしょうか?
実行してみてください。
位置、変わりましたか?
変わってねーぞ!って思うかもしれませんが実は変わっています。
といっても1ドット分だけ右に動いただけなので、全く動いているようにはみえないだけです。
なぜそうなったかというと、実はバックグラウンド(BG)とスプライト(SP)では座標指定の仕様が違います。
BGはチップ数分動くのに対して、SPは1ドットずつ動きます。
ということは、チップサイズ分動かせばいい感じの位置に移動するということですね。
書き直してみましょう
# スプライト表示
SPSET SP_PLAYER, SP_ANIM_NO[SP_PLAYER]
SPOFS SP_PLAYER, SZ, 0
SZは変数定義のところで16と設定していましたね。
SZの部分は横軸、つまりX座標で0のところはY座標です。
SPOFSはスプライトNO.とX座標とY座標それぞれの値を渡すことでうごきます。
それではもう一度実行してみましょう。
よし、いい感じですね!
しかしこのままではこれ以上動くことが出来ません。
では、どうすればキャラクターを動かせるでしょうか?
……そう、ゲームループの中でSPOFSを呼び出してX座標とY座標を変数にして
キー入力に応じて座標の数値を変化させられればよいですね。
スプライトを動かすためにキー入力処理を作ろう
プチコンにはキー入力を楽にしてくれる命令があります。
キー入力に関してはゲーム機の仕様などもありますから、
下手に自作しようとせず素直に用意された命令を使いましょう。
折角ゲーム機なのですからキーボード操作ではなくコントローラーで操作できるようにしてみましょう。
それでは早速キー入力のコードを打ち込んでいきましょうか。
VAR G_STOP_FLAG = TRUE
~~~省略~~~
VAR PLAYER_X = SZ*1
VAR PLAYER_Y = SZ*0
~~~省略~~~
WHILE G_STOP_FLAG
D_CONTROLLER
VSYNC 1
WEND
DEF D_CONTROLLER
VAR B = BUTTON()
IF B == #UP THEN PLAYER_Y = PLAYER_Y-1
IF B == #DOWN THEN PLAYER_Y = PLAYER_Y+1
IF B == #LEFT THEN PLAYER_X = PLAYER_X-1
IF B == #RIGHT THEN PLAYER_X = PLAYER_X+1
SPOFS 0,X,Y
END
定数でシャープを使っちゃってるのでそろそろBASICコードが見やすいようにしたほうがよさそうですね。
今回のコードの#はコメントじゃないのでちゃんと打ち込んでください
1つずつ見ていきましょう。
ゲームループの作成
ゲームループについてはもう説明不要ですね。
前回でソースコードを一旦全部消してしまったのでもう一度書いておきます。
まずはWHILEでループを作る為にフラグを設けました。
もちろんGOTOで作ってもらっても構いません。
フラグを用意しない分ゲームループ自体はGOTOの方が良いかもしれませんね。
ですが私はGOTOを使わざるを得ないときまでは使わないで行きます。
ゲームループの中には今回作るコントローラーのキー入力を監視する関数とVSYNCのみです。
プレイヤーの座標変数を作る
PLAYER_XとPLAYER_Yはそれぞれプレイヤーの座標に使います。
計算式が「SZ1」と「SZ0」になっています。
なのでかならず変数SZを定義した後に書いてください。
そうでないと未定義エラーがでてしまいます。
SZは16という数値が入っていますね。
直接16と書いてもいいのですが数値を直すときにSZもここも直さないといけない場合めんどうくさいので、
意味が同じ数値の場合は大元となるものの変数の数値を使いましょう。
16にしているのはチップサイズが16だからSZを使っているという感じです。
3DSだとSZが8になるのでSZの値を変更すればマップチップも8になるという寸法です。
コントローラー関数を作る
今回の目玉ですね。
まずは「D_CONTROLLER」という関数を作りましょう。
次に、ボタンの押されている状況を知るために関数内で変数を宣言します。
この場合はローカル変数になるので関数を抜けると使えなくなります。
変数BにBUTTON()という関数の返り値を代入していますね。
BUTTON()とは、押されているコントローラーのボタンの状態を取得できます。
そしてIFで押されたボタンと同じ数値になれば押している方向に先ほど用意したプレイヤーの座標に加減算して、
最後にSPOFSでプレイヤースプライトの位置を動かします。
ゲームループなのでまた最初に戻ってこのコントローラー関数が呼ばれるので、
ボタン状態を確認して押されている方向によってキャラの座標を変更します。
ちなみに、#UPは1、#DOWNは2、#LEFTは4、#RIGHTは8が格納されています。
二進数の状態で判断するためですね。
1は01、2は10、4は100、8は1000
4を3にしてしまうと、11となって上と下が同時に押されている状態とみなされてしまうからです。
そもそも十字キーは上下もしくは左右同時押しができないのでありえませんね。
一度実行して十字キーを操作してみましょう。
……どうでしょうか?
動けるようになりましたが十字キーを流れるように操作すると動きがとまったりしてストレスがあるでしょう。
その原因はコントローラー関数の中にある条件式にあります。
よくみると同時押しに対応していませんね。
IFが4つありますが条件式が == なので例えば右を例に挙げると、
右以外が押されていない状態の時という考え方も出来ます。
なので右下を押すと条件に一致しなくなるので動かなくなるんですね。
それでは斜め移動もできるようにしてみましょう。
VAR G_STOP_FLAG = TRUE
~~~省略~~~
VAR PLAYER_X = SZ*1
VAR PLAYER_Y = SZ*0
~~~省略~~~
WHILE G_STOP_FLAG
D_CONTROLLER
VSYNC 1
WEND
DEF D_CONTROLLER
VAR B = BUTTON()
IF (B AND #UP) > 0 THEN PLAYER_Y = PLAYER_Y-1
IF (B AND #DOWN) > 0 THEN PLAYER_Y = PLAYER_Y+1
IF (B AND #LEFT) > 0 THEN PLAYER_X = PLAYER_X-1
IF (B AND #RIGHT) > 0 THEN PLAYER_X = PLAYER_X+1
SPOFS 0,X,Y
END
それでは実行してみましょう。
……どうでしょうか?スムーズな動きになっていれば大丈夫です。
少し解説すると、まず計算とおなじようにカッコでくくったところを最初に計算してみます。
AND演算は同じビット位置同士がどちらも1の場合1を返すという仕組みです。
例えば4bitだけ見ると、上を押されている場合ボタン関数の返り値は「0001」の状態になりますね。
左を押されている場合の返り値は「0100」になります。
左と上が同時に押されている場合は「0101」になります。
左上が押された状態での条件式を見てみると、1つ目のIFは「0101」 AND 「0001」という状態になりますね。
結果は一番小さい1bitのところしか合っていないので、「0001」が結果として返ってきます。
その後、0より大きいかどうかの判断をしてTRUEであれば座標変更しています。
上と左の条件式が合致しているので、座標に減算しています。
一方、右と下はAND演算をすると合致するビットがないので0が返ってきます。
そうすると0より大きい場合という条件に満たないのでスルーされます。
これで8方向にグリグリと動けるようになったわけですね。
プチコンのスプライトの便利機能「SPVAR」の紹介
今回、X座標とY座標の変数を作ってプレイヤーの座標位置を設定しました。
プチコンでは実はスプライト専用の自由に使える変数が8つ用意されています。
SPVAR命令を使うことで変数に値を設定したり取得したりすることが出来ます。
なので以下のように書き直してみましょう。
VAR PLAYER_X = SZ*1
VAR PLAYER_Y = SZ*0
↓
SPVAR SP_PLAYER, 0, SZ*1 # X座標
SPVAR SP_PLAYER, 1, SZ*0 # Y座標
変数名はざっと見た感じ付けられない感じはしますね。
数値で指定するということは恐らく内部的には配列的な扱いになっているのかと思います。
別に最初のように変数を用意して使っても問題はありません。
メリットとしては、例えばシューティングゲームの弾幕とかの座標を保持したりすることに使えますね。
最初の方法だとスプライト毎に変数を作らなくてはいけなくなるので、スプライトに関してはSPVARを使った方がよいでしょう。
当たり判定(衝突判定)について考えてみよう
マップを自由に歩き回れるようになったのはいいですが、今のままだと木もすり抜けてしまう状態です。
キャラクターを操作できるゲームにはほぼ100%やらなくてはならないのが当たり判定の設定(衝突判定)ですね。
先ほどのSPVARのように便利な当たり判定用の命令が用意されています。
……ですが今回作ろうとしているものには使いません。
なぜかと言うと用意されている衝突判定命令は、
スプライトが何かにぶつかったかもしくはスプライト同士にぶつかったかがメインで作られています。
古いドラクエやFFタイプのゲームを作る場合はあまりスプライト同士がぶつかったか
という衝突判定は無駄な処理になってしまいます。
ドット移動タイプのRPGであれば役に立ちますが今回作るのは2Dの頃のドラクエやFF・ポケモンみたいに
1歩歩いたら1マス進むタイプのグリッド移動なのでスプライト同士の判定はつかいません。
移動する直前に進行方向に進むと何があるかをチェックするという方法が基本になります。
次回は当たり判定を作る前にグリッド移動を先に作ってしまいましょう。
最後に今回のソースコードを張り付けておきます。
それでは。
ACLS
# 変数定義
VAR G_STOP_FLAG = TRUE # ゲームループフラグ
VAR I # 汎用変数
VAR OX = 0, OY = 0 # BG読み込みのオフセット
VAR SZ=16, MW, MH # チップサイズ、マップ幅、マップ高さ
VAR BGW = CEIL(400/SZ), BGH = CEIL(240/SZ) # BG読み込み準備
DIM MAP[0] # マップレイヤー4枚+イベントレイヤー分
# 変数定義
VAR SP_PLAYER = 0 # PLAYERスプライトNo.
DIM SP_ANIM_NO[10] # スプライトの初期定義Noを格納する配列
# マップデータ準備
LOAD "DAT:TEST", MAP, 0 # マップデータのロード
MW = SHIFT(MAP) # 読み込んだデータ配列の0個目を配列から切り離して取得(1638415という数字が入ってる)
MH = MW AND &HFFFF : MW = MW >> 16 AND &HFFFF # 取り出したデータからシフト演算やらビット演算をする
# マップデータ描写
FOR I=0 TO 3
BGSCREEN I, BGW, BGH, SZ # 1画面分のBG
BGLOAD I, -OX, -OY - I * MH, MW, MH * ( I + 1), MAP # マップ描写
NEXT
# 配列初期値設定
SP_ANIM_NO[0] = 500 # PLAYERの初期定義Noを代入
# スプライト表示
SPSET SP_PLAYER, SP_ANIM_NO[SP_PLAYER] # プレイヤー用スプライトの設定
SPVAR SP_PLAYER, 0, SZ*1 # プレイヤースプライト用変数0 => X座標
SPVAR SP_PLAYER, 1, SZ*0 # プレイヤースプライト用変数1 => Y座標
# ゲームループ
WHILE G_STOP_FLAG
D_CONTROLLER # コントローラー関数呼び出し
VSYNC 1 # 垂直同期
WEND
# コントローラー関数
DEF D_CONTROLLER
VAR B = BUTTON()
IF (B AND \#UP) > 0 THEN SPVAR SP_PLAYER, 1, (SPVAR SP_PLAYER, 1)-1
IF (B AND \#DOWN) > 0 THEN SPVAR SP_PLAYER, 1, (SPVAR SP_PLAYER, 1)+1
IF (B AND \#LEFT) > 0 THEN SPVAR SP_PLAYER, 0, (SPVAR SP_PLAYER, 0)-1
IF (B AND \#RIGHT) > 0 THEN SPVAR SP_PLAYER, 0, (SPVAR SP_PLAYER, 0)+1
SPOFS 0,X,Y
END