【プチコン4講座】RPGを作るためのグリッド衝突判定

プチコン4

プチコン 衝突判定

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

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

前回「【プチコン4講座】RPGを作るためのグリッド移動の実装」でグリッド移動を実装しました。

ドット移動の時もそうでしたが、グリッド移動を実装しても壁を突き抜けてしまいます。

マップ移動をする際に当たり判定……つまり衝突判定というものを実装し、いわゆる壁を作る必要があるので
今回は移動する時に壁を判定する処理を作っていきます。

プチコンBIGの方では先に衝突判定の考え方を学びましたが、3から4になっても考え方は共通しているので省略します。

今回のことがよくわからなかった場合は

【プチコン講座】グリッド移動の衝突判定について考えてみよう

を参照してからもう一度今回の記事を試してみてください。

理解度が高まれば習得率がグンと変わります。

それではプチコン4でRPG作りその5回目始めましょう。

  1. 現在の座標からグリッド座標に変換する関数を作ろう
  2. マップ座標にアクセスするための関数を作ろう
  3. グリッド移動を実装しよう
  4. 作った関数を使って衝突判定行ってみよう

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

グリッド移動の場合のゲームではキャラクターの現在地を知るにはグリッドで知るのが都合が良いです。

ドットの座標で言うと人間的には理解し辛いです。

グリッドにしておけばそこからドット座標にも戻すことが出来ますので、
基本的にはグリッド座標にしておいた方イベントの管理などもしやすくなります。

それではドット座標からグリッド座標に変換する関数を作りましょう。

def D_GetGridXY(A_XY)
  return A_XY / #CSZ
end

ドット絵座標をチップサイズで割るだけです。

簡単ですね。

しかしこのままでは使いづらいのでグリッド変換関数を使って、
マップ配列のどの添え字に居るかを調べます。

多次元配列であれば、変換関数を使ってY行X列といった風にそれぞれの添え字に渡せばマップの座標にアクセスできますが、
そうすると処理負荷がとてつもないことになります。

1次元配列であれば添え字は1つしか与えられないためパッと見はマップのどこにいるのかはわかり辛くなります。

しかし変換関数さえ作っておけば容易にアクセスできるので、多次元配列を使う理由はなくなります。

他にもメリットがあって、レイヤー管理というものが容易になります。

本講座の最初にもマップのレイヤーを作ったことを思い出してください。

マップの大きさはどのレイヤーも同じでした。

なのでマップの大きさ分の数値をプラスするだけでそのレイヤーにアクセスすることが出来ます。

多次元配列仕様にすると、頭の回転の速い人でないと扱い辛くなってくるので個人的にはマップは1次元配列での管理をオススメします。

マップ座標にアクセスするための関数を作ろう

グリッド座標に変換する関数を作ったので次はマップ座標にアクセスできるようにしてみましょう。

作った関数から得た値と調べたいレイヤーのナンバーを与えると、
マップ配列の添え字を返す関数にします。

def D_GetMapPosition(A_X, A_Y, A_Layer)
  var gridX = D_GetGridXY(A_X)
  var gridY = D_GetGridXY(A_Y)

  var layerNo = (G_BGW * G_BGH) * A_Layer

  if A_Y <= 0 then
    return gridX + layerNo
  else
    return gridX +  (gridY * G_BGW) + layerNo
  endif
end

これでマップの好きな所にアクセスできるようになりました。

移動する前にこの関数を使ってそこに何があるのかを知れば衝突の判定をチェックできます。

では実際に作った関数を使ってみましょう。

グリッド移動を実装しよう

それではドット移動からグリッド移動に変更しましょう。

やることはキー操作をした時にdirect変数に1〜4の値を渡したと思います。

その値によって移動をきりわけていましたね。

ドット移動の場合は毎フレーム移動判定をしていましたが、
グリッド移動の場合は一回移動が発動したら次のマスまで動き続けるという仕組みになっています。

なのでなので以下のフラグが必要になります。

  • 移動中かどうか
  • どっちの方向を向いているか

グローバル変数でもいいですが、せっかくなのでスプライト変数を使いましょう。

それでは早速作っていきます。

def D_CheckCollision(A_Pos, A_Direct)
  var direct = 0
  var sumPos = 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

  sumPos = A_Pos + direct

  if G_MapData[sumPos] == 0 then
    return #TRUE
  endif
  return #FALSE
end

作った関数から得られるグリッド座標とキャラクターの向きを渡せばその先の方向にあるものを調べられます。

しかしこのままではマップ配列の添え字0に居るときに左か上を調べようとすると、
マップ配列の範囲外にアクセスすることになります。

そうするとエラーが返ってくるので最悪プログラムが止まってしまいます。

本来なら範囲外を調べてしまうかどうかを調べる関数もセットにしなければいけませんが、
1画面RPGは周囲に森を配置しているので仕組み上配列範囲外にアクセスすることはありませんので今回はチェック無しで行きます。

プチコンBIGではマップエディタを使って生成したマップデータファイルだったので、
座標を調べるとマップチップのナンバーが返ってきていたのですが今回は自作でマップを作っていますので
返ってくるのは今のところ0か1です。

0が通れる地面で1が木々でしたね。

なので0の場合は通れるという1を返して1であれば通れない0を返します。

作った関数を使って衝突判定行ってみよう

衝突判定を行うための関数がそろいました。

実際に使って木々は通れなくて地面は通れるという風に改造してみましょう。

' スプライトの移動
def D_SpriteGridMove A_Direct

  if SPVAR(0,"MoveFlag") == 1 then 
    case SPVAR(0,"Direct")
    when #B_RUP
      SPVAR 0, "Y", SPVAR(0, "Y") - 1
    when #B_RDOWN
      SPVAR 0, "Y", SPVAR(0, "Y") + 1
    when #B_RLEFT
      SPVAR 0, "X", SPVAR(0, "X") - 1
    when #B_RRIGHT
      SPVAR 0, "X", SPVAR(0, "X") + 1
    endcase

    ' キャラクターの移動
    SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")

    ' 割った余りが0になったら動きを止める
    if SPVAR(0, "X") mod #CSZ == 0 && SPVAR(0, "Y") mod #CSZ == 0 then
      SPVAR 0, "MoveFlag", 0
    endif
  else
    if A_Direct >= 0 then
      var pos = D_GetMapPosition(SPVAR(0, "X"), SPVAR(0, "Y"), 0)
      SPVAR 0, "Direct", A_ Direct
      if D_CheckCollision(pos, SPVAR(0, "Direct")) == #TRUE then
        SPVAR 0, "MoveFlag", 1
      endif
    endif
  endif
end

これで通れない場所の場合は移動フラグをONにせず動かさないようにできました。

一度プレイしてみてちゃんと地面は通れて木々は通れない状態になっているか確認しましょう。

──プチコンBIGの時はコントローラー関数に直接書いて言いましたが、
スプライト移動用の関数に切り分けていますのでそちらにまとめています。

段々とコードが長くなっていくのでプチコン4での講座では勉強も兼ねて、
途中でコードを別ファイルに切り分けていこうと思います。

最後に今回の完成版ソースコードを置いておきます。

それでは

'──────────────────────────
' ▼ 動作モード設定:変数宣言は必須
'──────────────────────────
OPTION STRICT
'──────────────────────────
' ▼ 画面のクリア
'──────────────────────────
ACLS
'──────────────────────────
' ▼ 定数・構造体の定義
'──────────────────────────
CONST #CSZ = 16
'──────────────────────────
' ▼ 変数・配列の定義
'──────────────────────────
' ┠─ 汎用変数
'━━━━━━━━━━━━━━━━━━━━━━━━━━
var G_I = 0, G_J = 0, ¥
G_BGW = 0, G_BGH = 0
' ゲームループを制御するフラグ
var G_GameLoopFlag = #TRUE
'──────────────────────────
' ┠─ マップシーン用配列
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' 空配列宣言
DIM G_MapData[]

'──────────────────────────
' ▼ プログラム初期化処理
'──────────────────────────
D_GAME_INITIALIZE
'──────────────────────────
' ゲームループ開始
'──────────────────────────
loop
  ' コントローラー関数呼び出し
  D_Controller

  ' ゲームループフラグ監視
  if !G_GameLoopFlag then BREAK
  ' フレームレート安定
  VSYNC 1
endloop

'──────────────────────────
' ▼ 関数の定義
'──────────────────────────
' ┠─ ▼ 初期化関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
DEF D_GameInitialize
  ' マップデータのロード
  D_MapInitialize "MAP001.DAT"
  ' マップデータの描写
  'D_MapDraw
  ' スプライトの初期化
  D_SpInitialize
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) > 0 then
    direct = #B_RUP
  endif
  if  (B AND BTN_DOWN) > 0 then
    direct = #B_RDOWN
  endif
  if  (B AND BTN_LEFT) > 0 then
    direct = #B_RLEFT
  endif
  if  (B AND BTN_RIGHT) > 0 then
    direct = #B_RRIGHT
  endif

  D_SpriteGridMove direct

end

'──────────────────────────
' ┠─ ▼ スプライト関連
'━━━━━━━━━━━━━━━━━━━━━━━━━━
' スプライト初期化
def D_SpInitialize
  ' プレイヤーキャラクターの描写と初期化【追加部分】
  SPSET 0, 500
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0
  SPVAR 0, "Direct", 0
  SPVAR 0, "MoveFlag", 0
  SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
end

' スプライトの移動
' スプライトの移動
def D_SpriteGridMove A_Direct

  if SPVAR(0,"MoveFlag") == 1 then 
    case SPVAR(0,"Direct")
    when #B_RUP
      SPVAR 0, "Y", SPVAR(0, "Y") - 1
    when #B_RDOWN
      SPVAR 0, "Y", SPVAR(0, "Y") + 1
    when #B_RLEFT
      SPVAR 0, "X", SPVAR(0, "X") - 1
    when #B_RRIGHT
      SPVAR 0, "X", SPVAR(0, "X") + 1
    endcase

    ' キャラクターの移動
    SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")

    ' 割った余りが0になったら動きを止める
    if SPVAR(0, "X") mod #CSZ == 0 && SPVAR(0, "Y") mod #CSZ == 0 then
      SPVAR 0, "MoveFlag", 0
    endif
  else
    if A_Direct >= 0 then
      var pos = D_GetMapPosition(SPVAR(0, "X"), SPVAR(0, "Y"), 0)
      SPVAR 0, "Direct", A_ Direct
      if D_CheckCollision(pos, SPVAR(0, "Direct")) == #TRUE then
        SPVAR 0, "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 gridY = D_GetGridXY(A_Y)

  var layerNo = (G_BGW * G_BGH) * A_Layer

  if A_Y <= 0 then
    return gridX + layerNo
  else
    return gridX +  (gridY * G_BGW) + layerNo
  endif
end

' 進む先のグリッドが移動可能か同化を調べる
def D_CheckCollision(A_Pos, A_Direct)
  var direct = 0
  var sumPos = 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

  sumPos = A_Pos + direct

  if G_MapData[sumPos] == 0 then
    return #TRUE
  endif
  return #FALSE
end