【プチコン4講座】ステージに衝突判定を実装して完成させよう

プチコン4

プチコン4 ゲーム完成

こんにちは。なおキーヌです。

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

スプライトの学習兼簡易アクションゲーム作りは今回で完成に向かいます。

これ元に様々なジャンルのミニゲームに分岐していこうかなと思いました。

というのもこのままダラダラとスプライト講座をやるとまた長い講座になってしまうので、
一旦ここでスプライトの基礎を使ったミニゲーム作りを〆ます。

それではプチコンミニゲーム講座第9回目を始めようと思います。

  1. ステージとキャラクターの衝突判定をしてみよう
  2. ステージの壁にぶつかったら移動させないようにする
  3. 敵も壁にぶつからせてみよう
  4. ゴール地点にたどり着いたらゲームクリアにしよう
  5. 画像を使ったゲームの基礎はこれで完了

ステージとキャラクターの衝突判定をしてみよう

前回のステージ構築時にSPCOLはやっているので、あとはプレイヤーと壁の衝突判定を取るだけですね。

衝突判定はスプライト同士でやるのは変わらないのですが、壁1つ1つと衝突判定を書いていては
コードも長くなってしまうのでしんどいですよね。

実はプチコン4にはまとめて衝突判定を取ってくれる便利な関数があります。

SPHITSP(スプライト番号[,先頭ID,末尾ID])

これを使えば先頭ID-末尾IDのスプライトにプレイヤーがぶつかった場合
そのぶつかったスプライトのIDを返してくれます。

スプライトを作るときにIDに予備を持たせていたのもこれが理由です。

このSPHITSPは連番じゃないとまとめて判定がとれないのでIDをな連番にしてやる必要があったんですね。

そして前回、コードのミスがあったので直しておきましょう。

壁にだけSPCOLをしていましたが、連番だと壁以外も衝突判定をみるようになっているので
SPCOLをしておかないとエラーが発生してしまいます。

以下のように修正してください。

' マップ用スプライトの準備
FOR G_J=0 to 5
  FOR G_I=0 to 5
    ' 数字の連番を作る
    VAR NUMBER = G_I+(G_J*6) + 200
    ' マップ配列の中身をみてスプライトの見た目を決める
    IF MAPDATA[NUMBER-200] == 1 THEN
      ' マップデータが1なら丸太を表示
      SPSET NUMBER, 1244
    ELSE 
      ' マップデータが0なら見た目を透明にする
      SPSET NUMBER, 0,0
    ENDIF

    ' 衝突判定を準備しておく
    SPCOL NUMBER

    ' ループに合わせてスプライトの位置を決める
    SPOFS NUMBER, G_I*16, G_J*16
  NEXT
NEXT

これでSPHITSPが連番で使えるようになりました。

ステージの壁にぶつかったら移動させないようにする

それでは実際に衝突判定を使って壁にぶつからせてみましょう。

コツはプレイヤーが移動した先がぶつかるかどうかです。

' ぶつかったスプライトID保持変数
VAR HITSP = -1 

' コントローラー関数
'───────────────────────────────
def D_Controller
  ' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
  VAR B = BUTTON(0)

  ' どのボタンでぶつかったかフラグ
  VAR D = 0


  ' 上ボタン処理
  if (B AND 1 << #B_LUP) != 0 then
    DEC Y, 2
    D = 1
  ' 下ボタン処理
  elseif (B AND 1 << #B_LDOWN) != 0 then
    INC Y, 2
    D = 2
  ' 左ボタン処理
  elseif (B AND 1 << #B_LLEFT) != 0 then
    DEC X, 2
    D = 3
  ' 右ボタン処理
  elseif (B AND 1 << #B_LRIGHT) != 0 then
    INC X, 2
    D = 4
  endif

  ' プレイヤーを移動させてみる
  SPOFS 0,X,Y

  ' 移動先がぶつかったか調べる
  HITSP = SPHITSP(0,200,235)
  ' ステージのどこかにぶつかっているか かつ スプライトが表示されているか
  IF HITSP != -1 && SPSHOW(HITSP) == #TRUE THEN
    CASE D
      WHEN 1:
        INC Y,2
      WHEN 2:
        DEC Y,2
      WHEN 3:
        INC X,2
      WHEN 4:
        DEC X,2
    ENDCASE
    ' プレイヤーを移動を補正する
    SPOFS 0,X,Y
  ENDIF

あまり褒められたやり方ではないのですが、衝突判定の基本です。

プチコン4の衝突判定はスプライト同士がぶつかっていないと判定してくれません。

なのでまず押されたキーで座標が加算された時に、
どの方向に動かしたかを記憶しておきます。

そして一度SPOFSで移動させてみます。

この時点ではまだ1フレーム経過がしていないので実際には動いて見えません。

1フレームの最後になってやっと画面に反映されるので内部的には
プレイヤーは押した方向に移動していますが画面では反映されていないのです。

移動した時点で壁にぶつかっていれば衝突判定を取れるので
もしぶつかっていたら押された方向分加減算された値を元に戻してやり、
再びSPOFSを使って動く前の位置に戻してやることで壁にめり込まないという仕組みです。

注意点として、プレイヤーのSPOFSはループのところにありましたが
そこからは消してコントローラー関数の中にいれているので注意してください。

頭の良い人ならもっときれいなやり方ができるかもしれませんが、
あいにく私の脳はそこまで優れていないのでこのゴリ押し手法でいっています(笑)

綺麗な書き方をしようが、ゴリ押ししようが最終結果が同じであれば大丈夫です。

答えが一つじゃないのがプログラミングです。

共同開発では怒られるかもしれませんが、個人開発なら今は動けばいいでしょう。

ちなみにぶつかった条件式のもう1つ条件式がある「SPSHOW」は表示されているか否かを判定します。

ステージのスプライトを生成するときに壁以外は最後に0を付けていたのを思い出してください。

あれはスプライト表示フラグを決めています。

何も指定しないと表示=1になるので、壁は表示・壁以外は非表示となっています。

敵も壁にぶつからせてみよう

本当は衝突判定を関数化したほうがいいのですが、
関数の作り方などを解説すると長くなるので一旦コピペで流用しましょう。

実はプログラムのコーディングとしては同じことを書くのはナンセンスで
プログラミング警察がシュバババと駆け寄ってくるかもしれません。

しかしんなこたぁどうでもいいんです。

綺麗なコードを書いているんじゃなくて動くコードを書いているので
コピペでも動けばいいです。

綺麗にするのは後からでも問題ありません。

その場でしか使わないコードを綺麗にしても後で使わなかったらただの時間の無駄ですからね。

まぁ、衝突判定は今後も使うのですが(笑)

エネミーの移動はループの中に書いてあるので、そこに衝突判定を盛り込んでみましょう。

VAR E_HITSP = -1
'----------------------------------
' ループ開始
LOOP

  ' コントローラー関数
  D_CONTROLLER

  ' 敵の向きの方向へ進ませる
  VAR E = 0
  IF E_DIRECT == 0 THEN
      DEC EX,1
      E = 1
  ELSE
      INC EX,1
      E = 2
  ENDIF

  ' 敵を指定座標へ動かす
  SPOFS 1, EX,EY

  ' 敵と壁の衝突判定をする
  E_HITSP = SPHITSP(1,200,235)
  ' ステージのどこかにぶつかっているか かつ スプライトが表示されているか
  IF E_HITSP != -1 && SPSHOW(E_HITSP) == #TRUE THEN
    CASE E
      WHEN 1:
        INC EX,1
        E_DIRECT = 1
      WHEN 2:
        DEC EX,1
        E_DIRECT = 0
    ENDCASE
    ' プレイヤーを移動を補正する
    SPOFS 1, EX,EY
  ENDIF

  ' プレイヤーと敵の衝突判定チェック
  IS_HIT = SPHITSP(0,1)

  ' 衝突していたらプレイヤーを初期値に戻す
  IF IS_HIT == #TRUE THEN
    X = #CS
    Y = #CS
    SPOFS 0,X,Y
  ENDIF

  ' 垂直同期
  VSYNC

ENDLOOP

今までは画面端でぶつかったら向きを変えて往復していましたが、
衝突判定を使って壁にぶつかったら反転するという仕組みにしました。

そして今までは敵にぶつかっていたらゲームを終了するようにしていましたが、
毎回実行を押さなければいけないのでぶつかったらプレイヤーを初期値に戻すようにしました。

これで何度もクリアできるまで挑めますね。

実際にプレイしてみてください。

ここまでコードをまばらに書いているのでどこにコードを埋め込んだらいいのかわからないよ!

って人はココまでのすべてのコードを記事の一番最後最後に載せていますので、
自分のコードのどこがまちがっているかを照らし合わせてみてください。

作ったゲームがムリゲーなのでバランス調整に挑戦してみよう

ゲームを実行してみて、右側の隙間にプレイヤーを移動させようとしてみてください。

ゴール設定はしていませんがそこがゴールです。

……どうでしょうか?

まずクリア出来ないでしょう。

私もプレイしてみましたが無理ですこれは。

恐らく1F単位で動かしてもクリア出来ないと思います。

なぜかというと、衝突判定が1ドットでも触れたらミス扱いになっているからです。

このままではクソゲーどころかムリゲーですよね。

ゲームとして成立していませんので改善しなければいけません。

いわゆるバランス調整ですね。

RPGを作ったときに敵が強すぎるから調整する。

それと同じことをします。

ですが私は完全なる答えをここでは書きません。

一応最後という事で、これは課題にしようと思います。

ココまで出来ればあとは学んできたことを応用するとクリアできるようなゲームに仕上げることが出来ます。

手段は問わないので、自分が納得いくゲームに仕上げてみてください。

一応コードは書きませんが改善の方法のヒント(ほぼネタバレ)の一例を置いておきます。

  • マップの大きさを変更してしまう
  • 衝突判定の大きさを変更してみる
  • 敵の動きを変更してみる
  • 隠し通路を作ってみる
  • 敵を倒してみる

プログラミングは答えが一つではありません。

ゲームプログラミングにおいても同じです。

ですが、1つだけ守ってほしいことがあります。

それはクソゲーにしないことです。

例えばこれをクソゲーにする場合、ワンボタンを押したらクリア位置に飛ぶという仕組みを作ってしまうことですね。

それをしてしまうとゲームとして成立していないのでクソゲー、いやゴミゲーです。

上の例でいうと隠し通路がそれに近いですかね。

でも何かのボタンを押して隠し通路を見つけることが出来る

としたら面白いと思いませんか?

ということで面白さを兼ね備えながらゲームをクリアできるようにしてみましょう。

画像を使ったゲームの基礎はこれで完了

これでスプライトを使った簡単なアクションゲームが完成しました。

ここから難易度の高いアクションゲームや、
シューティング要素を加えたロックマンみたいなアクションゲームにしても面白そうですね。

ここからアクションシューティングにするか、純粋なシューティングゲームにするか
RPGにするかパズルゲームにするかは自由です。

私もミニゲーム講座としてココまでの講座をやってくれた人向けに
ジャンル別のミニゲームの作り方を書いていこうと思います。

1日1記事なのでペースは遅いかもしれませんが、みてくださるとうれしいです。

最後に完成版のコードを載せておくので、動かない人は照らし合わせてみてください。

それでは次の講座でお会いしましょう。

' 画面クリア
ACLS

' チップサイズ
CONST #CS = 16
' 座標用変数
VAR X=#CS*1, Y=#CS*1, EX=#CS*4, EY=#CS*4
' 向き変数 
VAR E_DIRECT = 0
' ボタン用変数
X_BTN = 0, B_BTN = 0, Y_BTN = 0, A_BTN = 0 
' ループ用変数
VAR G_I = 0, G_J = 0
' プレイヤーの衝突判定フラグ
VAR IS_HIT = 0
' プレイヤーがぶつかったスプライトID保持変数
VAR HITSP = -1 
' 敵がぶつかったスプライトID保持変数
VAR E_HITSP = -1 

' マップデータ配列
DIM MAPDATA[] = [\
  1,1,1,1,1,1,\
  1,0,0,1,0,1,\
  1,0,0,1,0,1,\
  1,0,0,1,0,1,\
  1,0,0,0,0,1,\
  1,1,1,1,1,1\
]

' プレイヤーと敵のスプライト準備
SPSET 0, 500
SPSET 1, 1000

' プレイヤーと敵のスプライト衝突判定の準備
SPCOL 0
SPCOL 1


' マップ用スプライトの準備
FOR G_J=0 to 5
  FOR G_I=0 to 5
    ' 数字の連番を作る
    VAR NUMBER = G_I+(G_J*6) + 200
    ' マップ配列の中身をみてスプライトの見た目を決める
    IF MAPDATA[NUMBER-200] == 1 THEN
      ' マップデータが1なら丸太を表示
      SPSET NUMBER, 1244
    ELSE 
      ' マップデータが0なら見た目を透明にする
      SPSET NUMBER, 0,0
    ENDIF

    ' 衝突判定を準備しておく
    SPCOL NUMBER

    ' ループに合わせてスプライトの位置を決める
    SPOFS NUMBER, G_I*16, G_J*16
  NEXT
NEXT

'----------------------------------
' ループ開始
LOOP

  ' コントローラー関数
  D_CONTROLLER

  ' 敵の向きの方向へ進ませる
  VAR E = 0
  IF E_DIRECT == 0 THEN
      DEC EX,1
      E = 1
  ELSE
      INC EX,1
      E = 2
  ENDIF

  ' 敵を指定座標へ動かす
  SPOFS 1, EX,EY

  ' 敵と壁の衝突判定をする
  E_HITSP = SPHITSP(1,200,235)
  ' ステージのどこかにぶつかっているか かつ スプライトが表示されているか
  IF E_HITSP != -1 && SPSHOW(E_HITSP) == #TRUE THEN
    CASE E
      WHEN 1:
        INC EX,1
        E_DIRECT = 1
      WHEN 2:
        DEC EX,1
        E_DIRECT = 0
    ENDCASE
    ' プレイヤーを移動を補正する
    SPOFS 1, EX,EY
  ENDIF

  ' プレイヤーと敵の衝突判定チェック
  IS_HIT = SPHITSP(0,1)

  ' 衝突していたらプレイヤーを初期値に戻す
  IF IS_HIT == #TRUE THEN
    X = #CS
    Y = #CS
    SPOFS 0,X,Y
  ENDIF

  ' 垂直同期
  VSYNC

ENDLOOP

' ループ終了
'----------------------------------


' コントローラー関数
'───────────────────────────────
def D_Controller
  ' 0番目のコントローラー(つまり1コン)の押されているボタンを取得
  VAR B = BUTTON(0)

  ' どのボタンでぶつかったかフラグ
  VAR D = 0


  ' 上ボタン処理
  if (B AND 1 << #B_LUP) != 0 then
    DEC Y, 2
    D = 1
  ' 下ボタン処理
  elseif (B AND 1 << #B_LDOWN) != 0 then
    INC Y, 2
    D = 2
  ' 左ボタン処理
  elseif (B AND 1 << #B_LLEFT) != 0 then
    DEC X, 2
    D = 3
  ' 右ボタン処理
  elseif (B AND 1 << #B_LRIGHT) != 0 then
    INC X, 2
    D = 4
  endif

  ' プレイヤーを移動させてみる
  SPOFS 0,X,Y

  ' 移動先がぶつかったか調べる
  HITSP = SPHITSP(0,200,235)
  ' ステージのどこかにぶつかっているか かつ スプライトが表示されているか
  IF HITSP != -1 && SPSHOW(HITSP) == #TRUE THEN
    CASE D
      WHEN 1:
        INC Y,2
      WHEN 2:
        DEC Y,2
      WHEN 3:
        INC X,2
      WHEN 4:
        DEC X,2
    ENDCASE
    ' プレイヤーを移動を補正する
    SPOFS 0,X,Y
  ENDIF


  ' Xボタン処理
  if (B AND 1 << #B_RUP) != 0 then
    ' ボタンカウント命令に#B_RUPを渡す
    D_BtnPressCount #B_RUP
  else
    ' ボタンが離されたらカウントが1以上の時
    if X_BTN >= 1 then
      ' ボタンカウントを0にする
      X_BTN = 0
    endif
  endif

  ' Bボタン処理
  if (B AND 1 << #B_RDOWN) != 0 then
    ' ボタンカウント命令に#B_RDOWNを渡す
    D_BtnPressCount #B_RDOWN
  else
    ' ボタンが離されたらカウントが1以上の時
    if B_BTN >= 1 then
      ' ボタンカウントを0にする
      B_BTN = 0
    endif
  endif

  ' Yボタン処理
  if (B AND 1 << #B_RLEFT) != 0 then
    ' ボタンカウント命令に#B_RLEFTを渡す
    D_BtnPressCount #B_RLEFT
  else
    ' ボタンが離されたらカウントが1以上の時
    if Y_BTN >= 1 then
      ' ボタンカウントを0にする
      Y_BTN = 0
    endif
  endif


  ' Aボタン処理
  if (B AND 1 << #B_RRIGHT) != 0 then
    ' ボタンカウント命令に#B_RRIGHTを渡す
    D_BtnPressCount #B_RRIGHT
  else
    ' ボタンが離されたらカウントが1以上の時
    if A_BTN >= 1 then
      ' ボタンカウントを0にする
      A_BTN = 0
    endif
  endif

end


' ボタンカウント関数
'───────────────────────────────
def D_BtnPressCount A_Button

  ' ボタンカウント分岐
  case A_Button
    ' 渡されたボタンがXだったら
    when #B_RUP
      ' ボタンカウント配列の数が256未満かどうか調べる
      if X_BTN < 256 then 
        ' 条件に一致したら+1する
        INC X_BTN
      endif

    ' 渡されたボタンがBだったら
    when #B_RDOWN
      ' ボタンカウント配列の数が256未満かどうか調べる
      if B_BTN < 256 then 
        ' 条件に一致したら+1する
        INC B_BTN
      endif

    ' 渡されたボタンがYだったら
    when #B_RLEFT
      ' ボタンカウント配列の数が256未満かどうか調べる
      if Y_BTN < 256 then 
        ' 条件に一致したら+1する
        INC Y_BTN
      endif

    ' 渡されたボタンがAだったら
    when #B_RRIGHT
      ' ボタンカウント配列の数が256未満かどうか調べる
      if A_BTN < 256 then 
        ' 条件に一致したら+1する
        INC A_BTN
      endif
  endcase
end