【プチコン4講座】イベントシステム構築:配置と衝突判定の実装
こんにちは。継続の錬金術士なおキーヌです。
ブログ毎日更新は171日目になります。
前回「【プチコン4講座】イベントシステム構築:イベント配置とアニメ編」でイベントを画面に表示しました。
前回のままではイベントにめり込める状態になっていたので、イベント用の衝突判定を付けましょう。
仕組みはさほど難しくなく壁の衝突判定に少し条件式を加えるだけで完成できます。
その為にはイベントレイヤーに値を書き込む必要があります。
マップの配列にアクセスできる関数はすでに「【プチコン4講座】RPGを作るためのグリッド衝突判定」で作成済みでしたね。
後はその関数を使って配列にアクセスしてそれぞれのイベントIDを書き込んでやればいいわけです。
そしてキャラクターが移動する前にイベントレイヤーを見て通行可能であれば進み、
そうでなければ進むのを辞めるといったグリッド衝突判定でやったことのおさらいになります。
それではプチコン4でRPG作りその7回目始めましょう。
イベントIDをマップ配列のイベントレイヤーに書き込もう
プチコンBIGではレイヤー数値を直接リテラルで指定していましたが、
プチコン4では自作定数が使えるのでそこに定義してしまえばわかりやすくていいですね。
1つだけなら定数でもいいのですがもっといいものがあります。
それは「enum」という定義法です。
「enum」とは列挙型といって連番に名前を付けられるものとなっています。
プログラミング言語にはよくある仕組みなのですがプチコン4から実装されました。
プログラミング初級者の頃は最初は何に使うんだろこれと思っていましたが、
マップレイヤー数値の管理にはもってこいな仕組みなので使ってしまいましょう。
定数と何が違うのっていうと一々値を指定しなくても勝手に値を設定してくれるところですね。
しかし連番開始の数値だけはいれておかないといけないので最初だけ数値を入れます。
ENUM #LAYER_MAP1=0,#LAYER_MAP2,#LAYER_EV
使い方は定数と同じでシャープを頭に付けます。
これでパッと見読めるので数値で管理するよりわかりやすくなりました。
プチコンBIGでもそれ用の変数をつけてあげればわかりやすかったのですが、
むやみやたらに変数を作るのもいかがなものかなと思い当時は変更することはなかったのでリテラルにしていました。
enumが使える今、利用しない手はないですね。
イベントレイヤー書き換え関数の作成
それではenumを使ったイベントレイヤー書き換え関数を作成しましょう。
方法を説明しておくと、最初にマップをロードしたときに幅と高さを保持した
「G_BGW」「G_BGH」の変数がありますのでそれを利用するとマップの大きさを計算できます。
レイヤーは1つ重ねるたびに同じ大きさをプラスするので指定のレイヤーの指定の位置にアクセスする場合、
横縦サイズにレイヤー数を掛け算してやればいいことがわかります。
しかし一番下のレイヤーにアクセスする場合は0を与えると結果が0になってしまうので
0の時は掛け算しないように命令を組む必要があります。
今回はイベントレイヤー以外は基本アクセスしないので対策はしなくても良いと思いますが
一応しておいた方がプログラムの質的には良いでしょう。
' マップ配列書き換え(レイヤー指定
def D_MapArrayReWrite A_Id, A_Direct, A_Flag, A_Layer
var pos = D_GetMapPosition(SPVAR(A_Id,"X"), SPVAR(A_Id,"Y), A_Layer)
if A_Flag > 0 then
if A_Direct >= 0 then
var afterPos = pos + D_GetGridDirectPoint(A_Direct)
G_MapData[afterPos] = A_Id
else
G_MapData[pos] = A_Id
endif
else
var beforePos = pos - D_GetGridDirectPoint(A_Direct)
G_MapData[beforePos] = 0
endif
end
' 方向定数から1歩前のグリッド座標を得るための値を得る
def D_GetGridDirectPoint(A_Direct)
var direct = 0
case A_Direct
when #B_RUP
direct = -G_BGW
when #B_RDOWN
direct = G_BGW
when #B_RLEFT
direct = -1
when #B_RRIGHT
direct = 1
endcase
return direct
end
マップ配列書き換えはスプライト初期化時にループで一緒に呼び出してやると、
スプライトを表示しつつイベントレイヤーも書き換えできるので便利です。
更に移動時にも呼び出せるので移動前に移動先のマップ配列を自身のIDに書き換え、
移動完了後に移動前のマップ配列を0にして通れるようにします。
そしてその中で進む方向にアクセスするための関数も作りました。
「D_GetGridDirectPoint」は向いている方向のグリッドにアクセスするための数値を得られます。
これを使って衝突判定関数を少し改良してみましょう。
' 進む先のグリッドが移動可能かどうかを調べる
DEF D_CheckCollision(A_Pos, A_Direct)
var sumPos = 0
sumPos = A_Pos + D_GetGridDirectPoint(A_Direct)
if G_MapData[sumPos] == 0 then
return #TRUE
endif
return #FALSE
END
ながったらしい1つの処理を切り出したらかなりシンプルになりましたね。
このように流用できそうな処理は関数化してしまうと便利です。
私がコードを書いていてこれ関数にした方がいいんじゃない?という
発想が出てきたらプログラミングレベルが上昇しています。
時々気づかずに冗長に書くこともあるので、そこを見つけられるようになれれば
コードを見る力も鍛えられているということになります。
スプライト移動処理が常に動いていたので修正
コントローラー関数の最後にスプライトを動かす関数を呼び出していましたが、
ここに入れていると不都合なことがわかりました。
コントローラー関数は押した方向だけを返すようにして、
押されていなかった時は0を返すようにしましょう。
そしてスプライト移動の関数はムーブフラグがONでないときは、
押された方向をみて0であれば何もしないで0より大きければ移動したとみなします。
そしてスプライト移動の関数は一旦ゲームループにいれておきましょう。
のちにキーは値を返してそれを保持して、それぞれのシーンに対して値を渡すだけにしたいです。
これに関してはシーンを作成したときにまた説明します。
今はゲームループに入れておきましょう。
'──────────────────────────
' ゲームループ開始
'──────────────────────────
loop
' コントローラー関数呼び出し
D_SpriteGridMove 0, D_Controller()
' ゲームループフラグ監視
if !G_GameLoopFlag then BREAK
' フレームレート安定
VSYNC 1
endloop
' コントローラー関数
def D_Controller()
var B = BUTTON(0)
# #B_RRIGHTは右コントローラーの右ボタンつまりAボタンの意
var BTN_UP = 1 << #B_LUP
var BTN_DOWN = 1 << #B_LDOWN
var BTN_LEFT = 1 << #B_LLEFT
var BTN_RIGHT = 1 << #B_LRIGHT
var direct = -1
if (B and BTN_UP) > O then
direct = #B_RUP
endif
if (B AND BTN_DOWN) > O then
direct = B_RDOWN
endif
if (B AND BTN_LEFT) > O then
direct = B_RLEFT
endif
if (B AND BTN_RIGHT) > O then
direct = B_RRIGHT
endif
return direct
end
def D_SpriteGridMove A_Id, A_Direct
if SPVAR(A_Id,"MoveFlag") == 1 then
case SPVAR(A_Id,"Direct")
when #B_RUP
SPVAR A_Id, "Y", SPVAR(A_Id, "Y") - 1
when #B_RDOWN
SPVAR A_Id, "Y", SPVAR(A_Id, "Y") + 1
when #B_RLEFT
SPVAR A_Id, "X", SPVAR(A_Id, "X") - 1
when #B_RRIGHT
SPVAR A_Id, "X", SPVAR(A_Id, "X") + 1
endcase
' キャラクターの移動
SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
' 割った余りが0になったら動きを止める
if SPVAR(A_Id, "X") MOD #CSZ == 0 && SPVAR(A_Id, "Y") MOD #CSZ == 0 then
SPVAR A_Id, "MoveFlag", 0
endif
else
if A_Direct >= 0 then
' 現在のグリッド座標にアクセスするマップ配列添え字を取得
var pos = D_GetMapPosition(SPVAR(A_Id, "X"), SPVAR(A_Id, "Y"), 0)
' 移動の可否関係なく向きは変更しておく
SPVAR 0, "Direct", A_ Direct
' 向かう先が移動可能かどうかを調べる(MAPレイヤー&イベントレイヤー)
if D_CheckCollision(pos, SPVAR(A_Id, "Direct")) == #TRUE then
SPVAR A_Id, "MoveFlag", 1
endif
endif
endif
end
プレイヤーの衝突判定チェックを改良しよう
イベントレイヤー書き換えが可能になりました。
これに伴い衝突判定処理の改良を行いましょう。
文字でネタバレしておくと、方向キーを押した時に壁かどうかをチェックしていたと思いますが
それにイベントレイヤーのチェックを加えるだけです。
それと忘れずにスプライト初期化時にイベントレイヤーを書き替えます。
早速コーディングしてみましょう。
' スプライト初期化
def D_SpInitialize A_Id
SPSET A_Id, 500
SPVAR A_Id, "X", G_EvId[G_SpEvStep * A_Id + 1] * #CSZ
SPVAR A_Id, "Y", G_EvId[G_SpEvStep * A_Id + 2] * #CSZ
SPVAR A_Id, "Direct", -1
SPVAR A_Id, "MoveFlag", 0
SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
D_MapArrayReWrite A_Id, SPVAR(A_Id, "Direct"), 1, #LAYER_EV
SPANIM A_Id, "I",\
20,spAnimNo[A_Id],\
20,spAnimNo[A_Id]+1,\
20,spAnimNo[A_Id]+2,\
20,spAnimNo[A_Id]+3,\
G_EvId[G_SpEvStep * A_Id + 3]
end
' マップ配列添字生成
def D_GetMapArrayLayerNo(A_Pos,A_Layer)
' 与えられた位置に対してレイヤーを考慮した配列添字を返す
return A_Pos + ((G_BGW * G_BGH) * A_Layer)
end
' 進む先のグリッドが移動可能かどうかを調べる
DEF D_CheckCollision(A_Pos, A_Direct)
var sumPos = 0
var evPos = 0
sumPos = D_GetMapArrayLayerNo(A_Pos, #LAYER_MAP1) + D_GetGridDirectPoint(A_Direct)
evPos = D_GetMapArrayLayerNo(A_Pos, #LAYER_EV) + D_GetGridDirectPoint(A_Direct)
if G_MapData[sumPos] == 0 then
if G_MapData[evPos] == 0 then
return #TRUE
endif
endif
return #FALSE
END
位置と指定レイヤーを与えたらそれぞれのレイヤーのグリッド座標にアクセスするための関数を作りました。
これを作っておけば例えばイベント側でも特定のイベントが特定の位置にいるかどうかを調べやすくなります。
次に作った関数を利用して衝突判定チェックをしたいレイヤーの座標を取得して、
移動可能かどうかを調べるようにしました。
TRUEを返せば通れるように。
FALSEを返せば問答無用で移動をできないように。
この辺は隠し通路とかを作るときに条件を増やせばいいでしょう。
現在はマップレイヤー0とイベントレイヤーしか見ていません。
マップレイヤー1の指定座標の値が99だったら隠し通路なので通れるという許可を与えることもできます。
今回は作りませんが、知識として覚えておいてください。
次回の為にイベントを発動させる準備をしよう
次回は宝箱イベントを作ります。
その為に少し準備をしておきましょう。
プチコンBIGではREADやDATAを使ってデータを管理していたのですが、
プチコン4では直接配列に値が挿入しやすくなったので楽ですね。
もし外部ファイルにする場合はプチコンBIGと同じでDATA形式にした方が良いでしょう。
先にデータは外部ファイル化しようと思ったのですがやはり説明が面倒くさくなりそうなので、
ある程度進んでから外部ファイルに切り分ける記事を書こうかなと思います。
それでは宝箱を配置するついでにまずはモンスターが整列している状態なので、
所定の位置に配置させてみましょう。
' アニメーション開始スプライト定義No配列
var G_SpEvStep = 4
DIM G_SpAnimNo[] = [500,1040,920,1000,980,1080,1020,269,269,269,269]
DIM G_EvId[] = [0,1,0,0,\
1,5,5,0,\
2,4,11,0,\
3,23,13,0,\
4,13,3,0,\
5,8,2,0,\
6,22,4,0,\
7,6,7,-1,\
8,21,9,-1,\
9,3,11,-1,\
10,22,2,-1\
]
これで各々所定位置につきました。
実行してみるとわかると思うのですが宝箱の画像がバグっています。
というのも、歩行グラフィックと同じように画像が切り替わっているだけですね。
宝箱はなぜか手前に開いたグラフィックが用意されているので、
基本的にはアニメーションしないようにして宝箱が開いたら数値を変更して開いた画像にする。
という手法を取ります。
イベント配列の4つ目の数値は動くモノは0で動かないものは-1にしました。
というのも、SPANIM命令で最後に指定する数値が0だとアニメーションを無限ループします。
1以上の数値だと指定回数ループするようなので動かないものは-1にしました。
そしてこの数値を直接SPANIMに渡すとエラーが出ると思うので、
-1だった場合はSPANIMを実行しないという処理に書き替えましょう。
' スプライト初期化
def D_SpInitialize A_Id
SPSET A_Id, 500
SPVAR A_Id, "X", G_EvId[G_SpEvStep * A_Id + 1] * #CSZ
SPVAR A_Id, "Y", G_EvId[G_SpEvStep * A_Id + 2] * #CSZ
SPVAR A_Id, "Direct", -1
SPVAR A_Id, "MoveFlag", 0
SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
D_MapArrayReWrite A_Id, SPVAR(A_Id, "Direct"), 1, #LAYER_EV
if G_EvId[G_SpEvStep * A_Id + 3] != -1 then
SPANIM A_Id, "I",\
20,spAnimNo[A_Id],\
20,spAnimNo[A_Id]+1,\
20,spAnimNo[A_Id]+2,\
20,spAnimNo[A_Id]+3,\
G_EvId[G_SpEvStep * A_Id + 3]
endif
end
これでいい感じにプレイヤーとモンスターは足踏みアニメーションをして、
宝箱は静止した状態になったと思います。
──次回はAボタンを押してイベントを調べられるようにします。
ネタバレしておくと現状のままでは1フレームに1回調べてしまう状態になるので、
ボタンの制御をおこなう必要があります。
カーソルキーも1回押したら高速で動いてしまって選択肢でストレスでしかなくなりますから、
次回はキーを1回だけ反応させるという処理を中心に作っていきます。
最後に今回の完成版ソースコードを置いておきます。
それでは
'──────────────────────────
' ▼ 動作モード設定:変数宣言は必須
'──────────────────────────
OPTION STRICT
'──────────────────────────
' ▼ 画面のクリア
'──────────────────────────
ACLS
'──────────────────────────
' ▼ 定数・構造体の定義
'──────────────────────────
CONST #CSZ = 16
ENUM #LAYER_MAP1=0,#LAYER_MAP2,#LAYER_EV
'──────────────────────────
' ▼ 変数・配列の定義
'──────────────────────────
' ┠─ 汎用変数
'━━━━━━━━━━━━━━━━━━━━━━━━━━
var G_I = 0, G_J = 0, ¥
G_BGW = 0, G_BGH = 0
' ゲームループを制御するフラグ
var G_GameLoopFlag = #TRUE
'──────────────────────────
' ┠─ マップシーン用配列
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' 空配列宣言
DIM G_MapData[]
'──────────────────────────
' ┠─ スプライト用配列
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' アニメーション開始スプライト定義No配列
var G_SpEvStep = 4
DIM G_SpAnimNo[] = [500,1040,920,1000,980,1080,1020,269,269,269,269]
DIM G_EvId[] = [0,1,0,0,\
1,5,5,0,\
2,4,11,0,\
3,23,13,0,\
4,13,3,0,\
5,8,2,0,\
6,22,4,0,\
7,6,7,1,\
8,21,9,1,\
9,3,11,1,\
10,22,2,1\
]
'──────────────────────────
' ▼ プログラム初期化処理
'──────────────────────────
D_GAME_INITIALIZE
'──────────────────────────
' ゲームループ開始
'──────────────────────────
loop
' コントローラー関数呼び出し
D_SpriteGridMove 0, D_Controller()
' ゲームループフラグ監視
if !G_GameLoopFlag then BREAK
' フレームレート安定
VSYNC 1
endloop
'──────────────────────────
' ▼ 関数の定義
'──────────────────────────
' ┠─ ▼ 初期化関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
DEF D_GameInitialize
' マップデータのロード
D_MapInitialize "MAP001.DAT"
' マップデータの描写
'D_MapDraw
' スプライトの初期化
for G_I=0 to LEN(G_EvId)-1 step G_SpEvStep
D_SpInitialize G_EvId[G_I]
next
END
'──────────────────────────
' ┗━┓ ▼ マップ関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
DEF D_MAP_INITIALIZE A_FILENAME$
' マップデータファイル読み込み
G_MAPDATA = LOADV(A_FILENAME$)
' マップデータ配列の先頭2つを取り出す
G_BGW = SHIFT(G_MapData)
G_BGH = SHIFT(G_MapData)
END
'──────────────────────────
' ┗━┳━ ▼ マップデータ描写
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' マップデータの描写関数
DEF D_MapDraw
' マップチップ配列を描写
for G_J = 0 to G_BGH-1
for G_I = 0 to G_BGW-1
' ローカル変数CPに計算した値を入れる
var cp = G_I + ( G_J * G_BGW )
' CASE文でMAPDATA[CP]の内容に応じて処理を切り替える
case G_MapDat[cp]
' 0のとき
when 0:
' 地面を表示
TPUT 0, G_I, G_J, CHR(&HE8C9)
' 1のとき
when 1:
' 木を表示
TPUT 0, G_I, G_J, CHR(&HE8CA)
endcase
next
next
end
'──────────────────────────
' ┠─ コントローラー関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' コントローラー関数
def D_Controller()
var B = BUTTON(0)
# #B_RRIGHTは右コントローラーの右ボタンつまりAボタンの意
var BTN_UP = 1 << #B_LUP
var BTN_DOWN = 1 << #B_LDOWN
var BTN_LEFT = 1 << #B_LLEFT
var BTN_RIGHT = 1 << #B_LRIGHT
var direct = -1
if (B and BTN_UP) > O then
direct = #B_RUP
endif
if (B AND BTN_DOWN) > O then
direct = #B_RDOWN
endif
if (B AND BTN_LEFT) > O then
direct = #B_RLEFT
endif
if (B AND BTN_RIGHT) > O then
direct = #B_RRIGHT
endif
return direct
end
'──────────────────────────
' ┠─ ▼ スプライト関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' スプライト初期化
def D_SpInitialize A_Id
SPSET A_Id, 500
SPVAR A_Id, "X", G_EvId[G_SpEvStep * A_Id + 1] * #CSZ
SPVAR A_Id, "Y", G_EvId[G_SpEvStep * A_Id + 2] * #CSZ
SPVAR A_Id, "Direct", -1
SPVAR A_Id, "MoveFlag", 0
SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
D_MapArrayReWrite A_Id, SPVAR(A_Id, "Direct"), 1, #LAYER_EV
if G_EvId[G_SpEvStep * A_Id + 3] != -1 then
SPANIM A_Id, "I",\
20,spAnimNo[A_Id],\
20,spAnimNo[A_Id]+1,\
20,spAnimNo[A_Id]+2,\
20,spAnimNo[A_Id]+3,\
G_EvId[G_SpEvStep * A_Id + 3]
endif
end
' スプライトの移動
def D_SpriteGridMove A_Id, A_Direct
if SPVAR(A_Id,"MoveFlag") == 1 then
case SPVAR(A_Id,"Direct")
when #B_RUP
SPVAR A_Id, "Y", SPVAR(A_Id, "Y") - 1
when #B_RDOWN
SPVAR A_Id, "Y", SPVAR(A_Id, "Y") + 1
when #B_RLEFT
SPVAR A_Id, "X", SPVAR(A_Id, "X") - 1
when #B_RRIGHT
SPVAR A_Id, "X", SPVAR(A_Id, "X") + 1
endcase
' キャラクターの移動
SPOFS A_Id, SPVAR(A_Id,"X"), SPVAR(A_Id,"Y")
' 割った余りが0になったら動きを止める
if SPVAR(A_Id, "X") MOD #CSZ == 0 && SPVAR(A_Id, "Y") MOD #CSZ == 0 then
SPVAR A_Id, "MoveFlag", 0
endif
else
if A_Direct >= 0 then
' 現在のグリッド座標にアクセスするマップ配列添え字を取得
var pos = D_GetMapPosition(SPVAR(A_Id, "X"), SPVAR(A_Id, "Y"), 0)
' 移動の可否関係なく向きは変更しておく
SPVAR 0, "Direct", A_ Direct
' 向かう先が移動可能かどうかを調べる(MAPレイヤー&イベントレイヤー)
if D_CheckCollision(pos, SPVAR(A_Id, "Direct")) == #TRUE then
SPVAR A_Id, "MoveFlag", 1
endif
endif
endif
end
'──────────────────────────
' ┠─ ▼ 衝突判定関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' ドット座標をグリッド座標に変換
def D_GetGridXY(A_XY)
return A_XY / #CSZ
end
' グリッド座標とレイヤーを与えてマップ配列の値を取得する
def D_GetMapPosition(A_X, A_Y, A_LAYER)
var gridX = D_GetGridXY(A_X)
var gridX = D_GetGridXY(A_Y)
VAR layerNo = (G_BGW * G_BGH) * A_LAYER
IF A_Y <= 0 then
return gridX + layerNo
ELSE
return gridX + (GRIX_Y * G_BGW)) + layerNo
ENDIF
END
' 進む先のグリッドが移動可能かどうかを調べる
DEF D_CheckCollision(A_Pos, A_Direct)
var sumPos = 0
var evPos = 0
sumPos = D_GetMapArrayLayerNo(A_Pos, #LAYER_MAP1) + D_GetGridDirectPoint(A_Direct)
evPos = D_GetMapArrayLayerNo(A_Pos, #LAYER_EV) + D_GetGridDirectPoint(A_Direct)
if G_MapData[sumPos] == 0 then
if G_MapData[evPos] == 0 then
return #TRUE
endif
endif
return #FALSE
END
' 方向定数から1歩前のグリッド座標を得るための値を得る
def D_GetGridDirectPoint(A_Direct)
var direct = 0
case A_Direct
when #B_RUP
direct = -G_BGW
when #B_RDOWN
direct = G_BGW
when #B_RLEFT
direct = -1
when #B_RRIGHT
direct = 1
endcase
return direct
end
' マップ配列添字生成
def D_GetMapArrayLayerNo(A_Pos,A_Layer)
' 与えられた位置に対してレイヤーを考慮した配列添字を返す
return A_Pos + ((G_BGW * G_BGH) * A_Layer)
end
' マップ配列書き換え(レイヤー指定
def D_MapArrayReWrite A_Id, A_Direct, A_Flag, A_Layer
var pos = D_GetMapPosition(SPVAR(A_Id,"X"), SPVAR(A_Id,"Y), A_Layer)
if A_Flag > 0 then
if A_Direct >= 0 then
var afterPos = pos + D_GetGridDirectPoint(A_Direct)
G_MapData[afterPos] = A_Id
else
G_MapData[pos] = A_Id
endif
else
var beforePos = pos - D_GetGridDirectPoint(A_Direct)
G_MapData[beforePos] = 0
endif
end