【プチコン4講座】障害物がある方がゲームは燃える!

プチコン4

プチコン4 シューティングゲーム 障害物

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

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

第7回「【プチコン4講座】SPVARを使って弾を複数撃てるようにしよう」にて

SPVARを覚えると一気に自由度が高まる感じがします。

弾を連射するようにできましたが、障害物も敵もいないので撃つ意味が感じられませんね。

敵を作るとAIを考えないといけないので一気に難易度が上がってしまうためまずは障害物から作って避けゲーから作っていきます。

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

  1. 障害物用のスプライトを用意しよう
  2. 障害物は常に右から左へ進んでいく
  3. 画面外に消えたスプライトは再利用しよう
  4. シューティングゲーム作り講座第8回まとめ

障害物用のスプライトを用意しよう

今回は敵を作る前に障害物を作るよ。敵の動きを考えないで良い分楽に実装できるよ。でもシューティングゲームには障害物は避ける対象として欠かせないね。

確かに敵だけ倒し続ければいいならその場で連打してればいいだけだもんな。流石にそれはつまんねぇや。

私も連打は得意じゃないから動いて避ける方がいいなぁ。障害物もスプライトを使うんだよね?

うん、基本的には動くモノはスプライトって認識でいいよ。今回は管理番号20~99を障害物にしようか。

数字にするとスゲェ多いな!でも画面の広さから考えたら妥当か?

最初は少なくして、スコアが上がるにつれて出現するスプライトを増やせば難易度も自動調整出来ていいんじゃないかな。

そうね。ずっと同じ難易度じゃ飽きてきそうだし刺激としてもいいわね。

それじゃあスプライト初期化を弄っていくよ。


' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ' スプライトを定義
  SPSET 0, 352 ' プレイヤー
  SPVAR 0, "X", 0
  SPVAR 0, "Y", 0
  SPCOL 0

  ' ループさせて弾を生成
  for G_I=10 to 14
    SPSET G_I, 1322,0 ' 弾は最初からHIDE状態
    SPOFS G_I, -16, -16
    SPVAR G_I, "X", 0
    SPVAR G_I, "Y", 0
    SPVAR G_I, "SHOT_FLAG", 0
    SPCOL G_I
  next

  ' ループさせて障害物を生成
  for G_I=20 to 99
    SPSET G_I, 262 ' 岩
    SPOFS G_I, -16, -16
    SPVAR G_I, "X", 0
    SPVAR G_I, "Y", 0
    SPVAR G_I, "IS_HIT", 0

    SPCOL G_I, 1
  next

end

弾と同じようにスプライトを生成しているよ。障害物はさっき言った通り20~99の範囲にしよう。

この「SPCOL」って何かしら?

今回のメインとなるのがこの「SPCOL」なんだ。スプライト同士がぶつかったっていう処理をするための準備だよ。

全スプライトに付ける感じなのか。

そうだね。お互いにぶつかる準備ができていなければ衝突は成立しないから基本的に衝突判定を取りたいスプライトはSPCOLをしておくよ。ちなみにカンマを付けて1を指定すると例えば巨大化したときとかも衝突判定が自動的に大きくなるんだ。指定しないと最初の大きさのままだよ。

でもこれだけじゃ衝突判定は出来ないんだ。あくまで準備だからね。とりあえず今は準備だけで大丈夫だよ。

障害物は常に右から左へ進んでいく

障害物は右から現れて左に進んでいくよ。ゲームの設定としてはプレイヤーが常にアクセルを踏んでいる状態で動き続けているんだけどね。カメラはプレイヤーを固定しているから動いてないように見えるって感じで作っていこう。

カメラさんも大変ね。

スクロール処理って基本的にはプレイヤーは動いてなくて背景を動かしたりするのが基本なんだ。端っこまで行ったら背景のスクロールを停止してプレイヤーを動かすようにすれば自然にみえるね。RPGとかではよくある処理だよ。今回作るSTGは常に動き続けてるからそこまでしなくてもいいけどね。

そういえば初期化時に弾と同じく左上の画面外に設置してるけど、どうやってうまく画面に配置するんだ?

それはね、プログラミングには欠かせない乱数を使うんだ

乱数?

面の数を自由に決められるサイコロだと思ってくれればいいかな。自分の指定した範囲の中の数字をランダムに返してくれるんだ。意外と偏りがあったりするから1回出た場所の近くは一定期間出したくないってときは条件分岐とか変数で管理してやらないとだめだから今回はシンプルに完全ランダムでいくよ。

毎回決まったところに出て来たら覚えゲーって言われちゃうもんね。ゲームではランダム性は確かに大事だわ。RPGとかで完全ランダムは嫌だけどね(笑)

そうだね。ゲーム自体はとっても面白いのに最強武器の入手がランダム出現の宝箱なのはちょっと……ってことで早速乱数を使って障害物をランダムに配置するよ。

' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ~~ 省略 ~~

  ' ループさせて障害物を生成
  for G_I=20 to 99
    SPSET G_I, 262 ' 岩

    VAR X = 400 + RND(400)
    VAR Y = RND(225)
    SPOFS G_I, X, Y
    SPVAR G_I, "X", X
    SPVAR G_I, "Y", Y
    SPVAR G_I, "IS_HIT", 0
    SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
    SPCOL G_I, 1
  next
end

RND(n)はnの部分に数値を入れると「0~n-1」の範囲で数字を返してくれるんだ。

このXに+400してるのとYのRND(225)はどんな意図があるの?

プチコン4のデフォルトの画面解像度は「横:400」の「縦:240」なんだ。背景を作るときに「横:25」「縦:15」になってたと思うけど実は1チップ分が16x16だからそれぞれ掛け算すると400×240になるってわけさ。

なーるほど!+400してるのは画面外の右に出してるってわけね!でも縦は画面の範囲内だから0~240ってわけか。あれでもYは225よね……?

スプライトに限らずプログラミングでは左上が原点になることが多いから240だと画面外が視点になっちゃって見えなくなっちゃうからー16してるんだ。RNDは最大値-1だから225にしてる感じだね。

うーん、数字がいっぱい出てくると混乱するな……とりあえず動かしてみていいか?

あ、待って。このままだと画面外にいるまま動かないから常に左に動かして画面左外に出たらまた再配置する仕組みを作るよ。

' 岩の移動処理
'───────────────────────────────
def D_ObstacleMove
  for G_I=20 to 99
    ' 完全に画面左外でなければ
    if SPVAR(G_I,"X") > -16 then
      ' 岩のX座標を加算
      SPVAR G_I, "X", SPVAR(G_I,"X") + 5
      ' 岩を移動させる
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
    else
      ' 画面左外に行ったら右画面外にランダム再配置する
      VAR X = 400 + RND(400)
      VAR Y = RND(225)
      SPVAR G_I, "X", X
      SPVAR G_I, "Y", Y
      SPVAR G_I, "IS_HIT", 0
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
    endif
  next
end

ランダム配置のところ、全く同じ処理してるわね。

良い所に気付いたねマユミちゃん。そう、こういうあからさまなところは命令化してあげよう!

' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ~~ 省略 ~~

  ' ループさせて障害物を生成
  for G_I=20 to 99
    SPSET G_I, 262 ' 岩
    D_ObstacleRndPosSettings G_I
    SPCOL G_I, 1
  next
end

' 岩の移動処理
'───────────────────────────────
def D_ObstacleMove
  for G_I=20 to 99
    ' 完全に画面左外でなければ
    if SPVAR(G_I,"X") > -16 then
      ' 岩のX座標を加算
      SPVAR G_I, "X", SPVAR(G_I,"X") - 5
      ' 岩を移動させる
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
    else
      ' 画面左外に行ったら右画面外にランダム再配置する
      D_ObstacleRndPosSettings G_I
    endif
  next
end

' 障害物再配置
'───────────────────────────────
def D_ObstacleRndPosSettings A_I
  VAR X = 400 + RND(400)
  VAR Y = RND(225)
  SPVAR A_I, "X", X
  SPVAR A_I, "Y", Y
  SPVAR A_I, "IS_HIT", 0
  SPOFS A_I, SPVAR(A_I,"X"), SPVAR(A_I,"Y")
end

引数にループのG_Iを渡せばSPVARも指定できる!うん!完璧ね!

それじゃあ実行してみようか。ちゃんとゲームループのところに障害物移動関数を設定するのを忘れないでね。

' ループ開始
loop
  D_Controller
  D_PlayerMove
  D_PlayerShotMove
  D_ObstacleMove
  ' 垂直同期
  VSYNC
endloop

プチコン4 シューティングゲーム 表示結果1

ちょ、ちょっとちょっと!多すぎ!多すぎ!

wwwwwwwwwwwwwwwwwwwwwwww

あちゃー 20-99はちょっと多すぎたか(笑) 99から30に変更してみよう。

プチコン4 シューティングゲーム 表示結果2

うん、これなら私にも避けられそうだわ!

ダメだwww腹いってぇwwww

ちょっと、いつまでツボにはいってんのよ!

毎回数値変更するの面倒くさいからここは配列にして管理しておく方がいいね。弾の方もついでにやっちゃおう。

' スプライト管理番号まとめ変数
DIM G_SpvManageNo[] = [0,10,14,20,30]

' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ' スプライトを定義
  var PID = G_SpvManageNo[0]
  SPSET PID, 352 ' プレイヤー
  SPVAR PID, "X", 0
  SPVAR PID, "Y", 0
  SPCOL PID

  ' ループさせて弾を生成
  for G_I = G_SpvManageNo[1] to G_SpvManageNo[2]
    SPSET G_I, 1322,0 ' 弾は最初からHIDE状態
    SPOFS G_I, -16, -16
    SPVAR G_I, "X", 0
    SPVAR G_I, "Y", 0
    SPVAR G_I, "SHOT_FLAG", 0
    SPCOL G_I
  next

  ' ループさせて障害物を生成
  for G_I = G_SpvManageNo[3] to G_SpvManageNo[4]
    SPSET G_I, 262 ' 岩
    D_ObstacleRndPosSettings G_I
    SPCOL G_I, 1
  next
end

これで配列の数値を弄るだけでいいね。障害物の数を増やすときはもう一度SPSETするか初期化の時点では99にしておくかどっちかだね。処理的には後者の方が軽いけどあまり気にしなくてもいいよ。

ループで直接値を指定しているところはショット関数にもあるからそこも変更しておいてくれるかな。ここには書かないけど最後のコードまとめは変更しておくよ。

プレイヤーと障害物がぶつかった判定を作ってみよう

最後に折角だから衝突判定を作るよ。一番最初に準備はしてるから後はループで衝突したかどうかを常に判定を監視するんだ。早速書いてみるよ。

' ループ開始
loop
  D_Controller
  D_PlayerMove
  D_PlayerColCheck
  D_PlayerShotMove
  D_ObstacleMove
  ' 垂直同期
  VSYNC
endloop


' プレイヤー衝突チェック
'───────────────────────────────
def D_PlayerColCheck
  var isHit = SPHITSP(G_SpvManageNo[0] to G_SpvManageNo[3], to G_SpvManageNo[4]);

  if isHit != -1 then
    BEEP 91, -300
  endif
end

SPHITSP……プレイヤーのIDと3と4ってことは20~30。つまりプレイヤーは20~30のスプライトに当たっているかどうかってこと?

マユミちゃん鋭い!もっと詳しく言うと当たったスプライトのIDがisHitに格納されるんだ。

条件が-1じゃないときは音を鳴らすってことは何も当たっていないときは-1ってことだな?

そう、プレイヤー側からしたらとにかく石のどれかに当たってたら衝突音を鳴らせばいいわけだからこんな短く書けるんだ。後はライフ制にしたときに衝突した処理のところでライフを減らしたりすると面白いかもね。

でもなんか音が連続して聞こえるのは気のせいかしら?

プチコン4 シューティングゲーム 表示結果3

ううん、連続して鳴ってるんだ。当たってたら鳴らすだから完全に通り過ぎるまでは鳴ったままになるね。最初に設定していた岩が持つ「IS_HIT」変数を思い出してほしいんだ。

ぶつかったときに変数を変更して1以上であれば音を鳴らさないようにするって感じか?

お、リキくん正解。

へへっ、ボタンカウントと似たような感じだもんな!

それじゃあ衝突判定を改良するよ。

' プレイヤー衝突チェック
'───────────────────────────────
def D_PlayerColCheck
  var isHit = SPHITSP(G_SpvManageNo[0], G_SpvManageNo[3], G_SpvManageNo[4])

  if isHit != -1 && SPVAR(isHit,"IS_HIT") == 0 then
    BEEP 91, -300
    SPVAR isHit, "IS_HIT", 1
  endif
end

IS_HITは再配置時に初期化してるから0に戻す処理を書く必要は無いよ。これで実行してみようか。

うん、いい感じだね。今日はココまでだよ。一気に詰め込みすぎて覚えられなかったら意味がないからね。

シューティングゲーム作り講座第8回まとめ

SPCOL, 管理番号, [判定縮小フラグ]

スプライトの判定を行うための準備命令です。

判定フラグは1にすると拡大や伸縮をすると一緒に判定も変更されます。何も指定しなければ0でサイズ変化は無しになります。
注意点としてはSPSETをしてから実行しなければいけないと言うことです。

SPHITSP(主管理番号, 判定したい管理番号開始値, 判定したい管理番号終了値)

主番号が判定したい開始番号~終了番号のスプライトIDのどれかに触れたら判定されます。判定されたらそのIDが返ってきます。何も当たっていなければ-1が返ります。

対象全てにSPCOLを設定しておかなければいけません。

RND(n)

0~n-1の乱数を返してくれます。

障害物と当たり判定を実装しました。

まだライフもゲームオーバーも用意していないのでぶつかった音だけですが、避けゲーとしては少し完成に近づきましたね。

後はショットのアタリ判定を作って岩を破壊できるようにしてみるのも面白いかもしれませんね。

次回はショットにも衝突判定を付けてみましょう。

それでは。

' ゲーム初期化
ACLS
' ループ用変数
var G_I
' ボタンカウント配列
DIM G_BtnPressCount[4]

' スプライト管理番号まとめ変数
DIM G_SpvManageNo[] = [0,10,14,20,30]

D_Initialize

' ループ開始
loop
  D_Controller
  D_PlayerMove
  D_PlayerColCheck
  D_PlayerShotMove
  D_ObstacleMove
  ' 垂直同期
  VSYNC
endloop


' ゲーム初期化
'───────────────────────────────
def D_Initialize

  ' マップ描写
  D_MapDraw

  ' スプライト初期化
  D_SpriteInitialize
end

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

  ' 押されているボタンの情報を表示
  if (B AND 1 << #B_LUP) != 0 then
    ' 上を押されたらY座標を減算
    SPVAR 0,"Y", SPVAR(0,"Y")-1
  elseif (B AND 1 << #B_LDOWN) != 0 then
    ' 下を押されたらY座標を加算
    SPVAR 0,"Y", SPVAR(0,"Y")+1
  endif
  if (B AND 1 << #B_LLEFT) != 0 then
    ' 左を押されたらX座標を減算
    SPVAR 0,"X", SPVAR(0,"X")-1
  elseif (B AND 1 << #B_LRIGHT) != 0 then
    ' 右を押されたらX座標を加算
    SPVAR 0,"X", SPVAR(0,"X")+1
  endif

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

' ボタンカウント関数
'───────────────────────────────
def D_BtnPressCount A_Button
  ' ボタンカウント分岐
  case A_Button
    ' 渡されたボタンが#B_RLEFTだったら
    when #B_RLEFT
      ' ボタンカウント配列の数が256未満かどうか調べる
      if G_BtnPressCount[2] < 256 then 
        ' 条件に一致したら+1する
        INC G_BtnPressCount[2]
      endif
  endcase
end


' スプライトの初期化
'───────────────────────────────
def D_SpriteInitialize
  ' スプライトを定義
  var PID = G_SpvManageNo[0]
  SPSET PID, 352 ' プレイヤー
  SPVAR PID, "X", 0
  SPVAR PID, "Y", 0
  SPCOL PID

  ' ループさせて弾を生成

  for G_I = G_SpvManageNo[1] to G_SpvManageNo[2]
    SPSET G_I, 1322,0 ' 弾は最初からHIDE状態
    SPOFS G_I, -16, -16
    SPVAR G_I, "X", 0
    SPVAR G_I, "Y", 0
    SPVAR G_I, "SHOT_FLAG", 0
    SPCOL G_I
  next

  ' ループさせて障害物を生成
  for G_I = G_SpvManageNo[3] to G_SpvManageNo[4]
    SPSET G_I, 262 ' 岩
    D_ObstacleRndPosSettings G_I
    SPCOL G_I, 1
  next
end

' スプライト(プレイヤー)の移動
'───────────────────────────────
def D_PlayerMove
  ' プレイヤーの移動
  SPOPS 0, SPVAR(0,"X"), SPVAR(0,"Y")
end

' マップの描写
'───────────────────────────────
' マップの塗りつぶし描写
def D_MapDraw
  ' 背景の色味を変更
  TCOLOR 0, &Hdddddd00
  ' 背景の塗りつぶし
  TFILL 0,0,0,24,14,CHR$(&HE8D8)
end

' プレイヤーショット処理
'───────────────────────────────
def D_PlayerShot
  for G_I=G_SpvManageNo[1], to G_SpvManageNo[2]
    ' 弾にショットフラグが立っていなければ
    if SPVAR(G_I,"SHOT_FLAG") == 0 then
      BEEP 10
      ' 表示フラグをONにする
      SPSHOW G_I
      ' 出現フラグをONにする
      SPVAR G_I,"SHOT_FLAG", 1
      ' 弾の座標をプレイヤーの前方に設定する
      SPVAR G_I, "X", SPVAR(0,"X") + 8
      SPVAR G_I, "Y", SPVAR(0,"Y") + 4
      ' ショット命令が1回動いたら無条件でループを抜ける
      break
  next
end

' プレイヤーの弾の移動処理
'───────────────────────────────
def D_PlayerShotMove
  for G_I=G_SpvManageNo[1], to G_SpvManageNo[2]
    ' 弾にショットフラグが立っていれば
    if SPVAR(G_I,"SHOT_FLAG") == 1 then
      ' 弾のX座標を加算
      SPVAR G_I, "X", SPVAR(G_I,"X") + 8
      ' 弾を移動させる
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
      ' 弾の座標が400を越えたら
      if G_I, SPVAR(G_I,"X") > 400 then
        ' 画面外に行くのでショットフラグを0にして非表示にする
        SPVAR G_I,"SHOT_FLAG",0
        SPHIDE G_I
      endif
    endif
  next
end

' 岩の移動処理
'───────────────────────────────
def D_ObstacleMove
  for G_I=G_SpvManageNo[3], to G_SpvManageNo[4]
    ' 完全に画面左外でなければ
    if SPVAR(G_I,"X") > -16 then
      ' 岩のX座標を加算
      SPVAR G_I, "X", SPVAR(G_I,"X") - 5
      ' 岩を移動させる
      SPOFS G_I, SPVAR(G_I,"X"), SPVAR(G_I,"Y")
    else
      ' 画面左外に行ったら右画面外にランダム再配置する
      D_ObstacleRndPosSettings G_I
    endif
  next
end

' 障害物再配置
'───────────────────────────────
def D_ObstacleRndPosSettings A_I
  VAR X = 400 + RND(400)
  VAR Y = RND(225)
  SPVAR A_I, "X", X
  SPVAR A_I, "Y", Y
  SPVAR A_I, "IS_HIT", 0
  SPOFS A_I, SPVAR(A_I,"X"), SPVAR(A_I,"Y")
end

' プレイヤー衝突チェック
'───────────────────────────────
def D_PlayerColCheck
  var isHit = SPHITSP(G_SpvManageNo[0], G_SpvManageNo[3], G_SpvManageNo[4])

  if isHit != -1 && SPVAR(isHit,"IS_HIT") == 0 then
    BEEP 91, -300
    SPVAR isHit, "IS_HIT", 1
  endif
end