【プチコン講座】イベントシステム用の衝突判定の実装
こんにちは。継続の錬金術士なおキーヌです。
ブログ毎日更新は139日目になります。
前回「【プチコン講座】イベントシステム構築:イベント配置とアニメ編」でプレイヤーとイベントのアニメーションを実装しました。
見た目的にはかなりゲームぽくなってきましたが、
衝突判定もなにもないのでイベントはただ表示されているだけでしたね。
今回は表示するだけではなくイベントIDをマップ配列のイベントレイヤーに書き込んでしまいましょう。
衝突判定をする時にイベントレイヤーも確認することでイベントの衝突判定を作ることができます。
それではプチコンでRPG作り第11回目を始めましょう。
イベントIDをマップ配列のイベントレイヤーに書き込もう
実際にイベントのスプライトを表示する前にイベントレイヤーにイベントのIDを書き込んでしまいましょう。
マップは一次元配列なのでグリッド的には(幅*高さ)*3をすれば、
3つ目のレイヤーにアクセスできますね。
そしてイベントの座標さえわかれば、グリッド座標に変換する関数とかを使えば簡単にイベントの座標となる配列の添字にアクセスできます。
あとはイベント分回すのでループの時にイベントIDを取り出して、
該当するグリッドに自分自身のIDを書き込むだけです。
ここでピンと来た人は賢いです。
そう、今まで作ってきた関数でこれらの処理は既にもう作り終えています。
それではイベントレイヤーにスプライトのIDと座標を元にマップ配列を書き換える関数を作りましょう。
# イベントレイヤー書き換え
DEF D_EV_LAYER_REWRITE A_NO, A_DIRECT, A_FLAG
VAR POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)
IF A_FLAG > 0 THEN
IF A_DIRECT > 0 THEN
VAR AFTER_POS = POS + D_GET_DIRECT_POINT(A_DIRECT)
G_MAP[AFTER_POS] = A_NO
ELSE
G_MAP[POS] = A_NO
ENDIF
ELSE
VAR BEFORE_POS = POS - D_GET_DIRECT_POINT(A_DIRECT)
G_MAP[BEFORE_POS] = 0
ENDIF
END
# 方向定数から1歩前のグリッド座標を得るための値を得る
DEF D_GET_DIRECT_POINT(A_DIRECTION)
IF A_DIRECTION == #UP THEN
RETURN DIRECT = -MW
ELSEIF A_DIRECTION == #DOWN THEN
RETURN DIRECT = MW
ELSEIF A_DIRECTION == #LEFT THEN
RETURN DIRECT = -1
ELSEIF A_DIRECTION == #RIGHT THEN
RETURN DIRECT = 1
ENDIF
END
「D_EV_LAYER_REWRITE」関数は引数2つ目や3つ目の数値によって分岐させてイベントレイヤーを書き換えます。
自分のいる座標・進んだ先の座標・元々居た座標を取れるようにしています。
引数に渡すのはスプライトNo.と向いている方向(#UP等の定数)と数字フラグです。
数字フラグは0以下だと配列には矯正で自分の居るグリッドに0を書き込むようにしています。
それ以外の場合はスプライトのIDを書き込むようにしています。
この関数の使い方どころは
- スプライトが生成されたとき。
- スプライトが移動した瞬間。
- スプライトが移動し終わった瞬間。
です。
それと、もう一つ新たに作った「D_GET_DIRECT_POINT」という関数の内容ってどこかでみたことありませんか?
そう、衝突判定するときの最初の処理を一つ先の位置と元々居た位置の取得に使うので関数化しました。
よって、作った関数を使うのと既存の関数を少し調整するので下記のように変更してください。
# 向いている方向に止まるまで歩き続ける(グリッド移動)
DEF D_GRID_MOVE SPRITE_NO, SPRITE_DIRECTION
# 渡されたスプライトと向きによって移動方向を決定
IF SPRITE_DIRECTION == #UP THEN
SPVAR SPRITE_NO, 1, (SPVAR SPRITE_NO, 1) - 1
ELSEIF SPRITE_DIRECTION == #DOWN THEN
SPVAR SPRITE_NO, 1, (SPVAR SPRITE_NO, 1) + 1
ELSEIF SPRITE_DIRECTION == #LEFT THEN
SPVAR SPRITE_NO, 0, (SPVAR SPRITE_NO, 0) - 1
ELSEIF SPRITE_DIRECTION == #RIGHT THEN
SPVAR SPRITE_NO, 0, (SPVAR SPRITE_NO, 0) + 1
ENDIF
# 移動させる
SPOFS SPRITE_NO,(SPVAR SPRITE_NO, 0),(SPVAR SPRITE_NO, 1)
# 割った余りが0になったら動きを止める
IF (SPVAR SPRITE_NO, 0) MOD G_SZ == 0 && (SPVAR G_SPRITE_NO, 1) MOD G_SZ == 0 THEN
SPVAR G_SPRITE_NO, 2, 0
ENDIF
END
# 当たり判定チェック
DEF D_CHECK_COLLISION(A_POS, A_DIRECTION)
VAR SUM_POS = 0
VAR DIRECT = D_GET_DIRECT_POINT(A_DIRECTION)
# IF A_DIRECTION == #UP THEN
# DIRECT = -MW
# ELSEIF A_DIRECTION == #DOWN THEN
# DIRECT = MW
# ELSEIF A_DIRECTION == #LEFT THEN
# DIRECT = -1
# ELSEIF A_DIRECTION == #RIGHT THEN
# DIRECT = 1
# ENDIF
# ↑を↓のように関数化したものに置き換えた
SUM_POS = A_POS + DIRECT
IF MAP[SUM_POS] == 0 THEN
# 追加
D_EV_LAYER_REWRITE A_NO, A_DIRECTION, 1
RETURN 1
ELSE
RETURN 0
ENDIF
END
# スプライトセットアップ
DEF D_SP_SETUP A_NO, A_X, A_Y
VAR F = 20
VAR ANIM = G_SP_ANIM_NO[A_NO]
SPSET A_NO ANIM
SPSET A_NO 0, G_SZ * A_X
SPSET A_NO 1, G_SZ * A_Y
SPSET A_NO SPVAR(A_NO,0), SPVAR(A_NO,1)
# 【追加】
D_EV_LAYER_REWRITE A_NO, 0, 1
SPANIM A_NO, "I", F,ANIM, F,ANIM+1, F,ANIM+2, F,ANIM+3, 0
END
これで
- スプライトが生成されたとき。
- スプライトが移動した瞬間。
- スプライトが移動し終わった瞬間。
にマップレイヤーに自分のIDを書き込むことが出来ます。
ここまでやってちゃんと動くかどうか試してください。
まだ衝突判定にイベントレイヤーを調べていないので特に何も変わりませんが、バグがないかチェックするためです。
プレイヤーの衝突判定チェックを改良しよう
これで全てのイベントが衝突判定を書き込めたのと、移動によるイベントレイヤーの書き換えも出来ました。
今度はそれを判定するためにプレイヤーの衝突判定チェック関数を改良しましょう。
といってもそんなに難しいことはしません。
調べるレイヤーは0~4で指定できるので例えばレイヤー1、つまり木が置かれているレイヤーのチェックが終わったあと、レイヤー2か3(今回は3を使います)をチェックするだけです。
順番はどちらでもOKですが、できれば不動のもの(壁や木などのマップの一部)から先に判定したほうが自然な流れだと思います。
それでは改良してみましょう
# コントローラー関数
DEF D_CONTROLLER
IF SPVAR(SP_PLAYER, 2) != 1 THEN
VAR B = BUTTON()
IF (B AND #UP) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #UP) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #UP
ENDIF
ELSEIF (B AND #DOWN) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #DOWN) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #DOWN
ENDIF
ELSEIF (B AND #LEFT) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #LEFT) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #LEFT
ENDIF
ELSEIF (B AND #RIGHT) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #RIGHT) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #RIGHT
ENDIF
ENDIF
ELSE
D_GRID_MOVE SP_PLAYER, (SPVAR SP_PLAYER, 3)
ENDIF
END
# 当たり判定チェック
DEF D_CHECK_COLLISION(A_NO, A_DIRECTION)
VAR MP1_SUM_POS = 0
VAR EV_SUM_POS = 0
VAR DIRECT = D_GET_DIRECT_POINT(A_DIRECTION)
VAR MAP_POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 1)
VAR EVENT_POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)
MP1_SUM_POS = MAP_POS + DIRECT
EV_SUM_POS = EVENT_POS + DIRECT
IF G_MAP[MP1_SUM_POS] == 0 THEN
IF G_MAP[EV_SUM_POS] == 0 THEN
D_EV_LAYER_REWRITE A_NO, A_DIRECTION, 1
RETURN 1
ELSE
RETURN
ENDIF
ELSE
RETURN 0
ENDIF
END
キーコントローラーの部分はなるべくすっきりさせたいので、座標を取っていたものを衝突判定の方に移しました。
そして衝突判定の引数は座標ではなくスプライトナンバーを取ることに変更しています。
レイヤー1とレイヤー3の座標を取得してそれぞれの向かう先のマップ配列の中身が0であれば通れる。
そうでなければ通れないようにしています。
0以外でも通れないときはここに条件式を付け加えるようにすればOKです。
しかし0で通れてしまうとイベントがプレイヤーにめり込むことが可能になってしまっています。
今回作るゲームではイベントは足踏み以外は動かさないつもりなのでこのままでいきます。
まずは完成させることを目的としていますので、改良は後からでもいいでしょう。
それでは実行してみましょう。
イベントにぶつかってみてめり込まずにその場で止まってくれたら成功です。
これでなんだかモンスターたちの存在感が出たような気がしませんか?
次回の為にイベントを発動させる準備をしよう
最後に次回に使う宝箱イベントを作る為にイベント配列に宝箱の情報をいれてあげましょう。
しかしこのままイベントが増えてくると見辛くなってくるので、COPYを使っていたところを改良しようと思います。
COPYでDATAを複数行してまとめて入れられないかなと思ったんですが、無理っぽい感じがしたのでREADを使う方法にしました。
スプライト用の初期テクスチャやイベントデータ配列を以下のように書き直してください。
ついでにモンスターたちの所定位置を変更してみましょう。
DIM G_SP_ANIM_NO[9] # スプライトアニメーション初期値変数 添え字=ID
DIM G_EV_ID[32] # イベント用情報の配列
# データ定義場所
DATA 500,1040,920,1000,980,1020,269,269,269
DATA 1,5,5,2, 2,4,11,2, 3,23,13,2, 4,13,3,2, 5,22,4,2
DATA 6,6,7,1, 7,3,11,1, 8,22,3,1
# アニメーション配列初期値代入
FOR G_I=0 TO LEN(G_SP_ANIM_NO)-1
VAR N=0: READ N
G_SP_ANIM_NO[G_I] = N
NEXT
# イベントID,X座標,Y座標代入
FOR G_I=0 TO LEN(G_EV_ID)-1
VAR N=0: READ N
G_EV_ID[G_I] = N
NEXT
COPYを使っている時よりもだいぶみやすくなりましたね。
今はわかりやすいようにかいていますが、FORで回している部分に注目してください。
配列の長さ以外は同じなのに気付いたでしょうか?
これを関数化して配列を引数に渡してやればもっとスマートになります。
早速改良してみましょう。
D_FIRST_DATA_READ G_SP_ANIM_NO
D_FIRST_DATA_READ G_EV_ID
DEF DD_FIRST_DATA_READ A_ARRAY
FOR G_I=0 TO LEN(G_EV_ID)-1
VAR N=0: READ N
G_EV_ID[G_I] = N
NEXT
END
次に、イベントデータが3つずつから4つずつに増えたのでFORのSTEP3にしていたところを変更しましょう。
毎回FORのところまでいって変更するのが面倒くさいのでグローバル変数にしてしまってもいいかもしれません。
ここはお好きにどうぞ。私はグローバルに変更してイベントデータを読み込んでいるところの近くで定義しておきます。
VAR G_EV_STEP = 4
# イベント配置
FOR G_I=0 TO LEN(G_EV_ID)-1 STEP G_EV_STEP
D_SP_SETUP G_EV_ID[G_I], G_EV_ID[G_I+1], EV_ID[G_I+2]
NEXT
それでは実行してみましょう。
モンスターたちがいい感じの場所に行ってますね。
たからばこもいい感じですが、アニメーションが始まって他の画像が見えてしまっています。
それも当然でたからばこの画像は開閉の2種類しかありません。
そしてなぜか番号的に若い方が開いている宝箱なので、
閉じている宝箱を最初に設定していたらアニメーションをするとそこから+4までの画像が見えてしまいます。
たからばこミミック出ない限りアニメーションする必要はありませんね。
イベント配列を4つにしたのはこのためで、データ4つ目(0から数えると3つ目)はイベントタイプにしました。
- 0.何もない
- 1.静止状態
- 2.その場足踏み
- 3.移動
という風にしています。
今回のチュートリアルではモンスターを移動させないので3は使いませんが、一応書いておきました。
なのでモンスターは基本イベントタイプが2になりますね。
そして宝箱は1にしましょう。
アニメーション設定をするところはスプライトセットアップ関数でしたね。
そこを改良します。
# スプライトセットアップ
DEF D_SP_SETUP A_NO, A_X, A_Y
VAR F = 20
VAR ANIM = G_SP_ANIM_NO[A_NO]
SPSET A_NO ANIM
SPSET A_NO 0, G_SZ * A_X
SPSET A_NO 1, G_SZ * A_Y
SPSET A_NO SPVAR(A_NO,0), SPVAR(A_NO,1)
IF A_NO == 0 || G_EV_ID[(G_EV_STEP * A_NO)-1] == 2 THEN
SPANIM A_NO, "I", F,ANIM, F,ANIM+1, F,ANIM+2, F,ANIM+3, 0
ENDIF
END
スプライトNo.が0の時はプレイヤーなので条件突破でアニメーション開始。
もう一つの条件はイベント配列の各4つめのデータ(イベントタイプ)を見て2であればアニメーション開始をするようにしています。
もし動き回るタイプがいるのであれば2以上とやってもいいかもしれませんね。
──次回はAボタンを押したらイベントを発動させる仕組みを作りたいと思います。
イベントの中身までは作らずAボタンで調べてシーンを切り替えるというところまでにして、
半分はゲームになくてはならないシーンという概念についても話そうと思います。
最後に恒例のここまでのソースコードを張り付けておきます。
それでは。
ACLS
# 変数定義
VAR G_STOP_FLAG = TRUE # ゲームループフラグ
VAR G_I # 汎用変数
VAR G_OX = 0, G_OY = 0 # BG読み込みのオフセット
VAR G_SZ=16, G_MW, G_MH # チップサイズ、マップ幅、マップ高さ
VAR G_BGW = CEIL(400/G_SZ), G_BGH = CEIL(240/G_SZ) # BG読み込み準備
DIM G_MAP[0] # マップレイヤー4枚+イベントレイヤー分
# 変数定義
VAR G_SP_PLAYER = 0 # PLAYERスプライトNo.
DIM G_SP_ANIM_NO[9] # スプライトアニメーション初期値変数 添え字=ID
DIM G_EV_ID[32] # イベント用情報の配列
VAR G_EV_STEP = 4 # 1つのイベントデータ数
# データ定義場所
DATA 500,1040,920,1000,980,1020,269,269,269
DATA 1,5,5,2, 2,4,11,2, 3,23,13,2, 4,13,3,2, 5,22,4,2
DATA 6,6,7,1, 7,3,11,1, 8,22,3,1
# アニメーション配列初期値代入
D_FIRST_DATA_READ G_SP_ANIM_NO
# イベントID,X座標,Y座標代入
D_FIRST_DATA_READ G_EV_ID
# マップデータ準備
LOAD "DAT:TEST", G_MAP, 0 # マップデータのロード
G_MW = SHIFT(G_MAP) # 読み込んだデータ配列の0個目を配列から切り離して取得(1638415という数字が入ってる)
G_MH = G_MW AND &HFFFF : G_MW = G_MW >> 16 AND &HFFFF # 取り出したデータからシフト演算やらビット演算をする
# マップデータ描写
FOR G_I=0 TO 3
BGSCREEN G_I, G_BGW, G_BGH, G_SZ # 1画面分のBG
BGLOAD I, -G_OX, ( -G_OY - ( G_I * G_MH )), G_MW, G_MH * ( G_I + 1 ), G_MAP # マップ描写
NEXT
# プレイヤー配置
D_SP_SETUP G_SP_PLAYER, 1, 0
# イベント配置
FOR G_I=0 TO LEN(G_EV_ID)-1 STEP G_EV_STEP
D_SP_SETUP G_EV_ID[G_I], G_EV_ID[G_I+1], EV_ID[G_I+2]
NEXT
# ゲームループ
WHILE G_STOP_FLAG
D_CONTROLLER # コントローラー関数呼び出し
VSYNC 1 # 垂直同期
WEND
# コントローラー関数
DEF D_CONTROLLER
IF SPVAR(SP_PLAYER, 2) != 1 THEN
VAR B = BUTTON()
IF (B AND #UP) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #UP) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #UP
ENDIF
ELSEIF (B AND #DOWN) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #DOWN) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #DOWN
ENDIF
ELSEIF (B AND #LEFT) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #LEFT) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #LEFT
ENDIF
ELSEIF (B AND #RIGHT) > 0 THEN
IF D_CHECK_COLLISION(G_SP_PLAYER, #RIGHT) == 1 THEN
D_DIRECTION_MOVE SP_PLAYER, #RIGHT
ENDIF
ENDIF
ELSE
D_GRID_MOVE SP_PLAYER, (SPVAR SP_PLAYER, 3)
ENDIF
END
# 向きと移動フラグを設定
DEF D_DIRECTION_MOVE A_SPRITE_NO, A_SPRITE_DIRECTION
SPVAR A_SPRITE_NO, 2, 1
SPVAR A_SPRITE_NO, 3, A_DIRECTION
END
# 向いている方向に止まるまで歩き続ける(グリッド移動)
DEF D_GRID_MOVE SPRITE_NO, SPRITE_DIRECTION
# 渡されたスプライトと向きによって移動方向を決定
IF SPRITE_DIRECTION == #UP THEN
SPVAR SPRITE_NO, 1, (SPVAR SPRITE_NO, 1) - 1
ELSEIF SPRITE_DIRECTION == #DOWN THEN
SPVAR SPRITE_NO, 1, (SPVAR SPRITE_NO, 1) + 1
ELSEIF SPRITE_DIRECTION == #LEFT THEN
SPVAR SPRITE_NO, 0, (SPVAR SPRITE_NO, 0) - 1
ELSEIF SPRITE_DIRECTION == #RIGHT THEN
SPVAR SPRITE_NO, 0, (SPVAR SPRITE_NO, 0) + 1
ENDIF
# 移動させる
SPOFS SPRITE_NO,(SPVAR SPRITE_NO, 0),(SPVAR SPRITE_NO, 1)
# 割った余りが0になったら動きを止める
IF (SPVAR SPRITE_NO, 0) MOD G_SZ == 0 && (SPVAR G_SPRITE_NO, 1) MOD G_SZ == 0 THEN
SPVAR G_SPRITE_NO, 2, 0
ENDIF
END
# ドット座標からグリッド座標に変換
DEF D_GET_GRID_XY(A_X_OR_Y)
RETURN A_X_OR_Y / G_SZ
END
# MAP配列のどこにいるかチェック
DEF D_GET_MAP_POSITION(A_X, A_Y, A_LAYER)
VAR RETURN_POS = 0
VAR GRID_X = D_GET_GRID_XY(A_X)
VAR GRID_Y = D_GET_GRID_XY(A_Y)
VAR PULS_LAYER = (G_MW * G_MH) * A_LAYER
IF A_Y <= 0 THEN
RETURN_POS = GRID_X + PLUS_LAYER
ELSE
RETURN_POS = (GRID_X + (GRID_Y * G_MW)) + PLUS_LAYER
ENDIF
RETURN RETURN_POS
END
# 当たり判定チェック
DEF D_CHECK_COLLISION(A_NO, A_DIRECTION)
VAR MP1_SUM_POS = 0
VAR EV_SUM_POS = 0
VAR DIRECT = D_GET_DIRECT_POINT(A_DIRECTION)
VAR MAP_POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 1)
VAR EVENT_POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)
MP1_SUM_POS = MAP_POS + DIRECT
EV_SUM_POS = EVENT_POS + DIRECT
IF G_MAP[MP1_SUM_POS] == 0 THEN
IF G_MAP[EV_SUM_POS] == 0 THEN
D_EV_LAYER_REWRITE A_NO, A_DIRECTION, 1
RETURN 1
ELSE
RETURN
ENDIF
ELSE
RETURN 0
ENDIF
END
# スプライトセットアップ
DEF D_SP_SETUP A_NO, A_X, A_Y
VAR F = 20
VAR ANIM = G_SP_ANIM_NO[A_NO]
SPSET A_NO ANIM
SPSET A_NO 0, G_SZ * A_X
SPSET A_NO 1, G_SZ * A_Y
SPSET A_NO SPVAR(A_NO,0), SPVAR(A_NO,1)
IF A_NO == 0 || G_EV_ID[(G_EV_STEP * A_NO)-1] == 2 THEN
SPANIM A_NO, "I", F,ANIM, F,ANIM+1, F,ANIM+2, F,ANIM+3, 0
ENDIF
END
# イベントレイヤー書き換え
DEF D_EV_LAYER_REWRITE A_NO, A_DIRECT, A_FLAG
VAR POS = D_GET_MAP_POSITION(SPVAR(A_NO,0), SPVAR(A_NO,1), 3)
IF A_FLAG > 0 THEN
IF A_DIRECT > 0 THEN
VAR AFTER_POS = POS + D_GET_DIRECT_POINT(A_DIRECT)
G_MAP[AFTER_POS] = A_NO
ELSE
G_MAP[POS] = A_NO
ENDIF
ELSE
VAR BEFORE_POS = POS - D_GET_DIRECT_POINT(A_DIRECT)
G_MAP[BEFORE_POS] = 0
ENDIF
END
# 方向定数から1歩前のグリッド座標を得るための値を得る
DEF D_GET_DIRECT_POINT(A_DIRECTION)
IF A_DIRECTION == #UP THEN
RETURN DIRECT = -MW
ELSEIF A_DIRECTION == #DOWN THEN
RETURN DIRECT = MW
ELSEIF A_DIRECTION == #LEFT THEN
RETURN DIRECT = -1
ELSEIF A_DIRECTION == #RIGHT THEN
RETURN DIRECT = 1
ENDIF
END
# DATA初期読み込み
DEF DD_FIRST_DATA_READ A_ARRAY
FOR G_I=0 TO LEN(G_EV_ID)-1
VAR N=0: READ N
G_EV_ID[G_I] = N
NEXT
END