【プチコン4講座】ステージを作るための壁を実装しよう

プチコン4

プチコン4 パズルゲーム 壁を実装

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

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

前回は衝突判定をつかって倉庫番ライクゲームの基本的な仕組みは作り上げることが出来ました。

今回は衝突判定の復習も兼ねて、パズルゲームのステージを構成する1つとなる壁を実装しましょう。

マップはテキストスクリーンで作ると言いましたが、
壁は衝突判定を取るためにスプライトの方が都合がいいと判断したのでスプライトで実装します。

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

  1. 壁のスプライトの準備と衝突判定の設定
  2. プレイヤーと壁の衝突判定をとる
  3. 壺と壁の衝突判定を取る
  4. 次回に向けてステージの形を作ってみよう

壁のスプライトの準備と衝突判定の設定

まずは壁のスプライトを用意しましょう。

コチラも壺やクリアマークのように連番の方が衝突判定を取るために都合が良さそうです。

まずは簡単に壁用のスプライトを定義してみましょう。

' 壁用スプライト
FOR G_I = 51 TO OBJ+100
  SPSET G_I, 1469
  SPVAR G_I, "X", G_I * #CS
  SPVAR G_I, "Y", 0
  SPCOL G_I
  SPOFS 0, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
NEXT

壺とかとほとんど同じコードですね。

ちょっと考えたらこれらを関数化できると思うのですが、
今は関数化せずそのまま進めます。

今回作ったゲームの関数化についてはゲーム完成後におまけとしてやります。

プログラミング能力を上げたい方だけ見るくださいという感じでいきます。

プレイヤーと壁の衝突判定をとる

壁が用意できたので、プレイヤーと壁の衝突判定を取ってみましょう。

やることは正直前回より簡単です。

単純にプレイヤーの移動に合わせて壁にぶつかっているかどうかのチェックをして
ぶつかっていれば動かないし、ぶつかっていなければそのまま移動を適用する。

これだけですね。

とりあえず壺の判定の前に壁の判定をやってみます。


  ' プレイヤーを仮移動させる
  SPOFS 0, SPVAR(0,"X")+ADD_POS[0], SPVAR(0,"Y")+ADD_POS[1]

  ' プレイヤーと壁の判定をする
  HIT_OBJ = SPHITSP(0,51,100)

  ' 壁の衝突判定をとる
  IF (HIT_OBJ != -1)
    BEEP 52
    ' 壁に当たっていればプレイヤーの座標を元に戻す
    SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
  ELSE
    ' 壺の判定処理(省略
  ENDIF

結構省略していますが、やることはこれだけです。

説明するとまずプレイヤーを仮移動させるのは前回と同じですね。

次に壺の判定を取る前にまず壁との判定をとります。

衝突判定が取れなかった時のSPHITSPは「-1」を返すので判定が楽ですね。

今回作っているゲームの仕組みでは、壁にぶつかっていたら壺にぶつかることはありえないので
壁の判定を取ってぶつかっていたら、もう壺の判定を取る必要は無くなりますね。

壺と壁の衝突判定を取る

ココからが本番です。

プレイヤーと壁の判定を取ったら次は壺とプレイヤーの判定にうつるわけですが、
動かした壺が他の壺との判定を取る前に、まず壁との判定を取るようにしてみましょう。

そしてその後に通常通り壺との判定をすれば完璧ですね。

それでは壺と壁の判定を作りましょう。

分かりやすいように先ほどのプレイヤー:壁判定も組み込んでみます。


  ' プレイヤーを仮移動させる
  SPOFS 0, SPVAR(0,"X")+ADD_POS[0], SPVAR(0,"Y")+ADD_POS[1]

  ' プレイヤーと壁の判定をする
  HIT_OBJ = SPHITSP(0,51,100)

  ' 壁の衝突判定をとる
  IF (HIT_OBJ != -1) THEN
    BEEP 52
    ' 壁に当たっていればプレイヤーの座標を元に戻す
    SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
  ELSE
    ' プレイヤーと壁があたっていなければ壺の衝突判定を取る
    HIT_OBJ = SPHITSP(0,1,10)

    ' 壺に当たっていて居るかチェック
    IF HIT_OBJ != -1 THEN
      ' 壺を仮移動させる
      SPOFS HIT_OBJ, SPVAR(HIT_OBJ,"X")+ADD_POS[0], SPVAR(HIT_OBJ,"Y")+ADD_POS[1]

      '壺を仮移動させた先は壁かどうか
      IF SPHITSP(HIT_OBJ,51,100) != -1 THEN
        BEEP 52
        ' ぶつかってたら動かせないはずなのでプレイヤーと壺の座標を戻す
        SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
        SPOFS HIT_OBJ, SPVAR(HIT_OBJ,"X"), SPVAR(HIT_OBJ,"Y")
      ELSE
        ' 壺を仮移動させた先は壺かどうか
        IF SPHITSP(HIT_OBJ,1,10) != -1 THEN
          BEEP 52
          ' ぶつかってたら動かせないはずなのでプレイヤーと壺の座標を戻す
          SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
          SPOFS HIT_OBJ, SPVAR(HIT_OBJ,"X"), SPVAR(HIT_OBJ,"Y")
        ELSE
          BEEP 100
          ' プレイヤーも壺も移動できたので座標変数の変更を確定
          SPVAR 0, "X", SPVAR(0,"X")+ADD_POS[0]
          SPVAR 0, "Y", SPVAR(0,"X")+ADD_POS[1]
          SPVAR HIT_OBJ, "X", SPVAR(HIT_OBJ,"X")+ADD_POS[0]
          SPVAR HIT_OBJ, "Y", SPVAR(HIT_OBJ,"Y")+ADD_POS[1]
        ENDIF
      ENDIF
    ELSE
      ' 壺に当たっていなければプレイヤーは普通に移動するので座標変数を確定
      SPVAR 0, "X", SPVAR(0,"X")+ADD_POS[0]
      SPVAR 0, "Y", SPVAR(0,"Y")+ADD_POS[1]
    ENDIF
  ENDIF

ちょっと複雑になってきましたが、やっていることは結構単純です。

処理を箇条書きにしてなにをしているか再認識してみましょう。

  • プレイヤーを仮移動させる
  • プレイヤーが壁に当たっているか調べる
  • 壁に当たっていればプレイヤーの移動を停止:処理終了
  • 壁に当たっていなければプレイヤーが壺に当たっているか調べる
  • 壺に当たっていなければプレイヤーだけ移動:処理終了
  • 壺に当たっていれば壺をプレイヤーと同じ方向に仮移動させる
  • 壺が動いた先が壁であればプレイヤーも壺も移動停止:処理終了
  • 壺が壁にぶつかっていなければ他壺に当たっているか調べる
  • 動いた壺が他壺とぶつかっていればプレイヤーも壺も移動停止:処理終了
  • 動いた壺が他壺とぶつかっていなければプレイヤーも壺も移動させる:処理終了

こんな感じの処理です。

プログラミングに慣れている人の場合変に日本語にすると、
ゲシュタルト崩壊を起こして見辛く感じます。

次回に向けてステージの形を作ってみよう

今回折角壁を用意したのでステージらしくととのえてみましょう。

一応51-100が壁のスプライトなのですが、横10x縦10の壁を使っても
38スプライト(空間マス9×9)を使うだけなので、でっぱりの壁とか作ってもあまりそうです。

とりあえず最初のステージでそんなに広くてもあんまり意味なさそうです。

GB版の倉庫番ステージ1をみてみると……いきなり39個も壁を使ってました。

最悪、壁スプライト番号は51-200とかにすればよさそうですね。

もし障害物とかを実装するときはクリアマークの後ろにおけばよさそうです。

新たなスプライトを作るときは余裕をもって201番目以降とかにしましょう。

こんな感じにスプライト番号は余裕を持たせておくと後から焦らなくて安心です。

それでは真四角部屋だと味気ないので、
練習として倉庫番GBと同じ形のステージ1を作ってみたいと思います。

' ステージの左上座標
CONST STAGE_ORIGIN_X = 0 * #CS
CONST STAGE_ORIGIN_Y = 0 * #CS

' マップデータ
DIM MAPDATA[] = [\
  1,1,1,1,1,0,0,0,0,\
  1,0,0,0,1,0,0,0,0,\
  1,0,2,2,1,0,1,1,1,\
  1,0,2,0,1.0.1.3.1,\
  1,1,1,0,1,1,1,3,1,\
  0,1,1,0,0,0,0,3,1,\
  0,1,0,0,0,1,0,0,1,\
  0,1,0,0,0,1,1,1,1,\
  0,1,1,1,1,1,0,0,0\
]

まずマップ描写の開始位置の基準点定数を作りました。

この定数を使うことで画面の左上の地点を決めてマップ描写をずらすことが可能です。

ゲームデザインによってマップの描写位置を変更できるようにって感じです。

左上のままでもよければ特に変更なくてもOKです。

いじることによってのゲームプレイには支障はありません。

次に、マップデータは基本的に配列にして数値化しておくのが基本になります。

ココではマップ作製の概念については省略させてもらいます。

もしこのマップの作り方について詳しく知りたい方は

上記の記事を参考にしていただけると助かります。

この配列の数値をよく見てもらうとマップぽくなっていませんか?

0が歩ける床で、1が壁、2が壺で3がクリアマーク。

って感じにしました。

この数値は自分の好みで問題ありません。

分からないって人は合わせてもらえると大丈夫です。

この配列を使ってマップを描写してみましょう。

' ループ用変数
VAR G_I = 0, G=J = 0

' 描写カウント
VAR G_WALL_DRAW_COUNT = 0
VAR G_JAR_DRAW_COUNT = 0
VAR G_GOAL_DRAW_COUNT = 0

' マップの縦横サイズ
VAR G_BGW = 9
VAR G_BGH = 9

' マップ描写
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 MAPDATA[CP]
      ' 1(壁)のとき
      WHEN 1:
        ' 壁スプライトを設置
        SPOFS 51+G_WALL_DRAW_COUNT, (G_I * #CS) +STAGE_ORIGIN_X ,(G_J * #CS) +STAGE_ORIGIN_Y
        ' 次のスプライト管理番号を使うためにカウント
        INC G_WALL_DRAW_COUNT
      ' 2(壺)のとき
      WHEN 2:
        ' 壺スプライトを設置
        SPOFS 1+G_JAR_DRAW_COUNT, (G_I * #CS) +STAGE_ORIGIN_X ,(G_J * #CS) +STAGE_ORIGIN_Y
        ' 壺は動かすので座標を記憶しておく
        SPVAR 1+G_JAR_DRAW_COUNT, "X", (G_I * #CS) +STAGE_ORIGIN_X
        SPVAR 1+G_JAR_DRAW_COUNT, "Y", (G_J * #CS) +STAGE_ORIGIN_Y
        ' 次のスプライト管理番号を使うためにカウント
        INC G_JAR_DRAW_COUNT
      ' 2(壺)のとき
      WHEN 3:
        ' ゴールスプライトを設置
        SPOFS 11+G_JAR_DRAW_COUNT, (G_I * #CS) +STAGE_ORIGIN_X ,(G_J * #CS) +STAGE_ORIGIN_Y
        ' 次のスプライト管理番号を使うためにカウント
        INC G_GOAL_DRAW_COUNT
    ENDCASE
  NEXT
NEXT

プチコン4 出力結果その1

2次元マップの描写は入れ子ループを使うのでG_IのほかにG_Jという変数を増やしました。

これは後に勉強するテキストスクリーン(地面マップ描写)でも使うので今のうちに定義しておきましょう。

次に、DRAW_COUNT系の変数を用意したのは該当する値になったとき、
それぞれのスプライトの座標を変更するのですがカウントをしておかないと、
同じスプライトが動くだけになってしまいます。

なので1つ描写したらカウントしておいて、
次のスプライト管理番号を指定するようにするための変数を用意しました。

これを繰り返すとマップ配列と同じ場所に該当のスプライトが表示されます。

最後にプレイヤーの初期座標を設定してしまえばもうほとんどゲームとしてできあがりですね。

プレイヤーの座標を変えないと壁にぶつかった音がずっと鳴り響きます(笑)

問題点があるとすれば、使用していないスプライトが左上に表示されてしまっているということです。

これの対処法としてはいくつかあって、今全てのスプライトをSPSETしていますが
マップ描写の時にSPSETをしてしまえば無駄なスプライトの描写を減らすことが出来ます。

ステージが変わるときはまた0からのカウントになるので、
処理的にはすべてのスプライトを最初にSPSETをするよりは
マップに使うそれぞれのスプライト数を変数で決めておいてステージごとに適用するといった感じでしょうか。

これは次回で軽く修正したいと思います。

後は壺をクリアマークの上にのせてクリア判定を取るだけですね。

次回は壺をクリアマークの上に載せたらクリアフラグが1つ経つようにして、
指定されたクリアフラグ数になればステージクリア!という感じの処理を作りましょう。

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

プレイヤー以外のスプライト初期化時、Y軸を-16にして画面外に出すように変更しています。

ついでに壁スプライトも100から200に変更しているのに気を付けてください。

それでは

' 画面クリア
ACLS

' チップサイズ定数
CONST #CS = 16

' ステージの左上座標
CONST STAGE_ORIGIN_X = 0 * #CS
CONST STAGE_ORIGIN_Y = 0 * #CS

' ゴールと木箱の個数
VAR OBJ = 5

' ループ用変数
VAR G_I = 0, G=J = 0

' 描写カウント
VAR G_WALL_DRAW_COUNT = 0
VAR G_JAR_DRAW_COUNT = 0
VAR G_GOAL_DRAW_COUNT = 0

' マップの縦横サイズ
VAR G_BGW = 9
VAR G_BGH = 9

' マップデータ
DIM MAPDATA[] = [\
  1,1,1,1,1,0,0,0,0,\
  1,0,0,0,1,0,0,0,0,\
  1,0,2,2,1,0,1,1,1,\
  1,0,2,0,1.0.1.3.1,\
  1,1,1,0,1,1,1,3,1,\
  0,1,1,0,0,0,0,3,1,\
  0,1,0,0,0,1,0,0,1,\
  0,1,0,0,0,1,1,1,1,\
  0,1,1,1,1,1,0,0,0\
]

' ボタンカウント用変数
VAR UP_BTN = 0, DOWN_BTN = 0, LEFT_BTN = 0, RIGHT_BTN = 00

' 衝突判定用変数
VAR HIT_OBJ = -1

' プレイヤースプライトの準備
SPSET 0, 721
' スプライト管理番号0番目に「X」という名前の変数を宣言し、0を代入する
SPVAR 0, "X", #CS
' スプライト管理番号0番目に「Y」という名前の変数を宣言し、0を代入する
SPVAR 0, "Y", #CS
' スプライトの向き情報を設定
SPVAR 0, "DIRECT", 0
' スプライト衝突判定の準備
SPCOL 0

' 壺スプライトの準備
FOR G_I = 1 TO OBJ
  SPSET G_I, 265
  SPVAR G_I, "X", G_I * #CS
  SPVAR G_I, "Y", 0
  SPCOL G_I
  SPOFS 0, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
NEXT

' 壺を置くための目印スプライト準備
FOR G_I = 11 TO OBJ+10
  SPSET G_I, 224
  SPVAR G_I, "X", G_I * #CS
  SPVAR G_I, "Y", 0
  SPCOL G_I
  SPOFS 0, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
NEXT

' 壁用スプライト
FOR G_I = 51 TO OBJ+200
  SPSET G_I, 1469
  SPVAR G_I, "X", G_I * #CS
  SPVAR G_I, "Y", 0
  SPCOL G_I
  SPOFS 0, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
NEXT

' マップ描写
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 MAPDATA[CP]
      ' 1(壁)のとき
      WHEN 1:
        ' 壁スプライトを設置
        SPOFS 51+G_WALL_DRAW_COUNT, (G_I * #CS) +STAGE_ORIGIN_X ,(G_J * #CS) +STAGE_ORIGIN_Y
        ' 次のスプライト管理番号を使うためにカウント
        INC G_WALL_DRAW_COUNT
      ' 2(壺)のとき
      WHEN 2:
        ' 壺スプライトを設置
        SPOFS 1+G_JAR_DRAW_COUNT, (G_I * #CS) +STAGE_ORIGIN_X ,(G_J * #CS) +STAGE_ORIGIN_Y
        ' 壺は動かすので座標を記憶しておく
        SPVAR 1+G_JAR_DRAW_COUNT, "X", (G_I * #CS) +STAGE_ORIGIN_X
        SPVAR 1+G_JAR_DRAW_COUNT, "Y", (G_J * #CS) +STAGE_ORIGIN_Y
        ' 次のスプライト管理番号を使うためにカウント
        INC G_JAR_DRAW_COUNT
      ' 2(壺)のとき
      WHEN 3:
        ' ゴールスプライトを設置
        SPOFS 11+G_JAR_DRAW_COUNT, (G_I * #CS) +STAGE_ORIGIN_X ,(G_J * #CS) +STAGE_ORIGIN_Y
        ' 次のスプライト管理番号を使うためにカウント
        INC G_GOAL_DRAW_COUNT
    ENDCASE
  NEXT
NEXT

' ループ開始
LOOP
  D_Controller
  VSYNC
ENDLOOP


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

  ' 上ボタン処理
  if (B AND 1 << #B_LUP) != 0 then
    D_BtnPressCount #B_LUP
  else
    if UP_BTN >= 1 then
      UP_BTN = 0
    endif
  endif

  ' 下ボタン処理
  if (B AND 1 << #B_LDOWN) != 0 then
    D_BtnPressCount #B_LDOWN
  else
    if DOWN_BTN >= 1 then
      DOWN_BTN = 0
    endif
  endif

  ' 左ボタン処理
  if (B AND 1 << #B_LLEFT) != 0 then
    D_BtnPressCount #B_LLEFT
  else
    if LEFT_BTN >= 1 then
      LEFT_BTN = 0
    endif
  endif

  ' 右ボタン処理
  if (B AND 1 << #B_LRIGHT) != 0 then
    D_BtnPressCount #B_LRIGHT
  else
    if RIGHT_BTN >= 1 then
      RIGHT_BTN = 0
    endif
  endif

  ' プレイヤー移動
  D_PLAYER_MOVE

end

' プレイヤー移動関数
'───────────────────────────────
DEF D_PLAYER_MOVE

  ' 仮移動用配列
  DIM ADD_POS[] = [0,0]

  ' ボタンカウントに応じて向きを変更と仮移動数値を設定
  IF UP_BTN == 1 THEN
    SPVAR 0, "DIRECT", #B_LUP
    ADD_POS[1] = -#CS
  ELSEIF DOWN_BTN == 1 THEN
    SPVAR 0, "DIRECT", #B_LDOWN
    ADD_POS[1] = #CS
  ELSEIF LEFT_BTN == 1 THEN
    SPVAR 0, "DIRECT", #B_LLEFT
    ADD_POS[0] = -#CS
  ELSEIF RIGHT_BTN == 1 THEN
    SPVAR 0, "DIRECT", #B_LRIGHT
    ADD_POS[0] = #CS
  ENDIF


  ' プレイヤーを仮移動させる
  SPOFS 0, SPVAR(0,"X")+ADD_POS[0], SPVAR(0,"Y")+ADD_POS[1]

  ' プレイヤーと壁の判定をする
  HIT_OBJ = SPHITSP(0,51,200)

  ' 壁の衝突判定をとる
  IF (HIT_OBJ != -1) THEN
    ' 壁に当たっていればプレイヤーの座標を元に戻す
    SPVAR 0, "X", SPVAR(0,"X")
    SPVAR 0, "Y", SPVAR(0,"Y")
  ELSE
    ' プレイヤーと壁があたっていなければ壺の衝突判定を取る
    HIT_OBJ = SPHITSP(0,1,10)

    ' 壺に当たっていて居るかチェック
    IF HIT_OBJ != -1 THEN
      ' 壺を仮移動させる
      SPOFS HIT_OBJ, SPVAR(HIT_OBJ,"X")+ADD_POS[0], SPVAR(HIT_OBJ,"Y")+ADD_POS[1]

      '壺を仮移動させた先は壁かどうか
      IF SPHITSP(HIT_OBJ,51,200) != -1 THEN
        ' BEEP 52
        ' ぶつかってたら動かせないはずなのでプレイヤーと壺の座標を戻す
        SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
        SPOFS HIT_OBJ, SPVAR(HIT_OBJ,"X"), SPVAR(HIT_OBJ,"Y")
      ELSE
        ' 壺を仮移動させた先は壺かどうか
        IF SPHITSP(HIT_OBJ,1,10) != -1 THEN
          ' BEEP 52
          ' ぶつかってたら動かせないはずなのでプレイヤーと壺の座標を戻す
          SPOFS 0, SPVAR(0,"X"), SPVAR(0,"Y")
          SPOFS HIT_OBJ, SPVAR(HIT_OBJ,"X"), SPVAR(HIT_OBJ,"Y")
        ELSE
          ' BEEP 100
          ' プレイヤーも壺も移動できたので座標変数の変更を確定
          SPVAR 0, "X", SPVAR(0,"X")+ADD_POS[0]
          SPVAR 0, "Y", SPVAR(0,"X")+ADD_POS[1]
          SPVAR HIT_OBJ, "X", SPVAR(HIT_OBJ,"X")+ADD_POS[0]
          SPVAR HIT_OBJ, "Y", SPVAR(HIT_OBJ,"Y")+ADD_POS[1]
        ENDIF
      ENDIF
    ELSE
      ' 壺に当たっていなければプレイヤーは普通に移動するので座標変数を確定
      SPVAR 0, "X", SPVAR(0,"X")+ADD_POS[0]
      SPVAR 0, "Y", SPVAR(0,"Y")+ADD_POS[1]
    ENDIF
  ENDIF
  ' プレイヤーの向きを初期化
  SPVAR 0, "DIRECT", 0
END


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

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

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

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

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