【プチコン講座】グリッド移動の衝突判定を作ろう

プチコン

プチコン グリッド衝突判定

こんにちは。継続の錬金術士なおキーヌです。

ブログ毎日更新は136日目になります。

前回「【プチコン講座】グリッド移動の衝突判定について考えてみよう」でグリッド衝突判定について勉強しました。

グリッド式の衝突判定仕組みを理解したところで、実際にコーディングしてみましょう。
手順としては衝突判定したい元となるグリッド座標を求め、隣の座標にある数値を調べて移動可能かを判断します。

隣の座標を調べるのはキーを押した瞬間がベストですね。

それではプチコンでRPG作り第8回目を始めましょう。

  1. 現在の座標からグリッド座標に変換する関数を作ろう
  2. グリッド座標からMAP配列の添字に変換する関数を作ろう
  3. 隣のグリッドの情報を取得して移動できるか判定する関数を作ろう
  4. 移動開始前に衝突判定を行ってみよう
  5. 考察:どのレイヤーを衝突判定としてみるか

現在の座標からグリッド座標に変換する関数を作ろう

衝突判定のコードを書いていくにはまずは値を変換する関数を作っておく必要があります。

計算式を1つに詰め込んでもいいですが見通しが悪くなってしまうことと、
今後別のゲームに流用することも考えて関数化しておくのがベストです。

関数化しておけば、外部ライブラリとして切り離すこともできるので
別のゲーム作る時に読み込めば衝突判定のコードを書く時間を省略できます。

それではまず最初に現在の座標を元にどこのグリッド座標に変換する関数を作りましょう。

必要な情報はX座標とY座標とチップサイズ(変数SZ)です。


DEF D_GET_GRID_XY(A_X_OR_Y)
  RETURN A_X_OR_Y / SZ
END

これで対象の座標をグリッド座標に変換することができました。

グリッド座標からMAP配列の添字に変換する関数を作ろう

現在の座標を渡してグリッド座標に変換してくれる関数を作ったので、
今度はグリッド座標を掛け算してMAP配列の添字に使えるようにしましょう。

この時、引数に調べたいレイヤーを渡せるようにしておくといいでしょう。

レイヤー0ならばそのままでいいですが、レイヤー1を調べたい時は、
引数に1を渡してマップの横幅xマップの縦幅に引数の1を掛けたものを足してやれば別レイヤーの同じ位置を調べることができます。

この計算式を組んでおけばレイヤー2を調べたい時は2を渡せばいいですし、0を調べたい時は0を渡せば掛け算なので何も足されずレイヤー0を調べることができます。

文字だけだとわかりづらいかもしれないので実際にコードを書きましょう。


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 PLUS_LAYER% = (MW% * MH%) * A_LAYER

  IF A_Y <= 0 THEN
    RETURN_POS = GRID_X + PLUS_LAYER
  ELSE
    RETURN_POS = (GRID_X + (GRIX_Y * MW)) + PLUS_LAYER
  ENDIF

  RETURN RETURN_POS
END

これで指定レイヤーのグリッドにアクセスすることができるようになりました。

隣のグリッドの情報を取得して移動できるか判定する関数を作ろう

調べたい対象がMAP配列のどこにいるかを調べられるようになったので、
次は進むであろう隣の位置のグリッドを調べる関数を作りましょう。

前回も言いましたが左右であれば現在のMAP配列の添字に±1をするだけでいいですし、
上下であれば±マップの横幅分をすればアクセスできます。

それではコードを書きましょう。


DEF D_CHECK_COLLISION(A_POS, A_DIRECTION)
  VAR DIRECT = 0
  VAR SUM_POS = 0

  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
    RETURN 1
  ELSE
    RETURN 0
  ENDIF
END

ここで気をつけなければならないのが配列の範囲外を調べてしまうと、
配列の範囲外を参照しようとしたと出てエラーでゲームが停止してしまいます。

なので配列をアクセスする時に例えばプレイヤーがのX=5 / Y=0にいたとして、
上キーを押すとレイヤー0であればマイナスの添字で配列にアクセスすることになりますし、
Y座標が一番下にいた時に下キーを押すと次のレイヤーの一番上を参照することになってしまいます。

なので上下の場合はY座標が一番上か一番下にいる時は移動させないようにするか、
マップがループするのであればループするように処理を書かなければいけません。

今回作るゲームは周囲に壁がありますから配列外にアクセスしたり次のレイヤーにアクセスするということは起こりえませんが、
バグがないとは言い切れませんのでこういったエラーに対する処理は必ず作っておくのが好ましいです。

MAPの添え字にアクセスして帰ってくるのはマップチップのナンバーです。

草原であれば103か105とかだと思います。

スマイルツールでマップチップの数字を調べてみてください。

なので現状は衝突判定をチェックするレイヤーは、木だけを置いているレイヤーにするのが望ましいです。

木を置いていないところは0なので通れます。

逆にそれ以外の数値があった場合は通れないようにしています。

移動開始前に衝突判定を行ってみよう

時は来た。それだけだ。

というのは冗談で必要な関数は全て揃いました。

手順としてはキーコントローラー関数のところをまず改造しましょう。


DEF D_CONTROLLER
  IF SPVAR(SP_PLAYER, 2) != 1 THEN
    VAR B = BUTTON()
    VAR POS = D_GET_MAP_POSITION(SPVAR(SP_PLAYER,0), SPVAR(SP_PLAYER,1), 1)
    IF (B AND #UP) > 0 THEN
      IF D_CHECK_COLLISION(POS, #UP) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #UP
      ENDIF
    ELSEIF (B AND #DOWN) > 0 THEN
      IF D_CHECK_COLLISION(POS, #DOWN) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #DOWN
      ENDIF
    ELSEIF (B AND #LEFT) > 0 THEN
      IF D_CHECK_COLLISION(POS, #LEFT) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #LEFT
      ENDIF
    ELSEIF (B AND #RIGHT) > 0 THEN
      IF D_CHECK_COLLISION(POS, #RIGHT) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #RIGHT
      ENDIF
    ENDIF
  ELSE
    D_GRID_MOVE SP_PLAYER, (SPVAR SP_PLAYER, 3)
  ENDIF
END

キーコントローラー関数はもともと押されたキーで条件を分岐させて移動関数を呼び出していましたが、
押されたキーを元に進む方向が移動できるかどうかを判断する条件分岐を追加します。

その時に判断したいスプライトNoと方向キーを渡します。

あとは作った関数を処理する順番に回せば移動できるのかどうかを判断してくれますね。

実際に実行して移動してみてください。

……どうでしょうか?

だいぶゲームらしくなってきたのではないでしょうか?

あとはこれにプレイヤー以外のイベント(敵や宝箱)などを置けば見た目はゲームになりますね。

しかしここからが本番です。

次回はイベントの作りをする前にイベントの作り方をお話します。

また文字だけでコーディングじゃなくなるのですが、
作り方を考えておかないと確実に頓挫します。

私も過去に何回かRPGを作ろうとして、いつもグリッド移動の衝突判定までは作っていたのですがイベントの実装方法がよくわからず何度挫折したのかわかりません。

ですが、2019年のGWに8.5日掛けてイベント含めゲームをちゃんとプレイできるよう最後まで完成させることができました。

なので次回のイベント作りの話はしっかりと覚えておいて欲しいです。

考察:どのレイヤーを衝突判定としてみるか

最後に衝突判定の話に戻りますが、最終的にどのレイヤーをみて衝突判定をするかを考えなければいけません。

結論から言っておくと、壁もイベントも全て衝突判定レイヤーに自分自身の値を書き込めば衝突判定レイヤーだけを調べればいいので全部調べるよりは処理が軽くなります。

現状はレイヤー1、つまり壁の役割をしている木を置いているレイヤーだけで衝突判定を見ています。

レイヤー1だけでみているとイベントを実装した時に、
イベントと同じ場所にプレイヤーが重なることができてしまいますね。

特別な事情がない限りそれは避けたいので、イベントがいるレイヤーも見なければいけません。

しかし全部のレイヤーを毎回チェックするのはプレイヤー以外のイベントが増えた時、
無駄な処理が増えてしまいますので効率的ではありません。

今のハードの性能なら問題ないと思いますがこういった小さな効率化はプログラミングに置いてとても大切なことです。

確かに動けばいいですがそういう考えでは今後の成長の妨げになるので、
特段コーディングが難しくなければ軽い方の処理を選択したほうがいいでしょう。

対策としては壁などはマップが表示する前に衝突判定レイヤーの木のあるグリッドと同じグリッドに値をコピーすればいいですね。

壁には幽霊出ない限りめりこめないと思うので、イベントが衝突判定レイヤーを書き換えるのは後からにします。

衝突判定レイヤーに全てのイベントが存在するグリッドと同じグリッドに値を書き込めば、
移動する時にそのレイヤーだけを調べれば処理はぐっと軽くなります。

イベントが移動する時も、もともとイベントがいた衝突判定レイヤーのグリッドの値を元の0にして移動先のグリッドの値を変更すると言った感じです。

これは一例にすぎませんのでもっと効率のいいやり方があるかもしれません。

まずは一度全レイヤーのチェックをしてみてから処理落ちするようであれば対策する

ぐらいでいいと思います。

効率化は大切ですが、完成させないのが一番ダメなのでまずは自分の力で出来る範囲でやりましょう。

最後に完成版ソースコードを載せておきます。

それでは。

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を格納する配列

# 配列初期値設定
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座標
SPVAR SP_PLAYER, 2, 0 # 移動中フラグ
SPVAR SP_PLAYER, 3, 10 # 向き設定:最初は下向き
SPOFS SP_PLAYER, SPVAR(SP_PLAYER,0), SPVAR(SP_PLAYER,1) # 初期位置設定

# マップデータ準備
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

# ゲームループ
WHILE G_STOP_FLAG
  D_CONTROLLER # コントローラー関数呼び出し
  VSYNC 1 # 垂直同期
WEND

# コントローラー関数
DEF D_CONTROLLER
  IF SPVAR(SP_PLAYER, 2) != 1 THEN
    VAR B = BUTTON()
    VAR POS = D_GET_MAP_POSITION(SPVAR(SP_PLAYER,0), SPVAR(SP_PLAYER,1), 1)
    IF (B AND #UP) > 0 THEN
      IF D_CHECK_COLLISION(POS, #UP) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #UP
      ENDIF
    ELSEIF (B AND #DOWN) > 0 THEN
      IF D_CHECK_COLLISION(POS, #DOWN) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #DOWN
      ENDIF
    ELSEIF (B AND #LEFT) > 0 THEN
      IF D_CHECK_COLLISION(POS, #LEFT) == 1 THEN
        D_DIRECTION_MOVE SP_PLAYER, #LEFT
      ENDIF
    ELSEIF (B AND #RIGHT) > 0 THEN
      IF D_CHECK_COLLISION(POS, #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)%SZ == 0 && (SPVAR SPRITE_NO, 1)%SZ == 0 THEN
    SPVAR SPRITE_NO, 2, 0
  ENDIF
END

# ドット座標をグリッド座標に変換
DEF D_GET_GRID_XY(A_X_OR_Y)
  RETURN A_X_OR_Y / SZ
END

# グリッド座標で現在のマップ配列の添え字ナンバーを知る
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 PLUS_LAYER% = (MW% * MH%) * A_LAYER

  IF A_Y <= 0 THEN
    RETURN_POS = GRID_X + PLUS_LAYER
  ELSE
    RETURN_POS = (GRID_X + (GRIX_Y * MW)) + PLUS_LAYER
  ENDIF

  RETURN RETURN_POS
END

# 現在地の先を調べて移動可能かどうかを調べる(※衝突判定関数)
DEF D_CHECK_COLLISION(A_POS, A_DIRECTION)
  VAR DIRECT = 0
  VAR SUM_POS = 0

  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
    RETURN 1
  ELSE
    RETURN 0
  ENDIF
END